diff --git a/.dockerignore b/.dockerignore index add07b4bf7204e92fbb14bd0f92128ec714e8865..d89a9d83e2f9c4a393e3c793b0f0c77d362e2cc3 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 1302b197bdcb8f7e4f5d71b192b240fa59ba336c..6826566e717682008315d4acceb3057a18f322fe 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 fce5717ca9aeeb68052b508d1806e1a6894b074c..5da8c8945e2b0f7443015ff34718497a6211b9f1 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 a5949127f5a9f3f7b96c2372c3bea40951de0a9a..4a9d777769d7b75108d9c65824f8165ebe62ac17 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 8bca2eafacb42c1496ad9b3d1ae145ce4b7d4570..5a4d2ff25e19e7406f79c1b627510f4c94d58212 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 f08a13902ebae7fcf33c163a330193f2013d4b53..e8410b25f045c09b6226df2bd4be4a22f8559d73 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 77469c0623d3f2b9c3254432faf93e9a32b9fe77..c74a76cccefe6c6de610b30c264a81e74b2654df 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 5a6279a95e3dab632b215900bbaef417571123cc..d3170696c5fc08c67cbca61a203eced42e4eba0d 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 24614e97c271176f6939b3199c9a3e7613fc504b..b9c12abea2ef31c3419881f4464924f2eefdfffc 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 a0f437cf9182c2876197fb8b12a5be222712c481..e8f055cb7d740fc1ce15e88d496dce60a4b0ea6b 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 0637a088a01e8ddab3bf3fa98dbe804cbde1a0dc..0000000000000000000000000000000000000000 --- 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 12873a3e4e6a4515a02fe161dce4c57a49efabe4..824fb63c0f35969efeda1bb9cb2ded01e386d539 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 1ef2ac8a16c9420d5ed019d36a05e1201236849f..f6fb61d65c83e0352dc43455d10627708b2e35b9 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 8b9eb4b0409af37a947252a51b2de8cfe8d0851b..f3a6f7328ad34ea0e6cffc30fed3742937b2275a 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 d3fcc36c2fd43eac6c7c32d5c8cf38fc509b99eb..4272d7b1afa82ea1449fbd7f55ed79f2e3585a26 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 156925fb72e3e12a67f7c4fac935d4fdf7cb916c..54546adb55d5c26e215a610c3d40a65361d3c1ad 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 c63b2e0f5bbd67109e25bbd46ea9962570ccf4e5..64584e61400b414620ba640f2fbc0b79825c535e 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) - }) - .await?, - ) - } else { - None - }; - room.update(&mut cx, |room, cx| { - room.call(called_user_id, initial_project_id, cx) - }) - .await?; + 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 { - 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 4d129fab2ea5a3770db284a957b084c720b257b0..eba58304d7c89c8de749aee932034cb8079928eb 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 5139bb8d03cef71698ffb2286f134900c2bc269f..0757cebf3ad5d836edbd59f47a03cbab02c0c211 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 2aa33e6435483ab0c5d51b396d9ef3b33905d014..748eb48f7e27368588534266fe34e9183cb01b78 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 1201665571e16b63b92d2397043e7d0266548e85..01fd1773c41d466531870c013b839d57866d8695 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 456bcf6531a7d97e42fb395882c81937ea86fe9d..9301a1974aad839a222c60b5be290d058bdfa378 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 63ea7fdd9e128ee76206f7bcf7829bc05bee8d52..af30073ab4ef424b8c9f84557cdd692cdfbfcf46 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 92d4935b2308902f2b2695d369e681cce5b7e490..32cce1e6815451ce9ac7b8e6e261ed60bca41e3d 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 54e3c67f5dcb416460e333f30b785ddfbea36d8e..178d31ba6345ede2c749f8be5861d76f58878f08 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 3f2a777f87927e0f9bb5b75c533a20a184e76284..f2cb2eddbb56c74fe5feaca2b7a43ddc082a34c0 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 e44e52196d277a2d2435ee9f37f69a3b811d0827..0b4aa3ec9bfd3d978a8bfd855cb1a06b9c91ba22 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 ac13e361fd9144736c6ebbe2b96c97de1d2581b6..2dc4cc769a80c5fae0aa6b1e508bf259073f427a 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 3351fb9eb9c14c7f82f0f4d27243776a5b419161..184a432ea399b395367dae61555a83273607565f 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 b19bc92455e136ef9fb130d35aeca1d20b8a0898..d26e2c99ccfb53d9b5e83160464df1a5c73aeafe 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 743b98adb00b4edfb47ba2daf43ae261e6993d50..c4250c142be25805d7640b5c57844651d77ec0e4 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 6f0cfc68c76569aaf94abe155a1df43abd57670f..f05cca00bf125b2ee2a5cf83d52da112420982e0 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 6ad533665e7477494e221da71f85a31646671447..5d888bc093511f593d92732e266bccf7e39263f3 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 0815d9c8d8c02c6756f65c61ca0b4c5187d0b016..8488f3381e1e23cc8467ee49df2feea6c7f18574 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 0000000000000000000000000000000000000000..541194ec66a5953fbd8d69b75aa15e63cbf68a02 --- /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 d4625cbce0525e845ff2b85def695bcb707a029f..5b5d8f1162f352166ca2e9f733dabd2d3b129906 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 0dc0ce6f42a29b327e1f3e2bedb10e0477308dd3..6d5a5cb549d3e39702f2e9f4e6e4a926e60cea1d 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 d3078bce81493e749c9156635c12d895eea1bd30..596abe9bb67a90055c7523342de16704642f44cf 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 26dd37104154c7428a46881e12c5eee092901b9a..6cb7ef32ec41a3ccd8caa8de2c0ba932144962ab 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 e32276df41f78fa5365ea3fdfea2ec717143d043..99a74fe7f22f037efe142f0b4c7ca6e48539cae3 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 84b97468e0a873e01687055b82e1213be33d3a85..f9d50019854cd7e683d522231683f1e63a1f77f5 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, ) { - 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 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) - }); + 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; + } + 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 ff59c5dc1429da8e9d2ad845742edf80aa390d88..9b5cececff3659921a1ccbc12754158c709ecaf7 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 bce63ca0cf796ef0e9537ff7f40f241f196c5331..9d8922fab5f42f3c9effc71c7e4125967a2bbec1 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 043b21db21cf42e582d2742b41d13aac2fc89ca2..0d868d460c3f44267c79588d08081a747b67807f 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 6d003cae5dbaf42d54a85b56ea941d0851816538..f92b07da1dd49ef4de90e7bb9fc9a00ee4993fd4 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 7e7f44e5141ae2f87596ed9873ad9f388a93698d..c3a446faa7e89dd249db3a8ad0cfdceee973dd09 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 d9840fd3fab6bcbe372b48820e2fe05914c09ad1..77b58d1a0b272f125ded18a1e35045f7fec50bd1 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 7079d197f9dfca676e84b595a77a8387c0f8e776..da3c6bc4bd44ad76a01c407f41be7e6429a1bbd1 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 2d8d1a74fd152b11092eaa74e017d10f1816bc59..6e37735c1371ff466e345c95d0fa92d6d3c892a6 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 f1c19bca8a1115a22d528614b02a480789254c32..f3ce89adc5ec6fb3387c343aaaf8cd36bea94631 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 b65b09cf17c03c828031aed78bfebab6d4dc8ac0..345709abf33f89862579cedd8482318063d0da6a 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 d8dbfee17105136a7b02f826ddf44e9037ddbb81..8f8647e88d99c6aa0b9b430442e8293d1cfb0c20 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 0000000000000000000000000000000000000000..e2d663f075a4a2c827e581bd5abf27e2547fe580 --- /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 4b0dfc4df9f3f89fa0a3ea26efa83d47394cc61e..f95f24f557485211a854e5de680310f5fac50784 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 8185fbad9ac61244b1acad73baa9d2323222e974..bcef9d0af5ab983c7e6c7496d306e3b3bfa52518 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; - -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, -}; +use crate::{submit_feedback_button::SubmitFeedbackButton, system_specs::SystemSpecs}; -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]); - -pub fn init(cx: &mut MutableAppContext) { - cx.add_action(FeedbackEditor::deploy); -} - -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() - } -} +actions!(feedback, [GiveFeedback, SubmitFeedback]); -impl StatusItemView for FeedbackButton { - fn set_active_pane_item( - &mut self, - _: Option<&dyn ItemHandle>, - _: &mut gpui::ViewContext, - ) { - } +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); + } + }); + + 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 - } - - fn new(project: ModelHandle, cx: &mut ViewContext) -> Self { - let markdown_language = project.read(cx).languages().get_language("Markdown"); - - 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) + Self { + system_specs: system_specs.clone(), + editor, + project, + } } - 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"], - ); + 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 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 0000000000000000000000000000000000000000..bfe67ec4ae4b174bda009f3de8fdfbf250bd2982 --- /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 0000000000000000000000000000000000000000..470a53905e347edcdfab4eecb2901d02e58e35d4 --- /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 17e51a68153fd579cfc08630cf48b38f30a4dd6c..f20561826e4df70e730c48f889f593b3ff4421e7 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 6a79953f41f25c3c8d5037d17b63aec5adb230cc..f640f35036d3ff3a26566f2058e22cf64d7efa27 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 e1b6e11b46896787529a283518982c6811e2c6b5..7be254be4da2b0c0f47b9f03f4ae437df1407261 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 ad1fad85b1938220e4f2259ff623dc274748aac5..4bdd7755937fb161d240c12be2a2c1df28e2f73e 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 0000000000000000000000000000000000000000..2234bfa3911cf892792470c446e0a25c3e56af7e --- /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 0000000000000000000000000000000000000000..f0c1699f165ea8100ccdfe1facbfb8a3ac1a2d8e --- /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 3e75910475ab67622edc7aa37a4adf1ce61cba95..1d467ae745b3774ae862b889e0d8df597baa433e 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 0000000000000000000000000000000000000000..855f0e3041b117ec0812044b131524ce27cbc075 --- /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 9f11f09f8e9e33ebcd414e15546419ad00317ffe..41a802feb37fcda12c8b68b76759f173b350a793 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 0000000000000000000000000000000000000000..2ee7b542a89ce32364c7a645ece634ccd96d3668 --- /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 ca317d9e114cdae9987d47e6ab073b08c90dd689..6553b2fa8d00aed19a7f928d060652dc6f533d51 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 dbcecf9c2414952ceb10f16fcc868290fcb54db6..562f12295c4a3cd0dbbba28d46301de41f212ef7 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 c7de0352328d287b1248b80699c06df7fd07ae0e..cfc26d6869e3babec69e891bb86e03b86212d799 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 afd65d4f0424e0031090852d2441d1a6f8bd9420..c1cfd14e82d8d3e3235a7a2c3e7d507cd78d9648 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 28f5f80c8337696f06b47ccdb8ba595a3270d5df..b19989b2105b3f206f5f3658c2fb72e036a5c35c 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 99d607e4070c5eebb2039dba3754e6100a59b30a..76c2707d2638823e057eecdd30a2614d592f717f 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 7eb080083e9d4e2ed2f74597b95ebc7217f120af..342c1c66d0cf424f95728db7065ea39fa8b79b64 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 2f29898c26d2e9179560e40787d304e6320f33f6..688272137212f823ea4f09debd4e86f030e5ef32 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 89da409dbd4ed45e080625fb927c9e8d4eec363d..6a479681181e41ff454d2e7d50e554a9c7328598 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) +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_size(&self) -> NSSize { - NSSize::new(self.x() as f64, self.y() as f64) + fn to_ns_rect(&self) -> NSRect { + 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), + ) } } -impl RectFExt for RectF { - fn to_ns_rect(&self) -> NSRect { - NSRect::new(self.origin().to_ns_point(), self.size().to_ns_size()) +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 0638689cd4e2dfbdf9c4181c2785b487de54e01e..57827e1946622376808f35305645301d15d1a658 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 fdc7fbb50581a1d13f465e23ed1d7c50187fb590..98b6a66f03842a063adfe93209a0d6e0d8985684 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 33feb4808f17c15136df7daeee287a90af921f3a..812027d35c55a38101539709554a15e9b2ef06f6 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,27 +169,41 @@ impl StatusItem { } impl platform::Window for StatusItem { - fn as_any_mut(&mut self) -> &mut dyn std::any::Any { - self + fn bounds(&self) -> WindowBounds { + self.0.borrow().bounds() } - fn on_event(&mut self, callback: Box bool>) { - self.0.borrow_mut().event_callback = Some(callback); + fn content_size(&self) -> Vector2F { + self.0.borrow().content_size() } - fn on_appearance_changed(&mut self, callback: Box) { - self.0.borrow_mut().appearance_changed_callback = Some(callback); + fn scale_factor(&self) -> f32 { + self.0.borrow().scale_factor() } - fn on_active_status_change(&mut self, _: Box) {} - - fn on_resize(&mut self, _: Box) {} + fn titlebar_height(&self) -> f32 { + 0. + } - fn on_fullscreen(&mut self, _: Box) {} + fn appearance(&self) -> crate::Appearance { + unsafe { + let appearance: id = + msg_send![self.0.borrow().native_item.button(), effectiveAppearance]; + crate::Appearance::from_native(appearance) + } + } - fn on_should_close(&mut self, _: Box bool>) {} + fn screen(&self) -> Rc { + unsafe { + Rc::new(Screen { + native_screen: self.0.borrow().native_window().screen(), + }) + } + } - fn on_close(&mut self, _: Box) {} + fn as_any_mut(&mut self) -> &mut dyn std::any::Any { + self + } fn set_input_handler(&mut self, _: Box) {} @@ -224,46 +240,46 @@ impl platform::Window for StatusItem { unimplemented!() } + fn present_scene(&mut self, scene: Scene) { + self.0.borrow_mut().scene = Some(scene); + unsafe { + let _: () = msg_send![*self.0.borrow().native_view, setNeedsDisplay: YES]; + } + } + fn toggle_full_screen(&self) { unimplemented!() } - fn bounds(&self) -> RectF { - self.0.borrow().bounds() + fn on_event(&mut self, callback: Box bool>) { + self.0.borrow_mut().event_callback = Some(callback); } - fn content_size(&self) -> Vector2F { - self.0.borrow().content_size() - } + fn on_active_status_change(&mut self, _: Box) {} - fn scale_factor(&self) -> f32 { - self.0.borrow().scale_factor() - } + fn on_resize(&mut self, _: Box) {} - fn titlebar_height(&self) -> f32 { - 0. - } + fn on_fullscreen(&mut self, _: Box) {} - fn present_scene(&mut self, scene: Scene) { - self.0.borrow_mut().scene = Some(scene); - unsafe { - let _: () = msg_send![*self.0.borrow().native_view, setNeedsDisplay: YES]; - } + 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 appearance(&self) -> crate::Appearance { - unsafe { - let appearance: id = - msg_send![self.0.borrow().native_item.button(), effectiveAppearance]; - crate::Appearance::from_native(appearance) - } + 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 6126533644578ca0a023657eef851f26c16432a3..50e0de216c5a8e8e7476ac6832d9e27b6afd8a9e 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,32 +633,39 @@ impl Drop for Window { } impl platform::Window for Window { - fn as_any_mut(&mut self) -> &mut dyn Any { - self + fn bounds(&self) -> WindowBounds { + 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_resize(&mut self, callback: Box) { - self.0.as_ref().borrow_mut().resize_callback = Some(callback); + fn scale_factor(&self) -> f32 { + self.0.as_ref().borrow().scale_factor() } - 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_should_close(&mut self, callback: Box bool>) { - self.0.as_ref().borrow_mut().should_close_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_close(&mut self, callback: Box) { - self.0.as_ref().borrow_mut().close_callback = Some(callback); + fn screen(&self) -> Rc { + unsafe { + Rc::new(Screen { + native_screen: self.0.as_ref().borrow().native_window.screen(), + }) + } } - fn on_active_status_change(&mut self, callback: Box) { - self.0.as_ref().borrow_mut().activate_callback = Some(callback); + fn as_any_mut(&mut self) -> &mut dyn Any { + self } fn set_input_handler(&mut self, input_handler: Box) { @@ -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,122 +794,63 @@ 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 - ]; - - 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 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) - } - } + let app = NSApplication::sharedApplication(nil); - fn content_size(&self) -> Vector2F { - let NSSize { width, height, .. } = - unsafe { NSView::frame(self.native_window.contentView()) }.size; - vec2f(width as f32, height 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, + ); - fn scale_factor(&self) -> f32 { - get_scale_factor(self.native_window) - } + let window_number: NSInteger = msg_send![class!(NSWindow), windowNumberAtPoint:screen_point belowWindowWithWindowNumber:0]; + let top_most_window: id = msg_send![app, windowWithWindowNumber: window_number]; - 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]; + 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 + } } } } @@ -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 00cd524c1de6b99e9acfef3a43f1e1f3c3b02630..194684bd12917daf64fa4e56c763b33663640abd 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,39 +287,33 @@ 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)) } -} -impl super::Window for Window { - fn as_any_mut(&mut self) -> &mut dyn Any { - self + fn content_size(&self) -> Vector2F { + self.size } - fn on_event(&mut self, callback: Box bool>) { - self.event_handlers.push(callback); + fn scale_factor(&self) -> f32 { + self.scale_factor } - fn on_active_status_change(&mut self, callback: Box) { - self.active_status_change_handlers.push(callback); + fn titlebar_height(&self) -> f32 { + 24. } - fn on_fullscreen(&mut self, callback: Box) { - self.fullscreen_handlers.push(callback) + fn appearance(&self) -> crate::Appearance { + crate::Appearance::Light } - fn on_resize(&mut self, callback: Box) { - self.resize_handlers.push(callback); + fn screen(&self) -> Rc { + Rc::new(Screen) } - fn on_close(&mut self, callback: Box) { - self.close_handlers.push(callback); + fn as_any_mut(&mut self) -> &mut dyn Any { + self } fn set_input_handler(&mut self, _: Box) {} @@ -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 present_scene(&mut self, scene: crate::Scene) { + self.current_scene = Some(scene); + } + fn toggle_full_screen(&self) {} - fn bounds(&self) -> RectF { - RectF::new(Default::default(), self.size) + fn on_event(&mut self, callback: Box bool>) { + self.event_handlers.push(callback); } - fn content_size(&self) -> Vector2F { - self.size + fn on_active_status_change(&mut self, callback: Box) { + self.active_status_change_handlers.push(callback); } - fn scale_factor(&self) -> f32 { - self.scale_factor + fn on_resize(&mut self, callback: Box) { + self.resize_handlers.push(callback); } - fn titlebar_height(&self) -> f32 { - 24. + fn on_fullscreen(&mut self, callback: Box) { + self.fullscreen_handlers.push(callback) } - fn present_scene(&mut self, scene: crate::Scene) { - self.current_scene = Some(scene); + fn on_moved(&mut self, callback: Box) { + self.moved_handlers.push(callback); } - fn appearance(&self) -> crate::Appearance { - crate::Appearance::Light + fn on_should_close(&mut self, callback: Box bool>) { + self.should_close_handler = Some(callback); } - fn on_appearance_changed(&mut self, _: Box) {} -} + fn on_close(&mut self, callback: Box) { + self.close_handlers.push(callback); + } -pub fn platform() -> Platform { - Platform::new() -} + fn on_appearance_changed(&mut self, _: Box) {} -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 0909d95fd0a5ac4f53e5de600ca7bc5c540dfc00..c0785e11f3b1a928a2a3bd01bd24bc2ee8128693 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 4c72334910b514a257be8213410d1aa9dbb05b79..960c565bd49f02a90f41a50f3fb595adc6579b93 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 eb992b638ac9906a337f23c93b97f0b945d2ef9d..d784d43ece50602b53f1d019c2b6dd57d3463537 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 ab6c687b7a8e1e00e3b44433ffeab7138de80668..3e6561e47109870b4fae7d89844c5ebf9ed11be1 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 3c382b83b651741b385adc83a3b59449ff480bda..03d28025917842f37af861d8dba57dbf7e720339 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); - } } - } 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); + 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 { + 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; } - } - - result = Some((open, close)); - } - result + return Some((open, close)); + } + None + }) } #[allow(clippy::type_complexity)] diff --git a/crates/language/src/buffer_tests.rs b/crates/language/src/buffer_tests.rs index 0b2ef1d7a782713159187b0a5262021afa34b8fe..e6e75447637296d4b78cab1b2d9f791882cc7d27 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 = " + let mut assert = |selection_text, range_markers| { + assert_bracket_pairs(selection_text, range_markers, rust_lang(), cx) + }; + + assert( + indoc! {" mod x { + moˇd y { + + } + } + let foo = 1;"}, + vec![indoc! {" + mod x «{» mod y { + + } + «}» + let foo = 1;"}], + ); + assert( + indoc! {" + mod x { + mod 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) - )) + let foo = 1;"}, + vec![ + indoc! {" + mod x «{» + mod y { + + } + «}» + let foo = 1;"}, + indoc! {" + mod x { + mod y «{» + + «}» + } + let foo = 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( + 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_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) - )) + + assert( + indoc! {" + mod x { + mod y { + + } + ˇ} + 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 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 045e8dcd6f510772c2050a55cbbbb228823f40f3..4279ce665402be81f9ad26994bcebe8e1ea63a34 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 to_vec(&self) -> Vec> { - self.languages.read().iter().cloned().collect() + 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 language_names(&self) -> Vec { - self.languages - .read() - .iter() - .map(|language| language.name().to_string()) - .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 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 8d6673085494263970baf3fdbb3a0f6c21a939d0..670f479f10282932770a2fb0cc61f0fe92ae498f 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,46 +308,48 @@ 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; - } - - // 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(), + 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; } - } 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); - } + // 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(), + } + }; - 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(), - ); + 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(), + ); + } 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 47fd4f0b69d0f3b40702888c1953e0aa39f54af3..f45667e3c3e4b4d00f264b51ff4782e8471e1ebd 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 b7199a5287cd9c7a8b9c260aa463450f823fa3d9..660528daf12b73d332555b405840b3ad3533a5cf 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 0000000000000000000000000000000000000000..8521c4fd818e4a09425095c09352b6a7134bdb6e --- /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 0000000000000000000000000000000000000000..e75f84372024a32b62059bf37fe53f19cc7cf2bf --- /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 995a6514c5d3d8472b295f8d30769989177ef76e..8ed37a003c748bdea86472c10179cf40e1d80457 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())); - } - } - - 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?; - } + 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::>(); - // 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()); - } - }); - } - }); + 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())); + }); - 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()), - ) + 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()); + } + }); + } }); - 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, - &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 c9e159f391a1af7b8ea5a9d92ccb1932710fccc2..2f9f92af4e1e0d3415a2c52ffd401e41fd2005ea 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 b65cf9e39bd96367a3e0b0d45989fcbdce76cf9b..8b622ab607db49f6f5b54845ccf7d70397752225 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 e59353aae40e06773e4467731976e878baff1cbb..2ba920c318f04a24cc0f84321ebd7fe0d4b90c82 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 02e15290ab269b5f4fee7cdcc365f44e0777f31f..f613ba4df2132fe8114f12045d03ccb9ef81cb72 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 6b09f07db4f436e679c8681b30e63993fe8db413..1a56abc7835103428352867b840102ef740c952f 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 d3d5c437c5e6f6675d30ebf501322afd96d5d5d2..99c73815c9c3d656eef2afbb5d8a9e3f3ec7b6c5 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 4090bcc63af42b70507bf840378d3bc2b7a7725a..01992d94311e7dd348b06d3a7b6b042780204010 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 8b2c12a59bf8b673965838ed62319318a0574c6f..21939b26b0b15e257891ccde9882190e4689f148 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 8409a1dff5ac2c46c9b3731be660388a9707e61c..716ec766443bc997c57fb77f78d94f0290aad579 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 62212d8f18c66a9c59a4941b0fbb9268a4e5dc90..86d69afe5f8203224f5db5f7fd319b75ecb993c8 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,64 +234,44 @@ 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 Bind for [T; COUNT] { - 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 [T; COUNT] { + fn column_count() -> usize { + T::column_count() * 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] { +impl Bind for [T; COUNT] { fn bind(&self, statement: &Statement, start_index: i32) -> Result { let mut current_index = start_index; - for binding in *self { + for binding in self { current_index = binding.bind(statement, current_index)? } @@ -272,18 +279,21 @@ impl Bind for &[T] { } } +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,87 +311,116 @@ impl Column for PathBuf { } } -/// Unit impls do nothing. This simplifies query macros -impl Bind for () { - fn bind(&self, _statement: &Statement, start_index: i32) -> Result { - Ok(start_index) - } -} - -impl Column for () { - fn column(_statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { - Ok(((), start_index)) +impl StaticColumnCount for uuid::Uuid { + fn column_count() -> usize { + 1 } } -impl Bind for (T1, T2) { +impl Bind for uuid::Uuid { fn bind(&self, statement: &Statement, start_index: i32) -> Result { - let next_index = self.0.bind(statement, start_index)?; - self.1.bind(statement, next_index) + self.as_bytes().bind(statement, start_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 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 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 StaticColumnCount for () { + fn column_count() -> usize { + 0 } } - -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)) +/// Unit impls do nothing. This simplifies query macros +impl Bind for () { + fn bind(&self, _statement: &Statement, start_index: i32) -> Result { + Ok(start_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 () { + fn column(_statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { + Ok(((), start_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)) - } -} +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 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<$($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 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<$($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_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 41c505f85b11ea79ffcfbbcffeed29224c9fab63..b8e589e268c8f02693b11b7915b32eb8e0975d2f 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 f3ec6d48541b8f2fde85a1cffdcaba99501315c2..69d5685ba02ceadeeb5bec364366c76121aa12a9 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 df4a2987b5f0198fb380d5014d963dd2f69c2aff..488ee27c0c155f4de4c34d259e0d07faa6f43411 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 dd5c5fb3b0e6e9e374d468cdd5b586e25878036f..feed3d510f78c30a1b32fc57c3a6162b5f9c4816 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 26bd0931fe2d1d738406cf79406b8afea11df0c1..0da9ed47299d5b71d73d9173d80c526e66a0e507 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 847dfc5ee5c6710a7faefda6fe80131812733954..cc3025d96e640afd7f1925f24b8490c74b86794b 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 e463310b9810eb2d6f30e924b1b66166c9b2e295..bc338bbe269369f29fdea1d5794d30c172d2e316 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 3d4783604d3b9a2d9933f9e367a7ea26fb032397..d47625289bfd693201c0ea18de1e0fb216cebae2 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 8f6fc7460015a95532a02167a5016d08eab9641f..80ff31106948dec501b4d13863491964d9105ca6 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 252a64c7fda465c9ad4b69ae013fd71f189054c1..d999730a0d8b7abe21c0d9b39bfc3a9fb4ca49ca 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 3cda5d3e51078857d622478262b399400538f222..84ec68e6360d72bae7cbe35efcc33462af0fb63b 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 4cbaa382e84e3d099cc3a2aa2aa0276ce1f613f5..e8c158b637010d1faf3fbe2c0578f0f1fa82a0e7 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 e79cc269c9068a85fefe920ddecaf0c937a015d7..37e1f29ce280d457e3b3b1ded242333dce178676 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 c526e3b1dc7eba5a2edbed82bcaf7408bf0574d8..c58f66478fa682694d1da269afb7d299a89d5e2e 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; - } - - let editor = editor.read(cx); - let editor_mode = editor.mode(); - let newest_selection_empty = editor.selections.newest::(cx).is_empty(); + 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); + 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 62b30730e8ea8d4a3a4d9f28120359872c5e8211..25188a466cffa309bac6008dd854bdaf149482fb 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 d6391353cf7fc43980dd2866f69adf4d379721c8..0cac45fd1859e3950ef38e733525702295cfd8c5 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 723dac0581e33318ab0d47585fe93d39494374a6..f5614b4b474657deeef97041dfdcbe0c8cf6ef39 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 9f799ef37f9f3dbcecb168420d6892e3b8534587..33f142c21e692294a498d951c39b3ee05a0b1cf8 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 ac8771f969ef40d7f37f6b66f429cde7b3085247..2180fbdabb81ff3d991553c6771624d33f45a085 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 08bea7cae82f3ac78bad1f16e46d43fb30799888..fa1a400bc0e2519b14c9da981a77575788f8b096 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 0000000000000000000000000000000000000000..9dc0fc655b266fea00a336785306c3afcd7ba2ae --- /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 60680f82a22a3541958e59e6ec219d7174b4b20c..fc069fe6c8d7d481873a22ebc6b679b1aba85632 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 747541f87d6faaed9a3e3883a9a4d0e5f5072403..057658c3b59a2a35584268ceea544594e55686e6 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 0000000000000000000000000000000000000000..cafbea7db37c6fdccd0e3534bd2c4a2757aef2f0 --- /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 b1888bb243c41001d330f108225893a1f9fb6208..0e28976151d8ae64119071851528b9cfdbfb6b2b 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 43feede1904a9afe8dbf2b53b4987b9dfa21bc8d..141a345382603ea59c1699b008d2104deb4d18ae 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 7e56b864bfa5813ab6ed1f58174e8a3c4a0bb5e2..8e51a54178cceca5cbc9dbae70ba06be8007e3a1 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 03a866f2f6cf362a2592e4bd8ced681aae5fd17c..ddbea4c9f9a2fdfa55d8defc0dccef425330c543 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; - - CREATE TABLE pane_groups( - group_id INTEGER PRIMARY KEY, - workspace_id INTEGER NOT NULL, - parent_group_id INTEGER, // NULL indicates that this is a root node - position INTEGER, // NULL indicates that this is a root node - axis TEXT NOT NULL, // Enum: 'Vertical' / 'Horizontal' - FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) - ON DELETE CASCADE - ON UPDATE CASCADE, - FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE - ) STRICT; - - CREATE TABLE panes( - pane_id INTEGER PRIMARY KEY, - workspace_id INTEGER NOT NULL, - active INTEGER NOT NULL, // Boolean - FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) - ON DELETE CASCADE - ON UPDATE CASCADE - ) STRICT; - - CREATE TABLE center_panes( - pane_id INTEGER PRIMARY KEY, - parent_group_id INTEGER, // NULL means that this is a root pane - position INTEGER, // NULL means that this is a root pane - FOREIGN KEY(pane_id) REFERENCES panes(pane_id) - ON DELETE CASCADE, - FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE - ) STRICT; - - CREATE TABLE items( - item_id INTEGER NOT NULL, // This is the item's view id, so this is not unique - workspace_id INTEGER NOT NULL, - pane_id INTEGER NOT NULL, - kind TEXT NOT NULL, - position INTEGER NOT NULL, - active INTEGER NOT NULL, - FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) - ON DELETE CASCADE - ON UPDATE CASCADE, - FOREIGN KEY(pane_id) REFERENCES panes(pane_id) - ON DELETE CASCADE, - PRIMARY KEY(item_id, workspace_id) - ) STRICT; - )]; + &[sql!( + CREATE TABLE workspaces( + workspace_id INTEGER PRIMARY KEY, + workspace_location BLOB UNIQUE, + dock_visible INTEGER, // Boolean + dock_anchor TEXT, // Enum: 'Bottom' / 'Right' / 'Expanded' + dock_pane INTEGER, // NULL indicates that we don't have a dock pane yet + left_sidebar_open INTEGER, //Boolean + timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL, + FOREIGN KEY(dock_pane) REFERENCES panes(pane_id) + ) STRICT; + + CREATE TABLE pane_groups( + group_id INTEGER PRIMARY KEY, + workspace_id INTEGER NOT NULL, + parent_group_id INTEGER, // NULL indicates that this is a root node + position INTEGER, // NULL indicates that this is a root node + axis TEXT NOT NULL, // Enum: 'Vertical' / 'Horizontal' + FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) + ON DELETE CASCADE + ON UPDATE CASCADE, + FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE + ) STRICT; + + CREATE TABLE panes( + pane_id INTEGER PRIMARY KEY, + workspace_id INTEGER NOT NULL, + active INTEGER NOT NULL, // Boolean + FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) + ON DELETE CASCADE + ON UPDATE CASCADE + ) STRICT; + + CREATE TABLE center_panes( + pane_id INTEGER PRIMARY KEY, + parent_group_id INTEGER, // NULL means that this is a root pane + position INTEGER, // NULL means that this is a root pane + FOREIGN KEY(pane_id) REFERENCES panes(pane_id) + ON DELETE CASCADE, + FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE + ) STRICT; + + CREATE TABLE items( + item_id INTEGER NOT NULL, // This is the item's view id, so this is not unique + workspace_id INTEGER NOT NULL, + pane_id INTEGER NOT NULL, + kind TEXT NOT NULL, + position INTEGER NOT NULL, + active INTEGER NOT NULL, + FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) + ON DELETE CASCADE + ON UPDATE CASCADE, + FOREIGN KEY(pane_id) REFERENCES panes(pane_id) + ON DELETE CASCADE, + PRIMARY KEY(item_id, workspace_id) + ) STRICT; + ), + 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 b264114fb68820203f90cd8b139d2d85a4836864..507582b2160d4585c3dac1eb199c234c14ea8de4 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 b76535f6ed226fc0e63fa490779a731ef0dd089f..b3e107c81b0030b763326ac16c806c1c8c2aa0af 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 ec7ba8fae07ec149c378d1f4e9dc2dde4e4b6a25..95969de6b0b84d7692d6494d4026d40508619ae6 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 0000000000000000000000000000000000000000..459169afc84cb87ac9e88b101310e2143bcc55c3 --- /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 d159271f99b6682b6b24d284edba011b2e67d619..046866cc0c0954a4f9a7a0668e961fdf4ad6e9bc 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 240d1dc49e5ac6603b1c0760a2e11f748274b276..a99c80c00121534347eb35daebf5fb22414490df 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(); - - let mut language = Language::new(config, Some(grammar)); + .unwrap() +} - if let Some(query) = load_query(name, "/highlights") { - language = language - .with_highlights_query(query.as_ref()) - .expect("failed to evaluate highlights query"); - } - 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) +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"), } - 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 712e87101b63b74df8fc3aca86d413dde32c0a96..9fbb12857f74c14745d08253d1707327381fbf36 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 19692fdf44f08ed852c03ed9b7cad94f3c62eed2..dc84599e4eff350f7f27e17d15fb4d79692acd88 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 c5aff17e566b69b33e788ded686fd2eb8acef1d3..df28177f0bac637172a1befe3afdb677f7212781 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 0000000000000000000000000000000000000000..4bcffca9084257bd7595b8c19735418cc2b8d65b --- /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 0000000000000000000000000000000000000000..5f5bd60b93fa1f3daba5f31f12f2aec8f808424b --- /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 0000000000000000000000000000000000000000..effb37f945a1f0f2c9780d25e752d419f83e557e --- /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 0000000000000000000000000000000000000000..96389c79b4b827ce69d5c95389bb06065541e8d7 --- /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 0000000000000000000000000000000000000000..71e15a0c339a3a84280516fe037b8ef298d93b58 --- /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 0000000000000000000000000000000000000000..8bd8d88070052055a4152a049aabff4b57d0818e --- /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 0000000000000000000000000000000000000000..577054b4040d174954e365371842c459e1dfc1ba --- /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 ba6ccf7bf01b544bde4e91737cf2a89e4e9e4b36..1391494ab1ea1e324caf9da769c34b5962121679 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 a817de8e3ba87b2619d4935dbe536060d3522137..45f20e25a3570b7b939fa04cf7a2c14e3528ba15 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 30971fef1ae726802fc694c2c2e4c1f1e0d79079..40948d5005d75a8f04441cd5aef62829f0568a10 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 b52a7a8affdef6cf85f455759342617e70e5b862..7240173a89260b22a9508a9984e40dc8b1ab7410 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 01b62577ad030deca39225c26c5b054d88a8303d..5290158deaf037b1a9ddb8a52211871c58a2a815 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 bd1986b6b3aa9d00c3596f4b8e927c7961078219..43df33d158163c5a5b50a9e7e8d91e11f23268b2 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 0000000000000000000000000000000000000000..46569111f1ddc82743c4ec379c3e3424b7d6b326 --- /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 0000000000000000000000000000000000000000..9e8c9cd93c30f7697ead2161295b4583ffdfb93b --- /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 0000000000000000000000000000000000000000..08dac475b370666aa6186ed55e397fbce7a4c7ba --- /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 0000000000000000000000000000000000000000..06081f63cb45739e2af8d519a35be6b819c2ad58 --- /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 0000000000000000000000000000000000000000..e85eb1bf8ad167591691b0cbc3ccf2065b833521 --- /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 fe7e95cf247c01163f13b33bd51931e136e84f8a..a775b31bc48d51de3a496438fc7e707c96326f1d 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 834eb751e102a7fe1cd9222d0a42492a1a5a548a..52ca7d232494506d88fa5d978368267df0a3fb91 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 78d10670f7347e6d0055277500198424587c6710..16b5413fda2fe759e0f522e92fac0f074476df41 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 94efbdf0afb75013b690eeb796d58671e44658e9..6fe93ed66cc905d656509bac317b118a8a9cbb37 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 c233bf18725dac4b0c5d543526d813a66503452f..0000000000000000000000000000000000000000 --- 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 e1a917292c91f2f30f746db129982057aa1a9ca8..8a41f55c025bf4909b4298180588f043a6153894 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 a51b714dae7edfa339acf6c0675d6cd1946ca1b1..a41aee8a4c4fc65877efa1a71750858bfa5864ae 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 d338e7ab0b918db66b050908533af7ebc4417c4a..e166b653c89318c390a443db49e59cf30ba984fd 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 82341bf6db0d15ccb0a1ac84af5841b2279f6716..168ecf7a235bc1de63b04d1464fdea44906c1ec6 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 582f1c84968a5c1a25ddac5fd3c21ba907353c6d..b0a904b11d8a463e790b332f393319403f61aafc 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 - }, - "@swc/wasm": { - "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" + } + }, + "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==" - }, - "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==" + }, + "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==" + } } - } } diff --git a/styles/package.json b/styles/package.json index 11bcbadf73d333f1dd16f67eefa00474de292052..118269bc814b8474d35476792a2db8f26a23a626 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 0000000000000000000000000000000000000000..5026faef4ecac87c4d929829d8aa5e666968c81f --- /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 32749a7aaa832f51579cd383ec1481b4d2ca078e..4bb7b8fc09dfa596bc5a12ae97e7a1cb5eae963a 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 746443119d44377409e685a9a83a4fd0cb2bf63d..c7e1d4ead7ef8f426400add201351a19f7e9cfcc 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); -fillColorSchemes( - path.resolve(`${__dirname}/themes/internal`), - internalColorSchemes -); +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/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 267d83050667ccb130a8f0c4b20cf37574aaf2d7..5d04050fe123f78c736df9a266a1ca8eeeb58824 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 0000000000000000000000000000000000000000..46cb867ad90613325ea2cfa76800d7761a64f080 --- /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 520ceb67fe203d69a1ce54afdb05b1d731e0e85b..b76179b3c5e5b5b5c7d231aca32afb318b9a22d1 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 98cf83470465a6dbf24b9c06b969524569b97fb5..0959cabacee3715c3473be466efd8fe45b44cd69 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 d8293db3a78fe0019103b3f3fb02427f752c3f1b..fa51b1ec80172c3a00c063d9ed7384596db81306 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 c5b914d62bb2e31e6045dee7872120c4790d455b..23ccc57fd458a0c3fa414bd07aaee007f0a06ae5 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 1b2c2cf7e89dc0a0f678370be3ed55709a3a2a46..ee858627d76a40f32121408be50a16152953c751 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 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/styles/src/themes/one-dark.ts b/styles/src/themes/one-dark.ts index 612a71ccc1820bd3d2d26d5b4bb89400c0270670..42a765e3e3a4fef6c393bb84901e4c6d40eb9955 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 a5ac1f71589525cbd58a6c81bdcf30e876edded3..50f99becdcd814038c2350a94b76852da5d493b3 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 20d5dd1ebe61ac42d5ab7499675c97be6dba72f9..b1744f9c2009eced02ab31c1f9324a57866f30df 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 5920357bd31abb7808ed9521a72fd305b44afba6..a4c1737c2b35ec8476366ce81675abda5afb9e8f 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 9144a136d2ce2fc2e5a363080b92cbd647a2b4c9..e3c115213b9cd6df2e4cc27327c7837ee654a232 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 c625ab29863f51f3b4704dd934ac2ab61de6a5e8..0e1328feabe269e48cb51e6d494f98dfa88f2812 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 3e0fff61e8809a040a4d89fddd2211f98cc7fca0..98f9339d6ed9c02a4bd8820bc0f2057f525a8f10 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 bc5b7e1d246d832a5776dcefccfc7115faaaf71f..60e0b1834d06bf7c9f1c58fba402cbb9605a9f08 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