diff --git a/.dockerignore b/.dockerignore index add07b4bf7..d89a9d83e2 100644 --- a/.dockerignore +++ b/.dockerignore @@ -8,4 +8,4 @@ crates/collab/static/styles.css vendor/bin assets/themes/*.json assets/themes/internal/*.json -assets/themes/experiments/*.json +assets/themes/staff/*.json diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 1302b197bd..6826566e71 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,6 +1,6 @@ ## Description of feature or change -## Link to related issues from zed or insiders +## Link to related issues from zed or community ## Before Merging diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fce5717ca9..5da8c8945e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,7 +4,7 @@ on: push: branches: - main - - "v*" + - "v[0-9]+.[0-9]+.x" tags: - "v*" pull_request: @@ -42,6 +42,9 @@ jobs: clean: false submodules: 'recursive' + - name: Run check + run: cargo check --workspace + - name: Run tests run: cargo test --workspace --no-fail-fast diff --git a/.github/workflows/release_actions.yml b/.github/workflows/release_actions.yml index a5949127f5..4a9d777769 100644 --- a/.github/workflows/release_actions.yml +++ b/.github/workflows/release_actions.yml @@ -13,23 +13,14 @@ jobs: webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }} content: | 📣 Zed ${{ github.event.release.tag_name }} was just released! - + Restart your Zed or head to https://zed.dev/releases/latest to grab it. - + ```md # Changelog - + ${{ github.event.release.body }} ``` - discourse_release: - runs-on: ubuntu-latest - steps: - - name: Install Node - uses: actions/setup-node@v2 - if: ${{ ! github.event.release.prerelease }} - with: - node-version: '16' - - run: script/discourse_release ${{ secrets.DISCOURSE_RELEASES_API_KEY }} ${{ github.event.release.tag_name }} ${{ github.event.release.body }} mixpanel_release: runs-on: ubuntu-latest steps: @@ -40,7 +31,7 @@ jobs: architecture: "x64" cache: "pip" - run: pip install -r script/mixpanel_release/requirements.txt - - run: > + - run: > python script/mixpanel_release/main.py ${{ github.event.release.tag_name }} ${{ secrets.MIXPANEL_PROJECT_ID }} diff --git a/.gitignore b/.gitignore index 8bca2eafac..5a4d2ff25e 100644 --- a/.gitignore +++ b/.gitignore @@ -7,9 +7,8 @@ /crates/collab/static/styles.css /vendor/bin /assets/themes/*.json -/assets/themes/Internal/*.json -/assets/themes/Experiments/*.json -/assets/licenses.md +/assets/*licenses.md +/assets/themes/staff/*.json **/venv .build Packages diff --git a/Cargo.lock b/Cargo.lock index f08a13902e..e8410b25f0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -259,6 +259,21 @@ dependencies = [ "futures-lite", ] +[[package]] +name = "async-global-executor" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1b6f5d7df27bd294849f8eec66ecfc63d11814df7a4f5d74168a2394467b776" +dependencies = [ + "async-channel", + "async-executor", + "async-io", + "async-lock", + "blocking", + "futures-lite", + "once_cell", +] + [[package]] name = "async-io" version = "1.12.0" @@ -350,6 +365,32 @@ dependencies = [ "syn", ] +[[package]] +name = "async-std" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62565bb4402e926b29953c785397c6dc0391b7b446e45008b0049eb43cec6f5d" +dependencies = [ + "async-channel", + "async-global-executor", + "async-io", + "async-lock", + "crossbeam-utils 0.8.14", + "futures-channel", + "futures-core", + "futures-io", + "futures-lite", + "gloo-timers", + "kv-log-macro", + "log", + "memchr", + "once_cell", + "pin-project-lite 0.2.9", + "pin-utils", + "slab", + "wasm-bindgen-futures", +] + [[package]] name = "async-stream" version = "0.3.3" @@ -371,6 +412,20 @@ dependencies = [ "syn", ] +[[package]] +name = "async-tar" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c49359998a76e32ef6e870dbc079ebad8f1e53e8441c5dd39d27b44493fe331" +dependencies = [ + "async-std", + "filetime", + "libc", + "pin-project", + "redox_syscall", + "xattr", +] + [[package]] name = "async-task" version = "4.0.3" @@ -828,6 +883,7 @@ dependencies = [ "media", "postage", "project", + "settings", "util", ] @@ -1132,7 +1188,7 @@ dependencies = [ [[package]] name = "collab" -version = "0.5.3" +version = "0.5.4" dependencies = [ "anyhow", "async-tungstenite", @@ -1196,6 +1252,7 @@ name = "collab_ui" version = "0.1.0" dependencies = [ "anyhow", + "auto_update", "call", "client", "clock", @@ -1275,6 +1332,7 @@ source = "git+https://github.com/servo/core-foundation-rs?rev=079665882507dd5e2f dependencies = [ "core-foundation-sys", "libc", + "uuid 0.5.1", ] [[package]] @@ -1899,6 +1957,7 @@ dependencies = [ "tree-sitter-html", "tree-sitter-javascript", "tree-sitter-rust", + "tree-sitter-typescript 0.20.2", "unindent", "util", "workspace", @@ -2078,6 +2137,18 @@ dependencies = [ "workspace", ] +[[package]] +name = "filetime" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e884668cd0c7480504233e951174ddc3b382f7c2666e3b7310b5c4e7b0c37f9" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "redox_syscall", + "windows-sys 0.42.0", +] + [[package]] name = "fixedbitset" version = "0.4.2" @@ -2526,6 +2597,18 @@ dependencies = [ "regex", ] +[[package]] +name = "gloo-timers" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + [[package]] name = "go_to_line" version = "0.1.0" @@ -2591,6 +2674,7 @@ dependencies = [ "tiny-skia", "usvg", "util", + "uuid 1.2.2", "waker-fn", ] @@ -3141,6 +3225,15 @@ dependencies = [ "arrayvec 0.7.2", ] +[[package]] +name = "kv-log-macro" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" +dependencies = [ + "log", +] + [[package]] name = "language" version = "0.1.0" @@ -3158,6 +3251,7 @@ dependencies = [ "fuzzy", "git", "gpui", + "indoc", "lazy_static", "log", "lsp", @@ -3180,10 +3274,12 @@ dependencies = [ "tree-sitter-html", "tree-sitter-javascript", "tree-sitter-json 0.19.0", + "tree-sitter-markdown", "tree-sitter-python", "tree-sitter-ruby", "tree-sitter-rust", - "tree-sitter-typescript", + "tree-sitter-typescript 0.20.1", + "unicase", "unindent", "util", ] @@ -6012,6 +6108,7 @@ dependencies = [ "parking_lot 0.11.2", "smol", "thread_local", + "uuid 1.2.2", ] [[package]] @@ -6461,6 +6558,7 @@ dependencies = [ "settings", "smol", "theme", + "util", "workspace", ] @@ -6907,7 +7005,7 @@ dependencies = [ [[package]] name = "tree-sitter" version = "0.20.9" -source = "git+https://github.com/tree-sitter/tree-sitter?rev=36b5b6c89e55ad1a502f8b3234bb3e12ec83a5da#36b5b6c89e55ad1a502f8b3234bb3e12ec83a5da" +source = "git+https://github.com/tree-sitter/tree-sitter?rev=c51896d32dcc11a38e41f36e3deb1a6a9c4f4b14#c51896d32dcc11a38e41f36e3deb1a6a9c4f4b14" dependencies = [ "cc", "regex", @@ -7009,6 +7107,16 @@ dependencies = [ "tree-sitter", ] +[[package]] +name = "tree-sitter-lua" +version = "0.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d489873fd1a2fa6d5f04930bfc5c081c96f0c038c1437104518b5b842c69b282" +dependencies = [ + "cc", + "tree-sitter", +] + [[package]] name = "tree-sitter-markdown" version = "0.0.1" @@ -7085,6 +7193,24 @@ dependencies = [ "tree-sitter", ] +[[package]] +name = "tree-sitter-typescript" +version = "0.20.2" +source = "git+https://github.com/tree-sitter/tree-sitter-typescript?rev=5d20856f34315b068c41edaee2ac8a100081d259#5d20856f34315b068c41edaee2ac8a100081d259" +dependencies = [ + "cc", + "tree-sitter", +] + +[[package]] +name = "tree-sitter-yaml" +version = "0.0.1" +source = "git+https://github.com/zed-industries/tree-sitter-yaml?rev=9050a4a4a847ed29e25485b1292a36eab8ae3492#9050a4a4a847ed29e25485b1292a36eab8ae3492" +dependencies = [ + "cc", + "tree-sitter", +] + [[package]] name = "try-lock" version = "0.2.3" @@ -7322,6 +7448,12 @@ dependencies = [ "tempdir", ] +[[package]] +name = "uuid" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcc7e3b898aa6f6c08e5295b6c89258d1331e9ac578cc992fb818759951bdc22" + [[package]] name = "uuid" version = "0.8.2" @@ -8167,6 +8299,7 @@ dependencies = [ "smallvec", "theme", "util", + "uuid 1.2.2", ] [[package]] @@ -8179,6 +8312,15 @@ dependencies = [ "winapi-build", ] +[[package]] +name = "xattr" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d1526bbe5aaeb5eb06885f4d987bcdfa5e23187055de9b83fe00156a821fabc" +dependencies = [ + "libc", +] + [[package]] name = "xml-rs" version = "0.8.4" @@ -8214,13 +8356,14 @@ checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" [[package]] name = "zed" -version = "0.71.0" +version = "0.75.0" dependencies = [ "activity_indicator", "anyhow", "assets", "async-compression", "async-recursion 0.3.2", + "async-tar", "async-trait", "auto_update", "backtrace", @@ -8298,6 +8441,7 @@ dependencies = [ "tree-sitter-go", "tree-sitter-html", "tree-sitter-json 0.20.0", + "tree-sitter-lua", "tree-sitter-markdown", "tree-sitter-python", "tree-sitter-racket", @@ -8305,10 +8449,13 @@ dependencies = [ "tree-sitter-rust", "tree-sitter-scheme", "tree-sitter-toml", - "tree-sitter-typescript", + "tree-sitter-typescript 0.20.2", + "tree-sitter-yaml", "unindent", "url", + "urlencoding", "util", + "uuid 1.2.2", "vim", "workspace", ] diff --git a/Cargo.toml b/Cargo.toml index 77469c0623..c74a76ccce 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -69,7 +69,7 @@ serde_json = { version = "1.0", features = ["preserve_order", "raw_value"] } rand = { version = "0.8" } [patch.crates-io] -tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "36b5b6c89e55ad1a502f8b3234bb3e12ec83a5da" } +tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "c51896d32dcc11a38e41f36e3deb1a6a9c4f4b14" } async-task = { git = "https://github.com/zed-industries/async-task", rev = "341b57d6de98cdfd7b418567b8de2022ca993a6e" } # TODO - Remove when a version is released with this PR: https://github.com/servo/core-foundation-rs/pull/457 @@ -84,5 +84,3 @@ split-debuginfo = "unpacked" [profile.release] debug = true - - diff --git a/Dockerfile b/Dockerfile index 5a6279a95e..d3170696c5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,6 +5,7 @@ WORKDIR app COPY . . # Compile collab server +ARG CARGO_PROFILE_RELEASE_PANIC=abort RUN --mount=type=cache,target=./script/node_modules \ --mount=type=cache,target=/usr/local/cargo/registry \ --mount=type=cache,target=./target \ diff --git a/README.md b/README.md index 24614e97c2..b9c12abea2 100644 --- a/README.md +++ b/README.md @@ -49,30 +49,14 @@ script/zed-with-local-servers --release If you trigger `cmd-alt-i`, Zed will copy a JSON representation of the current window contents to the clipboard. You can paste this in a tool like [DJSON](https://chrome.google.com/webstore/detail/djson-json-viewer-formatt/chaeijjekipecdajnijdldjjipaegdjc?hl=en) to navigate the state of on-screen elements in a structured way. -### Staff Only Features +### Licensing -Many features (e.g. the terminal) take significant time and effort before they are polished enough to be released to even Alpha users. But Zed's team workflow relies on fast, daily PRs and there can be large merge conflicts for feature branchs that diverge for a few days. To bridge this gap, there is a `staff_mode` field in the Settings that staff can set to enable these unpolished or incomplete features. Note that this setting isn't leaked via autocompletion, but there is no mechanism to stop users from setting this anyway. As initilization of Zed components is only done once, on startup, setting `staff_mode` may require a restart to take effect. You can set staff only key bindings in the `assets/keymaps/internal.json` file, and add staff only themes in the `styles/src/themes/internal` directory +We use [`cargo-about`](https://github.com/EmbarkStudios/cargo-about) to automatically comply with open source licenses. If CI is failing, check the following: -### Experimental Features +- Is it showing a `no license specified` error for a crate you've created? If so, add `publish = false` under `[package]` in your crate's Cargo.toml. +- Is the error `failed to satisfy license requirements` for a dependency? If so, first determine what license the project has and whether this system is sufficient to comply with this license's requirements. If you're unsure, ask a lawyer. Once you've verified that this system is acceptable add the license's SPDX identifier to the `accepted` array in `script/licenses/zed-licenses.toml`. +- Is `cargo-about` unable to find the license for a dependency? If so, add a clarification field at the end of `script/licenses/zed-licenses.toml`, as specified in the [cargo-about book](https://embarkstudios.github.io/cargo-about/cli/generate/config.html#crate-configuration). -A user facing feature flag can be added to Zed by: - -* Adding a setting to the crates/settings/src/settings.rs FeatureFlags struct. Use a boolean for a simple on/off, or use a struct to experiment with different configuration options. -* If the feature needs keybindings, add a file to the `assets/keymaps/experiments/` folder, then update the `FeatureFlags::keymap_files()` method to check for your feature's flag and add it's keybindings's path to the method's list. -* If you want to add an experimental theme, add it to the `styles/src/themes/experiments` folder - -The Settings global should be initialized with the user's feature flags by the time the feature's `init(cx)` equivalent is called. - -To promote an experimental feature to a full feature: - -* If this is an experimental theme, move the theme file from the `styles/src/themes/experiments` folder to the `styles/src/themes/` folder -* Take the features settings (if any) and add them under a new variable in the Settings struct. Don't forget to add a `merge()` call in `set_user_settings()`! -* Take the feature's keybindings and add them to the default.json (or equivalent) file -* Remove the file from the `FeatureFlags::keymap_files()` method -* Remove the conditional in the feature's `init(cx)` equivalent. - - -That's it 😸 ### Wasm Plugins diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index a0f437cf91..e8f055cb7d 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -38,7 +38,7 @@ "cmd-n": "workspace::NewFile", "cmd-shift-n": "workspace::NewWindow", "cmd-o": "workspace::Open", - "alt-cmd-o": "recent_projects::Toggle", + "alt-cmd-o": "projects::OpenRecent", "ctrl-`": "workspace::NewTerminal" } }, @@ -164,6 +164,7 @@ "bindings": { "enter": "editor::Newline", "cmd-enter": "editor::NewlineBelow", + "alt-z": "editor::ToggleSoftWrap", "cmd-f": [ "buffer_search::Deploy", { @@ -227,7 +228,12 @@ "replace_newest": true } ], - "cmd-/": "editor::ToggleComments", + "cmd-/": [ + "editor::ToggleComments", + { + "advance_downwards": false + } + ], "alt-up": "editor::SelectLargerSyntaxNode", "alt-down": "editor::SelectSmallerSyntaxNode", "cmd-u": "editor::UndoSelection", @@ -433,8 +439,7 @@ { "context": "Workspace", "bindings": { - "shift-escape": "dock::FocusDock", - "cmd-shift-b": "workspace::ToggleRightSidebar" + "shift-escape": "dock::FocusDock" } }, { @@ -445,15 +450,16 @@ } }, { - "context": "Dock", + "context": "Pane", "bindings": { - "shift-escape": "dock::HideDock" + "cmd-escape": "dock::AddTabToDock" } }, { - "context": "Pane", + "context": "Dock", "bindings": { - "cmd-escape": "dock::MoveActiveItemToDock" + "shift-escape": "dock::HideDock", + "cmd-escape": "dock::RemoveTabFromDock" } }, { diff --git a/assets/keymaps/internal.json b/assets/keymaps/internal.json deleted file mode 100644 index 0637a088a0..0000000000 --- a/assets/keymaps/internal.json +++ /dev/null @@ -1 +0,0 @@ -[] \ No newline at end of file diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 12873a3e4e..824fb63c0f 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -315,7 +315,9 @@ { "context": "Editor && VimWaiting", "bindings": { - "*": "gpui::KeyPressed" + "tab": "vim::Tab", + "enter": "vim::Enter", + "escape": "editor::Cancel" } } ] \ No newline at end of file diff --git a/assets/settings/default.json b/assets/settings/default.json index 1ef2ac8a16..f6fb61d65c 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -20,6 +20,8 @@ // Whether to pop the completions menu while typing in an editor without // explicitly requesting it. "show_completions_on_input": true, + // Whether the screen sharing icon is showed in the os status bar. + "show_call_status_icon": true, // Whether new projects should start out 'online'. Online projects // appear in the contacts panel under your name, so that your contacts // can see which projects you are working on. Regardless of this @@ -88,6 +90,8 @@ // Send anonymized usage data like what languages you're using Zed with. "metrics": true }, + // Automatically update Zed + "auto_update": true, // Git gutter behavior configuration. "git": { // Control whether the git gutter is shown. May take 2 values: @@ -219,6 +223,9 @@ }, "TSX": { "tab_size": 2 + }, + "YAML": { + "tab_size": 2 } }, // LSP Specific settings. diff --git a/crates/activity_indicator/src/activity_indicator.rs b/crates/activity_indicator/src/activity_indicator.rs index 8b9eb4b040..f3a6f7328a 100644 --- a/crates/activity_indicator/src/activity_indicator.rs +++ b/crates/activity_indicator/src/activity_indicator.rs @@ -252,7 +252,11 @@ impl ActivityIndicator { "Installing Zed update…".to_string(), None, ), - AutoUpdateStatus::Updated => (None, "Restart to update Zed".to_string(), None), + AutoUpdateStatus::Updated => ( + None, + "Click to restart and update Zed".to_string(), + Some(Box::new(workspace::Restart)), + ), AutoUpdateStatus::Errored => ( Some(WARNING_ICON), "Auto update failed".to_string(), diff --git a/crates/auto_update/src/auto_update.rs b/crates/auto_update/src/auto_update.rs index d3fcc36c2f..4272d7b1af 100644 --- a/crates/auto_update/src/auto_update.rs +++ b/crates/auto_update/src/auto_update.rs @@ -2,15 +2,16 @@ mod update_notification; use anyhow::{anyhow, Context, Result}; use client::{http::HttpClient, ZED_SECRET_CLIENT_TOKEN}; +use client::{ZED_APP_PATH, ZED_APP_VERSION}; use db::kvp::KEY_VALUE_STORE; use gpui::{ actions, platform::AppVersion, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task, WeakViewHandle, }; -use lazy_static::lazy_static; use serde::Deserialize; +use settings::Settings; use smol::{fs::File, io::AsyncReadExt, process::Command}; -use std::{env, ffi::OsString, path::PathBuf, sync::Arc, time::Duration}; +use std::{ffi::OsString, sync::Arc, time::Duration}; use update_notification::UpdateNotification; use util::channel::ReleaseChannel; use workspace::Workspace; @@ -18,13 +19,6 @@ use workspace::Workspace; const SHOULD_SHOW_UPDATE_NOTIFICATION_KEY: &str = "auto-updater-should-show-updated-notification"; const POLL_INTERVAL: Duration = Duration::from_secs(60 * 60); -lazy_static! { - pub static ref ZED_APP_VERSION: Option = env::var("ZED_APP_VERSION") - .ok() - .and_then(|v| v.parse().ok()); - pub static ref ZED_APP_PATH: Option = env::var("ZED_APP_PATH").ok().map(PathBuf::from); -} - actions!(auto_update, [Check, DismissErrorMessage, ViewReleaseNotes]); #[derive(Clone, Copy, PartialEq, Eq)] @@ -60,7 +54,23 @@ pub fn init(http_client: Arc, server_url: String, cx: &mut Mutab let server_url = server_url; let auto_updater = cx.add_model(|cx| { let updater = AutoUpdater::new(version, http_client, server_url.clone()); - updater.start_polling(cx).detach(); + + let mut update_subscription = cx + .global::() + .auto_update + .then(|| updater.start_polling(cx)); + + cx.observe_global::(move |updater, cx| { + if cx.global::().auto_update { + if update_subscription.is_none() { + *(&mut update_subscription) = Some(updater.start_polling(cx)) + } + } else { + (&mut update_subscription).take(); + } + }) + .detach(); + updater }); cx.set_global(Some(auto_updater)); diff --git a/crates/call/Cargo.toml b/crates/call/Cargo.toml index 156925fb72..54546adb55 100644 --- a/crates/call/Cargo.toml +++ b/crates/call/Cargo.toml @@ -28,6 +28,7 @@ fs = { path = "../fs" } language = { path = "../language" } media = { path = "../media" } project = { path = "../project" } +settings = { path = "../settings" } util = { path = "../util" } anyhow = "1.0.38" diff --git a/crates/call/src/call.rs b/crates/call/src/call.rs index c63b2e0f5b..64584e6140 100644 --- a/crates/call/src/call.rs +++ b/crates/call/src/call.rs @@ -1,18 +1,22 @@ pub mod participant; pub mod room; +use std::sync::Arc; + use anyhow::{anyhow, Result}; use client::{proto, Client, TypedEnvelope, User, UserStore}; use collections::HashSet; +use futures::{future::Shared, FutureExt}; +use postage::watch; + use gpui::{ AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Subscription, Task, WeakModelHandle, }; -pub use participant::ParticipantLocation; -use postage::watch; use project::Project; + +pub use participant::ParticipantLocation; pub use room::Room; -use std::sync::Arc; pub fn init(client: Arc, user_store: ModelHandle, cx: &mut MutableAppContext) { let active_call = cx.add_model(|cx| ActiveCall::new(client, user_store, cx)); @@ -27,8 +31,10 @@ pub struct IncomingCall { pub initial_project: Option, } +/// Singleton global maintaining the user's participation in a room across workspaces. pub struct ActiveCall { room: Option<(ModelHandle, Vec)>, + pending_room_creation: Option, Arc>>>>, location: Option>, pending_invites: HashSet, incoming_call: ( @@ -52,6 +58,7 @@ impl ActiveCall { ) -> Self { Self { room: None, + pending_room_creation: None, location: None, pending_invites: Default::default(), incoming_call: watch::channel(), @@ -120,45 +127,74 @@ impl ActiveCall { initial_project: Option>, cx: &mut ModelContext, ) -> Task> { - let client = self.client.clone(); - let user_store = self.user_store.clone(); if !self.pending_invites.insert(called_user_id) { return Task::ready(Err(anyhow!("user was already invited"))); } - cx.notify(); - cx.spawn(|this, mut cx| async move { - let invite = async { - if let Some(room) = this.read_with(&cx, |this, _| this.room().cloned()) { - let initial_project_id = if let Some(initial_project) = initial_project { - Some( - room.update(&mut cx, |room, cx| { - room.share_project(initial_project, cx) - }) + + let room = if let Some(room) = self.room().cloned() { + Some(Task::ready(Ok(room)).shared()) + } else { + self.pending_room_creation.clone() + }; + + let invite = if let Some(room) = room { + cx.spawn_weak(|_, mut cx| async move { + let room = room.await.map_err(|err| anyhow!("{:?}", err))?; + + let initial_project_id = if let Some(initial_project) = initial_project { + Some( + room.update(&mut cx, |room, cx| room.share_project(initial_project, cx)) .await?, - ) - } else { - None - }; - - room.update(&mut cx, |room, cx| { - room.call(called_user_id, initial_project_id, cx) - }) - .await?; + ) } else { - let room = cx - .update(|cx| { - Room::create(called_user_id, initial_project, client, user_store, cx) - }) - .await?; - - this.update(&mut cx, |this, cx| this.set_room(Some(room), cx)) - .await?; + None }; - Ok(()) - }; + room.update(&mut cx, |room, cx| { + room.call(called_user_id, initial_project_id, cx) + }) + .await?; + anyhow::Ok(()) + }) + } else { + let client = self.client.clone(); + let user_store = self.user_store.clone(); + let room = cx + .spawn(|this, mut cx| async move { + let create_room = async { + let room = cx + .update(|cx| { + Room::create( + called_user_id, + initial_project, + client, + user_store, + cx, + ) + }) + .await?; + + this.update(&mut cx, |this, cx| this.set_room(Some(room.clone()), cx)) + .await?; + + anyhow::Ok(room) + }; + + let room = create_room.await; + this.update(&mut cx, |this, _| this.pending_room_creation = None); + room.map_err(Arc::new) + }) + .shared(); + self.pending_room_creation = Some(room.clone()); + cx.foreground().spawn(async move { + room.await.map_err(|err| anyhow!("{:?}", err))?; + anyhow::Ok(()) + }) + }; + + cx.spawn(|this, mut cx| async move { let result = invite.await; this.update(&mut cx, |this, cx| { this.pending_invites.remove(&called_user_id); diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 4d129fab2e..eba58304d7 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -15,7 +15,7 @@ use futures::{future::LocalBoxFuture, AsyncReadExt, FutureExt, SinkExt, StreamEx use gpui::{ actions, serde_json::{self, Value}, - AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, AnyWeakViewHandle, AppContext, + AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, AnyWeakViewHandle, AppContext, AppVersion, AsyncAppContext, Entity, ModelHandle, MutableAppContext, Task, View, ViewContext, ViewHandle, }; use http::HttpClient; @@ -55,6 +55,11 @@ lazy_static! { pub static ref ADMIN_API_TOKEN: Option = std::env::var("ZED_ADMIN_API_TOKEN") .ok() .and_then(|s| if s.is_empty() { None } else { Some(s) }); + pub static ref ZED_APP_VERSION: Option = std::env::var("ZED_APP_VERSION") + .ok() + .and_then(|v| v.parse().ok()); + pub static ref ZED_APP_PATH: Option = + std::env::var("ZED_APP_PATH").ok().map(PathBuf::from); } pub const ZED_SECRET_CLIENT_TOKEN: &str = "618033988749894"; @@ -1319,6 +1324,10 @@ impl Client { pub fn metrics_id(&self) -> Option> { self.telemetry.metrics_id() } + + pub fn is_staff(&self) -> Option { + self.telemetry.is_staff() + } } impl WeakSubscriber { diff --git a/crates/client/src/http.rs b/crates/client/src/http.rs index 5139bb8d03..0757cebf3a 100644 --- a/crates/client/src/http.rs +++ b/crates/client/src/http.rs @@ -9,7 +9,7 @@ pub use isahc::{ Error, }; use smol::future::FutureExt; -use std::sync::Arc; +use std::{sync::Arc, time::Duration}; pub use url::Url; pub type Request = isahc::Request; @@ -41,7 +41,13 @@ pub trait HttpClient: Send + Sync { } pub fn client() -> Arc { - Arc::new(isahc::HttpClient::builder().build().unwrap()) + Arc::new( + isahc::HttpClient::builder() + .connect_timeout(Duration::from_secs(5)) + .low_speed_timeout(100, Duration::from_secs(5)) + .build() + .unwrap(), + ) } impl HttpClient for isahc::HttpClient { diff --git a/crates/client/src/telemetry.rs b/crates/client/src/telemetry.rs index 2aa33e6435..748eb48f7e 100644 --- a/crates/client/src/telemetry.rs +++ b/crates/client/src/telemetry.rs @@ -40,6 +40,7 @@ struct TelemetryState { next_event_id: usize, flush_task: Option>, log_file: Option, + is_staff: Option, } const MIXPANEL_EVENTS_URL: &'static str = "https://api.mixpanel.com/track"; @@ -125,6 +126,7 @@ impl Telemetry { flush_task: Default::default(), next_event_id: 0, log_file: None, + is_staff: None, }), }); @@ -202,6 +204,7 @@ impl Telemetry { let device_id = state.device_id.clone(); let metrics_id: Option> = metrics_id.map(|id| id.into()); state.metrics_id = metrics_id.clone(); + state.is_staff = Some(is_staff); drop(state); if let Some((token, device_id)) = MIXPANEL_TOKEN.as_ref().zip(device_id) { @@ -282,6 +285,10 @@ impl Telemetry { self.state.lock().metrics_id.clone() } + pub fn is_staff(self: &Arc) -> Option { + self.state.lock().is_staff + } + fn flush(self: &Arc) { let mut state = self.state.lock(); let mut events = mem::take(&mut state.queue); diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index 1201665571..01fd1773c4 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -7,7 +7,7 @@ use postage::{sink::Sink, watch}; use rpc::proto::{RequestMessage, UsersResponse}; use settings::Settings; use std::sync::{Arc, Weak}; -use util::TryFutureExt as _; +use util::{StaffMode, TryFutureExt as _}; #[derive(Default, Debug)] pub struct User { @@ -148,6 +148,19 @@ impl UserStore { cx.read(|cx| cx.global::().telemetry()), ); + cx.update(|cx| { + cx.update_default_global(|staff_mode: &mut StaffMode, _| { + if !staff_mode.0 { + *staff_mode = StaffMode( + info.as_ref() + .map(|info| info.staff) + .unwrap_or_default(), + ) + } + () + }); + }); + current_user_tx.send(user).await.ok(); } } diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index 456bcf6531..9301a1974a 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -3,7 +3,7 @@ authors = ["Nathan Sobo "] default-run = "collab" edition = "2021" name = "collab" -version = "0.5.3" +version = "0.5.4" publish = false [[bin]] diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index 63ea7fdd9e..af30073ab4 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -595,7 +595,16 @@ impl Database { .await } - pub async fn remove_contact(&self, requester_id: UserId, responder_id: UserId) -> Result<()> { + /// Returns a bool indicating whether the removed contact had originally accepted or not + /// + /// Deletes the contact identified by the requester and responder ids, and then returns + /// whether the deleted contact had originally accepted or was a pending contact request. + /// + /// # Arguments + /// + /// * `requester_id` - The user that initiates this request + /// * `responder_id` - The user that will be removed + pub async fn remove_contact(&self, requester_id: UserId, responder_id: UserId) -> Result { self.transaction(|tx| async move { let (id_a, id_b) = if responder_id < requester_id { (responder_id, requester_id) @@ -603,20 +612,18 @@ impl Database { (requester_id, responder_id) }; - let result = contact::Entity::delete_many() + let contact = contact::Entity::find() .filter( contact::Column::UserIdA .eq(id_a) .and(contact::Column::UserIdB.eq(id_b)), ) - .exec(&*tx) - .await?; + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("no such contact"))?; - if result.rows_affected == 1 { - Ok(()) - } else { - Err(anyhow!("no such contact"))? - } + contact::Entity::delete_by_id(contact.id).exec(&*tx).await?; + Ok(contact.accepted) }) .await } @@ -1586,12 +1593,8 @@ impl Database { .filter( Condition::all() .add( - room_participant::Column::CallingConnectionId - .eq(connection.id as i32), - ) - .add( - room_participant::Column::CallingConnectionServerId - .eq(connection.owner_id as i32), + room_participant::Column::CallingUserId + .eq(leaving_participant.user_id), ) .add(room_participant::Column::AnsweringConnectionId.is_null()), ) @@ -1917,7 +1920,9 @@ impl Database { }; if let Some(db_worktree) = db_worktree { - project.worktree_root_names.push(db_worktree.root_name); + if db_worktree.visible { + project.worktree_root_names.push(db_worktree.root_name); + } } } } diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 92d4935b23..32cce1e681 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -1961,23 +1961,31 @@ async fn remove_contact( let requester_id = session.user_id; let responder_id = UserId::from_proto(request.user_id); let db = session.db().await; - db.remove_contact(requester_id, responder_id).await?; + let contact_accepted = db.remove_contact(requester_id, responder_id).await?; let pool = session.connection_pool().await; // Update outgoing contact requests of requester let mut update = proto::UpdateContacts::default(); - update - .remove_outgoing_requests - .push(responder_id.to_proto()); + if contact_accepted { + update.remove_contacts.push(responder_id.to_proto()); + } else { + update + .remove_outgoing_requests + .push(responder_id.to_proto()); + } for connection_id in pool.user_connection_ids(requester_id) { session.peer.send(connection_id, update.clone())?; } // Update incoming contact requests of responder let mut update = proto::UpdateContacts::default(); - update - .remove_incoming_requests - .push(requester_id.to_proto()); + if contact_accepted { + update.remove_contacts.push(requester_id.to_proto()); + } else { + update + .remove_incoming_requests + .push(requester_id.to_proto()); + } for connection_id in pool.user_connection_ids(responder_id) { session.peer.send(connection_id, update.clone())?; } diff --git a/crates/collab/src/tests.rs b/crates/collab/src/tests.rs index 54e3c67f5d..178d31ba63 100644 --- a/crates/collab/src/tests.rs +++ b/crates/collab/src/tests.rs @@ -11,7 +11,7 @@ use client::{ EstablishConnectionError, UserStore, }; use collections::{HashMap, HashSet}; -use fs::{FakeFs, HomeDir}; +use fs::FakeFs; use futures::{channel::oneshot, StreamExt as _}; use gpui::{ executor::Deterministic, test::EmptyView, ModelHandle, Task, TestAppContext, ViewHandle, @@ -101,7 +101,6 @@ impl TestServer { async fn create_client(&mut self, cx: &mut TestAppContext, name: &str) -> TestClient { cx.update(|cx| { - cx.set_global(HomeDir(Path::new("/tmp/").to_path_buf())); cx.set_global(Settings::test(cx)); }); @@ -197,7 +196,7 @@ impl TestServer { languages: Arc::new(LanguageRegistry::new(Task::ready(()))), themes: ThemeRegistry::new((), cx.font_cache()), fs: fs.clone(), - build_window_options: Default::default, + build_window_options: |_, _, _| Default::default(), initialize_workspace: |_, _, _| unimplemented!(), dock_default_item_factory: |_, _| unimplemented!(), }); diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index 3f2a777f87..f2cb2eddbb 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -166,9 +166,67 @@ async fn test_basic_calls( } ); + // Call user C again from user A. + active_call_a + .update(cx_a, |call, cx| { + call.invite(client_c.user_id().unwrap(), None, cx) + }) + .await + .unwrap(); + + deterministic.run_until_parked(); + assert_eq!( + room_participants(&room_a, cx_a), + RoomParticipants { + remote: vec!["user_b".to_string()], + pending: vec!["user_c".to_string()] + } + ); + assert_eq!( + room_participants(&room_b, cx_b), + RoomParticipants { + remote: vec!["user_a".to_string()], + pending: vec!["user_c".to_string()] + } + ); + + // User C accepts the call. + let call_c = incoming_call_c.next().await.unwrap().unwrap(); + assert_eq!(call_c.calling_user.github_login, "user_a"); + active_call_c + .update(cx_c, |call, cx| call.accept_incoming(cx)) + .await + .unwrap(); + assert!(incoming_call_c.next().await.unwrap().is_none()); + let room_c = active_call_c.read_with(cx_c, |call, _| call.room().unwrap().clone()); + + deterministic.run_until_parked(); + assert_eq!( + room_participants(&room_a, cx_a), + RoomParticipants { + remote: vec!["user_b".to_string(), "user_c".to_string()], + pending: Default::default() + } + ); + assert_eq!( + room_participants(&room_b, cx_b), + RoomParticipants { + remote: vec!["user_a".to_string(), "user_c".to_string()], + pending: Default::default() + } + ); + assert_eq!( + room_participants(&room_c, cx_c), + RoomParticipants { + remote: vec!["user_a".to_string(), "user_b".to_string()], + pending: Default::default() + } + ); + // User A shares their screen let display = MacOSDisplay::new(); let events_b = active_call_events(cx_b); + let events_c = active_call_events(cx_c); active_call_a .update(cx_a, |call, cx| { call.room().unwrap().update(cx, |room, cx| { @@ -181,9 +239,10 @@ async fn test_basic_calls( deterministic.run_until_parked(); + // User B observes the remote screen sharing track. assert_eq!(events_b.borrow().len(), 1); - let event = events_b.borrow().first().unwrap().clone(); - if let call::room::Event::RemoteVideoTracksChanged { participant_id } = event { + let event_b = events_b.borrow().first().unwrap().clone(); + if let call::room::Event::RemoteVideoTracksChanged { participant_id } = event_b { assert_eq!(participant_id, client_a.peer_id().unwrap()); room_b.read_with(cx_b, |room, _| { assert_eq!( @@ -197,6 +256,23 @@ async fn test_basic_calls( panic!("unexpected event") } + // User C observes the remote screen sharing track. + assert_eq!(events_c.borrow().len(), 1); + let event_c = events_c.borrow().first().unwrap().clone(); + if let call::room::Event::RemoteVideoTracksChanged { participant_id } = event_c { + assert_eq!(participant_id, client_a.peer_id().unwrap()); + room_c.read_with(cx_c, |room, _| { + assert_eq!( + room.remote_participants()[&client_a.user_id().unwrap()] + .tracks + .len(), + 1 + ); + }); + } else { + panic!("unexpected event") + } + // User A leaves the room. active_call_a.update(cx_a, |call, cx| { call.hang_up(cx).unwrap(); @@ -213,18 +289,28 @@ async fn test_basic_calls( assert_eq!( room_participants(&room_b, cx_b), RoomParticipants { - remote: Default::default(), + remote: vec!["user_c".to_string()], + pending: Default::default() + } + ); + assert_eq!( + room_participants(&room_c, cx_c), + RoomParticipants { + remote: vec!["user_b".to_string()], pending: Default::default() } ); // User B gets disconnected from the LiveKit server, which causes them - // to automatically leave the room. + // to automatically leave the room. User C leaves the room as well because + // nobody else is in there. server .test_live_kit_server - .disconnect_client(client_b.peer_id().unwrap().to_string()) + .disconnect_client(client_b.user_id().unwrap().to_string()) .await; - active_call_b.update(cx_b, |call, _| assert!(call.room().is_none())); + deterministic.run_until_parked(); + active_call_b.read_with(cx_b, |call, _| assert!(call.room().is_none())); + active_call_c.read_with(cx_c, |call, _| assert!(call.room().is_none())); assert_eq!( room_participants(&room_a, cx_a), RoomParticipants { @@ -239,6 +325,141 @@ async fn test_basic_calls( pending: Default::default() } ); + assert_eq!( + room_participants(&room_c, cx_c), + RoomParticipants { + remote: Default::default(), + pending: Default::default() + } + ); +} + +#[gpui::test(iterations = 10)] +async fn test_calling_multiple_users_simultaneously( + deterministic: Arc, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, + cx_c: &mut TestAppContext, + cx_d: &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; + let client_d = server.create_client(cx_d, "user_d").await; + server + .make_contacts(&mut [ + (&client_a, cx_a), + (&client_b, cx_b), + (&client_c, cx_c), + (&client_d, cx_d), + ]) + .await; + + let active_call_a = cx_a.read(ActiveCall::global); + let active_call_b = cx_b.read(ActiveCall::global); + let active_call_c = cx_c.read(ActiveCall::global); + let active_call_d = cx_d.read(ActiveCall::global); + + // Simultaneously call user B and user C from client A. + let b_invite = active_call_a.update(cx_a, |call, cx| { + call.invite(client_b.user_id().unwrap(), None, cx) + }); + let c_invite = active_call_a.update(cx_a, |call, cx| { + call.invite(client_c.user_id().unwrap(), None, cx) + }); + b_invite.await.unwrap(); + c_invite.await.unwrap(); + + let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone()); + deterministic.run_until_parked(); + assert_eq!( + room_participants(&room_a, cx_a), + RoomParticipants { + remote: Default::default(), + pending: vec!["user_b".to_string(), "user_c".to_string()] + } + ); + + // Call client D from client A. + active_call_a + .update(cx_a, |call, cx| { + call.invite(client_d.user_id().unwrap(), None, cx) + }) + .await + .unwrap(); + deterministic.run_until_parked(); + assert_eq!( + room_participants(&room_a, cx_a), + RoomParticipants { + remote: Default::default(), + pending: vec![ + "user_b".to_string(), + "user_c".to_string(), + "user_d".to_string() + ] + } + ); + + // Accept the call on all clients simultaneously. + let accept_b = active_call_b.update(cx_b, |call, cx| call.accept_incoming(cx)); + let accept_c = active_call_c.update(cx_c, |call, cx| call.accept_incoming(cx)); + let accept_d = active_call_d.update(cx_d, |call, cx| call.accept_incoming(cx)); + accept_b.await.unwrap(); + accept_c.await.unwrap(); + accept_d.await.unwrap(); + + deterministic.run_until_parked(); + + let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone()); + let room_c = active_call_c.read_with(cx_c, |call, _| call.room().unwrap().clone()); + let room_d = active_call_d.read_with(cx_d, |call, _| call.room().unwrap().clone()); + assert_eq!( + room_participants(&room_a, cx_a), + RoomParticipants { + remote: vec![ + "user_b".to_string(), + "user_c".to_string(), + "user_d".to_string(), + ], + pending: Default::default() + } + ); + assert_eq!( + room_participants(&room_b, cx_b), + RoomParticipants { + remote: vec![ + "user_a".to_string(), + "user_c".to_string(), + "user_d".to_string(), + ], + pending: Default::default() + } + ); + assert_eq!( + room_participants(&room_c, cx_c), + RoomParticipants { + remote: vec![ + "user_a".to_string(), + "user_b".to_string(), + "user_d".to_string(), + ], + pending: Default::default() + } + ); + assert_eq!( + room_participants(&room_d, cx_d), + RoomParticipants { + remote: vec![ + "user_a".to_string(), + "user_b".to_string(), + "user_c".to_string(), + ], + pending: Default::default() + } + ); } #[gpui::test(iterations = 10)] @@ -2023,7 +2244,7 @@ async fn test_propagate_saves_and_fs_changes( }); // Edit the buffer as the host and concurrently save as guest B. - let save_b = buffer_b.update(cx_b, |buf, cx| buf.save(cx)); + let save_b = project_b.update(cx_b, |project, cx| project.save_buffer(buffer_b.clone(), cx)); buffer_a.update(cx_a, |buf, cx| buf.edit([(0..0, "hi-a, ")], None, cx)); save_b.await.unwrap(); assert_eq!( @@ -2092,6 +2313,41 @@ async fn test_propagate_saves_and_fs_changes( assert_eq!(buffer.file().unwrap().path().to_str(), Some("file1.js")); assert_eq!(&*buffer.language().unwrap().name(), "JavaScript"); }); + + let new_buffer_a = project_a + .update(cx_a, |p, cx| p.create_buffer("", None, cx)) + .unwrap(); + let new_buffer_id = new_buffer_a.read_with(cx_a, |buffer, _| buffer.remote_id()); + let new_buffer_b = project_b + .update(cx_b, |p, cx| p.open_buffer_by_id(new_buffer_id, cx)) + .await + .unwrap(); + new_buffer_b.read_with(cx_b, |buffer, _| { + assert!(buffer.file().is_none()); + }); + + new_buffer_a.update(cx_a, |buffer, cx| { + buffer.edit([(0..0, "ok")], None, cx); + }); + project_a + .update(cx_a, |project, cx| { + project.save_buffer_as(new_buffer_a.clone(), "/a/file3.rs".into(), cx) + }) + .await + .unwrap(); + + deterministic.run_until_parked(); + new_buffer_b.read_with(cx_b, |buffer_b, _| { + assert_eq!( + buffer_b.file().unwrap().path().as_ref(), + Path::new("file3.rs") + ); + + new_buffer_a.read_with(cx_a, |buffer_a, _| { + assert_eq!(buffer_b.saved_mtime(), buffer_a.saved_mtime()); + assert_eq!(buffer_b.saved_version(), buffer_a.saved_version()); + }); + }); } #[gpui::test(iterations = 10)] @@ -2571,6 +2827,8 @@ async fn test_fs_operations( }) .await .unwrap(); + deterministic.run_until_parked(); + worktree_a.read_with(cx_a, |worktree, _| { assert_eq!( worktree @@ -2659,7 +2917,9 @@ async fn test_buffer_conflict_after_save( assert!(!buf.has_conflict()); }); - buffer_b.update(cx_b, |buf, cx| buf.save(cx)).await.unwrap(); + project_b.update(cx_b, |project, cx| project.save_buffer(buffer_b.clone(), cx)) + .await + .unwrap(); cx_a.foreground().forbid_parking(); buffer_b.read_with(cx_b, |buffer_b, _| assert!(!buffer_b.is_dirty())); buffer_b.read_with(cx_b, |buf, _| { @@ -5291,6 +5551,27 @@ async fn test_contacts( [("user_b".to_string(), "online", "free")] ); + // Test removing a contact + client_b + .user_store + .update(cx_b, |store, cx| { + store.remove_contact(client_c.user_id().unwrap(), cx) + }) + .await + .unwrap(); + deterministic.run_until_parked(); + assert_eq!( + contacts(&client_b, cx_b), + [ + ("user_a".to_string(), "offline", "free"), + ("user_d".to_string(), "online", "free") + ] + ); + assert_eq!( + contacts(&client_c, cx_c), + [("user_a".to_string(), "offline", "free"),] + ); + fn contacts( client: &TestClient, cx: &TestAppContext, @@ -5602,7 +5883,6 @@ async fn test_following( .downcast::() .unwrap() }); - assert!(cx_b.read(|cx| editor_b2.is_focused(cx))); assert_eq!( cx_b.read(|cx| editor_b2.project_path(cx)), Some((worktree_id, "2.txt").into()) diff --git a/crates/collab/src/tests/randomized_integration_tests.rs b/crates/collab/src/tests/randomized_integration_tests.rs index e44e52196d..0b4aa3ec9b 100644 --- a/crates/collab/src/tests/randomized_integration_tests.rs +++ b/crates/collab/src/tests/randomized_integration_tests.rs @@ -397,16 +397,18 @@ async fn apply_server_operation( log::info!("Added connection for {}", username); } - Operation::RemoveConnection { user_id } => { - log::info!("Simulating full disconnection of user {}", user_id); + Operation::RemoveConnection { + user_id: removed_user_id, + } => { + log::info!("Simulating full disconnection of user {}", removed_user_id); let client_ix = clients .iter() - .position(|(client, cx)| client.current_user_id(cx) == user_id); + .position(|(client, cx)| client.current_user_id(cx) == removed_user_id); let Some(client_ix) = client_ix else { return false }; let user_connection_ids = server .connection_pool .lock() - .user_connection_ids(user_id) + .user_connection_ids(removed_user_id) .collect::>(); assert_eq!(user_connection_ids.len(), 1); let removed_peer_id = user_connection_ids[0].into(); @@ -417,7 +419,7 @@ async fn apply_server_operation( server.disconnect_client(removed_peer_id); deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); deterministic.start_waiting(); - log::info!("Waiting for user {} to exit...", user_id); + log::info!("Waiting for user {} to exit...", removed_user_id); client_task.await; deterministic.finish_waiting(); server.allow_connections(); @@ -441,19 +443,17 @@ async fn apply_server_operation( .unwrap(); let pool = server.connection_pool.lock(); for contact in contacts { - if let db::Contact::Accepted { user_id: id, .. } = contact { - if pool.is_user_online(id) { - assert_ne!( - id, user_id, - "removed client is still a contact of another peer" - ); + if let db::Contact::Accepted { user_id, busy, .. } = contact { + if user_id == removed_user_id { + assert!(!pool.is_user_online(user_id)); + assert!(!busy); } } } } log::info!("{} removed", client.username); - plan.lock().user(user_id).online = false; + plan.lock().user(removed_user_id).online = false; client_cx.update(|cx| { cx.clear_globals(); drop(client); @@ -806,8 +806,8 @@ async fn apply_client_operation( ); ensure_project_shared(&project, client, cx).await; - let (requested_version, save) = - buffer.update(cx, |buffer, cx| (buffer.version(), buffer.save(cx))); + let requested_version = buffer.read_with(cx, |buffer, _| buffer.version()); + let save = project.update(cx, |project, cx| project.save_buffer(buffer, cx)); let save = cx.background().spawn(async move { let (saved_version, _, _) = save .await @@ -1972,15 +1972,3 @@ fn path_env_var(name: &str) -> Option { } Some(path) } - -async fn child_file_paths(client: &TestClient, dir_path: &Path) -> Vec { - let mut child_paths = client.fs.read_dir(dir_path).await.unwrap(); - let mut child_file_paths = Vec::new(); - while let Some(child_path) = child_paths.next().await { - let child_path = child_path.unwrap(); - if client.fs.is_file(&child_path).await { - child_file_paths.push(child_path); - } - } - child_file_paths -} diff --git a/crates/collab_ui/Cargo.toml b/crates/collab_ui/Cargo.toml index ac13e361fd..2dc4cc769a 100644 --- a/crates/collab_ui/Cargo.toml +++ b/crates/collab_ui/Cargo.toml @@ -22,6 +22,7 @@ test-support = [ ] [dependencies] +auto_update = { path = "../auto_update" } call = { path = "../call" } client = { path = "../client" } clock = { path = "../clock" } diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index 3351fb9eb9..184a432ea3 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -1,4 +1,4 @@ -use crate::{contact_notification::ContactNotification, contacts_popover}; +use crate::{contact_notification::ContactNotification, contacts_popover, ToggleScreenSharing}; use call::{ActiveCall, ParticipantLocation}; use client::{proto::PeerId, Authenticate, ContactEventKind, User, UserStore}; use clock::ReplicaId; @@ -10,21 +10,17 @@ use gpui::{ geometry::{rect::RectF, vector::vec2f, PathBuilder}, json::{self, ToJson}, Border, CursorStyle, Entity, ModelHandle, MouseButton, MutableAppContext, RenderContext, - Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, + Subscription, View, ViewContext, ViewHandle, WeakViewHandle, }; use settings::Settings; use std::ops::Range; use theme::Theme; use workspace::{FollowNextCollaborator, JoinProject, ToggleFollow, Workspace}; -actions!( - collab, - [ToggleCollaborationMenu, ToggleScreenSharing, ShareProject] -); +actions!(collab, [ToggleCollaborationMenu, ShareProject]); pub fn init(cx: &mut MutableAppContext) { cx.add_action(CollabTitlebarItem::toggle_contacts_popover); - cx.add_action(CollabTitlebarItem::toggle_screen_sharing); cx.add_action(CollabTitlebarItem::share_project); } @@ -172,19 +168,6 @@ impl CollabTitlebarItem { cx.notify(); } - pub fn toggle_screen_sharing(&mut self, _: &ToggleScreenSharing, cx: &mut ViewContext) { - if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() { - let toggle_screen_sharing = room.update(cx, |room, cx| { - if room.is_screen_sharing() { - Task::ready(room.unshare_screen(cx)) - } else { - room.share_screen(cx) - } - }); - toggle_screen_sharing.detach_and_log_err(cx); - } - } - fn render_toggle_contacts_button( &self, theme: &Theme, @@ -521,7 +504,9 @@ impl CollabTitlebarItem { workspace: &ViewHandle, cx: &mut RenderContext, ) -> Option { - let theme = &cx.global::().theme; + enum ConnectionStatusButton {} + + let theme = &cx.global::().theme.clone(); match &*workspace.read(cx).client().status().borrow() { client::Status::ConnectionError | client::Status::ConnectionLost @@ -544,13 +529,20 @@ impl CollabTitlebarItem { .boxed(), ), client::Status::UpgradeRequired => Some( - Label::new( - "Please update Zed to collaborate".to_string(), - theme.workspace.titlebar.outdated_warning.text.clone(), - ) - .contained() - .with_style(theme.workspace.titlebar.outdated_warning.container) - .aligned() + MouseEventHandler::::new(0, cx, |_, _| { + Label::new( + "Please update Zed to collaborate".to_string(), + theme.workspace.titlebar.outdated_warning.text.clone(), + ) + .contained() + .with_style(theme.workspace.titlebar.outdated_warning.container) + .aligned() + .boxed() + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, |_, cx| { + cx.dispatch_action(auto_update::Check); + }) .boxed(), ), _ => None, diff --git a/crates/collab_ui/src/collab_ui.rs b/crates/collab_ui/src/collab_ui.rs index b19bc92455..d26e2c99cc 100644 --- a/crates/collab_ui/src/collab_ui.rs +++ b/crates/collab_ui/src/collab_ui.rs @@ -6,14 +6,17 @@ mod contacts_popover; mod incoming_call_notification; mod notifications; mod project_shared_notification; +mod sharing_status_indicator; use anyhow::anyhow; use call::ActiveCall; pub use collab_titlebar_item::{CollabTitlebarItem, ToggleCollaborationMenu}; -use gpui::MutableAppContext; +use gpui::{actions, MutableAppContext, Task}; use std::sync::Arc; use workspace::{AppState, JoinProject, ToggleFollow, Workspace}; +actions!(collab, [ToggleScreenSharing]); + pub fn init(app_state: Arc, cx: &mut MutableAppContext) { collab_titlebar_item::init(cx); contact_notification::init(cx); @@ -22,39 +25,60 @@ pub fn init(app_state: Arc, cx: &mut MutableAppContext) { contacts_popover::init(cx); incoming_call_notification::init(cx); project_shared_notification::init(cx); + sharing_status_indicator::init(cx); + cx.add_global_action(toggle_screen_sharing); cx.add_global_action(move |action: &JoinProject, cx| { - let project_id = action.project_id; - let follow_user_id = action.follow_user_id; - let app_state = app_state.clone(); - cx.spawn(|mut cx| async move { - let existing_workspace = cx.update(|cx| { - cx.window_ids() - .filter_map(|window_id| cx.root_view::(window_id)) - .find(|workspace| { - workspace.read(cx).project().read(cx).remote_id() == Some(project_id) - }) - }); + join_project(action, app_state.clone(), cx); + }); +} - let workspace = if let Some(existing_workspace) = existing_workspace { - existing_workspace +pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut MutableAppContext) { + if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() { + let toggle_screen_sharing = room.update(cx, |room, cx| { + if room.is_screen_sharing() { + Task::ready(room.unshare_screen(cx)) } else { - let active_call = cx.read(ActiveCall::global); - let room = active_call - .read_with(&cx, |call, _| call.room().cloned()) - .ok_or_else(|| anyhow!("not in a call"))?; - let project = room - .update(&mut cx, |room, cx| { - room.join_project( - project_id, - app_state.languages.clone(), - app_state.fs.clone(), - cx, - ) - }) - .await?; + room.share_screen(cx) + } + }); + toggle_screen_sharing.detach_and_log_err(cx); + } +} - let (_, workspace) = cx.add_window((app_state.build_window_options)(), |cx| { +fn join_project(action: &JoinProject, app_state: Arc, cx: &mut MutableAppContext) { + let project_id = action.project_id; + let follow_user_id = action.follow_user_id; + cx.spawn(|mut cx| async move { + let existing_workspace = cx.update(|cx| { + cx.window_ids() + .filter_map(|window_id| cx.root_view::(window_id)) + .find(|workspace| { + workspace.read(cx).project().read(cx).remote_id() == Some(project_id) + }) + }); + + let workspace = if let Some(existing_workspace) = existing_workspace { + existing_workspace + } else { + let active_call = cx.read(ActiveCall::global); + let room = active_call + .read_with(&cx, |call, _| call.room().cloned()) + .ok_or_else(|| anyhow!("not in a call"))?; + let project = room + .update(&mut cx, |room, cx| { + room.join_project( + project_id, + app_state.languages.clone(), + app_state.fs.clone(), + cx, + ) + }) + .await?; + + let (_, workspace) = cx.add_window( + (app_state.build_window_options)(None, None, cx.platform().as_ref()), + |cx| { let mut workspace = Workspace::new( Default::default(), 0, @@ -64,44 +88,44 @@ pub fn init(app_state: Arc, cx: &mut MutableAppContext) { ); (app_state.initialize_workspace)(&mut workspace, &app_state, cx); workspace - }); - workspace - }; + }, + ); + workspace + }; - cx.activate_window(workspace.window_id()); - cx.platform().activate(true); + cx.activate_window(workspace.window_id()); + cx.platform().activate(true); - workspace.update(&mut cx, |workspace, cx| { - if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() { - let follow_peer_id = room - .read(cx) - .remote_participants() - .iter() - .find(|(_, participant)| participant.user.id == follow_user_id) - .map(|(_, p)| p.peer_id) - .or_else(|| { - // If we couldn't follow the given user, follow the host instead. - let collaborator = workspace - .project() - .read(cx) - .collaborators() - .values() - .find(|collaborator| collaborator.replica_id == 0)?; - Some(collaborator.peer_id) - }); + workspace.update(&mut cx, |workspace, cx| { + if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() { + let follow_peer_id = room + .read(cx) + .remote_participants() + .iter() + .find(|(_, participant)| participant.user.id == follow_user_id) + .map(|(_, p)| p.peer_id) + .or_else(|| { + // If we couldn't follow the given user, follow the host instead. + let collaborator = workspace + .project() + .read(cx) + .collaborators() + .values() + .find(|collaborator| collaborator.replica_id == 0)?; + Some(collaborator.peer_id) + }); - if let Some(follow_peer_id) = follow_peer_id { - if !workspace.is_following(follow_peer_id) { - workspace - .toggle_follow(&ToggleFollow(follow_peer_id), cx) - .map(|follow| follow.detach_and_log_err(cx)); - } + if let Some(follow_peer_id) = follow_peer_id { + if !workspace.is_following(follow_peer_id) { + workspace + .toggle_follow(&ToggleFollow(follow_peer_id), cx) + .map(|follow| follow.detach_and_log_err(cx)); } } - }); + } + }); - anyhow::Ok(()) - }) - .detach_and_log_err(cx); - }); + anyhow::Ok(()) + }) + .detach_and_log_err(cx); } diff --git a/crates/collab_ui/src/contact_list.rs b/crates/collab_ui/src/contact_list.rs index 743b98adb0..c4250c142b 100644 --- a/crates/collab_ui/src/contact_list.rs +++ b/crates/collab_ui/src/contact_list.rs @@ -1,22 +1,22 @@ -use std::{mem, sync::Arc}; - use crate::contacts_popover; use call::ActiveCall; use client::{proto::PeerId, Contact, User, UserStore}; use editor::{Cancel, Editor}; +use futures::StreamExt; use fuzzy::{match_strings, StringMatchCandidate}; use gpui::{ elements::*, geometry::{rect::RectF, vector::vec2f}, impl_actions, impl_internal_actions, keymap_matcher::KeymapContext, - AppContext, CursorStyle, Entity, ModelHandle, MouseButton, MutableAppContext, RenderContext, - Subscription, View, ViewContext, ViewHandle, + AppContext, CursorStyle, Entity, ModelHandle, MouseButton, MutableAppContext, PromptLevel, + RenderContext, Subscription, View, ViewContext, ViewHandle, }; use menu::{Confirm, SelectNext, SelectPrev}; use project::Project; use serde::Deserialize; use settings::Settings; +use std::{mem, sync::Arc}; use theme::IconButton; use util::ResultExt; use workspace::{JoinProject, OpenSharedScreen}; @@ -299,9 +299,19 @@ impl ContactList { } fn remove_contact(&mut self, request: &RemoveContact, cx: &mut ViewContext) { - self.user_store - .update(cx, |store, cx| store.remove_contact(request.0, cx)) - .detach(); + let user_id = request.0; + let user_store = self.user_store.clone(); + let prompt_message = "Are you sure you want to remove this contact?"; + let mut answer = cx.prompt(PromptLevel::Warning, prompt_message, &["Remove", "Cancel"]); + cx.spawn(|_, mut cx| async move { + if answer.next().await == Some(0) { + user_store + .update(&mut cx, |store, cx| store.remove_contact(user_id, cx)) + .await + .unwrap(); + } + }) + .detach(); } fn respond_to_contact_request( @@ -1051,7 +1061,7 @@ impl ContactList { let user_id = contact.user.id; let initial_project = project.clone(); let mut element = - MouseEventHandler::::new(contact.user.id as usize, cx, |_, _| { + MouseEventHandler::::new(contact.user.id as usize, cx, |_, cx| { Flex::row() .with_children(contact.user.avatar.clone().map(|avatar| { let status_badge = if contact.online { @@ -1093,6 +1103,27 @@ impl ContactList { .flex(1., true) .boxed(), ) + .with_child( + MouseEventHandler::::new( + contact.user.id as usize, + cx, + |mouse_state, _| { + let button_style = + theme.contact_button.style_for(mouse_state, false); + render_icon_button(button_style, "icons/x_mark_8.svg") + .aligned() + .flex_float() + .boxed() + }, + ) + .with_padding(Padding::uniform(2.)) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, move |_, cx| { + cx.dispatch_action(RemoveContact(user_id)) + }) + .flex_float() + .boxed(), + ) .with_children(if calling { Some( Label::new("Calling".to_string(), theme.calling_indicator.text.clone()) diff --git a/crates/collab_ui/src/contact_notification.rs b/crates/collab_ui/src/contact_notification.rs index 6f0cfc68c7..f05cca00bf 100644 --- a/crates/collab_ui/src/contact_notification.rs +++ b/crates/collab_ui/src/contact_notification.rs @@ -48,7 +48,7 @@ impl View for ContactNotification { ContactEventKind::Requested => render_user_notification( self.user.clone(), "wants to add you as a contact", - Some("They won't know if you decline."), + Some("They won't be alerted if you decline."), Dismiss(self.user.id), vec![ ( diff --git a/crates/collab_ui/src/incoming_call_notification.rs b/crates/collab_ui/src/incoming_call_notification.rs index 6ad533665e..5d888bc093 100644 --- a/crates/collab_ui/src/incoming_call_notification.rs +++ b/crates/collab_ui/src/incoming_call_notification.rs @@ -32,11 +32,12 @@ pub fn init(cx: &mut MutableAppContext) { }); for screen in cx.platform().screens() { - let screen_size = screen.size(); + let screen_bounds = screen.bounds(); let (window_id, _) = cx.add_window( WindowOptions { bounds: WindowBounds::Fixed(RectF::new( - vec2f(screen_size.x() - window_size.x() - PADDING, PADDING), + screen_bounds.upper_right() + - vec2f(PADDING + window_size.x(), PADDING), window_size, )), titlebar: None, diff --git a/crates/collab_ui/src/project_shared_notification.rs b/crates/collab_ui/src/project_shared_notification.rs index 0815d9c8d8..8488f3381e 100644 --- a/crates/collab_ui/src/project_shared_notification.rs +++ b/crates/collab_ui/src/project_shared_notification.rs @@ -31,11 +31,11 @@ pub fn init(cx: &mut MutableAppContext) { let window_size = vec2f(theme.window_width, theme.window_height); for screen in cx.platform().screens() { - let screen_size = screen.size(); + let screen_bounds = screen.bounds(); let (window_id, _) = cx.add_window( WindowOptions { bounds: WindowBounds::Fixed(RectF::new( - vec2f(screen_size.x() - window_size.x() - PADDING, PADDING), + screen_bounds.upper_right() - vec2f(PADDING + window_size.x(), PADDING), window_size, )), titlebar: None, diff --git a/crates/collab_ui/src/sharing_status_indicator.rs b/crates/collab_ui/src/sharing_status_indicator.rs new file mode 100644 index 0000000000..541194ec66 --- /dev/null +++ b/crates/collab_ui/src/sharing_status_indicator.rs @@ -0,0 +1,59 @@ +use call::ActiveCall; +use gpui::{ + color::Color, + elements::{MouseEventHandler, Svg}, + Appearance, Element, ElementBox, Entity, MouseButton, MutableAppContext, RenderContext, View, +}; +use settings::Settings; + +use crate::ToggleScreenSharing; + +pub fn init(cx: &mut MutableAppContext) { + let active_call = ActiveCall::global(cx); + + let mut status_indicator = None; + 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::().show_call_status_icon { + status_indicator = Some(cx.add_status_bar_item(|_| SharingStatusIndicator)); + } + } else if let Some((window_id, _)) = status_indicator.take() { + cx.remove_status_bar_item(window_id); + } + } + }) + .detach(); +} + +pub struct SharingStatusIndicator; + +impl Entity for SharingStatusIndicator { + type Event = (); +} + +impl View for SharingStatusIndicator { + fn ui_name() -> &'static str { + "SharingStatusIndicator" + } + + fn render(&mut self, cx: &mut RenderContext<'_, Self>) -> ElementBox { + let color = match cx.appearance { + Appearance::Light | Appearance::VibrantLight => Color::black(), + Appearance::Dark | Appearance::VibrantDark => Color::white(), + }; + + MouseEventHandler::::new(0, cx, |_, _| { + Svg::new("icons/disable_screen_sharing_12.svg") + .with_color(color) + .constrained() + .with_width(18.) + .aligned() + .boxed() + }) + .on_click(MouseButton::Left, |_, cx| { + cx.dispatch_action(ToggleScreenSharing); + }) + .boxed() + } +} diff --git a/crates/command_palette/src/command_palette.rs b/crates/command_palette/src/command_palette.rs index d4625cbce0..5b5d8f1162 100644 --- a/crates/command_palette/src/command_palette.rs +++ b/crates/command_palette/src/command_palette.rs @@ -65,7 +65,7 @@ impl CommandPalette { action, keystrokes: bindings .iter() - .filter_map(|binding| binding.keystrokes()) + .map(|binding| binding.keystrokes()) .last() .map_or(Vec::new(), |keystrokes| keystrokes.to_vec()), }) diff --git a/crates/context_menu/src/context_menu.rs b/crates/context_menu/src/context_menu.rs index 0dc0ce6f42..6d5a5cb549 100644 --- a/crates/context_menu/src/context_menu.rs +++ b/crates/context_menu/src/context_menu.rs @@ -63,6 +63,7 @@ pub struct ContextMenu { visible: bool, previously_focused_view_id: Option, clicked: bool, + parent_view_id: usize, _actions_observation: Subscription, } @@ -114,6 +115,8 @@ impl View for ContextMenu { impl ContextMenu { pub fn new(cx: &mut ViewContext) -> Self { + let parent_view_id = cx.parent().unwrap(); + Self { show_count: 0, anchor_position: Default::default(), @@ -123,6 +126,7 @@ impl ContextMenu { visible: Default::default(), previously_focused_view_id: Default::default(), clicked: false, + parent_view_id, _actions_observation: cx.observe_actions(Self::action_dispatched), } } @@ -251,6 +255,7 @@ impl ContextMenu { } fn render_menu_for_measurement(&self, cx: &mut RenderContext) -> impl Element { + let window_id = cx.window_id(); let style = cx.global::().theme.context_menu.clone(); Flex::row() .with_child( @@ -289,6 +294,8 @@ impl ContextMenu { Some(ix) == self.selected_index, ); KeystrokeLabel::new( + window_id, + self.parent_view_id, action.boxed_clone(), style.keystroke.container, style.keystroke.text.clone(), @@ -318,6 +325,7 @@ impl ContextMenu { let style = cx.global::().theme.context_menu.clone(); + let window_id = cx.window_id(); MouseEventHandler::::new(0, cx, |_, cx| { Flex::column() .with_children(self.items.iter().enumerate().map(|(ix, item)| { @@ -337,6 +345,8 @@ impl ContextMenu { ) .with_child({ KeystrokeLabel::new( + window_id, + self.parent_view_id, action.boxed_clone(), style.keystroke.container, style.keystroke.text.clone(), diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index d3078bce81..596abe9bb6 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -21,6 +21,7 @@ use language::{ use project::{DiagnosticSummary, Project, ProjectPath}; use serde_json::json; use settings::Settings; +use smallvec::SmallVec; use std::{ any::{Any, TypeId}, cmp::Ordering, @@ -579,7 +580,7 @@ impl Item for ProjectDiagnosticsEditor { .update(cx, |editor, cx| editor.git_diff_recalc(project, cx)) } - fn to_item_events(event: &Self::Event) -> Vec { + fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]> { Editor::to_item_events(event) } diff --git a/crates/editor/Cargo.toml b/crates/editor/Cargo.toml index 26dd371041..6cb7ef32ec 100644 --- a/crates/editor/Cargo.toml +++ b/crates/editor/Cargo.toml @@ -17,7 +17,8 @@ test-support = [ "project/test-support", "util/test-support", "workspace/test-support", - "tree-sitter-rust" + "tree-sitter-rust", + "tree-sitter-typescript" ] [dependencies] @@ -58,6 +59,7 @@ smol = "1.2" tree-sitter-rust = { version = "*", optional = true } tree-sitter-html = { version = "*", optional = true } tree-sitter-javascript = { version = "*", optional = true } +tree-sitter-typescript = { git = "https://github.com/tree-sitter/tree-sitter-typescript", rev = "5d20856f34315b068c41edaee2ac8a100081d259", optional = true } [dev-dependencies] text = { path = "../text", features = ["test-support"] } @@ -75,4 +77,5 @@ unindent = "0.1.7" tree-sitter = "0.20" tree-sitter-rust = "0.20" tree-sitter-html = "0.19" +tree-sitter-typescript = { git = "https://github.com/tree-sitter/tree-sitter-typescript", rev = "5d20856f34315b068c41edaee2ac8a100081d259" } tree-sitter-javascript = "0.20" diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index e32276df41..99a74fe7f2 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -337,7 +337,7 @@ impl DisplaySnapshot { .map(|h| h.text) } - // Returns text chunks starting at the end of the given display row in reverse until the start of the file + /// Returns text chunks starting at the end of the given display row in reverse until the start of the file pub fn reverse_text_chunks(&self, display_row: u32) -> impl Iterator { (0..=display_row).into_iter().rev().flat_map(|row| { self.blocks_snapshot @@ -411,6 +411,67 @@ impl DisplaySnapshot { }) } + /// Returns an iterator of the start positions of the occurances of `target` in the `self` after `from` + /// Stops if `condition` returns false for any of the character position pairs observed. + pub fn find_while<'a>( + &'a self, + from: DisplayPoint, + target: &str, + condition: impl FnMut(char, DisplayPoint) -> bool + 'a, + ) -> impl Iterator + 'a { + Self::find_internal(self.chars_at(from), target.chars().collect(), condition) + } + + /// Returns an iterator of the end positions of the occurances of `target` in the `self` before `from` + /// Stops if `condition` returns false for any of the character position pairs observed. + pub fn reverse_find_while<'a>( + &'a self, + from: DisplayPoint, + target: &str, + condition: impl FnMut(char, DisplayPoint) -> bool + 'a, + ) -> impl Iterator + 'a { + Self::find_internal( + self.reverse_chars_at(from), + target.chars().rev().collect(), + condition, + ) + } + + fn find_internal<'a>( + iterator: impl Iterator + 'a, + target: Vec, + mut condition: impl FnMut(char, DisplayPoint) -> bool + 'a, + ) -> impl Iterator + 'a { + // List of partial matches with the index of the last seen character in target and the starting point of the match + let mut partial_matches: Vec<(usize, DisplayPoint)> = Vec::new(); + iterator + .take_while(move |(ch, point)| condition(*ch, *point)) + .filter_map(move |(ch, point)| { + if Some(&ch) == target.get(0) { + partial_matches.push((0, point)); + } + + let mut found = None; + // Keep partial matches that have the correct next character + partial_matches.retain_mut(|(match_position, match_start)| { + if target.get(*match_position) == Some(&ch) { + *match_position += 1; + if *match_position == target.len() { + found = Some(match_start.clone()); + // This match is completed. No need to keep tracking it + false + } else { + true + } + } else { + false + } + }); + + found + }) + } + pub fn column_to_chars(&self, display_row: u32, target: u32) -> u32 { let mut count = 0; let mut column = 0; @@ -627,7 +688,7 @@ pub mod tests { use smol::stream::StreamExt; use std::{env, sync::Arc}; use theme::SyntaxTheme; - use util::test::{marked_text_ranges, sample_text}; + use util::test::{marked_text_offsets, marked_text_ranges, sample_text}; use Bias::*; #[gpui::test(iterations = 100)] @@ -1418,6 +1479,32 @@ pub mod tests { ) } + #[test] + fn test_find_internal() { + assert("This is a ˇtest of find internal", "test"); + assert("Some text ˇaˇaˇaa with repeated characters", "aa"); + + fn assert(marked_text: &str, target: &str) { + let (text, expected_offsets) = marked_text_offsets(marked_text); + + let chars = text + .chars() + .enumerate() + .map(|(index, ch)| (ch, DisplayPoint::new(0, index as u32))); + let target = target.chars(); + + assert_eq!( + expected_offsets + .into_iter() + .map(|offset| offset as u32) + .collect::>(), + DisplaySnapshot::find_internal(chars, target.collect(), |_, _| true) + .map(|point| point.column()) + .collect::>() + ) + } + } + fn syntax_chunks<'a>( rows: Range, map: &ModelHandle, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 84b97468e0..f9d5001985 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -77,14 +77,14 @@ use std::{ cmp::{self, Ordering, Reverse}, mem, num::NonZeroU32, - ops::{Deref, DerefMut, Range, RangeInclusive}, + ops::{Deref, DerefMut, Range}, path::Path, sync::Arc, time::{Duration, Instant}, }; pub use sum_tree::Bias; use theme::{DiagnosticStyle, Theme}; -use util::{post_inc, ResultExt, TryFutureExt}; +use util::{post_inc, ResultExt, TryFutureExt, RangeExt}; use workspace::{ItemNavHistory, ViewId, Workspace, WorkspaceId}; use crate::git::diff_hunk_to_display; @@ -154,6 +154,12 @@ pub struct ConfirmCodeAction { pub item_ix: Option, } +#[derive(Clone, Default, Deserialize, PartialEq)] +pub struct ToggleComments { + #[serde(default)] + pub advance_downwards: bool, +} + actions!( editor, [ @@ -216,7 +222,6 @@ actions!( AddSelectionBelow, Tab, TabPrev, - ToggleComments, ShowCharacterPalette, SelectLargerSyntaxNode, SelectSmallerSyntaxNode, @@ -236,6 +241,7 @@ actions!( RestartLanguageServer, Hover, Format, + ToggleSoftWrap ] ); @@ -250,6 +256,7 @@ impl_actions!( MovePageDown, ConfirmCompletion, ConfirmCodeAction, + ToggleComments, ] ); @@ -346,6 +353,7 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_action(Editor::toggle_code_actions); cx.add_action(Editor::open_excerpts); cx.add_action(Editor::jump); + cx.add_action(Editor::toggle_soft_wrap); cx.add_async_action(Editor::format); cx.add_action(Editor::restart_language_server); cx.add_action(Editor::show_character_palette); @@ -400,7 +408,7 @@ pub enum SelectMode { All, } -#[derive(Copy, Clone, PartialEq, Eq)] +#[derive(Copy, Clone, PartialEq, Eq, Debug)] pub enum EditorMode { SingleLine, AutoHeight { max_lines: usize }, @@ -810,7 +818,7 @@ impl CompletionsMenu { fuzzy::match_strings( &self.match_candidates, query, - false, + query.chars().any(|c| c.is_uppercase()), 100, &Default::default(), executor, @@ -1732,11 +1740,13 @@ impl Editor { } pub fn handle_input(&mut self, text: &str, cx: &mut ViewContext) { + let text: Arc = text.into(); + if !self.input_enabled { + cx.emit(Event::InputIgnored { text }); return; } - let text: Arc = text.into(); let selections = self.selections.all_adjusted(cx); let mut edits = Vec::new(); let mut new_selections = Vec::with_capacity(selections.len()); @@ -1814,9 +1824,9 @@ impl Editor { } } } - // If an opening bracket is typed while text is selected, then - // surround that text with the bracket pair. - else if is_bracket_pair_start { + // If an opening bracket is 1 character long and is typed while + // text is selected, then surround that text with the bracket pair. + else if is_bracket_pair_start && bracket_pair.start.chars().count() == 1 { edits.push((selection.start..selection.start, text.clone())); edits.push(( selection.end..selection.end, @@ -3800,7 +3810,7 @@ impl Editor { } } - if matches!(self.mode, EditorMode::SingleLine) { + if self.mode == EditorMode::SingleLine { cx.propagate_action(); return; } @@ -4462,7 +4472,7 @@ impl Editor { } } - pub fn toggle_comments(&mut self, _: &ToggleComments, cx: &mut ViewContext) { + pub fn toggle_comments(&mut self, action: &ToggleComments, cx: &mut ViewContext) { self.transact(cx, |this, cx| { let mut selections = this.selections.all::(cx); let mut edits = Vec::new(); @@ -4681,6 +4691,34 @@ impl Editor { drop(snapshot); this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections)); + + let selections = this.selections.all::(cx); + let selections_on_single_row = selections.windows(2).all(|selections| { + selections[0].start.row == selections[1].start.row + && selections[0].end.row == selections[1].end.row + && selections[0].start.row == selections[0].end.row + }); + let selections_selecting = selections + .iter() + .any(|selection| selection.start != selection.end); + let advance_downwards = action.advance_downwards + && selections_on_single_row + && !selections_selecting + && this.mode != EditorMode::SingleLine; + + if advance_downwards { + let snapshot = this.buffer.read(cx).snapshot(cx); + + this.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.move_cursors_with(|display_snapshot, display_point, _| { + let mut point = display_point.to_point(display_snapshot); + point.row += 1; + point = snapshot.clip_point(point, Bias::Left); + let display_point = point.to_display_point(display_snapshot); + (display_point, SelectionGoal::Column(display_point.column())) + }) + }); + } }); } @@ -4750,27 +4788,52 @@ impl Editor { _: &MoveToEnclosingBracket, cx: &mut ViewContext, ) { - let buffer = self.buffer.read(cx).snapshot(cx); - let mut selections = self.selections.all::(cx); - for selection in &mut selections { - if let Some((open_range, close_range)) = - buffer.enclosing_bracket_ranges(selection.start..selection.end) - { - let close_range = close_range.to_inclusive(); - let destination = if close_range.contains(&selection.start) - && close_range.contains(&selection.end) - { - open_range.end - } else { - *close_range.start() - }; - selection.start = destination; - selection.end = destination; - } - } - self.change_selections(Some(Autoscroll::fit()), cx, |s| { - s.select(selections); + s.move_offsets_with(|snapshot, selection| { + let Some(enclosing_bracket_ranges) = snapshot.enclosing_bracket_ranges(selection.start..selection.end) else { return; }; + + let mut best_length = usize::MAX; + let mut best_inside = false; + let mut best_in_bracket_range = false; + let mut best_destination = None; + for (open, close) in enclosing_bracket_ranges { + let close = close.to_inclusive(); + let length = close.end() - open.start; + let inside = selection.start >= open.end && selection.end <= *close.start(); + let in_bracket_range = open.to_inclusive().contains(&selection.head()) || close.contains(&selection.head()); + + // If best is next to a bracket and current isn't, skip + if !in_bracket_range && best_in_bracket_range { + continue; + } + + // Prefer smaller lengths unless best is inside and current isn't + if length > best_length && (best_inside || !inside) { + continue; + } + + best_length = length; + best_inside = inside; + best_in_bracket_range = in_bracket_range; + best_destination = Some(if close.contains(&selection.start) && close.contains(&selection.end) { + if inside { + open.end + } else { + open.start + } + } else { + if inside { + *close.start() + } else { + *close.end() + } + }); + } + + if let Some(destination) = best_destination { + selection.collapse_to(destination, SelectionGoal::None); + } + }) }); } @@ -5042,7 +5105,7 @@ impl Editor { pane.update(cx, |pane, _| pane.enable_history()); }); - } else { + } else if !definitions.is_empty() { let replica_id = editor_handle.read(cx).replica_id(cx); let title = definitions .iter() @@ -5810,6 +5873,19 @@ impl Editor { .update(cx, |map, cx| map.set_wrap_width(width, cx)) } + pub fn toggle_soft_wrap(&mut self, _: &ToggleSoftWrap, cx: &mut ViewContext) { + if self.soft_wrap_mode_override.is_some() { + 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, + }; + self.soft_wrap_mode_override = Some(soft_wrap); + } + cx.notify(); + } + pub fn highlight_rows(&mut self, rows: Option>) { self.highlighted_rows = rows; } @@ -6187,6 +6263,9 @@ impl Deref for EditorSnapshot { #[derive(Clone, Debug, PartialEq, Eq)] pub enum Event { + InputIgnored { + text: Arc, + }, ExcerptsAdded { buffer: ModelHandle, predecessor: ExcerptId, @@ -6253,8 +6332,10 @@ impl View for Editor { } fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { - let focused_event = EditorFocused(cx.handle()); - cx.emit_global(focused_event); + if cx.is_self_focused() { + let focused_event = EditorFocused(cx.handle()); + cx.emit_global(focused_event); + } if let Some(rename) = self.pending_rename.as_ref() { cx.focus(&rename.editor); } else { @@ -6393,26 +6474,29 @@ impl View for Editor { text: &str, cx: &mut ViewContext, ) { + self.transact(cx, |this, cx| { + if this.input_enabled { + let new_selected_ranges = if let Some(range_utf16) = range_utf16 { + let range_utf16 = OffsetUtf16(range_utf16.start)..OffsetUtf16(range_utf16.end); + Some(this.selection_replacement_ranges(range_utf16, cx)) + } else { + this.marked_text_ranges(cx) + }; + + if let Some(new_selected_ranges) = new_selected_ranges { + this.change_selections(None, cx, |selections| { + selections.select_ranges(new_selected_ranges) + }); + } + } + + this.handle_input(text, cx); + }); + if !self.input_enabled { return; } - self.transact(cx, |this, cx| { - let new_selected_ranges = if let Some(range_utf16) = range_utf16 { - let range_utf16 = OffsetUtf16(range_utf16.start)..OffsetUtf16(range_utf16.end); - Some(this.selection_replacement_ranges(range_utf16, cx)) - } else { - this.marked_text_ranges(cx) - }; - - if let Some(new_selected_ranges) = new_selected_ranges { - this.change_selections(None, cx, |selections| { - selections.select_ranges(new_selected_ranges) - }); - } - this.handle_input(text, cx); - }); - if let Some(transaction) = self.ime_transaction { self.buffer.update(cx, |buffer, cx| { buffer.group_until_transaction(transaction, cx); @@ -6909,21 +6993,6 @@ pub fn split_words<'a>(text: &'a str) -> impl std::iter::Iterator { - fn sorted(&self) -> Range; - fn to_inclusive(&self) -> RangeInclusive; -} - -impl RangeExt for Range { - fn sorted(&self) -> Self { - cmp::min(&self.start, &self.end).clone()..cmp::max(&self.start, &self.end).clone() - } - - fn to_inclusive(&self) -> RangeInclusive { - self.start.clone()..=self.end.clone() - } -} - trait RangeToAnchorExt { fn to_anchors(self, snapshot: &MultiBufferSnapshot) -> Range; } diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index ff59c5dc14..9b5cececff 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -3452,12 +3452,20 @@ async fn test_surround_with_pair(cx: &mut gpui::TestAppContext) { cx.update(|cx| cx.set_global(Settings::test(cx))); let language = Arc::new(Language::new( LanguageConfig { - brackets: vec![BracketPair { - start: "{".to_string(), - end: "}".to_string(), - close: true, - newline: true, - }], + brackets: vec![ + BracketPair { + start: "{".to_string(), + end: "}".to_string(), + close: true, + newline: true, + }, + BracketPair { + start: "/* ".to_string(), + end: "*/".to_string(), + close: true, + ..Default::default() + }, + ], ..Default::default() }, Some(tree_sitter_rust::language()), @@ -3526,6 +3534,67 @@ async fn test_surround_with_pair(cx: &mut gpui::TestAppContext) { DisplayPoint::new(2, 0)..DisplayPoint::new(2, 1) ] ); + + // Ensure inserting the first character of a multi-byte bracket pair + // doesn't surround the selections with the bracket. + view.handle_input("/", cx); + assert_eq!( + view.text(cx), + " + / + / + / + " + .unindent() + ); + assert_eq!( + view.selections.display_ranges(cx), + [ + DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), + DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1), + DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1) + ] + ); + + view.undo(&Undo, cx); + assert_eq!( + view.text(cx), + " + a + b + c + " + .unindent() + ); + assert_eq!( + view.selections.display_ranges(cx), + [ + DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1), + DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1), + DisplayPoint::new(2, 0)..DisplayPoint::new(2, 1) + ] + ); + + // Ensure inserting the last character of a multi-byte bracket pair + // doesn't surround the selections with the bracket. + view.handle_input("*", cx); + assert_eq!( + view.text(cx), + " + * + * + * + " + .unindent() + ); + assert_eq!( + view.selections.display_ranges(cx), + [ + DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), + DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1), + DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1) + ] + ); }); } @@ -4382,7 +4451,7 @@ async fn test_toggle_comment(cx: &mut gpui::TestAppContext) { DisplayPoint::new(3, 5)..DisplayPoint::new(3, 6), ]) }); - editor.toggle_comments(&ToggleComments, cx); + editor.toggle_comments(&ToggleComments::default(), cx); assert_eq!( editor.text(cx), " @@ -4400,7 +4469,7 @@ async fn test_toggle_comment(cx: &mut gpui::TestAppContext) { editor.change_selections(None, cx, |s| { s.select_display_ranges([DisplayPoint::new(1, 3)..DisplayPoint::new(3, 6)]) }); - editor.toggle_comments(&ToggleComments, cx); + editor.toggle_comments(&ToggleComments::default(), cx); assert_eq!( editor.text(cx), " @@ -4417,7 +4486,7 @@ async fn test_toggle_comment(cx: &mut gpui::TestAppContext) { editor.change_selections(None, cx, |s| { s.select_display_ranges([DisplayPoint::new(2, 0)..DisplayPoint::new(3, 0)]) }); - editor.toggle_comments(&ToggleComments, cx); + editor.toggle_comments(&ToggleComments::default(), cx); assert_eq!( editor.text(cx), " @@ -4432,6 +4501,139 @@ 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))); + + let language = Arc::new(Language::new( + LanguageConfig { + line_comment: Some("// ".into()), + ..Default::default() + }, + Some(tree_sitter_rust::language()), + )); + + let registry = Arc::new(LanguageRegistry::test()); + registry.add(language.clone()); + + cx.update_buffer(|buffer, cx| { + buffer.set_language_registry(registry); + buffer.set_language(Some(language), cx); + }); + + let toggle_comments = &ToggleComments { + advance_downwards: true, + }; + + // Single cursor on one line -> advance + // Cursor moves horizontally 3 characters as well on non-blank line + cx.set_state(indoc!( + "fn a() { + ˇdog(); + cat(); + }" + )); + cx.update_editor(|editor, cx| { + editor.toggle_comments(toggle_comments, cx); + }); + cx.assert_editor_state(indoc!( + "fn a() { + // dog(); + catˇ(); + }" + )); + + // Single selection on one line -> don't advance + cx.set_state(indoc!( + "fn a() { + «dog()ˇ»; + cat(); + }" + )); + cx.update_editor(|editor, cx| { + editor.toggle_comments(toggle_comments, cx); + }); + cx.assert_editor_state(indoc!( + "fn a() { + // «dog()ˇ»; + cat(); + }" + )); + + // Multiple cursors on one line -> advance + cx.set_state(indoc!( + "fn a() { + ˇdˇog(); + cat(); + }" + )); + cx.update_editor(|editor, cx| { + editor.toggle_comments(toggle_comments, cx); + }); + cx.assert_editor_state(indoc!( + "fn a() { + // dog(); + catˇ(ˇ); + }" + )); + + // Multiple cursors on one line, with selection -> don't advance + cx.set_state(indoc!( + "fn a() { + ˇdˇog«()ˇ»; + cat(); + }" + )); + cx.update_editor(|editor, cx| { + editor.toggle_comments(toggle_comments, cx); + }); + cx.assert_editor_state(indoc!( + "fn a() { + // ˇdˇog«()ˇ»; + cat(); + }" + )); + + // Single cursor on one line -> advance + // Cursor moves to column 0 on blank line + cx.set_state(indoc!( + "fn a() { + ˇdog(); + + cat(); + }" + )); + cx.update_editor(|editor, cx| { + editor.toggle_comments(toggle_comments, cx); + }); + cx.assert_editor_state(indoc!( + "fn a() { + // dog(); + ˇ + cat(); + }" + )); + + // Single cursor on one line -> advance + // Cursor starts and ends at column 0 + cx.set_state(indoc!( + "fn a() { + ˇ dog(); + cat(); + }" + )); + cx.update_editor(|editor, cx| { + editor.toggle_comments(toggle_comments, cx); + }); + cx.assert_editor_state(indoc!( + "fn a() { + // dog(); + ˇ cat(); + }" + )); +} + #[gpui::test] async fn test_toggle_block_comment(cx: &mut gpui::TestAppContext) { let mut cx = EditorTestContext::new(cx); @@ -4482,7 +4684,7 @@ async fn test_toggle_block_comment(cx: &mut gpui::TestAppContext) { "# .unindent(), ); - cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments, cx)); + cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx)); cx.assert_editor_state( &r#" @@ -4491,7 +4693,7 @@ async fn test_toggle_block_comment(cx: &mut gpui::TestAppContext) { "# .unindent(), ); - cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments, cx)); + cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx)); cx.assert_editor_state( &r#"

A

ˇ @@ -4513,7 +4715,7 @@ async fn test_toggle_block_comment(cx: &mut gpui::TestAppContext) { .unindent(), ); - cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments, cx)); + cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx)); cx.assert_editor_state( &r#" @@ -5459,6 +5661,54 @@ fn test_split_words() { assert_eq!(split("helloworld"), &["helloworld"]); } +#[gpui::test] +async fn test_move_to_enclosing_bracket(cx: &mut gpui::TestAppContext) { + let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await; + let mut assert = |before, after| { + let _state_context = cx.set_state(before); + cx.update_editor(|editor, cx| { + editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, cx) + }); + cx.assert_editor_state(after); + }; + + // Outside bracket jumps to outside of matching bracket + assert("console.logˇ(var);", "console.log(var)ˇ;"); + assert("console.log(var)ˇ;", "console.logˇ(var);"); + + // Inside bracket jumps to inside of matching bracket + assert("console.log(ˇvar);", "console.log(varˇ);"); + assert("console.log(varˇ);", "console.log(ˇvar);"); + + // When outside a bracket and inside, favor jumping to the inside bracket + assert( + "console.log('foo', [1, 2, 3]ˇ);", + "console.log(ˇ'foo', [1, 2, 3]);", + ); + assert( + "console.log(ˇ'foo', [1, 2, 3]);", + "console.log('foo', [1, 2, 3]ˇ);", + ); + + // Bias forward if two options are equally likely + assert( + "let result = curried_fun()ˇ();", + "let result = curried_fun()()ˇ;", + ); + + // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller + assert( + indoc! {" + function test() { + console.log('test')ˇ + }"}, + indoc! {" + function test() { + console.logˇ('test') + }"}, + ); +} + fn empty_range(row: usize, column: usize) -> Range { let point = DisplayPoint::new(row as u32, column as u32); point..point diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index bce63ca0cf..9d8922fab5 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1534,15 +1534,14 @@ impl Element for EditorElement { let snapshot = self.update_view(cx.app, |view, cx| { view.set_visible_line_count(size.y() / line_height); + let editor_width = text_width - gutter_margin - overscroll.x() - em_width; let wrap_width = match view.soft_wrap_mode(cx) { - SoftWrap::None => Some((MAX_LINE_LEN / 2) as f32 * em_advance), - SoftWrap::EditorWidth => { - Some(text_width - gutter_margin - overscroll.x() - em_width) - } - SoftWrap::Column(column) => Some(column as f32 * em_advance), + SoftWrap::None => (MAX_LINE_LEN / 2) as f32 * em_advance, + SoftWrap::EditorWidth => editor_width, + SoftWrap::Column(column) => editor_width.min(column as f32 * em_advance), }; - if view.set_wrap_width(wrap_width, cx) { + if view.set_wrap_width(Some(wrap_width), cx) { view.snapshot(cx) } else { snapshot diff --git a/crates/editor/src/highlight_matching_bracket.rs b/crates/editor/src/highlight_matching_bracket.rs index 043b21db21..0d868d460c 100644 --- a/crates/editor/src/highlight_matching_bracket.rs +++ b/crates/editor/src/highlight_matching_bracket.rs @@ -17,7 +17,7 @@ pub fn refresh_matching_bracket_highlights(editor: &mut Editor, cx: &mut ViewCon let snapshot = editor.snapshot(cx); if let Some((opening_range, closing_range)) = snapshot .buffer_snapshot - .enclosing_bracket_ranges(head..head) + .innermost_enclosing_bracket_ranges(head..head) { editor.highlight_background::( vec![ diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 6d003cae5d..f92b07da1d 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -331,7 +331,7 @@ impl InfoPopover { if let Some(language) = content .language .clone() - .and_then(|language| project.languages().get_language(&language)) + .and_then(|language| project.languages().language_for_name(&language)) { let runs = language .highlight_text(&content.text.as_str().into(), 0..content.text.len()); diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 7e7f44e514..c3a446faa7 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -2,12 +2,10 @@ use crate::{ display_map::ToDisplayPoint, link_go_to_definition::hide_link_definition, movement::surrounding_word, persistence::DB, scroll::ScrollAnchor, Anchor, Autoscroll, Editor, Event, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, NavigationData, ToPoint as _, - FORMAT_TIMEOUT, }; use anyhow::{anyhow, Context, Result}; use collections::HashSet; use futures::future::try_join_all; -use futures::FutureExt; use gpui::{ elements::*, geometry::vector::vec2f, AppContext, Entity, ModelHandle, MutableAppContext, RenderContext, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, @@ -16,9 +14,10 @@ use language::{ proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, OffsetRangeExt, Point, SelectionGoal, }; -use project::{FormatTrigger, Item as _, Project, ProjectPath}; +use project::{Item as _, Project, ProjectPath}; use rpc::proto::{self, update_view}; use settings::Settings; +use smallvec::SmallVec; use std::{ borrow::Cow, cmp::{self, Ordering}, @@ -609,32 +608,12 @@ impl Item for Editor { cx: &mut ViewContext, ) -> Task> { self.report_event("save editor", cx); - - let buffer = self.buffer().clone(); - let buffers = buffer.read(cx).all_buffers(); - let mut timeout = cx.background().timer(FORMAT_TIMEOUT).fuse(); - let format = project.update(cx, |project, cx| { - project.format(buffers, true, FormatTrigger::Save, cx) - }); - cx.spawn(|_, mut cx| async move { - let transaction = futures::select_biased! { - _ = timeout => { - log::warn!("timed out waiting for formatting"); - None - } - transaction = format.log_err().fuse() => transaction, - }; - - buffer - .update(&mut cx, |buffer, cx| { - if let Some(transaction) = transaction { - if !buffer.is_singleton() { - buffer.push_transaction(&transaction.0); - } - } - - buffer.save(cx) - }) + let format = self.perform_format(project.clone(), cx); + let buffers = self.buffer().clone().read(cx).all_buffers(); + cx.as_mut().spawn(|mut cx| async move { + format.await?; + project + .update(&mut cx, |project, cx| project.save_buffers(buffers, cx)) .await?; Ok(()) }) @@ -693,8 +672,8 @@ impl Item for Editor { Task::ready(Ok(())) } - fn to_item_events(event: &Self::Event) -> Vec { - let mut result = Vec::new(); + fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]> { + let mut result = SmallVec::new(); match event { Event::Closed => result.push(ItemEvent::CloseItem), Event::Saved | Event::TitleChanged => { @@ -1094,7 +1073,7 @@ impl StatusItemView for CursorPosition { active_pane_item: Option<&dyn ItemHandle>, cx: &mut ViewContext, ) { - if let Some(editor) = active_pane_item.and_then(|item| item.downcast::()) { + if let Some(editor) = active_pane_item.and_then(|item| item.act_as::(cx)) { self._observe_active_editor = Some(cx.observe(&editor, Self::update_position)); self.update_position(editor, cx); } else { @@ -1158,7 +1137,6 @@ fn path_for_file<'a>( mod tests { use super::*; use gpui::MutableAppContext; - use language::RopeFingerprint; use std::{ path::{Path, PathBuf}, sync::Arc, @@ -1204,17 +1182,6 @@ mod tests { todo!() } - fn save( - &self, - _: u64, - _: language::Rope, - _: clock::Global, - _: project::LineEnding, - _: &mut MutableAppContext, - ) -> gpui::Task> { - todo!() - } - fn as_any(&self) -> &dyn std::any::Any { todo!() } diff --git a/crates/editor/src/mouse_context_menu.rs b/crates/editor/src/mouse_context_menu.rs index d9840fd3fa..77b58d1a0b 100644 --- a/crates/editor/src/mouse_context_menu.rs +++ b/crates/editor/src/mouse_context_menu.rs @@ -52,8 +52,8 @@ pub fn deploy_context_menu( AnchorCorner::TopLeft, vec![ ContextMenuItem::item("Rename Symbol", Rename), - ContextMenuItem::item("Go To Definition", GoToDefinition), - ContextMenuItem::item("Go To Type Definition", GoToTypeDefinition), + ContextMenuItem::item("Go to Definition", GoToDefinition), + ContextMenuItem::item("Go to Type Definition", GoToTypeDefinition), ContextMenuItem::item("Find All References", FindAllReferences), ContextMenuItem::item( "Code Actions", diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index 7079d197f9..da3c6bc4bd 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -1,7 +1,6 @@ mod anchor; pub use anchor::{Anchor, AnchorRangeExt}; -use anyhow::Result; use clock::ReplicaId; use collections::{BTreeMap, Bound, HashMap, HashSet}; use futures::{channel::mpsc, SinkExt}; @@ -385,9 +384,13 @@ impl MultiBuffer { _ => Default::default(), }; - #[allow(clippy::type_complexity)] - let mut buffer_edits: HashMap, Arc, bool, u32)>> = - Default::default(); + struct BufferEdit { + range: Range, + new_text: Arc, + is_insertion: bool, + original_indent_column: u32, + } + let mut buffer_edits: HashMap> = Default::default(); let mut cursor = snapshot.excerpts.cursor::(); for (ix, (range, new_text)) in edits.enumerate() { let new_text: Arc = new_text.into(); @@ -422,12 +425,12 @@ impl MultiBuffer { buffer_edits .entry(start_excerpt.buffer_id) .or_insert(Vec::new()) - .push(( - buffer_start..buffer_end, + .push(BufferEdit { + range: buffer_start..buffer_end, new_text, - true, + is_insertion: true, original_indent_column, - )); + }); } else { let start_excerpt_range = buffer_start ..start_excerpt @@ -444,21 +447,21 @@ impl MultiBuffer { buffer_edits .entry(start_excerpt.buffer_id) .or_insert(Vec::new()) - .push(( - start_excerpt_range, - new_text.clone(), - true, + .push(BufferEdit { + range: start_excerpt_range, + new_text: new_text.clone(), + is_insertion: true, original_indent_column, - )); + }); buffer_edits .entry(end_excerpt.buffer_id) .or_insert(Vec::new()) - .push(( - end_excerpt_range, - new_text.clone(), - false, + .push(BufferEdit { + range: end_excerpt_range, + new_text: new_text.clone(), + is_insertion: false, original_indent_column, - )); + }); cursor.seek(&range.start, Bias::Right, &()); cursor.next(&()); @@ -469,19 +472,19 @@ impl MultiBuffer { buffer_edits .entry(excerpt.buffer_id) .or_insert(Vec::new()) - .push(( - excerpt.range.context.to_offset(&excerpt.buffer), - new_text.clone(), - false, + .push(BufferEdit { + range: excerpt.range.context.to_offset(&excerpt.buffer), + new_text: new_text.clone(), + is_insertion: false, original_indent_column, - )); + }); cursor.next(&()); } } } for (buffer_id, mut edits) in buffer_edits { - edits.sort_unstable_by_key(|(range, _, _, _)| range.start); + edits.sort_unstable_by_key(|edit| edit.range.start); self.buffers.borrow()[&buffer_id] .buffer .update(cx, |buffer, cx| { @@ -490,14 +493,19 @@ impl MultiBuffer { let mut original_indent_columns = Vec::new(); let mut deletions = Vec::new(); let empty_str: Arc = "".into(); - while let Some(( + while let Some(BufferEdit { mut range, new_text, mut is_insertion, original_indent_column, - )) = edits.next() + }) = edits.next() { - while let Some((next_range, _, next_is_insertion, _)) = edits.peek() { + while let Some(BufferEdit { + range: next_range, + is_insertion: next_is_insertion, + .. + }) = edits.peek() + { if range.end >= next_range.start { range.end = cmp::max(next_range.end, range.end); is_insertion |= *next_is_insertion; @@ -1279,20 +1287,6 @@ impl MultiBuffer { .map(|state| state.buffer.clone()) } - pub fn save(&mut self, cx: &mut ModelContext) -> Task> { - let mut save_tasks = Vec::new(); - for BufferState { buffer, .. } in self.buffers.borrow().values() { - save_tasks.push(buffer.update(cx, |buffer, cx| buffer.save(cx))); - } - - cx.spawn(|_, _| async move { - for save in save_tasks { - save.await?; - } - Ok(()) - }) - } - pub fn is_completion_trigger(&self, position: T, text: &str, cx: &AppContext) -> bool where T: ToOffset, @@ -2621,57 +2615,89 @@ impl MultiBufferSnapshot { self.parse_count } - pub fn enclosing_bracket_ranges( + /// Returns the smallest enclosing bracket ranges containing the given range or + /// None if no brackets contain range or the range is not contained in a single + /// excerpt + pub fn innermost_enclosing_bracket_ranges( &self, range: Range, ) -> Option<(Range, Range)> { let range = range.start.to_offset(self)..range.end.to_offset(self); - let mut cursor = self.excerpts.cursor::(); - cursor.seek(&range.start, Bias::Right, &()); - let start_excerpt = cursor.item(); + // Get the ranges of the innermost pair of brackets. + let mut result: Option<(Range, Range)> = None; - cursor.seek(&range.end, Bias::Right, &()); - let end_excerpt = cursor.item(); + let Some(enclosing_bracket_ranges) = self.enclosing_bracket_ranges(range.clone()) else { return None; }; - start_excerpt - .zip(end_excerpt) - .and_then(|(start_excerpt, end_excerpt)| { - if start_excerpt.id != end_excerpt.id { - return None; + for (open, close) in enclosing_bracket_ranges { + let len = close.end - open.start; + + if let Some((existing_open, existing_close)) = &result { + let existing_len = existing_close.end - existing_open.start; + if len > existing_len { + continue; } + } - let excerpt_buffer_start = start_excerpt - .range - .context - .start - .to_offset(&start_excerpt.buffer); - let excerpt_buffer_end = excerpt_buffer_start + start_excerpt.text_summary.len; + result = Some((open, close)); + } - let start_in_buffer = - excerpt_buffer_start + range.start.saturating_sub(*cursor.start()); - let end_in_buffer = - excerpt_buffer_start + range.end.saturating_sub(*cursor.start()); - let (mut start_bracket_range, mut end_bracket_range) = start_excerpt - .buffer - .enclosing_bracket_ranges(start_in_buffer..end_in_buffer)?; + result + } - if start_bracket_range.start >= excerpt_buffer_start - && end_bracket_range.end <= excerpt_buffer_end - { + /// Returns enclosing bracket ranges containing the given range or returns None if the range is + /// not contained in a single excerpt + pub fn enclosing_bracket_ranges<'a, T: ToOffset>( + &'a self, + range: Range, + ) -> Option, Range)> + 'a> { + let range = range.start.to_offset(self)..range.end.to_offset(self); + + self.bracket_ranges(range.clone()).map(|range_pairs| { + range_pairs + .filter(move |(open, close)| open.start <= range.start && close.end >= range.end) + }) + } + + /// Returns bracket range pairs overlapping the given `range` or returns None if the `range` is + /// not contained in a single excerpt + pub fn bracket_ranges<'a, T: ToOffset>( + &'a self, + range: Range, + ) -> Option, Range)> + 'a> { + let range = range.start.to_offset(self)..range.end.to_offset(self); + let excerpt = self.excerpt_containing(range.clone()); + excerpt.map(|(excerpt, excerpt_offset)| { + let excerpt_buffer_start = excerpt.range.context.start.to_offset(&excerpt.buffer); + let excerpt_buffer_end = excerpt_buffer_start + excerpt.text_summary.len; + + let start_in_buffer = excerpt_buffer_start + range.start.saturating_sub(excerpt_offset); + let end_in_buffer = excerpt_buffer_start + range.end.saturating_sub(excerpt_offset); + + excerpt + .buffer + .bracket_ranges(start_in_buffer..end_in_buffer) + .filter_map(move |(start_bracket_range, end_bracket_range)| { + if start_bracket_range.start < excerpt_buffer_start + || end_bracket_range.end > excerpt_buffer_end + { + return None; + } + + let mut start_bracket_range = start_bracket_range.clone(); start_bracket_range.start = - cursor.start() + (start_bracket_range.start - excerpt_buffer_start); + excerpt_offset + (start_bracket_range.start - excerpt_buffer_start); start_bracket_range.end = - cursor.start() + (start_bracket_range.end - excerpt_buffer_start); + excerpt_offset + (start_bracket_range.end - excerpt_buffer_start); + + let mut end_bracket_range = end_bracket_range.clone(); end_bracket_range.start = - cursor.start() + (end_bracket_range.start - excerpt_buffer_start); + excerpt_offset + (end_bracket_range.start - excerpt_buffer_start); end_bracket_range.end = - cursor.start() + (end_bracket_range.end - excerpt_buffer_start); + excerpt_offset + (end_bracket_range.end - excerpt_buffer_start); Some((start_bracket_range, end_bracket_range)) - } else { - None - } - }) + }) + }) } pub fn diagnostics_update_count(&self) -> usize { @@ -2812,40 +2838,23 @@ impl MultiBufferSnapshot { pub fn range_for_syntax_ancestor(&self, range: Range) -> Option> { let range = range.start.to_offset(self)..range.end.to_offset(self); - let mut cursor = self.excerpts.cursor::(); - cursor.seek(&range.start, Bias::Right, &()); - let start_excerpt = cursor.item(); - - cursor.seek(&range.end, Bias::Right, &()); - let end_excerpt = cursor.item(); - - start_excerpt - .zip(end_excerpt) - .and_then(|(start_excerpt, end_excerpt)| { - if start_excerpt.id != end_excerpt.id { - return None; - } - - let excerpt_buffer_start = start_excerpt - .range - .context - .start - .to_offset(&start_excerpt.buffer); - let excerpt_buffer_end = excerpt_buffer_start + start_excerpt.text_summary.len; + self.excerpt_containing(range.clone()) + .and_then(|(excerpt, excerpt_offset)| { + let excerpt_buffer_start = excerpt.range.context.start.to_offset(&excerpt.buffer); + let excerpt_buffer_end = excerpt_buffer_start + excerpt.text_summary.len; let start_in_buffer = - excerpt_buffer_start + range.start.saturating_sub(*cursor.start()); - let end_in_buffer = - excerpt_buffer_start + range.end.saturating_sub(*cursor.start()); - let mut ancestor_buffer_range = start_excerpt + excerpt_buffer_start + range.start.saturating_sub(excerpt_offset); + let end_in_buffer = excerpt_buffer_start + range.end.saturating_sub(excerpt_offset); + let mut ancestor_buffer_range = excerpt .buffer .range_for_syntax_ancestor(start_in_buffer..end_in_buffer)?; ancestor_buffer_range.start = cmp::max(ancestor_buffer_range.start, excerpt_buffer_start); ancestor_buffer_range.end = cmp::min(ancestor_buffer_range.end, excerpt_buffer_end); - let start = cursor.start() + (ancestor_buffer_range.start - excerpt_buffer_start); - let end = cursor.start() + (ancestor_buffer_range.end - excerpt_buffer_start); + let start = excerpt_offset + (ancestor_buffer_range.start - excerpt_buffer_start); + let end = excerpt_offset + (ancestor_buffer_range.end - excerpt_buffer_start); Some(start..end) }) } @@ -2929,6 +2938,35 @@ impl MultiBufferSnapshot { None } + /// Returns the excerpt containing range and its offset start within the multibuffer or none if `range` spans multiple excerpts + fn excerpt_containing<'a, T: ToOffset>( + &'a self, + range: Range, + ) -> Option<(&'a Excerpt, usize)> { + let range = range.start.to_offset(self)..range.end.to_offset(self); + + let mut cursor = self.excerpts.cursor::(); + cursor.seek(&range.start, Bias::Right, &()); + let start_excerpt = cursor.item(); + + if range.start == range.end { + return start_excerpt.map(|excerpt| (excerpt, *cursor.start())); + } + + cursor.seek(&range.end, Bias::Right, &()); + let end_excerpt = cursor.item(); + + start_excerpt + .zip(end_excerpt) + .and_then(|(start_excerpt, end_excerpt)| { + if start_excerpt.id != end_excerpt.id { + return None; + } + + Some((start_excerpt, *cursor.start())) + }) + } + pub fn remote_selections_in_range<'a>( &'a self, range: &'a Range, diff --git a/crates/editor/src/persistence.rs b/crates/editor/src/persistence.rs index 2d8d1a74fd..6e37735c13 100644 --- a/crates/editor/src/persistence.rs +++ b/crates/editor/src/persistence.rs @@ -6,7 +6,7 @@ use db::{define_connection, query}; use workspace::{ItemId, WorkspaceDb, WorkspaceId}; define_connection!( - // Current table shape using pseudo-rust syntax: + // Current schema shape using pseudo-rust syntax: // editors( // item_id: usize, // workspace_id: usize, diff --git a/crates/editor/src/selections_collection.rs b/crates/editor/src/selections_collection.rs index f1c19bca8a..f3ce89adc5 100644 --- a/crates/editor/src/selections_collection.rs +++ b/crates/editor/src/selections_collection.rs @@ -659,6 +659,31 @@ impl<'a> MutableSelectionsCollection<'a> { } } + pub fn move_offsets_with( + &mut self, + mut move_selection: impl FnMut(&MultiBufferSnapshot, &mut Selection), + ) { + let mut changed = false; + let snapshot = self.buffer().clone(); + let selections = self + .all::(self.cx) + .into_iter() + .map(|selection| { + let mut moved_selection = selection.clone(); + move_selection(&snapshot, &mut moved_selection); + if selection != moved_selection { + changed = true; + } + moved_selection + }) + .collect(); + drop(snapshot); + + if changed { + self.select(selections) + } + } + pub fn move_heads_with( &mut self, mut update_head: impl FnMut( diff --git a/crates/editor/src/test/editor_lsp_test_context.rs b/crates/editor/src/test/editor_lsp_test_context.rs index b65b09cf17..345709abf3 100644 --- a/crates/editor/src/test/editor_lsp_test_context.rs +++ b/crates/editor/src/test/editor_lsp_test_context.rs @@ -1,4 +1,5 @@ use std::{ + borrow::Cow, ops::{Deref, DerefMut, Range}, sync::Arc, }; @@ -7,7 +8,8 @@ use anyhow::Result; use futures::Future; use gpui::{json, ViewContext, ViewHandle}; -use language::{point_to_lsp, FakeLspAdapter, Language, LanguageConfig}; +use indoc::indoc; +use language::{point_to_lsp, FakeLspAdapter, Language, LanguageConfig, LanguageQueries}; use lsp::{notification, request}; use project::Project; use smol::stream::StreamExt; @@ -60,7 +62,7 @@ impl<'a> EditorLspTestContext<'a> { params .fs .as_fake() - .insert_tree("/root", json!({ "dir": { file_name: "" }})) + .insert_tree("/root", json!({ "dir": { file_name.clone(): "" }})) .await; let (window_id, workspace) = cx.add_window(|cx| { @@ -105,7 +107,7 @@ impl<'a> EditorLspTestContext<'a> { }, lsp, workspace, - buffer_lsp_url: lsp::Url::from_file_path("/root/dir/file.rs").unwrap(), + buffer_lsp_url: lsp::Url::from_file_path(format!("/root/dir/{file_name}")).unwrap(), } } @@ -120,7 +122,59 @@ impl<'a> EditorLspTestContext<'a> { ..Default::default() }, Some(tree_sitter_rust::language()), - ); + ) + .with_queries(LanguageQueries { + indents: Some(Cow::from(indoc! {r#" + [ + ((where_clause) _ @end) + (field_expression) + (call_expression) + (assignment_expression) + (let_declaration) + (let_chain) + (await_expression) + ] @indent + + (_ "[" "]" @end) @indent + (_ "<" ">" @end) @indent + (_ "{" "}" @end) @indent + (_ "(" ")" @end) @indent"#})), + brackets: Some(Cow::from(indoc! {r#" + ("(" @open ")" @close) + ("[" @open "]" @close) + ("{" @open "}" @close) + ("<" @open ">" @close) + ("\"" @open "\"" @close) + (closure_parameters "|" @open "|" @close)"#})), + ..Default::default() + }) + .expect("Could not parse queries"); + + Self::new(language, capabilities, cx).await + } + + pub async fn new_typescript( + capabilities: lsp::ServerCapabilities, + cx: &'a mut gpui::TestAppContext, + ) -> EditorLspTestContext<'a> { + let language = Language::new( + LanguageConfig { + name: "Typescript".into(), + path_suffixes: vec!["ts".to_string()], + ..Default::default() + }, + Some(tree_sitter_typescript::language_typescript()), + ) + .with_queries(LanguageQueries { + brackets: Some(Cow::from(indoc! {r#" + ("(" @open ")" @close) + ("[" @open "]" @close) + ("{" @open "}" @close) + ("<" @open ">" @close) + ("\"" @open "\"" @close)"#})), + ..Default::default() + }) + .expect("Could not parse queries"); Self::new(language, capabilities, cx).await } diff --git a/crates/editor/src/test/editor_test_context.rs b/crates/editor/src/test/editor_test_context.rs index d8dbfee171..8f8647e88d 100644 --- a/crates/editor/src/test/editor_test_context.rs +++ b/crates/editor/src/test/editor_test_context.rs @@ -162,10 +162,13 @@ impl<'a> EditorTestContext<'a> { /// embedded range markers that represent the ranges and directions of /// each selection. /// + /// Returns a context handle so that assertion failures can print what + /// editor state was needed to cause the failure. + /// /// See the `util::test::marked_text_ranges` function for more information. pub fn set_state(&mut self, marked_text: &str) -> ContextHandle { let _state_context = self.add_assertion_context(format!( - "Editor State: \"{}\"", + "Initial Editor State: \"{}\"", marked_text.escape_debug().to_string() )); let (unmarked_text, selection_ranges) = marked_text_ranges(marked_text, true); diff --git a/crates/feedback/src/deploy_feedback_button.rs b/crates/feedback/src/deploy_feedback_button.rs new file mode 100644 index 0000000000..e2d663f075 --- /dev/null +++ b/crates/feedback/src/deploy_feedback_button.rs @@ -0,0 +1,44 @@ +use gpui::{ + elements::{MouseEventHandler, ParentElement, Stack, Text}, + CursorStyle, Element, ElementBox, Entity, MouseButton, RenderContext, View, ViewContext, +}; +use settings::Settings; +use workspace::{item::ItemHandle, StatusItemView}; + +use crate::feedback_editor::GiveFeedback; + +pub struct DeployFeedbackButton; + +impl Entity for DeployFeedbackButton { + type Event = (); +} + +impl View for DeployFeedbackButton { + fn ui_name() -> &'static str { + "DeployFeedbackButton" + } + + fn render(&mut self, cx: &mut RenderContext<'_, Self>) -> ElementBox { + Stack::new() + .with_child( + MouseEventHandler::::new(0, cx, |state, cx| { + let theme = &cx.global::().theme; + let theme = &theme.workspace.status_bar.feedback; + + Text::new( + "Give Feedback".to_string(), + theme.style_for(state, true).clone(), + ) + .boxed() + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, |_, cx| cx.dispatch_action(GiveFeedback)) + .boxed(), + ) + .boxed() + } +} + +impl StatusItemView for DeployFeedbackButton { + fn set_active_pane_item(&mut self, _: Option<&dyn ItemHandle>, _: &mut ViewContext) {} +} diff --git a/crates/feedback/src/feedback.rs b/crates/feedback/src/feedback.rs index 4b0dfc4df9..f95f24f557 100644 --- a/crates/feedback/src/feedback.rs +++ b/crates/feedback/src/feedback.rs @@ -1,11 +1,15 @@ +pub mod deploy_feedback_button; +pub mod feedback_editor; +pub mod feedback_info_text; +pub mod submit_feedback_button; + use std::sync::Arc; -pub mod feedback_editor; mod system_specs; -use gpui::{actions, impl_actions, ClipboardItem, ViewContext}; +use gpui::{actions, impl_actions, ClipboardItem, MutableAppContext, PromptLevel, ViewContext}; use serde::Deserialize; use system_specs::SystemSpecs; -use workspace::Workspace; +use workspace::{AppState, Workspace}; #[derive(Deserialize, Clone, PartialEq)] pub struct OpenBrowser { @@ -16,30 +20,39 @@ impl_actions!(zed, [OpenBrowser]); actions!( zed, - [CopySystemSpecsIntoClipboard, FileBugReport, RequestFeature,] + [CopySystemSpecsIntoClipboard, FileBugReport, RequestFeature] ); -pub fn init(cx: &mut gpui::MutableAppContext) { - feedback_editor::init(cx); +pub fn init(app_state: Arc, cx: &mut MutableAppContext) { + let system_specs = SystemSpecs::new(&cx); + let system_specs_text = system_specs.to_string(); + + feedback_editor::init(system_specs, app_state, cx); cx.add_global_action(move |action: &OpenBrowser, cx| cx.platform().open_url(&action.url)); + let url = format!( + "https://github.com/zed-industries/community/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml&environment={}", + urlencoding::encode(&system_specs_text) + ); + cx.add_action( - |_: &mut Workspace, _: &CopySystemSpecsIntoClipboard, cx: &mut ViewContext| { - let system_specs = SystemSpecs::new(cx).to_string(); - let item = ClipboardItem::new(system_specs.clone()); + move |_: &mut Workspace, + _: &CopySystemSpecsIntoClipboard, + cx: &mut ViewContext| { cx.prompt( - gpui::PromptLevel::Info, - &format!("Copied into clipboard:\n\n{system_specs}"), + PromptLevel::Info, + &format!("Copied into clipboard:\n\n{system_specs_text}"), &["OK"], ); + let item = ClipboardItem::new(system_specs_text.clone()); cx.write_to_clipboard(item); }, ); cx.add_action( |_: &mut Workspace, _: &RequestFeature, cx: &mut ViewContext| { - let url = "https://github.com/zed-industries/feedback/issues/new?assignees=&labels=enhancement%2Ctriage&template=0_feature_request.yml"; + let url = "https://github.com/zed-industries/community/issues/new?assignees=&labels=enhancement%2Ctriage&template=0_feature_request.yml"; cx.dispatch_action(OpenBrowser { url: url.into(), }); @@ -47,14 +60,9 @@ pub fn init(cx: &mut gpui::MutableAppContext) { ); cx.add_action( - |_: &mut Workspace, _: &FileBugReport, cx: &mut ViewContext| { - let system_specs_text = SystemSpecs::new(cx).to_string(); - let url = format!( - "https://github.com/zed-industries/feedback/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml&environment={}", - urlencoding::encode(&system_specs_text) - ); + move |_: &mut Workspace, _: &FileBugReport, cx: &mut ViewContext| { cx.dispatch_action(OpenBrowser { - url: url.into(), + url: url.clone().into(), }); }, ); diff --git a/crates/feedback/src/feedback_editor.rs b/crates/feedback/src/feedback_editor.rs index 8185fbad9a..bcef9d0af5 100644 --- a/crates/feedback/src/feedback_editor.rs +++ b/crates/feedback/src/feedback_editor.rs @@ -1,91 +1,57 @@ -use std::{ops::Range, sync::Arc}; +use std::{ + any::TypeId, + ops::{Range, RangeInclusive}, + sync::Arc, +}; use anyhow::bail; -use client::{Client, ZED_SECRET_CLIENT_TOKEN}; +use client::{Client, ZED_SECRET_CLIENT_TOKEN, ZED_SERVER_URL}; use editor::{Anchor, Editor}; use futures::AsyncReadExt; use gpui::{ actions, - elements::{ChildView, Flex, Label, MouseEventHandler, ParentElement, Stack, Text}, - serde_json, AnyViewHandle, AppContext, CursorStyle, Element, ElementBox, Entity, ModelHandle, - MouseButton, MutableAppContext, PromptLevel, RenderContext, Task, View, ViewContext, - ViewHandle, + elements::{ChildView, Flex, Label, ParentElement}, + serde_json, AnyViewHandle, AppContext, Element, ElementBox, Entity, ModelHandle, + MutableAppContext, PromptLevel, RenderContext, Task, View, ViewContext, ViewHandle, + WeakViewHandle, }; use isahc::Request; use language::Buffer; use postage::prelude::Stream; -use lazy_static::lazy_static; use project::Project; use serde::Serialize; -use settings::Settings; use workspace::{ item::{Item, ItemHandle}, searchable::{SearchableItem, SearchableItemHandle}, - StatusItemView, Workspace, + smallvec::SmallVec, + AppState, Workspace, }; -use crate::system_specs::SystemSpecs; +use crate::{submit_feedback_button::SubmitFeedbackButton, system_specs::SystemSpecs}; -lazy_static! { - pub static ref ZED_SERVER_URL: String = - std::env::var("ZED_SERVER_URL").unwrap_or_else(|_| "https://zed.dev".to_string()); -} - -const FEEDBACK_CHAR_COUNT_RANGE: Range = Range { - start: 10, - end: 1000, -}; - -const FEEDBACK_PLACEHOLDER_TEXT: &str = "Thanks for spending time with Zed. Enter your feedback here as Markdown. Save the tab to submit your feedback."; +const FEEDBACK_CHAR_LIMIT: RangeInclusive = 10..=5000; const FEEDBACK_SUBMISSION_ERROR_TEXT: &str = "Feedback failed to submit, see error log for details."; -actions!(feedback, [SubmitFeedback, GiveFeedback, DeployFeedback]); +actions!(feedback, [GiveFeedback, SubmitFeedback]); -pub fn init(cx: &mut MutableAppContext) { - cx.add_action(FeedbackEditor::deploy); -} +pub fn init(system_specs: SystemSpecs, app_state: Arc, cx: &mut MutableAppContext) { + cx.add_action({ + move |workspace: &mut Workspace, _: &GiveFeedback, cx: &mut ViewContext| { + FeedbackEditor::deploy(system_specs.clone(), workspace, app_state.clone(), cx); + } + }); -pub struct FeedbackButton; - -impl Entity for FeedbackButton { - type Event = (); -} - -impl View for FeedbackButton { - fn ui_name() -> &'static str { - "FeedbackButton" - } - - fn render(&mut self, cx: &mut RenderContext<'_, Self>) -> ElementBox { - Stack::new() - .with_child( - MouseEventHandler::::new(0, cx, |state, cx| { - let theme = &cx.global::().theme; - let theme = &theme.workspace.status_bar.feedback; - - Text::new( - "Give Feedback".to_string(), - theme.style_for(state, true).clone(), - ) - .boxed() - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, |_, cx| cx.dispatch_action(GiveFeedback)) - .boxed(), - ) - .boxed() - } -} - -impl StatusItemView for FeedbackButton { - fn set_active_pane_item( - &mut self, - _: Option<&dyn ItemHandle>, - _: &mut gpui::ViewContext, - ) { - } + cx.add_async_action( + |submit_feedback_button: &mut SubmitFeedbackButton, _: &SubmitFeedback, cx| { + if let Some(active_item) = submit_feedback_button.active_item.as_ref() { + Some(active_item.update(cx, |feedback_editor, cx| feedback_editor.handle_save(cx))) + } else { + None + } + }, + ); } #[derive(Serialize)] @@ -93,17 +59,20 @@ struct FeedbackRequestBody<'a> { feedback_text: &'a str, metrics_id: Option>, system_specs: SystemSpecs, + is_staff: bool, token: &'a str, } #[derive(Clone)] -struct FeedbackEditor { +pub(crate) struct FeedbackEditor { + system_specs: SystemSpecs, editor: ViewHandle, project: ModelHandle, } impl FeedbackEditor { - fn new_with_buffer( + fn new( + system_specs: SystemSpecs, project: ModelHandle, buffer: ModelHandle, cx: &mut ViewContext, @@ -111,46 +80,40 @@ impl FeedbackEditor { let editor = cx.add_view(|cx| { let mut editor = Editor::for_buffer(buffer, Some(project.clone()), cx); editor.set_vertical_scroll_margin(5, cx); - editor.set_placeholder_text(FEEDBACK_PLACEHOLDER_TEXT, cx); editor }); cx.subscribe(&editor, |_, _, e, cx| cx.emit(e.clone())) .detach(); - let this = Self { editor, project }; - this + Self { + system_specs: system_specs.clone(), + editor, + project, + } } - fn new(project: ModelHandle, cx: &mut ViewContext) -> Self { - let markdown_language = project.read(cx).languages().get_language("Markdown"); + fn handle_save(&mut self, cx: &mut ViewContext) -> Task> { + let feedback_text = self.editor.read(cx).text(cx); + let feedback_char_count = feedback_text.chars().count(); + let feedback_text = feedback_text.trim().to_string(); - let buffer = project - .update(cx, |project, cx| { - project.create_buffer("", markdown_language, cx) - }) - .expect("creating buffers on a local workspace always succeeds"); - - Self::new_with_buffer(project, buffer, cx) - } - - fn handle_save( - &mut self, - _: gpui::ModelHandle, - cx: &mut ViewContext, - ) -> Task> { - let feedback_text_length = self.editor.read(cx).buffer().read(cx).len(cx); - - if feedback_text_length <= FEEDBACK_CHAR_COUNT_RANGE.start { - cx.prompt( - PromptLevel::Critical, - &format!( - "Feedback must be longer than {} characters", - FEEDBACK_CHAR_COUNT_RANGE.start - ), - &["OK"], - ); + let error = if feedback_char_count < *FEEDBACK_CHAR_LIMIT.start() { + Some(format!( + "Feedback can't be shorter than {} characters.", + FEEDBACK_CHAR_LIMIT.start() + )) + } else if feedback_char_count > *FEEDBACK_CHAR_LIMIT.end() { + Some(format!( + "Feedback can't be longer than {} characters.", + FEEDBACK_CHAR_LIMIT.end() + )) + } else { + None + }; + if let Some(error) = error { + cx.prompt(PromptLevel::Critical, &error, &["OK"]); return Task::ready(Ok(())); } @@ -162,8 +125,7 @@ impl FeedbackEditor { let this = cx.handle(); let client = cx.global::>().clone(); - let feedback_text = self.editor.read(cx).text(cx); - let specs = SystemSpecs::new(cx); + let specs = self.system_specs.clone(); cx.spawn(|_, mut cx| async move { let answer = answer.recv().await; @@ -206,12 +168,14 @@ impl FeedbackEditor { let feedback_endpoint = format!("{}/api/feedback", *ZED_SERVER_URL); let metrics_id = zed_client.metrics_id(); + let is_staff = zed_client.is_staff(); let http_client = zed_client.http_client(); let request = FeedbackRequestBody { feedback_text: &feedback_text, metrics_id, system_specs, + is_staff: is_staff.unwrap_or(false), token: ZED_SECRET_CLIENT_TOKEN, }; @@ -236,10 +200,26 @@ impl FeedbackEditor { } impl FeedbackEditor { - pub fn deploy(workspace: &mut Workspace, _: &GiveFeedback, cx: &mut ViewContext) { - let feedback_editor = - cx.add_view(|cx| FeedbackEditor::new(workspace.project().clone(), cx)); - workspace.add_item(Box::new(feedback_editor), cx); + pub fn deploy( + system_specs: SystemSpecs, + workspace: &mut Workspace, + app_state: Arc, + cx: &mut ViewContext, + ) { + workspace + .with_local_workspace(&app_state, cx, |workspace, cx| { + let project = workspace.project().clone(); + let markdown_language = project.read(cx).languages().language_for_name("Markdown"); + let buffer = project + .update(cx, |project, cx| { + project.create_buffer("", markdown_language, cx) + }) + .expect("creating buffers on a local workspace always succeeds"); + let feedback_editor = + cx.add_view(|cx| FeedbackEditor::new(system_specs, project, buffer, cx)); + workspace.add_item(Box::new(feedback_editor), cx); + }) + .detach(); } } @@ -264,12 +244,7 @@ impl Entity for FeedbackEditor { } impl Item for FeedbackEditor { - fn tab_content( - &self, - _: Option, - style: &theme::Tab, - _: &gpui::AppContext, - ) -> ElementBox { + fn tab_content(&self, _: Option, style: &theme::Tab, _: &AppContext) -> ElementBox { Flex::row() .with_child( Label::new("Feedback".to_string(), style.label.clone()) @@ -284,40 +259,40 @@ impl Item for FeedbackEditor { self.editor.for_each_project_item(cx, f) } - fn to_item_events(_: &Self::Event) -> Vec { - Vec::new() + fn to_item_events(_: &Self::Event) -> SmallVec<[workspace::item::ItemEvent; 2]> { + SmallVec::new() } - fn is_singleton(&self, _: &gpui::AppContext) -> bool { + fn is_singleton(&self, _: &AppContext) -> bool { true } fn set_nav_history(&mut self, _: workspace::ItemNavHistory, _: &mut ViewContext) {} - fn can_save(&self, _: &gpui::AppContext) -> bool { + fn can_save(&self, _: &AppContext) -> bool { true } fn save( &mut self, - project: gpui::ModelHandle, + _: ModelHandle, cx: &mut ViewContext, ) -> Task> { - self.handle_save(project, cx) + self.handle_save(cx) } fn save_as( &mut self, - project: gpui::ModelHandle, + _: ModelHandle, _: std::path::PathBuf, cx: &mut ViewContext, ) -> Task> { - self.handle_save(project, cx) + self.handle_save(cx) } fn reload( &mut self, - _: gpui::ModelHandle, + _: ModelHandle, _: &mut ViewContext, ) -> Task> { unreachable!("reload should not have been called") @@ -339,7 +314,8 @@ impl Item for FeedbackEditor { .as_singleton() .expect("Feedback buffer is only ever singleton"); - Some(Self::new_with_buffer( + Some(Self::new( + self.system_specs.clone(), self.project.clone(), buffer.clone(), cx, @@ -351,8 +327,8 @@ impl Item for FeedbackEditor { } fn deserialize( - _: gpui::ModelHandle, - _: gpui::WeakViewHandle, + _: ModelHandle, + _: WeakViewHandle, _: workspace::WorkspaceId, _: workspace::ItemId, _: &mut ViewContext, @@ -363,6 +339,21 @@ impl Item for FeedbackEditor { fn as_searchable(&self, handle: &ViewHandle) -> Option> { Some(Box::new(handle.clone())) } + + fn act_as_type( + &self, + type_id: TypeId, + self_handle: &ViewHandle, + _: &AppContext, + ) -> Option { + if type_id == TypeId::of::() { + Some(self_handle.into()) + } else if type_id == TypeId::of::() { + Some((&self.editor).into()) + } else { + None + } + } } impl SearchableItem for FeedbackEditor { diff --git a/crates/feedback/src/feedback_info_text.rs b/crates/feedback/src/feedback_info_text.rs new file mode 100644 index 0000000000..bfe67ec4ae --- /dev/null +++ b/crates/feedback/src/feedback_info_text.rs @@ -0,0 +1,60 @@ +use gpui::{ + elements::Label, Element, ElementBox, Entity, RenderContext, View, ViewContext, ViewHandle, +}; +use settings::Settings; +use workspace::{item::ItemHandle, ToolbarItemLocation, ToolbarItemView}; + +use crate::feedback_editor::FeedbackEditor; + +pub struct FeedbackInfoText { + active_item: Option>, +} + +impl FeedbackInfoText { + pub fn new() -> Self { + Self { + active_item: Default::default(), + } + } +} + +impl Entity for FeedbackInfoText { + type Event = (); +} + +impl View for FeedbackInfoText { + fn ui_name() -> &'static str { + "FeedbackInfoText" + } + + fn render(&mut self, cx: &mut RenderContext) -> ElementBox { + let theme = cx.global::().theme.clone(); + let text = "We read whatever you submit here. For issues and discussions, visit the community repo on GitHub."; + Label::new(text.to_string(), theme.feedback.info_text.text.clone()) + .contained() + .aligned() + .left() + .clipped() + .boxed() + } +} + +impl ToolbarItemView for FeedbackInfoText { + fn set_active_pane_item( + &mut self, + active_pane_item: Option<&dyn ItemHandle>, + cx: &mut ViewContext, + ) -> workspace::ToolbarItemLocation { + cx.notify(); + if let Some(feedback_editor) = active_pane_item.and_then(|i| i.downcast::()) + { + self.active_item = Some(feedback_editor); + ToolbarItemLocation::PrimaryLeft { + flex: Some((1., false)), + } + } else { + self.active_item = None; + ToolbarItemLocation::Hidden + } + } +} diff --git a/crates/feedback/src/submit_feedback_button.rs b/crates/feedback/src/submit_feedback_button.rs new file mode 100644 index 0000000000..470a53905e --- /dev/null +++ b/crates/feedback/src/submit_feedback_button.rs @@ -0,0 +1,76 @@ +use gpui::{ + elements::{Label, MouseEventHandler}, + CursorStyle, Element, ElementBox, Entity, MouseButton, RenderContext, View, ViewContext, + ViewHandle, +}; +use settings::Settings; +use workspace::{item::ItemHandle, ToolbarItemLocation, ToolbarItemView}; + +use crate::feedback_editor::{FeedbackEditor, SubmitFeedback}; + +pub struct SubmitFeedbackButton { + pub(crate) active_item: Option>, +} + +impl SubmitFeedbackButton { + pub fn new() -> Self { + Self { + active_item: Default::default(), + } + } +} + +impl Entity for SubmitFeedbackButton { + type Event = (); +} + +impl View for SubmitFeedbackButton { + fn ui_name() -> &'static str { + "SubmitFeedbackButton" + } + + fn render(&mut self, cx: &mut RenderContext) -> ElementBox { + let theme = cx.global::().theme.clone(); + enum SubmitFeedbackButton {} + MouseEventHandler::::new(0, cx, |state, _| { + let style = theme.feedback.submit_button.style_for(state, false); + Label::new("Submit as Markdown".into(), style.text.clone()) + .contained() + .with_style(style.container) + .boxed() + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, |_, cx| { + cx.dispatch_action(SubmitFeedback) + }) + .aligned() + .contained() + .with_margin_left(theme.feedback.button_margin) + .with_tooltip::( + 0, + "cmd-s".into(), + Some(Box::new(SubmitFeedback)), + theme.tooltip.clone(), + cx, + ) + .boxed() + } +} + +impl ToolbarItemView for SubmitFeedbackButton { + fn set_active_pane_item( + &mut self, + active_pane_item: Option<&dyn ItemHandle>, + cx: &mut ViewContext, + ) -> workspace::ToolbarItemLocation { + cx.notify(); + if let Some(feedback_editor) = active_pane_item.and_then(|i| i.downcast::()) + { + self.active_item = Some(feedback_editor); + ToolbarItemLocation::PrimaryRight { flex: None } + } else { + self.active_item = None; + ToolbarItemLocation::Hidden + } + } +} diff --git a/crates/feedback/src/system_specs.rs b/crates/feedback/src/system_specs.rs index 17e51a6815..f20561826e 100644 --- a/crates/feedback/src/system_specs.rs +++ b/crates/feedback/src/system_specs.rs @@ -1,14 +1,15 @@ -use std::{env, fmt::Display}; - -use gpui::AppContext; +use client::ZED_APP_VERSION; +use gpui::{AppContext, AppVersion}; use human_bytes::human_bytes; use serde::Serialize; +use std::{env, fmt::Display}; use sysinfo::{System, SystemExt}; use util::channel::ReleaseChannel; -#[derive(Debug, Serialize)] +#[derive(Clone, Debug, Serialize)] pub struct SystemSpecs { - app_version: &'static str, + #[serde(serialize_with = "serialize_app_version")] + app_version: Option, release_channel: &'static str, os_name: &'static str, os_version: Option, @@ -19,18 +20,24 @@ pub struct SystemSpecs { impl SystemSpecs { pub fn new(cx: &AppContext) -> Self { let platform = cx.platform(); + let app_version = ZED_APP_VERSION.or_else(|| platform.app_version().ok()); + let release_channel = cx.global::().dev_name(); + let os_name = platform.os_name(); let system = System::new_all(); + let memory = system.total_memory(); + let architecture = env::consts::ARCH; + let os_version = platform + .os_version() + .ok() + .map(|os_version| os_version.to_string()); SystemSpecs { - app_version: env!("CARGO_PKG_VERSION"), - release_channel: cx.global::().dev_name(), - os_name: platform.os_name(), - os_version: platform - .os_version() - .ok() - .map(|os_version| os_version.to_string()), - memory: system.total_memory(), - architecture: env::consts::ARCH, + app_version, + release_channel, + os_name, + os_version, + memory, + architecture, } } } @@ -41,14 +48,28 @@ impl Display for SystemSpecs { Some(os_version) => format!("OS: {} {}", self.os_name, os_version), None => format!("OS: {}", self.os_name), }; + let app_version_information = self + .app_version + .as_ref() + .map(|app_version| format!("Zed: v{} ({})", app_version, self.release_channel)); let system_specs = [ - format!("Zed: v{} ({})", self.app_version, self.release_channel), - os_information, - format!("Memory: {}", human_bytes(self.memory as f64)), - format!("Architecture: {}", self.architecture), + app_version_information, + Some(os_information), + Some(format!("Memory: {}", human_bytes(self.memory as f64))), + Some(format!("Architecture: {}", self.architecture)), ] + .into_iter() + .flatten() + .collect::>() .join("\n"); write!(f, "{system_specs}") } } + +fn serialize_app_version(version: &Option, serializer: S) -> Result +where + S: serde::Serializer, +{ + version.map(|v| v.to_string()).serialize(serializer) +} diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index 6a79953f41..f640f35036 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -13,7 +13,6 @@ use smol::io::{AsyncReadExt, AsyncWriteExt}; use std::borrow::Cow; use std::cmp; use std::io::Write; -use std::ops::Deref; use std::sync::Arc; use std::{ io, @@ -94,16 +93,6 @@ impl LineEnding { } } -pub struct HomeDir(pub PathBuf); - -impl Deref for HomeDir { - type Target = PathBuf; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - #[async_trait::async_trait] pub trait Fs: Send + Sync { async fn create_dir(&self, path: &Path) -> Result<()>; diff --git a/crates/gpui/Cargo.toml b/crates/gpui/Cargo.toml index e1b6e11b46..7be254be4d 100644 --- a/crates/gpui/Cargo.toml +++ b/crates/gpui/Cargo.toml @@ -47,6 +47,7 @@ smol = "1.2" time = { version = "0.3", features = ["serde", "serde-well-known"] } tiny-skia = "0.5" usvg = "0.14" +uuid = { version = "1.1.2", features = ["v4"] } waker-fn = "1.1.0" [build-dependencies] @@ -66,7 +67,7 @@ media = { path = "../media" } anyhow = "1" block = "0.1" cocoa = "0.24" -core-foundation = "0.9.3" +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" } diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index ad1fad85b1..4bdd775593 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -1,7 +1,10 @@ pub mod action; mod callback_collection; +mod menu; +pub(crate) mod ref_counts; #[cfg(any(test, feature = "test-support"))] pub mod test_app_context; +mod window_input_handler; use std::{ any::{type_name, Any, TypeId}, @@ -19,31 +22,38 @@ use std::{ }; use anyhow::{anyhow, Context, Result}; -use lazy_static::lazy_static; use parking_lot::Mutex; +use pathfinder_geometry::vector::Vector2F; use postage::oneshot; use smallvec::SmallVec; use smol::prelude::*; +use uuid::Uuid; pub use action::*; use callback_collection::CallbackCollection; use collections::{hash_map::Entry, HashMap, HashSet, VecDeque}; +pub use menu::*; use platform::Event; #[cfg(any(test, feature = "test-support"))] +use ref_counts::LeakDetector; +#[cfg(any(test, feature = "test-support"))] pub use test_app_context::{ContextHandle, TestAppContext}; +use window_input_handler::WindowInputHandler; use crate::{ elements::ElementBox, executor::{self, Task}, - geometry::rect::RectF, keymap_matcher::{self, Binding, KeymapContext, KeymapMatcher, Keystroke, MatchResult}, platform::{self, KeyDownEvent, Platform, PromptLevel, WindowOptions}, presenter::Presenter, util::post_inc, - Appearance, AssetCache, AssetSource, ClipboardItem, FontCache, InputHandler, KeyUpEvent, + Appearance, AssetCache, AssetSource, ClipboardItem, FontCache, KeyUpEvent, ModifiersChangedEvent, MouseButton, MouseRegionId, PathPromptOptions, TextLayoutCache, + WindowBounds, }; +use self::ref_counts::RefCounts; + pub trait Entity: 'static { type Event; @@ -171,36 +181,17 @@ pub trait UpdateView { T: View; } -pub struct Menu<'a> { - pub name: &'a str, - pub items: Vec>, -} - -pub enum MenuItem<'a> { - Separator, - Submenu(Menu<'a>), - Action { - name: &'a str, - action: Box, - }, -} - #[derive(Clone)] pub struct App(Rc>); #[derive(Clone)] pub struct AsyncAppContext(Rc>); -pub struct WindowInputHandler { - app: Rc>, - window_id: usize, -} - impl App { pub fn new(asset_source: impl AssetSource) -> Result { let platform = platform::current::platform(); - let foreground_platform = platform::current::foreground_platform(); let foreground = Rc::new(executor::Foreground::platform(platform.dispatcher())?); + let foreground_platform = platform::current::foreground_platform(foreground.clone()); let app = Self(Rc::new(RefCell::new(MutableAppContext::new( foreground, Arc::new(executor::Background::new()), @@ -217,33 +208,7 @@ impl App { cx.borrow_mut().quit(); } })); - foreground_platform.on_will_open_menu(Box::new({ - let cx = app.0.clone(); - move || { - let mut cx = cx.borrow_mut(); - cx.keystroke_matcher.clear_pending(); - } - })); - foreground_platform.on_validate_menu_command(Box::new({ - let cx = app.0.clone(); - move |action| { - let cx = cx.borrow_mut(); - !cx.keystroke_matcher.has_pending_keystrokes() && cx.is_action_available(action) - } - })); - foreground_platform.on_menu_command(Box::new({ - let cx = app.0.clone(); - move |action| { - let mut cx = cx.borrow_mut(); - if let Some(key_window_id) = cx.cx.platform.key_window_id() { - if let Some(view_id) = cx.focused_view_id(key_window_id) { - cx.handle_dispatch_action_from_effect(key_window_id, Some(view_id), action); - return; - } - } - cx.dispatch_global_action_any(action); - } - })); + setup_menu_handlers(foreground_platform.as_ref(), &app); app.0.borrow_mut().weak_self = Some(Rc::downgrade(&app.0)); Ok(app) @@ -346,94 +311,6 @@ impl App { } } -impl WindowInputHandler { - fn read_focused_view(&self, f: F) -> Option - where - F: FnOnce(&dyn AnyView, &AppContext) -> T, - { - // Input-related application hooks are sometimes called by the OS during - // a call to a window-manipulation API, like prompting the user for file - // paths. In that case, the AppContext will already be borrowed, so any - // InputHandler methods need to fail gracefully. - // - // See https://github.com/zed-industries/feedback/issues/444 - let app = self.app.try_borrow().ok()?; - - let view_id = app.focused_view_id(self.window_id)?; - let view = app.cx.views.get(&(self.window_id, view_id))?; - let result = f(view.as_ref(), &app); - Some(result) - } - - fn update_focused_view(&mut self, f: F) -> Option - where - F: FnOnce(usize, usize, &mut dyn AnyView, &mut MutableAppContext) -> T, - { - let mut app = self.app.try_borrow_mut().ok()?; - app.update(|app| { - let view_id = app.focused_view_id(self.window_id)?; - let mut view = app.cx.views.remove(&(self.window_id, view_id))?; - let result = f(self.window_id, view_id, view.as_mut(), &mut *app); - app.cx.views.insert((self.window_id, view_id), view); - Some(result) - }) - } -} - -impl InputHandler for WindowInputHandler { - fn text_for_range(&self, range: Range) -> Option { - self.read_focused_view(|view, cx| view.text_for_range(range.clone(), cx)) - .flatten() - } - - fn selected_text_range(&self) -> Option> { - self.read_focused_view(|view, cx| view.selected_text_range(cx)) - .flatten() - } - - fn replace_text_in_range(&mut self, range: Option>, text: &str) { - self.update_focused_view(|window_id, view_id, view, cx| { - view.replace_text_in_range(range, text, cx, window_id, view_id); - }); - } - - fn marked_text_range(&self) -> Option> { - self.read_focused_view(|view, cx| view.marked_text_range(cx)) - .flatten() - } - - fn unmark_text(&mut self) { - self.update_focused_view(|window_id, view_id, view, cx| { - view.unmark_text(cx, window_id, view_id); - }); - } - - fn replace_and_mark_text_in_range( - &mut self, - range: Option>, - new_text: &str, - new_selected_range: Option>, - ) { - self.update_focused_view(|window_id, view_id, view, cx| { - view.replace_and_mark_text_in_range( - range, - new_text, - new_selected_range, - cx, - window_id, - view_id, - ); - }); - } - - fn rect_for_range(&self, range_utf16: Range) -> Option { - let app = self.app.borrow(); - let (presenter, _) = app.presenters_and_platform_windows.get(&self.window_id)?; - let presenter = presenter.borrow(); - presenter.rect_for_text_range(range_utf16, &app) - } -} - impl AsyncAppContext { pub fn spawn(&self, f: F) -> Task where @@ -593,6 +470,7 @@ type ReleaseObservationCallback = Box; type WindowActivationCallback = Box bool>; type WindowFullscreenCallback = Box bool>; +type WindowBoundsCallback = Box bool>; type KeystrokeCallback = Box< dyn FnMut(&Keystroke, &MatchResult, Option<&Box>, &mut MutableAppContext) -> bool, >; @@ -623,6 +501,7 @@ pub struct MutableAppContext { action_dispatch_observations: CallbackCollection<(), ActionObservationCallback>, window_activation_observations: CallbackCollection, window_fullscreen_observations: CallbackCollection, + window_bounds_observations: CallbackCollection, keystroke_observations: CallbackCollection, #[allow(clippy::type_complexity)] @@ -680,6 +559,7 @@ impl MutableAppContext { global_observations: Default::default(), window_activation_observations: Default::default(), window_fullscreen_observations: Default::default(), + window_bounds_observations: Default::default(), keystroke_observations: Default::default(), action_dispatch_observations: Default::default(), presenters_and_platform_windows: Default::default(), @@ -865,8 +745,16 @@ impl MutableAppContext { } } + pub fn is_topmost_window_for_position(&self, window_id: usize, position: Vector2F) -> bool { + self.presenters_and_platform_windows + .get(&window_id) + .map_or(false, |(_, window)| { + window.is_topmost_for_position(position) + }) + } + pub fn window_ids(&self) -> impl Iterator + '_ { - self.cx.windows.keys().cloned() + self.cx.windows.keys().copied() } pub fn activate_window(&self, window_id: usize) { @@ -896,8 +784,14 @@ impl MutableAppContext { .map_or(false, |window| window.is_fullscreen) } - pub fn window_bounds(&self, window_id: usize) -> RectF { - self.presenters_and_platform_windows[&window_id].1.bounds() + pub fn window_bounds(&self, window_id: usize) -> Option { + let (_, window) = self.presenters_and_platform_windows.get(&window_id)?; + Some(window.bounds()) + } + + pub fn window_display_uuid(&self, window_id: usize) -> Option { + let (_, window) = self.presenters_and_platform_windows.get(&window_id)?; + window.screen().display_uuid() } pub fn render_view(&mut self, params: RenderParams) -> Result { @@ -964,11 +858,6 @@ impl MutableAppContext { result } - pub fn set_menus(&mut self, menus: Vec) { - self.foreground_platform - .set_menus(menus, &self.keystroke_matcher); - } - fn show_character_palette(&self, window_id: usize) { let (_, window) = &self.presenters_and_platform_windows[&window_id]; window.show_character_palette(); @@ -1011,6 +900,10 @@ impl MutableAppContext { self.foreground_platform.prompt_for_new_path(directory) } + pub fn reveal_path(&self, path: &Path) { + self.foreground_platform.reveal_path(path) + } + pub fn emit_global(&mut self, payload: E) { self.pending_effects.push_back(Effect::GlobalEvent { payload: Box::new(payload), @@ -1231,6 +1124,23 @@ impl MutableAppContext { ) } + fn observe_window_bounds(&mut self, window_id: usize, callback: F) -> Subscription + where + F: 'static + FnMut(WindowBounds, Uuid, &mut MutableAppContext) -> bool, + { + let subscription_id = post_inc(&mut self.next_subscription_id); + self.pending_effects + .push_back(Effect::WindowBoundsObservation { + window_id, + subscription_id, + callback: Box::new(callback), + }); + Subscription::WindowBoundsObservation( + self.window_bounds_observations + .subscribe(window_id, subscription_id), + ) + } + pub fn observe_keystrokes(&mut self, window_id: usize, callback: F) -> Subscription where F: 'static @@ -1295,6 +1205,31 @@ impl MutableAppContext { self.action_deserializers.keys().copied() } + /// Return keystrokes that would dispatch the given action on the given view. + pub(crate) fn keystrokes_for_action( + &mut self, + window_id: usize, + view_id: usize, + action: &dyn Action, + ) -> Option> { + let mut contexts = Vec::new(); + for view_id in self.ancestors(window_id, view_id) { + if let Some(view) = self.views.get(&(window_id, view_id)) { + contexts.push(view.keymap_context(self)); + } + } + + self.keystroke_matcher + .bindings_for_action_type(action.as_any().type_id()) + .find_map(|b| { + if b.match_context(&contexts) { + Some(b.keystrokes().into()) + } else { + None + } + }) + } + pub fn available_actions( &self, window_id: usize, @@ -1302,8 +1237,10 @@ impl MutableAppContext { ) -> impl Iterator, SmallVec<[&Binding; 1]>)> { let mut action_types: HashSet<_> = self.global_actions.keys().copied().collect(); + let mut contexts = Vec::new(); for view_id in self.ancestors(window_id, view_id) { if let Some(view) = self.views.get(&(window_id, view_id)) { + contexts.push(view.keymap_context(self)); let view_type = view.as_any().type_id(); if let Some(actions) = self.actions.get(&view_type) { action_types.extend(actions.keys().copied()); @@ -1320,6 +1257,7 @@ impl MutableAppContext { deserialize("{}").ok()?, self.keystroke_matcher .bindings_for_action_type(*type_id) + .filter(|b| b.match_context(&contexts)) .collect(), )) } else { @@ -1347,34 +1285,6 @@ impl MutableAppContext { self.global_actions.contains_key(&action_type) } - /// Return keystrokes that would dispatch the given action closest to the focused view, if there are any. - pub(crate) fn keystrokes_for_action( - &mut self, - window_id: usize, - view_stack: &[usize], - action: &dyn Action, - ) -> Option> { - self.keystroke_matcher.contexts.clear(); - for view_id in view_stack.iter().rev() { - let view = self - .cx - .views - .get(&(window_id, *view_id)) - .expect("view in responder chain does not exist"); - self.keystroke_matcher - .contexts - .push(view.keymap_context(self.as_ref())); - let keystrokes = self - .keystroke_matcher - .keystrokes_for_action(action, &self.keystroke_matcher.contexts); - if keystrokes.is_some() { - return keystrokes; - } - } - - None - } - // Traverses the parent tree. Walks down the tree toward the passed // view calling visit with true. Then walks back up the tree calling visit with false. // If `visit` returns false this function will immediately return. @@ -1405,21 +1315,6 @@ impl MutableAppContext { true } - /// Returns an iterator over all of the view ids from the passed view up to the root of the window - /// Includes the passed view itself - fn ancestors(&self, window_id: usize, mut view_id: usize) -> impl Iterator + '_ { - std::iter::once(view_id) - .into_iter() - .chain(std::iter::from_fn(move || { - if let Some(ParentId::View(parent_id)) = self.parents.get(&(window_id, view_id)) { - view_id = *parent_id; - Some(view_id) - } else { - None - } - })) - } - fn actions_mut( &mut self, capture_phase: bool, @@ -1765,6 +1660,13 @@ impl MutableAppContext { })); } + { + let mut app = self.upgrade(); + window.on_moved(Box::new(move || { + app.update(|cx| cx.window_was_moved(window_id)) + })); + } + { let mut app = self.upgrade(); window.on_fullscreen(Box::new(move |is_fullscreen| { @@ -1886,10 +1788,11 @@ impl MutableAppContext { { self.update(|this| { let view_id = post_inc(&mut this.next_entity_id); + // Make sure we can tell child views about their parent + this.cx.parents.insert((window_id, view_id), parent_id); let mut cx = ViewContext::new(this, window_id, view_id); let handle = if let Some(view) = build_view(&mut cx) { this.cx.views.insert((window_id, view_id), Box::new(view)); - this.cx.parents.insert((window_id, view_id), parent_id); if let Some(window) = this.cx.windows.get_mut(&window_id) { window .invalidation @@ -1899,6 +1802,7 @@ impl MutableAppContext { } Some(ViewHandle::new(window_id, view_id, &this.cx.ref_counts)) } else { + this.cx.parents.remove(&(window_id, view_id)); None }; handle @@ -2062,6 +1966,11 @@ impl MutableAppContext { .invalidation .get_or_insert(WindowInvalidation::default()); } + self.handle_window_moved(window_id); + } + + Effect::MoveWindow { window_id } => { + self.handle_window_moved(window_id); } Effect::WindowActivationObservation { @@ -2094,6 +2003,16 @@ impl MutableAppContext { is_fullscreen, } => self.handle_fullscreen_effect(window_id, is_fullscreen), + Effect::WindowBoundsObservation { + window_id, + subscription_id, + callback, + } => self.window_bounds_observations.add_callback( + window_id, + subscription_id, + callback, + ), + Effect::RefreshWindows => { refreshing = true; } @@ -2188,6 +2107,11 @@ impl MutableAppContext { .push_back(Effect::ResizeWindow { window_id }); } + fn window_was_moved(&mut self, window_id: usize) { + self.pending_effects + .push_back(Effect::MoveWindow { window_id }); + } + fn window_was_fullscreen_changed(&mut self, window_id: usize, is_fullscreen: bool) { self.pending_effects.push_back(Effect::FullscreenWindow { window_id, @@ -2320,11 +2244,21 @@ impl MutableAppContext { let window = this.cx.windows.get_mut(&window_id)?; window.is_fullscreen = is_fullscreen; - let mut observations = this.window_fullscreen_observations.clone(); - observations.emit(window_id, this, |callback, this| { + let mut fullscreen_observations = this.window_fullscreen_observations.clone(); + fullscreen_observations.emit(window_id, this, |callback, this| { callback(is_fullscreen, this) }); + if let Some((uuid, bounds)) = this + .window_display_uuid(window_id) + .zip(this.window_bounds(window_id)) + { + let mut bounds_observations = this.window_bounds_observations.clone(); + bounds_observations.emit(window_id, this, |callback, this| { + callback(bounds, uuid, this) + }); + } + Some(()) }); } @@ -2501,6 +2435,20 @@ impl MutableAppContext { } } + fn handle_window_moved(&mut self, window_id: usize) { + if let Some((display, bounds)) = self + .window_display_uuid(window_id) + .zip(self.window_bounds(window_id)) + { + self.window_bounds_observations + .clone() + .emit(window_id, self, move |callback, this| { + callback(bounds, display, this); + true + }); + } + } + pub fn focus(&mut self, window_id: usize, view_id: Option) { self.pending_effects .push_back(Effect::Focus { window_id, view_id }); @@ -2724,6 +2672,42 @@ impl AppContext { panic!("no global has been added for {}", type_name::()); } } + + /// Returns an iterator over all of the view ids from the passed view up to the root of the window + /// Includes the passed view itself + fn ancestors(&self, window_id: usize, mut view_id: usize) -> impl Iterator + '_ { + std::iter::once(view_id) + .into_iter() + .chain(std::iter::from_fn(move || { + if let Some(ParentId::View(parent_id)) = self.parents.get(&(window_id, view_id)) { + view_id = *parent_id; + Some(view_id) + } else { + None + } + })) + } + + /// Returns the id of the parent of the given view, or none if the given + /// view is the root. + fn parent(&self, window_id: usize, view_id: usize) -> Option { + if let Some(ParentId::View(view_id)) = self.parents.get(&(window_id, view_id)) { + Some(*view_id) + } else { + None + } + } + + pub fn is_child_focused(&self, view: impl Into) -> bool { + let view = view.into(); + if let Some(focused_view_id) = self.focused_view_id(view.window_id) { + self.ancestors(view.window_id, focused_view_id) + .skip(1) // Skip self id + .any(|parent| parent == view.view_id) + } else { + false + } + } } impl ReadModel for AppContext { @@ -2878,9 +2862,8 @@ pub enum Effect { ResizeWindow { window_id: usize, }, - FullscreenWindow { + MoveWindow { window_id: usize, - is_fullscreen: bool, }, ActivateWindow { window_id: usize, @@ -2891,11 +2874,20 @@ pub enum Effect { subscription_id: usize, callback: WindowActivationCallback, }, + FullscreenWindow { + window_id: usize, + is_fullscreen: bool, + }, WindowFullscreenObservation { window_id: usize, subscription_id: usize, callback: WindowFullscreenCallback, }, + WindowBoundsObservation { + window_id: usize, + subscription_id: usize, + callback: WindowBoundsCallback, + }, Keystroke { window_id: usize, keystroke: Keystroke, @@ -3006,6 +2998,10 @@ impl Debug for Effect { .debug_struct("Effect::RefreshWindow") .field("window_id", window_id) .finish(), + Effect::MoveWindow { window_id } => f + .debug_struct("Effect::MoveWindow") + .field("window_id", window_id) + .finish(), Effect::WindowActivationObservation { window_id, subscription_id, @@ -3040,6 +3036,16 @@ impl Debug for Effect { .field("window_id", window_id) .field("subscription_id", subscription_id) .finish(), + + Effect::WindowBoundsObservation { + window_id, + subscription_id, + callback: _, + } => f + .debug_struct("Effect::WindowBoundsObservation") + .field("window_id", window_id) + .field("subscription_id", subscription_id) + .finish(), Effect::RefreshWindows => f.debug_struct("Effect::FullViewRefresh").finish(), Effect::WindowShouldCloseSubscription { window_id, .. } => f .debug_struct("Effect::WindowShouldCloseSubscription") @@ -3615,10 +3621,6 @@ impl<'a, T: View> ViewContext<'a, T> { self.app.toggle_window_full_screen(self.window_id) } - pub fn window_bounds(&self) -> RectF { - self.app.window_bounds(self.window_id) - } - pub fn prompt( &self, level: PromptLevel, @@ -3639,6 +3641,10 @@ impl<'a, T: View> ViewContext<'a, T> { self.app.prompt_for_new_path(directory) } + pub fn reveal_path(&self, path: &Path) { + self.app.reveal_path(path) + } + pub fn debug_elements(&self) -> crate::json::Value { self.app.debug_elements(self.window_id).unwrap() } @@ -3735,6 +3741,10 @@ impl<'a, T: View> ViewContext<'a, T> { .build_and_insert_view(self.window_id, ParentId::View(self.view_id), build_view) } + pub fn parent(&mut self) -> Option { + self.cx.parent(self.window_id, self.view_id) + } + pub fn reparent(&mut self, view_handle: impl Into) { let view_handle = view_handle.into(); if self.window_id != view_handle.window_id { @@ -3892,7 +3902,7 @@ impl<'a, T: View> ViewContext<'a, T> { }) } - pub fn observe_keystroke(&mut self, mut callback: F) -> Subscription + pub fn observe_keystrokes(&mut self, mut callback: F) -> Subscription where F: 'static + FnMut( @@ -3919,6 +3929,24 @@ impl<'a, T: View> ViewContext<'a, T> { ) } + pub fn observe_window_bounds(&mut self, mut callback: F) -> Subscription + where + F: 'static + FnMut(&mut T, WindowBounds, Uuid, &mut ViewContext), + { + let observer = self.weak_handle(); + self.app + .observe_window_bounds(self.window_id(), move |bounds, display, cx| { + if let Some(observer) = observer.upgrade(cx) { + observer.update(cx, |observer, cx| { + callback(observer, bounds, display, cx); + }); + true + } else { + false + } + }) + } + pub fn emit(&mut self, payload: T::Event) { self.app.pending_effects.push_back(Effect::Event { entity_id: self.view_id, @@ -4781,6 +4809,12 @@ impl From> for AnyViewHandle { } } +impl PartialEq> for AnyViewHandle { + fn eq(&self, other: &ViewHandle) -> bool { + self.window_id == other.window_id && self.view_id == other.view_id + } +} + impl Drop for AnyViewHandle { fn drop(&mut self) { self.ref_counts @@ -5083,6 +5117,7 @@ pub enum Subscription { FocusObservation(callback_collection::Subscription), WindowActivationObservation(callback_collection::Subscription), WindowFullscreenObservation(callback_collection::Subscription), + WindowBoundsObservation(callback_collection::Subscription), KeystrokeObservation(callback_collection::Subscription), ReleaseObservation(callback_collection::Subscription), ActionObservation(callback_collection::Subscription<(), ActionObservationCallback>), @@ -5098,6 +5133,7 @@ impl Subscription { Subscription::FocusObservation(subscription) => subscription.id(), Subscription::WindowActivationObservation(subscription) => subscription.id(), Subscription::WindowFullscreenObservation(subscription) => subscription.id(), + Subscription::WindowBoundsObservation(subscription) => subscription.id(), Subscription::KeystrokeObservation(subscription) => subscription.id(), Subscription::ReleaseObservation(subscription) => subscription.id(), Subscription::ActionObservation(subscription) => subscription.id(), @@ -5114,211 +5150,13 @@ impl Subscription { Subscription::KeystrokeObservation(subscription) => subscription.detach(), Subscription::WindowActivationObservation(subscription) => subscription.detach(), Subscription::WindowFullscreenObservation(subscription) => subscription.detach(), + Subscription::WindowBoundsObservation(subscription) => subscription.detach(), Subscription::ReleaseObservation(subscription) => subscription.detach(), Subscription::ActionObservation(subscription) => subscription.detach(), } } } -lazy_static! { - static ref LEAK_BACKTRACE: bool = - std::env::var("LEAK_BACKTRACE").map_or(false, |b| !b.is_empty()); -} - -#[cfg(any(test, feature = "test-support"))] -#[derive(Default)] -pub struct LeakDetector { - next_handle_id: usize, - #[allow(clippy::type_complexity)] - handle_backtraces: HashMap< - usize, - ( - Option<&'static str>, - HashMap>, - ), - >, -} - -#[cfg(any(test, feature = "test-support"))] -impl LeakDetector { - fn handle_created(&mut self, type_name: Option<&'static str>, entity_id: usize) -> usize { - let handle_id = post_inc(&mut self.next_handle_id); - let entry = self.handle_backtraces.entry(entity_id).or_default(); - let backtrace = if *LEAK_BACKTRACE { - Some(backtrace::Backtrace::new_unresolved()) - } else { - None - }; - if let Some(type_name) = type_name { - entry.0.get_or_insert(type_name); - } - entry.1.insert(handle_id, backtrace); - handle_id - } - - fn handle_dropped(&mut self, entity_id: usize, handle_id: usize) { - if let Some((_, backtraces)) = self.handle_backtraces.get_mut(&entity_id) { - assert!(backtraces.remove(&handle_id).is_some()); - if backtraces.is_empty() { - self.handle_backtraces.remove(&entity_id); - } - } - } - - pub fn assert_dropped(&mut self, entity_id: usize) { - if let Some((type_name, backtraces)) = self.handle_backtraces.get_mut(&entity_id) { - for trace in backtraces.values_mut().flatten() { - trace.resolve(); - eprintln!("{:?}", crate::util::CwdBacktrace(trace)); - } - - let hint = if *LEAK_BACKTRACE { - "" - } else { - " – set LEAK_BACKTRACE=1 for more information" - }; - - panic!( - "{} handles to {} {} still exist{}", - backtraces.len(), - type_name.unwrap_or("entity"), - entity_id, - hint - ); - } - } - - pub fn detect(&mut self) { - let mut found_leaks = false; - for (id, (type_name, backtraces)) in self.handle_backtraces.iter_mut() { - eprintln!( - "leaked {} handles to {} {}", - backtraces.len(), - type_name.unwrap_or("entity"), - id - ); - for trace in backtraces.values_mut().flatten() { - trace.resolve(); - eprintln!("{:?}", crate::util::CwdBacktrace(trace)); - } - found_leaks = true; - } - - let hint = if *LEAK_BACKTRACE { - "" - } else { - " – set LEAK_BACKTRACE=1 for more information" - }; - assert!(!found_leaks, "detected leaked handles{}", hint); - } -} - -#[derive(Default)] -struct RefCounts { - entity_counts: HashMap, - element_state_counts: HashMap, - dropped_models: HashSet, - dropped_views: HashSet<(usize, usize)>, - dropped_element_states: HashSet, - - #[cfg(any(test, feature = "test-support"))] - leak_detector: Arc>, -} - -struct ElementStateRefCount { - ref_count: usize, - frame_id: usize, -} - -impl RefCounts { - fn inc_model(&mut self, model_id: usize) { - match self.entity_counts.entry(model_id) { - Entry::Occupied(mut entry) => { - *entry.get_mut() += 1; - } - Entry::Vacant(entry) => { - entry.insert(1); - self.dropped_models.remove(&model_id); - } - } - } - - fn inc_view(&mut self, window_id: usize, view_id: usize) { - match self.entity_counts.entry(view_id) { - Entry::Occupied(mut entry) => *entry.get_mut() += 1, - Entry::Vacant(entry) => { - entry.insert(1); - self.dropped_views.remove(&(window_id, view_id)); - } - } - } - - fn inc_element_state(&mut self, id: ElementStateId, frame_id: usize) { - match self.element_state_counts.entry(id) { - Entry::Occupied(mut entry) => { - let entry = entry.get_mut(); - if entry.frame_id == frame_id || entry.ref_count >= 2 { - panic!("used the same element state more than once in the same frame"); - } - entry.ref_count += 1; - entry.frame_id = frame_id; - } - Entry::Vacant(entry) => { - entry.insert(ElementStateRefCount { - ref_count: 1, - frame_id, - }); - self.dropped_element_states.remove(&id); - } - } - } - - fn dec_model(&mut self, model_id: usize) { - let count = self.entity_counts.get_mut(&model_id).unwrap(); - *count -= 1; - if *count == 0 { - self.entity_counts.remove(&model_id); - self.dropped_models.insert(model_id); - } - } - - fn dec_view(&mut self, window_id: usize, view_id: usize) { - let count = self.entity_counts.get_mut(&view_id).unwrap(); - *count -= 1; - if *count == 0 { - self.entity_counts.remove(&view_id); - self.dropped_views.insert((window_id, view_id)); - } - } - - fn dec_element_state(&mut self, id: ElementStateId) { - let entry = self.element_state_counts.get_mut(&id).unwrap(); - entry.ref_count -= 1; - if entry.ref_count == 0 { - self.element_state_counts.remove(&id); - self.dropped_element_states.insert(id); - } - } - - fn is_entity_alive(&self, entity_id: usize) -> bool { - self.entity_counts.contains_key(&entity_id) - } - - fn take_dropped( - &mut self, - ) -> ( - HashSet, - HashSet<(usize, usize)>, - HashSet, - ) { - ( - std::mem::take(&mut self.dropped_models), - std::mem::take(&mut self.dropped_views), - std::mem::take(&mut self.dropped_element_states), - ) - } -} - #[cfg(test)] mod tests { use super::*; @@ -6374,6 +6212,8 @@ mod tests { cx.focus(&view_1); cx.focus(&view_2); }); + assert!(cx.is_child_focused(view_1.clone())); + assert!(!cx.is_child_focused(view_2.clone())); assert_eq!( mem::take(&mut *view_events.lock()), [ @@ -6398,6 +6238,8 @@ mod tests { ); view_1.update(cx, |_, cx| cx.focus(&view_1)); + assert!(!cx.is_child_focused(view_1.clone())); + assert!(!cx.is_child_focused(view_2.clone())); assert_eq!( mem::take(&mut *view_events.lock()), ["view 2 blurred", "view 1 focused"], diff --git a/crates/gpui/src/app/menu.rs b/crates/gpui/src/app/menu.rs new file mode 100644 index 0000000000..2234bfa391 --- /dev/null +++ b/crates/gpui/src/app/menu.rs @@ -0,0 +1,52 @@ +use crate::{Action, App, ForegroundPlatform, MutableAppContext}; + +pub struct Menu<'a> { + pub name: &'a str, + pub items: Vec>, +} + +pub enum MenuItem<'a> { + Separator, + Submenu(Menu<'a>), + Action { + name: &'a str, + action: Box, + }, +} + +impl MutableAppContext { + pub fn set_menus(&mut self, menus: Vec) { + self.foreground_platform + .set_menus(menus, &self.keystroke_matcher); + } +} + +pub(crate) fn setup_menu_handlers(foreground_platform: &dyn ForegroundPlatform, app: &App) { + foreground_platform.on_will_open_menu(Box::new({ + let cx = app.0.clone(); + move || { + let mut cx = cx.borrow_mut(); + cx.keystroke_matcher.clear_pending(); + } + })); + foreground_platform.on_validate_menu_command(Box::new({ + let cx = app.0.clone(); + move |action| { + let cx = cx.borrow_mut(); + !cx.keystroke_matcher.has_pending_keystrokes() && cx.is_action_available(action) + } + })); + foreground_platform.on_menu_command(Box::new({ + let cx = app.0.clone(); + move |action| { + let mut cx = cx.borrow_mut(); + if let Some(key_window_id) = cx.cx.platform.key_window_id() { + if let Some(view_id) = cx.focused_view_id(key_window_id) { + cx.handle_dispatch_action_from_effect(key_window_id, Some(view_id), action); + return; + } + } + cx.dispatch_global_action_any(action); + } + })); +} diff --git a/crates/gpui/src/app/ref_counts.rs b/crates/gpui/src/app/ref_counts.rs new file mode 100644 index 0000000000..f0c1699f16 --- /dev/null +++ b/crates/gpui/src/app/ref_counts.rs @@ -0,0 +1,220 @@ +#[cfg(any(test, feature = "test-support"))] +use std::sync::Arc; + +use lazy_static::lazy_static; +#[cfg(any(test, feature = "test-support"))] +use parking_lot::Mutex; + +use collections::{hash_map::Entry, HashMap, HashSet}; + +#[cfg(any(test, feature = "test-support"))] +use crate::util::post_inc; +use crate::ElementStateId; + +lazy_static! { + static ref LEAK_BACKTRACE: bool = + std::env::var("LEAK_BACKTRACE").map_or(false, |b| !b.is_empty()); +} + +struct ElementStateRefCount { + ref_count: usize, + frame_id: usize, +} + +#[derive(Default)] +pub struct RefCounts { + entity_counts: HashMap, + element_state_counts: HashMap, + dropped_models: HashSet, + dropped_views: HashSet<(usize, usize)>, + dropped_element_states: HashSet, + + #[cfg(any(test, feature = "test-support"))] + pub leak_detector: Arc>, +} + +impl RefCounts { + #[cfg(any(test, feature = "test-support"))] + pub fn new(leak_detector: Arc>) -> Self { + Self { + #[cfg(any(test, feature = "test-support"))] + leak_detector, + ..Default::default() + } + } + + pub fn inc_model(&mut self, model_id: usize) { + match self.entity_counts.entry(model_id) { + Entry::Occupied(mut entry) => { + *entry.get_mut() += 1; + } + Entry::Vacant(entry) => { + entry.insert(1); + self.dropped_models.remove(&model_id); + } + } + } + + pub fn inc_view(&mut self, window_id: usize, view_id: usize) { + match self.entity_counts.entry(view_id) { + Entry::Occupied(mut entry) => *entry.get_mut() += 1, + Entry::Vacant(entry) => { + entry.insert(1); + self.dropped_views.remove(&(window_id, view_id)); + } + } + } + + pub fn inc_element_state(&mut self, id: ElementStateId, frame_id: usize) { + match self.element_state_counts.entry(id) { + Entry::Occupied(mut entry) => { + let entry = entry.get_mut(); + if entry.frame_id == frame_id || entry.ref_count >= 2 { + panic!("used the same element state more than once in the same frame"); + } + entry.ref_count += 1; + entry.frame_id = frame_id; + } + Entry::Vacant(entry) => { + entry.insert(ElementStateRefCount { + ref_count: 1, + frame_id, + }); + self.dropped_element_states.remove(&id); + } + } + } + + pub fn dec_model(&mut self, model_id: usize) { + let count = self.entity_counts.get_mut(&model_id).unwrap(); + *count -= 1; + if *count == 0 { + self.entity_counts.remove(&model_id); + self.dropped_models.insert(model_id); + } + } + + pub fn dec_view(&mut self, window_id: usize, view_id: usize) { + let count = self.entity_counts.get_mut(&view_id).unwrap(); + *count -= 1; + if *count == 0 { + self.entity_counts.remove(&view_id); + self.dropped_views.insert((window_id, view_id)); + } + } + + pub fn dec_element_state(&mut self, id: ElementStateId) { + let entry = self.element_state_counts.get_mut(&id).unwrap(); + entry.ref_count -= 1; + if entry.ref_count == 0 { + self.element_state_counts.remove(&id); + self.dropped_element_states.insert(id); + } + } + + pub fn is_entity_alive(&self, entity_id: usize) -> bool { + self.entity_counts.contains_key(&entity_id) + } + + pub fn take_dropped( + &mut self, + ) -> ( + HashSet, + HashSet<(usize, usize)>, + HashSet, + ) { + ( + std::mem::take(&mut self.dropped_models), + std::mem::take(&mut self.dropped_views), + std::mem::take(&mut self.dropped_element_states), + ) + } +} + +#[cfg(any(test, feature = "test-support"))] +#[derive(Default)] +pub struct LeakDetector { + next_handle_id: usize, + #[allow(clippy::type_complexity)] + handle_backtraces: HashMap< + usize, + ( + Option<&'static str>, + HashMap>, + ), + >, +} + +#[cfg(any(test, feature = "test-support"))] +impl LeakDetector { + pub fn handle_created(&mut self, type_name: Option<&'static str>, entity_id: usize) -> usize { + let handle_id = post_inc(&mut self.next_handle_id); + let entry = self.handle_backtraces.entry(entity_id).or_default(); + let backtrace = if *LEAK_BACKTRACE { + Some(backtrace::Backtrace::new_unresolved()) + } else { + None + }; + if let Some(type_name) = type_name { + entry.0.get_or_insert(type_name); + } + entry.1.insert(handle_id, backtrace); + handle_id + } + + pub fn handle_dropped(&mut self, entity_id: usize, handle_id: usize) { + if let Some((_, backtraces)) = self.handle_backtraces.get_mut(&entity_id) { + assert!(backtraces.remove(&handle_id).is_some()); + if backtraces.is_empty() { + self.handle_backtraces.remove(&entity_id); + } + } + } + + pub fn assert_dropped(&mut self, entity_id: usize) { + if let Some((type_name, backtraces)) = self.handle_backtraces.get_mut(&entity_id) { + for trace in backtraces.values_mut().flatten() { + trace.resolve(); + eprintln!("{:?}", crate::util::CwdBacktrace(trace)); + } + + let hint = if *LEAK_BACKTRACE { + "" + } else { + " – set LEAK_BACKTRACE=1 for more information" + }; + + panic!( + "{} handles to {} {} still exist{}", + backtraces.len(), + type_name.unwrap_or("entity"), + entity_id, + hint + ); + } + } + + pub fn detect(&mut self) { + let mut found_leaks = false; + for (id, (type_name, backtraces)) in self.handle_backtraces.iter_mut() { + eprintln!( + "leaked {} handles to {} {}", + backtraces.len(), + type_name.unwrap_or("entity"), + id + ); + for trace in backtraces.values_mut().flatten() { + trace.resolve(); + eprintln!("{:?}", crate::util::CwdBacktrace(trace)); + } + found_leaks = true; + } + + let hint = if *LEAK_BACKTRACE { + "" + } else { + " – set LEAK_BACKTRACE=1 for more information" + }; + assert!(!found_leaks, "detected leaked handles{}", hint); + } +} diff --git a/crates/gpui/src/app/test_app_context.rs b/crates/gpui/src/app/test_app_context.rs index 3e75910475..1d467ae745 100644 --- a/crates/gpui/src/app/test_app_context.rs +++ b/crates/gpui/src/app/test_app_context.rs @@ -19,13 +19,14 @@ use smol::stream::StreamExt; use crate::{ executor, geometry::vector::Vector2F, keymap_matcher::Keystroke, platform, Action, AnyViewHandle, AppContext, Appearance, Entity, Event, FontCache, InputHandler, KeyDownEvent, - LeakDetector, ModelContext, ModelHandle, MutableAppContext, Platform, ReadModelWith, - ReadViewWith, RenderContext, Task, UpdateModel, UpdateView, View, ViewContext, ViewHandle, - WeakHandle, WindowInputHandler, + ModelContext, ModelHandle, MutableAppContext, Platform, ReadModelWith, ReadViewWith, + RenderContext, Task, UpdateModel, UpdateView, View, ViewContext, ViewHandle, WeakHandle, }; use collections::BTreeMap; -use super::{AsyncAppContext, RefCounts}; +use super::{ + ref_counts::LeakDetector, window_input_handler::WindowInputHandler, AsyncAppContext, RefCounts, +}; #[derive(Clone)] pub struct TestAppContext { @@ -53,11 +54,7 @@ impl TestAppContext { platform, foreground_platform.clone(), font_cache, - RefCounts { - #[cfg(any(test, feature = "test-support"))] - leak_detector, - ..Default::default() - }, + RefCounts::new(leak_detector), (), ); cx.next_entity_id = first_entity_id; @@ -625,6 +622,8 @@ impl ViewHandle { } } +/// Tracks string context to be printed when assertions fail. +/// Often this is done by storing a context string in the manager and returning the handle. #[derive(Clone)] pub struct AssertionContextManager { id: Arc, @@ -655,6 +654,9 @@ impl AssertionContextManager { } } +/// Used to track the lifetime of a piece of context so that it can be provided when an assertion fails. +/// For example, in the EditorTestContext, `set_state` returns a context handle so that if an assertion fails, +/// the state that was set initially for the failure can be printed in the error message pub struct ContextHandle { id: usize, manager: AssertionContextManager, diff --git a/crates/gpui/src/app/window_input_handler.rs b/crates/gpui/src/app/window_input_handler.rs new file mode 100644 index 0000000000..855f0e3041 --- /dev/null +++ b/crates/gpui/src/app/window_input_handler.rs @@ -0,0 +1,98 @@ +use std::{cell::RefCell, ops::Range, rc::Rc}; + +use pathfinder_geometry::rect::RectF; + +use crate::{AnyView, AppContext, InputHandler, MutableAppContext}; + +pub struct WindowInputHandler { + pub app: Rc>, + pub window_id: usize, +} + +impl WindowInputHandler { + fn read_focused_view(&self, f: F) -> Option + where + F: FnOnce(&dyn AnyView, &AppContext) -> T, + { + // Input-related application hooks are sometimes called by the OS during + // a call to a window-manipulation API, like prompting the user for file + // paths. In that case, the AppContext will already be borrowed, so any + // InputHandler methods need to fail gracefully. + // + // See https://github.com/zed-industries/community/issues/444 + let app = self.app.try_borrow().ok()?; + + let view_id = app.focused_view_id(self.window_id)?; + let view = app.cx.views.get(&(self.window_id, view_id))?; + let result = f(view.as_ref(), &app); + Some(result) + } + + fn update_focused_view(&mut self, f: F) -> Option + where + F: FnOnce(usize, usize, &mut dyn AnyView, &mut MutableAppContext) -> T, + { + let mut app = self.app.try_borrow_mut().ok()?; + app.update(|app| { + let view_id = app.focused_view_id(self.window_id)?; + let mut view = app.cx.views.remove(&(self.window_id, view_id))?; + let result = f(self.window_id, view_id, view.as_mut(), &mut *app); + app.cx.views.insert((self.window_id, view_id), view); + Some(result) + }) + } +} + +impl InputHandler for WindowInputHandler { + fn text_for_range(&self, range: Range) -> Option { + self.read_focused_view(|view, cx| view.text_for_range(range.clone(), cx)) + .flatten() + } + + fn selected_text_range(&self) -> Option> { + self.read_focused_view(|view, cx| view.selected_text_range(cx)) + .flatten() + } + + fn replace_text_in_range(&mut self, range: Option>, text: &str) { + self.update_focused_view(|window_id, view_id, view, cx| { + view.replace_text_in_range(range, text, cx, window_id, view_id); + }); + } + + fn marked_text_range(&self) -> Option> { + self.read_focused_view(|view, cx| view.marked_text_range(cx)) + .flatten() + } + + fn unmark_text(&mut self) { + self.update_focused_view(|window_id, view_id, view, cx| { + view.unmark_text(cx, window_id, view_id); + }); + } + + fn replace_and_mark_text_in_range( + &mut self, + range: Option>, + new_text: &str, + new_selected_range: Option>, + ) { + self.update_focused_view(|window_id, view_id, view, cx| { + view.replace_and_mark_text_in_range( + range, + new_text, + new_selected_range, + cx, + window_id, + view_id, + ); + }); + } + + fn rect_for_range(&self, range_utf16: Range) -> Option { + let app = self.app.borrow(); + let (presenter, _) = app.presenters_and_platform_windows.get(&self.window_id)?; + let presenter = presenter.borrow(); + presenter.rect_for_text_range(range_utf16, &app) + } +} diff --git a/crates/gpui/src/elements.rs b/crates/gpui/src/elements.rs index 9f11f09f8e..41a802feb3 100644 --- a/crates/gpui/src/elements.rs +++ b/crates/gpui/src/elements.rs @@ -1,5 +1,6 @@ mod align; mod canvas; +mod clipped; mod constrained_box; mod container; mod empty; @@ -19,12 +20,12 @@ mod text; mod tooltip; mod uniform_list; -use self::expanded::Expanded; pub use self::{ align::*, canvas::*, constrained_box::*, container::*, empty::*, flex::*, hook::*, image::*, keystroke_label::*, label::*, list::*, mouse_event_handler::*, overlay::*, resizable::*, stack::*, svg::*, text::*, tooltip::*, uniform_list::*, }; +use self::{clipped::Clipped, expanded::Expanded}; pub use crate::presenter::ChildView; use crate::{ geometry::{ @@ -135,6 +136,13 @@ pub trait Element { Align::new(self.boxed()) } + fn clipped(self) -> Clipped + where + Self: 'static + Sized, + { + Clipped::new(self.boxed()) + } + fn contained(self) -> Container where Self: 'static + Sized, diff --git a/crates/gpui/src/elements/clipped.rs b/crates/gpui/src/elements/clipped.rs new file mode 100644 index 0000000000..2ee7b542a8 --- /dev/null +++ b/crates/gpui/src/elements/clipped.rs @@ -0,0 +1,69 @@ +use std::ops::Range; + +use pathfinder_geometry::{rect::RectF, vector::Vector2F}; +use serde_json::json; + +use crate::{ + json, DebugContext, Element, ElementBox, LayoutContext, MeasurementContext, PaintContext, + SizeConstraint, +}; + +pub struct Clipped { + child: ElementBox, +} + +impl Clipped { + pub fn new(child: ElementBox) -> Self { + Self { child } + } +} + +impl Element for Clipped { + type LayoutState = (); + type PaintState = (); + + fn layout( + &mut self, + constraint: SizeConstraint, + cx: &mut LayoutContext, + ) -> (Vector2F, Self::LayoutState) { + (self.child.layout(constraint, cx), ()) + } + + fn paint( + &mut self, + bounds: RectF, + visible_bounds: RectF, + _: &mut Self::LayoutState, + cx: &mut PaintContext, + ) -> Self::PaintState { + cx.scene.push_layer(Some(bounds)); + self.child.paint(bounds.origin(), visible_bounds, cx); + cx.scene.pop_layer(); + } + + fn rect_for_text_range( + &self, + range_utf16: Range, + _: RectF, + _: RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + cx: &MeasurementContext, + ) -> Option { + self.child.rect_for_text_range(range_utf16, cx) + } + + fn debug( + &self, + _: RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + cx: &DebugContext, + ) -> json::Value { + json!({ + "type": "Clipped", + "child": self.child.debug(cx) + }) + } +} diff --git a/crates/gpui/src/elements/keystroke_label.rs b/crates/gpui/src/elements/keystroke_label.rs index ca317d9e11..6553b2fa8d 100644 --- a/crates/gpui/src/elements/keystroke_label.rs +++ b/crates/gpui/src/elements/keystroke_label.rs @@ -12,15 +12,21 @@ pub struct KeystrokeLabel { action: Box, container_style: ContainerStyle, text_style: TextStyle, + window_id: usize, + view_id: usize, } impl KeystrokeLabel { pub fn new( + window_id: usize, + view_id: usize, action: Box, container_style: ContainerStyle, text_style: TextStyle, ) -> Self { Self { + window_id, + view_id, action, container_style, text_style, @@ -37,7 +43,10 @@ impl Element for KeystrokeLabel { constraint: SizeConstraint, cx: &mut LayoutContext, ) -> (Vector2F, ElementBox) { - let mut element = if let Some(keystrokes) = cx.keystrokes_for_action(self.action.as_ref()) { + let mut element = if let Some(keystrokes) = + cx.app + .keystrokes_for_action(self.window_id, self.view_id, self.action.as_ref()) + { Flex::row() .with_children(keystrokes.iter().map(|keystroke| { Label::new(keystroke.to_string(), self.text_style.clone()) diff --git a/crates/gpui/src/elements/tooltip.rs b/crates/gpui/src/elements/tooltip.rs index dbcecf9c24..562f12295c 100644 --- a/crates/gpui/src/elements/tooltip.rs +++ b/crates/gpui/src/elements/tooltip.rs @@ -61,11 +61,14 @@ impl Tooltip { ) -> Self { struct ElementState(Tag); struct MouseEventHandlerState(Tag); + let focused_view_id = cx.focused_view_id(cx.window_id); let state_handle = cx.default_element_state::, Rc>(id); let state = state_handle.read(cx).clone(); let tooltip = if state.visible.get() { let mut collapsed_tooltip = Self::render_tooltip( + cx.window_id, + focused_view_id, text.clone(), style.clone(), action.as_ref().map(|a| a.boxed_clone()), @@ -74,7 +77,7 @@ impl Tooltip { .boxed(); Some( Overlay::new( - Self::render_tooltip(text, style, action, false) + Self::render_tooltip(cx.window_id, focused_view_id, text, style, action, false) .constrained() .dynamically(move |constraint, cx| { SizeConstraint::strict_along( @@ -128,6 +131,8 @@ impl Tooltip { } pub fn render_tooltip( + window_id: usize, + focused_view_id: Option, text: String, style: TooltipStyle, action: Option>, @@ -144,13 +149,18 @@ impl Tooltip { text.flex(1., false).aligned().boxed() } }) - .with_children(action.map(|action| { - let keystroke_label = - KeystrokeLabel::new(action, style.keystroke.container, style.keystroke.text); + .with_children(action.and_then(|action| { + let keystroke_label = KeystrokeLabel::new( + window_id, + focused_view_id?, + action, + style.keystroke.container, + style.keystroke.text, + ); if measure { - keystroke_label.boxed() + Some(keystroke_label.boxed()) } else { - keystroke_label.aligned().boxed() + Some(keystroke_label.aligned().boxed()) } })) .contained() diff --git a/crates/gpui/src/keymap_matcher.rs b/crates/gpui/src/keymap_matcher.rs index c7de035232..cfc26d6869 100644 --- a/crates/gpui/src/keymap_matcher.rs +++ b/crates/gpui/src/keymap_matcher.rs @@ -5,25 +5,16 @@ mod keystroke; use std::{any::TypeId, fmt::Debug}; -use collections::HashMap; -use serde::Deserialize; +use collections::{BTreeMap, HashMap}; use smallvec::SmallVec; -use crate::{impl_actions, Action}; +use crate::Action; pub use binding::{Binding, BindingMatchResult}; pub use keymap::Keymap; pub use keymap_context::{KeymapContext, KeymapContextPredicate}; pub use keystroke::Keystroke; -#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize)] -pub struct KeyPressed { - #[serde(default)] - pub keystroke: Keystroke, -} - -impl_actions!(gpui, [KeyPressed]); - pub struct KeymapMatcher { pub contexts: Vec, pending_views: HashMap, @@ -69,13 +60,28 @@ impl KeymapMatcher { !self.pending_keystrokes.is_empty() } + /// Pushes a keystroke onto the matcher. + /// The result of the new keystroke is returned: + /// MatchResult::None => + /// No match is valid for this key given any pending keystrokes. + /// MatchResult::Pending => + /// There exist bindings which are still waiting for more keys. + /// MatchResult::Complete(matches) => + /// 1 or more bindings have recieved the necessary key presses. + /// The order of the matched actions is by order in the keymap file first and + /// position of the matching view second. pub fn push_keystroke( &mut self, keystroke: Keystroke, mut dispatch_path: Vec<(usize, KeymapContext)>, ) -> MatchResult { let mut any_pending = false; - let mut matched_bindings: Vec<(usize, Box)> = Vec::new(); + // Collect matched bindings into an ordered list using the position in the matching binding first, + // and then the order the binding matched in the view tree second. + // The key is the reverse position of the binding in the bindings list so that later bindings + // match before earlier ones in the user's config + let mut matched_bindings: BTreeMap)>> = + Default::default(); let first_keystroke = self.pending_keystrokes.is_empty(); self.pending_keystrokes.push(keystroke.clone()); @@ -84,35 +90,33 @@ impl KeymapMatcher { self.contexts .extend(dispatch_path.iter_mut().map(|e| std::mem::take(&mut e.1))); - for (i, (view_id, _)) in dispatch_path.into_iter().enumerate() { + // Find the bindings which map the pending keystrokes and current context + for (i, (view_id, _)) in dispatch_path.iter().enumerate() { // Don't require pending view entry if there are no pending keystrokes - if !first_keystroke && !self.pending_views.contains_key(&view_id) { + if !first_keystroke && !self.pending_views.contains_key(view_id) { continue; } // If there is a previous view context, invalidate that view if it // has changed - if let Some(previous_view_context) = self.pending_views.remove(&view_id) { + if let Some(previous_view_context) = self.pending_views.remove(view_id) { if previous_view_context != self.contexts[i] { continue; } } - // Find the bindings which map the pending keystrokes and current context - for binding in self.keymap.bindings().iter().rev() { + for (order, binding) in self.keymap.bindings().iter().rev().enumerate() { match binding.match_keys_and_context(&self.pending_keystrokes, &self.contexts[i..]) { - BindingMatchResult::Complete(mut action) => { - // Swap in keystroke for special KeyPressed action - if action.name() == "KeyPressed" && action.namespace() == "gpui" { - action = Box::new(KeyPressed { - keystroke: keystroke.clone(), - }); - } - matched_bindings.push((view_id, action)) + BindingMatchResult::Complete(action) => { + matched_bindings + .entry(order) + .or_default() + .push((*view_id, action)); } BindingMatchResult::Partial => { - self.pending_views.insert(view_id, self.contexts[i].clone()); + self.pending_views + .insert(*view_id, self.contexts[i].clone()); any_pending = true; } _ => {} @@ -125,7 +129,9 @@ impl KeymapMatcher { } if !matched_bindings.is_empty() { - MatchResult::Matches(matched_bindings) + // Collect the sorted matched bindings into the final vec for ease of use + // Matched bindings are in order by precedence + MatchResult::Matches(matched_bindings.into_values().flatten().collect()) } else if any_pending { MatchResult::Pending } else { diff --git a/crates/gpui/src/keymap_matcher/binding.rs b/crates/gpui/src/keymap_matcher/binding.rs index afd65d4f04..c1cfd14e82 100644 --- a/crates/gpui/src/keymap_matcher/binding.rs +++ b/crates/gpui/src/keymap_matcher/binding.rs @@ -7,7 +7,7 @@ use super::{KeymapContext, KeymapContextPredicate, Keystroke}; pub struct Binding { action: Box, - keystrokes: Option>, + keystrokes: SmallVec<[Keystroke; 2]>, context_predicate: Option, } @@ -23,16 +23,10 @@ impl Binding { None }; - let keystrokes = if keystrokes == "*" { - None // Catch all context - } else { - Some( - keystrokes - .split_whitespace() - .map(Keystroke::parse) - .collect::>()?, - ) - }; + let keystrokes = keystrokes + .split_whitespace() + .map(Keystroke::parse) + .collect::>()?; Ok(Self { keystrokes, @@ -41,7 +35,7 @@ impl Binding { }) } - fn match_context(&self, contexts: &[KeymapContext]) -> bool { + pub fn match_context(&self, contexts: &[KeymapContext]) -> bool { self.context_predicate .as_ref() .map(|predicate| predicate.eval(contexts)) @@ -53,20 +47,10 @@ impl Binding { pending_keystrokes: &Vec, contexts: &[KeymapContext], ) -> BindingMatchResult { - if self - .keystrokes - .as_ref() - .map(|keystrokes| keystrokes.starts_with(&pending_keystrokes)) - .unwrap_or(true) - && self.match_context(contexts) + if self.keystrokes.as_ref().starts_with(&pending_keystrokes) && self.match_context(contexts) { // If the binding is completed, push it onto the matches list - if self - .keystrokes - .as_ref() - .map(|keystrokes| keystrokes.len() == pending_keystrokes.len()) - .unwrap_or(true) - { + if self.keystrokes.as_ref().len() == pending_keystrokes.len() { BindingMatchResult::Complete(self.action.boxed_clone()) } else { BindingMatchResult::Partial @@ -82,14 +66,14 @@ impl Binding { contexts: &[KeymapContext], ) -> Option> { if self.action.eq(action) && self.match_context(contexts) { - self.keystrokes.clone() + Some(self.keystrokes.clone()) } else { None } } - pub fn keystrokes(&self) -> Option<&[Keystroke]> { - self.keystrokes.as_deref() + pub fn keystrokes(&self) -> &[Keystroke] { + self.keystrokes.as_slice() } pub fn action(&self) -> &dyn Action { diff --git a/crates/gpui/src/keymap_matcher/keymap_context.rs b/crates/gpui/src/keymap_matcher/keymap_context.rs index 28f5f80c83..b19989b210 100644 --- a/crates/gpui/src/keymap_matcher/keymap_context.rs +++ b/crates/gpui/src/keymap_matcher/keymap_context.rs @@ -43,7 +43,7 @@ impl KeymapContextPredicate { pub fn eval(&self, contexts: &[KeymapContext]) -> bool { let Some(context) = contexts.first() else { return false }; match self { - Self::Identifier(name) => context.set.contains(name.as_str()), + Self::Identifier(name) => (&context.set).contains(name.as_str()), Self::Equal(left, right) => context .map .get(left) diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index 99d607e407..76c2707d26 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -18,11 +18,15 @@ use crate::{ text_layout::{LineLayout, RunStyle}, Action, ClipboardItem, Menu, Scene, }; -use anyhow::{anyhow, Result}; +use anyhow::{anyhow, bail, Result}; use async_task::Runnable; pub use event::*; use postage::oneshot; use serde::Deserialize; +use sqlez::{ + bindable::{Bind, Column, StaticColumnCount}, + statement::Statement, +}; use std::{ any::Any, fmt::{self, Debug, Display}, @@ -33,6 +37,7 @@ use std::{ sync::Arc, }; use time::UtcOffset; +use uuid::Uuid; pub trait Platform: Send + Sync { fn dispatcher(&self) -> Arc; @@ -44,6 +49,7 @@ pub trait Platform: Send + Sync { fn unhide_other_apps(&self); fn quit(&self); + fn screen_by_id(&self, id: Uuid) -> Option>; fn screens(&self) -> Vec>; fn open_window( @@ -74,6 +80,7 @@ pub trait Platform: Send + Sync { fn app_version(&self) -> Result; fn os_name(&self) -> &'static str; fn os_version(&self) -> Result; + fn restart(&self); } pub(crate) trait ForegroundPlatform { @@ -93,6 +100,7 @@ pub(crate) trait ForegroundPlatform { options: PathPromptOptions, ) -> oneshot::Receiver>>; fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver>; + fn reveal_path(&self, path: &Path); } pub trait Dispatcher: Send + Sync { @@ -117,17 +125,19 @@ pub trait InputHandler { pub trait Screen: Debug { fn as_any(&self) -> &dyn Any; - fn size(&self) -> Vector2F; + fn bounds(&self) -> RectF; + fn display_uuid(&self) -> Option; } pub trait Window { + fn bounds(&self) -> WindowBounds; + fn content_size(&self) -> Vector2F; + fn scale_factor(&self) -> f32; + fn titlebar_height(&self) -> f32; + fn appearance(&self) -> Appearance; + fn screen(&self) -> Rc; + fn as_any_mut(&mut self) -> &mut dyn Any; - fn on_event(&mut self, callback: Box bool>); - fn on_active_status_change(&mut self, callback: Box); - fn on_resize(&mut self, callback: Box); - fn on_fullscreen(&mut self, callback: Box); - fn on_should_close(&mut self, callback: Box bool>); - fn on_close(&mut self, callback: Box); fn set_input_handler(&mut self, input_handler: Box); fn prompt(&self, level: PromptLevel, msg: &str, answers: &[&str]) -> oneshot::Receiver; fn activate(&self); @@ -136,15 +146,18 @@ pub trait Window { fn show_character_palette(&self); fn minimize(&self); fn zoom(&self); + fn present_scene(&mut self, scene: Scene); fn toggle_full_screen(&self); - fn bounds(&self) -> RectF; - fn content_size(&self) -> Vector2F; - fn scale_factor(&self) -> f32; - fn titlebar_height(&self) -> f32; - fn present_scene(&mut self, scene: Scene); - fn appearance(&self) -> Appearance; + fn on_event(&mut self, callback: Box bool>); + fn on_active_status_change(&mut self, callback: Box); + fn on_resize(&mut self, callback: Box); + fn on_fullscreen(&mut self, callback: Box); + fn on_moved(&mut self, callback: Box); + fn on_should_close(&mut self, callback: Box bool>); + fn on_close(&mut self, callback: Box); fn on_appearance_changed(&mut self, callback: Box); + fn is_topmost_for_position(&self, position: Vector2F) -> bool; } #[derive(Debug)] @@ -185,12 +198,70 @@ pub enum WindowKind { PopUp, } -#[derive(Debug)] +#[derive(Copy, Clone, Debug, PartialEq)] pub enum WindowBounds { + Fullscreen, Maximized, Fixed(RectF), } +impl StaticColumnCount for WindowBounds { + fn column_count() -> usize { + 5 + } +} + +impl Bind for WindowBounds { + fn bind(&self, statement: &Statement, start_index: i32) -> Result { + let (region, next_index) = match self { + WindowBounds::Fullscreen => { + let next_index = statement.bind("Fullscreen", start_index)?; + (None, next_index) + } + WindowBounds::Maximized => { + let next_index = statement.bind("Maximized", start_index)?; + (None, next_index) + } + WindowBounds::Fixed(region) => { + let next_index = statement.bind("Fixed", start_index)?; + (Some(*region), next_index) + } + }; + + statement.bind( + region.map(|region| { + ( + region.min_x(), + region.min_y(), + region.width(), + region.height(), + ) + }), + next_index, + ) + } +} + +impl Column for WindowBounds { + fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { + let (window_state, next_index) = String::column(statement, start_index)?; + let bounds = match window_state.as_str() { + "Fullscreen" => WindowBounds::Fullscreen, + "Maximized" => WindowBounds::Maximized, + "Fixed" => { + let ((x, y, width, height), _) = Column::column(statement, next_index)?; + WindowBounds::Fixed(RectF::new( + Vector2F::new(x, y), + Vector2F::new(width, height), + )) + } + _ => bail!("Window State did not have a valid string"), + }; + + Ok((bounds, next_index + 4)) + } +} + pub struct PathPromptOptions { pub files: bool, pub directories: bool, diff --git a/crates/gpui/src/platform/mac.rs b/crates/gpui/src/platform/mac.rs index 7eb080083e..342c1c66d0 100644 --- a/crates/gpui/src/platform/mac.rs +++ b/crates/gpui/src/platform/mac.rs @@ -12,20 +12,27 @@ mod sprite_cache; mod status_item; mod window; -use cocoa::base::{BOOL, NO, YES}; +use cocoa::{ + base::{id, nil, BOOL, NO, YES}, + foundation::{NSAutoreleasePool, NSNotFound, NSString, NSUInteger}, +}; pub use dispatcher::Dispatcher; pub use fonts::FontSystem; use platform::{MacForegroundPlatform, MacPlatform}; pub use renderer::Surface; -use std::{rc::Rc, sync::Arc}; +use std::{ops::Range, rc::Rc, sync::Arc}; use window::Window; +use crate::executor; + pub(crate) fn platform() -> Arc { Arc::new(MacPlatform::new()) } -pub(crate) fn foreground_platform() -> Rc { - Rc::new(MacForegroundPlatform::default()) +pub(crate) fn foreground_platform( + foreground: Rc, +) -> Rc { + Rc::new(MacForegroundPlatform::new(foreground)) } trait BoolExt { @@ -41,3 +48,57 @@ impl BoolExt for bool { } } } + +#[repr(C)] +#[derive(Copy, Clone, Debug)] +struct NSRange { + pub location: NSUInteger, + pub length: NSUInteger, +} + +impl NSRange { + fn invalid() -> Self { + Self { + location: NSNotFound as NSUInteger, + length: 0, + } + } + + fn is_valid(&self) -> bool { + self.location != NSNotFound as NSUInteger + } + + fn to_range(self) -> Option> { + if self.is_valid() { + let start = self.location as usize; + let end = start + self.length as usize; + Some(start..end) + } else { + None + } + } +} + +impl From> for NSRange { + fn from(range: Range) -> Self { + NSRange { + location: range.start as NSUInteger, + length: range.len() as NSUInteger, + } + } +} + +unsafe impl objc::Encode for NSRange { + fn encode() -> objc::Encoding { + let encoding = format!( + "{{NSRange={}{}}}", + NSUInteger::encode().as_str(), + NSUInteger::encode().as_str() + ); + unsafe { objc::Encoding::from_str(&encoding) } + } +} + +unsafe fn ns_string(string: &str) -> id { + NSString::alloc(nil).init_str(string).autorelease() +} diff --git a/crates/gpui/src/platform/mac/event.rs b/crates/gpui/src/platform/mac/event.rs index 2f29898c26..6882721372 100644 --- a/crates/gpui/src/platform/mac/event.rs +++ b/crates/gpui/src/platform/mac/event.rs @@ -125,6 +125,7 @@ impl Event { button, position: vec2f( native_event.locationInWindow().x as f32, + // MacOS screen coordinates are relative to bottom left window_height - native_event.locationInWindow().y as f32, ), modifiers: read_modifiers(native_event), @@ -150,6 +151,7 @@ impl Event { button, position: vec2f( native_event.locationInWindow().x as f32, + // MacOS view coordinates are relative to bottom left window_height - native_event.locationInWindow().y as f32, ), modifiers: read_modifiers(native_event), diff --git a/crates/gpui/src/platform/mac/geometry.rs b/crates/gpui/src/platform/mac/geometry.rs index 89da409dbd..6a47968118 100644 --- a/crates/gpui/src/platform/mac/geometry.rs +++ b/crates/gpui/src/platform/mac/geometry.rs @@ -1,27 +1,97 @@ -use cocoa::foundation::{NSPoint, NSRect, NSSize}; -use pathfinder_geometry::{rect::RectF, vector::Vector2F}; +use cocoa::{ + appkit::NSWindow, + base::id, + foundation::{NSPoint, NSRect, NSSize}, +}; +use objc::{msg_send, sel, sel_impl}; +use pathfinder_geometry::{ + rect::RectF, + vector::{vec2f, Vector2F}, +}; + +///! Macos screen have a y axis that goings up from the bottom of the screen and +///! an origin at the bottom left of the main display. pub trait Vector2FExt { - fn to_ns_point(&self) -> NSPoint; - fn to_ns_size(&self) -> NSSize; + /// Converts self to an NSPoint with y axis pointing up. + fn to_screen_ns_point(&self, native_window: id, window_height: f64) -> NSPoint; +} +impl Vector2FExt for Vector2F { + fn to_screen_ns_point(&self, native_window: id, window_height: f64) -> NSPoint { + unsafe { + let point = NSPoint::new(self.x() as f64, window_height - self.y() as f64); + msg_send![native_window, convertPointToScreen: point] + } + } } pub trait RectFExt { + /// Converts self to an NSRect with y axis pointing up. + /// The resulting NSRect will have an origin at the bottom left of the rectangle. + /// Also takes care of converting from window scaled coordinates to screen coordinates + fn to_screen_ns_rect(&self, native_window: id) -> NSRect; + + /// Converts self to an NSRect with y axis point up. + /// The resulting NSRect will have an origin at the bottom left of the rectangle. + /// Unlike to_screen_ns_rect, coordinates are not converted and are assumed to already be in screen scale fn to_ns_rect(&self) -> NSRect; } - -impl Vector2FExt for Vector2F { - fn to_ns_point(&self) -> NSPoint { - NSPoint::new(self.x() as f64, self.y() as f64) - } - - fn to_ns_size(&self) -> NSSize { - NSSize::new(self.x() as f64, self.y() as f64) - } -} - impl RectFExt for RectF { + fn to_screen_ns_rect(&self, native_window: id) -> NSRect { + unsafe { native_window.convertRectToScreen_(self.to_ns_rect()) } + } + fn to_ns_rect(&self) -> NSRect { - NSRect::new(self.origin().to_ns_point(), self.size().to_ns_size()) + NSRect::new( + NSPoint::new( + self.origin_x() as f64, + -(self.origin_y() + self.height()) as f64, + ), + NSSize::new(self.width() as f64, self.height() as f64), + ) + } +} + +pub trait NSRectExt { + /// Converts self to a RectF with y axis pointing down. + /// The resulting RectF will have an origin at the top left of the rectangle. + /// Also takes care of converting from screen scale coordinates to window coordinates + fn to_window_rectf(&self, native_window: id) -> RectF; + + /// Converts self to a RectF with y axis pointing down. + /// The resulting RectF will have an origin at the top left of the rectangle. + /// Unlike to_screen_ns_rect, coordinates are not converted and are assumed to already be in screen scale + fn to_rectf(&self) -> RectF; + + fn intersects(&self, other: Self) -> bool; +} +impl NSRectExt for NSRect { + fn to_window_rectf(&self, native_window: id) -> RectF { + unsafe { + self.origin.x; + let rect: NSRect = native_window.convertRectFromScreen_(*self); + rect.to_rectf() + } + } + + fn to_rectf(&self) -> RectF { + RectF::new( + vec2f( + self.origin.x as f32, + -(self.origin.y + self.size.height) as f32, + ), + vec2f(self.size.width as f32, self.size.height as f32), + ) + } + + fn intersects(&self, other: Self) -> bool { + self.size.width > 0. + && self.size.height > 0. + && other.size.width > 0. + && other.size.height > 0. + && self.origin.x <= other.origin.x + other.size.width + && self.origin.x + self.size.width >= other.origin.x + && self.origin.y <= other.origin.y + other.size.height + && self.origin.y + self.size.height >= other.origin.y } } diff --git a/crates/gpui/src/platform/mac/platform.rs b/crates/gpui/src/platform/mac/platform.rs index 0638689cd4..57827e1946 100644 --- a/crates/gpui/src/platform/mac/platform.rs +++ b/crates/gpui/src/platform/mac/platform.rs @@ -16,7 +16,7 @@ use cocoa::{ NSEventModifierFlags, NSMenu, NSMenuItem, NSModalResponse, NSOpenPanel, NSPasteboard, NSPasteboardTypeString, NSSavePanel, NSWindow, }, - base::{id, nil, selector, YES}, + base::{id, nil, selector, BOOL, YES}, foundation::{ NSArray, NSAutoreleasePool, NSBundle, NSData, NSInteger, NSProcessInfo, NSString, NSUInteger, NSURL, @@ -45,6 +45,7 @@ use std::{ ffi::{c_void, CStr, OsStr}, os::{raw::c_char, unix::ffi::OsStrExt}, path::{Path, PathBuf}, + process::Command, ptr, rc::Rc, slice, str, @@ -113,10 +114,8 @@ unsafe fn build_classes() { } } -#[derive(Default)] pub struct MacForegroundPlatform(RefCell); -#[derive(Default)] pub struct MacForegroundPlatformState { become_active: Option>, resign_active: Option>, @@ -128,9 +127,26 @@ pub struct MacForegroundPlatformState { open_urls: Option)>>, finish_launching: Option>, menu_actions: Vec>, + foreground: Rc, } impl MacForegroundPlatform { + pub fn new(foreground: Rc) -> Self { + Self(RefCell::new(MacForegroundPlatformState { + become_active: Default::default(), + resign_active: Default::default(), + quit: Default::default(), + event: Default::default(), + menu_command: Default::default(), + validate_menu_command: Default::default(), + will_open_menu: Default::default(), + open_urls: Default::default(), + finish_launching: Default::default(), + menu_actions: Default::default(), + foreground, + })) + } + unsafe fn create_menu_bar( &self, menus: Vec, @@ -184,7 +200,7 @@ impl MacForegroundPlatform { .map(|binding| binding.keystrokes()); let item; - if let Some(keystrokes) = keystrokes.flatten() { + if let Some(keystrokes) = keystrokes { if keystrokes.len() == 1 { let keystroke = &keystrokes[0]; let mut mask = NSEventModifierFlags::empty(); @@ -398,6 +414,26 @@ impl platform::ForegroundPlatform for MacForegroundPlatform { done_rx } } + + fn reveal_path(&self, path: &Path) { + unsafe { + let path = path.to_path_buf(); + self.0 + .borrow() + .foreground + .spawn(async move { + let full_path = ns_string(path.to_str().unwrap_or("")); + let root_full_path = ns_string(""); + let workspace: id = msg_send![class!(NSWorkspace), sharedWorkspace]; + let _: BOOL = msg_send![ + workspace, + selectFile: full_path + inFileViewerRootedAtPath: root_full_path + ]; + }) + .detach(); + } + } } pub struct MacPlatform { @@ -440,6 +476,10 @@ impl platform::Platform for MacPlatform { self.dispatcher.clone() } + fn fonts(&self) -> Arc { + self.fonts.clone() + } + fn activate(&self, ignoring_other_apps: bool) { unsafe { let app = NSApplication::sharedApplication(nil); @@ -488,6 +528,10 @@ impl platform::Platform for MacPlatform { } } + fn screen_by_id(&self, id: uuid::Uuid) -> Option> { + Screen::find_by_id(id).map(|screen| Rc::new(screen) as Rc<_>) + } + fn screens(&self) -> Vec> { Screen::all() .into_iter() @@ -512,10 +556,6 @@ impl platform::Platform for MacPlatform { Box::new(StatusItem::add(self.fonts())) } - fn fonts(&self) -> Arc { - self.fonts.clone() - } - fn write_to_clipboard(&self, item: ClipboardItem) { unsafe { self.pasteboard.clearContents(); @@ -699,7 +739,9 @@ impl platform::Platform for MacPlatform { unsafe { let cursor: id = match style { CursorStyle::Arrow => msg_send![class!(NSCursor), arrowCursor], - CursorStyle::ResizeLeftRight => msg_send![class!(NSCursor), resizeLeftRightCursor], + CursorStyle::ResizeLeftRight => { + msg_send![class!(NSCursor), resizeLeftRightCursor] + } CursorStyle::ResizeUpDown => msg_send![class!(NSCursor), resizeUpDownCursor], CursorStyle::PointingHand => msg_send![class!(NSCursor), pointingHandCursor], CursorStyle::IBeam => msg_send![class!(NSCursor), IBeamCursor], @@ -784,6 +826,21 @@ impl platform::Platform for MacPlatform { }) } } + + fn restart(&self) { + #[cfg(debug_assertions)] + let path = std::env::current_exe(); + + #[cfg(not(debug_assertions))] + let path = self.app_path().or_else(|_| std::env::current_exe()); + + let command = path.and_then(|path| Command::new("/usr/bin/open").arg(path).spawn()); + + match command { + Err(err) => log::error!("Unable to restart application {}", err), + Ok(_child) => self.quit(), + } + } } unsafe fn path_from_objc(path: id) -> PathBuf { @@ -853,8 +910,8 @@ extern "C" fn open_urls(this: &mut Object, _: Sel, _: id, urls: id) { (0..urls.count()) .into_iter() .filter_map(|i| { - let path = urls.objectAtIndex(i); - match CStr::from_ptr(path.absoluteString().UTF8String() as *mut c_char).to_str() { + let url = urls.objectAtIndex(i); + match CStr::from_ptr(url.absoluteString().UTF8String() as *mut c_char).to_str() { Ok(string) => Some(string.to_string()), Err(err) => { log::error!("error converting path to string: {}", err); diff --git a/crates/gpui/src/platform/mac/screen.rs b/crates/gpui/src/platform/mac/screen.rs index fdc7fbb505..98b6a66f03 100644 --- a/crates/gpui/src/platform/mac/screen.rs +++ b/crates/gpui/src/platform/mac/screen.rs @@ -1,14 +1,25 @@ -use std::any::Any; +use std::{any::Any, ffi::c_void}; -use crate::{ - geometry::vector::{vec2f, Vector2F}, - platform, -}; +use crate::platform; use cocoa::{ appkit::NSScreen, base::{id, nil}, - foundation::NSArray, + foundation::{NSArray, NSDictionary}, }; +use core_foundation::{ + number::{kCFNumberIntType, CFNumberGetValue, CFNumberRef}, + uuid::{CFUUIDGetUUIDBytes, CFUUIDRef}, +}; +use core_graphics::display::CGDirectDisplayID; +use pathfinder_geometry::rect::RectF; +use uuid::Uuid; + +use super::{geometry::NSRectExt, ns_string}; + +#[link(name = "ApplicationServices", kind = "framework")] +extern "C" { + pub fn CGDisplayCreateUUIDFromDisplayID(display: CGDirectDisplayID) -> CFUUIDRef; +} #[derive(Debug)] pub struct Screen { @@ -16,11 +27,23 @@ pub struct Screen { } impl Screen { + pub fn find_by_id(uuid: Uuid) -> Option { + unsafe { + let native_screens = NSScreen::screens(nil); + (0..NSArray::count(native_screens)) + .into_iter() + .map(|ix| Screen { + native_screen: native_screens.objectAtIndex(ix), + }) + .find(|screen| platform::Screen::display_uuid(screen) == Some(uuid)) + } + } + pub fn all() -> Vec { let mut screens = Vec::new(); unsafe { let native_screens = NSScreen::screens(nil); - for ix in 0..native_screens.count() { + for ix in 0..NSArray::count(native_screens) { screens.push(Screen { native_screen: native_screens.objectAtIndex(ix), }); @@ -35,10 +58,52 @@ impl platform::Screen for Screen { self } - fn size(&self) -> Vector2F { + fn display_uuid(&self) -> Option { + unsafe { + // Screen ids are not stable. Further, the default device id is also unstable across restarts. + // CGDisplayCreateUUIDFromDisplayID is stable but not exposed in the bindings we use. + // This approach is similar to that which winit takes + // https://github.com/rust-windowing/winit/blob/402cbd55f932e95dbfb4e8b5e8551c49e56ff9ac/src/platform_impl/macos/monitor.rs#L99 + let device_description = self.native_screen.deviceDescription(); + let key = ns_string("NSScreenNumber"); + let device_id_obj = device_description.objectForKey_(key); + let mut device_id: u32 = 0; + CFNumberGetValue( + device_id_obj as CFNumberRef, + kCFNumberIntType, + (&mut device_id) as *mut _ as *mut c_void, + ); + let cfuuid = CGDisplayCreateUUIDFromDisplayID(device_id as CGDirectDisplayID); + if cfuuid.is_null() { + return None; + } + + let bytes = CFUUIDGetUUIDBytes(cfuuid); + Some(Uuid::from_bytes([ + bytes.byte0, + bytes.byte1, + bytes.byte2, + bytes.byte3, + bytes.byte4, + bytes.byte5, + bytes.byte6, + bytes.byte7, + bytes.byte8, + bytes.byte9, + bytes.byte10, + bytes.byte11, + bytes.byte12, + bytes.byte13, + bytes.byte14, + bytes.byte15, + ])) + } + } + + fn bounds(&self) -> RectF { unsafe { let frame = self.native_screen.frame(); - vec2f(frame.size.width as f32, frame.size.height as f32) + frame.to_rectf() } } } diff --git a/crates/gpui/src/platform/mac/status_item.rs b/crates/gpui/src/platform/mac/status_item.rs index 33feb4808f..812027d35c 100644 --- a/crates/gpui/src/platform/mac/status_item.rs +++ b/crates/gpui/src/platform/mac/status_item.rs @@ -7,7 +7,7 @@ use crate::{ self, mac::{platform::NSViewLayerContentsRedrawDuringViewResize, renderer::Renderer}, }, - Event, FontSystem, Scene, + Event, FontSystem, Scene, WindowBounds, }; use cocoa::{ appkit::{NSScreen, NSSquareStatusItemLength, NSStatusBar, NSStatusItem, NSView, NSWindow}, @@ -32,6 +32,8 @@ use std::{ sync::Arc, }; +use super::screen::Screen; + static mut VIEW_CLASS: *const Class = ptr::null(); const STATE_IVAR: &str = "state"; @@ -167,28 +169,42 @@ impl StatusItem { } impl platform::Window for StatusItem { + fn bounds(&self) -> WindowBounds { + self.0.borrow().bounds() + } + + fn content_size(&self) -> Vector2F { + self.0.borrow().content_size() + } + + fn scale_factor(&self) -> f32 { + self.0.borrow().scale_factor() + } + + fn titlebar_height(&self) -> f32 { + 0. + } + + fn appearance(&self) -> crate::Appearance { + unsafe { + let appearance: id = + msg_send![self.0.borrow().native_item.button(), effectiveAppearance]; + crate::Appearance::from_native(appearance) + } + } + + fn screen(&self) -> Rc { + unsafe { + Rc::new(Screen { + native_screen: self.0.borrow().native_window().screen(), + }) + } + } + fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self } - fn on_event(&mut self, callback: Box bool>) { - self.0.borrow_mut().event_callback = Some(callback); - } - - fn on_appearance_changed(&mut self, callback: Box) { - self.0.borrow_mut().appearance_changed_callback = Some(callback); - } - - fn on_active_status_change(&mut self, _: Box) {} - - fn on_resize(&mut self, _: Box) {} - - fn on_fullscreen(&mut self, _: Box) {} - - fn on_should_close(&mut self, _: Box bool>) {} - - fn on_close(&mut self, _: Box) {} - fn set_input_handler(&mut self, _: Box) {} fn prompt( @@ -224,26 +240,6 @@ impl platform::Window for StatusItem { unimplemented!() } - fn toggle_full_screen(&self) { - unimplemented!() - } - - fn bounds(&self) -> RectF { - self.0.borrow().bounds() - } - - fn content_size(&self) -> Vector2F { - self.0.borrow().content_size() - } - - fn scale_factor(&self) -> f32 { - self.0.borrow().scale_factor() - } - - fn titlebar_height(&self) -> f32 { - 0. - } - fn present_scene(&mut self, scene: Scene) { self.0.borrow_mut().scene = Some(scene); unsafe { @@ -251,19 +247,39 @@ impl platform::Window for StatusItem { } } - fn appearance(&self) -> crate::Appearance { - unsafe { - let appearance: id = - msg_send![self.0.borrow().native_item.button(), effectiveAppearance]; - crate::Appearance::from_native(appearance) - } + fn toggle_full_screen(&self) { + unimplemented!() + } + + fn on_event(&mut self, callback: Box bool>) { + self.0.borrow_mut().event_callback = Some(callback); + } + + fn on_active_status_change(&mut self, _: Box) {} + + fn on_resize(&mut self, _: Box) {} + + fn on_fullscreen(&mut self, _: Box) {} + + fn on_moved(&mut self, _: Box) {} + + fn on_should_close(&mut self, _: Box bool>) {} + + fn on_close(&mut self, _: Box) {} + + fn on_appearance_changed(&mut self, callback: Box) { + self.0.borrow_mut().appearance_changed_callback = Some(callback); + } + + fn is_topmost_for_position(&self, _: Vector2F) -> bool { + true } } impl StatusItemState { - fn bounds(&self) -> RectF { + fn bounds(&self) -> WindowBounds { unsafe { - let window: id = msg_send![self.native_item.button(), window]; + let window: id = self.native_window(); let screen_frame = window.screen().visibleFrame(); let window_frame = NSWindow::frame(window); let origin = vec2f( @@ -275,7 +291,7 @@ impl StatusItemState { window_frame.size.width as f32, window_frame.size.height as f32, ); - RectF::new(origin, size) + WindowBounds::Fixed(RectF::new(origin, size)) } } @@ -293,6 +309,10 @@ impl StatusItemState { NSScreen::backingScaleFactor(window.screen()) as f32 } } + + pub fn native_window(&self) -> id { + unsafe { msg_send![self.native_item.button(), window] } + } } extern "C" fn dealloc_view(this: &Object, _: Sel) { diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index 6126533644..50e0de216c 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -19,12 +19,10 @@ use cocoa::{ appkit::{ CGPoint, NSApplication, NSBackingStoreBuffered, NSScreen, NSView, NSViewHeightSizable, NSViewWidthSizable, NSWindow, NSWindowButton, NSWindowCollectionBehavior, - NSWindowStyleMask, + NSWindowStyleMask, NSWindowTitleVisibility, }, base::{id, nil}, - foundation::{ - NSAutoreleasePool, NSInteger, NSNotFound, NSPoint, NSRect, NSSize, NSString, NSUInteger, - }, + foundation::{NSAutoreleasePool, NSInteger, NSPoint, NSRect, NSSize, NSString, NSUInteger}, }; use core_graphics::display::CGRect; use ctor::ctor; @@ -52,6 +50,11 @@ use std::{ time::Duration, }; +use super::{ + geometry::{NSRectExt, Vector2FExt}, + ns_string, NSRange, +}; + const WINDOW_STATE_IVAR: &str = "windowState"; static mut WINDOW_CLASS: *const Class = ptr::null(); @@ -76,56 +79,6 @@ const NSTrackingInVisibleRect: NSUInteger = 0x200; #[allow(non_upper_case_globals)] const NSWindowAnimationBehaviorUtilityWindow: NSInteger = 4; -#[repr(C)] -#[derive(Copy, Clone, Debug)] -struct NSRange { - pub location: NSUInteger, - pub length: NSUInteger, -} - -impl NSRange { - fn invalid() -> Self { - Self { - location: NSNotFound as NSUInteger, - length: 0, - } - } - - fn is_valid(&self) -> bool { - self.location != NSNotFound as NSUInteger - } - - fn to_range(self) -> Option> { - if self.is_valid() { - let start = self.location as usize; - let end = start + self.length as usize; - Some(start..end) - } else { - None - } - } -} - -impl From> for NSRange { - fn from(range: Range) -> Self { - NSRange { - location: range.start as NSUInteger, - length: range.len() as NSUInteger, - } - } -} - -unsafe impl objc::Encode for NSRange { - fn encode() -> objc::Encoding { - let encoding = format!( - "{{NSRange={}{}}}", - NSUInteger::encode().as_str(), - NSUInteger::encode().as_str() - ); - unsafe { objc::Encoding::from_str(&encoding) } - } -} - #[ctor] unsafe fn build_classes() { WINDOW_CLASS = build_window_class("GPUIWindow", class!(NSWindow)); @@ -295,6 +248,10 @@ unsafe fn build_window_class(name: &'static str, superclass: &Class) -> *const C sel!(windowWillExitFullScreen:), window_will_exit_fullscreen as extern "C" fn(&Object, Sel, id), ); + decl.add_method( + sel!(windowDidMove:), + window_did_move as extern "C" fn(&Object, Sel, id), + ); decl.add_method( sel!(windowDidBecomeKey:), window_did_change_key_status as extern "C" fn(&Object, Sel, id), @@ -311,8 +268,6 @@ unsafe fn build_window_class(name: &'static str, superclass: &Class) -> *const C decl.register() } -pub struct Window(Rc>); - ///Used to track what the IME does when we send it a keystroke. ///This is only used to handle the case where the IME mysteriously ///swallows certain keys. @@ -325,6 +280,11 @@ enum ImeState { None, } +struct InsertText { + replacement_range: Option>, + text: String, +} + struct WindowState { id: usize, native_window: id, @@ -333,6 +293,7 @@ struct WindowState { activate_callback: Option>, resize_callback: Option>, fullscreen_callback: Option>, + moved_callback: Option>, should_close_callback: Option bool>>, close_callback: Option>, appearance_changed_callback: Option>, @@ -352,11 +313,109 @@ struct WindowState { ime_text: Option, } -struct InsertText { - replacement_range: Option>, - text: String, +impl WindowState { + fn move_traffic_light(&self) { + if let Some(traffic_light_position) = self.traffic_light_position { + let titlebar_height = self.titlebar_height(); + + unsafe { + let close_button: id = msg_send![ + self.native_window, + standardWindowButton: NSWindowButton::NSWindowCloseButton + ]; + let min_button: id = msg_send![ + self.native_window, + standardWindowButton: NSWindowButton::NSWindowMiniaturizeButton + ]; + let zoom_button: id = msg_send![ + self.native_window, + standardWindowButton: NSWindowButton::NSWindowZoomButton + ]; + + let mut close_button_frame: CGRect = msg_send![close_button, frame]; + let mut min_button_frame: CGRect = msg_send![min_button, frame]; + let mut zoom_button_frame: CGRect = msg_send![zoom_button, frame]; + let mut origin = vec2f( + traffic_light_position.x(), + titlebar_height + - traffic_light_position.y() + - close_button_frame.size.height as f32, + ); + let button_spacing = + (min_button_frame.origin.x - close_button_frame.origin.x) as f32; + + close_button_frame.origin = CGPoint::new(origin.x() as f64, origin.y() as f64); + let _: () = msg_send![close_button, setFrame: close_button_frame]; + origin.set_x(origin.x() + button_spacing); + + min_button_frame.origin = CGPoint::new(origin.x() as f64, origin.y() as f64); + let _: () = msg_send![min_button, setFrame: min_button_frame]; + origin.set_x(origin.x() + button_spacing); + + zoom_button_frame.origin = CGPoint::new(origin.x() as f64, origin.y() as f64); + let _: () = msg_send![zoom_button, setFrame: zoom_button_frame]; + } + } + } + + fn is_fullscreen(&self) -> bool { + unsafe { + let style_mask = self.native_window.styleMask(); + style_mask.contains(NSWindowStyleMask::NSFullScreenWindowMask) + } + } + + fn bounds(&self) -> WindowBounds { + unsafe { + if self.is_fullscreen() { + return WindowBounds::Fullscreen; + } + + let window_frame = self.frame(); + if window_frame == self.native_window.screen().visibleFrame().to_rectf() { + WindowBounds::Maximized + } else { + WindowBounds::Fixed(window_frame) + } + } + } + + // Returns the window bounds in window coordinates + fn frame(&self) -> RectF { + unsafe { + let ns_frame = NSWindow::frame(self.native_window); + ns_frame.to_rectf() + } + } + + fn content_size(&self) -> Vector2F { + let NSSize { width, height, .. } = + unsafe { NSView::frame(self.native_window.contentView()) }.size; + vec2f(width as f32, height as f32) + } + + fn scale_factor(&self) -> f32 { + get_scale_factor(self.native_window) + } + + fn titlebar_height(&self) -> f32 { + unsafe { + let frame = NSWindow::frame(self.native_window); + let content_layout_rect: CGRect = msg_send![self.native_window, contentLayoutRect]; + (frame.size.height - content_layout_rect.size.height) as f32 + } + } + + fn present_scene(&mut self, scene: Scene) { + self.scene_to_render = Some(scene); + unsafe { + let _: () = msg_send![self.native_window.contentView(), setNeedsDisplay: YES]; + } + } } +pub struct Window(Rc>); + impl Window { pub fn open( id: usize, @@ -390,7 +449,7 @@ impl Window { } }; let native_window = native_window.initWithContentRect_styleMask_backing_defer_screen_( - RectF::new(Default::default(), vec2f(1024., 768.)).to_ns_rect(), + NSRect::new(NSPoint::new(0., 0.), NSSize::new(1024., 768.)), style_mask, NSBackingStoreBuffered, NO, @@ -405,30 +464,26 @@ impl Window { let screen = native_window.screen(); match options.bounds { + WindowBounds::Fullscreen => { + native_window.toggleFullScreen_(nil); + } WindowBounds::Maximized => { native_window.setFrame_display_(screen.visibleFrame(), YES); } - WindowBounds::Fixed(top_left_bounds) => { - let frame = screen.visibleFrame(); - let bottom_left_bounds = RectF::new( - vec2f( - top_left_bounds.origin_x(), - frame.size.height as f32 - - top_left_bounds.origin_y() - - top_left_bounds.height(), - ), - top_left_bounds.size(), - ) - .to_ns_rect(); - native_window.setFrame_display_( - native_window.convertRectToScreen_(bottom_left_bounds), - YES, - ); + WindowBounds::Fixed(rect) => { + let screen_frame = screen.visibleFrame(); + let ns_rect = rect.to_ns_rect(); + if ns_rect.intersects(screen_frame) { + native_window.setFrame_display_(ns_rect, YES); + } else { + native_window.setFrame_display_(screen_frame, YES); + } } } let native_view: id = msg_send![VIEW_CLASS, alloc]; let native_view = NSView::init(native_view); + assert!(!native_view.is_null()); let window = Self(Rc::new(RefCell::new(WindowState { @@ -441,6 +496,7 @@ impl Window { close_callback: None, activate_callback: None, fullscreen_callback: None, + moved_callback: None, appearance_changed_callback: None, input_handler: None, pending_key_down: None, @@ -480,6 +536,7 @@ impl Window { .map_or(true, |titlebar| titlebar.appears_transparent) { native_window.setTitlebarAppearsTransparent_(YES); + native_window.setTitleVisibility_(NSWindowTitleVisibility::NSWindowTitleHidden); } native_view.setAutoresizingMask_(NSViewWidthSizable | NSViewHeightSizable); @@ -576,34 +633,41 @@ impl Drop for Window { } impl platform::Window for Window { + fn bounds(&self) -> WindowBounds { + self.0.as_ref().borrow().bounds() + } + + fn content_size(&self) -> Vector2F { + self.0.as_ref().borrow().content_size() + } + + fn scale_factor(&self) -> f32 { + self.0.as_ref().borrow().scale_factor() + } + + fn titlebar_height(&self) -> f32 { + self.0.as_ref().borrow().titlebar_height() + } + + fn appearance(&self) -> crate::Appearance { + unsafe { + let appearance: id = msg_send![self.0.borrow().native_window, effectiveAppearance]; + crate::Appearance::from_native(appearance) + } + } + + fn screen(&self) -> Rc { + unsafe { + Rc::new(Screen { + native_screen: self.0.as_ref().borrow().native_window.screen(), + }) + } + } + fn as_any_mut(&mut self) -> &mut dyn Any { self } - fn on_event(&mut self, callback: Box bool>) { - self.0.as_ref().borrow_mut().event_callback = Some(callback); - } - - fn on_resize(&mut self, callback: Box) { - self.0.as_ref().borrow_mut().resize_callback = Some(callback); - } - - fn on_fullscreen(&mut self, callback: Box) { - self.0.as_ref().borrow_mut().fullscreen_callback = Some(callback); - } - - fn on_should_close(&mut self, callback: Box bool>) { - self.0.as_ref().borrow_mut().should_close_callback = Some(callback); - } - - fn on_close(&mut self, callback: Box) { - self.0.as_ref().borrow_mut().close_callback = Some(callback); - } - - fn on_active_status_change(&mut self, callback: Box) { - self.0.as_ref().borrow_mut().activate_callback = Some(callback); - } - fn set_input_handler(&mut self, input_handler: Box) { self.0.as_ref().borrow_mut().input_handler = Some(input_handler); } @@ -671,7 +735,8 @@ impl platform::Window for Window { let app = NSApplication::sharedApplication(nil); let window = self.0.borrow().native_window; let title = ns_string(title); - msg_send![app, changeWindowsItem:window title:title filename:false] + let _: () = msg_send![app, changeWindowsItem:window title:title filename:false]; + let _: () = msg_send![window, setTitle: title]; } } @@ -713,6 +778,10 @@ impl platform::Window for Window { .detach(); } + fn present_scene(&mut self, scene: Scene) { + self.0.as_ref().borrow_mut().present_scene(scene); + } + fn toggle_full_screen(&self) { let this = self.0.borrow(); let window = this.native_window; @@ -725,124 +794,65 @@ impl platform::Window for Window { .detach(); } - fn bounds(&self) -> RectF { - self.0.as_ref().borrow().bounds() + fn on_event(&mut self, callback: Box bool>) { + self.0.as_ref().borrow_mut().event_callback = Some(callback); } - fn content_size(&self) -> Vector2F { - self.0.as_ref().borrow().content_size() + fn on_active_status_change(&mut self, callback: Box) { + self.0.as_ref().borrow_mut().activate_callback = Some(callback); } - fn scale_factor(&self) -> f32 { - self.0.as_ref().borrow().scale_factor() + fn on_resize(&mut self, callback: Box) { + self.0.as_ref().borrow_mut().resize_callback = Some(callback); } - fn present_scene(&mut self, scene: Scene) { - self.0.as_ref().borrow_mut().present_scene(scene); + fn on_fullscreen(&mut self, callback: Box) { + self.0.as_ref().borrow_mut().fullscreen_callback = Some(callback); } - fn titlebar_height(&self) -> f32 { - self.0.as_ref().borrow().titlebar_height() + fn on_moved(&mut self, callback: Box) { + self.0.as_ref().borrow_mut().moved_callback = Some(callback); } - fn appearance(&self) -> crate::Appearance { - unsafe { - let appearance: id = msg_send![self.0.borrow().native_window, effectiveAppearance]; - crate::Appearance::from_native(appearance) - } + fn on_should_close(&mut self, callback: Box bool>) { + self.0.as_ref().borrow_mut().should_close_callback = Some(callback); + } + + fn on_close(&mut self, callback: Box) { + self.0.as_ref().borrow_mut().close_callback = Some(callback); } fn on_appearance_changed(&mut self, callback: Box) { self.0.borrow_mut().appearance_changed_callback = Some(callback); } -} -impl WindowState { - fn move_traffic_light(&self) { - if let Some(traffic_light_position) = self.traffic_light_position { - let titlebar_height = self.titlebar_height(); + fn is_topmost_for_position(&self, position: Vector2F) -> bool { + let self_borrow = self.0.borrow(); + let self_id = self_borrow.id; - unsafe { - let close_button: id = msg_send![ - self.native_window, - standardWindowButton: NSWindowButton::NSWindowCloseButton - ]; - let min_button: id = msg_send![ - self.native_window, - standardWindowButton: NSWindowButton::NSWindowMiniaturizeButton - ]; - let zoom_button: id = msg_send![ - self.native_window, - standardWindowButton: NSWindowButton::NSWindowZoomButton - ]; + unsafe { + let app = NSApplication::sharedApplication(nil); - let mut close_button_frame: CGRect = msg_send![close_button, frame]; - let mut min_button_frame: CGRect = msg_send![min_button, frame]; - let mut zoom_button_frame: CGRect = msg_send![zoom_button, frame]; - let mut origin = vec2f( - traffic_light_position.x(), - titlebar_height - - traffic_light_position.y() - - close_button_frame.size.height as f32, - ); - let button_spacing = - (min_button_frame.origin.x - close_button_frame.origin.x) as f32; + // Convert back to screen coordinates + let screen_point = position.to_screen_ns_point( + self_borrow.native_window, + self_borrow.content_size().y() as f64, + ); - close_button_frame.origin = CGPoint::new(origin.x() as f64, origin.y() as f64); - let _: () = msg_send![close_button, setFrame: close_button_frame]; - origin.set_x(origin.x() + button_spacing); + let window_number: NSInteger = msg_send![class!(NSWindow), windowNumberAtPoint:screen_point belowWindowWithWindowNumber:0]; + let top_most_window: id = msg_send![app, windowWithWindowNumber: window_number]; - min_button_frame.origin = CGPoint::new(origin.x() as f64, origin.y() as f64); - let _: () = msg_send![min_button, setFrame: min_button_frame]; - origin.set_x(origin.x() + button_spacing); - - zoom_button_frame.origin = CGPoint::new(origin.x() as f64, origin.y() as f64); - let _: () = msg_send![zoom_button, setFrame: zoom_button_frame]; + let is_panel: BOOL = msg_send![top_most_window, isKindOfClass: PANEL_CLASS]; + let is_window: BOOL = msg_send![top_most_window, isKindOfClass: WINDOW_CLASS]; + if is_panel == YES || is_window == YES { + let topmost_window_id = get_window_state(&*top_most_window).borrow().id; + topmost_window_id == self_id + } else { + // Someone else's window is on top + false } } } - - fn bounds(&self) -> RectF { - unsafe { - let screen_frame = self.native_window.screen().visibleFrame(); - let window_frame = NSWindow::frame(self.native_window); - let origin = vec2f( - window_frame.origin.x as f32, - (window_frame.origin.y - screen_frame.size.height - window_frame.size.height) - as f32, - ); - let size = vec2f( - window_frame.size.width as f32, - window_frame.size.height as f32, - ); - RectF::new(origin, size) - } - } - - fn content_size(&self) -> Vector2F { - let NSSize { width, height, .. } = - unsafe { NSView::frame(self.native_window.contentView()) }.size; - vec2f(width as f32, height as f32) - } - - fn scale_factor(&self) -> f32 { - get_scale_factor(self.native_window) - } - - fn titlebar_height(&self) -> f32 { - unsafe { - let frame = NSWindow::frame(self.native_window); - let content_layout_rect: CGRect = msg_send![self.native_window, contentLayoutRect]; - (frame.size.height - content_layout_rect.size.height) as f32 - } - } - - fn present_scene(&mut self, scene: Scene) { - self.scene_to_render = Some(scene); - unsafe { - let _: () = msg_send![self.native_window.contentView(), setNeedsDisplay: YES]; - } - } } fn get_scale_factor(native_window: id) -> f32 { @@ -1106,6 +1116,16 @@ fn window_fullscreen_changed(this: &Object, is_fullscreen: bool) { } } +extern "C" fn window_did_move(this: &Object, _: Sel, _: id) { + let window_state = unsafe { get_window_state(this) }; + let mut window_state_borrow = window_state.as_ref().borrow_mut(); + if let Some(mut callback) = window_state_borrow.moved_callback.take() { + drop(window_state_borrow); + callback(); + window_state.borrow_mut().moved_callback = Some(callback); + } +} + extern "C" fn window_did_change_key_status(this: &Object, selector: Sel, _: id) { let window_state = unsafe { get_window_state(this) }; let window_state_borrow = window_state.borrow(); @@ -1468,10 +1488,6 @@ async fn synthetic_drag( } } -unsafe fn ns_string(string: &str) -> id { - NSString::alloc(nil).init_str(string).autorelease() -} - fn with_input_handler(window: &Object, f: F) -> Option where F: FnOnce(&mut dyn InputHandler) -> R, diff --git a/crates/gpui/src/platform/test.rs b/crates/gpui/src/platform/test.rs index 00cd524c1d..194684bd12 100644 --- a/crates/gpui/src/platform/test.rs +++ b/crates/gpui/src/platform/test.rs @@ -5,7 +5,7 @@ use crate::{ vector::{vec2f, Vector2F}, }, keymap_matcher::KeymapMatcher, - Action, ClipboardItem, + Action, ClipboardItem, Menu, }; use anyhow::{anyhow, Result}; use collections::VecDeque; @@ -20,11 +20,20 @@ use std::{ }; use time::UtcOffset; -pub struct Platform { - dispatcher: Arc, - fonts: Arc, - current_clipboard_item: Mutex>, - cursor: Mutex, +struct Dispatcher; + +impl super::Dispatcher for Dispatcher { + fn is_main_thread(&self) -> bool { + true + } + + fn run_on_main_thread(&self, task: async_task::Runnable) { + task.run(); + } +} + +pub fn foreground_platform() -> ForegroundPlatform { + ForegroundPlatform::default() } #[derive(Default)] @@ -32,23 +41,6 @@ pub struct ForegroundPlatform { last_prompt_for_new_path_args: RefCell>)>>, } -struct Dispatcher; - -pub struct Window { - pub(crate) size: Vector2F, - scale_factor: f32, - current_scene: Option, - event_handlers: Vec bool>>, - pub(crate) resize_handlers: Vec>, - close_handlers: Vec>, - fullscreen_handlers: Vec>, - pub(crate) active_status_change_handlers: Vec>, - pub(crate) should_close_handler: Option bool>>, - pub(crate) title: Option, - pub(crate) edited: bool, - pub(crate) pending_prompts: RefCell>>, -} - #[cfg(any(test, feature = "test-support"))] impl ForegroundPlatform { pub(crate) fn simulate_new_path_selection( @@ -85,7 +77,7 @@ impl super::ForegroundPlatform for ForegroundPlatform { fn on_menu_command(&self, _: Box) {} fn on_validate_menu_command(&self, _: Box bool>) {} fn on_will_open_menu(&self, _: Box) {} - fn set_menus(&self, _: Vec, _: &KeymapMatcher) {} + fn set_menus(&self, _: Vec, _: &KeymapMatcher) {} fn prompt_for_paths( &self, @@ -100,6 +92,19 @@ impl super::ForegroundPlatform for ForegroundPlatform { *self.last_prompt_for_new_path_args.borrow_mut() = Some((path.to_path_buf(), done_tx)); done_rx } + + fn reveal_path(&self, _: &Path) {} +} + +pub fn platform() -> Platform { + Platform::new() +} + +pub struct Platform { + dispatcher: Arc, + fonts: Arc, + current_clipboard_item: Mutex>, + cursor: Mutex, } impl Platform { @@ -132,6 +137,10 @@ impl super::Platform for Platform { fn quit(&self) {} + fn screen_by_id(&self, _id: uuid::Uuid) -> Option> { + None + } + fn screens(&self) -> Vec> { Default::default() } @@ -143,7 +152,7 @@ impl super::Platform for Platform { _executor: Rc, ) -> Box { Box::new(Window::new(match options.bounds { - WindowBounds::Maximized => vec2f(1024., 768.), + WindowBounds::Maximized | WindowBounds::Fullscreen => vec2f(1024., 768.), WindowBounds::Fixed(rect) => rect.size(), })) } @@ -217,6 +226,41 @@ impl super::Platform for Platform { patch: 0, }) } + + fn restart(&self) {} +} + +#[derive(Debug)] +pub struct Screen; + +impl super::Screen for Screen { + fn as_any(&self) -> &dyn Any { + self + } + + fn bounds(&self) -> RectF { + RectF::new(Vector2F::zero(), Vector2F::new(1920., 1080.)) + } + + fn display_uuid(&self) -> Option { + Some(uuid::Uuid::new_v4()) + } +} + +pub struct Window { + pub(crate) size: Vector2F, + scale_factor: f32, + current_scene: Option, + event_handlers: Vec bool>>, + pub(crate) resize_handlers: Vec>, + pub(crate) moved_handlers: Vec>, + close_handlers: Vec>, + fullscreen_handlers: Vec>, + pub(crate) active_status_change_handlers: Vec>, + pub(crate) should_close_handler: Option bool>>, + pub(crate) title: Option, + pub(crate) edited: bool, + pub(crate) pending_prompts: RefCell>>, } impl Window { @@ -225,6 +269,7 @@ impl Window { size, event_handlers: Default::default(), resize_handlers: Default::default(), + moved_handlers: Default::default(), close_handlers: Default::default(), should_close_handler: Default::default(), active_status_change_handlers: Default::default(), @@ -242,41 +287,35 @@ impl Window { } } -impl super::Dispatcher for Dispatcher { - fn is_main_thread(&self) -> bool { - true - } - - fn run_on_main_thread(&self, task: async_task::Runnable) { - task.run(); - } -} - impl super::Window for Window { + fn bounds(&self) -> WindowBounds { + WindowBounds::Fixed(RectF::new(Vector2F::zero(), self.size)) + } + + fn content_size(&self) -> Vector2F { + self.size + } + + fn scale_factor(&self) -> f32 { + self.scale_factor + } + + fn titlebar_height(&self) -> f32 { + 24. + } + + fn appearance(&self) -> crate::Appearance { + crate::Appearance::Light + } + + fn screen(&self) -> Rc { + Rc::new(Screen) + } + fn as_any_mut(&mut self) -> &mut dyn Any { self } - fn on_event(&mut self, callback: Box bool>) { - self.event_handlers.push(callback); - } - - fn on_active_status_change(&mut self, callback: Box) { - self.active_status_change_handlers.push(callback); - } - - fn on_fullscreen(&mut self, callback: Box) { - self.fullscreen_handlers.push(callback) - } - - fn on_resize(&mut self, callback: Box) { - self.resize_handlers.push(callback); - } - - fn on_close(&mut self, callback: Box) { - self.close_handlers.push(callback); - } - fn set_input_handler(&mut self, _: Box) {} fn prompt(&self, _: crate::PromptLevel, _: &str, _: &[&str]) -> oneshot::Receiver { @@ -295,49 +334,49 @@ impl super::Window for Window { self.edited = edited; } - fn on_should_close(&mut self, callback: Box bool>) { - self.should_close_handler = Some(callback); - } - fn show_character_palette(&self) {} fn minimize(&self) {} fn zoom(&self) {} - fn toggle_full_screen(&self) {} - - fn bounds(&self) -> RectF { - RectF::new(Default::default(), self.size) - } - - fn content_size(&self) -> Vector2F { - self.size - } - - fn scale_factor(&self) -> f32 { - self.scale_factor - } - - fn titlebar_height(&self) -> f32 { - 24. - } - fn present_scene(&mut self, scene: crate::Scene) { self.current_scene = Some(scene); } - fn appearance(&self) -> crate::Appearance { - crate::Appearance::Light + fn toggle_full_screen(&self) {} + + fn on_event(&mut self, callback: Box bool>) { + self.event_handlers.push(callback); + } + + fn on_active_status_change(&mut self, callback: Box) { + self.active_status_change_handlers.push(callback); + } + + fn on_resize(&mut self, callback: Box) { + self.resize_handlers.push(callback); + } + + fn on_fullscreen(&mut self, callback: Box) { + self.fullscreen_handlers.push(callback) + } + + fn on_moved(&mut self, callback: Box) { + self.moved_handlers.push(callback); + } + + fn on_should_close(&mut self, callback: Box bool>) { + self.should_close_handler = Some(callback); + } + + fn on_close(&mut self, callback: Box) { + self.close_handlers.push(callback); } fn on_appearance_changed(&mut self, _: Box) {} -} -pub fn platform() -> Platform { - Platform::new() -} - -pub fn foreground_platform() -> ForegroundPlatform { - ForegroundPlatform::default() + fn is_topmost_for_position(&self, _position: Vector2F) -> bool { + true + } } diff --git a/crates/gpui/src/presenter.rs b/crates/gpui/src/presenter.rs index 0909d95fd0..c0785e11f3 100644 --- a/crates/gpui/src/presenter.rs +++ b/crates/gpui/src/presenter.rs @@ -4,7 +4,6 @@ use crate::{ font_cache::FontCache, geometry::rect::RectF, json::{self, ToJson}, - keymap_matcher::Keystroke, platform::{CursorStyle, Event}, scene::{ CursorRegion, MouseClick, MouseDown, MouseDownOut, MouseDrag, MouseEvent, MouseHover, @@ -23,7 +22,7 @@ use pathfinder_geometry::vector::{vec2f, Vector2F}; use serde_json::json; use smallvec::SmallVec; use sqlez::{ - bindable::{Bind, Column}, + bindable::{Bind, Column, StaticColumnCount}, statement::Statement, }; use std::{ @@ -316,7 +315,10 @@ impl Presenter { break; } } - cx.platform().set_cursor_style(style_to_assign); + + if cx.is_topmost_window_for_position(self.window_id, *position) { + cx.platform().set_cursor_style(style_to_assign); + } if !event_reused { if pressed_button.is_some() { @@ -601,14 +603,6 @@ pub struct LayoutContext<'a> { } impl<'a> LayoutContext<'a> { - pub(crate) fn keystrokes_for_action( - &mut self, - action: &dyn Action, - ) -> Option> { - self.app - .keystrokes_for_action(self.window_id, &self.view_stack, action) - } - fn layout(&mut self, view_id: usize, constraint: SizeConstraint) -> Vector2F { let print_error = |view_id| { format!( @@ -929,6 +923,7 @@ impl ToJson for Axis { } } +impl StaticColumnCount for Axis {} impl Bind for Axis { fn bind(&self, statement: &Statement, start_index: i32) -> anyhow::Result { match self { diff --git a/crates/gpui/src/presenter/event_dispatcher.rs b/crates/gpui/src/presenter/event_dispatcher.rs index 4c72334910..960c565bd4 100644 --- a/crates/gpui/src/presenter/event_dispatcher.rs +++ b/crates/gpui/src/presenter/event_dispatcher.rs @@ -209,6 +209,7 @@ impl EventDispatcher { break; } } + cx.platform().set_cursor_style(style_to_assign); if !event_reused { diff --git a/crates/gpui/src/test.rs b/crates/gpui/src/test.rs index eb992b638a..d784d43ece 100644 --- a/crates/gpui/src/test.rs +++ b/crates/gpui/src/test.rs @@ -1,14 +1,3 @@ -use crate::{ - elements::Empty, - executor::{self, ExecutorEvent}, - platform, - util::CwdBacktrace, - Element, ElementBox, Entity, FontCache, Handle, LeakDetector, MutableAppContext, Platform, - RenderContext, Subscription, TestAppContext, View, -}; -use futures::StreamExt; -use parking_lot::Mutex; -use smol::channel; use std::{ fmt::Write, panic::{self, RefUnwindSafe}, @@ -19,6 +8,20 @@ use std::{ }, }; +use futures::StreamExt; +use parking_lot::Mutex; +use smol::channel; + +use crate::{ + app::ref_counts::LeakDetector, + elements::Empty, + executor::{self, ExecutorEvent}, + platform, + util::CwdBacktrace, + Element, ElementBox, Entity, FontCache, Handle, MutableAppContext, Platform, RenderContext, + Subscription, TestAppContext, View, +}; + #[cfg(test)] #[ctor::ctor] fn init_logger() { diff --git a/crates/language/Cargo.toml b/crates/language/Cargo.toml index ab6c687b7a..3e6561e471 100644 --- a/crates/language/Cargo.toml +++ b/crates/language/Cargo.toml @@ -54,6 +54,7 @@ smol = "1.2" tree-sitter = "0.20" tree-sitter-rust = { version = "*", optional = true } tree-sitter-typescript = { version = "*", optional = true } +unicase = "2.6" [dev-dependencies] client = { path = "../client", features = ["test-support"] } @@ -65,13 +66,15 @@ settings = { path = "../settings", features = ["test-support"] } util = { path = "../util", features = ["test-support"] } ctor = "0.1" env_logger = "0.9" +indoc = "1.0.4" rand = "0.8.3" +tree-sitter-embedded-template = "*" tree-sitter-html = "*" tree-sitter-javascript = "*" tree-sitter-json = "*" +tree-sitter-markdown = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "330ecab87a3e3a7211ac69bbadc19eabecdb1cca" } tree-sitter-rust = "*" tree-sitter-python = "*" tree-sitter-typescript = "*" tree-sitter-ruby = "*" -tree-sitter-embedded-template = "*" unindent = "0.1.7" diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 3c382b83b6..03d2802591 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -41,7 +41,7 @@ pub use text::{Buffer as TextBuffer, BufferSnapshot as TextBufferSnapshot, Opera use theme::SyntaxTheme; #[cfg(any(test, feature = "test-support"))] use util::RandomCharIter; -use util::TryFutureExt as _; +use util::{RangeExt, TryFutureExt as _}; #[cfg(any(test, feature = "test-support"))] pub use {tree_sitter_rust, tree_sitter_typescript}; @@ -214,15 +214,6 @@ pub trait File: Send + Sync { fn is_deleted(&self) -> bool; - fn save( - &self, - buffer_id: u64, - text: Rope, - version: clock::Global, - line_ending: LineEnding, - cx: &mut MutableAppContext, - ) -> Task>; - fn as_any(&self) -> &dyn Any; fn to_proto(&self) -> rpc::proto::File; @@ -529,33 +520,6 @@ impl Buffer { self.file.as_ref() } - pub fn save( - &mut self, - cx: &mut ModelContext, - ) -> Task> { - let file = if let Some(file) = self.file.as_ref() { - file - } else { - return Task::ready(Err(anyhow!("buffer has no file"))); - }; - let text = self.as_rope().clone(); - let version = self.version(); - let save = file.save( - self.remote_id(), - text, - version, - self.line_ending(), - cx.as_mut(), - ); - cx.spawn(|this, mut cx| async move { - let (version, fingerprint, mtime) = save.await?; - this.update(&mut cx, |this, cx| { - this.did_save(version.clone(), fingerprint, mtime, None, cx); - }); - Ok((version, fingerprint, mtime)) - }) - } - pub fn saved_version(&self) -> &clock::Global { &self.saved_version } @@ -585,16 +549,11 @@ impl Buffer { version: clock::Global, fingerprint: RopeFingerprint, mtime: SystemTime, - new_file: Option>, cx: &mut ModelContext, ) { self.saved_version = version; self.saved_version_fingerprint = fingerprint; self.saved_mtime = mtime; - if let Some(new_file) = new_file { - self.file = Some(new_file); - self.file_update_count += 1; - } cx.emit(Event::Saved); cx.notify(); } @@ -661,36 +620,35 @@ impl Buffer { new_file: Arc, cx: &mut ModelContext, ) -> Task<()> { - let old_file = if let Some(file) = self.file.as_ref() { - file - } else { - return Task::ready(()); - }; let mut file_changed = false; let mut task = Task::ready(()); - if new_file.path() != old_file.path() { - file_changed = true; - } - - if new_file.is_deleted() { - if !old_file.is_deleted() { + if let Some(old_file) = self.file.as_ref() { + if new_file.path() != old_file.path() { file_changed = true; - if !self.is_dirty() { - cx.emit(Event::DirtyChanged); + } + + if new_file.is_deleted() { + if !old_file.is_deleted() { + file_changed = true; + if !self.is_dirty() { + cx.emit(Event::DirtyChanged); + } + } + } else { + let new_mtime = new_file.mtime(); + if new_mtime != old_file.mtime() { + file_changed = true; + + if !self.is_dirty() { + let reload = self.reload(cx).log_err().map(drop); + task = cx.foreground().spawn(reload); + } } } } else { - let new_mtime = new_file.mtime(); - if new_mtime != old_file.mtime() { - file_changed = true; - - if !self.is_dirty() { - let reload = self.reload(cx).log_err().map(drop); - task = cx.foreground().spawn(reload); - } - } - } + file_changed = true; + }; if file_changed { self.file_update_count += 1; @@ -797,6 +755,10 @@ impl Buffer { self.parsing_in_background } + pub fn contains_unknown_injections(&self) -> bool { + self.syntax_map.lock().contains_unknown_injections() + } + #[cfg(test)] pub fn set_sync_parse_timeout(&mut self, timeout: Duration) { self.sync_parse_timeout = timeout; @@ -825,7 +787,7 @@ impl Buffer { /// initiate an additional reparse recursively. To avoid concurrent parses /// for the same buffer, we only initiate a new parse if we are not already /// parsing in the background. - fn reparse(&mut self, cx: &mut ModelContext) { + pub fn reparse(&mut self, cx: &mut ModelContext) { if self.parsing_in_background { return; } @@ -842,13 +804,13 @@ impl Buffer { syntax_map.interpolate(&text); let language_registry = syntax_map.language_registry(); let mut syntax_snapshot = syntax_map.snapshot(); - let syntax_map_version = syntax_map.parsed_version(); drop(syntax_map); let parse_task = cx.background().spawn({ let language = language.clone(); + let language_registry = language_registry.clone(); async move { - syntax_snapshot.reparse(&syntax_map_version, &text, language_registry, language); + syntax_snapshot.reparse(&text, language_registry, language); syntax_snapshot } }); @@ -858,7 +820,7 @@ impl Buffer { .block_with_timeout(self.sync_parse_timeout, parse_task) { Ok(new_syntax_snapshot) => { - self.did_finish_parsing(new_syntax_snapshot, parsed_version, cx); + self.did_finish_parsing(new_syntax_snapshot, cx); return; } Err(parse_task) => { @@ -870,9 +832,15 @@ impl Buffer { this.language.as_ref().map_or(true, |current_language| { !Arc::ptr_eq(&language, current_language) }); - let parse_again = - this.version.changed_since(&parsed_version) || grammar_changed; - this.did_finish_parsing(new_syntax_map, parsed_version, cx); + let language_registry_changed = new_syntax_map + .contains_unknown_injections() + && language_registry.map_or(false, |registry| { + registry.version() != new_syntax_map.language_registry_version() + }); + let parse_again = language_registry_changed + || grammar_changed + || this.version.changed_since(&parsed_version); + this.did_finish_parsing(new_syntax_map, cx); this.parsing_in_background = false; if parse_again { this.reparse(cx); @@ -884,14 +852,9 @@ impl Buffer { } } - fn did_finish_parsing( - &mut self, - syntax_snapshot: SyntaxSnapshot, - version: clock::Global, - cx: &mut ModelContext, - ) { + fn did_finish_parsing(&mut self, syntax_snapshot: SyntaxSnapshot, cx: &mut ModelContext) { self.parse_count += 1; - self.syntax_map.lock().did_parse(syntax_snapshot, version); + self.syntax_map.lock().did_parse(syntax_snapshot); self.request_autoindent(cx); cx.emit(Event::Reparsed); cx.notify(); @@ -1384,12 +1347,12 @@ impl Buffer { .enumerate() .zip(&edit_operation.as_edit().unwrap().new_text) .map(|((ix, (range, _)), new_text)| { - let new_text_len = new_text.len(); + let new_text_length = new_text.len(); let old_start = range.start.to_point(&before_edit); let new_start = (delta + range.start as isize) as usize; - delta += new_text_len as isize - (range.end as isize - range.start as isize); + delta += new_text_length as isize - (range.end as isize - range.start as isize); - let mut range_of_insertion_to_indent = 0..new_text_len; + let mut range_of_insertion_to_indent = 0..new_text_length; let mut first_line_is_new = false; let mut original_indent_column = None; @@ -2242,7 +2205,6 @@ impl BufferSnapshot { .map(|g| g.outline_config.as_ref().unwrap()) .collect::>(); - let mut chunks = self.chunks(0..self.len(), true); let mut stack = Vec::>::new(); let mut items = Vec::new(); while let Some(mat) = matches.peek() { @@ -2261,9 +2223,7 @@ impl BufferSnapshot { continue; } - let mut text = String::new(); - let mut name_ranges = Vec::new(); - let mut highlight_ranges = Vec::new(); + let mut buffer_ranges = Vec::new(); for capture in mat.captures { let node_is_name; if capture.index == config.name_capture_ix { @@ -2281,12 +2241,27 @@ impl BufferSnapshot { range.start + self.line_len(start.row as u32) as usize - start.column; } + buffer_ranges.push((range, node_is_name)); + } + + if buffer_ranges.is_empty() { + continue; + } + + let mut text = String::new(); + let mut highlight_ranges = Vec::new(); + let mut name_ranges = Vec::new(); + let mut chunks = self.chunks( + buffer_ranges.first().unwrap().0.start..buffer_ranges.last().unwrap().0.end, + true, + ); + for (buffer_range, is_name) in buffer_ranges { if !text.is_empty() { text.push(' '); } - if node_is_name { + if is_name { let mut start = text.len(); - let end = start + range.len(); + let end = start + buffer_range.len(); // When multiple names are captured, then the matcheable text // includes the whitespace in between the names. @@ -2297,12 +2272,12 @@ impl BufferSnapshot { name_ranges.push(start..end); } - let mut offset = range.start; + let mut offset = buffer_range.start; chunks.seek(offset); for mut chunk in chunks.by_ref() { - if chunk.text.len() > range.end - offset { - chunk.text = &chunk.text[0..(range.end - offset)]; - offset = range.end; + if chunk.text.len() > buffer_range.end - offset { + chunk.text = &chunk.text[0..(buffer_range.end - offset)]; + offset = buffer_range.end; } else { offset += chunk.text.len(); } @@ -2316,7 +2291,7 @@ impl BufferSnapshot { highlight_ranges.push((start..end, style)); } text.push_str(chunk.text); - if offset >= range.end { + if offset >= buffer_range.end { break; } } @@ -2341,56 +2316,50 @@ impl BufferSnapshot { Some(items) } - pub fn enclosing_bracket_ranges( - &self, + /// Returns bracket range pairs overlapping or adjacent to `range` + pub fn bracket_ranges<'a, T: ToOffset>( + &'a self, range: Range, - ) -> Option<(Range, Range)> { + ) -> impl Iterator, Range)> + 'a { // Find bracket pairs that *inclusively* contain the given range. - let range = range.start.to_offset(self)..range.end.to_offset(self); - let mut matches = self.syntax.matches( - range.start.saturating_sub(1)..self.len().min(range.end + 1), - &self.text, - |grammar| grammar.brackets_config.as_ref().map(|c| &c.query), - ); + let range = range.start.to_offset(self).saturating_sub(1) + ..self.len().min(range.end.to_offset(self) + 1); + + let mut matches = self.syntax.matches(range.clone(), &self.text, |grammar| { + grammar.brackets_config.as_ref().map(|c| &c.query) + }); let configs = matches .grammars() .iter() .map(|grammar| grammar.brackets_config.as_ref().unwrap()) .collect::>(); - // Get the ranges of the innermost pair of brackets. - let mut result: Option<(Range, Range)> = None; - while let Some(mat) = matches.peek() { - let mut open = None; - let mut close = None; - let config = &configs[mat.grammar_index]; - for capture in mat.captures { - if capture.index == config.open_capture_ix { - open = Some(capture.node.byte_range()); - } else if capture.index == config.close_capture_ix { - close = Some(capture.node.byte_range()); + iter::from_fn(move || { + while let Some(mat) = matches.peek() { + let mut open = None; + let mut close = None; + let config = &configs[mat.grammar_index]; + for capture in mat.captures { + if capture.index == config.open_capture_ix { + open = Some(capture.node.byte_range()); + } else if capture.index == config.close_capture_ix { + close = Some(capture.node.byte_range()); + } } - } - matches.advance(); + matches.advance(); - let Some((open, close)) = open.zip(close) else { continue }; - if open.start > range.start || close.end < range.end { - continue; - } - let len = close.end - open.start; + let Some((open, close)) = open.zip(close) else { continue }; - if let Some((existing_open, existing_close)) = &result { - let existing_len = existing_close.end - existing_open.start; - if len > existing_len { + let bracket_range = open.start..=close.end; + if !bracket_range.overlaps(&range) { continue; } + + return Some((open, close)); } - - result = Some((open, close)); - } - - result + None + }) } #[allow(clippy::type_complexity)] diff --git a/crates/language/src/buffer_tests.rs b/crates/language/src/buffer_tests.rs index 0b2ef1d7a7..e6e7544763 100644 --- a/crates/language/src/buffer_tests.rs +++ b/crates/language/src/buffer_tests.rs @@ -3,6 +3,7 @@ use clock::ReplicaId; use collections::BTreeMap; use fs::LineEnding; use gpui::{ModelHandle, MutableAppContext}; +use indoc::indoc; use proto::deserialize_operation; use rand::prelude::*; use settings::Settings; @@ -15,7 +16,7 @@ use std::{ }; use text::network::Network; use unindent::Unindent as _; -use util::{post_inc, test::marked_text_ranges, RandomCharIter}; +use util::{assert_set_eq, post_inc, test::marked_text_ranges, RandomCharIter}; #[cfg(test)] #[ctor::ctor] @@ -51,7 +52,7 @@ fn test_line_endings(cx: &mut gpui::MutableAppContext) { #[gpui::test] fn test_select_language() { - let registry = LanguageRegistry::test(); + let registry = Arc::new(LanguageRegistry::test()); registry.add(Arc::new(Language::new( LanguageConfig { name: "Rust".into(), @@ -71,27 +72,33 @@ fn test_select_language() { // matching file extension assert_eq!( - registry.select_language("zed/lib.rs").map(|l| l.name()), + registry.language_for_path("zed/lib.rs").map(|l| l.name()), Some("Rust".into()) ); assert_eq!( - registry.select_language("zed/lib.mk").map(|l| l.name()), + registry.language_for_path("zed/lib.mk").map(|l| l.name()), Some("Make".into()) ); // matching filename assert_eq!( - registry.select_language("zed/Makefile").map(|l| l.name()), + registry.language_for_path("zed/Makefile").map(|l| l.name()), Some("Make".into()) ); // matching suffix that is not the full file extension or filename - assert_eq!(registry.select_language("zed/cars").map(|l| l.name()), None); assert_eq!( - registry.select_language("zed/a.cars").map(|l| l.name()), + registry.language_for_path("zed/cars").map(|l| l.name()), + None + ); + assert_eq!( + registry.language_for_path("zed/a.cars").map(|l| l.name()), + None + ); + assert_eq!( + registry.language_for_path("zed/sumk").map(|l| l.name()), None ); - assert_eq!(registry.select_language("zed/sumk").map(|l| l.name()), None); } #[gpui::test] @@ -570,53 +577,117 @@ async fn test_symbols_containing(cx: &mut gpui::TestAppContext) { #[gpui::test] fn test_enclosing_bracket_ranges(cx: &mut MutableAppContext) { - cx.set_global(Settings::test(cx)); - let buffer = cx.add_model(|cx| { - let text = " - mod x { - mod y { + let mut assert = |selection_text, range_markers| { + assert_bracket_pairs(selection_text, range_markers, rust_lang(), cx) + }; + assert( + indoc! {" + mod x { + moˇd y { + } } - " - .unindent(); - Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx) - }); - let buffer = buffer.read(cx); - assert_eq!( - buffer.enclosing_bracket_point_ranges(Point::new(1, 6)..Point::new(1, 6)), - Some(( - Point::new(0, 6)..Point::new(0, 7), - Point::new(4, 0)..Point::new(4, 1) - )) - ); - assert_eq!( - buffer.enclosing_bracket_point_ranges(Point::new(1, 10)..Point::new(1, 10)), - Some(( - Point::new(1, 10)..Point::new(1, 11), - Point::new(3, 4)..Point::new(3, 5) - )) - ); - assert_eq!( - buffer.enclosing_bracket_point_ranges(Point::new(3, 5)..Point::new(3, 5)), - Some(( - Point::new(1, 10)..Point::new(1, 11), - Point::new(3, 4)..Point::new(3, 5) - )) + let foo = 1;"}, + vec![indoc! {" + mod x «{» + mod y { + + } + «}» + let foo = 1;"}], ); - assert_eq!( - buffer.enclosing_bracket_point_ranges(Point::new(4, 1)..Point::new(4, 1)), - Some(( - Point::new(0, 6)..Point::new(0, 7), - Point::new(4, 0)..Point::new(4, 1) - )) + assert( + indoc! {" + mod x { + mod y ˇ{ + + } + } + let foo = 1;"}, + vec![ + indoc! {" + mod x «{» + mod y { + + } + «}» + let foo = 1;"}, + indoc! {" + mod x { + mod y «{» + + «}» + } + let foo = 1;"}, + ], + ); + + assert( + indoc! {" + mod x { + mod y { + + }ˇ + } + let foo = 1;"}, + vec![ + indoc! {" + mod x «{» + mod y { + + } + «}» + let foo = 1;"}, + indoc! {" + mod x { + mod y «{» + + «}» + } + let foo = 1;"}, + ], + ); + + assert( + indoc! {" + mod x { + mod y { + + } + ˇ} + let foo = 1;"}, + vec![indoc! {" + mod x «{» + mod y { + + } + «}» + let foo = 1;"}], + ); + + assert( + indoc! {" + mod x { + mod y { + + } + } + let fˇoo = 1;"}, + vec![], ); // Regression test: avoid crash when querying at the end of the buffer. - assert_eq!( - buffer.enclosing_bracket_point_ranges(Point::new(4, 1)..Point::new(5, 0)), - None + assert( + indoc! {" + mod x { + mod y { + + } + } + let foo = 1;ˇ"}, + vec![], ); } @@ -624,52 +695,34 @@ fn test_enclosing_bracket_ranges(cx: &mut MutableAppContext) { fn test_enclosing_bracket_ranges_where_brackets_are_not_outermost_children( cx: &mut MutableAppContext, ) { - let javascript_language = Arc::new( - Language::new( - LanguageConfig { - name: "JavaScript".into(), - ..Default::default() - }, - Some(tree_sitter_javascript::language()), - ) - .with_brackets_query( - r#" - ("{" @open "}" @close) - ("(" @open ")" @close) - "#, - ) - .unwrap(), - ); - - cx.set_global(Settings::test(cx)); - let buffer = cx.add_model(|cx| { - let text = " - for (const a in b) { - // a comment that's longer than the for-loop header - } - " - .unindent(); - Buffer::new(0, text, cx).with_language(javascript_language, cx) - }); - - let buffer = buffer.read(cx); - assert_eq!( - buffer.enclosing_bracket_point_ranges(Point::new(0, 18)..Point::new(0, 18)), - Some(( - Point::new(0, 4)..Point::new(0, 5), - Point::new(0, 17)..Point::new(0, 18) - )) + let mut assert = |selection_text, bracket_pair_texts| { + assert_bracket_pairs(selection_text, bracket_pair_texts, javascript_lang(), cx) + }; + + assert( + indoc! {" + for (const a in b)ˇ { + // a comment that's longer than the for-loop header + }"}, + vec![indoc! {" + for «(»const a in b«)» { + // a comment that's longer than the for-loop header + }"}], ); + eprintln!("-----------------------"); // Regression test: even though the parent node of the parentheses (the for loop) does // intersect the given range, the parentheses themselves do not contain the range, so // they should not be returned. Only the curly braces contain the range. - assert_eq!( - buffer.enclosing_bracket_point_ranges(Point::new(0, 20)..Point::new(0, 20)), - Some(( - Point::new(0, 19)..Point::new(0, 20), - Point::new(2, 0)..Point::new(2, 1) - )) + assert( + indoc! {" + for (const a in b) {ˇ + // a comment that's longer than the for-loop header + }"}, + vec![indoc! {" + for (const a in b) «{» + // a comment that's longer than the for-loop header + «}»"}], ); } @@ -1886,21 +1939,6 @@ fn test_contiguous_ranges() { ); } -impl Buffer { - pub fn enclosing_bracket_point_ranges( - &self, - range: Range, - ) -> Option<(Range, Range)> { - self.snapshot() - .enclosing_bracket_ranges(range) - .map(|(start, end)| { - let point_start = start.start.to_point(self)..start.end.to_point(self); - let point_end = end.start.to_point(self)..end.end.to_point(self); - (point_start, point_end) - }) - } -} - fn ruby_lang() -> Language { Language::new( LanguageConfig { @@ -1984,6 +2022,23 @@ fn json_lang() -> Language { ) } +fn javascript_lang() -> Language { + Language::new( + LanguageConfig { + name: "JavaScript".into(), + ..Default::default() + }, + Some(tree_sitter_javascript::language()), + ) + .with_brackets_query( + r#" + ("{" @open "}" @close) + ("(" @open ")" @close) + "#, + ) + .unwrap() +} + fn get_tree_sexp(buffer: &ModelHandle, cx: &gpui::TestAppContext) -> String { buffer.read_with(cx, |buffer, _| { let snapshot = buffer.snapshot(); @@ -1991,3 +2046,34 @@ fn get_tree_sexp(buffer: &ModelHandle, cx: &gpui::TestAppContext) -> Str layers[0].node.to_sexp() }) } + +// Assert that the enclosing bracket ranges around the selection match the pairs indicated by the marked text in `range_markers` +fn assert_bracket_pairs( + selection_text: &'static str, + bracket_pair_texts: Vec<&'static str>, + language: Language, + cx: &mut MutableAppContext, +) { + 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) + }); + let buffer = buffer.update(cx, |buffer, _cx| buffer.snapshot()); + + let selection_range = selection_ranges[0].clone(); + + let bracket_pairs = bracket_pair_texts + .into_iter() + .map(|pair_text| { + let (bracket_text, ranges) = marked_text_ranges(pair_text, false); + assert_eq!(bracket_text, expected_text); + (ranges[0].clone(), ranges[1].clone()) + }) + .collect::>(); + + assert_set_eq!( + buffer.bracket_ranges(selection_range).collect::>(), + bracket_pairs + ); +} diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 045e8dcd6f..4279ce6654 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -16,7 +16,7 @@ use futures::{ future::{BoxFuture, Shared}, FutureExt, TryFutureExt, }; -use gpui::{MutableAppContext, Task}; +use gpui::{executor::Background, MutableAppContext, Task}; use highlight_map::HighlightMap; use lazy_static::lazy_static; use parking_lot::{Mutex, RwLock}; @@ -26,6 +26,7 @@ use serde::{de, Deserialize, Deserializer}; use serde_json::Value; use std::{ any::Any, + borrow::Cow, cell::RefCell, fmt::Debug, hash::Hash, @@ -41,6 +42,7 @@ use std::{ use syntax_map::SyntaxSnapshot; use theme::{SyntaxTheme, Theme}; use tree_sitter::{self, Query}; +use unicase::UniCase; use util::ResultExt; #[cfg(any(test, feature = "test-support"))] @@ -88,8 +90,7 @@ pub struct CachedLspAdapter { } impl CachedLspAdapter { - pub async fn new(adapter: T) -> Arc { - let adapter = Box::new(adapter); + pub async fn new(adapter: Box) -> Arc { let name = adapter.name().await; let server_args = adapter.server_args().await; let initialization_options = adapter.initialization_options().await; @@ -247,6 +248,16 @@ pub struct LanguageConfig { pub overrides: HashMap, } +#[derive(Debug, Default)] +pub struct LanguageQueries { + pub highlights: Option>, + pub brackets: Option>, + pub indents: Option>, + pub outline: Option>, + pub injections: Option>, + pub overrides: Option>, +} + #[derive(Clone)] pub struct LanguageScope { language: Arc, @@ -406,8 +417,17 @@ pub enum LanguageServerBinaryStatus { Failed { error: String }, } +struct AvailableLanguage { + path: &'static str, + config: LanguageConfig, + grammar: tree_sitter::Language, + lsp_adapter: Option>, + get_queries: fn(&str) -> LanguageQueries, +} + pub struct LanguageRegistry { languages: RwLock>>, + available_languages: RwLock>, language_server_download_dir: Option>, lsp_binary_statuses_tx: async_broadcast::Sender<(Arc, LanguageServerBinaryStatus)>, lsp_binary_statuses_rx: async_broadcast::Receiver<(Arc, LanguageServerBinaryStatus)>, @@ -421,6 +441,8 @@ pub struct LanguageRegistry { >, subscription: RwLock<(watch::Sender<()>, watch::Receiver<()>)>, theme: RwLock>>, + executor: Option>, + version: AtomicUsize, } impl LanguageRegistry { @@ -429,12 +451,15 @@ impl LanguageRegistry { Self { language_server_download_dir: None, languages: Default::default(), + available_languages: Default::default(), lsp_binary_statuses_tx, lsp_binary_statuses_rx, login_shell_env_loaded: login_shell_env_loaded.shared(), lsp_binary_paths: Default::default(), subscription: RwLock::new(watch::channel()), theme: Default::default(), + version: Default::default(), + executor: None, } } @@ -443,11 +468,50 @@ impl LanguageRegistry { Self::new(Task::ready(())) } + pub fn set_executor(&mut self, executor: Arc) { + self.executor = Some(executor); + } + + pub fn register( + &self, + path: &'static str, + config: LanguageConfig, + grammar: tree_sitter::Language, + lsp_adapter: Option>, + get_queries: fn(&str) -> LanguageQueries, + ) { + self.available_languages.write().push(AvailableLanguage { + path, + config, + grammar, + lsp_adapter, + get_queries, + }); + } + + pub fn language_names(&self) -> Vec { + let mut result = self + .available_languages + .read() + .iter() + .map(|l| l.config.name.to_string()) + .chain( + self.languages + .read() + .iter() + .map(|l| l.config.name.to_string()), + ) + .collect::>(); + result.sort_unstable(); + result + } + pub fn add(&self, language: Arc) { if let Some(theme) = self.theme.read().clone() { language.set_theme(&theme.editor.syntax); } self.languages.write().push(language); + self.version.fetch_add(1, SeqCst); *self.subscription.write().0.borrow_mut() = (); } @@ -455,6 +519,10 @@ impl LanguageRegistry { self.subscription.read().1.clone() } + pub fn version(&self) -> usize { + self.version.load(SeqCst) + } + pub fn set_theme(&self, theme: Arc) { *self.theme.write() = Some(theme.clone()); for language in self.languages.read().iter() { @@ -466,42 +534,79 @@ impl LanguageRegistry { self.language_server_download_dir = Some(path.into()); } - pub fn get_language(&self, name: &str) -> Option> { - self.languages - .read() - .iter() - .find(|language| language.name().to_lowercase() == name.to_lowercase()) - .cloned() + pub fn language_for_name(self: &Arc, name: &str) -> Option> { + let name = UniCase::new(name); + self.get_or_load_language(|config| UniCase::new(config.name.as_ref()) == name) } - pub fn to_vec(&self) -> Vec> { - self.languages.read().iter().cloned().collect() + pub fn language_for_name_or_extension(self: &Arc, string: &str) -> Option> { + let string = UniCase::new(string); + self.get_or_load_language(|config| { + UniCase::new(config.name.as_ref()) == string + || config + .path_suffixes + .iter() + .any(|suffix| UniCase::new(suffix) == string) + }) } - pub fn language_names(&self) -> Vec { - self.languages - .read() - .iter() - .map(|language| language.name().to_string()) - .collect() - } - - pub fn select_language(&self, path: impl AsRef) -> Option> { + pub fn language_for_path(self: &Arc, path: impl AsRef) -> Option> { let path = path.as_ref(); let filename = path.file_name().and_then(|name| name.to_str()); let extension = path.extension().and_then(|name| name.to_str()); let path_suffixes = [extension, filename]; - self.languages + self.get_or_load_language(|config| { + config + .path_suffixes + .iter() + .any(|suffix| path_suffixes.contains(&Some(suffix.as_str()))) + }) + } + + fn get_or_load_language( + self: &Arc, + callback: impl Fn(&LanguageConfig) -> bool, + ) -> Option> { + if let Some(language) = self + .languages .read() .iter() - .find(|language| { - language - .config - .path_suffixes - .iter() - .any(|suffix| path_suffixes.contains(&Some(suffix.as_str()))) - }) - .cloned() + .find(|language| callback(&language.config)) + { + return Some(language.clone()); + } + + if let Some(executor) = self.executor.clone() { + let mut available_languages = self.available_languages.write(); + + if let Some(ix) = available_languages.iter().position(|l| callback(&l.config)) { + let language = available_languages.remove(ix); + drop(available_languages); + let name = language.config.name.clone(); + let this = self.clone(); + executor + .spawn(async move { + let queries = (language.get_queries)(&language.path); + let language = Language::new(language.config, Some(language.grammar)) + .with_lsp_adapter(language.lsp_adapter) + .await; + match language.with_queries(queries) { + Ok(language) => this.add(Arc::new(language)), + Err(err) => { + log::error!("failed to load language {}: {}", name, err); + return; + } + }; + }) + .detach(); + } + } + + None + } + + pub fn to_vec(&self) -> Vec> { + self.languages.read().iter().cloned().collect() } pub fn start_language_server( @@ -705,12 +810,70 @@ impl Language { self.grammar.as_ref().map(|g| g.id) } + pub fn with_queries(mut self, queries: LanguageQueries) -> Result { + if let Some(query) = queries.highlights { + self = self + .with_highlights_query(query.as_ref()) + .expect("failed to evaluate highlights query"); + } + if let Some(query) = queries.brackets { + self = self + .with_brackets_query(query.as_ref()) + .expect("failed to load brackets query"); + } + if let Some(query) = queries.indents { + self = self + .with_indents_query(query.as_ref()) + .expect("failed to load indents query"); + } + if let Some(query) = queries.outline { + self = self + .with_outline_query(query.as_ref()) + .expect("failed to load outline query"); + } + if let Some(query) = queries.injections { + self = self + .with_injection_query(query.as_ref()) + .expect("failed to load injection query"); + } + if let Some(query) = queries.overrides { + self = self + .with_override_query(query.as_ref()) + .expect("failed to load override query"); + } + Ok(self) + } pub fn with_highlights_query(mut self, source: &str) -> Result { let grammar = self.grammar_mut(); grammar.highlights_query = Some(Query::new(grammar.ts_language, source)?); Ok(self) } + pub fn with_outline_query(mut self, source: &str) -> Result { + let grammar = self.grammar_mut(); + let query = Query::new(grammar.ts_language, source)?; + let mut item_capture_ix = None; + let mut name_capture_ix = None; + let mut context_capture_ix = None; + get_capture_indices( + &query, + &mut [ + ("item", &mut item_capture_ix), + ("name", &mut name_capture_ix), + ("context", &mut context_capture_ix), + ], + ); + if let Some((item_capture_ix, name_capture_ix)) = item_capture_ix.zip(name_capture_ix) { + grammar.outline_config = Some(OutlineConfig { + query, + item_capture_ix, + name_capture_ix, + context_capture_ix, + }); + } + Ok(self) + } + pub fn with_brackets_query(mut self, source: &str) -> Result { let grammar = self.grammar_mut(); let query = Query::new(grammar.ts_language, source)?; @@ -761,31 +924,6 @@ impl Language { Ok(self) } - pub fn with_outline_query(mut self, source: &str) -> Result { - let grammar = self.grammar_mut(); - let query = Query::new(grammar.ts_language, source)?; - let mut item_capture_ix = None; - let mut name_capture_ix = None; - let mut context_capture_ix = None; - get_capture_indices( - &query, - &mut [ - ("item", &mut item_capture_ix), - ("name", &mut name_capture_ix), - ("context", &mut context_capture_ix), - ], - ); - if let Some((item_capture_ix, name_capture_ix)) = item_capture_ix.zip(name_capture_ix) { - grammar.outline_config = Some(OutlineConfig { - query, - item_capture_ix, - name_capture_ix, - context_capture_ix, - }); - } - Ok(self) - } - pub fn with_injection_query(mut self, source: &str) -> Result { let grammar = self.grammar_mut(); let query = Query::new(grammar.ts_language, source)?; @@ -858,8 +996,10 @@ impl Language { Arc::get_mut(self.grammar.as_mut().unwrap()).unwrap() } - pub fn with_lsp_adapter(mut self, lsp_adapter: Arc) -> Self { - self.adapter = Some(lsp_adapter); + pub async fn with_lsp_adapter(mut self, lsp_adapter: Option>) -> Self { + if let Some(adapter) = lsp_adapter { + self.adapter = Some(CachedLspAdapter::new(adapter).await); + } self } @@ -870,7 +1010,7 @@ impl Language { ) -> mpsc::UnboundedReceiver { let (servers_tx, servers_rx) = mpsc::unbounded(); self.fake_adapter = Some((servers_tx, fake_lsp_adapter.clone())); - let adapter = CachedLspAdapter::new(fake_lsp_adapter).await; + let adapter = CachedLspAdapter::new(Box::new(fake_lsp_adapter)).await; self.adapter = Some(adapter); servers_rx } diff --git a/crates/language/src/syntax_map.rs b/crates/language/src/syntax_map.rs index 8d66730854..670f479f10 100644 --- a/crates/language/src/syntax_map.rs +++ b/crates/language/src/syntax_map.rs @@ -5,8 +5,9 @@ use parking_lot::Mutex; use std::{ borrow::Cow, cell::RefCell, - cmp::{Ordering, Reverse}, + cmp::{self, Ordering, Reverse}, collections::BinaryHeap, + iter, ops::{Deref, DerefMut, Range}, sync::Arc, }; @@ -26,8 +27,6 @@ lazy_static! { #[derive(Default)] pub struct SyntaxMap { - parsed_version: clock::Global, - interpolated_version: clock::Global, snapshot: SyntaxSnapshot, language_registry: Option>, } @@ -35,6 +34,9 @@ pub struct SyntaxMap { #[derive(Clone, Default)] pub struct SyntaxSnapshot { layers: SumTree, + parsed_version: clock::Global, + interpolated_version: clock::Global, + language_registry_version: usize, } #[derive(Default)] @@ -89,8 +91,34 @@ struct SyntaxMapMatchesLayer<'a> { struct SyntaxLayer { depth: usize, range: Range, - tree: tree_sitter::Tree, - language: Arc, + content: SyntaxLayerContent, +} + +#[derive(Clone)] +enum SyntaxLayerContent { + Parsed { + tree: tree_sitter::Tree, + language: Arc, + }, + Pending { + language_name: Arc, + }, +} + +impl SyntaxLayerContent { + fn language_id(&self) -> Option { + match self { + SyntaxLayerContent::Parsed { language, .. } => language.id(), + SyntaxLayerContent::Pending { .. } => None, + } + } + + fn tree(&self) -> Option<&Tree> { + match self { + SyntaxLayerContent::Parsed { tree, .. } => Some(tree), + SyntaxLayerContent::Pending { .. } => None, + } + } } #[derive(Debug)] @@ -107,6 +135,7 @@ struct SyntaxLayerSummary { range: Range, last_layer_range: Range, last_layer_language: Option, + contains_unknown_injections: bool, } #[derive(Clone, Debug)] @@ -130,12 +159,26 @@ struct SyntaxLayerPositionBeforeChange { struct ParseStep { depth: usize, - language: Arc, + language: ParseStepLanguage, range: Range, included_ranges: Vec, mode: ParseMode, } +enum ParseStepLanguage { + Loaded { language: Arc }, + Pending { name: Arc }, +} + +impl ParseStepLanguage { + fn id(&self) -> Option { + match self { + ParseStepLanguage::Loaded { language } => language.id(), + ParseStepLanguage::Pending { .. } => None, + } + } +} + enum ParseMode { Single, Combined { @@ -176,30 +219,17 @@ impl SyntaxMap { self.language_registry.clone() } - pub fn parsed_version(&self) -> clock::Global { - self.parsed_version.clone() - } - pub fn interpolate(&mut self, text: &BufferSnapshot) { - self.snapshot.interpolate(&self.interpolated_version, text); - self.interpolated_version = text.version.clone(); + self.snapshot.interpolate(text); } #[cfg(test)] pub fn reparse(&mut self, language: Arc, text: &BufferSnapshot) { - self.snapshot.reparse( - &self.parsed_version, - text, - self.language_registry.clone(), - language, - ); - self.parsed_version = text.version.clone(); - self.interpolated_version = text.version.clone(); + self.snapshot + .reparse(text, self.language_registry.clone(), language); } - pub fn did_parse(&mut self, snapshot: SyntaxSnapshot, version: clock::Global) { - self.interpolated_version = version.clone(); - self.parsed_version = version; + pub fn did_parse(&mut self, snapshot: SyntaxSnapshot) { self.snapshot = snapshot; } @@ -213,10 +243,12 @@ impl SyntaxSnapshot { self.layers.is_empty() } - pub fn interpolate(&mut self, from_version: &clock::Global, text: &BufferSnapshot) { + fn interpolate(&mut self, text: &BufferSnapshot) { let edits = text - .anchored_edits_since::<(usize, Point)>(&from_version) + .anchored_edits_since::<(usize, Point)>(&self.interpolated_version) .collect::>(); + self.interpolated_version = text.version().clone(); + if edits.is_empty() { return; } @@ -276,47 +308,49 @@ impl SyntaxSnapshot { } let mut layer = layer.clone(); - for (edit, edit_range) in &edits[first_edit_ix_for_depth..] { - // Ignore any edits that follow this layer. - if edit_range.start.cmp(&layer.range.end, text).is_ge() { - break; + if let SyntaxLayerContent::Parsed { tree, .. } = &mut layer.content { + for (edit, edit_range) in &edits[first_edit_ix_for_depth..] { + // Ignore any edits that follow this layer. + if edit_range.start.cmp(&layer.range.end, text).is_ge() { + break; + } + + // Apply any edits that intersect this layer to the layer's syntax tree. + let tree_edit = if edit_range.start.cmp(&layer.range.start, text).is_ge() { + tree_sitter::InputEdit { + start_byte: edit.new.start.0 - start_byte, + old_end_byte: edit.new.start.0 - start_byte + + (edit.old.end.0 - edit.old.start.0), + new_end_byte: edit.new.end.0 - start_byte, + start_position: (edit.new.start.1 - start_point).to_ts_point(), + old_end_position: (edit.new.start.1 - start_point + + (edit.old.end.1 - edit.old.start.1)) + .to_ts_point(), + new_end_position: (edit.new.end.1 - start_point).to_ts_point(), + } + } else { + let node = tree.root_node(); + tree_sitter::InputEdit { + start_byte: 0, + old_end_byte: node.end_byte(), + new_end_byte: 0, + start_position: Default::default(), + old_end_position: node.end_position(), + new_end_position: Default::default(), + } + }; + + tree.edit(&tree_edit); } - // Apply any edits that intersect this layer to the layer's syntax tree. - let tree_edit = if edit_range.start.cmp(&layer.range.start, text).is_ge() { - tree_sitter::InputEdit { - start_byte: edit.new.start.0 - start_byte, - old_end_byte: edit.new.start.0 - start_byte - + (edit.old.end.0 - edit.old.start.0), - new_end_byte: edit.new.end.0 - start_byte, - start_position: (edit.new.start.1 - start_point).to_ts_point(), - old_end_position: (edit.new.start.1 - start_point - + (edit.old.end.1 - edit.old.start.1)) - .to_ts_point(), - new_end_position: (edit.new.end.1 - start_point).to_ts_point(), - } - } else { - let node = layer.tree.root_node(); - tree_sitter::InputEdit { - start_byte: 0, - old_end_byte: node.end_byte(), - new_end_byte: 0, - start_position: Default::default(), - old_end_position: node.end_position(), - new_end_position: Default::default(), - } - }; - - layer.tree.edit(&tree_edit); + debug_assert!( + tree.root_node().end_byte() <= text.len(), + "tree's size {}, is larger than text size {}", + tree.root_node().end_byte(), + text.len(), + ); } - debug_assert!( - layer.tree.root_node().end_byte() <= text.len(), - "tree's size {}, is larger than text size {}", - layer.tree.root_node().end_byte(), - text.len(), - ); - layers.push(layer, text); cursor.next(text); } @@ -328,12 +362,58 @@ impl SyntaxSnapshot { pub fn reparse( &mut self, - from_version: &clock::Global, text: &BufferSnapshot, registry: Option>, root_language: Arc, ) { - let edits = text.edits_since::(from_version).collect::>(); + let edit_ranges = text + .edits_since::(&self.parsed_version) + .map(|edit| edit.new) + .collect::>(); + self.reparse_with_ranges(text, root_language.clone(), edit_ranges, registry.as_ref()); + + if let Some(registry) = registry { + if registry.version() != self.language_registry_version { + let mut resolved_injection_ranges = Vec::new(); + let mut cursor = self + .layers + .filter::<_, ()>(|summary| summary.contains_unknown_injections); + cursor.next(text); + while let Some(layer) = cursor.item() { + let SyntaxLayerContent::Pending { language_name } = &layer.content else { unreachable!() }; + if { + let language_registry = ®istry; + language_registry.language_for_name_or_extension(language_name) + } + .is_some() + { + resolved_injection_ranges.push(layer.range.to_offset(text)); + } + + cursor.next(text); + } + drop(cursor); + + if !resolved_injection_ranges.is_empty() { + self.reparse_with_ranges( + text, + root_language, + resolved_injection_ranges, + Some(®istry), + ); + } + self.language_registry_version = registry.version(); + } + } + } + + fn reparse_with_ranges( + &mut self, + text: &BufferSnapshot, + root_language: Arc, + invalidated_ranges: Vec>, + registry: Option<&Arc>, + ) { let max_depth = self.layers.summary().max_depth; let mut cursor = self.layers.cursor::(); cursor.next(&text); @@ -344,7 +424,9 @@ impl SyntaxSnapshot { let mut combined_injection_ranges = HashMap::default(); queue.push(ParseStep { depth: 0, - language: root_language.clone(), + language: ParseStepLanguage::Loaded { + language: root_language, + }, included_ranges: vec![tree_sitter::Range { start_byte: 0, end_byte: text.len(), @@ -415,12 +497,11 @@ impl SyntaxSnapshot { let (step_start_byte, step_start_point) = step.range.start.summary::<(usize, Point)>(text); let step_end_byte = step.range.end.to_offset(text); - let Some(grammar) = step.language.grammar.as_deref() else { continue }; let mut old_layer = cursor.item(); if let Some(layer) = old_layer { if layer.range.to_offset(text) == (step_start_byte..step_end_byte) - && layer.language.id() == step.language.id() + && layer.content.language_id() == step.language.id() { cursor.next(&text); } else { @@ -428,89 +509,130 @@ impl SyntaxSnapshot { } } - let tree; - let changed_ranges; - let mut included_ranges = step.included_ranges; - if let Some(old_layer) = old_layer { - if let ParseMode::Combined { - parent_layer_changed_ranges, - .. - } = step.mode - { - included_ranges = splice_included_ranges( - old_layer.tree.included_ranges(), - &parent_layer_changed_ranges, - &included_ranges, - ); - } + let content = match step.language { + ParseStepLanguage::Loaded { language } => { + let Some(grammar) = language.grammar() else { continue }; + let tree; + let changed_ranges; + let mut included_ranges = step.included_ranges; + if let Some(SyntaxLayerContent::Parsed { tree: old_tree, .. }) = + old_layer.map(|layer| &layer.content) + { + if let ParseMode::Combined { + parent_layer_changed_ranges, + .. + } = step.mode + { + included_ranges = splice_included_ranges( + old_tree.included_ranges(), + &parent_layer_changed_ranges, + &included_ranges, + ); + } - tree = parse_text( - grammar, - text.as_rope(), - step_start_byte, - step_start_point, - included_ranges, - Some(old_layer.tree.clone()), - ); - changed_ranges = join_ranges( - edits.iter().map(|e| e.new.clone()).filter(|range| { - range.start <= step_end_byte && range.end >= step_start_byte - }), - old_layer - .tree - .changed_ranges(&tree) - .map(|r| step_start_byte + r.start_byte..step_start_byte + r.end_byte), - ); - } else { - tree = parse_text( - grammar, - text.as_rope(), - step_start_byte, - step_start_point, - included_ranges, - None, - ); - changed_ranges = vec![step_start_byte..step_end_byte]; - } + tree = parse_text( + grammar, + text.as_rope(), + step_start_byte, + step_start_point, + included_ranges, + Some(old_tree.clone()), + ); + changed_ranges = join_ranges( + invalidated_ranges.iter().cloned().filter(|range| { + range.start <= step_end_byte && range.end >= step_start_byte + }), + old_tree.changed_ranges(&tree).map(|r| { + step_start_byte + r.start_byte..step_start_byte + r.end_byte + }), + ); + } else { + tree = parse_text( + grammar, + text.as_rope(), + step_start_byte, + step_start_point, + included_ranges, + None, + ); + changed_ranges = vec![step_start_byte..step_end_byte]; + } + + if let (Some((config, registry)), false) = ( + grammar.injection_config.as_ref().zip(registry.as_ref()), + changed_ranges.is_empty(), + ) { + for range in &changed_ranges { + changed_regions.insert( + ChangedRegion { + depth: step.depth + 1, + range: text.anchor_before(range.start) + ..text.anchor_after(range.end), + }, + text, + ); + } + get_injections( + config, + text, + tree.root_node_with_offset( + step_start_byte, + step_start_point.to_ts_point(), + ), + registry, + step.depth + 1, + &changed_ranges, + &mut combined_injection_ranges, + &mut queue, + ); + } + + SyntaxLayerContent::Parsed { tree, language } + } + ParseStepLanguage::Pending { name } => SyntaxLayerContent::Pending { + language_name: name, + }, + }; layers.push( SyntaxLayer { depth: step.depth, range: step.range, - tree: tree.clone(), - language: step.language.clone(), + content, }, &text, ); - - if let (Some((config, registry)), false) = ( - grammar.injection_config.as_ref().zip(registry.as_ref()), - changed_ranges.is_empty(), - ) { - for range in &changed_ranges { - changed_regions.insert( - ChangedRegion { - depth: step.depth + 1, - range: text.anchor_before(range.start)..text.anchor_after(range.end), - }, - text, - ); - } - get_injections( - config, - text, - tree.root_node_with_offset(step_start_byte, step_start_point.to_ts_point()), - registry, - step.depth + 1, - &changed_ranges, - &mut combined_injection_ranges, - &mut queue, - ); - } } drop(cursor); self.layers = layers; + self.interpolated_version = text.version.clone(); + self.parsed_version = text.version.clone(); + #[cfg(debug_assertions)] + self.check_invariants(text); + } + + #[cfg(debug_assertions)] + fn check_invariants(&self, text: &BufferSnapshot) { + let mut max_depth = 0; + let mut prev_range: Option> = None; + for layer in self.layers.iter() { + if layer.depth == max_depth { + if let Some(prev_range) = prev_range { + match layer.range.start.cmp(&prev_range.start, text) { + Ordering::Less => panic!("layers out of order"), + Ordering::Equal => { + assert!(layer.range.end.cmp(&prev_range.end, text).is_ge()) + } + Ordering::Greater => {} + } + } + } else if layer.depth < max_depth { + panic!("layers out of order") + } + max_depth = layer.depth; + prev_range = Some(layer.range.clone()); + } } pub fn single_tree_captures<'a>( @@ -585,23 +707,34 @@ impl SyntaxSnapshot { }); cursor.next(buffer); - std::iter::from_fn(move || { - if let Some(layer) = cursor.item() { - let info = SyntaxLayerInfo { - language: &layer.language, - depth: layer.depth, - node: layer.tree.root_node_with_offset( - layer.range.start.to_offset(buffer), - layer.range.start.to_point(buffer).to_ts_point(), - ), - }; - cursor.next(buffer); - Some(info) - } else { - None + iter::from_fn(move || { + while let Some(layer) = cursor.item() { + if let SyntaxLayerContent::Parsed { tree, language } = &layer.content { + let info = SyntaxLayerInfo { + language, + depth: layer.depth, + node: tree.root_node_with_offset( + layer.range.start.to_offset(buffer), + layer.range.start.to_point(buffer).to_ts_point(), + ), + }; + cursor.next(buffer); + return Some(info); + } else { + cursor.next(buffer); + } } + None }) } + + pub fn contains_unknown_injections(&self) -> bool { + self.layers.summary().contains_unknown_injections + } + + pub fn language_registry_version(&self) -> usize { + self.language_registry_version + } } impl<'a> SyntaxMapCaptures<'a> { @@ -963,20 +1096,20 @@ fn get_injections( config: &InjectionConfig, text: &BufferSnapshot, node: Node, - language_registry: &LanguageRegistry, + language_registry: &Arc, depth: usize, changed_ranges: &[Range], combined_injection_ranges: &mut HashMap, Vec>, queue: &mut BinaryHeap, -) -> bool { - let mut result = false; +) { let mut query_cursor = QueryCursorHandle::new(); let mut prev_match = None; combined_injection_ranges.clear(); for pattern in &config.patterns { if let (Some(language_name), true) = (pattern.language.as_ref(), pattern.combined) { - if let Some(language) = language_registry.get_language(language_name) { + if let Some(language) = language_registry.language_for_name_or_extension(language_name) + { combined_injection_ranges.insert(language, Vec::new()); } } @@ -1004,21 +1137,29 @@ fn get_injections( prev_match = Some((mat.pattern_index, content_range.clone())); let combined = config.patterns[mat.pattern_index].combined; - let language_name = config.patterns[mat.pattern_index] - .language - .as_ref() - .map(|s| Cow::Borrowed(s.as_ref())) - .or_else(|| { - let ix = config.language_capture_ix?; - let node = mat.nodes_for_capture_index(ix).next()?; - Some(Cow::Owned(text.text_for_range(node.byte_range()).collect())) - }); + + let mut language_name = None; + let mut step_range = content_range.clone(); + if let Some(name) = config.patterns[mat.pattern_index].language.as_ref() { + language_name = Some(Cow::Borrowed(name.as_ref())) + } else if let Some(language_node) = config + .language_capture_ix + .and_then(|ix| mat.nodes_for_capture_index(ix).next()) + { + step_range.start = cmp::min(content_range.start, language_node.start_byte()); + step_range.end = cmp::max(content_range.end, language_node.end_byte()); + language_name = Some(Cow::Owned( + text.text_for_range(language_node.byte_range()).collect(), + )) + }; if let Some(language_name) = language_name { - if let Some(language) = language_registry.get_language(language_name.as_ref()) { - result = true; - let range = text.anchor_before(content_range.start) - ..text.anchor_after(content_range.end); + let language = { + let language_name: &str = &language_name; + language_registry.language_for_name_or_extension(language_name) + }; + let range = text.anchor_before(step_range.start)..text.anchor_after(step_range.end); + if let Some(language) = language { if combined { combined_injection_ranges .get_mut(&language.clone()) @@ -1027,12 +1168,22 @@ fn get_injections( } else { queue.push(ParseStep { depth, - language, + language: ParseStepLanguage::Loaded { language }, included_ranges: content_ranges, range, mode: ParseMode::Single, }); } + } else { + queue.push(ParseStep { + depth, + language: ParseStepLanguage::Pending { + name: language_name.into(), + }, + included_ranges: content_ranges, + range, + mode: ParseMode::Single, + }); } } } @@ -1043,7 +1194,7 @@ fn get_injections( let range = text.anchor_before(node.start_byte())..text.anchor_after(node.end_byte()); queue.push(ParseStep { depth, - language, + language: ParseStepLanguage::Loaded { language }, range, included_ranges, mode: ParseMode::Combined { @@ -1052,8 +1203,6 @@ fn get_injections( }, }) } - - result } fn splice_included_ranges( @@ -1282,6 +1431,7 @@ impl Default for SyntaxLayerSummary { range: Anchor::MAX..Anchor::MIN, last_layer_range: Anchor::MIN..Anchor::MAX, last_layer_language: None, + contains_unknown_injections: false, } } } @@ -1294,7 +1444,7 @@ impl sum_tree::Summary for SyntaxLayerSummary { self.max_depth = other.max_depth; self.range = other.range.clone(); } else { - if other.range.start.cmp(&self.range.start, buffer).is_lt() { + if self.range == (Anchor::MAX..Anchor::MAX) { self.range.start = other.range.start; } if other.range.end.cmp(&self.range.end, buffer).is_gt() { @@ -1303,6 +1453,7 @@ impl sum_tree::Summary for SyntaxLayerSummary { } self.last_layer_range = other.last_layer_range.clone(); self.last_layer_language = other.last_layer_language; + self.contains_unknown_injections |= other.contains_unknown_injections; } } @@ -1352,7 +1503,8 @@ impl sum_tree::Item for SyntaxLayer { max_depth: self.depth, range: self.range.clone(), last_layer_range: self.range.clone(), - last_layer_language: self.language.id(), + last_layer_language: self.content.language_id(), + contains_unknown_injections: matches!(self.content, SyntaxLayerContent::Pending { .. }), } } } @@ -1362,7 +1514,7 @@ impl std::fmt::Debug for SyntaxLayer { f.debug_struct("SyntaxLayer") .field("depth", &self.depth) .field("range", &self.range) - .field("tree", &self.tree) + .field("tree", &self.content.tree()) .finish() } } @@ -1593,6 +1745,84 @@ mod tests { ); } + #[gpui::test] + fn test_dynamic_language_injection() { + let registry = Arc::new(LanguageRegistry::test()); + let markdown = Arc::new(markdown_lang()); + registry.add(markdown.clone()); + registry.add(Arc::new(rust_lang())); + registry.add(Arc::new(ruby_lang())); + + let mut buffer = Buffer::new( + 0, + 0, + r#" + This is a code block: + + ```rs + fn foo() {} + ``` + "# + .unindent(), + ); + + let mut syntax_map = SyntaxMap::new(); + syntax_map.set_language_registry(registry.clone()); + syntax_map.reparse(markdown.clone(), &buffer); + assert_layers_for_range( + &syntax_map, + &buffer, + Point::new(3, 0)..Point::new(3, 0), + &[ + "...(fenced_code_block (fenced_code_block_delimiter) (info_string (language)) (code_fence_content) (fenced_code_block_delimiter...", + "...(function_item name: (identifier) parameters: (parameters) body: (block)...", + ], + ); + + // Replace Rust with Ruby in code block. + let macro_name_range = range_for_text(&buffer, "rs"); + buffer.edit([(macro_name_range, "ruby")]); + syntax_map.interpolate(&buffer); + syntax_map.reparse(markdown.clone(), &buffer); + assert_layers_for_range( + &syntax_map, + &buffer, + Point::new(3, 0)..Point::new(3, 0), + &[ + "...(fenced_code_block (fenced_code_block_delimiter) (info_string (language)) (code_fence_content) (fenced_code_block_delimiter...", + "...(call method: (identifier) arguments: (argument_list (call method: (identifier) arguments: (argument_list) block: (block)...", + ], + ); + + // Replace Ruby with a language that hasn't been loaded yet. + let macro_name_range = range_for_text(&buffer, "ruby"); + buffer.edit([(macro_name_range, "html")]); + syntax_map.interpolate(&buffer); + syntax_map.reparse(markdown.clone(), &buffer); + assert_layers_for_range( + &syntax_map, + &buffer, + Point::new(3, 0)..Point::new(3, 0), + &[ + "...(fenced_code_block (fenced_code_block_delimiter) (info_string (language)) (code_fence_content) (fenced_code_block_delimiter..." + ], + ); + assert!(syntax_map.contains_unknown_injections()); + + registry.add(Arc::new(html_lang())); + syntax_map.reparse(markdown.clone(), &buffer); + assert_layers_for_range( + &syntax_map, + &buffer, + Point::new(3, 0)..Point::new(3, 0), + &[ + "...(fenced_code_block (fenced_code_block_delimiter) (info_string (language)) (code_fence_content) (fenced_code_block_delimiter...", + "(fragment (text))", + ], + ); + assert!(!syntax_map.contains_unknown_injections()); + } + #[gpui::test] fn test_typing_multiple_new_injections() { let (buffer, syntax_map) = test_edit_sequence( @@ -2157,16 +2387,14 @@ mod tests { .zip(new_syntax_map.layers.iter()) { assert_eq!(old_layer.range, new_layer.range); + let Some(old_tree) = old_layer.content.tree() else { continue }; + let Some(new_tree) = new_layer.content.tree() else { continue }; let old_start_byte = old_layer.range.start.to_offset(old_buffer); let new_start_byte = new_layer.range.start.to_offset(new_buffer); let old_start_point = old_layer.range.start.to_point(old_buffer).to_ts_point(); let new_start_point = new_layer.range.start.to_point(new_buffer).to_ts_point(); - let old_node = old_layer - .tree - .root_node_with_offset(old_start_byte, old_start_point); - let new_node = new_layer - .tree - .root_node_with_offset(new_start_byte, new_start_point); + let old_node = old_tree.root_node_with_offset(old_start_byte, old_start_point); + let new_node = new_tree.root_node_with_offset(new_start_byte, new_start_point); check_node_edits( old_layer.depth, &old_layer.range, @@ -2254,7 +2482,8 @@ mod tests { registry.add(Arc::new(ruby_lang())); registry.add(Arc::new(html_lang())); registry.add(Arc::new(erb_lang())); - let language = registry.get_language(language_name).unwrap(); + registry.add(Arc::new(markdown_lang())); + let language = registry.language_for_name(language_name).unwrap(); let mut buffer = Buffer::new(0, 0, Default::default()); let mut mutated_syntax_map = SyntaxMap::new(); @@ -2392,6 +2621,26 @@ mod tests { .unwrap() } + fn markdown_lang() -> Language { + Language::new( + LanguageConfig { + name: "Markdown".into(), + path_suffixes: vec!["md".into()], + ..Default::default() + }, + Some(tree_sitter_markdown::language()), + ) + .with_injection_query( + r#" + (fenced_code_block + (info_string + (language) @language) + (code_fence_content) @content) + "#, + ) + .unwrap() + } + fn range_for_text(buffer: &Buffer, text: &str) -> Range { let start = buffer.as_rope().to_string().find(text).unwrap(); start..start + text.len() diff --git a/crates/live_kit_client/src/prod.rs b/crates/live_kit_client/src/prod.rs index 47fd4f0b69..f45667e3c3 100644 --- a/crates/live_kit_client/src/prod.rs +++ b/crates/live_kit_client/src/prod.rs @@ -128,14 +128,9 @@ impl Room { let url = url.to_string(); let token = token.to_string(); async move { - match rx.await.unwrap().context("error connecting to room") { - Ok(()) => { - *this.connection.lock().0.borrow_mut() = - ConnectionState::Connected { url, token }; - Ok(()) - } - Err(err) => Err(err), - } + rx.await.unwrap().context("error connecting to room")?; + *this.connection.lock().0.borrow_mut() = ConnectionState::Connected { url, token }; + Ok(()) } } diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index b7199a5287..660528daf1 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -1,3 +1,4 @@ +use log::warn; pub use lsp_types::request::*; pub use lsp_types::*; @@ -64,6 +65,7 @@ struct Request<'a, T> { #[derive(Serialize, Deserialize)] struct AnyResponse<'a> { + jsonrpc: &'a str, id: usize, #[serde(default)] error: Option, @@ -203,8 +205,9 @@ impl LanguageServer { } else { on_unhandled_notification(msg); } - } else if let Ok(AnyResponse { id, error, result }) = - serde_json::from_slice(&buffer) + } else if let Ok(AnyResponse { + id, error, result, .. + }) = serde_json::from_slice(&buffer) { if let Some(handler) = response_handlers .lock() @@ -220,10 +223,10 @@ impl LanguageServer { } } } else { - return Err(anyhow!( - "failed to deserialize message:\n{}", + warn!( + "Failed to deserialize message:\n{}", std::str::from_utf8(&buffer)? - )); + ); } // Don't starve the main thread when receiving lots of messages at once. @@ -460,35 +463,57 @@ impl LanguageServer { method, Box::new(move |id, params, cx| { if let Some(id) = id { - if let Some(params) = serde_json::from_str(params).log_err() { - let response = f(params, cx.clone()); - cx.foreground() - .spawn({ - let outbound_tx = outbound_tx.clone(); - async move { - let response = match response.await { - Ok(result) => Response { - jsonrpc: JSON_RPC_VERSION, - id, - result: Some(result), - error: None, - }, - Err(error) => Response { - jsonrpc: JSON_RPC_VERSION, - id, - result: None, - error: Some(Error { - message: error.to_string(), - }), - }, - }; - if let Some(response) = serde_json::to_vec(&response).log_err() - { - outbound_tx.try_send(response).ok(); + match serde_json::from_str(params) { + Ok(params) => { + let response = f(params, cx.clone()); + cx.foreground() + .spawn({ + let outbound_tx = outbound_tx.clone(); + async move { + let response = match response.await { + Ok(result) => Response { + jsonrpc: JSON_RPC_VERSION, + id, + result: Some(result), + error: None, + }, + Err(error) => Response { + jsonrpc: JSON_RPC_VERSION, + id, + result: None, + error: Some(Error { + message: error.to_string(), + }), + }, + }; + if let Some(response) = + serde_json::to_vec(&response).log_err() + { + outbound_tx.try_send(response).ok(); + } } - } - }) - .detach(); + }) + .detach(); + } + Err(error) => { + log::error!( + "error deserializing {} request: {:?}, message: {:?}", + method, + error, + params + ); + let response = AnyResponse { + jsonrpc: JSON_RPC_VERSION, + id, + result: None, + error: Some(Error { + message: error.to_string(), + }), + }; + if let Some(response) = serde_json::to_vec(&response).log_err() { + outbound_tx.try_send(response).ok(); + } + } } } }), diff --git a/crates/pando/Cargo.toml b/crates/pando/Cargo.toml new file mode 100644 index 0000000000..8521c4fd81 --- /dev/null +++ b/crates/pando/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "pando" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +path = "src/pando.rs" + +[features] +test-support = [] + +[dependencies] +anyhow = "1.0.38" +client = { path = "../client" } +gpui = { path = "../gpui" } +settings = { path = "../settings" } +theme = { path = "../theme" } +workspace = { path = "../workspace" } +sqlez = { path = "../sqlez" } +sqlez_macros = { path = "../sqlez_macros" } \ No newline at end of file diff --git a/assets/keymaps/experiments/.gitkeep b/crates/pando/src/file_format.rs similarity index 100% rename from assets/keymaps/experiments/.gitkeep rename to crates/pando/src/file_format.rs diff --git a/crates/pando/src/pando.rs b/crates/pando/src/pando.rs new file mode 100644 index 0000000000..e75f843720 --- /dev/null +++ b/crates/pando/src/pando.rs @@ -0,0 +1,15 @@ +//! ## Goals +//! - Opinionated Subset of Obsidian. Only the things that cant be done other ways in zed +//! - Checked in .zp file is an sqlite db containing graph metadata +//! - All nodes are file urls +//! - Markdown links auto add soft linked nodes to the db +//! - Links create positioning data regardless of if theres a file +//! - Lock links to make structure that doesn't rotate or spread +//! - Drag from file finder to pando item to add it in +//! - For linked files, zoom out to see closest linking pando file + +//! ## Plan +//! - [ ] Make item backed by .zp sqlite file with camera position by user account +//! - [ ] Render grid of dots and allow scrolling around the grid +//! - [ ] Add scale property to layer canvas and manipulate it with pinch zooming +//! - [ ] Allow dropping files onto .zp pane. Their relative path is recorded into the file along with diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 995a6514c5..8ed37a003c 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -12,7 +12,7 @@ use clock::ReplicaId; use collections::{hash_map, BTreeMap, HashMap, HashSet}; use futures::{ channel::{mpsc, oneshot}, - future::Shared, + future::{try_join_all, Shared}, AsyncWriteExt, Future, FutureExt, StreamExt, TryFutureExt, }; use gpui::{ @@ -28,8 +28,8 @@ use language::{ range_from_lsp, range_to_lsp, Anchor, Bias, Buffer, CachedLspAdapter, CharKind, CodeAction, CodeLabel, Completion, Diagnostic, DiagnosticEntry, DiagnosticSet, Event as BufferEvent, File as _, Language, LanguageRegistry, LanguageServerName, LocalFile, OffsetRangeExt, - Operation, Patch, PointUtf16, TextBufferSnapshot, ToOffset, ToPointUtf16, Transaction, - Unclipped, + Operation, Patch, PointUtf16, RopeFingerprint, TextBufferSnapshot, ToOffset, ToPointUtf16, + Transaction, Unclipped, }; use lsp::{ DiagnosticSeverity, DiagnosticTag, DocumentHighlightKind, LanguageServer, LanguageString, @@ -59,7 +59,7 @@ use std::{ atomic::{AtomicUsize, Ordering::SeqCst}, Arc, }, - time::Instant, + time::{Duration, Instant, SystemTime}, }; use terminal::{Terminal, TerminalBuilder}; use util::{debug_panic, defer, post_inc, ResultExt, TryFutureExt as _}; @@ -185,6 +185,7 @@ pub enum LanguageServerState { language: Arc, adapter: Arc, server: Arc, + simulate_disk_based_diagnostics_completion: Option>, }, } @@ -550,15 +551,16 @@ impl Project { if !cx.read(|cx| cx.has_global::()) { cx.update(|cx| { cx.set_global(Settings::test(cx)); - cx.set_global(HomeDir(Path::new("/tmp/").to_path_buf())) }); } - let languages = Arc::new(LanguageRegistry::test()); + let mut languages = LanguageRegistry::test(); + languages.set_executor(cx.background()); let http_client = client::test::FakeHttpClient::with_404_response(); let client = cx.update(|cx| client::Client::new(http_client.clone(), cx)); let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx)); - let project = cx.update(|cx| Project::local(client, user_store, languages, fs, cx)); + let project = + cx.update(|cx| Project::local(client, user_store, Arc::new(languages), fs, cx)); for path in root_paths { let (tree, _) = project .update(cx, |project, cx| { @@ -1426,11 +1428,41 @@ impl Project { } } + pub fn save_buffers( + &self, + buffers: HashSet>, + cx: &mut ModelContext, + ) -> Task> { + cx.spawn(|this, mut cx| async move { + let save_tasks = buffers + .into_iter() + .map(|buffer| this.update(&mut cx, |this, cx| this.save_buffer(buffer, cx))); + try_join_all(save_tasks).await?; + Ok(()) + }) + } + + pub fn save_buffer( + &self, + buffer: ModelHandle, + cx: &mut ModelContext, + ) -> Task> { + let Some(file) = File::from_dyn(buffer.read(cx).file()) else { + return Task::ready(Err(anyhow!("buffer doesn't have a file"))); + }; + let worktree = file.worktree.clone(); + let path = file.path.clone(); + worktree.update(cx, |worktree, cx| match worktree { + Worktree::Local(worktree) => worktree.save_buffer(buffer, path, false, cx), + Worktree::Remote(worktree) => worktree.save_buffer(buffer, cx), + }) + } + pub fn save_buffer_as( &mut self, buffer: ModelHandle, abs_path: PathBuf, - cx: &mut ModelContext, + cx: &mut ModelContext, ) -> Task> { let worktree_task = self.find_or_create_local_worktree(&abs_path, true, cx); let old_path = @@ -1443,11 +1475,11 @@ impl Project { } let (worktree, path) = worktree_task.await?; worktree - .update(&mut cx, |worktree, cx| { - worktree - .as_local_mut() - .unwrap() - .save_buffer_as(buffer.clone(), path, cx) + .update(&mut cx, |worktree, cx| match worktree { + Worktree::Local(worktree) => { + worktree.save_buffer(buffer.clone(), path.into(), true, cx) + } + Worktree::Remote(_) => panic!("cannot remote buffers as new files"), }) .await?; this.update(&mut cx, |this, cx| { @@ -1480,6 +1512,10 @@ impl Project { buffer: &ModelHandle, cx: &mut ModelContext, ) -> Result<()> { + buffer.update(cx, |buffer, _| { + buffer.set_language_registry(self.languages.clone()) + }); + let remote_id = buffer.read(cx).remote_id(); let open_buffer = if self.is_remote() || self.is_shared() { OpenBuffer::Strong(buffer.clone()) @@ -1713,19 +1749,39 @@ impl Project { .log_err(); } - // After saving a buffer, simulate disk-based diagnostics being finished for languages - // that don't support a disk-based progress token. - let (lsp_adapter, language_server) = - self.language_server_for_buffer(buffer.read(cx), cx)?; - if lsp_adapter.disk_based_diagnostics_progress_token.is_none() { - let server_id = language_server.server_id(); - self.disk_based_diagnostics_finished(server_id, cx); - self.broadcast_language_server_update( - server_id, - proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated( - proto::LspDiskBasedDiagnosticsUpdated {}, - ), - ); + let language_server_id = self.language_server_id_for_buffer(buffer.read(cx), cx)?; + if let Some(LanguageServerState::Running { + adapter, + simulate_disk_based_diagnostics_completion, + .. + }) = self.language_servers.get_mut(&language_server_id) + { + // After saving a buffer using a language server that doesn't provide + // a disk-based progress token, kick off a timer that will reset every + // time the buffer is saved. If the timer eventually fires, simulate + // disk-based diagnostics being finished so that other pieces of UI + // (e.g., project diagnostics view, diagnostic status bar) can update. + // We don't emit an event right away because the language server might take + // some time to publish diagnostics. + if adapter.disk_based_diagnostics_progress_token.is_none() { + const DISK_BASED_DIAGNOSTICS_DEBOUNCE: Duration = Duration::from_secs(1); + + let task = cx.spawn_weak(|this, mut cx| async move { + cx.background().timer(DISK_BASED_DIAGNOSTICS_DEBOUNCE).await; + if let Some(this) = this.upgrade(&cx) { + this.update(&mut cx, |this, cx | { + this.disk_based_diagnostics_finished(language_server_id, cx); + this.broadcast_language_server_update( + language_server_id, + proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated( + proto::LspDiskBasedDiagnosticsUpdated {}, + ), + ); + }); + } + }); + *simulate_disk_based_diagnostics_completion = Some(task); + } } } _ => {} @@ -1746,6 +1802,7 @@ impl Project { adapter, language, server, + .. }) = self.language_servers.get(id) { return Some((adapter, language, server)); @@ -1764,19 +1821,29 @@ impl Project { while let Some(()) = subscription.next().await { if let Some(project) = project.upgrade(&cx) { project.update(&mut cx, |project, cx| { - let mut buffers_without_language = Vec::new(); + let mut plain_text_buffers = Vec::new(); + let mut buffers_with_unknown_injections = Vec::new(); for buffer in project.opened_buffers.values() { - if let Some(buffer) = buffer.upgrade(cx) { - if buffer.read(cx).language().is_none() { - buffers_without_language.push(buffer); + if let Some(handle) = buffer.upgrade(cx) { + let buffer = &handle.read(cx); + if buffer.language().is_none() + || buffer.language() == Some(&*language::PLAIN_TEXT) + { + plain_text_buffers.push(handle); + } else if buffer.contains_unknown_injections() { + buffers_with_unknown_injections.push(handle); } } } - for buffer in buffers_without_language { + for buffer in plain_text_buffers { project.assign_language_to_buffer(&buffer, cx); project.register_buffer_with_language_server(&buffer, cx); } + + for buffer in buffers_with_unknown_injections { + buffer.update(cx, |buffer, cx| buffer.reparse(cx)); + } }); } } @@ -1790,12 +1857,11 @@ impl Project { ) -> Option<()> { // If the buffer has a language, set it and start the language server if we haven't already. let full_path = buffer.read(cx).file()?.full_path(cx); - let new_language = self.languages.select_language(&full_path)?; + let new_language = self.languages.language_for_path(&full_path)?; buffer.update(cx, |buffer, cx| { if buffer.language().map_or(true, |old_language| { !Arc::ptr_eq(old_language, &new_language) }) { - buffer.set_language_registry(self.languages.clone()); buffer.set_language(Some(new_language.clone()), cx); } }); @@ -2025,6 +2091,7 @@ impl Project { adapter: adapter.clone(), language, server: language_server.clone(), + simulate_disk_based_diagnostics_completion: None, }, ); this.language_server_statuses.insert( @@ -2200,7 +2267,7 @@ impl Project { }) .collect(); for (worktree_id, worktree_abs_path, full_path) in language_server_lookup_info { - let language = self.languages.select_language(&full_path)?; + let language = self.languages.language_for_path(&full_path)?; self.restart_language_server(worktree_id, worktree_abs_path, language, cx); } @@ -2785,126 +2852,126 @@ impl Project { trigger: FormatTrigger, cx: &mut ModelContext, ) -> Task> { - let mut local_buffers = Vec::new(); - let mut remote_buffers = None; - for buffer_handle in buffers { - let buffer = buffer_handle.read(cx); - if let Some(file) = File::from_dyn(buffer.file()) { - if let Some(buffer_abs_path) = file.as_local().map(|f| f.abs_path(cx)) { - if let Some((_, server)) = self.language_server_for_buffer(buffer, cx) { - local_buffers.push((buffer_handle, buffer_abs_path, server.clone())); - } - } else { - remote_buffers.get_or_insert(Vec::new()).push(buffer_handle); - } - } else { - return Task::ready(Ok(Default::default())); - } - } + if self.is_local() { + let mut buffers_with_paths_and_servers = buffers + .into_iter() + .filter_map(|buffer_handle| { + let buffer = buffer_handle.read(cx); + let file = File::from_dyn(buffer.file())?; + let buffer_abs_path = file.as_local()?.abs_path(cx); + let (_, server) = self.language_server_for_buffer(buffer, cx)?; + Some((buffer_handle, buffer_abs_path, server.clone())) + }) + .collect::>(); - let remote_buffers = self.remote_id().zip(remote_buffers); - let client = self.client.clone(); - - cx.spawn(|this, mut cx| async move { - let mut project_transaction = ProjectTransaction::default(); - - if let Some((project_id, remote_buffers)) = remote_buffers { - let response = client - .request(proto::FormatBuffers { - project_id, - trigger: trigger as i32, - buffer_ids: remote_buffers - .iter() - .map(|buffer| buffer.read_with(&cx, |buffer, _| buffer.remote_id())) - .collect(), - }) - .await? - .transaction - .ok_or_else(|| anyhow!("missing transaction"))?; - project_transaction = this - .update(&mut cx, |this, cx| { - this.deserialize_project_transaction(response, push_to_history, cx) - }) - .await?; - } - - // Do not allow multiple concurrent formatting requests for the - // same buffer. - this.update(&mut cx, |this, _| { - local_buffers - .retain(|(buffer, _, _)| this.buffers_being_formatted.insert(buffer.id())); - }); - let _cleanup = defer({ - let this = this.clone(); - let mut cx = cx.clone(); - let local_buffers = &local_buffers; - move || { - this.update(&mut cx, |this, _| { - for (buffer, _, _) in local_buffers { - this.buffers_being_formatted.remove(&buffer.id()); - } - }); - } - }); - - for (buffer, buffer_abs_path, language_server) in &local_buffers { - let (format_on_save, formatter, tab_size) = buffer.read_with(&cx, |buffer, cx| { - let settings = cx.global::(); - let language_name = buffer.language().map(|language| language.name()); - ( - settings.format_on_save(language_name.as_deref()), - settings.formatter(language_name.as_deref()), - settings.tab_size(language_name.as_deref()), - ) + cx.spawn(|this, mut cx| async move { + // Do not allow multiple concurrent formatting requests for the + // same buffer. + this.update(&mut cx, |this, _| { + buffers_with_paths_and_servers + .retain(|(buffer, _, _)| this.buffers_being_formatted.insert(buffer.id())); }); - let transaction = match (formatter, format_on_save) { - (_, FormatOnSave::Off) if trigger == FormatTrigger::Save => continue, + let _cleanup = defer({ + let this = this.clone(); + let mut cx = cx.clone(); + let local_buffers = &buffers_with_paths_and_servers; + move || { + this.update(&mut cx, |this, _| { + for (buffer, _, _) in local_buffers { + this.buffers_being_formatted.remove(&buffer.id()); + } + }); + } + }); - (Formatter::LanguageServer, FormatOnSave::On | FormatOnSave::Off) - | (_, FormatOnSave::LanguageServer) => Self::format_via_lsp( - &this, - &buffer, - &buffer_abs_path, - &language_server, - tab_size, - &mut cx, - ) - .await - .context("failed to format via language server")?, + let mut project_transaction = ProjectTransaction::default(); + for (buffer, buffer_abs_path, language_server) in &buffers_with_paths_and_servers { + let (format_on_save, formatter, tab_size) = + buffer.read_with(&cx, |buffer, cx| { + let settings = cx.global::(); + let language_name = buffer.language().map(|language| language.name()); + ( + settings.format_on_save(language_name.as_deref()), + settings.formatter(language_name.as_deref()), + settings.tab_size(language_name.as_deref()), + ) + }); - ( - Formatter::External { command, arguments }, - FormatOnSave::On | FormatOnSave::Off, - ) - | (_, FormatOnSave::External { command, arguments }) => { - Self::format_via_external_command( + let transaction = match (formatter, format_on_save) { + (_, FormatOnSave::Off) if trigger == FormatTrigger::Save => continue, + + (Formatter::LanguageServer, FormatOnSave::On | FormatOnSave::Off) + | (_, FormatOnSave::LanguageServer) => Self::format_via_lsp( + &this, &buffer, &buffer_abs_path, - &command, - &arguments, + &language_server, + tab_size, &mut cx, ) .await - .context(format!( - "failed to format via external command {:?}", - command - ))? - } - }; + .context("failed to format via language server")?, - if let Some(transaction) = transaction { - if !push_to_history { - buffer.update(&mut cx, |buffer, _| { - buffer.forget_transaction(transaction.id) - }); + ( + Formatter::External { command, arguments }, + FormatOnSave::On | FormatOnSave::Off, + ) + | (_, FormatOnSave::External { command, arguments }) => { + Self::format_via_external_command( + &buffer, + &buffer_abs_path, + &command, + &arguments, + &mut cx, + ) + .await + .context(format!( + "failed to format via external command {:?}", + command + ))? + } + }; + + if let Some(transaction) = transaction { + if !push_to_history { + buffer.update(&mut cx, |buffer, _| { + buffer.forget_transaction(transaction.id) + }); + } + project_transaction.0.insert(buffer.clone(), transaction); } - project_transaction.0.insert(buffer.clone(), transaction); } - } - Ok(project_transaction) - }) + Ok(project_transaction) + }) + } else { + let remote_id = self.remote_id(); + let client = self.client.clone(); + cx.spawn(|this, mut cx| async move { + let mut project_transaction = ProjectTransaction::default(); + if let Some(project_id) = remote_id { + let response = client + .request(proto::FormatBuffers { + project_id, + trigger: trigger as i32, + buffer_ids: buffers + .iter() + .map(|buffer| buffer.read_with(&cx, |buffer, _| buffer.remote_id())) + .collect(), + }) + .await? + .transaction + .ok_or_else(|| anyhow!("missing transaction"))?; + project_transaction = this + .update(&mut cx, |this, cx| { + this.deserialize_project_transaction(response, push_to_history, cx) + }) + .await?; + } + Ok(project_transaction) + }) + } } async fn format_via_lsp( @@ -3095,6 +3162,7 @@ impl Project { adapter, language, server, + .. }) = self.language_servers.get(server_id) { let adapter = adapter.clone(); @@ -3160,7 +3228,7 @@ impl Project { let signature = this.symbol_signature(&project_path); let language = this .languages - .select_language(&project_path.path) + .language_for_path(&project_path.path) .unwrap_or(adapter_language.clone()); let language_server_name = adapter.name.clone(); Some(async move { @@ -4395,16 +4463,19 @@ impl Project { renamed_buffers.push((cx.handle(), old_path)); } - 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(); + 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(); } - buffer.file_updated(Arc::new(new_file), cx).detach(); } }); } else { @@ -5117,8 +5188,9 @@ impl Project { }) .await; - let (saved_version, fingerprint, mtime) = - buffer.update(&mut cx, |buffer, cx| buffer.save(cx)).await?; + let (saved_version, fingerprint, mtime) = this + .update(&mut cx, |this, cx| this.save_buffer(buffer, cx)) + .await?; Ok(proto::BufferSaved { project_id, buffer_id, @@ -5936,7 +6008,7 @@ impl Project { worktree_id, path: PathBuf::from(serialized_symbol.path).into(), }; - let language = languages.select_language(&path.path); + let language = languages.language_for_path(&path.path); Ok(Symbol { language_server_name: LanguageServerName( serialized_symbol.language_server_name.into(), @@ -5988,7 +6060,7 @@ impl Project { .and_then(|buffer| buffer.upgrade(cx)); if let Some(buffer) = buffer { buffer.update(cx, |buffer, cx| { - buffer.did_save(version, fingerprint, mtime, None, cx); + buffer.did_save(version, fingerprint, mtime, cx); }); } Ok(()) @@ -6168,22 +6240,27 @@ impl Project { buffer: &Buffer, cx: &AppContext, ) -> Option<(&Arc, &Arc)> { + let server_id = self.language_server_id_for_buffer(buffer, cx)?; + let server = self.language_servers.get(&server_id)?; + if let LanguageServerState::Running { + adapter, server, .. + } = server + { + Some((adapter, server)) + } else { + None + } + } + + fn language_server_id_for_buffer(&self, buffer: &Buffer, cx: &AppContext) -> Option { if let Some((file, language)) = File::from_dyn(buffer.file()).zip(buffer.language()) { let name = language.lsp_adapter()?.name.clone(); let worktree_id = file.worktree_id(cx); let key = (worktree_id, name); - - if let Some(server_id) = self.language_server_ids.get(&key) { - if let Some(LanguageServerState::Running { - adapter, server, .. - }) = self.language_servers.get(server_id) - { - return Some((adapter, server)); - } - } + self.language_server_ids.get(&key).copied() + } else { + None } - - None } } diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index c9e159f391..2f9f92af4e 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -243,8 +243,8 @@ async fn test_managing_language_servers( ); // Save notifications are reported to all servers. - toml_buffer - .update(cx, |buffer, cx| buffer.save(cx)) + project + .update(cx, |project, cx| project.save_buffer(toml_buffer, cx)) .await .unwrap(); assert_eq!( @@ -2083,12 +2083,13 @@ async fn test_save_file(cx: &mut gpui::TestAppContext) { .update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx)) .await .unwrap(); - buffer - .update(cx, |buffer, cx| { - assert_eq!(buffer.text(), "the old contents"); - buffer.edit([(0..0, "a line of text.\n".repeat(10 * 1024))], None, cx); - buffer.save(cx) - }) + buffer.update(cx, |buffer, cx| { + assert_eq!(buffer.text(), "the old contents"); + buffer.edit([(0..0, "a line of text.\n".repeat(10 * 1024))], None, cx); + }); + + project + .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx)) .await .unwrap(); @@ -2112,11 +2113,12 @@ async fn test_save_in_single_file_worktree(cx: &mut gpui::TestAppContext) { .update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx)) .await .unwrap(); - buffer - .update(cx, |buffer, cx| { - buffer.edit([(0..0, "a line of text.\n".repeat(10 * 1024))], None, cx); - buffer.save(cx) - }) + buffer.update(cx, |buffer, cx| { + buffer.edit([(0..0, "a line of text.\n".repeat(10 * 1024))], None, cx); + }); + + project + .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx)) .await .unwrap(); @@ -2130,6 +2132,20 @@ async fn test_save_as(cx: &mut gpui::TestAppContext) { fs.insert_tree("/dir", json!({})).await; let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; + + let languages = project.read_with(cx, |project, _| project.languages().clone()); + languages.register( + "/some/path", + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".into()], + ..Default::default() + }, + tree_sitter_rust::language(), + None, + |_| Default::default(), + ); + let buffer = project.update(cx, |project, cx| { project.create_buffer("", None, cx).unwrap() }); @@ -2137,23 +2153,30 @@ async fn test_save_as(cx: &mut gpui::TestAppContext) { buffer.edit([(0..0, "abc")], None, cx); assert!(buffer.is_dirty()); assert!(!buffer.has_conflict()); + assert_eq!(buffer.language().unwrap().name().as_ref(), "Plain Text"); }); project .update(cx, |project, cx| { - project.save_buffer_as(buffer.clone(), "/dir/file1".into(), cx) + project.save_buffer_as(buffer.clone(), "/dir/file1.rs".into(), cx) }) .await .unwrap(); - assert_eq!(fs.load(Path::new("/dir/file1")).await.unwrap(), "abc"); + assert_eq!(fs.load(Path::new("/dir/file1.rs")).await.unwrap(), "abc"); + + cx.foreground().run_until_parked(); buffer.read_with(cx, |buffer, cx| { - assert_eq!(buffer.file().unwrap().full_path(cx), Path::new("dir/file1")); + assert_eq!( + buffer.file().unwrap().full_path(cx), + Path::new("dir/file1.rs") + ); assert!(!buffer.is_dirty()); assert!(!buffer.has_conflict()); + assert_eq!(buffer.language().unwrap().name().as_ref(), "Rust"); }); let opened_buffer = project .update(cx, |project, cx| { - project.open_local_buffer("/dir/file1", cx) + project.open_local_buffer("/dir/file1.rs", cx) }) .await .unwrap(); @@ -2462,7 +2485,6 @@ async fn test_buffer_is_dirty(cx: &mut gpui::TestAppContext) { buffer.version(), buffer.as_rope().fingerprint(), buffer.file().unwrap().mtime(), - None, cx, ); }); @@ -2682,11 +2704,11 @@ async fn test_buffer_line_endings(cx: &mut gpui::TestAppContext) { }); // Save a file with windows line endings. The file is written correctly. - buffer2 - .update(cx, |buffer, cx| { - buffer.set_text("one\ntwo\nthree\nfour\n", cx); - buffer.save(cx) - }) + buffer2.update(cx, |buffer, cx| { + buffer.set_text("one\ntwo\nthree\nfour\n", cx); + }); + project + .update(cx, |project, cx| project.save_buffer(buffer2, cx)) .await .unwrap(); assert_eq!( diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index b65cf9e39b..8b622ab607 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -5,8 +5,8 @@ use anyhow::{anyhow, Context, Result}; use client::{proto, Client}; use clock::ReplicaId; use collections::{HashMap, VecDeque}; +use fs::LineEnding; use fs::{repository::GitRepository, Fs}; -use fs::{HomeDir, LineEnding}; use futures::{ channel::{ mpsc::{self, UnboundedSender}, @@ -20,6 +20,7 @@ use gpui::{ executor, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task, }; +use language::File as _; use language::{ proto::{ deserialize_fingerprint, deserialize_version, serialize_fingerprint, serialize_line_ending, @@ -49,6 +50,7 @@ use std::{ time::{Duration, SystemTime}, }; use sum_tree::{Bias, Edit, SeekTarget, SumTree, TreeMap, TreeSet}; +use util::paths::HOME; use util::{ResultExt, TryFutureExt}; #[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, PartialOrd, Ord)] @@ -723,34 +725,69 @@ impl LocalWorktree { }) } - pub fn save_buffer_as( + pub fn save_buffer( &self, buffer_handle: ModelHandle, - path: impl Into>, + path: Arc, + has_changed_file: bool, cx: &mut ModelContext, - ) -> Task> { + ) -> Task> { + let handle = cx.handle(); let buffer = buffer_handle.read(cx); + + let rpc = self.client.clone(); + let buffer_id = buffer.remote_id(); + let project_id = self.share.as_ref().map(|share| share.project_id); + let text = buffer.as_rope().clone(); let fingerprint = text.fingerprint(); let version = buffer.version(); let save = self.write_file(path, text, buffer.line_ending(), cx); - let handle = cx.handle(); + cx.as_mut().spawn(|mut cx| async move { let entry = save.await?; - let file = File { - entry_id: entry.id, - worktree: handle, - path: entry.path, - mtime: entry.mtime, - is_local: true, - is_deleted: false, - }; + + if has_changed_file { + let new_file = Arc::new(File { + entry_id: entry.id, + worktree: handle, + path: entry.path, + mtime: entry.mtime, + is_local: true, + is_deleted: false, + }); + + if let Some(project_id) = project_id { + rpc.send(proto::UpdateBufferFile { + project_id, + buffer_id, + file: Some(new_file.to_proto()), + }) + .log_err(); + } + + buffer_handle.update(&mut cx, |buffer, cx| { + if has_changed_file { + buffer.file_updated(new_file, cx).detach(); + } + }); + } + + if let Some(project_id) = project_id { + rpc.send(proto::BufferSaved { + project_id, + buffer_id, + version: serialize_version(&version), + mtime: Some(entry.mtime.into()), + fingerprint: serialize_fingerprint(fingerprint), + })?; + } buffer_handle.update(&mut cx, |buffer, cx| { - buffer.did_save(version, fingerprint, file.mtime, Some(Arc::new(file)), cx); + buffer.did_save(version.clone(), fingerprint, entry.mtime, cx); }); - Ok(()) + Ok((version, fingerprint, entry.mtime)) }) } @@ -1084,6 +1121,39 @@ impl RemoteWorktree { self.disconnected = true; } + pub fn save_buffer( + &self, + buffer_handle: ModelHandle, + cx: &mut ModelContext, + ) -> Task> { + let buffer = buffer_handle.read(cx); + let buffer_id = buffer.remote_id(); + let version = buffer.version(); + let rpc = self.client.clone(); + let project_id = self.project_id; + cx.as_mut().spawn(|mut cx| async move { + let response = rpc + .request(proto::SaveBuffer { + project_id, + buffer_id, + version: serialize_version(&version), + }) + .await?; + let version = deserialize_version(response.version); + let fingerprint = deserialize_fingerprint(&response.fingerprint)?; + let mtime = response + .mtime + .ok_or_else(|| anyhow!("missing mtime"))? + .into(); + + buffer_handle.update(&mut cx, |buffer, cx| { + buffer.did_save(version.clone(), fingerprint, mtime, cx); + }); + + Ok((version, fingerprint, mtime)) + }) + } + pub fn update_from_remote(&mut self, update: proto::UpdateWorktree) { if let Some(updates_tx) = &self.updates_tx { updates_tx @@ -1831,9 +1901,9 @@ impl language::File for File { } else { let path = worktree.abs_path(); - if worktree.is_local() && path.starts_with(cx.global::().as_path()) { + if worktree.is_local() && path.starts_with(HOME.as_path()) { full_path.push("~"); - full_path.push(path.strip_prefix(cx.global::().as_path()).unwrap()); + full_path.push(path.strip_prefix(HOME.as_path()).unwrap()); } else { full_path.push(path) } @@ -1858,57 +1928,6 @@ impl language::File for File { self.is_deleted } - fn save( - &self, - buffer_id: u64, - text: Rope, - version: clock::Global, - line_ending: LineEnding, - cx: &mut MutableAppContext, - ) -> Task> { - self.worktree.update(cx, |worktree, cx| match worktree { - Worktree::Local(worktree) => { - let rpc = worktree.client.clone(); - let project_id = worktree.share.as_ref().map(|share| share.project_id); - let fingerprint = text.fingerprint(); - let save = worktree.write_file(self.path.clone(), text, line_ending, cx); - cx.background().spawn(async move { - let entry = save.await?; - if let Some(project_id) = project_id { - rpc.send(proto::BufferSaved { - project_id, - buffer_id, - version: serialize_version(&version), - mtime: Some(entry.mtime.into()), - fingerprint: serialize_fingerprint(fingerprint), - })?; - } - Ok((version, fingerprint, entry.mtime)) - }) - } - Worktree::Remote(worktree) => { - let rpc = worktree.client.clone(); - let project_id = worktree.project_id; - cx.foreground().spawn(async move { - let response = rpc - .request(proto::SaveBuffer { - project_id, - buffer_id, - version: serialize_version(&version), - }) - .await?; - let version = deserialize_version(response.version); - let fingerprint = deserialize_fingerprint(&response.fingerprint)?; - let mtime = response - .mtime - .ok_or_else(|| anyhow!("missing mtime"))? - .into(); - Ok((version, fingerprint, mtime)) - }) - } - }) - } - fn as_any(&self) -> &dyn Any { self } diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index e59353aae4..2ba920c318 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -119,6 +119,7 @@ actions!( AddFile, Copy, CopyPath, + RevealInFinder, Cut, Paste, Delete, @@ -147,6 +148,7 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_action(ProjectPanel::cancel); cx.add_action(ProjectPanel::copy); cx.add_action(ProjectPanel::copy_path); + cx.add_action(ProjectPanel::reveal_in_finder); cx.add_action(ProjectPanel::cut); cx.add_action( |this: &mut ProjectPanel, action: &Paste, cx: &mut ViewContext| { @@ -305,6 +307,7 @@ impl ProjectPanel { } menu_entries.push(ContextMenuItem::item("New File", AddFile)); menu_entries.push(ContextMenuItem::item("New Folder", AddDirectory)); + menu_entries.push(ContextMenuItem::item("Reveal in Finder", RevealInFinder)); menu_entries.push(ContextMenuItem::Separator); menu_entries.push(ContextMenuItem::item("Copy", Copy)); menu_entries.push(ContextMenuItem::item("Copy Path", CopyPath)); @@ -787,6 +790,12 @@ impl ProjectPanel { } } + fn reveal_in_finder(&mut self, _: &RevealInFinder, cx: &mut ViewContext) { + if let Some((worktree, entry)) = self.selected_entry(cx) { + cx.reveal_path(&worktree.abs_path().join(&entry.path)); + } + } + fn move_entry( &mut self, &MoveProjectEntry { diff --git a/crates/recent_projects/src/recent_projects.rs b/crates/recent_projects/src/recent_projects.rs index 02e15290ab..f613ba4df2 100644 --- a/crates/recent_projects/src/recent_projects.rs +++ b/crates/recent_projects/src/recent_projects.rs @@ -11,9 +11,12 @@ use highlighted_workspace_location::HighlightedWorkspaceLocation; use ordered_float::OrderedFloat; use picker::{Picker, PickerDelegate}; use settings::Settings; -use workspace::{OpenPaths, Workspace, WorkspaceLocation, WORKSPACE_DB}; +use workspace::{ + notifications::simple_message_notification::MessageNotification, OpenPaths, Workspace, + WorkspaceLocation, WORKSPACE_DB, +}; -actions!(recent_projects, [Toggle]); +actions!(projects, [OpenRecent]); pub fn init(cx: &mut MutableAppContext) { cx.add_action(RecentProjectsView::toggle); @@ -40,9 +43,9 @@ impl RecentProjectsView { } } - fn toggle(_: &mut Workspace, _: &Toggle, cx: &mut ViewContext) { + fn toggle(_: &mut Workspace, _: &OpenRecent, cx: &mut ViewContext) { cx.spawn(|workspace, mut cx| async move { - let workspace_locations = cx + let workspace_locations: Vec<_> = cx .background() .spawn(async { WORKSPACE_DB @@ -56,12 +59,20 @@ impl RecentProjectsView { .await; workspace.update(&mut cx, |workspace, cx| { - workspace.toggle_modal(cx, |_, cx| { - let view = cx.add_view(|cx| Self::new(workspace_locations, cx)); - cx.subscribe(&view, Self::on_event).detach(); - view - }); - }) + if !workspace_locations.is_empty() { + workspace.toggle_modal(cx, |_, cx| { + let view = cx.add_view(|cx| Self::new(workspace_locations, cx)); + cx.subscribe(&view, Self::on_event).detach(); + view + }); + } else { + workspace.show_notification(0, cx, |cx| { + cx.add_view(|_| { + MessageNotification::new_message("No recent projects to open.") + }) + }) + } + }); }) .detach(); } diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index 6b09f07db4..1a56abc783 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -9,7 +9,7 @@ use std::fmt; use std::{ cmp, fmt::Debug, - io, iter, mem, + io, iter, time::{Duration, SystemTime, UNIX_EPOCH}, }; @@ -489,16 +489,26 @@ pub fn split_worktree_update( return None; } - let chunk_size = cmp::min(message.updated_entries.len(), max_chunk_size); - let updated_entries = message.updated_entries.drain(..chunk_size).collect(); - done = message.updated_entries.is_empty(); + let updated_entries_chunk_size = cmp::min(message.updated_entries.len(), max_chunk_size); + let updated_entries = message + .updated_entries + .drain(..updated_entries_chunk_size) + .collect(); + + let removed_entries_chunk_size = cmp::min(message.removed_entries.len(), max_chunk_size); + let removed_entries = message + .removed_entries + .drain(..removed_entries_chunk_size) + .collect(); + + done = message.updated_entries.is_empty() && message.removed_entries.is_empty(); Some(UpdateWorktree { project_id: message.project_id, worktree_id: message.worktree_id, root_name: message.root_name.clone(), abs_path: message.abs_path.clone(), updated_entries, - removed_entries: mem::take(&mut message.removed_entries), + removed_entries, scan_id: message.scan_id, is_last_update: done && message.is_last_update, }) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index d3d5c437c5..99c73815c9 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -16,6 +16,7 @@ use gpui::{ use menu::Confirm; use project::{search::SearchQuery, Project}; use settings::Settings; +use smallvec::SmallVec; use std::{ any::{Any, TypeId}, mem, @@ -259,11 +260,7 @@ impl Item for ProjectSearchView { .boxed(), ) .with_children(self.model.read(cx).active_query.as_ref().map(|query| { - let query_text = if query.as_str().len() > MAX_TAB_TITLE_LEN { - query.as_str()[..MAX_TAB_TITLE_LEN].to_string() + "…" - } else { - query.as_str().to_string() - }; + let query_text = util::truncate_and_trailoff(query.as_str(), MAX_TAB_TITLE_LEN); Label::new(query_text, tab_theme.label.clone()) .aligned() @@ -349,11 +346,13 @@ impl Item for ProjectSearchView { .update(cx, |editor, cx| editor.git_diff_recalc(project, cx)) } - fn to_item_events(event: &Self::Event) -> Vec { + fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]> { match event { - ViewEvent::UpdateTab => vec![ItemEvent::UpdateBreadcrumbs, ItemEvent::UpdateTab], + ViewEvent::UpdateTab => { + smallvec::smallvec![ItemEvent::UpdateBreadcrumbs, ItemEvent::UpdateTab] + } ViewEvent::EditorEvent(editor_event) => Editor::to_item_events(editor_event), - _ => Vec::new(), + _ => SmallVec::new(), } } @@ -575,9 +574,9 @@ impl ProjectSearchView { self.active_match_index = None; } else { let prev_search_id = mem::replace(&mut self.search_id, self.model.read(cx).search_id); - let reset_selections = self.search_id != prev_search_id; + let is_new_search = self.search_id != prev_search_id; self.results_editor.update(cx, |editor, cx| { - if reset_selections { + if is_new_search { editor.change_selections(Some(Autoscroll::fit()), cx, |s| { s.select_ranges(match_ranges.first().cloned()) }); @@ -588,7 +587,7 @@ impl ProjectSearchView { cx, ); }); - if self.query_editor.is_focused(cx) { + if is_new_search && self.query_editor.is_focused(cx) { self.focus_results_editor(cx); } } diff --git a/crates/settings/src/keymap_file.rs b/crates/settings/src/keymap_file.rs index 4090bcc63a..01992d9431 100644 --- a/crates/settings/src/keymap_file.rs +++ b/crates/settings/src/keymap_file.rs @@ -1,4 +1,4 @@ -use crate::{parse_json_with_comments, Settings}; +use crate::parse_json_with_comments; use anyhow::{Context, Result}; use assets::Assets; use collections::BTreeMap; @@ -42,16 +42,7 @@ struct ActionWithData(Box, Box); impl KeymapFileContent { pub fn load_defaults(cx: &mut MutableAppContext) { - let settings = cx.global::(); - let mut paths = vec!["keymaps/default.json", "keymaps/vim.json"]; - - if settings.staff_mode { - paths.push("keymaps/internal.json") - } - - paths.extend(settings.experiments.keymap_files()); - - for path in paths { + for path in ["keymaps/default.json", "keymaps/vim.json"] { Self::load(path, cx).unwrap(); } } diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index 8b2c12a59b..21939b26b0 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -15,7 +15,7 @@ use schemars::{ use serde::{de::DeserializeOwned, Deserialize, Serialize}; use serde_json::Value; use sqlez::{ - bindable::{Bind, Column}, + bindable::{Bind, Column, StaticColumnCount}, statement::Statement, }; use std::{collections::HashMap, fmt::Write as _, num::NonZeroU32, str, sync::Arc}; @@ -27,7 +27,6 @@ pub use keymap_file::{keymap_file_json_schema, KeymapFileContent}; #[derive(Clone)] pub struct Settings { - pub experiments: FeatureFlags, pub buffer_font_family: FamilyId, pub default_buffer_font_size: f32, pub buffer_font_size: f32, @@ -36,6 +35,7 @@ pub struct Settings { 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 default_dock_anchor: DockAnchor, @@ -53,7 +53,7 @@ pub struct Settings { pub theme: Arc, pub telemetry_defaults: TelemetrySettings, pub telemetry_overrides: TelemetrySettings, - pub staff_mode: bool, + pub auto_update: bool, } #[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] @@ -71,17 +71,6 @@ impl TelemetrySettings { } } -#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] -pub struct FeatureFlags { - pub experimental_themes: bool, -} - -impl FeatureFlags { - pub fn keymap_files(&self) -> Vec<&'static str> { - vec![] - } -} - #[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] pub struct GitSettings { pub git_gutter: Option, @@ -253,6 +242,7 @@ pub enum DockAnchor { Expanded, } +impl StaticColumnCount for DockAnchor {} impl Bind for DockAnchor { fn bind(&self, statement: &Statement, start_index: i32) -> anyhow::Result { match self { @@ -282,7 +272,6 @@ impl Column for DockAnchor { #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] pub struct SettingsFileContent { - pub experiments: Option, #[serde(default)] pub projects_online_by_default: Option, #[serde(default)] @@ -300,6 +289,8 @@ pub struct SettingsFileContent { #[serde(default)] pub show_completions_on_input: Option, #[serde(default)] + pub show_call_status_icon: Option, + #[serde(default)] pub vim_mode: Option, #[serde(default)] pub autosave: Option, @@ -323,7 +314,7 @@ pub struct SettingsFileContent { #[serde(default)] pub telemetry: TelemetrySettings, #[serde(default)] - pub staff_mode: Option, + pub auto_update: Option, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] @@ -351,7 +342,6 @@ impl Settings { .unwrap(); Self { - experiments: FeatureFlags::default(), buffer_font_family: font_cache .load_family(&[defaults.buffer_font_family.as_ref().unwrap()]) .unwrap(), @@ -362,6 +352,7 @@ impl Settings { 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(), default_dock_anchor: defaults.default_dock_anchor.unwrap(), @@ -387,7 +378,7 @@ impl Settings { theme: themes.get(&defaults.theme.unwrap()).unwrap(), telemetry_defaults: defaults.telemetry, telemetry_overrides: Default::default(), - staff_mode: false, + auto_update: defaults.auto_update.unwrap(), } } @@ -424,8 +415,6 @@ impl Settings { ); merge(&mut self.vim_mode, data.vim_mode); merge(&mut self.autosave, data.autosave); - merge(&mut self.experiments, data.experiments); - merge(&mut self.staff_mode, data.staff_mode); merge(&mut self.default_dock_anchor, data.default_dock_anchor); // Ensure terminal font is loaded, so we can request it in terminal_element layout @@ -442,6 +431,7 @@ impl Settings { 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( @@ -551,7 +541,6 @@ impl Settings { #[cfg(any(test, feature = "test-support"))] pub fn test(cx: &gpui::AppContext) -> Settings { Settings { - experiments: FeatureFlags::default(), buffer_font_family: cx.font_cache().load_family(&["Monaco"]).unwrap(), buffer_font_size: 14., active_pane_magnification: 1., @@ -560,6 +549,7 @@ impl Settings { cursor_blink: true, hover_popover_enabled: true, show_completions_on_input: true, + show_call_status_icon: true, vim_mode: false, autosave: Autosave::Off, default_dock_anchor: DockAnchor::Bottom, @@ -588,7 +578,7 @@ impl Settings { metrics: Some(true), }, telemetry_overrides: Default::default(), - staff_mode: false, + auto_update: true, } } @@ -646,8 +636,6 @@ pub fn settings_file_json_schema( ]); let root_schema_object = &mut root_schema.schema.object.as_mut().unwrap(); - // Avoid automcomplete for non-user facing settings - root_schema_object.properties.remove("staff_mode"); root_schema_object.properties.extend([ ( "theme".to_owned(), diff --git a/crates/sqlez/Cargo.toml b/crates/sqlez/Cargo.toml index 8409a1dff5..716ec76644 100644 --- a/crates/sqlez/Cargo.toml +++ b/crates/sqlez/Cargo.toml @@ -15,3 +15,4 @@ thread_local = "1.1.4" lazy_static = "1.4" parking_lot = "0.11.1" futures = "0.3" +uuid = { version = "1.1.2", features = ["v4"] } \ No newline at end of file diff --git a/crates/sqlez/src/bindable.rs b/crates/sqlez/src/bindable.rs index 62212d8f18..86d69afe5f 100644 --- a/crates/sqlez/src/bindable.rs +++ b/crates/sqlez/src/bindable.rs @@ -9,6 +9,12 @@ use anyhow::{Context, Result}; use crate::statement::{SqlType, Statement}; +pub trait StaticColumnCount { + fn column_count() -> usize { + 1 + } +} + pub trait Bind { fn bind(&self, statement: &Statement, start_index: i32) -> Result; } @@ -17,6 +23,7 @@ pub trait Column: Sized { fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)>; } +impl StaticColumnCount for bool {} impl Bind for bool { fn bind(&self, statement: &Statement, start_index: i32) -> Result { statement @@ -33,6 +40,7 @@ impl Column for bool { } } +impl StaticColumnCount for &[u8] {} impl Bind for &[u8] { fn bind(&self, statement: &Statement, start_index: i32) -> Result { statement @@ -42,6 +50,7 @@ impl Bind for &[u8] { } } +impl StaticColumnCount for &[u8; C] {} impl Bind for &[u8; C] { fn bind(&self, statement: &Statement, start_index: i32) -> Result { statement @@ -51,6 +60,15 @@ impl Bind for &[u8; C] { } } +impl Column for [u8; C] { + fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { + let bytes_slice = statement.column_blob(start_index)?; + let array = bytes_slice.try_into()?; + Ok((array, start_index + 1)) + } +} + +impl StaticColumnCount for Vec {} impl Bind for Vec { fn bind(&self, statement: &Statement, start_index: i32) -> Result { statement @@ -70,6 +88,7 @@ impl Column for Vec { } } +impl StaticColumnCount for f64 {} impl Bind for f64 { fn bind(&self, statement: &Statement, start_index: i32) -> Result { statement @@ -89,6 +108,7 @@ impl Column for f64 { } } +impl StaticColumnCount for f32 {} impl Bind for f32 { fn bind(&self, statement: &Statement, start_index: i32) -> Result { statement @@ -109,6 +129,7 @@ impl Column for f32 { } } +impl StaticColumnCount for i32 {} impl Bind for i32 { fn bind(&self, statement: &Statement, start_index: i32) -> Result { statement @@ -126,6 +147,7 @@ impl Column for i32 { } } +impl StaticColumnCount for i64 {} impl Bind for i64 { fn bind(&self, statement: &Statement, start_index: i32) -> Result { statement @@ -142,6 +164,7 @@ impl Column for i64 { } } +impl StaticColumnCount for u32 {} impl Bind for u32 { fn bind(&self, statement: &Statement, start_index: i32) -> Result { (*self as i64) @@ -157,6 +180,7 @@ impl Column for u32 { } } +impl StaticColumnCount for usize {} impl Bind for usize { fn bind(&self, statement: &Statement, start_index: i32) -> Result { (*self as i64) @@ -172,6 +196,7 @@ impl Column for usize { } } +impl StaticColumnCount for &str {} impl Bind for &str { fn bind(&self, statement: &Statement, start_index: i32) -> Result { statement.bind_text(start_index, self)?; @@ -179,6 +204,7 @@ impl Bind for &str { } } +impl StaticColumnCount for Arc {} impl Bind for Arc { fn bind(&self, statement: &Statement, start_index: i32) -> Result { statement.bind_text(start_index, self.as_ref())?; @@ -186,6 +212,7 @@ impl Bind for Arc { } } +impl StaticColumnCount for String {} impl Bind for String { fn bind(&self, statement: &Statement, start_index: i32) -> Result { statement.bind_text(start_index, self)?; @@ -207,27 +234,40 @@ impl Column for String { } } -impl Bind for Option { - fn bind(&self, statement: &Statement, start_index: i32) -> Result { +impl StaticColumnCount for Option { + fn column_count() -> usize { + T::column_count() + } +} +impl Bind for Option { + fn bind(&self, statement: &Statement, mut start_index: i32) -> Result { if let Some(this) = self { this.bind(statement, start_index) } else { - statement.bind_null(start_index)?; - Ok(start_index + 1) + for _ in 0..T::column_count() { + statement.bind_null(start_index)?; + start_index += 1; + } + Ok(start_index) } } } -impl Column for Option { +impl Column for Option { fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { if let SqlType::Null = statement.column_type(start_index)? { - Ok((None, start_index + 1)) + Ok((None, start_index + T::column_count() as i32)) } else { T::column(statement, start_index).map(|(result, next_index)| (Some(result), next_index)) } } } +impl StaticColumnCount for [T; COUNT] { + fn column_count() -> usize { + T::column_count() * COUNT + } +} impl Bind for [T; COUNT] { fn bind(&self, statement: &Statement, start_index: i32) -> Result { let mut current_index = start_index; @@ -239,51 +279,21 @@ impl Bind for [T; COUNT] { } } -impl Column for [T; COUNT] { - fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { - let mut array = [Default::default(); COUNT]; - let mut current_index = start_index; - for i in 0..COUNT { - (array[i], current_index) = T::column(statement, current_index)?; - } - Ok((array, current_index)) - } -} - -impl Bind for Vec { - fn bind(&self, statement: &Statement, start_index: i32) -> Result { - let mut current_index = start_index; - for binding in self.iter() { - current_index = binding.bind(statement, current_index)? - } - - Ok(current_index) - } -} - -impl Bind for &[T] { - fn bind(&self, statement: &Statement, start_index: i32) -> Result { - let mut current_index = start_index; - for binding in *self { - current_index = binding.bind(statement, current_index)? - } - - Ok(current_index) - } -} - +impl StaticColumnCount for &Path {} impl Bind for &Path { fn bind(&self, statement: &Statement, start_index: i32) -> Result { self.as_os_str().as_bytes().bind(statement, start_index) } } +impl StaticColumnCount for Arc {} impl Bind for Arc { fn bind(&self, statement: &Statement, start_index: i32) -> Result { self.as_ref().bind(statement, start_index) } } +impl StaticColumnCount for PathBuf {} impl Bind for PathBuf { fn bind(&self, statement: &Statement, start_index: i32) -> Result { (self.as_ref() as &Path).bind(statement, start_index) @@ -301,6 +311,30 @@ impl Column for PathBuf { } } +impl StaticColumnCount for uuid::Uuid { + fn column_count() -> usize { + 1 + } +} + +impl Bind for uuid::Uuid { + fn bind(&self, statement: &Statement, start_index: i32) -> Result { + self.as_bytes().bind(statement, start_index) + } +} + +impl Column for uuid::Uuid { + fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { + let (bytes, next_index) = Column::column(statement, start_index)?; + Ok((uuid::Uuid::from_bytes(bytes), next_index)) + } +} + +impl StaticColumnCount for () { + fn column_count() -> usize { + 0 + } +} /// Unit impls do nothing. This simplifies query macros impl Bind for () { fn bind(&self, _statement: &Statement, start_index: i32) -> Result { @@ -314,74 +348,79 @@ impl Column for () { } } -impl Bind for (T1, T2) { - fn bind(&self, statement: &Statement, start_index: i32) -> Result { - let next_index = self.0.bind(statement, start_index)?; - self.1.bind(statement, next_index) +macro_rules! impl_tuple_row_traits { + ( $($local:ident: $type:ident),+ ) => { + impl<$($type: StaticColumnCount),+> StaticColumnCount for ($($type,)+) { + fn column_count() -> usize { + let mut count = 0; + $(count += $type::column_count();)+ + count + } + } + + impl<$($type: Bind),+> Bind for ($($type,)+) { + fn bind(&self, statement: &Statement, start_index: i32) -> Result { + let mut next_index = start_index; + let ($($local,)+) = self; + $(next_index = $local.bind(statement, next_index)?;)+ + Ok(next_index) + } + } + + impl<$($type: Column),+> Column for ($($type,)+) { + fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { + let mut next_index = start_index; + Ok(( + ( + $({ + let value; + (value, next_index) = $type::column(statement, next_index)?; + value + },)+ + ), + next_index, + )) + } + } } } -impl Column for (T1, T2) { - fn column<'a>(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { - let (first, next_index) = T1::column(statement, start_index)?; - let (second, next_index) = T2::column(statement, next_index)?; - Ok(((first, second), next_index)) - } -} - -impl Bind for (T1, T2, T3) { - fn bind(&self, statement: &Statement, start_index: i32) -> Result { - let next_index = self.0.bind(statement, start_index)?; - let next_index = self.1.bind(statement, next_index)?; - self.2.bind(statement, next_index) - } -} - -impl Column for (T1, T2, T3) { - fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { - let (first, next_index) = T1::column(statement, start_index)?; - let (second, next_index) = T2::column(statement, next_index)?; - let (third, next_index) = T3::column(statement, next_index)?; - Ok(((first, second, third), next_index)) - } -} - -impl Bind for (T1, T2, T3, T4) { - fn bind(&self, statement: &Statement, start_index: i32) -> Result { - let next_index = self.0.bind(statement, start_index)?; - let next_index = self.1.bind(statement, next_index)?; - let next_index = self.2.bind(statement, next_index)?; - self.3.bind(statement, next_index) - } -} - -impl Column for (T1, T2, T3, T4) { - fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { - let (first, next_index) = T1::column(statement, start_index)?; - let (second, next_index) = T2::column(statement, next_index)?; - let (third, next_index) = T3::column(statement, next_index)?; - let (fourth, next_index) = T4::column(statement, next_index)?; - Ok(((first, second, third, fourth), next_index)) - } -} - -impl Bind for (T1, T2, T3, T4, T5) { - fn bind(&self, statement: &Statement, start_index: i32) -> Result { - let next_index = self.0.bind(statement, start_index)?; - let next_index = self.1.bind(statement, next_index)?; - let next_index = self.2.bind(statement, next_index)?; - let next_index = self.3.bind(statement, next_index)?; - self.4.bind(statement, next_index) - } -} - -impl Column for (T1, T2, T3, T4, T5) { - fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { - let (first, next_index) = T1::column(statement, start_index)?; - let (second, next_index) = T2::column(statement, next_index)?; - let (third, next_index) = T3::column(statement, next_index)?; - let (fourth, next_index) = T4::column(statement, next_index)?; - let (fifth, next_index) = T5::column(statement, next_index)?; - Ok(((first, second, third, fourth, fifth), next_index)) - } -} +impl_tuple_row_traits!(t1: T1, t2: T2); +impl_tuple_row_traits!(t1: T1, t2: T2, t3: T3); +impl_tuple_row_traits!(t1: T1, t2: T2, t3: T3, t4: T4); +impl_tuple_row_traits!(t1: T1, t2: T2, t3: T3, t4: T4, t5: T5); +impl_tuple_row_traits!(t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6); +impl_tuple_row_traits!(t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6, t7: T7); +impl_tuple_row_traits!( + t1: T1, + t2: T2, + t3: T3, + t4: T4, + t5: T5, + t6: T6, + t7: T7, + t8: T8 +); +impl_tuple_row_traits!( + t1: T1, + t2: T2, + t3: T3, + t4: T4, + t5: T5, + t6: T6, + t7: T7, + t8: T8, + t9: T9 +); +impl_tuple_row_traits!( + t1: T1, + t2: T2, + t3: T3, + t4: T4, + t5: T5, + t6: T6, + t7: T7, + t8: T8, + t9: T9, + t10: T10 +); diff --git a/crates/sqlez/src/migrations.rs b/crates/sqlez/src/migrations.rs index 41c505f85b..b8e589e268 100644 --- a/crates/sqlez/src/migrations.rs +++ b/crates/sqlez/src/migrations.rs @@ -4,12 +4,36 @@ // to creating a new db?) // Otherwise any missing migrations are run on the connection -use anyhow::{anyhow, Result}; +use std::ffi::CString; + +use anyhow::{anyhow, Context, Result}; use indoc::{formatdoc, indoc}; +use libsqlite3_sys::sqlite3_exec; use crate::connection::Connection; impl Connection { + fn eager_exec(&self, sql: &str) -> anyhow::Result<()> { + let sql_str = CString::new(sql).context("Error creating cstr")?; + unsafe { + sqlite3_exec( + self.sqlite3, + sql_str.as_c_str().as_ptr(), + None, + 0 as *mut _, + 0 as *mut _, + ); + } + self.last_error() + .with_context(|| format!("Prepare call failed for query:\n{}", sql))?; + + Ok(()) + } + + /// Migrate the database, for the given domain. + /// Note: Unlike everything else in SQLez, migrations are run eagerly, without first + /// preparing the SQL statements. This makes it possible to do multi-statement schema + /// updates in a single string without running into prepare errors. pub fn migrate(&self, domain: &'static str, migrations: &[&'static str]) -> Result<()> { self.with_savepoint("migrating", || { // Setup the migrations table unconditionally @@ -47,7 +71,7 @@ impl Connection { } } - self.exec(migration)?()?; + self.eager_exec(migration)?; store_completed_migration((domain, index, *migration))?; } @@ -257,4 +281,38 @@ mod test { // Verify new migration returns error when run assert!(second_migration_result.is_err()) } + + #[test] + fn test_create_alter_drop() { + let connection = Connection::open_memory(Some("test_create_alter_drop")); + + connection + .migrate("first_migration", &["CREATE TABLE table1(a TEXT) STRICT;"]) + .unwrap(); + + connection + .exec("INSERT INTO table1(a) VALUES (\"test text\");") + .unwrap()() + .unwrap(); + + connection + .migrate( + "second_migration", + &[indoc! {" + CREATE TABLE table2(b TEXT) STRICT; + + INSERT INTO table2 (b) + SELECT a FROM table1; + + DROP TABLE table1; + + ALTER TABLE table2 RENAME TO table1; + "}], + ) + .unwrap(); + + let res = &connection.select::("SELECT b FROM table1").unwrap()().unwrap()[0]; + + assert_eq!(res, "test text"); + } } diff --git a/crates/sqlez/src/statement.rs b/crates/sqlez/src/statement.rs index f3ec6d4854..69d5685ba0 100644 --- a/crates/sqlez/src/statement.rs +++ b/crates/sqlez/src/statement.rs @@ -238,12 +238,11 @@ impl<'a> Statement<'a> { pub fn bind(&self, value: T, index: i32) -> Result { debug_assert!(index > 0); - value.bind(self, index) + Ok(value.bind(self, index)?) } pub fn column(&mut self) -> Result { - let (result, _) = T::column(self, 0)?; - Ok(result) + Ok(T::column(self, 0)?.0) } pub fn column_type(&mut self, index: i32) -> Result { diff --git a/crates/sqlez/src/typed_statements.rs b/crates/sqlez/src/typed_statements.rs index df4a2987b5..488ee27c0c 100644 --- a/crates/sqlez/src/typed_statements.rs +++ b/crates/sqlez/src/typed_statements.rs @@ -7,11 +7,23 @@ use crate::{ }; impl Connection { + /// Prepare a statement which has no bindings and returns nothing. + /// + /// Note: If there are multiple statements that depend upon each other + /// (such as those which make schema changes), preparation will fail. + /// Use a true migration instead. pub fn exec<'a>(&'a self, query: &str) -> Result Result<()>> { let mut statement = Statement::prepare(self, query)?; Ok(move || statement.exec()) } + /// Prepare a statement which takes a binding, but returns nothing. + /// The bindings for a given invocation should be passed to the returned + /// closure + /// + /// Note: If there are multiple statements that depend upon each other + /// (such as those which make schema changes), preparation will fail. + /// Use a true migration instead. pub fn exec_bound<'a, B: Bind>( &'a self, query: &str, @@ -20,6 +32,11 @@ impl Connection { Ok(move |bindings| statement.with_bindings(bindings)?.exec()) } + /// Prepare a statement which has no bindings and returns a `Vec`. + /// + /// Note: If there are multiple statements that depend upon each other + /// (such as those which make schema changes), preparation will fail. + /// Use a true migration instead. pub fn select<'a, C: Column>( &'a self, query: &str, @@ -28,6 +45,11 @@ impl Connection { Ok(move || statement.rows::()) } + /// Prepare a statement which takes a binding and returns a `Vec`. + /// + /// Note: If there are multiple statements that depend upon each other + /// (such as those which make schema changes), preparation will fail. + /// Use a true migration instead. pub fn select_bound<'a, B: Bind, C: Column>( &'a self, query: &str, @@ -36,6 +58,13 @@ impl Connection { Ok(move |bindings| statement.with_bindings(bindings)?.rows::()) } + /// Prepare a statement that selects a single row from the database. + /// Will return none if no rows are returned and will error if more than + /// 1 row + /// + /// Note: If there are multiple statements that depend upon each other + /// (such as those which make schema changes), preparation will fail. + /// Use a true migration instead. pub fn select_row<'a, C: Column>( &'a self, query: &str, @@ -44,6 +73,13 @@ impl Connection { Ok(move || statement.maybe_row::()) } + /// Prepare a statement which takes a binding and selects a single row + /// from the database. WIll return none if no rows are returned and will + /// error if more than 1 row is returned. + /// + /// Note: If there are multiple statements that depend upon each other + /// (such as those which make schema changes), preparation will fail. + /// Use a true migration instead. pub fn select_row_bound<'a, B: Bind, C: Column>( &'a self, query: &str, diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index dd5c5fb3b0..feed3d510f 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -32,17 +32,14 @@ use mappings::mouse::{ use procinfo::LocalProcessInfo; use settings::{AlternateScroll, Settings, Shell, TerminalBlink}; -use util::ResultExt; use std::{ cmp::min, collections::{HashMap, VecDeque}, fmt::Display, - io, ops::{Deref, Index, RangeInclusive, Sub}, - os::unix::{prelude::AsRawFd, process::CommandExt}, + os::unix::prelude::AsRawFd, path::PathBuf, - process::Command, sync::Arc, time::{Duration, Instant}, }; @@ -643,6 +640,8 @@ impl Terminal { if (new_cursor.line.0 as usize) < term.screen_lines() - 1 { term.grid_mut().reset_region((new_cursor.line + 1)..); } + + cx.emit(Event::Wakeup); } InternalEvent::Scroll(scroll) => { term.scroll_display(*scroll); @@ -734,7 +733,7 @@ impl Terminal { if let Some((url, url_match)) = found_url { if *open { - open_uri(&url).log_err(); + cx.platform().open_url(url.as_str()); } else { self.update_hyperlink(prev_hyperlink, url, url_match); } @@ -1075,7 +1074,7 @@ impl Terminal { if self.selection_phase == SelectionPhase::Ended { let mouse_cell_index = content_index_for_mouse(position, &self.last_content); if let Some(link) = self.last_content.cells[mouse_cell_index].hyperlink() { - open_uri(link.uri()).log_err(); + cx.platform().open_url(link.uri()); } else { self.events .push_back(InternalEvent::FindHyperlink(position, true)); @@ -1234,31 +1233,6 @@ fn content_index_for_mouse<'a>(pos: Vector2F, content: &'a TerminalContent) -> u line * content.size.columns() + col } -fn open_uri(uri: &str) -> Result<(), std::io::Error> { - let mut command = Command::new("open"); - command.arg(uri); - - unsafe { - command - .pre_exec(|| { - match libc::fork() { - -1 => return Err(io::Error::last_os_error()), - 0 => (), - _ => libc::_exit(0), - } - - if libc::setsid() == -1 { - return Err(io::Error::last_os_error()); - } - - Ok(()) - }) - .spawn()? - .wait() - .map(|_| ()) - } -} - #[cfg(test)] mod tests { use alacritty_terminal::{ diff --git a/crates/terminal_view/src/persistence.rs b/crates/terminal_view/src/persistence.rs index 26bd0931fe..0da9ed4729 100644 --- a/crates/terminal_view/src/persistence.rs +++ b/crates/terminal_view/src/persistence.rs @@ -14,6 +14,26 @@ define_connection! { FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE ) STRICT; + ), + // Remove the unique constraint on the item_id table + // SQLite doesn't have a way of doing this automatically, so + // we have to do this silly copying. + sql!( + CREATE TABLE terminals2 ( + workspace_id INTEGER, + item_id INTEGER, + working_directory BLOB, + PRIMARY KEY(workspace_id, item_id), + FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) + ON DELETE CASCADE + ) STRICT; + + INSERT INTO terminals2 (workspace_id, item_id, working_directory) + SELECT workspace_id, item_id, working_directory FROM terminals; + + DROP TABLE terminals; + + ALTER TABLE terminals2 RENAME TO terminals; )]; } diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 847dfc5ee5..cc3025d96e 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -21,6 +21,7 @@ use gpui::{ use project::{LocalWorktree, Project}; use serde::Deserialize; use settings::{Settings, TerminalBlink, WorkingDirectory}; +use smallvec::SmallVec; use smol::Timer; use terminal::{ alacritty_terminal::{ @@ -664,12 +665,12 @@ impl Item for TerminalView { Some(Box::new(handle.clone())) } - fn to_item_events(event: &Self::Event) -> Vec { + fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]> { match event { - Event::BreadcrumbsChanged => vec![ItemEvent::UpdateBreadcrumbs], - Event::TitleChanged | Event::Wakeup => vec![ItemEvent::UpdateTab], - Event::CloseTerminal => vec![ItemEvent::CloseItem], - _ => vec![], + Event::BreadcrumbsChanged => smallvec::smallvec![ItemEvent::UpdateBreadcrumbs], + Event::TitleChanged | Event::Wakeup => smallvec::smallvec![ItemEvent::UpdateTab], + Event::CloseTerminal => smallvec::smallvec![ItemEvent::CloseItem], + _ => smallvec::smallvec![], } } diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index e463310b98..bc338bbe26 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -36,6 +36,7 @@ pub struct Theme { pub incoming_call_notification: IncomingCallNotification, pub tooltip: TooltipStyle, pub terminal: TerminalStyle, + pub feedback: FeedbackStyle, pub color_scheme: ColorScheme, } @@ -806,6 +807,13 @@ pub struct TerminalStyle { pub dim_foreground: Color, } +#[derive(Clone, Deserialize, Default)] +pub struct FeedbackStyle { + pub submit_button: Interactive, + pub button_margin: f32, + pub info_text: ContainedText, +} + #[derive(Clone, Deserialize, Default)] pub struct ColorScheme { pub name: String, diff --git a/crates/theme/src/theme_registry.rs b/crates/theme/src/theme_registry.rs index 3d4783604d..d47625289b 100644 --- a/crates/theme/src/theme_registry.rs +++ b/crates/theme/src/theme_registry.rs @@ -22,20 +22,13 @@ impl ThemeRegistry { }) } - pub fn list(&self, internal: bool, experiments: bool) -> impl Iterator + '_ { + pub fn list(&self, staff: bool) -> impl Iterator + '_ { let mut dirs = self.assets.list("themes/"); - if !internal { + if !staff { dirs = dirs .into_iter() - .filter(|path| !path.starts_with("themes/Internal")) - .collect() - } - - if !experiments { - dirs = dirs - .into_iter() - .filter(|path| !path.starts_with("themes/Experiments")) + .filter(|path| !path.starts_with("themes/staff")) .collect() } @@ -62,13 +55,13 @@ impl ThemeRegistry { .load(&asset_path) .with_context(|| format!("failed to load theme file {}", asset_path))?; - let mut theme: Theme = fonts::with_font_cache(self.font_cache.clone(), || { + // Allocate into the heap directly, the Theme struct is too large to fit in the stack. + let mut theme: Arc = fonts::with_font_cache(self.font_cache.clone(), || { serde_path_to_error::deserialize(&mut serde_json::Deserializer::from_slice(&theme_json)) })?; // Reset name to be the file path, so that we can use it to access the stored themes - theme.meta.name = name.into(); - let theme = Arc::new(theme); + Arc::get_mut(&mut theme).unwrap().meta.name = name.into(); self.themes.lock().insert(name.to_string(), theme.clone()); Ok(theme) } diff --git a/crates/theme_selector/Cargo.toml b/crates/theme_selector/Cargo.toml index 8f6fc74600..80ff311069 100644 --- a/crates/theme_selector/Cargo.toml +++ b/crates/theme_selector/Cargo.toml @@ -16,6 +16,7 @@ picker = { path = "../picker" } theme = { path = "../theme" } settings = { path = "../settings" } workspace = { path = "../workspace" } +util = { path = "../util" } log = { version = "0.4.16", features = ["kv_unstable_serde"] } parking_lot = "0.11.1" postage = { version = "0.4.1", features = ["futures-traits"] } diff --git a/crates/theme_selector/src/theme_selector.rs b/crates/theme_selector/src/theme_selector.rs index 252a64c7fd..d999730a0d 100644 --- a/crates/theme_selector/src/theme_selector.rs +++ b/crates/theme_selector/src/theme_selector.rs @@ -7,6 +7,7 @@ use picker::{Picker, PickerDelegate}; use settings::{settings_file::SettingsFile, Settings}; use std::sync::Arc; use theme::{Theme, ThemeMeta, ThemeRegistry}; +use util::StaffMode; use workspace::{AppState, Workspace}; pub struct ThemeSelector { @@ -44,10 +45,7 @@ impl ThemeSelector { let original_theme = settings.theme.clone(); let mut theme_names = registry - .list( - settings.staff_mode, - settings.experiments.experimental_themes, - ) + .list(**cx.default_global::()) .collect::>(); theme_names.sort_unstable_by(|a, b| { a.is_light diff --git a/crates/theme_testbench/src/theme_testbench.rs b/crates/theme_testbench/src/theme_testbench.rs index 3cda5d3e51..84ec68e636 100644 --- a/crates/theme_testbench/src/theme_testbench.rs +++ b/crates/theme_testbench/src/theme_testbench.rs @@ -11,6 +11,7 @@ use gpui::{ }; use project::Project; use settings::Settings; +use smallvec::SmallVec; use theme::{ColorScheme, Layer, Style, StyleSet}; use workspace::{ item::{Item, ItemEvent}, @@ -350,8 +351,8 @@ impl Item for ThemeTestbench { gpui::Task::ready(Ok(())) } - fn to_item_events(_: &Self::Event) -> Vec { - Vec::new() + fn to_item_events(_: &Self::Event) -> SmallVec<[ItemEvent; 2]> { + SmallVec::new() } fn serialized_item_kind() -> Option<&'static str> { diff --git a/crates/util/Cargo.toml b/crates/util/Cargo.toml index 4cbaa382e8..e8c158b637 100644 --- a/crates/util/Cargo.toml +++ b/crates/util/Cargo.toml @@ -5,6 +5,7 @@ edition = "2021" publish = false [lib] +path = "src/util.rs" doctest = false [features] @@ -22,7 +23,6 @@ serde_json = { version = "1.0", features = ["preserve_order"], optional = true } git2 = { version = "0.15", default-features = false, optional = true } dirs = "3.0" - [dev-dependencies] tempdir = { version = "0.3.7" } serde_json = { version = "1.0", features = ["preserve_order"] } diff --git a/crates/util/src/lib.rs b/crates/util/src/util.rs similarity index 77% rename from crates/util/src/lib.rs rename to crates/util/src/util.rs index e79cc269c9..37e1f29ce2 100644 --- a/crates/util/src/lib.rs +++ b/crates/util/src/util.rs @@ -3,16 +3,28 @@ pub mod paths; #[cfg(any(test, feature = "test-support"))] pub mod test; -pub use backtrace::Backtrace; -use futures::Future; -use rand::{seq::SliceRandom, Rng}; use std::{ - cmp::Ordering, - ops::AddAssign, + cmp::{self, Ordering}, + ops::{AddAssign, Range, RangeInclusive}, pin::Pin, task::{Context, Poll}, }; +pub use backtrace::Backtrace; +use futures::Future; +use rand::{seq::SliceRandom, Rng}; + +#[derive(Debug, Default)] +pub struct StaffMode(pub bool); + +impl std::ops::Deref for StaffMode { + type Target = bool; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + #[macro_export] macro_rules! debug_panic { ( $($fmt_arg:tt)* ) => { @@ -35,10 +47,10 @@ pub fn truncate(s: &str, max_chars: usize) -> &str { pub fn truncate_and_trailoff(s: &str, max_chars: usize) -> String { debug_assert!(max_chars >= 5); - if s.len() > max_chars { - format!("{}…", truncate(s, max_chars.saturating_sub(3))) - } else { - s.to_string() + let truncation_ix = s.char_indices().map(|(i, _)| i).nth(max_chars); + match truncation_ix { + Some(length) => s[..length].to_string() + "…", + None => s.to_string(), } } @@ -234,6 +246,46 @@ macro_rules! async_iife { }; } +pub trait RangeExt { + fn sorted(&self) -> Self; + fn to_inclusive(&self) -> RangeInclusive; + fn overlaps(&self, other: &Range) -> bool; +} + +impl RangeExt for Range { + fn sorted(&self) -> Self { + cmp::min(&self.start, &self.end).clone()..cmp::max(&self.start, &self.end).clone() + } + + fn to_inclusive(&self) -> RangeInclusive { + self.start.clone()..=self.end.clone() + } + + fn overlaps(&self, other: &Range) -> bool { + self.contains(&other.start) + || self.contains(&other.end) + || other.contains(&self.start) + || other.contains(&self.end) + } +} + +impl RangeExt for RangeInclusive { + fn sorted(&self) -> Self { + cmp::min(self.start(), self.end()).clone()..=cmp::max(self.start(), self.end()).clone() + } + + fn to_inclusive(&self) -> RangeInclusive { + self.clone() + } + + fn overlaps(&self, other: &Range) -> bool { + self.contains(&other.start) + || self.contains(&other.end) + || other.contains(&self.start()) + || other.contains(&self.end()) + } +} + #[cfg(test)] mod tests { use super::*; @@ -265,4 +317,12 @@ mod tests { assert_eq!(foo, None); } + + #[test] + fn test_trancate_and_trailoff() { + assert_eq!(truncate_and_trailoff("", 5), ""); + assert_eq!(truncate_and_trailoff("èèèèèè", 7), "èèèèèè"); + assert_eq!(truncate_and_trailoff("èèèèèè", 6), "èèèèèè"); + assert_eq!(truncate_and_trailoff("èèèèèè", 5), "èèèèè…"); + } } diff --git a/crates/vim/src/editor_events.rs b/crates/vim/src/editor_events.rs index c526e3b1dc..c58f66478f 100644 --- a/crates/vim/src/editor_events.rs +++ b/crates/vim/src/editor_events.rs @@ -1,62 +1,66 @@ -use editor::{EditorBlurred, EditorCreated, EditorFocused, EditorMode, EditorReleased}; +use editor::{EditorBlurred, EditorFocused, EditorMode, EditorReleased, Event}; use gpui::MutableAppContext; use crate::{state::Mode, Vim}; pub fn init(cx: &mut MutableAppContext) { - cx.subscribe_global(editor_created).detach(); - cx.subscribe_global(editor_focused).detach(); - cx.subscribe_global(editor_blurred).detach(); - cx.subscribe_global(editor_released).detach(); + cx.subscribe_global(focused).detach(); + cx.subscribe_global(blurred).detach(); + cx.subscribe_global(released).detach(); } -fn editor_created(EditorCreated(editor): &EditorCreated, cx: &mut MutableAppContext) { - cx.update_default_global(|vim: &mut Vim, cx| { - vim.editors.insert(editor.id(), editor.downgrade()); - vim.sync_vim_settings(cx); - }) -} - -fn editor_focused(EditorFocused(editor): &EditorFocused, cx: &mut MutableAppContext) { +fn focused(EditorFocused(editor): &EditorFocused, cx: &mut MutableAppContext) { Vim::update(cx, |vim, cx| { + if let Some(previously_active_editor) = vim + .active_editor + .as_ref() + .and_then(|editor| editor.upgrade(cx)) + { + vim.unhook_vim_settings(previously_active_editor, cx); + } + vim.active_editor = Some(editor.downgrade()); - vim.selection_subscription = Some(cx.subscribe(editor, |editor, event, cx| { - if editor.read(cx).leader_replica_id().is_none() { - if let editor::Event::SelectionsChanged { local: true } = event { - let newest_empty = editor.read(cx).selections.newest::(cx).is_empty(); - editor_local_selections_changed(newest_empty, cx); + vim.editor_subscription = Some(cx.subscribe(editor, |editor, event, cx| match event { + Event::SelectionsChanged { local: true } => { + let editor = editor.read(cx); + if editor.leader_replica_id().is_none() { + let newest_empty = editor.selections.newest::(cx).is_empty(); + local_selections_changed(newest_empty, cx); } } + Event::InputIgnored { text } => { + Vim::active_editor_input_ignored(text.clone(), cx); + } + _ => {} })); - if !vim.enabled { - return; + if vim.enabled { + let editor = editor.read(cx); + let editor_mode = editor.mode(); + let newest_selection_empty = editor.selections.newest::(cx).is_empty(); + + if editor_mode == EditorMode::Full && !newest_selection_empty { + vim.switch_mode(Mode::Visual { line: false }, true, cx); + } } - let editor = editor.read(cx); - let editor_mode = editor.mode(); - let newest_selection_empty = editor.selections.newest::(cx).is_empty(); - - if editor_mode == EditorMode::Full && !newest_selection_empty { - vim.switch_mode(Mode::Visual { line: false }, true, cx); - } + vim.sync_vim_settings(cx); }); } -fn editor_blurred(EditorBlurred(editor): &EditorBlurred, cx: &mut MutableAppContext) { +fn blurred(EditorBlurred(editor): &EditorBlurred, cx: &mut MutableAppContext) { Vim::update(cx, |vim, cx| { if let Some(previous_editor) = vim.active_editor.clone() { if previous_editor == editor.clone() { vim.active_editor = None; } } - vim.sync_vim_settings(cx); + vim.unhook_vim_settings(editor.clone(), cx); }) } -fn editor_released(EditorReleased(editor): &EditorReleased, cx: &mut MutableAppContext) { +fn released(EditorReleased(editor): &EditorReleased, cx: &mut MutableAppContext) { cx.update_default_global(|vim: &mut Vim, _| { - vim.editors.remove(&editor.id()); if let Some(previous_editor) = vim.active_editor.clone() { if previous_editor == editor.clone() { vim.active_editor = None; @@ -65,7 +69,7 @@ fn editor_released(EditorReleased(editor): &EditorReleased, cx: &mut MutableAppC }); } -fn editor_local_selections_changed(newest_empty: bool, cx: &mut MutableAppContext) { +fn local_selections_changed(newest_empty: bool, cx: &mut MutableAppContext) { Vim::update(cx, |vim, cx| { if vim.enabled && vim.state.mode == Mode::Normal && !newest_empty { vim.switch_mode(Mode::Visual { line: false }, false, cx) diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index 62b30730e8..25188a466c 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -1,7 +1,9 @@ +use std::sync::Arc; + use editor::{ char_kind, display_map::{DisplaySnapshot, ToDisplayPoint}, - movement, Bias, CharKind, DisplayPoint, + movement, Bias, CharKind, DisplayPoint, ToOffset, }; use gpui::{actions, impl_actions, MutableAppContext}; use language::{Point, Selection, SelectionGoal}; @@ -15,7 +17,7 @@ use crate::{ Vim, }; -#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq)] pub enum Motion { Left, Backspace, @@ -32,8 +34,8 @@ pub enum Motion { StartOfDocument, EndOfDocument, Matching, - FindForward { before: bool, character: char }, - FindBackward { after: bool, character: char }, + FindForward { before: bool, text: Arc }, + FindBackward { after: bool, text: Arc }, } #[derive(Clone, Deserialize, PartialEq)] @@ -134,7 +136,7 @@ pub(crate) fn motion(motion: Motion, cx: &mut MutableAppContext) { // Motion handling is specified here: // https://github.com/vim/vim/blob/master/runtime/doc/motion.txt impl Motion { - pub fn linewise(self) -> bool { + pub fn linewise(&self) -> bool { use Motion::*; matches!( self, @@ -142,12 +144,12 @@ impl Motion { ) } - pub fn infallible(self) -> bool { + pub fn infallible(&self) -> bool { use Motion::*; matches!(self, StartOfDocument | CurrentLine | EndOfDocument) } - pub fn inclusive(self) -> bool { + pub fn inclusive(&self) -> bool { use Motion::*; match self { Down @@ -171,13 +173,14 @@ impl Motion { } pub fn move_point( - self, + &self, map: &DisplaySnapshot, point: DisplayPoint, goal: SelectionGoal, times: usize, ) -> Option<(DisplayPoint, SelectionGoal)> { use Motion::*; + let infallible = self.infallible(); let (new_point, goal) = match self { Left => (left(map, point, times), SelectionGoal::None), Backspace => (backspace(map, point, times), SelectionGoal::None), @@ -185,15 +188,15 @@ impl Motion { Up => up(map, point, goal, times), Right => (right(map, point, times), SelectionGoal::None), NextWordStart { ignore_punctuation } => ( - next_word_start(map, point, ignore_punctuation, times), + next_word_start(map, point, *ignore_punctuation, times), SelectionGoal::None, ), NextWordEnd { ignore_punctuation } => ( - next_word_end(map, point, ignore_punctuation, times), + next_word_end(map, point, *ignore_punctuation, times), SelectionGoal::None, ), PreviousWordStart { ignore_punctuation } => ( - previous_word_start(map, point, ignore_punctuation, times), + previous_word_start(map, point, *ignore_punctuation, times), SelectionGoal::None, ), FirstNonWhitespace => (first_non_whitespace(map, point), SelectionGoal::None), @@ -203,22 +206,22 @@ impl Motion { StartOfDocument => (start_of_document(map, point, times), SelectionGoal::None), EndOfDocument => (end_of_document(map, point, times), SelectionGoal::None), Matching => (matching(map, point), SelectionGoal::None), - FindForward { before, character } => ( - find_forward(map, point, before, character, times), + FindForward { before, text } => ( + find_forward(map, point, *before, text.clone(), times), SelectionGoal::None, ), - FindBackward { after, character } => ( - find_backward(map, point, after, character, times), + FindBackward { after, text } => ( + find_backward(map, point, *after, text.clone(), times), SelectionGoal::None, ), }; - (new_point != point || self.infallible()).then_some((new_point, goal)) + (new_point != point || infallible).then_some((new_point, goal)) } // Expands a selection using self motion for an operator pub fn expand_selection( - self, + &self, map: &DisplaySnapshot, selection: &mut Selection, times: usize, @@ -254,7 +257,7 @@ impl Motion { // but "d}" will not include that line. let mut inclusive = self.inclusive(); if !inclusive - && self != Motion::Backspace + && self != &Motion::Backspace && selection.end.row() > selection.start.row() && selection.end.column() == 0 { @@ -447,18 +450,53 @@ fn end_of_document(map: &DisplaySnapshot, point: DisplayPoint, line: usize) -> D map.clip_point(new_point, Bias::Left) } -fn matching(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint { - let offset = point.to_offset(map, Bias::Left); - if let Some((open_range, close_range)) = - map.buffer_snapshot.enclosing_bracket_ranges(offset..offset) - { - if open_range.contains(&offset) { - close_range.start.to_display_point(map) - } else { - open_range.start.to_display_point(map) +fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint { + // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1200 + let point = display_point.to_point(map); + let offset = point.to_offset(&map.buffer_snapshot); + + // Ensure the range is contained by the current line. + let mut line_end = map.next_line_boundary(point).0; + if line_end == point { + line_end = map.max_point().to_point(map); + } + line_end.column = line_end.column.saturating_sub(1); + + let line_range = map.prev_line_boundary(point).0..line_end; + let ranges = map.buffer_snapshot.bracket_ranges(line_range.clone()); + if let Some(ranges) = ranges { + let line_range = line_range.start.to_offset(&map.buffer_snapshot) + ..line_range.end.to_offset(&map.buffer_snapshot); + let mut closest_pair_destination = None; + let mut closest_distance = usize::MAX; + + for (open_range, close_range) in ranges { + if open_range.start >= offset && line_range.contains(&open_range.start) { + let distance = open_range.start - offset; + if distance < closest_distance { + closest_pair_destination = Some(close_range.start); + closest_distance = distance; + continue; + } + } + + if close_range.start >= offset && line_range.contains(&close_range.start) { + let distance = close_range.start - offset; + if distance < closest_distance { + closest_pair_destination = Some(open_range.start); + closest_distance = distance; + continue; + } + } + + continue; } + + closest_pair_destination + .map(|destination| destination.to_display_point(map)) + .unwrap_or(display_point) } else { - point + display_point } } @@ -466,45 +504,42 @@ fn find_forward( map: &DisplaySnapshot, from: DisplayPoint, before: bool, - target: char, - mut times: usize, + target: Arc, + times: usize, ) -> DisplayPoint { - let mut previous_point = from; - - for (ch, point) in map.chars_at(from) { - if ch == target && point != from { - times -= 1; - if times == 0 { - return if before { previous_point } else { point }; + map.find_while(from, target.as_ref(), |ch, _| ch != '\n') + .skip_while(|found_at| found_at == &from) + .nth(times - 1) + .map(|mut found| { + if before { + *found.column_mut() -= 1; + found = map.clip_point(found, Bias::Right); + found + } else { + found } - } else if ch == '\n' { - break; - } - previous_point = point; - } - - from + }) + .unwrap_or(from) } fn find_backward( map: &DisplaySnapshot, from: DisplayPoint, after: bool, - target: char, - mut times: usize, + target: Arc, + times: usize, ) -> DisplayPoint { - let mut previous_point = from; - for (ch, point) in map.reverse_chars_at(from) { - if ch == target && point != from { - times -= 1; - if times == 0 { - return if after { previous_point } else { point }; + map.reverse_find_while(from, target.as_ref(), |ch, _| ch != '\n') + .skip_while(|found_at| found_at == &from) + .nth(times - 1) + .map(|mut found| { + if after { + *found.column_mut() += 1; + found = map.clip_point(found, Bias::Left); + found + } else { + found } - } else if ch == '\n' { - break; - } - previous_point = point; - } - - from + }) + .unwrap_or(from) } diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index d6391353cf..0cac45fd18 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -2,7 +2,7 @@ mod change; mod delete; mod yank; -use std::{borrow::Cow, cmp::Ordering}; +use std::{borrow::Cow, cmp::Ordering, sync::Arc}; use crate::{ motion::Motion, @@ -424,7 +424,7 @@ fn scroll(editor: &mut Editor, amount: &ScrollAmount, cx: &mut ViewContext, cx: &mut MutableAppContext) { Vim::update(cx, |vim, cx| { vim.update_active_editor(cx, |editor, cx| { editor.transact(cx, |editor, cx| { @@ -453,7 +453,7 @@ pub(crate) fn normal_replace(text: &str, cx: &mut MutableAppContext) { ( range.start.to_offset(&map, Bias::Left) ..range.end.to_offset(&map, Bias::Left), - text, + text.clone(), ) }) .collect::>(); @@ -824,17 +824,34 @@ mod test { ˇ brown fox"}) .await; - cx.assert(indoc! {" + + cx.assert_manual( + indoc! {" fn test() { println!(ˇ); - } - "}) - .await; - cx.assert(indoc! {" + }"}, + Mode::Normal, + indoc! {" + fn test() { + println!(); + ˇ + }"}, + Mode::Insert, + ); + + cx.assert_manual( + indoc! {" fn test(ˇ) { println!(); - }"}) - .await; + }"}, + Mode::Normal, + indoc! {" + fn test() { + ˇ + println!(); + }"}, + Mode::Insert, + ); } #[gpui::test] @@ -857,13 +874,15 @@ mod test { // Our indentation is smarter than vims. So we don't match here cx.assert_manual( indoc! {" - fn test() - println!(ˇ);"}, + fn test() { + println!(ˇ); + }"}, Mode::Normal, indoc! {" - fn test() + fn test() { ˇ - println!();"}, + println!(); + }"}, Mode::Insert, ); cx.assert_manual( @@ -994,14 +1013,14 @@ mod test { #[gpui::test] async fn test_capital_f_and_capital_t(cx: &mut gpui::TestAppContext) { let mut cx = NeovimBackedTestContext::new(cx).await; - for count in 1..=3 { - let test_case = indoc! {" - ˇaaaˇbˇ ˇbˇ ˇbˇbˇ aˇaaˇbaaa - ˇ ˇbˇaaˇa ˇbˇbˇb - ˇ - ˇb + let test_case = indoc! {" + ˇaaaˇbˇ ˇbˇ ˇbˇbˇ aˇaaˇbaaa + ˇ ˇbˇaaˇa ˇbˇbˇb + ˇ + ˇb "}; + for count in 1..=3 { cx.assert_binding_matches_all([&count.to_string(), "shift-f", "b"], test_case) .await; @@ -1009,4 +1028,13 @@ mod test { .await; } } + + #[gpui::test] + async fn test_percent(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await.binding(["%"]); + cx.assert_all("ˇconsole.logˇ(ˇvaˇrˇ)ˇ;").await; + cx.assert_all("ˇconsole.logˇ(ˇ'var', ˇ[ˇ1, ˇ2, 3ˇ]ˇ)ˇ;") + .await; + cx.assert_all("let result = curried_funˇ(ˇ)ˇ(ˇ)ˇ;").await; + } } diff --git a/crates/vim/src/test/vim_test_context.rs b/crates/vim/src/test/vim_test_context.rs index 723dac0581..f5614b4b47 100644 --- a/crates/vim/src/test/vim_test_context.rs +++ b/crates/vim/src/test/vim_test_context.rs @@ -1,33 +1,29 @@ use std::ops::{Deref, DerefMut}; -use editor::test::editor_test_context::EditorTestContext; -use gpui::{json::json, AppContext, ContextHandle, ViewHandle}; -use project::Project; +use editor::test::{ + editor_lsp_test_context::EditorLspTestContext, editor_test_context::EditorTestContext, +}; +use gpui::{AppContext, ContextHandle}; use search::{BufferSearchBar, ProjectSearchBar}; -use workspace::{pane, AppState, WorkspaceHandle}; use crate::{state::Operator, *}; use super::VimBindingTestContext; pub struct VimTestContext<'a> { - cx: EditorTestContext<'a>, - workspace: ViewHandle, + cx: EditorLspTestContext<'a>, } impl<'a> VimTestContext<'a> { pub async fn new(cx: &'a mut gpui::TestAppContext, enabled: bool) -> VimTestContext<'a> { cx.update(|cx| { - editor::init(cx); - pane::init(cx); search::init(cx); crate::init(cx); settings::KeymapFileContent::load("keymaps/vim.json", cx).unwrap(); }); - let params = cx.update(AppState::test); - let project = Project::test(params.fs.clone(), [], cx).await; + let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await; cx.update(|cx| { cx.update_global(|settings: &mut Settings, _| { @@ -35,25 +31,11 @@ impl<'a> VimTestContext<'a> { }); }); - params - .fs - .as_fake() - .insert_tree("/root", json!({ "dir": { "test.txt": "" } })) - .await; - - let (window_id, workspace) = cx.add_window(|cx| { - Workspace::new( - Default::default(), - 0, - project.clone(), - |_, _| unimplemented!(), - cx, - ) - }); + let window_id = cx.window_id; // Setup search toolbars and keypress hook - workspace.update(cx, |workspace, cx| { - observe_keypresses(window_id, cx); + cx.update_workspace(|workspace, cx| { + observe_keystrokes(window_id, cx); workspace.active_pane().update(cx, |pane, cx| { pane.toolbar().update(cx, |toolbar, cx| { let buffer_search_bar = cx.add_view(BufferSearchBar::new); @@ -64,44 +46,14 @@ impl<'a> VimTestContext<'a> { }); }); - project - .update(cx, |project, cx| { - project.find_or_create_local_worktree("/root", true, cx) - }) - .await - .unwrap(); - cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx)) - .await; - - let file = cx.read(|cx| workspace.file_project_paths(cx)[0].clone()); - let item = workspace - .update(cx, |workspace, cx| { - workspace.open_path(file, None, true, cx) - }) - .await - .expect("Could not open test file"); - - let editor = cx.update(|cx| { - item.act_as::(cx) - .expect("Opened test file wasn't an editor") - }); - editor.update(cx, |_, cx| cx.focus_self()); - - Self { - cx: EditorTestContext { - cx, - window_id, - editor, - }, - workspace, - } + Self { cx } } pub fn workspace(&mut self, read: F) -> T where F: FnOnce(&Workspace, &AppContext) -> T, { - self.workspace.read_with(self.cx.cx, read) + self.cx.workspace.read_with(self.cx.cx.cx, read) } pub fn enable_vim(&mut self) { diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 9f799ef37f..33f142c21e 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -10,13 +10,12 @@ mod state; mod utils; mod visual; -use collections::HashMap; +use std::sync::Arc; + use command_palette::CommandPaletteFilter; use editor::{Bias, Cancel, Editor, EditorMode}; use gpui::{ - impl_actions, - keymap_matcher::{KeyPressed, Keystroke}, - MutableAppContext, Subscription, ViewContext, WeakViewHandle, + actions, impl_actions, MutableAppContext, Subscription, ViewContext, ViewHandle, WeakViewHandle, }; use language::CursorShape; use motion::Motion; @@ -36,6 +35,7 @@ pub struct PushOperator(pub Operator); #[derive(Clone, Deserialize, PartialEq)] struct Number(u8); +actions!(vim, [Tab, Enter]); impl_actions!(vim, [Number, SwitchMode, PushOperator]); pub fn init(cx: &mut MutableAppContext) { @@ -58,11 +58,6 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_action(|_: &mut Workspace, n: &Number, cx: _| { Vim::update(cx, |vim, cx| vim.push_number(n, cx)); }); - cx.add_action( - |_: &mut Workspace, KeyPressed { keystroke }: &KeyPressed, cx| { - Vim::key_pressed(keystroke, cx); - }, - ); // Editor Actions cx.add_action(|_: &mut Editor, _: &Cancel, cx| { @@ -80,8 +75,16 @@ pub fn init(cx: &mut MutableAppContext) { } }); + cx.add_action(|_: &mut Workspace, _: &Tab, cx| { + Vim::active_editor_input_ignored(" ".into(), cx) + }); + + cx.add_action(|_: &mut Workspace, _: &Enter, cx| { + Vim::active_editor_input_ignored("\n".into(), cx) + }); + // Sync initial settings with the rest of the app - Vim::update(cx, |state, cx| state.sync_vim_settings(cx)); + Vim::update(cx, |vim, cx| vim.sync_vim_settings(cx)); // Any time settings change, update vim mode to match cx.observe_global::(|cx| { @@ -92,7 +95,7 @@ pub fn init(cx: &mut MutableAppContext) { .detach(); } -pub fn observe_keypresses(window_id: usize, cx: &mut MutableAppContext) { +pub fn observe_keystrokes(window_id: usize, cx: &mut MutableAppContext) { cx.observe_keystrokes(window_id, |_keystroke, _result, handled_by, cx| { if let Some(handled_by) = handled_by { // Keystroke is handled by the vim system, so continue forward @@ -104,11 +107,14 @@ pub fn observe_keypresses(window_id: usize, cx: &mut MutableAppContext) { } } - Vim::update(cx, |vim, cx| { - if vim.active_operator().is_some() { - // If the keystroke is not handled by vim, we should clear the operator + Vim::update(cx, |vim, cx| match vim.active_operator() { + Some( + Operator::FindForward { .. } | Operator::FindBackward { .. } | Operator::Replace, + ) => {} + Some(_) => { vim.clear_operator(cx); } + _ => {} }); true }) @@ -117,9 +123,8 @@ pub fn observe_keypresses(window_id: usize, cx: &mut MutableAppContext) { #[derive(Default)] pub struct Vim { - editors: HashMap>, active_editor: Option>, - selection_subscription: Option, + editor_subscription: Option, enabled: bool, state: VimState, @@ -160,24 +165,26 @@ impl Vim { } // Adjust selections - for editor in self.editors.values() { - if let Some(editor) = editor.upgrade(cx) { - editor.update(cx, |editor, cx| { - editor.change_selections(None, cx, |s| { - s.move_with(|map, selection| { - if self.state.empty_selections_only() { - let new_head = map.clip_point(selection.head(), Bias::Left); - selection.collapse_to(new_head, selection.goal) - } else { - selection.set_head( - map.clip_point(selection.head(), Bias::Left), - selection.goal, - ); - } - }); - }) + if let Some(editor) = self + .active_editor + .as_ref() + .and_then(|editor| editor.upgrade(cx)) + { + editor.update(cx, |editor, cx| { + editor.change_selections(None, cx, |s| { + s.move_with(|map, selection| { + if self.state.empty_selections_only() { + let new_head = map.clip_point(selection.head(), Bias::Left); + selection.collapse_to(new_head, selection.goal) + } else { + selection.set_head( + map.clip_point(selection.head(), Bias::Left), + selection.goal, + ); + } + }); }) - } + }) } } @@ -220,24 +227,24 @@ impl Vim { self.state.operator_stack.last().copied() } - fn key_pressed(keystroke: &Keystroke, cx: &mut ViewContext) { + fn active_editor_input_ignored(text: Arc, cx: &mut MutableAppContext) { + if text.is_empty() { + return; + } + match Vim::read(cx).active_operator() { Some(Operator::FindForward { before }) => { - if let Some(character) = keystroke.key.chars().next() { - motion::motion(Motion::FindForward { before, character }, cx) - } + motion::motion(Motion::FindForward { before, text }, cx) } Some(Operator::FindBackward { after }) => { - if let Some(character) = keystroke.key.chars().next() { - motion::motion(Motion::FindBackward { after, character }, cx) - } + motion::motion(Motion::FindBackward { after, text }, cx) } Some(Operator::Replace) => match Vim::read(cx).state.mode { - Mode::Normal => normal_replace(&keystroke.key, cx), - Mode::Visual { line } => visual_replace(&keystroke.key, line, cx), + Mode::Normal => normal_replace(text, cx), + Mode::Visual { line } => visual_replace(text, line, cx), _ => Vim::update(cx, |vim, cx| vim.clear_operator(cx)), }, - _ => cx.propagate_action(), + _ => {} } } @@ -264,26 +271,33 @@ impl Vim { } }); - for editor in self.editors.values() { - if let Some(editor) = editor.upgrade(cx) { + if let Some(editor) = self + .active_editor + .as_ref() + .and_then(|editor| editor.upgrade(cx)) + { + if self.enabled && editor.read(cx).mode() == EditorMode::Full { editor.update(cx, |editor, cx| { - if self.enabled && editor.mode() == EditorMode::Full { - editor.set_cursor_shape(cursor_shape, cx); - editor.set_clip_at_line_ends(state.clip_at_line_end(), cx); - editor.set_input_enabled(!state.vim_controlled()); - editor.selections.line_mode = - matches!(state.mode, Mode::Visual { line: true }); - let context_layer = state.keymap_context_layer(); - editor.set_keymap_context_layer::(context_layer); - } else { - editor.set_cursor_shape(CursorShape::Bar, cx); - editor.set_clip_at_line_ends(false, cx); - editor.set_input_enabled(true); - editor.selections.line_mode = false; - editor.remove_keymap_context_layer::(); - } + editor.set_cursor_shape(cursor_shape, cx); + editor.set_clip_at_line_ends(state.clip_at_line_end(), cx); + editor.set_input_enabled(!state.vim_controlled()); + editor.selections.line_mode = matches!(state.mode, Mode::Visual { line: true }); + let context_layer = state.keymap_context_layer(); + editor.set_keymap_context_layer::(context_layer); }); + } else { + self.unhook_vim_settings(editor, cx); } } } + + fn unhook_vim_settings(&self, editor: ViewHandle, cx: &mut MutableAppContext) { + editor.update(cx, |editor, cx| { + editor.set_cursor_shape(CursorShape::Bar, cx); + editor.set_clip_at_line_ends(false, cx); + editor.set_input_enabled(true); + editor.selections.line_mode = false; + editor.remove_keymap_context_layer::(); + }); + } } diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index ac8771f969..2180fbdabb 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -1,4 +1,4 @@ -use std::borrow::Cow; +use std::{borrow::Cow, sync::Arc}; use collections::HashMap; use editor::{ @@ -313,7 +313,7 @@ pub fn paste(_: &mut Workspace, _: &VisualPaste, cx: &mut ViewContext }); } -pub(crate) fn visual_replace(text: &str, line: bool, cx: &mut MutableAppContext) { +pub(crate) fn visual_replace(text: Arc, line: bool, cx: &mut MutableAppContext) { Vim::update(cx, |vim, cx| { vim.update_active_editor(cx, |editor, cx| { editor.transact(cx, |editor, cx| { @@ -650,7 +650,7 @@ mod test { The quick brown the ˇfox jumps over - dog"}, + dog"}, Mode::Normal, ); } diff --git a/crates/vim/test_data/test_o.json b/crates/vim/test_data/test_o.json index 08bea7cae8..fa1a400bc0 100644 --- a/crates/vim/test_data/test_o.json +++ b/crates/vim/test_data/test_o.json @@ -1 +1 @@ -[{"Text":"\n"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"The quick\n"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"The quick\n\nbrown fox\njumps over"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"The quick\nbrown fox\n\njumps over"},{"Mode":"Insert"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Insert"},{"Text":"The quick\nbrown fox\njumps over\n"},{"Mode":"Insert"},{"Selection":{"start":[3,0],"end":[3,0]}},{"Mode":"Insert"},{"Text":"The quick\n\n\nbrown fox"},{"Mode":"Insert"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Insert"},{"Text":"fn test() {\n println!();\n \n}\n"},{"Mode":"Insert"},{"Selection":{"start":[2,4],"end":[2,4]}},{"Mode":"Insert"},{"Text":"fn test() {\n\n println!();\n}"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"}] \ No newline at end of file +[{"Text":"\n"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"The quick\n"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"The quick\n\nbrown fox\njumps over"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"The quick\nbrown fox\n\njumps over"},{"Mode":"Insert"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Insert"},{"Text":"The quick\nbrown fox\njumps over\n"},{"Mode":"Insert"},{"Selection":{"start":[3,0],"end":[3,0]}},{"Mode":"Insert"},{"Text":"The quick\n\n\nbrown fox"},{"Mode":"Insert"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Insert"}] \ No newline at end of file diff --git a/crates/vim/test_data/test_percent.json b/crates/vim/test_data/test_percent.json new file mode 100644 index 0000000000..9dc0fc655b --- /dev/null +++ b/crates/vim/test_data/test_percent.json @@ -0,0 +1 @@ +[{"Text":"console.log(var);"},{"Mode":"Normal"},{"Selection":{"start":[0,15],"end":[0,15]}},{"Mode":"Normal"},{"Text":"console.log(var);"},{"Mode":"Normal"},{"Selection":{"start":[0,15],"end":[0,15]}},{"Mode":"Normal"},{"Text":"console.log(var);"},{"Mode":"Normal"},{"Selection":{"start":[0,11],"end":[0,11]}},{"Mode":"Normal"},{"Text":"console.log(var);"},{"Mode":"Normal"},{"Selection":{"start":[0,11],"end":[0,11]}},{"Mode":"Normal"},{"Text":"console.log(var);"},{"Mode":"Normal"},{"Selection":{"start":[0,11],"end":[0,11]}},{"Mode":"Normal"},{"Text":"console.log(var);"},{"Mode":"Normal"},{"Selection":{"start":[0,16],"end":[0,16]}},{"Mode":"Normal"},{"Text":"console.log('var', [1, 2, 3]);"},{"Mode":"Normal"},{"Selection":{"start":[0,28],"end":[0,28]}},{"Mode":"Normal"},{"Text":"console.log('var', [1, 2, 3]);"},{"Mode":"Normal"},{"Selection":{"start":[0,28],"end":[0,28]}},{"Mode":"Normal"},{"Text":"console.log('var', [1, 2, 3]);"},{"Mode":"Normal"},{"Selection":{"start":[0,27],"end":[0,27]}},{"Mode":"Normal"},{"Text":"console.log('var', [1, 2, 3]);"},{"Mode":"Normal"},{"Selection":{"start":[0,27],"end":[0,27]}},{"Mode":"Normal"},{"Text":"console.log('var', [1, 2, 3]);"},{"Mode":"Normal"},{"Selection":{"start":[0,19],"end":[0,19]}},{"Mode":"Normal"},{"Text":"console.log('var', [1, 2, 3]);"},{"Mode":"Normal"},{"Selection":{"start":[0,19],"end":[0,19]}},{"Mode":"Normal"},{"Text":"console.log('var', [1, 2, 3]);"},{"Mode":"Normal"},{"Selection":{"start":[0,19],"end":[0,19]}},{"Mode":"Normal"},{"Text":"console.log('var', [1, 2, 3]);"},{"Mode":"Normal"},{"Selection":{"start":[0,11],"end":[0,11]}},{"Mode":"Normal"},{"Text":"console.log('var', [1, 2, 3]);"},{"Mode":"Normal"},{"Selection":{"start":[0,29],"end":[0,29]}},{"Mode":"Normal"},{"Text":"let result = curried_fun()();"},{"Mode":"Normal"},{"Selection":{"start":[0,25],"end":[0,25]}},{"Mode":"Normal"},{"Text":"let result = curried_fun()();"},{"Mode":"Normal"},{"Selection":{"start":[0,24],"end":[0,24]}},{"Mode":"Normal"},{"Text":"let result = curried_fun()();"},{"Mode":"Normal"},{"Selection":{"start":[0,27],"end":[0,27]}},{"Mode":"Normal"},{"Text":"let result = curried_fun()();"},{"Mode":"Normal"},{"Selection":{"start":[0,26],"end":[0,26]}},{"Mode":"Normal"},{"Text":"let result = curried_fun()();"},{"Mode":"Normal"},{"Selection":{"start":[0,28],"end":[0,28]}},{"Mode":"Normal"}] \ No newline at end of file diff --git a/crates/workspace/Cargo.toml b/crates/workspace/Cargo.toml index 60680f82a2..fc069fe6c8 100644 --- a/crates/workspace/Cargo.toml +++ b/crates/workspace/Cargo.toml @@ -46,6 +46,7 @@ serde = { version = "1.0", features = ["derive", "rc"] } serde_json = { version = "1.0", features = ["preserve_order"] } smallvec = { version = "1.6", features = ["union"] } indoc = "1.0.4" +uuid = { version = "1.1.2", features = ["v4"] } [dev-dependencies] call = { path = "../call", features = ["test-support"] } diff --git a/crates/workspace/src/dock.rs b/crates/workspace/src/dock.rs index 747541f87d..057658c3b5 100644 --- a/crates/workspace/src/dock.rs +++ b/crates/workspace/src/dock.rs @@ -1,19 +1,20 @@ +mod toggle_dock_button; + +use serde::Deserialize; + use collections::HashMap; use gpui::{ actions, - elements::{ChildView, Container, Empty, MouseEventHandler, ParentElement, Side, Stack, Svg}, + elements::{ChildView, Container, Empty, MouseEventHandler, ParentElement, Side, Stack}, geometry::vector::Vector2F, - impl_internal_actions, Border, CursorStyle, Element, ElementBox, Entity, MouseButton, - MutableAppContext, RenderContext, SizeConstraint, View, ViewContext, ViewHandle, - WeakViewHandle, + impl_internal_actions, Border, CursorStyle, Element, ElementBox, MouseButton, + MutableAppContext, RenderContext, SizeConstraint, ViewContext, ViewHandle, }; -use serde::Deserialize; use settings::{DockAnchor, Settings}; use theme::Theme; -use crate::{ - handle_dropped_item, sidebar::SidebarSide, ItemHandle, Pane, StatusItemView, Workspace, -}; +use crate::{sidebar::SidebarSide, ItemHandle, Pane, Workspace}; +pub use toggle_dock_button::ToggleDockButton; #[derive(PartialEq, Clone, Deserialize)] pub struct MoveDock(pub DockAnchor); @@ -29,7 +30,8 @@ actions!( AnchorDockRight, AnchorDockBottom, ExpandDock, - MoveActiveItemToDock, + AddTabToDock, + RemoveTabFromDock, ] ); impl_internal_actions!(dock, [MoveDock, AddDefaultItemToDock]); @@ -54,7 +56,8 @@ pub fn init(cx: &mut MutableAppContext) { }, ); cx.add_action( - |workspace: &mut Workspace, _: &MoveActiveItemToDock, cx: &mut ViewContext| { + |workspace: &mut Workspace, _: &AddTabToDock, cx: &mut ViewContext| { + eprintln!("Add tab to dock"); if let Some(active_item) = workspace.active_item(cx) { let item_id = active_item.id(); @@ -66,6 +69,42 @@ pub fn init(cx: &mut MutableAppContext) { let destination_index = to.read(cx).items_len() + 1; + Pane::move_item( + workspace, + from.clone(), + to.clone(), + item_id, + destination_index, + cx, + ); + } + }, + ); + cx.add_action( + |workspace: &mut Workspace, _: &RemoveTabFromDock, cx: &mut ViewContext| { + eprintln!("Removing tab from dock"); + if let Some(active_item) = workspace.active_item(cx) { + let item_id = active_item.id(); + + let from = workspace.dock_pane(); + let to = workspace + .last_active_center_pane + .as_ref() + .and_then(|pane| pane.upgrade(cx)) + .unwrap_or_else(|| { + workspace + .panes + .first() + .expect("There must be a pane") + .clone() + }); + + if from.id() == to.id() { + return; + } + + let destination_index = to.read(cx).items_len() + 1; + Pane::move_item( workspace, from.clone(), @@ -376,108 +415,6 @@ impl Dock { } } -pub struct ToggleDockButton { - workspace: WeakViewHandle, -} - -impl ToggleDockButton { - pub fn new(workspace: ViewHandle, cx: &mut ViewContext) -> Self { - // When dock moves, redraw so that the icon and toggle status matches. - cx.subscribe(&workspace, |_, _, _, cx| cx.notify()).detach(); - - Self { - workspace: workspace.downgrade(), - } - } -} - -impl Entity for ToggleDockButton { - type Event = (); -} - -impl View for ToggleDockButton { - fn ui_name() -> &'static str { - "Dock Toggle" - } - - fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox { - let workspace = self.workspace.upgrade(cx); - - if workspace.is_none() { - return Empty::new().boxed(); - } - - let workspace = workspace.unwrap(); - let dock_position = workspace.read(cx).dock.position; - - let theme = cx.global::().theme.clone(); - - let button = MouseEventHandler::::new(0, cx, { - let theme = theme.clone(); - move |state, _| { - let style = theme - .workspace - .status_bar - .sidebar_buttons - .item - .style_for(state, dock_position.is_visible()); - - Svg::new(icon_for_dock_anchor(dock_position.anchor())) - .with_color(style.icon_color) - .constrained() - .with_width(style.icon_size) - .with_height(style.icon_size) - .contained() - .with_style(style.container) - .boxed() - } - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_up(MouseButton::Left, move |event, cx| { - let dock_pane = workspace.read(cx.app).dock_pane(); - let drop_index = dock_pane.read(cx.app).items_len() + 1; - handle_dropped_item(event, &dock_pane.downgrade(), drop_index, false, None, cx); - }); - - if dock_position.is_visible() { - button - .on_click(MouseButton::Left, |_, cx| { - cx.dispatch_action(HideDock); - }) - .with_tooltip::( - 0, - "Hide Dock".into(), - Some(Box::new(HideDock)), - theme.tooltip.clone(), - cx, - ) - } else { - button - .on_click(MouseButton::Left, |_, cx| { - cx.dispatch_action(FocusDock); - }) - .with_tooltip::( - 0, - "Focus Dock".into(), - Some(Box::new(FocusDock)), - theme.tooltip.clone(), - cx, - ) - } - .boxed() - } -} - -impl StatusItemView for ToggleDockButton { - fn set_active_pane_item( - &mut self, - _active_pane_item: Option<&dyn crate::ItemHandle>, - _cx: &mut ViewContext, - ) { - //Not applicable - } -} - #[cfg(test)] mod tests { use std::{ @@ -485,7 +422,7 @@ mod tests { path::PathBuf, }; - use gpui::{AppContext, TestAppContext, UpdateView, ViewContext}; + use gpui::{AppContext, TestAppContext, UpdateView, View, ViewContext}; use project::{FakeFs, Project}; use settings::Settings; @@ -534,6 +471,8 @@ mod tests { }], }, left_sidebar_open: false, + bounds: Default::default(), + display: Default::default(), }; let fs = FakeFs::new(cx.background()); diff --git a/crates/workspace/src/dock/toggle_dock_button.rs b/crates/workspace/src/dock/toggle_dock_button.rs new file mode 100644 index 0000000000..cafbea7db3 --- /dev/null +++ b/crates/workspace/src/dock/toggle_dock_button.rs @@ -0,0 +1,112 @@ +use gpui::{ + elements::{Empty, MouseEventHandler, Svg}, + CursorStyle, Element, ElementBox, Entity, MouseButton, View, ViewContext, ViewHandle, + WeakViewHandle, +}; +use settings::Settings; + +use crate::{handle_dropped_item, StatusItemView, Workspace}; + +use super::{icon_for_dock_anchor, FocusDock, HideDock}; + +pub struct ToggleDockButton { + workspace: WeakViewHandle, +} + +impl ToggleDockButton { + pub fn new(workspace: ViewHandle, cx: &mut ViewContext) -> Self { + // When dock moves, redraw so that the icon and toggle status matches. + cx.subscribe(&workspace, |_, _, _, cx| cx.notify()).detach(); + + Self { + workspace: workspace.downgrade(), + } + } +} + +impl Entity for ToggleDockButton { + type Event = (); +} + +impl View for ToggleDockButton { + fn ui_name() -> &'static str { + "Dock Toggle" + } + + fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox { + let workspace = self.workspace.upgrade(cx); + + if workspace.is_none() { + return Empty::new().boxed(); + } + + let workspace = workspace.unwrap(); + let dock_position = workspace.read(cx).dock.position; + + let theme = cx.global::().theme.clone(); + + let button = MouseEventHandler::::new(0, cx, { + let theme = theme.clone(); + move |state, _| { + let style = theme + .workspace + .status_bar + .sidebar_buttons + .item + .style_for(state, dock_position.is_visible()); + + Svg::new(icon_for_dock_anchor(dock_position.anchor())) + .with_color(style.icon_color) + .constrained() + .with_width(style.icon_size) + .with_height(style.icon_size) + .contained() + .with_style(style.container) + .boxed() + } + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_up(MouseButton::Left, move |event, cx| { + let dock_pane = workspace.read(cx.app).dock_pane(); + let drop_index = dock_pane.read(cx.app).items_len() + 1; + handle_dropped_item(event, &dock_pane.downgrade(), drop_index, false, None, cx); + }); + + if dock_position.is_visible() { + button + .on_click(MouseButton::Left, |_, cx| { + cx.dispatch_action(HideDock); + }) + .with_tooltip::( + 0, + "Hide Dock".into(), + Some(Box::new(HideDock)), + theme.tooltip.clone(), + cx, + ) + } else { + button + .on_click(MouseButton::Left, |_, cx| { + cx.dispatch_action(FocusDock); + }) + .with_tooltip::( + 0, + "Focus Dock".into(), + Some(Box::new(FocusDock)), + theme.tooltip.clone(), + cx, + ) + } + .boxed() + } +} + +impl StatusItemView for ToggleDockButton { + fn set_active_pane_item( + &mut self, + _active_pane_item: Option<&dyn crate::ItemHandle>, + _cx: &mut ViewContext, + ) { + //Not applicable + } +} diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index b1888bb243..0e28976151 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -88,7 +88,7 @@ pub trait Item: View { ) -> Task> { Task::ready(Ok(())) } - fn to_item_events(event: &Self::Event) -> Vec; + fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]>; fn should_close_item_on_event(_: &Self::Event) -> bool { false } @@ -723,6 +723,7 @@ pub(crate) mod test { RenderContext, Task, View, ViewContext, ViewHandle, WeakViewHandle, }; use project::{Project, ProjectEntryId, ProjectPath, WorktreeId}; + use smallvec::SmallVec; use std::{any::Any, borrow::Cow, cell::Cell, path::Path}; pub struct TestProjectItem { @@ -985,8 +986,8 @@ pub(crate) mod test { Task::ready(Ok(())) } - fn to_item_events(_: &Self::Event) -> Vec { - vec![ItemEvent::UpdateTab, ItemEvent::Edit] + fn to_item_events(_: &Self::Event) -> SmallVec<[ItemEvent; 2]> { + [ItemEvent::UpdateTab, ItemEvent::Edit].into() } fn serialized_item_kind() -> Option<&'static str> { diff --git a/crates/workspace/src/notifications.rs b/crates/workspace/src/notifications.rs index 43feede190..141a345382 100644 --- a/crates/workspace/src/notifications.rs +++ b/crates/workspace/src/notifications.rs @@ -121,7 +121,6 @@ impl Workspace { } pub mod simple_message_notification { - use std::process::Command; use gpui::{ actions, @@ -147,14 +146,8 @@ pub mod simple_message_notification { pub fn init(cx: &mut MutableAppContext) { cx.add_action(MessageNotification::dismiss); cx.add_action( - |_workspace: &mut Workspace, open_action: &OsOpen, _cx: &mut ViewContext| { - #[cfg(target_os = "macos")] - { - let mut command = Command::new("open"); - command.arg(open_action.0.clone()); - - command.spawn().ok(); - } + |_workspace: &mut Workspace, open_action: &OsOpen, cx: &mut ViewContext| { + cx.platform().open_url(open_action.0.as_str()); }, ) } @@ -174,7 +167,7 @@ pub mod simple_message_notification { } impl MessageNotification { - pub fn new_messsage>(message: S) -> MessageNotification { + pub fn new_message>(message: S) -> MessageNotification { Self { message: message.as_ref().to_string(), click_action: None, @@ -320,7 +313,7 @@ where Err(err) => { workspace.show_notification(0, cx, |cx| { cx.add_view(|_cx| { - simple_message_notification::MessageNotification::new_messsage(format!( + simple_message_notification::MessageNotification::new_message(format!( "Error: {:?}", err, )) diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 7e56b864bf..8e51a54178 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -1420,23 +1420,40 @@ impl View for Pane { Stack::new() .with_child( MouseEventHandler::::new(0, cx, |_, cx| { + let active_item_index = self.active_item_index; + if let Some(active_item) = self.active_item() { Flex::column() .with_child({ + let theme = cx.global::().theme.clone(); + + let mut stack = Stack::new(); + + enum TabBarEventHandler {} + stack.add_child( + MouseEventHandler::::new(0, cx, |_, _| { + Empty::new() + .contained() + .with_style(theme.workspace.tab_bar.container) + .boxed() + }) + .on_click(MouseButton::Left, move |_, cx| { + cx.dispatch_action(ActivateItem(active_item_index)); + }) + .boxed(), + ); + let mut tab_row = Flex::row() .with_child(self.render_tabs(cx).flex(1., true).named("tabs")); - // Render pane buttons - let theme = cx.global::().theme.clone(); if self.is_active { tab_row.add_child(self.render_tab_bar_buttons(&theme, cx)) } - tab_row + stack.add_child(tab_row.boxed()); + stack .constrained() .with_height(theme.workspace.tab_bar.height) - .contained() - .with_style(theme.workspace.tab_bar.container) .flex(1., false) .named("tab bar") }) @@ -1527,7 +1544,7 @@ impl View for Pane { } cx.focus(active_item); - } else { + } else if focused != self.tab_bar_context_menu { self.last_focused_view_by_item .insert(active_item.id(), focused.downgrade()); } diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs index 03a866f2f6..ddbea4c9f9 100644 --- a/crates/workspace/src/persistence.rs +++ b/crates/workspace/src/persistence.rs @@ -6,9 +6,10 @@ use std::path::Path; use anyhow::{anyhow, bail, Context, Result}; use db::{define_connection, query, sqlez::connection::Connection, sqlez_macros::sql}; -use gpui::Axis; +use gpui::{Axis, WindowBounds}; use util::{unzip_option, ResultExt}; +use uuid::Uuid; use crate::dock::DockPosition; use crate::WorkspaceId; @@ -19,64 +20,118 @@ use model::{ }; define_connection! { + // Current schema shape using pseudo-rust syntax: + // + // workspaces( + // workspace_id: usize, // Primary key for workspaces + // workspace_location: Bincode>, + // dock_visible: bool, + // dock_anchor: DockAnchor, // 'Bottom' / 'Right' / 'Expanded' + // dock_pane: Option, // PaneId + // left_sidebar_open: boolean, + // timestamp: String, // UTC YYYY-MM-DD HH:MM:SS + // window_state: String, // WindowBounds Discriminant + // window_x: Option, // WindowBounds::Fixed RectF x + // window_y: Option, // WindowBounds::Fixed RectF y + // window_width: Option, // WindowBounds::Fixed RectF width + // window_height: Option, // WindowBounds::Fixed RectF height + // display: Option, // Display id + // ) + // + // pane_groups( + // group_id: usize, // Primary key for pane_groups + // workspace_id: usize, // References workspaces table + // parent_group_id: Option, // None indicates that this is the root node + // position: Optiopn, // None indicates that this is the root node + // axis: Option, // 'Vertical', 'Horizontal' + // ) + // + // panes( + // pane_id: usize, // Primary key for panes + // workspace_id: usize, // References workspaces table + // active: bool, + // ) + // + // center_panes( + // pane_id: usize, // Primary key for center_panes + // parent_group_id: Option, // References pane_groups. If none, this is the root + // position: Option, // None indicates this is the root + // ) + // + // CREATE TABLE items( + // item_id: usize, // This is the item's view id, so this is not unique + // workspace_id: usize, // References workspaces table + // pane_id: usize, // References panes table + // kind: String, // Indicates which view this connects to. This is the key in the item_deserializers global + // position: usize, // Position of the item in the parent pane. This is equivalent to panes' position column + // active: bool, // Indicates if this item is the active one in the pane + // ) pub static ref DB: WorkspaceDb<()> = - &[sql!( - CREATE TABLE workspaces( - workspace_id INTEGER PRIMARY KEY, - workspace_location BLOB UNIQUE, - dock_visible INTEGER, // Boolean - dock_anchor TEXT, // Enum: 'Bottom' / 'Right' / 'Expanded' - dock_pane INTEGER, // NULL indicates that we don't have a dock pane yet - left_sidebar_open INTEGER, //Boolean - timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL, - FOREIGN KEY(dock_pane) REFERENCES panes(pane_id) - ) STRICT; + &[sql!( + CREATE TABLE workspaces( + workspace_id INTEGER PRIMARY KEY, + workspace_location BLOB UNIQUE, + dock_visible INTEGER, // Boolean + dock_anchor TEXT, // Enum: 'Bottom' / 'Right' / 'Expanded' + dock_pane INTEGER, // NULL indicates that we don't have a dock pane yet + left_sidebar_open INTEGER, //Boolean + timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL, + FOREIGN KEY(dock_pane) REFERENCES panes(pane_id) + ) STRICT; - CREATE TABLE pane_groups( - group_id INTEGER PRIMARY KEY, - workspace_id INTEGER NOT NULL, - parent_group_id INTEGER, // NULL indicates that this is a root node - position INTEGER, // NULL indicates that this is a root node - axis TEXT NOT NULL, // Enum: 'Vertical' / 'Horizontal' - FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) - ON DELETE CASCADE - ON UPDATE CASCADE, - FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE - ) STRICT; + CREATE TABLE pane_groups( + group_id INTEGER PRIMARY KEY, + workspace_id INTEGER NOT NULL, + parent_group_id INTEGER, // NULL indicates that this is a root node + position INTEGER, // NULL indicates that this is a root node + axis TEXT NOT NULL, // Enum: 'Vertical' / 'Horizontal' + FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) + ON DELETE CASCADE + ON UPDATE CASCADE, + FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE + ) STRICT; - CREATE TABLE panes( - pane_id INTEGER PRIMARY KEY, - workspace_id INTEGER NOT NULL, - active INTEGER NOT NULL, // Boolean - FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) - ON DELETE CASCADE - ON UPDATE CASCADE - ) STRICT; + CREATE TABLE panes( + pane_id INTEGER PRIMARY KEY, + workspace_id INTEGER NOT NULL, + active INTEGER NOT NULL, // Boolean + FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) + ON DELETE CASCADE + ON UPDATE CASCADE + ) STRICT; - CREATE TABLE center_panes( - pane_id INTEGER PRIMARY KEY, - parent_group_id INTEGER, // NULL means that this is a root pane - position INTEGER, // NULL means that this is a root pane - FOREIGN KEY(pane_id) REFERENCES panes(pane_id) - ON DELETE CASCADE, - FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE - ) STRICT; + CREATE TABLE center_panes( + pane_id INTEGER PRIMARY KEY, + parent_group_id INTEGER, // NULL means that this is a root pane + position INTEGER, // NULL means that this is a root pane + FOREIGN KEY(pane_id) REFERENCES panes(pane_id) + ON DELETE CASCADE, + FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE + ) STRICT; - CREATE TABLE items( - item_id INTEGER NOT NULL, // This is the item's view id, so this is not unique - workspace_id INTEGER NOT NULL, - pane_id INTEGER NOT NULL, - kind TEXT NOT NULL, - position INTEGER NOT NULL, - active INTEGER NOT NULL, - FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) - ON DELETE CASCADE - ON UPDATE CASCADE, - FOREIGN KEY(pane_id) REFERENCES panes(pane_id) - ON DELETE CASCADE, - PRIMARY KEY(item_id, workspace_id) - ) STRICT; - )]; + CREATE TABLE items( + item_id INTEGER NOT NULL, // This is the item's view id, so this is not unique + workspace_id INTEGER NOT NULL, + pane_id INTEGER NOT NULL, + kind TEXT NOT NULL, + position INTEGER NOT NULL, + active INTEGER NOT NULL, + FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) + ON DELETE CASCADE + ON UPDATE CASCADE, + FOREIGN KEY(pane_id) REFERENCES panes(pane_id) + ON DELETE CASCADE, + PRIMARY KEY(item_id, workspace_id) + ) STRICT; + ), + sql!( + ALTER TABLE workspaces ADD COLUMN window_state TEXT; + ALTER TABLE workspaces ADD COLUMN window_x REAL; + ALTER TABLE workspaces ADD COLUMN window_y REAL; + ALTER TABLE workspaces ADD COLUMN window_width REAL; + ALTER TABLE workspaces ADD COLUMN window_height REAL; + ALTER TABLE workspaces ADD COLUMN display BLOB; + )]; } impl WorkspaceDb { @@ -91,14 +146,27 @@ impl WorkspaceDb { // Note that we re-assign the workspace_id here in case it's empty // and we've grabbed the most recent workspace - let (workspace_id, workspace_location, left_sidebar_open, dock_position): ( + let (workspace_id, workspace_location, left_sidebar_open, dock_position, bounds, display): ( WorkspaceId, WorkspaceLocation, bool, DockPosition, - ) = - self.select_row_bound(sql!{ - SELECT workspace_id, workspace_location, left_sidebar_open, dock_visible, dock_anchor + Option, + Option, + ) = self + .select_row_bound(sql! { + SELECT + workspace_id, + workspace_location, + left_sidebar_open, + dock_visible, + dock_anchor, + window_state, + window_x, + window_y, + window_width, + window_height, + display FROM workspaces WHERE workspace_location = ? }) @@ -120,6 +188,8 @@ impl WorkspaceDb { .log_err()?, dock_position, left_sidebar_open, + bounds, + display, }) } @@ -142,22 +212,22 @@ impl WorkspaceDb { // Upsert conn.exec_bound(sql!( - INSERT INTO workspaces( - workspace_id, - workspace_location, - left_sidebar_open, - dock_visible, - dock_anchor, - timestamp - ) - VALUES (?1, ?2, ?3, ?4, ?5, CURRENT_TIMESTAMP) - ON CONFLICT DO - UPDATE SET - workspace_location = ?2, - left_sidebar_open = ?3, - dock_visible = ?4, - dock_anchor = ?5, - timestamp = CURRENT_TIMESTAMP + INSERT INTO workspaces( + workspace_id, + workspace_location, + left_sidebar_open, + dock_visible, + dock_anchor, + timestamp + ) + VALUES (?1, ?2, ?3, ?4, ?5, CURRENT_TIMESTAMP) + ON CONFLICT DO + UPDATE SET + workspace_location = ?2, + left_sidebar_open = ?3, + dock_visible = ?4, + dock_anchor = ?5, + timestamp = CURRENT_TIMESTAMP ))?(( workspace.id, &workspace.location, @@ -177,7 +247,7 @@ impl WorkspaceDb { conn.exec_bound(sql!( UPDATE workspaces SET dock_pane = ? - WHERE workspace_id = ? + WHERE workspace_id = ? ))?((dock_id, workspace.id)) .context("Finishing initialization with dock pane")?; @@ -261,27 +331,27 @@ impl WorkspaceDb { self.select_bound::(sql!( SELECT group_id, axis, pane_id, active FROM (SELECT - group_id, - axis, - NULL as pane_id, - NULL as active, - position, - parent_group_id, - workspace_id - FROM pane_groups - UNION - SELECT - NULL, - NULL, - center_panes.pane_id, - panes.active as active, - position, - parent_group_id, - panes.workspace_id as workspace_id - FROM center_panes - JOIN panes ON center_panes.pane_id = panes.pane_id) - WHERE parent_group_id IS ? AND workspace_id = ? - ORDER BY position + group_id, + axis, + NULL as pane_id, + NULL as active, + position, + parent_group_id, + workspace_id + FROM pane_groups + UNION + SELECT + NULL, + NULL, + center_panes.pane_id, + panes.active as active, + position, + parent_group_id, + panes.workspace_id as workspace_id + FROM center_panes + JOIN panes ON center_panes.pane_id = panes.pane_id) + WHERE parent_group_id IS ? AND workspace_id = ? + ORDER BY position ))?((group_id, workspace_id))? .into_iter() .map(|(group_id, axis, pane_id, active)| { @@ -319,9 +389,9 @@ impl WorkspaceDb { let (parent_id, position) = unzip_option(parent); let group_id = conn.select_row_bound::<_, i64>(sql!( - INSERT INTO pane_groups(workspace_id, parent_group_id, position, axis) - VALUES (?, ?, ?, ?) - RETURNING group_id + INSERT INTO pane_groups(workspace_id, parent_group_id, position, axis) + VALUES (?, ?, ?, ?) + RETURNING group_id ))?((workspace_id, parent_id, position, *axis))? .ok_or_else(|| anyhow!("Couldn't retrieve group_id from inserted pane_group"))?; @@ -383,7 +453,7 @@ impl WorkspaceDb { Ok(self.select_bound(sql!( SELECT kind, item_id, active FROM items WHERE pane_id = ? - ORDER BY position + ORDER BY position ))?(pane_id)?) } @@ -410,6 +480,19 @@ impl WorkspaceDb { WHERE workspace_id = ? } } + + query! { + pub async fn set_window_bounds(workspace_id: WorkspaceId, bounds: WindowBounds, display: Uuid) -> Result<()> { + UPDATE workspaces + SET window_state = ?2, + window_x = ?3, + window_y = ?4, + window_width = ?5, + window_height = ?6, + display = ?7 + WHERE workspace_id = ?1 + } + } } #[cfg(test)] @@ -436,7 +519,7 @@ mod tests { text TEXT, workspace_id INTEGER, FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) - ON DELETE CASCADE + ON DELETE CASCADE ) STRICT; )], ) @@ -485,7 +568,7 @@ mod tests { workspace_id INTEGER, FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) - ON DELETE CASCADE + ON DELETE CASCADE ) STRICT;)], ) }) @@ -499,6 +582,8 @@ mod tests { center_group: Default::default(), dock_pane: Default::default(), left_sidebar_open: true, + bounds: Default::default(), + display: Default::default(), }; let mut workspace_2 = SerializedWorkspace { @@ -508,6 +593,8 @@ mod tests { center_group: Default::default(), dock_pane: Default::default(), left_sidebar_open: false, + bounds: Default::default(), + display: Default::default(), }; db.save_workspace(workspace_1.clone()).await; @@ -614,6 +701,8 @@ mod tests { center_group, dock_pane, left_sidebar_open: true, + bounds: Default::default(), + display: Default::default(), }; db.save_workspace(workspace.clone()).await; @@ -642,6 +731,8 @@ mod tests { center_group: Default::default(), dock_pane: Default::default(), left_sidebar_open: true, + bounds: Default::default(), + display: Default::default(), }; let mut workspace_2 = SerializedWorkspace { @@ -651,6 +742,8 @@ mod tests { center_group: Default::default(), dock_pane: Default::default(), left_sidebar_open: false, + bounds: Default::default(), + display: Default::default(), }; db.save_workspace(workspace_1.clone()).await; @@ -687,6 +780,8 @@ mod tests { center_group: Default::default(), dock_pane: Default::default(), left_sidebar_open: false, + bounds: Default::default(), + display: Default::default(), }; db.save_workspace(workspace_3.clone()).await; @@ -722,6 +817,8 @@ mod tests { center_group: center_group.clone(), dock_pane, left_sidebar_open: true, + bounds: Default::default(), + display: Default::default(), } } diff --git a/crates/workspace/src/persistence/model.rs b/crates/workspace/src/persistence/model.rs index b264114fb6..507582b216 100644 --- a/crates/workspace/src/persistence/model.rs +++ b/crates/workspace/src/persistence/model.rs @@ -6,15 +6,16 @@ use std::{ use anyhow::{Context, Result}; use async_recursion::async_recursion; -use gpui::{AsyncAppContext, Axis, ModelHandle, Task, ViewHandle}; +use gpui::{AsyncAppContext, Axis, ModelHandle, Task, ViewHandle, WindowBounds}; use db::sqlez::{ - bindable::{Bind, Column}, + bindable::{Bind, Column, StaticColumnCount}, statement::Statement, }; use project::Project; use settings::DockAnchor; use util::ResultExt; +use uuid::Uuid; use crate::{ dock::DockPosition, ItemDeserializers, Member, Pane, PaneAxis, Workspace, WorkspaceId, @@ -40,6 +41,7 @@ impl, T: IntoIterator> From for WorkspaceLocation { } } +impl StaticColumnCount for WorkspaceLocation {} impl Bind for &WorkspaceLocation { fn bind(&self, statement: &Statement, start_index: i32) -> Result { bincode::serialize(&self.0) @@ -58,7 +60,7 @@ impl Column for WorkspaceLocation { } } -#[derive(Debug, PartialEq, Eq, Clone)] +#[derive(Debug, PartialEq, Clone)] pub struct SerializedWorkspace { pub id: WorkspaceId, pub location: WorkspaceLocation, @@ -66,6 +68,8 @@ pub struct SerializedWorkspace { pub center_group: SerializedPaneGroup, pub dock_pane: SerializedPane, pub left_sidebar_open: bool, + pub bounds: Option, + pub display: Option, } #[derive(Debug, PartialEq, Eq, Clone)] @@ -237,6 +241,11 @@ impl Default for SerializedItem { } } +impl StaticColumnCount for SerializedItem { + fn column_count() -> usize { + 3 + } +} impl Bind for &SerializedItem { fn bind(&self, statement: &Statement, start_index: i32) -> Result { let next_index = statement.bind(self.kind.clone(), start_index)?; @@ -261,6 +270,11 @@ impl Column for SerializedItem { } } +impl StaticColumnCount for DockPosition { + fn column_count() -> usize { + 2 + } +} impl Bind for DockPosition { fn bind(&self, statement: &Statement, start_index: i32) -> Result { let next_index = statement.bind(self.is_visible(), start_index)?; diff --git a/crates/workspace/src/shared_screen.rs b/crates/workspace/src/shared_screen.rs index b76535f6ed..b3e107c81b 100644 --- a/crates/workspace/src/shared_screen.rs +++ b/crates/workspace/src/shared_screen.rs @@ -13,6 +13,7 @@ use gpui::{ }; use project::Project; use settings::Settings; +use smallvec::SmallVec; use std::{ path::PathBuf, sync::{Arc, Weak}, @@ -177,9 +178,9 @@ impl Item for SharedScreen { Task::ready(Err(anyhow!("Item::reload called on SharedScreen"))) } - fn to_item_events(event: &Self::Event) -> Vec { + fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]> { match event { - Event::Close => vec![ItemEvent::CloseItem], + Event::Close => smallvec::smallvec!(ItemEvent::CloseItem), } } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index ec7ba8fae0..95969de6b0 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -14,6 +14,8 @@ pub mod sidebar; mod status_bar; mod toolbar; +pub use smallvec; + use anyhow::{anyhow, Result}; use call::ActiveCall; use client::{ @@ -37,8 +39,8 @@ use gpui::{ keymap_matcher::KeymapContext, platform::{CursorStyle, WindowOptions}, AnyModelHandle, AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, - MouseButton, MutableAppContext, PathPromptOptions, PromptLevel, RenderContext, SizeConstraint, - Task, View, ViewContext, ViewHandle, WeakViewHandle, + MouseButton, MutableAppContext, PathPromptOptions, Platform, PromptLevel, RenderContext, + SizeConstraint, Task, View, ViewContext, ViewHandle, WeakViewHandle, WindowBounds, }; use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ProjectItem}; use language::LanguageRegistry; @@ -96,11 +98,10 @@ actions!( ActivateNextPane, FollowNextCollaborator, ToggleLeftSidebar, - ToggleRightSidebar, NewTerminal, NewSearch, Feedback, - ShowNotif, + Restart ] ); @@ -199,6 +200,7 @@ pub fn init(app_state: Arc, cx: &mut MutableAppContext) { cx.add_async_action(Workspace::toggle_follow); cx.add_async_action(Workspace::follow_next_collaborator); cx.add_async_action(Workspace::close); + cx.add_global_action(Workspace::close_global); cx.add_async_action(Workspace::save_all); cx.add_action(Workspace::open_shared_screen); cx.add_action(Workspace::add_folder_to_project); @@ -230,9 +232,6 @@ pub fn init(app_state: Arc, cx: &mut MutableAppContext) { cx.add_action(|workspace: &mut Workspace, _: &ToggleLeftSidebar, cx| { workspace.toggle_sidebar(SidebarSide::Left, cx); }); - cx.add_action(|workspace: &mut Workspace, _: &ToggleRightSidebar, cx| { - workspace.toggle_sidebar(SidebarSide::Right, cx); - }); cx.add_action(Workspace::activate_pane_at_index); cx.add_action(Workspace::split_pane_with_item); @@ -340,7 +339,8 @@ pub struct AppState { pub client: Arc, pub user_store: ModelHandle, pub fs: Arc, - pub build_window_options: fn() -> WindowOptions<'static>, + pub build_window_options: + fn(Option, Option, &dyn Platform) -> WindowOptions<'static>, pub initialize_workspace: fn(&mut Workspace, &Arc, &mut ViewContext), pub dock_default_item_factory: DockDefaultItemFactory, } @@ -348,9 +348,6 @@ pub struct AppState { impl AppState { #[cfg(any(test, feature = "test-support"))] pub fn test(cx: &mut MutableAppContext) -> Arc { - use fs::HomeDir; - - cx.set_global(HomeDir(Path::new("/tmp/").to_path_buf())); let settings = Settings::test(cx); cx.set_global(settings); @@ -367,7 +364,7 @@ impl AppState { languages, user_store, initialize_workspace: |_, _, _| {}, - build_window_options: Default::default, + build_window_options: |_, _, _| Default::default(), dock_default_item_factory: |_, _| unimplemented!(), }) } @@ -495,19 +492,24 @@ impl Workspace { cx.subscribe(&project, move |this, _, event, cx| { match event { project::Event::RemoteIdChanged(remote_id) => { + this.update_window_title(cx); this.project_remote_id_changed(*remote_id, cx); } + project::Event::CollaboratorLeft(peer_id) => { this.collaborator_left(*peer_id, cx); } + project::Event::WorktreeRemoved(_) | project::Event::WorktreeAdded => { this.update_window_title(cx); this.serialize_workspace(cx); } + project::Event::DisconnectedFromHost => { this.update_window_edited(cx); cx.blur(); } + _ => {} } cx.notify() @@ -682,18 +684,64 @@ impl Workspace { DB.next_id().await.unwrap_or(0) }; + let (bounds, display) = serialized_workspace + .as_ref() + .and_then(|sw| sw.bounds.zip(sw.display)) + .and_then(|(mut bounds, display)| { + // Stored bounds are relative to the containing display. So convert back to global coordinates if that screen still exists + if let WindowBounds::Fixed(mut window_bounds) = bounds { + if let Some(screen) = cx.platform().screen_by_id(display) { + let screen_bounds = screen.bounds(); + window_bounds + .set_origin_x(window_bounds.origin_x() + screen_bounds.origin_x()); + window_bounds + .set_origin_y(window_bounds.origin_y() + screen_bounds.origin_y()); + bounds = WindowBounds::Fixed(window_bounds); + } else { + // Screen no longer exists. Return none here. + return None; + } + } + + Some((bounds, display)) + }) + .unzip(); + // Use the serialized workspace to construct the new window - let (_, workspace) = cx.add_window((app_state.build_window_options)(), |cx| { - let mut workspace = Workspace::new( - serialized_workspace, - workspace_id, - project_handle, - app_state.dock_default_item_factory, - cx, - ); - (app_state.initialize_workspace)(&mut workspace, &app_state, cx); - workspace - }); + let (_, workspace) = cx.add_window( + (app_state.build_window_options)(bounds, display, cx.platform().as_ref()), + |cx| { + let mut workspace = Workspace::new( + serialized_workspace, + workspace_id, + project_handle, + app_state.dock_default_item_factory, + cx, + ); + (app_state.initialize_workspace)(&mut workspace, &app_state, cx); + cx.observe_window_bounds(move |_, mut bounds, display, cx| { + // Transform fixed bounds to be stored in terms of the containing display + if let WindowBounds::Fixed(mut window_bounds) = bounds { + if let Some(screen) = cx.platform().screen_by_id(display) { + let screen_bounds = screen.bounds(); + window_bounds.set_origin_x( + window_bounds.origin_x() - screen_bounds.origin_x(), + ); + window_bounds.set_origin_y( + window_bounds.origin_y() - screen_bounds.origin_y(), + ); + bounds = WindowBounds::Fixed(window_bounds); + } + } + + cx.background() + .spawn(DB.set_window_bounds(workspace_id, bounds, display)) + .detach_and_log_err(cx); + }) + .detach(); + workspace + }, + ); notify_if_database_failed(&workspace, &mut cx); @@ -824,6 +872,15 @@ impl Workspace { } } + pub fn close_global(_: &CloseWindow, cx: &mut MutableAppContext) { + let id = cx.window_ids().find(|&id| cx.window_is_active(id)); + if let Some(id) = id { + //This can only get called when the window's project connection has been lost + //so we don't need to prompt the user for anything and instead just close the window + cx.remove_window(id); + } + } + pub fn close( &mut self, _: &CloseWindow, @@ -852,6 +909,7 @@ impl Workspace { .window_ids() .flat_map(|window_id| cx.root_view::(window_id)) .count(); + cx.spawn(|this, mut cx| async move { if let Some(active_call) = active_call { if !quitting @@ -867,6 +925,7 @@ impl Workspace { ) .next() .await; + if answer == Some(1) { return anyhow::Ok(false); } else { @@ -1271,7 +1330,19 @@ impl Workspace { focus_item: bool, cx: &mut ViewContext, ) -> Task, anyhow::Error>> { - let pane = pane.unwrap_or_else(|| self.active_pane().downgrade()); + let pane = pane.unwrap_or_else(|| { + if !self.dock_active() { + self.active_pane().downgrade() + } else { + self.last_active_center_pane.clone().unwrap_or_else(|| { + self.panes + .first() + .expect("There must be an active pane") + .downgrade() + }) + } + }); + let task = self.load_path(path.into(), cx); cx.spawn(|this, mut cx| async move { let (project_entry_id, build_item) = task.await?; @@ -1578,6 +1649,10 @@ impl Workspace { self.dock.pane() } + fn dock_active(&self) -> bool { + &self.active_pane == self.dock.pane() + } + fn project_remote_id_changed(&mut self, remote_id: Option, cx: &mut ViewContext) { if let Some(remote_id) = remote_id { self.remote_entity_subscription = @@ -1788,8 +1863,9 @@ impl Workspace { } fn update_window_title(&mut self, cx: &mut ViewContext) { - let mut title = String::new(); let project = self.project().read(cx); + let mut title = String::new(); + if let Some(path) = self.active_item(cx).and_then(|item| item.project_path(cx)) { let filename = path .path @@ -1803,20 +1879,30 @@ impl Workspace { .root_name(), )) }); + if let Some(filename) = filename { title.push_str(filename.as_ref()); title.push_str(" — "); } } + for (i, name) in project.worktree_root_names(cx).enumerate() { if i > 0 { title.push_str(", "); } title.push_str(name); } + if title.is_empty() { title = "empty project".to_string(); } + + if project.is_remote() { + title.push_str(" ↙"); + } else if project.is_shared() { + title.push_str(" ↗"); + } + cx.set_window_title(&title); } @@ -2129,7 +2215,7 @@ impl Workspace { let call = self.active_call()?; let room = call.read(cx).room()?.read(cx); let participant = room.remote_participant_for_peer_id(leader_id)?; - let mut items_to_add = Vec::new(); + let mut items_to_activate = Vec::new(); match participant.location { call::ParticipantLocation::SharedProject { project_id } => { if Some(project_id) == self.project.read(cx).remote_id() { @@ -2138,12 +2224,12 @@ impl Workspace { .active_view_id .and_then(|id| state.items_by_leader_view_id.get(&id)) { - items_to_add.push((pane.clone(), item.boxed_clone())); + items_to_activate.push((pane.clone(), item.boxed_clone())); } else { if let Some(shared_screen) = self.shared_screen_for_peer(leader_id, pane, cx) { - items_to_add.push((pane.clone(), Box::new(shared_screen))); + items_to_activate.push((pane.clone(), Box::new(shared_screen))); } } } @@ -2153,20 +2239,26 @@ impl Workspace { call::ParticipantLocation::External => { for (pane, _) in self.follower_states_by_leader.get(&leader_id)? { if let Some(shared_screen) = self.shared_screen_for_peer(leader_id, pane, cx) { - items_to_add.push((pane.clone(), Box::new(shared_screen))); + items_to_activate.push((pane.clone(), Box::new(shared_screen))); } } } } - for (pane, item) in items_to_add { + for (pane, item) in items_to_activate { + let active_item_was_focused = pane + .read(cx) + .active_item() + .map(|active_item| cx.is_child_focused(active_item.to_any())) + .unwrap_or_default(); + if let Some(index) = pane.update(cx, |pane, _| pane.index_for_item(item.as_ref())) { pane.update(cx, |pane, cx| pane.activate_item(index, false, false, cx)); } else { Pane::add_item(self, &pane, item.boxed_clone(), false, false, None, cx); } - if pane == self.active_pane { + if active_item_was_focused { pane.update(cx, |pane, cx| pane.focus_active_item(cx)); } } @@ -2328,6 +2420,8 @@ impl Workspace { dock_pane, center_group, left_sidebar_open: self.left_sidebar.read(cx).is_open(), + bounds: Default::default(), + display: Default::default(), }; cx.background() @@ -2428,7 +2522,7 @@ fn notify_if_database_failed(workspace: &ViewHandle, cx: &mut AsyncAp indoc::indoc! {" Failed to load any database file :( "}, - OsOpen("https://github.com/zed-industries/feedback/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml".to_string()), + OsOpen("https://github.com/zed-industries/community/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml".to_string()), "Click to let us know about this error" ) }) diff --git a/crates/zed/BundleDocumentTypes.plist b/crates/zed/BundleDocumentTypes.plist new file mode 100644 index 0000000000..459169afc8 --- /dev/null +++ b/crates/zed/BundleDocumentTypes.plist @@ -0,0 +1,62 @@ +CFBundleDocumentTypes + + + CFBundleTypeIconFile + Document + CFBundleTypeRole + Editor + LSHandlerRank + Default + LSItemContentTypes + + public.text + public.plain-text + public.utf8-plain-text + + + + CFBundleTypeIconFile + Document + CFBundleTypeName + Zed Text Document + CFBundleTypeRole + Editor + CFBundleTypeOSTypes + + **** + + LSHandlerRank + Default + CFBundleTypeExtensions + + Gemfile + c + c++ + cc + cpp + css + erb + ex + exs + go + h + h++ + hh + hpp + html + js + json + jsx + md + py + rb + rkt + rs + scm + toml + ts + tsx + txt + + + diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index d159271f99..046866cc0c 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -3,7 +3,7 @@ authors = ["Nathan Sobo "] description = "The fast, collaborative code editor." edition = "2021" name = "zed" -version = "0.71.0" +version = "0.75.0" publish = false [lib] @@ -60,6 +60,7 @@ vim = { path = "../vim" } workspace = { path = "../workspace" } anyhow = "1.0.38" async-compression = { version = "0.3", features = ["gzip", "futures-bufread"] } +async-tar = "0.4.2" async-recursion = "0.3" async-trait = "0.1" backtrace = "0.3" @@ -104,12 +105,17 @@ tree-sitter-rust = "0.20.3" tree-sitter-markdown = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "330ecab87a3e3a7211ac69bbadc19eabecdb1cca" } tree-sitter-python = "0.20.2" tree-sitter-toml = { git = "https://github.com/tree-sitter/tree-sitter-toml", rev = "342d9be207c2dba869b9967124c679b5e6fd0ebe" } -tree-sitter-typescript = "0.20.1" +tree-sitter-typescript = { git = "https://github.com/tree-sitter/tree-sitter-typescript", rev = "5d20856f34315b068c41edaee2ac8a100081d259" } + tree-sitter-ruby = "0.20.0" tree-sitter-html = "0.19.0" tree-sitter-scheme = { git = "https://github.com/6cdh/tree-sitter-scheme", rev = "af0fd1fa452cb2562dc7b5c8a8c55551c39273b9"} tree-sitter-racket = { git = "https://github.com/zed-industries/tree-sitter-racket", rev = "eb010cf2c674c6fd9a6316a84e28ef90190fe51a"} +tree-sitter-yaml = { git = "https://github.com/zed-industries/tree-sitter-yaml", rev = "9050a4a4a847ed29e25485b1292a36eab8ae3492"} +tree-sitter-lua = "0.0.14" url = "2.2" +urlencoding = "2.1.2" +uuid = { version = "1.1.2", features = ["v4"] } [dev-dependencies] call = { path = "../call", features = ["test-support"] } diff --git a/crates/zed/src/languages.rs b/crates/zed/src/languages.rs index 240d1dc49e..a99c80c001 100644 --- a/crates/zed/src/languages.rs +++ b/crates/zed/src/languages.rs @@ -1,7 +1,5 @@ use anyhow::Context; -use gpui::executor::Background; pub use language::*; -use lazy_static::lazy_static; use rust_embed::RustEmbed; use std::{borrow::Cow, str, sync::Arc}; @@ -12,11 +10,12 @@ mod html; mod installation; mod json; mod language_plugin; +mod lua; mod python; mod ruby; mod rust; - mod typescript; +mod yaml; // 1. Add tree-sitter-{language} parser to zed crate // 2. Create a language directory in zed/crates/zed/src/languages and add the language to init function below @@ -32,32 +31,17 @@ mod typescript; #[exclude = "*.rs"] struct LanguageDir; -// TODO - Remove this once the `init` function is synchronous again. -lazy_static! { - pub static ref LANGUAGE_NAMES: Vec = LanguageDir::iter() - .filter_map(|path| { - if path.ends_with("config.toml") { - let config = LanguageDir::get(&path)?; - let config = toml::from_slice::(&config.data).ok()?; - Some(config.name.to_string()) - } else { - None - } - }) - .collect(); -} - -pub async fn init(languages: Arc, _executor: Arc) { +pub fn init(languages: Arc) { for (name, grammar, lsp_adapter) in [ ( "c", tree_sitter_c::language(), - Some(CachedLspAdapter::new(c::CLspAdapter).await), + Some(Box::new(c::CLspAdapter) as Box), ), ( "cpp", tree_sitter_cpp::language(), - Some(CachedLspAdapter::new(c::CLspAdapter).await), + Some(Box::new(c::CLspAdapter)), ), ( "css", @@ -67,17 +51,17 @@ pub async fn init(languages: Arc, _executor: Arc) ( "elixir", tree_sitter_elixir::language(), - Some(CachedLspAdapter::new(elixir::ElixirLspAdapter).await), + Some(Box::new(elixir::ElixirLspAdapter)), ), ( "go", tree_sitter_go::language(), - Some(CachedLspAdapter::new(go::GoLspAdapter).await), + Some(Box::new(go::GoLspAdapter)), ), ( "json", tree_sitter_json::language(), - Some(CachedLspAdapter::new(json::JsonLspAdapter).await), + Some(Box::new(json::JsonLspAdapter)), ), ( "markdown", @@ -87,12 +71,12 @@ pub async fn init(languages: Arc, _executor: Arc) ( "python", tree_sitter_python::language(), - Some(CachedLspAdapter::new(python::PythonLspAdapter).await), + Some(Box::new(python::PythonLspAdapter)), ), ( "rust", tree_sitter_rust::language(), - Some(CachedLspAdapter::new(rust::RustLspAdapter).await), + Some(Box::new(rust::RustLspAdapter)), ), ( "toml", @@ -102,89 +86,92 @@ pub async fn init(languages: Arc, _executor: Arc) ( "tsx", tree_sitter_typescript::language_tsx(), - Some(CachedLspAdapter::new(typescript::TypeScriptLspAdapter).await), + Some(Box::new(typescript::TypeScriptLspAdapter)), ), ( "typescript", tree_sitter_typescript::language_typescript(), - Some(CachedLspAdapter::new(typescript::TypeScriptLspAdapter).await), + Some(Box::new(typescript::TypeScriptLspAdapter)), ), ( "javascript", tree_sitter_typescript::language_tsx(), - Some(CachedLspAdapter::new(typescript::TypeScriptLspAdapter).await), + Some(Box::new(typescript::TypeScriptLspAdapter)), ), ( "html", tree_sitter_html::language(), - Some(CachedLspAdapter::new(html::HtmlLspAdapter).await), + Some(Box::new(html::HtmlLspAdapter)), ), ( "ruby", tree_sitter_ruby::language(), - Some(CachedLspAdapter::new(ruby::RubyLanguageServer).await), + Some(Box::new(ruby::RubyLanguageServer)), ), ( "erb", tree_sitter_embedded_template::language(), - Some(CachedLspAdapter::new(ruby::RubyLanguageServer).await), + Some(Box::new(ruby::RubyLanguageServer)), + ), + ( + "scheme", + tree_sitter_scheme::language(), + None, // + ), + ( + "racket", + tree_sitter_racket::language(), + None, // + ), + ( + "lua", + tree_sitter_lua::language(), + Some(Box::new(lua::LuaLspAdapter)), + ), + ( + "yaml", + tree_sitter_yaml::language(), + Some(Box::new(yaml::YamlLspAdapter)), ), - ("scheme", tree_sitter_scheme::language(), None), - ("racket", tree_sitter_racket::language(), None), ] { - languages.add(language(name, grammar, lsp_adapter)); + languages.register(name, load_config(name), grammar, lsp_adapter, load_queries); } } -pub(crate) fn language( +#[cfg(any(test, feature = "test-support"))] +pub async fn language( name: &str, grammar: tree_sitter::Language, - lsp_adapter: Option>, + lsp_adapter: Option>, ) -> Arc { - let config = toml::from_slice( + Arc::new( + Language::new(load_config(name), Some(grammar)) + .with_lsp_adapter(lsp_adapter) + .await + .with_queries(load_queries(name)) + .unwrap(), + ) +} + +fn load_config(name: &str) -> LanguageConfig { + toml::from_slice( &LanguageDir::get(&format!("{}/config.toml", name)) .unwrap() .data, ) .with_context(|| format!("failed to load config.toml for language {name:?}")) - .unwrap(); + .unwrap() +} - let mut language = Language::new(config, Some(grammar)); - - if let Some(query) = load_query(name, "/highlights") { - language = language - .with_highlights_query(query.as_ref()) - .expect("failed to evaluate highlights query"); +fn load_queries(name: &str) -> LanguageQueries { + LanguageQueries { + highlights: load_query(name, "/highlights"), + brackets: load_query(name, "/brackets"), + indents: load_query(name, "/indents"), + outline: load_query(name, "/outline"), + injections: load_query(name, "/injections"), + overrides: load_query(name, "/overrides"), } - if let Some(query) = load_query(name, "/brackets") { - language = language - .with_brackets_query(query.as_ref()) - .expect("failed to load brackets query"); - } - if let Some(query) = load_query(name, "/indents") { - language = language - .with_indents_query(query.as_ref()) - .expect("failed to load indents query"); - } - if let Some(query) = load_query(name, "/outline") { - language = language - .with_outline_query(query.as_ref()) - .expect("failed to load outline query"); - } - if let Some(query) = load_query(name, "/injections") { - language = language - .with_injection_query(query.as_ref()) - .expect("failed to load injection query"); - } - if let Some(query) = load_query(name, "/overrides") { - language = language - .with_override_query(query.as_ref()) - .expect("failed to load override query"); - } - if let Some(lsp_adapter) = lsp_adapter { - language = language.with_lsp_adapter(lsp_adapter) - } - Arc::new(language) } fn load_query(name: &str, filename_prefix: &str) -> Option> { diff --git a/crates/zed/src/languages/c.rs b/crates/zed/src/languages/c.rs index 712e87101b..9fbb12857f 100644 --- a/crates/zed/src/languages/c.rs +++ b/crates/zed/src/languages/c.rs @@ -248,17 +248,19 @@ impl super::LspAdapter for CLspAdapter { #[cfg(test)] mod tests { - use gpui::MutableAppContext; + use gpui::TestAppContext; use language::{AutoindentMode, Buffer}; use settings::Settings; #[gpui::test] - fn test_c_autoindent(cx: &mut MutableAppContext) { + async fn test_c_autoindent(cx: &mut TestAppContext) { cx.foreground().set_block_on_ticks(usize::MAX..=usize::MAX); - let mut settings = Settings::test(cx); - settings.editor_overrides.tab_size = Some(2.try_into().unwrap()); - cx.set_global(settings); - let language = crate::languages::language("c", tree_sitter_c::language(), None); + cx.update(|cx| { + let mut settings = Settings::test(cx); + settings.editor_overrides.tab_size = Some(2.try_into().unwrap()); + cx.set_global(settings); + }); + let language = crate::languages::language("c", tree_sitter_c::language(), None).await; cx.add_model(|cx| { let mut buffer = Buffer::new(0, "", cx).with_language(language, cx); diff --git a/crates/zed/src/languages/go.rs b/crates/zed/src/languages/go.rs index 19692fdf44..dc84599e4e 100644 --- a/crates/zed/src/languages/go.rs +++ b/crates/zed/src/languages/go.rs @@ -314,8 +314,9 @@ mod tests { let language = language( "go", tree_sitter_go::language(), - Some(CachedLspAdapter::new(GoLspAdapter).await), - ); + Some(Box::new(GoLspAdapter)), + ) + .await; let theme = SyntaxTheme::new(vec![ ("type".into(), Color::green().into()), diff --git a/crates/zed/src/languages/installation.rs b/crates/zed/src/languages/installation.rs index c5aff17e56..df28177f0b 100644 --- a/crates/zed/src/languages/installation.rs +++ b/crates/zed/src/languages/installation.rs @@ -39,6 +39,7 @@ pub async fn npm_package_latest_version(name: &str) -> Result { let output = smol::process::Command::new("npm") .args(["-fetch-retry-mintimeout", "2000"]) .args(["-fetch-retry-maxtimeout", "5000"]) + .args(["-fetch-timeout", "5000"]) .args(["info", name, "--json"]) .output() .await @@ -64,6 +65,7 @@ pub async fn npm_install_packages( let output = smol::process::Command::new("npm") .args(["-fetch-retry-mintimeout", "2000"]) .args(["-fetch-retry-maxtimeout", "5000"]) + .args(["-fetch-timeout", "5000"]) .arg("install") .arg("--prefix") .arg(directory) diff --git a/crates/zed/src/languages/lua.rs b/crates/zed/src/languages/lua.rs new file mode 100644 index 0000000000..4bcffca908 --- /dev/null +++ b/crates/zed/src/languages/lua.rs @@ -0,0 +1,108 @@ +use std::{any::Any, env::consts, path::PathBuf, sync::Arc}; + +use anyhow::{anyhow, bail, Result}; +use async_compression::futures::bufread::GzipDecoder; +use async_tar::Archive; +use async_trait::async_trait; +use client::http::HttpClient; +use futures::{io::BufReader, StreamExt}; +use language::LanguageServerName; +use smol::fs; +use util::{async_iife, ResultExt}; + +use super::installation::{latest_github_release, GitHubLspBinaryVersion}; + +#[derive(Copy, Clone)] +pub struct LuaLspAdapter; + +#[async_trait] +impl super::LspAdapter for LuaLspAdapter { + async fn name(&self) -> LanguageServerName { + LanguageServerName("lua-language-server".into()) + } + + async fn server_args(&self) -> Vec { + vec![ + "--logpath=~/lua-language-server.log".into(), + "--loglevel=trace".into(), + ] + } + + async fn fetch_latest_server_version( + &self, + http: Arc, + ) -> Result> { + let release = latest_github_release("LuaLS/lua-language-server", http).await?; + let version = release.name.clone(); + let platform = match consts::ARCH { + "x86_64" => "x64", + "aarch64" => "arm64", + other => bail!("Running on unsupported platform: {other}"), + }; + let asset_name = format!("lua-language-server-{version}-darwin-{platform}.tar.gz"); + let asset = release + .assets + .iter() + .find(|asset| asset.name == asset_name) + .ok_or_else(|| anyhow!("no asset found matching {:?}", asset_name))?; + let version = GitHubLspBinaryVersion { + name: release.name.clone(), + url: asset.browser_download_url.clone(), + }; + Ok(Box::new(version) as Box<_>) + } + + async fn fetch_server_binary( + &self, + version: Box, + http: Arc, + container_dir: PathBuf, + ) -> Result { + let version = version.downcast::().unwrap(); + + let binary_path = container_dir.join("bin/lua-language-server"); + + if fs::metadata(&binary_path).await.is_err() { + let mut response = http + .get(&version.url, Default::default(), true) + .await + .map_err(|err| anyhow!("error downloading release: {}", err))?; + let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut())); + let archive = Archive::new(decompressed_bytes); + archive.unpack(container_dir).await?; + } + + fs::set_permissions( + &binary_path, + ::from_mode(0o755), + ) + .await?; + Ok(binary_path) + } + + async fn cached_server_binary(&self, container_dir: PathBuf) -> Option { + async_iife!({ + let mut last_binary_path = None; + let mut entries = fs::read_dir(&container_dir).await?; + while let Some(entry) = entries.next().await { + let entry = entry?; + if entry.file_type().await?.is_file() + && entry + .file_name() + .to_str() + .map_or(false, |name| name == "lua-language-server") + { + last_binary_path = Some(entry.path()); + } + } + + if let Some(path) = last_binary_path { + Ok(path) + } else { + Err(anyhow!("no cached binary")) + } + }) + .await + .log_err() + } +} diff --git a/crates/zed/src/languages/lua/brackets.scm b/crates/zed/src/languages/lua/brackets.scm new file mode 100644 index 0000000000..5f5bd60b93 --- /dev/null +++ b/crates/zed/src/languages/lua/brackets.scm @@ -0,0 +1,3 @@ +("[" @open "]" @close) +("{" @open "}" @close) +("(" @open ")" @close) \ No newline at end of file diff --git a/crates/zed/src/languages/lua/config.toml b/crates/zed/src/languages/lua/config.toml new file mode 100644 index 0000000000..effb37f945 --- /dev/null +++ b/crates/zed/src/languages/lua/config.toml @@ -0,0 +1,15 @@ +name = "Lua" +path_suffixes = ["lua"] +line_comment = "-- " +autoclose_before = ",]}" +brackets = [ +{ start = "{", end = "}", close = true, newline = true }, +{ start = "[", end = "]", close = true, newline = true }, +{ start = "\"", end = "\"", close = true, newline = false }, +] + +[overrides.string] +brackets = [ +{ start = "{", end = "}", close = true, newline = true }, +{ start = "[", end = "]", close = true, newline = true }, +] \ No newline at end of file diff --git a/crates/zed/src/languages/lua/highlights.scm b/crates/zed/src/languages/lua/highlights.scm new file mode 100644 index 0000000000..96389c79b4 --- /dev/null +++ b/crates/zed/src/languages/lua/highlights.scm @@ -0,0 +1,192 @@ +;; Keywords + +"return" @keyword + +[ + "goto" + "in" + "local" +] @keyword + +(break_statement) @keyword + +(do_statement +[ + "do" + "end" +] @keyword) + +(while_statement +[ + "while" + "do" + "end" +] @keyword) + +(repeat_statement +[ + "repeat" + "until" +] @keyword) + +(if_statement +[ + "if" + "elseif" + "else" + "then" + "end" +] @keyword) + +(elseif_statement +[ + "elseif" + "then" + "end" +] @keyword) + +(else_statement +[ + "else" + "end" +] @keyword) + +(for_statement +[ + "for" + "do" + "end" +] @keyword) + +(function_declaration +[ + "function" + "end" +] @keyword) + +(function_definition +[ + "function" + "end" +] @keyword) + +;; Operators + +[ + "and" + "not" + "or" +] @operator + +[ + "+" + "-" + "*" + "/" + "%" + "^" + "#" + "==" + "~=" + "<=" + ">=" + "<" + ">" + "=" + "&" + "~" + "|" + "<<" + ">>" + "//" + ".." +] @operator + +;; Punctuations + +[ + ";" + ":" + "," + "." +] @punctuation.delimiter + +;; Brackets + +[ + "(" + ")" + "[" + "]" + "{" + "}" +] @punctuation.bracket + +;; Variables + +(identifier) @variable + +((identifier) @variable.special + (#eq? @variable.special "self")) + +(variable_list + attribute: (attribute + (["<" ">"] @punctuation.bracket + (identifier) @attribute))) + +;; Constants + +((identifier) @constant + (#lua-match? @constant "^[A-Z][A-Z_0-9]*$")) + +(vararg_expression) @constant + +(nil) @constant.builtin + +[ + (false) + (true) +] @boolean + +;; Tables + +(field name: (identifier) @field) + +(dot_index_expression field: (identifier) @field) + +(table_constructor +[ + "{" + "}" +] @constructor) + +;; Functions + +(parameters (identifier) @parameter) + +(function_call name: (identifier) @function.call) +(function_declaration name: (identifier) @function) + +(function_call name: (dot_index_expression field: (identifier) @function.call)) +(function_declaration name: (dot_index_expression field: (identifier) @function)) + +(method_index_expression method: (identifier) @method) + +(function_call + (identifier) @function.builtin + (#any-of? @function.builtin + ;; built-in functions in Lua 5.1 + "assert" "collectgarbage" "dofile" "error" "getfenv" "getmetatable" "ipairs" + "load" "loadfile" "loadstring" "module" "next" "pairs" "pcall" "print" + "rawequal" "rawget" "rawset" "require" "select" "setfenv" "setmetatable" + "tonumber" "tostring" "type" "unpack" "xpcall")) + +;; Others + +(comment) @comment + +(hash_bang_line) @preproc + +(number) @number + +(string) @string \ No newline at end of file diff --git a/crates/zed/src/languages/lua/indents.scm b/crates/zed/src/languages/lua/indents.scm new file mode 100644 index 0000000000..71e15a0c33 --- /dev/null +++ b/crates/zed/src/languages/lua/indents.scm @@ -0,0 +1,10 @@ +(if_statement "end" @end) @indent +(do_statement "end" @end) @indent +(while_statement "end" @end) @indent +(for_statement "end" @end) @indent +(repeat_statement "until" @end) @indent +(function_declaration "end" @end) @indent + +(_ "[" "]" @end) @indent +(_ "{" "}" @end) @indent +(_ "(" ")" @end) @indent \ No newline at end of file diff --git a/crates/zed/src/languages/lua/outline.scm b/crates/zed/src/languages/lua/outline.scm new file mode 100644 index 0000000000..8bd8d88070 --- /dev/null +++ b/crates/zed/src/languages/lua/outline.scm @@ -0,0 +1,3 @@ +(function_declaration + "function" @context + name: (_) @name) @item \ No newline at end of file diff --git a/crates/zed/src/languages/markdown/injections.scm b/crates/zed/src/languages/markdown/injections.scm new file mode 100644 index 0000000000..577054b404 --- /dev/null +++ b/crates/zed/src/languages/markdown/injections.scm @@ -0,0 +1,4 @@ +(fenced_code_block + (info_string + (language) @language) + (code_fence_content) @content) diff --git a/crates/zed/src/languages/python.rs b/crates/zed/src/languages/python.rs index ba6ccf7bf0..1391494ab1 100644 --- a/crates/zed/src/languages/python.rs +++ b/crates/zed/src/languages/python.rs @@ -165,17 +165,20 @@ impl LspAdapter for PythonLspAdapter { #[cfg(test)] mod tests { - use gpui::{ModelContext, MutableAppContext}; + use gpui::{ModelContext, TestAppContext}; use language::{AutoindentMode, Buffer}; use settings::Settings; #[gpui::test] - fn test_python_autoindent(cx: &mut MutableAppContext) { + async fn test_python_autoindent(cx: &mut TestAppContext) { cx.foreground().set_block_on_ticks(usize::MAX..=usize::MAX); - let language = crate::languages::language("python", tree_sitter_python::language(), None); - let mut settings = Settings::test(cx); - settings.editor_overrides.tab_size = Some(2.try_into().unwrap()); - cx.set_global(settings); + 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.add_model(|cx| { let mut buffer = Buffer::new(0, "", cx).with_language(language, cx); diff --git a/crates/zed/src/languages/python/config.toml b/crates/zed/src/languages/python/config.toml index a817de8e3b..45f20e25a3 100644 --- a/crates/zed/src/languages/python/config.toml +++ b/crates/zed/src/languages/python/config.toml @@ -11,7 +11,7 @@ brackets = [ ] auto_indent_using_last_non_empty_line = false -increase_indent_pattern = ":$" +increase_indent_pattern = ":\\s*$" decrease_indent_pattern = "^\\s*(else|elif|except|finally)\\b.*:" [overrides.comment] diff --git a/crates/zed/src/languages/rust.rs b/crates/zed/src/languages/rust.rs index 30971fef1a..40948d5005 100644 --- a/crates/zed/src/languages/rust.rs +++ b/crates/zed/src/languages/rust.rs @@ -255,8 +255,8 @@ impl LspAdapter for RustLspAdapter { #[cfg(test)] mod tests { use super::*; - use crate::languages::{language, CachedLspAdapter}; - use gpui::{color::Color, MutableAppContext}; + use crate::languages::language; + use gpui::{color::Color, TestAppContext}; use settings::Settings; use theme::SyntaxTheme; @@ -306,8 +306,9 @@ mod tests { let language = language( "rust", tree_sitter_rust::language(), - Some(CachedLspAdapter::new(RustLspAdapter).await), - ); + Some(Box::new(RustLspAdapter)), + ) + .await; let grammar = language.grammar().unwrap(); let theme = SyntaxTheme::new(vec![ ("type".into(), Color::green().into()), @@ -391,8 +392,9 @@ mod tests { let language = language( "rust", tree_sitter_rust::language(), - Some(CachedLspAdapter::new(RustLspAdapter).await), - ); + Some(Box::new(RustLspAdapter)), + ) + .await; let grammar = language.grammar().unwrap(); let theme = SyntaxTheme::new(vec![ ("type".into(), Color::green().into()), @@ -431,12 +433,15 @@ mod tests { } #[gpui::test] - fn test_rust_autoindent(cx: &mut MutableAppContext) { + async fn test_rust_autoindent(cx: &mut TestAppContext) { cx.foreground().set_block_on_ticks(usize::MAX..=usize::MAX); - let language = crate::languages::language("rust", tree_sitter_rust::language(), None); - let mut settings = Settings::test(cx); - settings.editor_overrides.tab_size = Some(2.try_into().unwrap()); - cx.set_global(settings); + cx.update(|cx| { + let mut settings = Settings::test(cx); + settings.editor_overrides.tab_size = Some(2.try_into().unwrap()); + cx.set_global(settings); + }); + + let language = crate::languages::language("rust", tree_sitter_rust::language(), None).await; cx.add_model(|cx| { let mut buffer = Buffer::new(0, "", cx).with_language(language, cx); diff --git a/crates/zed/src/languages/rust/highlights.scm b/crates/zed/src/languages/rust/highlights.scm index b52a7a8aff..7240173a89 100644 --- a/crates/zed/src/languages/rust/highlights.scm +++ b/crates/zed/src/languages/rust/highlights.scm @@ -12,6 +12,15 @@ field: (field_identifier) @function.method) ]) +(generic_function + function: [ + (identifier) @function + (scoped_identifier + name: (identifier) @function) + (field_expression + field: (field_identifier) @function.method) + ]) + (function_item name: (identifier) @function.definition) (function_signature_item name: (identifier) @function.definition) diff --git a/crates/zed/src/languages/typescript.rs b/crates/zed/src/languages/typescript.rs index 01b62577ad..5290158dea 100644 --- a/crates/zed/src/languages/typescript.rs +++ b/crates/zed/src/languages/typescript.rs @@ -154,17 +154,17 @@ impl LspAdapter for TypeScriptLspAdapter { #[cfg(test)] mod tests { - - use gpui::MutableAppContext; + use gpui::TestAppContext; use unindent::Unindent; #[gpui::test] - fn test_outline(cx: &mut MutableAppContext) { + async fn test_outline(cx: &mut TestAppContext) { let language = crate::languages::language( "typescript", tree_sitter_typescript::language_typescript(), None, - ); + ) + .await; let text = r#" function a() { @@ -183,7 +183,7 @@ mod tests { let buffer = cx.add_model(|cx| language::Buffer::new(0, text, cx).with_language(language, cx)); - let outline = buffer.read(cx).snapshot().outline(None).unwrap(); + let outline = buffer.read_with(cx, |buffer, _| buffer.snapshot().outline(None).unwrap()); assert_eq!( outline .items diff --git a/crates/zed/src/languages/typescript/highlights.scm b/crates/zed/src/languages/typescript/highlights.scm index bd1986b6b3..43df33d158 100644 --- a/crates/zed/src/languages/typescript/highlights.scm +++ b/crates/zed/src/languages/typescript/highlights.scm @@ -175,6 +175,7 @@ "new" "of" "return" + "satisfies" "set" "static" "switch" diff --git a/crates/zed/src/languages/yaml.rs b/crates/zed/src/languages/yaml.rs new file mode 100644 index 0000000000..46569111f1 --- /dev/null +++ b/crates/zed/src/languages/yaml.rs @@ -0,0 +1,93 @@ +use std::{any::Any, path::PathBuf, sync::Arc}; + +use anyhow::{anyhow, Context, Result}; +use async_trait::async_trait; +use client::http::HttpClient; +use futures::StreamExt; +use smol::fs; + +use language::{LanguageServerName, LspAdapter}; +use util::ResultExt; + +use super::installation::{npm_install_packages, npm_package_latest_version}; + +pub struct YamlLspAdapter; + +impl YamlLspAdapter { + const BIN_PATH: &'static str = "node_modules/yaml-language-server/bin/yaml-language-server"; +} + +#[async_trait] +impl LspAdapter for YamlLspAdapter { + async fn name(&self) -> LanguageServerName { + LanguageServerName("yaml-language-server".into()) + } + + async fn server_args(&self) -> Vec { + vec!["--stdio".into()] + } + + async fn fetch_latest_server_version( + &self, + _: Arc, + ) -> Result> { + Ok(Box::new(npm_package_latest_version("yaml-language-server").await?) as Box<_>) + } + + async fn fetch_server_binary( + &self, + version: Box, + _: Arc, + container_dir: PathBuf, + ) -> Result { + let version = version.downcast::().unwrap(); + let version_dir = container_dir.join(version.as_str()); + fs::create_dir_all(&version_dir) + .await + .context("failed to create version directory")?; + let binary_path = version_dir.join(Self::BIN_PATH); + + if fs::metadata(&binary_path).await.is_err() { + npm_install_packages([("yaml-language-server", version.as_str())], &version_dir) + .await?; + + if let Some(mut entries) = fs::read_dir(&container_dir).await.log_err() { + while let Some(entry) = entries.next().await { + if let Some(entry) = entry.log_err() { + let entry_path = entry.path(); + if entry_path.as_path() != version_dir { + fs::remove_dir_all(&entry_path).await.log_err(); + } + } + } + } + } + + Ok(binary_path) + } + + async fn cached_server_binary(&self, container_dir: PathBuf) -> Option { + (|| async move { + let mut last_version_dir = None; + let mut entries = fs::read_dir(&container_dir).await?; + while let Some(entry) = entries.next().await { + let entry = entry?; + if entry.file_type().await?.is_dir() { + last_version_dir = Some(entry.path()); + } + } + let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?; + let bin_path = last_version_dir.join(Self::BIN_PATH); + if bin_path.exists() { + Ok(bin_path) + } else { + Err(anyhow!( + "missing executable in directory {:?}", + last_version_dir + )) + } + })() + .await + .log_err() + } +} diff --git a/crates/zed/src/languages/yaml/brackets.scm b/crates/zed/src/languages/yaml/brackets.scm new file mode 100644 index 0000000000..9e8c9cd93c --- /dev/null +++ b/crates/zed/src/languages/yaml/brackets.scm @@ -0,0 +1,3 @@ +("[" @open "]" @close) +("{" @open "}" @close) +("\"" @open "\"" @close) diff --git a/crates/zed/src/languages/yaml/config.toml b/crates/zed/src/languages/yaml/config.toml new file mode 100644 index 0000000000..08dac475b3 --- /dev/null +++ b/crates/zed/src/languages/yaml/config.toml @@ -0,0 +1,17 @@ +name = "YAML" +path_suffixes = ["yml", "yaml"] +line_comment = "# " +autoclose_before = ",]}" +brackets = [ + { start = "{", end = "}", close = true, newline = true }, + { start = "[", end = "]", close = true, newline = true }, + { start = "\"", end = "\"", close = true, newline = false }, +] + +increase_indent_pattern = ":\\s*[|>]?\\s*$" + +[overrides.string] +brackets = [ + { start = "{", end = "}", close = true, newline = true }, + { start = "[", end = "]", close = true, newline = true }, +] diff --git a/crates/zed/src/languages/yaml/highlights.scm b/crates/zed/src/languages/yaml/highlights.scm new file mode 100644 index 0000000000..06081f63cb --- /dev/null +++ b/crates/zed/src/languages/yaml/highlights.scm @@ -0,0 +1,49 @@ +(boolean_scalar) @boolean +(null_scalar) @constant.builtin + +[ + (double_quote_scalar) + (single_quote_scalar) + (block_scalar) + (string_scalar) +] @string + +(escape_sequence) @string.escape + +[ + (integer_scalar) + (float_scalar) +] @number + +(comment) @comment + +[ + (anchor_name) + (alias_name) + (tag) +] @type + +key: (flow_node (plain_scalar (string_scalar) @property)) + +[ + "," + "-" + ":" + ">" + "?" + "|" +] @punctuation.delimiter + +[ + "[" + "]" + "{" + "}" +] @punctuation.bracket + +[ + "*" + "&" + "---" + "..." +] @punctuation.special \ No newline at end of file diff --git a/crates/zed/src/languages/yaml/outline.scm b/crates/zed/src/languages/yaml/outline.scm new file mode 100644 index 0000000000..e85eb1bf8a --- /dev/null +++ b/crates/zed/src/languages/yaml/outline.scm @@ -0,0 +1 @@ +(block_mapping_pair key: (flow_node (plain_scalar (string_scalar) @name))) @item \ No newline at end of file diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index fe7e95cf24..a775b31bc4 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -3,7 +3,6 @@ use anyhow::{anyhow, Context, Result}; use assets::Assets; -use auto_update::ZED_APP_VERSION; use backtrace::Backtrace; use cli::{ ipc::{self, IpcSender}, @@ -12,7 +11,7 @@ use cli::{ use client::{ self, http::{self, HttpClient}, - UserStore, ZED_SECRET_CLIENT_TOKEN, + UserStore, ZED_APP_VERSION, ZED_SECRET_CLIENT_TOKEN, }; use futures::{ @@ -24,7 +23,7 @@ use isahc::{config::Configurable, Request}; use language::LanguageRegistry; use log::LevelFilter; use parking_lot::Mutex; -use project::{Fs, HomeDir}; +use project::Fs; use serde_json::json; use settings::{ self, settings_file::SettingsFile, KeymapFileContent, Settings, SettingsFileContent, @@ -32,13 +31,15 @@ use settings::{ }; use simplelog::ConfigBuilder; use smol::process::Command; -use std::fs::OpenOptions; use std::{env, ffi::OsStr, panic, path::PathBuf, sync::Arc, thread, time::Duration}; +use std::{fs::OpenOptions, os::unix::prelude::OsStrExt}; use terminal_view::{get_working_directory, TerminalView}; use fs::RealFs; use settings::watched_json::{watch_keymap_file, watch_settings_file, WatchedJsonFile}; use theme::ThemeRegistry; +#[cfg(debug_assertions)] +use util::StaffMode; use util::{channel::RELEASE_CHANNEL, paths, ResultExt, TryFutureExt}; use workspace::{ self, item::ItemHandle, notifications::NotifyResultExt, AppState, NewFile, OpenPaths, Workspace, @@ -90,7 +91,10 @@ fn main() { let paths: Vec<_> = urls .iter() .flat_map(|url| url.strip_prefix("file://")) - .map(|path| PathBuf::from(path)) + .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) @@ -101,7 +105,9 @@ fn main() { app.run(move |cx| { cx.set_global(*RELEASE_CHANNEL); - cx.set_global(HomeDir(paths::HOME.to_path_buf())); + + #[cfg(debug_assertions)] + cx.set_global(StaffMode(true)); let (settings_file_content, keymap_file) = cx.background().block(config_files).unwrap(); @@ -117,11 +123,10 @@ fn main() { let client = client::Client::new(http.clone(), cx); let mut languages = LanguageRegistry::new(login_shell_env_loaded); + languages.set_executor(cx.background().clone()); languages.set_language_server_download_dir(paths::LANGUAGES_DIR.clone()); let languages = Arc::new(languages); - let init_languages = cx - .background() - .spawn(languages::init(languages.clone(), cx.background().clone())); + languages::init(languages.clone()); let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http.clone(), cx)); watch_keymap_file(keymap_file, cx); @@ -133,7 +138,6 @@ fn main() { client::init(client.clone(), cx); command_palette::init(cx); editor::init(cx); - feedback::init(cx); go_to_line::init(cx); file_finder::init(cx); outline::init(cx); @@ -149,14 +153,7 @@ fn main() { cx.spawn(|cx| watch_themes(fs.clone(), themes.clone(), cx)) .detach(); - cx.spawn({ - let languages = languages.clone(); - |cx| async move { - cx.read(|cx| languages.set_theme(cx.global::().theme.clone())); - init_languages.await; - } - }) - .detach(); + languages.set_theme(cx.global::().theme.clone()); cx.observe_global::({ let languages = languages.clone(); move |cx| languages.set_theme(cx.global::().theme.clone()) @@ -188,6 +185,7 @@ fn main() { theme_selector::init(app_state.clone(), cx); zed::init(&app_state, cx); collab_ui::init(app_state.clone(), cx); + feedback::init(app_state.clone(), cx); cx.set_menus(menus::menus()); @@ -571,6 +569,14 @@ async fn handle_cli_connection( if let Some(request) = requests.next().await { match request { CliRequest::Open { paths, wait } => { + let paths = if paths.is_empty() { + workspace::last_opened_workspace_paths() + .await + .map(|location| location.paths().to_vec()) + .unwrap_or(paths) + } else { + paths + }; let (workspace, items) = cx .update(|cx| workspace::open_paths(&paths, &app_state, cx)) .await; diff --git a/crates/zed/src/menus.rs b/crates/zed/src/menus.rs index 834eb751e1..52ca7d2324 100644 --- a/crates/zed/src/menus.rs +++ b/crates/zed/src/menus.rs @@ -81,7 +81,7 @@ pub fn menus() -> Vec> { }, MenuItem::Action { name: "Open Recent...", - action: Box::new(recent_projects::Toggle), + action: Box::new(recent_projects::OpenRecent), }, MenuItem::Separator, MenuItem::Action { @@ -146,7 +146,7 @@ pub fn menus() -> Vec> { MenuItem::Separator, MenuItem::Action { name: "Toggle Line Comment", - action: Box::new(editor::ToggleComments), + action: Box::new(editor::ToggleComments::default()), }, MenuItem::Action { name: "Emoji & Symbols", @@ -219,10 +219,6 @@ pub fn menus() -> Vec> { name: "Toggle Left Sidebar", action: Box::new(workspace::ToggleLeftSidebar), }, - MenuItem::Action { - name: "Toggle Right Sidebar", - action: Box::new(workspace::ToggleRightSidebar), - }, MenuItem::Submenu(Menu { name: "Editor Layout", items: vec![ @@ -293,7 +289,7 @@ pub fn menus() -> Vec> { action: Box::new(editor::GoToTypeDefinition), }, MenuItem::Action { - name: "Go to References", + name: "Find All References", action: Box::new(editor::FindAllReferences), }, MenuItem::Action { @@ -337,6 +333,10 @@ pub fn menus() -> Vec> { name: "View Telemetry Log", action: Box::new(crate::OpenTelemetryLog), }, + MenuItem::Action { + name: "View Dependency Licenses", + action: Box::new(crate::OpenLicenses), + }, MenuItem::Separator, MenuItem::Action { name: "Copy System Specs Into Clipboard", diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 78d10670f7..16b5413fda 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -11,6 +11,9 @@ use collections::VecDeque; pub use editor; use editor::{Editor, MultiBuffer}; +use feedback::{ + feedback_info_text::FeedbackInfoText, submit_feedback_button::SubmitFeedbackButton, +}; use futures::StreamExt; use gpui::{ actions, @@ -20,7 +23,7 @@ use gpui::{ }, impl_actions, platform::{WindowBounds, WindowOptions}, - AssetSource, AsyncAppContext, PromptLevel, TitlebarOptions, ViewContext, WindowKind, + AssetSource, AsyncAppContext, Platform, PromptLevel, TitlebarOptions, ViewContext, WindowKind, }; use language::Rope; use lazy_static::lazy_static; @@ -32,9 +35,10 @@ use serde::Deserialize; use serde_json::to_string_pretty; use settings::{keymap_file_json_schema, settings_file_json_schema, Settings}; use std::{borrow::Cow, env, path::Path, str, sync::Arc}; -use util::{channel::ReleaseChannel, paths, ResultExt}; +use util::{channel::ReleaseChannel, paths, ResultExt, StaffMode}; +use uuid::Uuid; pub use workspace; -use workspace::{sidebar::SidebarSide, AppState, Workspace}; +use workspace::{sidebar::SidebarSide, AppState, Restart, Workspace}; #[derive(Deserialize, Clone, PartialEq)] pub struct OpenBrowser { @@ -126,6 +130,7 @@ pub fn init(app_state: &Arc, cx: &mut gpui::MutableAppContext) { }, ); cx.add_global_action(quit); + cx.add_global_action(restart); 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, cx| { @@ -234,7 +239,11 @@ pub fn init(app_state: &Arc, cx: &mut gpui::MutableAppContext) { |workspace: &mut Workspace, _: &DebugElements, cx: &mut ViewContext| { let content = to_string_pretty(&cx.debug_elements()).unwrap(); let project = workspace.project().clone(); - let json_language = project.read(cx).languages().get_language("JSON").unwrap(); + let json_language = project + .read(cx) + .languages() + .language_for_name("JSON") + .unwrap(); if project.read(cx).is_remote() { cx.propagate_action(); } else if let Some(buffer) = project @@ -282,6 +291,10 @@ pub fn initialize_workspace( toolbar.add_item(buffer_search_bar, cx); let project_search_bar = cx.add_view(|_| ProjectSearchBar::new()); toolbar.add_item(project_search_bar, cx); + let submit_feedback_button = cx.add_view(|_| SubmitFeedbackButton::new()); + toolbar.add_item(submit_feedback_button, cx); + let feedback_info_text = cx.add_view(|_| FeedbackInfoText::new()); + toolbar.add_item(feedback_info_text, cx); }) }); } @@ -292,17 +305,12 @@ pub fn initialize_workspace( cx.emit(workspace::Event::PaneAdded(workspace.active_pane().clone())); cx.emit(workspace::Event::PaneAdded(workspace.dock_pane().clone())); - let settings = cx.global::(); - let theme_names = app_state .themes - .list( - settings.staff_mode, - settings.experiments.experimental_themes, - ) + .list(**cx.default_global::()) .map(|meta| meta.name) .collect(); - let language_names = &languages::LANGUAGE_NAMES; + let language_names = app_state.languages.language_names(); workspace.project().update(cx, |project, cx| { let action_names = cx.all_action_names().collect::>(); @@ -314,7 +322,7 @@ pub fn initialize_workspace( "schemas": [ { "fileMatch": [schema_file_match(&paths::SETTINGS)], - "schema": settings_file_json_schema(theme_names, language_names), + "schema": settings_file_json_schema(theme_names, &language_names), }, { "fileMatch": [schema_file_match(&paths::KEYMAP)], @@ -344,7 +352,8 @@ pub fn initialize_workspace( let activity_indicator = activity_indicator::ActivityIndicator::new(workspace, app_state.languages.clone(), cx); let cursor_position = cx.add_view(|_| editor::items::CursorPosition::new()); - let feedback_button = cx.add_view(|_| feedback::feedback_editor::FeedbackButton {}); + let feedback_button = + cx.add_view(|_| feedback::deploy_feedback_button::DeployFeedbackButton {}); workspace.status_bar().update(cx, |status_bar, cx| { status_bar.add_left_item(diagnostic_summary, cx); status_bar.add_left_item(activity_indicator, cx); @@ -355,7 +364,7 @@ pub fn initialize_workspace( auto_update::notify_of_any_new_update(cx.weak_handle(), cx); let window_id = cx.window_id(); - vim::observe_keypresses(window_id, cx); + vim::observe_keystrokes(window_id, cx); cx.on_window_should_close(|workspace, cx| { if let Some(task) = workspace.close(&Default::default(), cx) { @@ -365,14 +374,22 @@ pub fn initialize_workspace( }); } -pub fn build_window_options() -> WindowOptions<'static> { - let bounds = if let Some((position, size)) = ZED_WINDOW_POSITION.zip(*ZED_WINDOW_SIZE) { - WindowBounds::Fixed(RectF::new(position, size)) - } else { - WindowBounds::Maximized - }; +pub fn build_window_options( + bounds: Option, + display: Option, + platform: &dyn Platform, +) -> WindowOptions<'static> { + let bounds = bounds + .or_else(|| { + ZED_WINDOW_POSITION + .zip(*ZED_WINDOW_SIZE) + .map(|(position, size)| WindowBounds::Fixed(RectF::new(position, size))) + }) + .unwrap_or(WindowBounds::Maximized); + + let screen = display.and_then(|display| platform.screen_by_id(display)); + WindowOptions { - bounds, titlebar: Some(TitlebarOptions { title: None, appears_transparent: true, @@ -382,10 +399,15 @@ pub fn build_window_options() -> WindowOptions<'static> { focus: true, kind: WindowKind::Normal, is_movable: true, - screen: None, + bounds, + screen, } } +fn restart(_: &Restart, cx: &mut gpui::MutableAppContext) { + cx.platform().restart(); +} + fn quit(_: &Quit, cx: &mut gpui::MutableAppContext) { let mut workspaces = cx .window_ids() @@ -597,13 +619,13 @@ fn open_telemetry_log_file( .update(cx, |project, cx| project.create_buffer("", None, cx)) .expect("creating buffers on a local workspace always succeeds"); buffer.update(cx, |buffer, cx| { - buffer.set_language(app_state.languages.get_language("JSON"), cx); + buffer.set_language(app_state.languages.language_for_name("JSON"), cx); buffer.edit( [( 0..0, concat!( "// Zed collects anonymous usage data to help us understand how people are using the app.\n", - "// After the beta release, we'll provide the ability to opt out of this telemetry.\n", + "// Telemetry can be disabled via the `settings.json` file.\n", "// Here is the data that has been reported for the current session:\n", "\n" ), @@ -646,7 +668,7 @@ fn open_bundled_file( .unwrap_or_else(|| Cow::Borrowed(b"File not found")); let text = str::from_utf8(text.as_ref()).unwrap(); project - .create_buffer(text, project.languages().get_language(language), cx) + .create_buffer(text, project.languages().language_for_name(language), cx) .expect("creating buffers on a local workspace always succeeds") }); let buffer = @@ -1854,7 +1876,7 @@ mod tests { let settings = Settings::defaults(Assets, cx.font_cache(), &themes); let mut has_default_theme = false; - for theme_name in themes.list(false, false).map(|meta| meta.name) { + 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 { has_default_theme = true; diff --git a/script/bundle b/script/bundle index 94efbdf0af..6fe93ed66c 100755 --- a/script/bundle +++ b/script/bundle @@ -22,7 +22,7 @@ cargo build --release --package cli --target x86_64-apple-darwin echo "Creating application bundle" pushd crates/zed -channel=$(cat RELEASE_CHANNEL) +channel=$(/{while(getline line<\"./crates/zed/BundleDocumentTypes.plist\"){print line}}1" \ + "${app_path}/Contents/WithoutDocumentTypes.plist" \ + > "${app_path}/Contents/Info.plist" +rm "${app_path}/Contents/WithoutDocumentTypes.plist" + if [[ -n $MACOS_CERTIFICATE && -n $MACOS_CERTIFICATE_PASSWORD && -n $APPLE_NOTARIZATION_USERNAME && -n $APPLE_NOTARIZATION_PASSWORD ]]; then echo "Signing bundle with Apple-issued certificate" security create-keychain -p "$MACOS_CERTIFICATE_PASSWORD" zed.keychain || echo "" diff --git a/script/discourse_release b/script/discourse_release deleted file mode 100755 index c233bf1872..0000000000 --- a/script/discourse_release +++ /dev/null @@ -1,38 +0,0 @@ -#!/usr/bin/env node --redirect-warnings=/dev/null - -main(); - -async function main() { - const apiKey = process.argv[2] - const zedVersion = process.argv[3] - const releaseNotes = process.argv[4] - const postBody = ` - 📣 Zed ${zedVersion} was just released! - - Restart your Zed or head to the [releases page](https://zed.dev/releases/latest) to grab it. - - --- - - ${releaseNotes} - ` - - const title = `${zedVersion} Release Notes` - - const options = { - method: "POST", - headers: { - "Api-Key": apiKey, - "Api-Username": "system" - }, - body: new URLSearchParams({ - title: title, - raw: postBody, - category: "8" - }) - }; - - fetch("https://forum.zed.dev/posts.json", options) - .then(response => response.json()) - .then(response => console.log(response)) - .catch(err => console.error(err)); -} \ No newline at end of file diff --git a/script/generate-licenses b/script/generate-licenses index e1a917292c..8a41f55c02 100755 --- a/script/generate-licenses +++ b/script/generate-licenses @@ -2,14 +2,28 @@ set -e +OUTPUT_FILE=$(pwd)/assets/licenses.md + +> $OUTPUT_FILE + +echo -e "# ###### THEME LICENSES ######\n" >> $OUTPUT_FILE + +echo "Generating theme licenses" +cd styles +npm ci +npm run --silent build-licenses >> $OUTPUT_FILE +cd .. + +echo -e "# ###### CODE LICENSES ######\n" >> $OUTPUT_FILE + [[ "$(cargo about --version)" == "cargo-about 0.5.2" ]] || cargo install cargo-about --locked --git https://github.com/zed-industries/cargo-about --branch error-code-on-warn -cargo about generate --fail-on-missing-license -o assets/licenses.md -c script/licenses/zed-licenses.toml script/licenses/template.hbs.md +echo "Generating cargo licenses" +cargo about generate --fail-on-missing-license -c script/licenses/zed-licenses.toml script/licenses/template.hbs.md >> $OUTPUT_FILE -# cargo about automatically html-escapes all output, so we need to undo it here: -sed -i '' 's/"/"/g' assets/licenses.md -sed -i '' 's/'/'\''/g' assets/licenses.md # `'\''` ends the string, appends a single quote, and re-opens the string -sed -i '' 's/=/=/g' assets/licenses.md -sed -i '' 's/`/`/g' assets/licenses.md -sed -i '' 's/<//g' assets/licenses.md \ No newline at end of file +sed -i '' 's/"/"/g' $OUTPUT_FILE +sed -i '' 's/'/'\''/g' $OUTPUT_FILE # The ` '\'' ` thing ends the string, appends a single quote, and re-opens the string +sed -i '' 's/=/=/g' $OUTPUT_FILE +sed -i '' 's/`/`/g' $OUTPUT_FILE +sed -i '' 's/<//g' $OUTPUT_FILE \ No newline at end of file diff --git a/script/licenses/template.hbs.md b/script/licenses/template.hbs.md index a51b714dae..a41aee8a4c 100644 --- a/script/licenses/template.hbs.md +++ b/script/licenses/template.hbs.md @@ -1,20 +1,15 @@ -# Third Party Licenses - -This page lists the licenses of the projects used in Zed. - ## Overview of licenses: {{#each overview}} * {{name}} ({{count}}) {{/each}} -## All license texts: - +### All license texts: {{#each licenses}} -### {{name}} +#### {{name}} -#### Used by: +##### Used by: {{#each used_by}} * [{{crate.name}} {{crate.version}}]({{#if crate.repository}} {{crate.repository}} {{else}} https://crates.io/crates/{{crate.name}} {{/if}}) @@ -23,5 +18,4 @@ This page lists the licenses of the projects used in Zed. {{text}} -------------------------------------------------------------------------------- - {{/each}} \ No newline at end of file diff --git a/script/licenses/zed-licenses.toml b/script/licenses/zed-licenses.toml index d338e7ab0b..e166b653c8 100644 --- a/script/licenses/zed-licenses.toml +++ b/script/licenses/zed-licenses.toml @@ -1,3 +1,5 @@ +# NOTE: This file's location is hardcoded into the theme build system in +# styles/src/buildLicenses.ts no-clearly-defined = true private = { ignore = true } accepted = [ diff --git a/script/start-local-collaboration b/script/start-local-collaboration index 82341bf6db..168ecf7a23 100755 --- a/script/start-local-collaboration +++ b/script/start-local-collaboration @@ -31,9 +31,10 @@ scale_factor=1 if [[ $resolution_line =~ Retina ]]; then scale_factor=2; fi width=$(expr ${screen_size[0]} / 2 / $scale_factor) height=${screen_size[1] / $scale_factor} +y=$(expr $height / 2) -position_1=0,0 -position_2=${width},0 +position_1=0,${y} +position_2=${width},${y} # Authenticate using the collab server's admin secret. export ZED_STATELESS=1 diff --git a/styles/package-lock.json b/styles/package-lock.json index 582f1c8496..b0a904b11d 100644 --- a/styles/package-lock.json +++ b/styles/package-lock.json @@ -1,316 +1,327 @@ { - "name": "styles", - "version": "1.0.0", - "lockfileVersion": 2, - "requires": true, - "packages": { - "": { - "name": "styles", - "version": "1.0.0", - "license": "ISC", - "dependencies": { - "@types/chroma-js": "^2.1.3", - "@types/node": "^17.0.23", - "case-anything": "^2.1.10", - "chroma-js": "^2.4.2", - "ts-node": "^10.7.0" - } - }, - "node_modules/@cspotcode/source-map-consumer": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz", - "integrity": "sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg==", - "engines": { - "node": ">= 12" - } - }, - "node_modules/@cspotcode/source-map-support": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz", - "integrity": "sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA==", - "dependencies": { - "@cspotcode/source-map-consumer": "0.8.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@tsconfig/node10": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.8.tgz", - "integrity": "sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg==" - }, - "node_modules/@tsconfig/node12": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.9.tgz", - "integrity": "sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw==" - }, - "node_modules/@tsconfig/node14": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.1.tgz", - "integrity": "sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg==" - }, - "node_modules/@tsconfig/node16": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.2.tgz", - "integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==" - }, - "node_modules/@types/chroma-js": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@types/chroma-js/-/chroma-js-2.1.3.tgz", - "integrity": "sha512-1xGPhoSGY1CPmXLCBcjVZSQinFjL26vlR8ZqprsBWiFyED4JacJJ9zHhh5aaUXqbY9B37mKQ73nlydVAXmr1+g==" - }, - "node_modules/@types/node": { - "version": "17.0.23", - "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.23.tgz", - "integrity": "sha512-UxDxWn7dl97rKVeVS61vErvw086aCYhDLyvRQZ5Rk65rZKepaFdm53GeqXaKBuOhED4e9uWq34IC3TdSdJJ2Gw==" - }, - "node_modules/acorn": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz", - "integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-walk": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", - "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/arg": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", - "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==" - }, - "node_modules/case-anything": { - "version": "2.1.10", - "resolved": "https://registry.npmjs.org/case-anything/-/case-anything-2.1.10.tgz", - "integrity": "sha512-JczJwVrCP0jPKh05McyVsuOg6AYosrB9XWZKbQzXeDAm2ClE/PJE/BcrrQrVyGYH7Jg8V/LDupmyL4kFlVsVFQ==", - "engines": { - "node": ">=12.13" - }, - "funding": { - "url": "https://github.com/sponsors/mesqueeb" - } - }, - "node_modules/chroma-js": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chroma-js/-/chroma-js-2.4.2.tgz", - "integrity": "sha512-U9eDw6+wt7V8z5NncY2jJfZa+hUH8XEj8FQHgFJTrUFnJfXYf4Ml4adI2vXZOjqRDpFWtYVWypDfZwnJ+HIR4A==" - }, - "node_modules/create-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", - "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==" - }, - "node_modules/diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/make-error": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", - "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==" - }, - "node_modules/ts-node": { - "version": "10.7.0", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.7.0.tgz", - "integrity": "sha512-TbIGS4xgJoX2i3do417KSaep1uRAW/Lu+WAL2doDHC0D6ummjirVOXU5/7aiZotbQ5p1Zp9tP7U6cYhA0O7M8A==", - "dependencies": { - "@cspotcode/source-map-support": "0.7.0", - "@tsconfig/node10": "^1.0.7", - "@tsconfig/node12": "^1.0.7", - "@tsconfig/node14": "^1.0.0", - "@tsconfig/node16": "^1.0.2", - "acorn": "^8.4.1", - "acorn-walk": "^8.1.1", - "arg": "^4.1.0", - "create-require": "^1.1.0", - "diff": "^4.0.1", - "make-error": "^1.1.1", - "v8-compile-cache-lib": "^3.0.0", - "yn": "3.1.1" - }, - "bin": { - "ts-node": "dist/bin.js", - "ts-node-cwd": "dist/bin-cwd.js", - "ts-node-esm": "dist/bin-esm.js", - "ts-node-script": "dist/bin-script.js", - "ts-node-transpile-only": "dist/bin-transpile.js", - "ts-script": "dist/bin-script-deprecated.js" - }, - "peerDependencies": { - "@swc/core": ">=1.2.50", - "@swc/wasm": ">=1.2.50", - "@types/node": "*", - "typescript": ">=2.7" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true + "name": "styles", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "styles", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@types/chroma-js": "^2.1.3", + "@types/node": "^17.0.23", + "case-anything": "^2.1.10", + "chroma-js": "^2.4.2", + "toml": "^3.0.0", + "ts-node": "^10.7.0" + } }, - "@swc/wasm": { - "optional": true + "node_modules/@cspotcode/source-map-consumer": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz", + "integrity": "sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg==", + "engines": { + "node": ">= 12" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz", + "integrity": "sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA==", + "dependencies": { + "@cspotcode/source-map-consumer": "0.8.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.8.tgz", + "integrity": "sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg==" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.9.tgz", + "integrity": "sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw==" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.1.tgz", + "integrity": "sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg==" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.2.tgz", + "integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==" + }, + "node_modules/@types/chroma-js": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@types/chroma-js/-/chroma-js-2.1.3.tgz", + "integrity": "sha512-1xGPhoSGY1CPmXLCBcjVZSQinFjL26vlR8ZqprsBWiFyED4JacJJ9zHhh5aaUXqbY9B37mKQ73nlydVAXmr1+g==" + }, + "node_modules/@types/node": { + "version": "17.0.23", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.23.tgz", + "integrity": "sha512-UxDxWn7dl97rKVeVS61vErvw086aCYhDLyvRQZ5Rk65rZKepaFdm53GeqXaKBuOhED4e9uWq34IC3TdSdJJ2Gw==" + }, + "node_modules/acorn": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz", + "integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==" + }, + "node_modules/case-anything": { + "version": "2.1.10", + "resolved": "https://registry.npmjs.org/case-anything/-/case-anything-2.1.10.tgz", + "integrity": "sha512-JczJwVrCP0jPKh05McyVsuOg6AYosrB9XWZKbQzXeDAm2ClE/PJE/BcrrQrVyGYH7Jg8V/LDupmyL4kFlVsVFQ==", + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/chroma-js": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chroma-js/-/chroma-js-2.4.2.tgz", + "integrity": "sha512-U9eDw6+wt7V8z5NncY2jJfZa+hUH8XEj8FQHgFJTrUFnJfXYf4Ml4adI2vXZOjqRDpFWtYVWypDfZwnJ+HIR4A==" + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==" + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==" + }, + "node_modules/toml": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/toml/-/toml-3.0.0.tgz", + "integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==" + }, + "node_modules/ts-node": { + "version": "10.7.0", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.7.0.tgz", + "integrity": "sha512-TbIGS4xgJoX2i3do417KSaep1uRAW/Lu+WAL2doDHC0D6ummjirVOXU5/7aiZotbQ5p1Zp9tP7U6cYhA0O7M8A==", + "dependencies": { + "@cspotcode/source-map-support": "0.7.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.0", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/typescript": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.3.tgz", + "integrity": "sha512-yNIatDa5iaofVozS/uQJEl3JRWLKKGJKh6Yaiv0GLGSuhpFJe7P3SbHZ8/yjAHRQwKRoA6YZqlfjXWmVzoVSMw==", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.0.tgz", + "integrity": "sha512-mpSYqfsFvASnSn5qMiwrr4VKfumbPyONLCOPmsR3A6pTY/r0+tSaVbgPWSAIuzbk3lCTa+FForeTiO+wBQGkjA==" + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "engines": { + "node": ">=6" + } } - } }, - "node_modules/typescript": { - "version": "4.6.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.3.tgz", - "integrity": "sha512-yNIatDa5iaofVozS/uQJEl3JRWLKKGJKh6Yaiv0GLGSuhpFJe7P3SbHZ8/yjAHRQwKRoA6YZqlfjXWmVzoVSMw==", - "peer": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=4.2.0" - } - }, - "node_modules/v8-compile-cache-lib": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.0.tgz", - "integrity": "sha512-mpSYqfsFvASnSn5qMiwrr4VKfumbPyONLCOPmsR3A6pTY/r0+tSaVbgPWSAIuzbk3lCTa+FForeTiO+wBQGkjA==" - }, - "node_modules/yn": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", - "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "engines": { - "node": ">=6" - } + "dependencies": { + "@cspotcode/source-map-consumer": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz", + "integrity": "sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg==" + }, + "@cspotcode/source-map-support": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz", + "integrity": "sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA==", + "requires": { + "@cspotcode/source-map-consumer": "0.8.0" + } + }, + "@tsconfig/node10": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.8.tgz", + "integrity": "sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg==" + }, + "@tsconfig/node12": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.9.tgz", + "integrity": "sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw==" + }, + "@tsconfig/node14": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.1.tgz", + "integrity": "sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg==" + }, + "@tsconfig/node16": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.2.tgz", + "integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==" + }, + "@types/chroma-js": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@types/chroma-js/-/chroma-js-2.1.3.tgz", + "integrity": "sha512-1xGPhoSGY1CPmXLCBcjVZSQinFjL26vlR8ZqprsBWiFyED4JacJJ9zHhh5aaUXqbY9B37mKQ73nlydVAXmr1+g==" + }, + "@types/node": { + "version": "17.0.23", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.23.tgz", + "integrity": "sha512-UxDxWn7dl97rKVeVS61vErvw086aCYhDLyvRQZ5Rk65rZKepaFdm53GeqXaKBuOhED4e9uWq34IC3TdSdJJ2Gw==" + }, + "acorn": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz", + "integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==" + }, + "acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==" + }, + "arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==" + }, + "case-anything": { + "version": "2.1.10", + "resolved": "https://registry.npmjs.org/case-anything/-/case-anything-2.1.10.tgz", + "integrity": "sha512-JczJwVrCP0jPKh05McyVsuOg6AYosrB9XWZKbQzXeDAm2ClE/PJE/BcrrQrVyGYH7Jg8V/LDupmyL4kFlVsVFQ==" + }, + "chroma-js": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chroma-js/-/chroma-js-2.4.2.tgz", + "integrity": "sha512-U9eDw6+wt7V8z5NncY2jJfZa+hUH8XEj8FQHgFJTrUFnJfXYf4Ml4adI2vXZOjqRDpFWtYVWypDfZwnJ+HIR4A==" + }, + "create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==" + }, + "diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==" + }, + "make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==" + }, + "toml": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/toml/-/toml-3.0.0.tgz", + "integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==" + }, + "ts-node": { + "version": "10.7.0", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.7.0.tgz", + "integrity": "sha512-TbIGS4xgJoX2i3do417KSaep1uRAW/Lu+WAL2doDHC0D6ummjirVOXU5/7aiZotbQ5p1Zp9tP7U6cYhA0O7M8A==", + "requires": { + "@cspotcode/source-map-support": "0.7.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.0", + "yn": "3.1.1" + } + }, + "typescript": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.3.tgz", + "integrity": "sha512-yNIatDa5iaofVozS/uQJEl3JRWLKKGJKh6Yaiv0GLGSuhpFJe7P3SbHZ8/yjAHRQwKRoA6YZqlfjXWmVzoVSMw==", + "peer": true + }, + "v8-compile-cache-lib": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.0.tgz", + "integrity": "sha512-mpSYqfsFvASnSn5qMiwrr4VKfumbPyONLCOPmsR3A6pTY/r0+tSaVbgPWSAIuzbk3lCTa+FForeTiO+wBQGkjA==" + }, + "yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==" + } } - }, - "dependencies": { - "@cspotcode/source-map-consumer": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz", - "integrity": "sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg==" - }, - "@cspotcode/source-map-support": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz", - "integrity": "sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA==", - "requires": { - "@cspotcode/source-map-consumer": "0.8.0" - } - }, - "@tsconfig/node10": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.8.tgz", - "integrity": "sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg==" - }, - "@tsconfig/node12": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.9.tgz", - "integrity": "sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw==" - }, - "@tsconfig/node14": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.1.tgz", - "integrity": "sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg==" - }, - "@tsconfig/node16": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.2.tgz", - "integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==" - }, - "@types/chroma-js": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@types/chroma-js/-/chroma-js-2.1.3.tgz", - "integrity": "sha512-1xGPhoSGY1CPmXLCBcjVZSQinFjL26vlR8ZqprsBWiFyED4JacJJ9zHhh5aaUXqbY9B37mKQ73nlydVAXmr1+g==" - }, - "@types/node": { - "version": "17.0.23", - "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.23.tgz", - "integrity": "sha512-UxDxWn7dl97rKVeVS61vErvw086aCYhDLyvRQZ5Rk65rZKepaFdm53GeqXaKBuOhED4e9uWq34IC3TdSdJJ2Gw==" - }, - "acorn": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz", - "integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==" - }, - "acorn-walk": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", - "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==" - }, - "arg": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", - "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==" - }, - "case-anything": { - "version": "2.1.10", - "resolved": "https://registry.npmjs.org/case-anything/-/case-anything-2.1.10.tgz", - "integrity": "sha512-JczJwVrCP0jPKh05McyVsuOg6AYosrB9XWZKbQzXeDAm2ClE/PJE/BcrrQrVyGYH7Jg8V/LDupmyL4kFlVsVFQ==" - }, - "chroma-js": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chroma-js/-/chroma-js-2.4.2.tgz", - "integrity": "sha512-U9eDw6+wt7V8z5NncY2jJfZa+hUH8XEj8FQHgFJTrUFnJfXYf4Ml4adI2vXZOjqRDpFWtYVWypDfZwnJ+HIR4A==" - }, - "create-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", - "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==" - }, - "diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==" - }, - "make-error": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", - "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==" - }, - "ts-node": { - "version": "10.7.0", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.7.0.tgz", - "integrity": "sha512-TbIGS4xgJoX2i3do417KSaep1uRAW/Lu+WAL2doDHC0D6ummjirVOXU5/7aiZotbQ5p1Zp9tP7U6cYhA0O7M8A==", - "requires": { - "@cspotcode/source-map-support": "0.7.0", - "@tsconfig/node10": "^1.0.7", - "@tsconfig/node12": "^1.0.7", - "@tsconfig/node14": "^1.0.0", - "@tsconfig/node16": "^1.0.2", - "acorn": "^8.4.1", - "acorn-walk": "^8.1.1", - "arg": "^4.1.0", - "create-require": "^1.1.0", - "diff": "^4.0.1", - "make-error": "^1.1.1", - "v8-compile-cache-lib": "^3.0.0", - "yn": "3.1.1" - } - }, - "typescript": { - "version": "4.6.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.3.tgz", - "integrity": "sha512-yNIatDa5iaofVozS/uQJEl3JRWLKKGJKh6Yaiv0GLGSuhpFJe7P3SbHZ8/yjAHRQwKRoA6YZqlfjXWmVzoVSMw==", - "peer": true - }, - "v8-compile-cache-lib": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.0.tgz", - "integrity": "sha512-mpSYqfsFvASnSn5qMiwrr4VKfumbPyONLCOPmsR3A6pTY/r0+tSaVbgPWSAIuzbk3lCTa+FForeTiO+wBQGkjA==" - }, - "yn": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", - "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==" - } - } } diff --git a/styles/package.json b/styles/package.json index 11bcbadf73..118269bc81 100644 --- a/styles/package.json +++ b/styles/package.json @@ -4,7 +4,8 @@ "description": "", "main": "index.js", "scripts": { - "build": "ts-node ./src/buildThemes.ts" + "build": "ts-node ./src/buildThemes.ts", + "build-licenses": "ts-node ./src/buildLicenses.ts" }, "author": "", "license": "ISC", @@ -13,6 +14,7 @@ "@types/node": "^17.0.23", "case-anything": "^2.1.10", "chroma-js": "^2.4.2", + "toml": "^3.0.0", "ts-node": "^10.7.0" } } diff --git a/styles/src/buildLicenses.ts b/styles/src/buildLicenses.ts new file mode 100644 index 0000000000..5026faef4e --- /dev/null +++ b/styles/src/buildLicenses.ts @@ -0,0 +1,73 @@ +import * as fs from "fs"; +import toml from "toml"; +import { + schemeMeta +} from "./colorSchemes"; +import { Meta } from "./themes/common/colorScheme"; +import https from "https"; +import crypto from "crypto"; + +const accepted_licenses_file = `${__dirname}/../../script/licenses/zed-licenses.toml` + +// Use the cargo-about configuration file as the source of truth for supported licenses. +function parseAcceptedToml(file: string): string[] { + let buffer = fs.readFileSync(file).toString(); + + let obj = toml.parse(buffer); + + if (!Array.isArray(obj.accepted)) { + throw Error("Accepted license source is malformed") + } + + return obj.accepted +} + + +function checkLicenses(schemeMeta: Meta[], licenses: string[]) { + for (let meta of schemeMeta) { + // FIXME: Add support for conjuctions and conditions + if (licenses.indexOf(meta.license.SPDX) < 0) { + throw Error(`License for theme ${meta.name} (${meta.license.SPDX}) is not supported`) + } + } +} + + +function getLicenseText(schemeMeta: Meta[], callback: (meta: Meta, license_text: string) => void) { + for (let meta of schemeMeta) { + // The following copied from the example code on nodejs.org: + // https://nodejs.org/api/http.html#httpgetoptions-callback + https.get(meta.license.https_url, (res) => { + const { statusCode } = res; + + if (statusCode < 200 || statusCode >= 300) { + throw new Error(`Failed to fetch license for: ${meta.name}, Status Code: ${statusCode}`); + } + + res.setEncoding('utf8'); + let rawData = ''; + res.on('data', (chunk) => { rawData += chunk; }); + res.on('end', () => { + const hash = crypto.createHash('sha256').update(rawData).digest('hex'); + if (meta.license.license_checksum == hash) { + callback(meta, rawData) + } else { + throw Error(`Checksum for ${meta.name} did not match file downloaded from ${meta.license.https_url}`) + } + }); + }).on('error', (e) => { + throw e + }); + } +} + +function writeLicense(schemeMeta: Meta, text: String) { + process.stdout.write(`## [${schemeMeta.name}](${schemeMeta.url})\n\n${text}\n********************************************************************************\n\n`) +} + +const accepted_licenses = parseAcceptedToml(accepted_licenses_file); +checkLicenses(schemeMeta, accepted_licenses) + +getLicenseText(schemeMeta, (meta, text) => { + writeLicense(meta, text) +}); diff --git a/styles/src/buildThemes.ts b/styles/src/buildThemes.ts index 32749a7aaa..4bb7b8fc09 100644 --- a/styles/src/buildThemes.ts +++ b/styles/src/buildThemes.ts @@ -1,17 +1,16 @@ import * as fs from "fs"; -import * as path from "path"; import { tmpdir } from "os"; -import app from "./styleTree/app"; +import * as path from "path"; import colorSchemes, { - internalColorSchemes, - experimentalColorSchemes, + staffColorSchemes, } from "./colorSchemes"; -import snakeCase from "./utils/snakeCase"; +import app from "./styleTree/app"; import { ColorScheme } from "./themes/common/colorScheme"; +import snakeCase from "./utils/snakeCase"; -const themeDirectory = `${__dirname}/../../assets/themes`; -const internalDirectory = `${themeDirectory}/Internal`; -const experimentsDirectory = `${themeDirectory}/Experiments`; +const assetsDirectory = `${__dirname}/../../assets` +const themeDirectory = `${assetsDirectory}/themes`; +const staffDirectory = `${themeDirectory}/staff`; const tempDirectory = fs.mkdtempSync(path.join(tmpdir(), "build-themes")); @@ -32,8 +31,7 @@ function clearThemes(themeDirectory: string) { } clearThemes(themeDirectory); -clearThemes(internalDirectory); -clearThemes(experimentsDirectory); +clearThemes(staffDirectory); function writeThemes(colorSchemes: ColorScheme[], outputDirectory: string) { for (let colorScheme of colorSchemes) { @@ -49,5 +47,4 @@ function writeThemes(colorSchemes: ColorScheme[], outputDirectory: string) { // Write new themes to theme directory writeThemes(colorSchemes, themeDirectory); -writeThemes(internalColorSchemes, internalDirectory); -writeThemes(experimentalColorSchemes, experimentsDirectory); +writeThemes(staffColorSchemes, staffDirectory); diff --git a/styles/src/colorSchemes.ts b/styles/src/colorSchemes.ts index 746443119d..c7e1d4ead7 100644 --- a/styles/src/colorSchemes.ts +++ b/styles/src/colorSchemes.ts @@ -1,35 +1,54 @@ import fs from "fs"; import path from "path"; -import { ColorScheme } from "./themes/common/colorScheme"; +import { ColorScheme, Meta } from "./themes/common/colorScheme"; const colorSchemes: ColorScheme[] = []; export default colorSchemes; -const internalColorSchemes: ColorScheme[] = []; -export { internalColorSchemes }; +const schemeMeta: Meta[] = []; +export { schemeMeta }; + +const staffColorSchemes: ColorScheme[] = []; +export { staffColorSchemes }; const experimentalColorSchemes: ColorScheme[] = []; export { experimentalColorSchemes }; -function fillColorSchemes(themesPath: string, colorSchemes: ColorScheme[]) { +const themes_directory = path.resolve(`${__dirname}/themes`); + +function for_all_color_schemes_in(themesPath: string, callback: (module: any, path: string) => void) { for (const fileName of fs.readdirSync(themesPath)) { if (fileName == "template.ts") continue; const filePath = path.join(themesPath, fileName); if (fs.statSync(filePath).isFile()) { const colorScheme = require(filePath); - if (colorScheme.dark) colorSchemes.push(colorScheme.dark); - if (colorScheme.light) colorSchemes.push(colorScheme.light); + callback(colorScheme, path.basename(filePath)); } } } -fillColorSchemes(path.resolve(`${__dirname}/themes`), colorSchemes); +function fillColorSchemes(themesPath: string, colorSchemes: ColorScheme[]) { + for_all_color_schemes_in(themesPath, (colorScheme, _path) => { + if (colorScheme.dark) colorSchemes.push(colorScheme.dark); + if (colorScheme.light) colorSchemes.push(colorScheme.light); + }) +} + +fillColorSchemes(themes_directory, colorSchemes); fillColorSchemes( - path.resolve(`${__dirname}/themes/internal`), - internalColorSchemes -); -fillColorSchemes( - path.resolve(`${__dirname}/themes/experiments`), - experimentalColorSchemes + path.resolve(`${themes_directory}/staff`), + staffColorSchemes ); + +function fillMeta(themesPath: string, meta: Meta[]) { + for_all_color_schemes_in(themesPath, (colorScheme, path) => { + if (colorScheme.meta) { + meta.push(colorScheme.meta) + } else { + throw Error(`Public theme ${path} must have a meta field`) + } + }) +} + +fillMeta(themes_directory, schemeMeta); diff --git a/styles/src/styleTree/app.ts b/styles/src/styleTree/app.ts index 267d830506..5d04050fe1 100644 --- a/styles/src/styleTree/app.ts +++ b/styles/src/styleTree/app.ts @@ -19,6 +19,7 @@ import terminal from "./terminal"; import contactList from "./contactList"; import incomingCallNotification from "./incomingCallNotification"; import { ColorScheme } from "../themes/common/colorScheme"; +import feedback from "./feedback"; export default function app(colorScheme: ColorScheme): Object { return { @@ -51,6 +52,7 @@ export default function app(colorScheme: ColorScheme): Object { simpleMessageNotification: simpleMessageNotification(colorScheme), tooltip: tooltip(colorScheme), terminal: terminal(colorScheme), + feedback: feedback(colorScheme), colorScheme: { ...colorScheme, players: Object.values(colorScheme.players), diff --git a/styles/src/styleTree/feedback.ts b/styles/src/styleTree/feedback.ts new file mode 100644 index 0000000000..46cb867ad9 --- /dev/null +++ b/styles/src/styleTree/feedback.ts @@ -0,0 +1,37 @@ + +import { ColorScheme } from "../themes/common/colorScheme"; +import { background, border, text } from "./components"; + +export default function feedback(colorScheme: ColorScheme) { + let layer = colorScheme.highest; + + return { + submit_button: { + ...text(layer, "mono", "on"), + background: background(layer, "on"), + cornerRadius: 6, + border: border(layer, "on"), + margin: { + right: 4, + }, + padding: { + bottom: 2, + left: 10, + right: 10, + top: 2, + }, + clicked: { + ...text(layer, "mono", "on", "pressed"), + background: background(layer, "on", "pressed"), + border: border(layer, "on", "pressed"), + }, + hover: { + ...text(layer, "mono", "on", "hovered"), + background: background(layer, "on", "hovered"), + border: border(layer, "on", "hovered"), + }, + }, + button_margin: 8, + info_text: text(layer, "sans", "default", { size: "xs" }), + }; +} diff --git a/styles/src/themes/andromeda.ts b/styles/src/themes/andromeda.ts index 520ceb67fe..b76179b3c5 100644 --- a/styles/src/themes/andromeda.ts +++ b/styles/src/themes/andromeda.ts @@ -1,13 +1,8 @@ import chroma from "chroma-js"; +import { Meta } from "./common/colorScheme"; import { colorRamp, createColorScheme } from "./common/ramps"; const name = "Andromeda"; -const author = "EliverLara"; -const url = "https://github.com/EliverLara/Andromeda"; -const license = { - type: "MIT", - url: "https://github.com/EliverLara/Andromeda/blob/master/LICENSE.md", -}; const ramps = { neutral: chroma @@ -33,3 +28,14 @@ const ramps = { }; export const dark = createColorScheme(`${name}`, false, ramps); + +export const meta: Meta = { + name, + author: "EliverLara", + license: { + SPDX: "MIT", + https_url: "https://raw.githubusercontent.com/EliverLara/Andromeda/master/LICENSE.md", + license_checksum: "2f7886f1a05cefc2c26f5e49de1a39fa4466413c1ccb06fc80960e73f5ed4b89" + }, + url: "https://github.com/EliverLara/Andromeda" +} \ No newline at end of file diff --git a/styles/src/themes/atelier-cave.ts b/styles/src/themes/atelier-cave.ts index 98cf834704..0959cabace 100644 --- a/styles/src/themes/atelier-cave.ts +++ b/styles/src/themes/atelier-cave.ts @@ -1,13 +1,8 @@ import chroma from "chroma-js"; +import { Meta } from "./common/colorScheme"; import { colorRamp, createColorScheme } from "./common/ramps"; const name = "Atelier Cave"; -const author = "atelierbram"; -const url = "https://atelierbram.github.io/syntax-highlighting/atelier-schemes/cave/"; -const license = { - type: "MIT", - url: "https://github.com/atelierbram/syntax-highlighting/blob/master/LICENSE", -}; export const dark = createColorScheme(`${name} Dark`, false, { neutral: chroma @@ -54,3 +49,15 @@ export const light = createColorScheme(`${name} Light`, true, { violet: colorRamp(chroma("#955ae7")), magenta: colorRamp(chroma("#bf40bf")), }); + + +export const meta: Meta = { + name, + author: "atelierbram", + license: { + SPDX: "MIT", + https_url: "https://atelierbram.mit-license.org/license.txt", + license_checksum: "f95ce526ef4e7eecf7a832bba0e3451cc1000f9ce63eb01ed6f64f8109f5d0a5" + }, + url: "https://atelierbram.github.io/syntax-highlighting/atelier-schemes/cave/" +} \ No newline at end of file diff --git a/styles/src/themes/atelier-sulphurpool.ts b/styles/src/themes/atelier-sulphurpool.ts index d8293db3a7..fa51b1ec80 100644 --- a/styles/src/themes/atelier-sulphurpool.ts +++ b/styles/src/themes/atelier-sulphurpool.ts @@ -1,13 +1,8 @@ import chroma from "chroma-js"; +import { Meta } from "./common/colorScheme"; import { colorRamp, createColorScheme } from "./common/ramps"; const name = "Atelier Sulphurpool"; -const author = "atelierbram"; -const url = "https://atelierbram.github.io/syntax-highlighting/atelier-schemes/dune/"; -const license = { - type: "MIT", - url: "https://github.com/atelierbram/syntax-highlighting/blob/master/LICENSE", -}; const ramps = { neutral: chroma @@ -34,3 +29,14 @@ const ramps = { export const dark = createColorScheme(`${name} Dark`, false, ramps); export const light = createColorScheme(`${name} Light`, true, ramps); + +export const meta: Meta = { + name, + author: "atelierbram", + license: { + SPDX: "MIT", + https_url: "https://atelierbram.mit-license.org/license.txt", + license_checksum: "f95ce526ef4e7eecf7a832bba0e3451cc1000f9ce63eb01ed6f64f8109f5d0a5" + }, + url: "https://atelierbram.github.io/syntax-highlighting/atelier-schemes/sulphurpool/" +} \ No newline at end of file diff --git a/styles/src/themes/common/base16.ts b/styles/src/themes/common/base16.ts index c5b914d62b..23ccc57fd4 100644 --- a/styles/src/themes/common/base16.ts +++ b/styles/src/themes/common/base16.ts @@ -1,3 +1,6 @@ +// NOTE – This should be removed +// I (Nate) need to come back and check if we are still using this anywhere + import chroma, { Color, Scale } from "chroma-js"; import { fontWeights } from "../../common"; import { withOpacity } from "../../utils/color"; diff --git a/styles/src/themes/common/colorScheme.ts b/styles/src/themes/common/colorScheme.ts index 1b2c2cf7e8..ee858627d7 100644 --- a/styles/src/themes/common/colorScheme.ts +++ b/styles/src/themes/common/colorScheme.ts @@ -16,6 +16,28 @@ export interface ColorScheme { players: Players; } +export interface Meta { + name: string, + author: string, + url: string, + license: License +} + +export interface License { + SPDX: SPDXExpression, + /// A url where we can download the license's text + https_url: string, + license_checksum: string +} + +// License name -> License text +export interface Licenses { + [key: string]: string +} + +// FIXME: Add support for the SPDX expression syntax +export type SPDXExpression = "MIT"; + export interface Player { cursor: string; selection: string; diff --git a/styles/src/themes/internal/.gitkeep b/styles/src/themes/internal/.gitkeep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/styles/src/themes/one-dark.ts b/styles/src/themes/one-dark.ts index 612a71ccc1..42a765e3e3 100644 --- a/styles/src/themes/one-dark.ts +++ b/styles/src/themes/one-dark.ts @@ -1,13 +1,8 @@ import chroma from "chroma-js"; +import { Meta } from "./common/colorScheme"; import { colorRamp, createColorScheme } from "./common/ramps"; const name = "One Dark"; -const author = "simurai"; -const url = "https://github.com/atom/atom/tree/master/packages/one-dark-ui"; -const license = { - type: "MIT", - url: "https://github.com/atom/atom/blob/master/packages/one-dark-ui/LICENSE.md", -}; export const dark = createColorScheme(`${name}`, false, { neutral: chroma @@ -32,3 +27,14 @@ export const dark = createColorScheme(`${name}`, false, { violet: colorRamp(chroma("#c678dd")), magenta: colorRamp(chroma("#be5046")), }); + +export const meta: Meta = { + name, + author: "simurai", + license: { + SPDX: "MIT", + https_url: "https://raw.githubusercontent.com/atom/atom/master/packages/one-light-ui/LICENSE.md", + license_checksum: "d5af8fc171f6f600c0ab4e7597dca398dda80dbe6821ce01cef78e859e7a00f8" + }, + url: "https://github.com/atom/atom/tree/master/packages/one-dark-ui" +} diff --git a/styles/src/themes/one-light.ts b/styles/src/themes/one-light.ts index a5ac1f7158..50f99becdc 100644 --- a/styles/src/themes/one-light.ts +++ b/styles/src/themes/one-light.ts @@ -1,13 +1,8 @@ import chroma from "chroma-js"; +import { Meta } from "./common/colorScheme"; import { colorRamp, createColorScheme } from "./common/ramps"; const name = "One Light"; -const author = "simurai"; -const url = "https://github.com/atom/atom/tree/master/packages/one-light-ui"; -const license = { - type: "MIT", - url: "https://github.com/atom/atom/blob/master/packages/one-light-ui/LICENSE.md", -}; export const light = createColorScheme(`${name}`, true, { neutral: chroma.scale([ @@ -31,3 +26,14 @@ export const light = createColorScheme(`${name}`, true, { violet: colorRamp(chroma("#a626a4")), magenta: colorRamp(chroma("#986801")), }); + +export const meta: Meta = { + name, + author: "simurai", + license: { + SPDX: "MIT", + https_url: "https://raw.githubusercontent.com/atom/atom/master/packages/one-light-ui/LICENSE.md", + license_checksum: "d5af8fc171f6f600c0ab4e7597dca398dda80dbe6821ce01cef78e859e7a00f8" + }, + url: "https://github.com/atom/atom/tree/master/packages/one-light-ui" +} diff --git a/styles/src/themes/rose-pine-dawn.ts b/styles/src/themes/rose-pine-dawn.ts index 20d5dd1ebe..b1744f9c20 100644 --- a/styles/src/themes/rose-pine-dawn.ts +++ b/styles/src/themes/rose-pine-dawn.ts @@ -1,13 +1,8 @@ import chroma from "chroma-js"; +import { Meta } from "./common/colorScheme"; import { colorRamp, createColorScheme } from "./common/ramps"; const name = "Rosé Pine Dawn"; -const author = "edunfelt"; -const url = "https://github.com/edunfelt/base16-rose-pine-scheme"; -const license = { - type: "MIT", - url: "https://github.com/edunfelt/base16-rose-pine-scheme/blob/main/rose-pine-dawn.yaml", -}; const ramps = { neutral: chroma @@ -33,3 +28,14 @@ const ramps = { }; export const light = createColorScheme(`${name}`, true, ramps); + +export const meta: Meta = { + name, + author: "edunfelt", + license: { + SPDX: "MIT", + https_url: "https://raw.githubusercontent.com/edunfelt/base16-rose-pine-scheme/main/LICENSE", + license_checksum: "6ca1b9da8c78c8441c5aa43d024a4e4a7bf59d1ecca1480196e94fda0f91ee4a" + }, + url: "https://github.com/edunfelt/base16-rose-pine-scheme" +} \ No newline at end of file diff --git a/styles/src/themes/rose-pine-moon.ts b/styles/src/themes/rose-pine-moon.ts index 5920357bd3..a4c1737c2b 100644 --- a/styles/src/themes/rose-pine-moon.ts +++ b/styles/src/themes/rose-pine-moon.ts @@ -1,13 +1,8 @@ import chroma from "chroma-js"; +import { Meta } from "./common/colorScheme"; import { colorRamp, createColorScheme } from "./common/ramps"; const name = "Rosé Pine Moon"; -const author = "edunfelt"; -const url = "https://github.com/edunfelt/base16-rose-pine-scheme"; -const license = { - type: "MIT", - url: "https://github.com/edunfelt/base16-rose-pine-scheme/blob/main/rose-pine-moon.yaml", -}; const ramps = { neutral: chroma @@ -33,3 +28,14 @@ const ramps = { }; export const dark = createColorScheme(`${name}`, false, ramps); + +export const meta: Meta = { + name, + author: "edunfelt", + license: { + SPDX: "MIT", + https_url: "https://raw.githubusercontent.com/edunfelt/base16-rose-pine-scheme/main/LICENSE", + license_checksum: "6ca1b9da8c78c8441c5aa43d024a4e4a7bf59d1ecca1480196e94fda0f91ee4a" + }, + url: "https://github.com/edunfelt/base16-rose-pine-scheme" +} \ No newline at end of file diff --git a/styles/src/themes/rose-pine.ts b/styles/src/themes/rose-pine.ts index 9144a136d2..e3c115213b 100644 --- a/styles/src/themes/rose-pine.ts +++ b/styles/src/themes/rose-pine.ts @@ -1,13 +1,8 @@ import chroma from "chroma-js"; +import { Meta } from "./common/colorScheme"; import { colorRamp, createColorScheme } from "./common/ramps"; const name = "Rosé Pine"; -const author = "edunfelt"; -const url = "https://github.com/edunfelt/base16-rose-pine-scheme"; -const license = { - type: "MIT", - url: "https://github.com/edunfelt/base16-rose-pine-scheme", -}; const ramps = { neutral: chroma.scale([ @@ -31,3 +26,14 @@ const ramps = { }; export const dark = createColorScheme(`${name}`, false, ramps); + +export const meta: Meta = { + name, + author: "edunfelt", + license: { + SPDX: "MIT", + https_url: "https://raw.githubusercontent.com/edunfelt/base16-rose-pine-scheme/main/LICENSE", + license_checksum: "6ca1b9da8c78c8441c5aa43d024a4e4a7bf59d1ecca1480196e94fda0f91ee4a" + }, + url: "https://github.com/edunfelt/base16-rose-pine-scheme" +} \ No newline at end of file diff --git a/styles/src/themes/sandcastle.ts b/styles/src/themes/sandcastle.ts index c625ab2986..0e1328feab 100644 --- a/styles/src/themes/sandcastle.ts +++ b/styles/src/themes/sandcastle.ts @@ -1,13 +1,8 @@ import chroma from "chroma-js"; +import { Meta } from "./common/colorScheme"; import { colorRamp, createColorScheme } from "./common/ramps"; const name = "Sandcastle"; -const author = "gessig"; -const url = "https://github.com/gessig/base16-sandcastle-scheme"; -const license = { - type: "MIT", - url: "https://github.com/gessig/base16-sandcastle-scheme/blob/master/LICENSE", -}; const ramps = { neutral: chroma.scale([ @@ -31,3 +26,15 @@ const ramps = { }; export const dark = createColorScheme(`${name}`, false, ramps); + +export const meta: Meta = { + name, + author: "gessig", + license: { + SPDX: "MIT", + https_url: "https://raw.githubusercontent.com/gessig/base16-sandcastle-scheme/master/LICENSE", + license_checksum: "8399d44b4d935b60be9fee0a76d7cc9a817b4f3f11574c9d6d1e8fd57e72ffdc" + }, + url: "https://github.com/gessig/base16-sandcastle-scheme" +} + diff --git a/styles/src/themes/solarized.ts b/styles/src/themes/solarized.ts index 3e0fff61e8..98f9339d6e 100644 --- a/styles/src/themes/solarized.ts +++ b/styles/src/themes/solarized.ts @@ -1,13 +1,8 @@ import chroma from "chroma-js"; +import { Meta as Metadata } from "./common/colorScheme"; import { colorRamp, createColorScheme } from "./common/ramps"; const name = "Solarized"; -const author = "Ethan Schoonover"; -const url = "https://github.com/altercation/solarized"; -const license = { - type: "MIT", - url: "https://github.com/altercation/solarized/blob/master/README.md", -}; const ramps = { neutral: chroma @@ -34,3 +29,15 @@ const ramps = { export const dark = createColorScheme(`${name} Dark`, false, ramps); export const light = createColorScheme(`${name} Light`, true, ramps); + +export const meta: Metadata = { + name, + author: "Ethan Schoonover", + license: { + SPDX: "MIT", + https_url: "https://raw.githubusercontent.com/altercation/solarized/master/LICENSE", + license_checksum: "494aefdabf86acce06bd63001ad8aedad4ee38da23509d3f917d95aa3368b9a6" + }, + url: "https://github.com/altercation/solarized" +} + diff --git a/styles/src/themes/experiments/.gitkeep b/styles/src/themes/staff/.gitkeep similarity index 100% rename from styles/src/themes/experiments/.gitkeep rename to styles/src/themes/staff/.gitkeep diff --git a/styles/src/themes/experiments/abruzzo.ts b/styles/src/themes/staff/abruzzo.ts similarity index 100% rename from styles/src/themes/experiments/abruzzo.ts rename to styles/src/themes/staff/abruzzo.ts diff --git a/styles/src/themes/internal/atelier-dune.ts b/styles/src/themes/staff/atelier-dune.ts similarity index 100% rename from styles/src/themes/internal/atelier-dune.ts rename to styles/src/themes/staff/atelier-dune.ts diff --git a/styles/src/themes/internal/atelier-heath.ts b/styles/src/themes/staff/atelier-heath.ts similarity index 100% rename from styles/src/themes/internal/atelier-heath.ts rename to styles/src/themes/staff/atelier-heath.ts diff --git a/styles/src/themes/internal/atelier-seaside.ts b/styles/src/themes/staff/atelier-seaside.ts similarity index 100% rename from styles/src/themes/internal/atelier-seaside.ts rename to styles/src/themes/staff/atelier-seaside.ts diff --git a/styles/src/themes/internal/ayu-mirage.ts b/styles/src/themes/staff/ayu-mirage.ts similarity index 100% rename from styles/src/themes/internal/ayu-mirage.ts rename to styles/src/themes/staff/ayu-mirage.ts diff --git a/styles/src/themes/internal/ayu.ts b/styles/src/themes/staff/ayu.ts similarity index 100% rename from styles/src/themes/internal/ayu.ts rename to styles/src/themes/staff/ayu.ts diff --git a/styles/src/themes/experiments/brushtrees.ts b/styles/src/themes/staff/brushtrees.ts similarity index 100% rename from styles/src/themes/experiments/brushtrees.ts rename to styles/src/themes/staff/brushtrees.ts diff --git a/styles/src/themes/internal/dracula.ts b/styles/src/themes/staff/dracula.ts similarity index 100% rename from styles/src/themes/internal/dracula.ts rename to styles/src/themes/staff/dracula.ts diff --git a/styles/src/themes/internal/gruvbox-medium.ts b/styles/src/themes/staff/gruvbox-medium.ts similarity index 100% rename from styles/src/themes/internal/gruvbox-medium.ts rename to styles/src/themes/staff/gruvbox-medium.ts diff --git a/styles/src/themes/internal/monokai.ts b/styles/src/themes/staff/monokai.ts similarity index 100% rename from styles/src/themes/internal/monokai.ts rename to styles/src/themes/staff/monokai.ts diff --git a/styles/src/themes/internal/nord.ts b/styles/src/themes/staff/nord.ts similarity index 100% rename from styles/src/themes/internal/nord.ts rename to styles/src/themes/staff/nord.ts diff --git a/styles/src/themes/internal/seti-ui.ts b/styles/src/themes/staff/seti-ui.ts similarity index 100% rename from styles/src/themes/internal/seti-ui.ts rename to styles/src/themes/staff/seti-ui.ts diff --git a/styles/src/themes/internal/tokyo-night-storm.ts b/styles/src/themes/staff/tokyo-night-storm.ts similarity index 100% rename from styles/src/themes/internal/tokyo-night-storm.ts rename to styles/src/themes/staff/tokyo-night-storm.ts diff --git a/styles/src/themes/internal/tokyo-night.ts b/styles/src/themes/staff/tokyo-night.ts similarity index 100% rename from styles/src/themes/internal/tokyo-night.ts rename to styles/src/themes/staff/tokyo-night.ts diff --git a/styles/src/themes/internal/zed-pro.ts b/styles/src/themes/staff/zed-pro.ts similarity index 100% rename from styles/src/themes/internal/zed-pro.ts rename to styles/src/themes/staff/zed-pro.ts diff --git a/styles/src/themes/internal/zenburn.ts b/styles/src/themes/staff/zenburn.ts similarity index 100% rename from styles/src/themes/internal/zenburn.ts rename to styles/src/themes/staff/zenburn.ts diff --git a/styles/src/themes/summercamp.ts b/styles/src/themes/summercamp.ts index bc5b7e1d24..60e0b1834d 100644 --- a/styles/src/themes/summercamp.ts +++ b/styles/src/themes/summercamp.ts @@ -1,13 +1,8 @@ import chroma from "chroma-js"; +import { Meta } from "./common/colorScheme"; import { colorRamp, createColorScheme } from "./common/ramps"; const name = "Summercamp"; -const author = "zoefiri"; -const url = "https://github.com/zoefiri/base16-sc"; -const license = { - type: "MIT", - url: "https://github.com/zoefiri/base16-sc/blob/master/summercamp.yaml", -}; const ramps = { neutral: chroma @@ -33,3 +28,13 @@ const ramps = { }; export const dark = createColorScheme(`${name}`, false, ramps); +export const meta: Meta = { + name, + author: "zoefiri", + url: "https://github.com/zoefiri/base16-sc", + license: { + SPDX: "MIT", + https_url: "https://raw.githubusercontent.com/zoefiri/base16-sc/master/LICENSE", + license_checksum: "fadcc834b7eaf2943800956600e8aeea4b495ecf6490f4c4b6c91556a90accaf" + } +} \ No newline at end of file