Merge branch 'main' into randomized-tests-operation-script

This commit is contained in:
Max Brunsfeld 2023-02-20 10:39:00 -08:00
commit 51cea1b1fb
207 changed files with 7644 additions and 3698 deletions

View file

@ -8,4 +8,4 @@ crates/collab/static/styles.css
vendor/bin vendor/bin
assets/themes/*.json assets/themes/*.json
assets/themes/internal/*.json assets/themes/internal/*.json
assets/themes/experiments/*.json assets/themes/staff/*.json

View file

@ -1,6 +1,6 @@
## Description of feature or change ## Description of feature or change
## Link to related issues from zed or insiders ## Link to related issues from zed or community
## Before Merging ## Before Merging

View file

@ -4,7 +4,7 @@ on:
push: push:
branches: branches:
- main - main
- "v*" - "v[0-9]+.[0-9]+.x"
tags: tags:
- "v*" - "v*"
pull_request: pull_request:
@ -42,6 +42,9 @@ jobs:
clean: false clean: false
submodules: 'recursive' submodules: 'recursive'
- name: Run check
run: cargo check --workspace
- name: Run tests - name: Run tests
run: cargo test --workspace --no-fail-fast run: cargo test --workspace --no-fail-fast

View file

@ -21,15 +21,6 @@ jobs:
${{ github.event.release.body }} ${{ 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: mixpanel_release:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:

5
.gitignore vendored
View file

@ -7,9 +7,8 @@
/crates/collab/static/styles.css /crates/collab/static/styles.css
/vendor/bin /vendor/bin
/assets/themes/*.json /assets/themes/*.json
/assets/themes/Internal/*.json /assets/*licenses.md
/assets/themes/Experiments/*.json /assets/themes/staff/*.json
/assets/licenses.md
**/venv **/venv
.build .build
Packages Packages

157
Cargo.lock generated
View file

@ -259,6 +259,21 @@ dependencies = [
"futures-lite", "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]] [[package]]
name = "async-io" name = "async-io"
version = "1.12.0" version = "1.12.0"
@ -350,6 +365,32 @@ dependencies = [
"syn", "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]] [[package]]
name = "async-stream" name = "async-stream"
version = "0.3.3" version = "0.3.3"
@ -371,6 +412,20 @@ dependencies = [
"syn", "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]] [[package]]
name = "async-task" name = "async-task"
version = "4.0.3" version = "4.0.3"
@ -828,6 +883,7 @@ dependencies = [
"media", "media",
"postage", "postage",
"project", "project",
"settings",
"util", "util",
] ]
@ -1132,7 +1188,7 @@ dependencies = [
[[package]] [[package]]
name = "collab" name = "collab"
version = "0.5.3" version = "0.5.4"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-tungstenite", "async-tungstenite",
@ -1196,6 +1252,7 @@ name = "collab_ui"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"auto_update",
"call", "call",
"client", "client",
"clock", "clock",
@ -1275,6 +1332,7 @@ source = "git+https://github.com/servo/core-foundation-rs?rev=079665882507dd5e2f
dependencies = [ dependencies = [
"core-foundation-sys", "core-foundation-sys",
"libc", "libc",
"uuid 0.5.1",
] ]
[[package]] [[package]]
@ -1899,6 +1957,7 @@ dependencies = [
"tree-sitter-html", "tree-sitter-html",
"tree-sitter-javascript", "tree-sitter-javascript",
"tree-sitter-rust", "tree-sitter-rust",
"tree-sitter-typescript 0.20.2",
"unindent", "unindent",
"util", "util",
"workspace", "workspace",
@ -2078,6 +2137,18 @@ dependencies = [
"workspace", "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]] [[package]]
name = "fixedbitset" name = "fixedbitset"
version = "0.4.2" version = "0.4.2"
@ -2526,6 +2597,18 @@ dependencies = [
"regex", "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]] [[package]]
name = "go_to_line" name = "go_to_line"
version = "0.1.0" version = "0.1.0"
@ -2591,6 +2674,7 @@ dependencies = [
"tiny-skia", "tiny-skia",
"usvg", "usvg",
"util", "util",
"uuid 1.2.2",
"waker-fn", "waker-fn",
] ]
@ -3141,6 +3225,15 @@ dependencies = [
"arrayvec 0.7.2", "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]] [[package]]
name = "language" name = "language"
version = "0.1.0" version = "0.1.0"
@ -3158,6 +3251,7 @@ dependencies = [
"fuzzy", "fuzzy",
"git", "git",
"gpui", "gpui",
"indoc",
"lazy_static", "lazy_static",
"log", "log",
"lsp", "lsp",
@ -3180,10 +3274,12 @@ dependencies = [
"tree-sitter-html", "tree-sitter-html",
"tree-sitter-javascript", "tree-sitter-javascript",
"tree-sitter-json 0.19.0", "tree-sitter-json 0.19.0",
"tree-sitter-markdown",
"tree-sitter-python", "tree-sitter-python",
"tree-sitter-ruby", "tree-sitter-ruby",
"tree-sitter-rust", "tree-sitter-rust",
"tree-sitter-typescript", "tree-sitter-typescript 0.20.1",
"unicase",
"unindent", "unindent",
"util", "util",
] ]
@ -6012,6 +6108,7 @@ dependencies = [
"parking_lot 0.11.2", "parking_lot 0.11.2",
"smol", "smol",
"thread_local", "thread_local",
"uuid 1.2.2",
] ]
[[package]] [[package]]
@ -6461,6 +6558,7 @@ dependencies = [
"settings", "settings",
"smol", "smol",
"theme", "theme",
"util",
"workspace", "workspace",
] ]
@ -6907,7 +7005,7 @@ dependencies = [
[[package]] [[package]]
name = "tree-sitter" name = "tree-sitter"
version = "0.20.9" 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 = [ dependencies = [
"cc", "cc",
"regex", "regex",
@ -7009,6 +7107,16 @@ dependencies = [
"tree-sitter", "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]] [[package]]
name = "tree-sitter-markdown" name = "tree-sitter-markdown"
version = "0.0.1" version = "0.0.1"
@ -7085,6 +7193,24 @@ dependencies = [
"tree-sitter", "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]] [[package]]
name = "try-lock" name = "try-lock"
version = "0.2.3" version = "0.2.3"
@ -7322,6 +7448,12 @@ dependencies = [
"tempdir", "tempdir",
] ]
[[package]]
name = "uuid"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bcc7e3b898aa6f6c08e5295b6c89258d1331e9ac578cc992fb818759951bdc22"
[[package]] [[package]]
name = "uuid" name = "uuid"
version = "0.8.2" version = "0.8.2"
@ -8167,6 +8299,7 @@ dependencies = [
"smallvec", "smallvec",
"theme", "theme",
"util", "util",
"uuid 1.2.2",
] ]
[[package]] [[package]]
@ -8179,6 +8312,15 @@ dependencies = [
"winapi-build", "winapi-build",
] ]
[[package]]
name = "xattr"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d1526bbe5aaeb5eb06885f4d987bcdfa5e23187055de9b83fe00156a821fabc"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "xml-rs" name = "xml-rs"
version = "0.8.4" version = "0.8.4"
@ -8214,13 +8356,14 @@ checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"
[[package]] [[package]]
name = "zed" name = "zed"
version = "0.71.0" version = "0.75.0"
dependencies = [ dependencies = [
"activity_indicator", "activity_indicator",
"anyhow", "anyhow",
"assets", "assets",
"async-compression", "async-compression",
"async-recursion 0.3.2", "async-recursion 0.3.2",
"async-tar",
"async-trait", "async-trait",
"auto_update", "auto_update",
"backtrace", "backtrace",
@ -8298,6 +8441,7 @@ dependencies = [
"tree-sitter-go", "tree-sitter-go",
"tree-sitter-html", "tree-sitter-html",
"tree-sitter-json 0.20.0", "tree-sitter-json 0.20.0",
"tree-sitter-lua",
"tree-sitter-markdown", "tree-sitter-markdown",
"tree-sitter-python", "tree-sitter-python",
"tree-sitter-racket", "tree-sitter-racket",
@ -8305,10 +8449,13 @@ dependencies = [
"tree-sitter-rust", "tree-sitter-rust",
"tree-sitter-scheme", "tree-sitter-scheme",
"tree-sitter-toml", "tree-sitter-toml",
"tree-sitter-typescript", "tree-sitter-typescript 0.20.2",
"tree-sitter-yaml",
"unindent", "unindent",
"url", "url",
"urlencoding",
"util", "util",
"uuid 1.2.2",
"vim", "vim",
"workspace", "workspace",
] ]

View file

@ -69,7 +69,7 @@ serde_json = { version = "1.0", features = ["preserve_order", "raw_value"] }
rand = { version = "0.8" } rand = { version = "0.8" }
[patch.crates-io] [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" } 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 # 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] [profile.release]
debug = true debug = true

View file

@ -5,6 +5,7 @@ WORKDIR app
COPY . . COPY . .
# Compile collab server # Compile collab server
ARG CARGO_PROFILE_RELEASE_PANIC=abort
RUN --mount=type=cache,target=./script/node_modules \ RUN --mount=type=cache,target=./script/node_modules \
--mount=type=cache,target=/usr/local/cargo/registry \ --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=./target \ --mount=type=cache,target=./target \

View file

@ -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. 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 ### Wasm Plugins

View file

@ -38,7 +38,7 @@
"cmd-n": "workspace::NewFile", "cmd-n": "workspace::NewFile",
"cmd-shift-n": "workspace::NewWindow", "cmd-shift-n": "workspace::NewWindow",
"cmd-o": "workspace::Open", "cmd-o": "workspace::Open",
"alt-cmd-o": "recent_projects::Toggle", "alt-cmd-o": "projects::OpenRecent",
"ctrl-`": "workspace::NewTerminal" "ctrl-`": "workspace::NewTerminal"
} }
}, },
@ -164,6 +164,7 @@
"bindings": { "bindings": {
"enter": "editor::Newline", "enter": "editor::Newline",
"cmd-enter": "editor::NewlineBelow", "cmd-enter": "editor::NewlineBelow",
"alt-z": "editor::ToggleSoftWrap",
"cmd-f": [ "cmd-f": [
"buffer_search::Deploy", "buffer_search::Deploy",
{ {
@ -227,7 +228,12 @@
"replace_newest": true "replace_newest": true
} }
], ],
"cmd-/": "editor::ToggleComments", "cmd-/": [
"editor::ToggleComments",
{
"advance_downwards": false
}
],
"alt-up": "editor::SelectLargerSyntaxNode", "alt-up": "editor::SelectLargerSyntaxNode",
"alt-down": "editor::SelectSmallerSyntaxNode", "alt-down": "editor::SelectSmallerSyntaxNode",
"cmd-u": "editor::UndoSelection", "cmd-u": "editor::UndoSelection",
@ -433,8 +439,7 @@
{ {
"context": "Workspace", "context": "Workspace",
"bindings": { "bindings": {
"shift-escape": "dock::FocusDock", "shift-escape": "dock::FocusDock"
"cmd-shift-b": "workspace::ToggleRightSidebar"
} }
}, },
{ {
@ -445,15 +450,16 @@
} }
}, },
{ {
"context": "Dock", "context": "Pane",
"bindings": { "bindings": {
"shift-escape": "dock::HideDock" "cmd-escape": "dock::AddTabToDock"
} }
}, },
{ {
"context": "Pane", "context": "Dock",
"bindings": { "bindings": {
"cmd-escape": "dock::MoveActiveItemToDock" "shift-escape": "dock::HideDock",
"cmd-escape": "dock::RemoveTabFromDock"
} }
}, },
{ {

View file

@ -1 +0,0 @@
[]

View file

@ -315,7 +315,9 @@
{ {
"context": "Editor && VimWaiting", "context": "Editor && VimWaiting",
"bindings": { "bindings": {
"*": "gpui::KeyPressed" "tab": "vim::Tab",
"enter": "vim::Enter",
"escape": "editor::Cancel"
} }
} }
] ]

View file

@ -20,6 +20,8 @@
// Whether to pop the completions menu while typing in an editor without // Whether to pop the completions menu while typing in an editor without
// explicitly requesting it. // explicitly requesting it.
"show_completions_on_input": true, "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 // Whether new projects should start out 'online'. Online projects
// appear in the contacts panel under your name, so that your contacts // appear in the contacts panel under your name, so that your contacts
// can see which projects you are working on. Regardless of this // 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. // Send anonymized usage data like what languages you're using Zed with.
"metrics": true "metrics": true
}, },
// Automatically update Zed
"auto_update": true,
// Git gutter behavior configuration. // Git gutter behavior configuration.
"git": { "git": {
// Control whether the git gutter is shown. May take 2 values: // Control whether the git gutter is shown. May take 2 values:
@ -219,6 +223,9 @@
}, },
"TSX": { "TSX": {
"tab_size": 2 "tab_size": 2
},
"YAML": {
"tab_size": 2
} }
}, },
// LSP Specific settings. // LSP Specific settings.

View file

@ -252,7 +252,11 @@ impl ActivityIndicator {
"Installing Zed update…".to_string(), "Installing Zed update…".to_string(),
None, 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 => ( AutoUpdateStatus::Errored => (
Some(WARNING_ICON), Some(WARNING_ICON),
"Auto update failed".to_string(), "Auto update failed".to_string(),

View file

@ -2,15 +2,16 @@ mod update_notification;
use anyhow::{anyhow, Context, Result}; use anyhow::{anyhow, Context, Result};
use client::{http::HttpClient, ZED_SECRET_CLIENT_TOKEN}; use client::{http::HttpClient, ZED_SECRET_CLIENT_TOKEN};
use client::{ZED_APP_PATH, ZED_APP_VERSION};
use db::kvp::KEY_VALUE_STORE; use db::kvp::KEY_VALUE_STORE;
use gpui::{ use gpui::{
actions, platform::AppVersion, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, actions, platform::AppVersion, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle,
MutableAppContext, Task, WeakViewHandle, MutableAppContext, Task, WeakViewHandle,
}; };
use lazy_static::lazy_static;
use serde::Deserialize; use serde::Deserialize;
use settings::Settings;
use smol::{fs::File, io::AsyncReadExt, process::Command}; 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 update_notification::UpdateNotification;
use util::channel::ReleaseChannel; use util::channel::ReleaseChannel;
use workspace::Workspace; use workspace::Workspace;
@ -18,13 +19,6 @@ use workspace::Workspace;
const SHOULD_SHOW_UPDATE_NOTIFICATION_KEY: &str = "auto-updater-should-show-updated-notification"; const SHOULD_SHOW_UPDATE_NOTIFICATION_KEY: &str = "auto-updater-should-show-updated-notification";
const POLL_INTERVAL: Duration = Duration::from_secs(60 * 60); const POLL_INTERVAL: Duration = Duration::from_secs(60 * 60);
lazy_static! {
pub static ref ZED_APP_VERSION: Option<AppVersion> = env::var("ZED_APP_VERSION")
.ok()
.and_then(|v| v.parse().ok());
pub static ref ZED_APP_PATH: Option<PathBuf> = env::var("ZED_APP_PATH").ok().map(PathBuf::from);
}
actions!(auto_update, [Check, DismissErrorMessage, ViewReleaseNotes]); actions!(auto_update, [Check, DismissErrorMessage, ViewReleaseNotes]);
#[derive(Clone, Copy, PartialEq, Eq)] #[derive(Clone, Copy, PartialEq, Eq)]
@ -60,7 +54,23 @@ pub fn init(http_client: Arc<dyn HttpClient>, server_url: String, cx: &mut Mutab
let server_url = server_url; let server_url = server_url;
let auto_updater = cx.add_model(|cx| { let auto_updater = cx.add_model(|cx| {
let updater = AutoUpdater::new(version, http_client, server_url.clone()); let updater = AutoUpdater::new(version, http_client, server_url.clone());
updater.start_polling(cx).detach();
let mut update_subscription = cx
.global::<Settings>()
.auto_update
.then(|| updater.start_polling(cx));
cx.observe_global::<Settings, _>(move |updater, cx| {
if cx.global::<Settings>().auto_update {
if update_subscription.is_none() {
*(&mut update_subscription) = Some(updater.start_polling(cx))
}
} else {
(&mut update_subscription).take();
}
})
.detach();
updater updater
}); });
cx.set_global(Some(auto_updater)); cx.set_global(Some(auto_updater));

View file

@ -28,6 +28,7 @@ fs = { path = "../fs" }
language = { path = "../language" } language = { path = "../language" }
media = { path = "../media" } media = { path = "../media" }
project = { path = "../project" } project = { path = "../project" }
settings = { path = "../settings" }
util = { path = "../util" } util = { path = "../util" }
anyhow = "1.0.38" anyhow = "1.0.38"

View file

@ -1,18 +1,22 @@
pub mod participant; pub mod participant;
pub mod room; pub mod room;
use std::sync::Arc;
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use client::{proto, Client, TypedEnvelope, User, UserStore}; use client::{proto, Client, TypedEnvelope, User, UserStore};
use collections::HashSet; use collections::HashSet;
use futures::{future::Shared, FutureExt};
use postage::watch;
use gpui::{ use gpui::{
AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext,
Subscription, Task, WeakModelHandle, Subscription, Task, WeakModelHandle,
}; };
pub use participant::ParticipantLocation;
use postage::watch;
use project::Project; use project::Project;
pub use participant::ParticipantLocation;
pub use room::Room; pub use room::Room;
use std::sync::Arc;
pub fn init(client: Arc<Client>, user_store: ModelHandle<UserStore>, cx: &mut MutableAppContext) { pub fn init(client: Arc<Client>, user_store: ModelHandle<UserStore>, cx: &mut MutableAppContext) {
let active_call = cx.add_model(|cx| ActiveCall::new(client, user_store, cx)); let active_call = cx.add_model(|cx| ActiveCall::new(client, user_store, cx));
@ -27,8 +31,10 @@ pub struct IncomingCall {
pub initial_project: Option<proto::ParticipantProject>, pub initial_project: Option<proto::ParticipantProject>,
} }
/// Singleton global maintaining the user's participation in a room across workspaces.
pub struct ActiveCall { pub struct ActiveCall {
room: Option<(ModelHandle<Room>, Vec<Subscription>)>, room: Option<(ModelHandle<Room>, Vec<Subscription>)>,
pending_room_creation: Option<Shared<Task<Result<ModelHandle<Room>, Arc<anyhow::Error>>>>>,
location: Option<WeakModelHandle<Project>>, location: Option<WeakModelHandle<Project>>,
pending_invites: HashSet<u64>, pending_invites: HashSet<u64>,
incoming_call: ( incoming_call: (
@ -52,6 +58,7 @@ impl ActiveCall {
) -> Self { ) -> Self {
Self { Self {
room: None, room: None,
pending_room_creation: None,
location: None, location: None,
pending_invites: Default::default(), pending_invites: Default::default(),
incoming_call: watch::channel(), incoming_call: watch::channel(),
@ -120,45 +127,74 @@ impl ActiveCall {
initial_project: Option<ModelHandle<Project>>, initial_project: Option<ModelHandle<Project>>,
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
) -> Task<Result<()>> { ) -> Task<Result<()>> {
let client = self.client.clone();
let user_store = self.user_store.clone();
if !self.pending_invites.insert(called_user_id) { if !self.pending_invites.insert(called_user_id) {
return Task::ready(Err(anyhow!("user was already invited"))); return Task::ready(Err(anyhow!("user was already invited")));
} }
cx.notify(); cx.notify();
cx.spawn(|this, mut cx| async move {
let invite = async { let room = if let Some(room) = self.room().cloned() {
if let Some(room) = this.read_with(&cx, |this, _| this.room().cloned()) { Some(Task::ready(Ok(room)).shared())
let initial_project_id = if let Some(initial_project) = initial_project { } else {
Some( self.pending_room_creation.clone()
room.update(&mut cx, |room, cx| { };
room.share_project(initial_project, cx)
}) 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?, .await?,
) )
} else {
None
};
room.update(&mut cx, |room, cx| {
room.call(called_user_id, initial_project_id, cx)
})
.await?;
} else { } else {
let room = cx None
.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?;
}; };
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; let result = invite.await;
this.update(&mut cx, |this, cx| { this.update(&mut cx, |this, cx| {
this.pending_invites.remove(&called_user_id); this.pending_invites.remove(&called_user_id);

View file

@ -15,7 +15,7 @@ use futures::{future::LocalBoxFuture, AsyncReadExt, FutureExt, SinkExt, StreamEx
use gpui::{ use gpui::{
actions, actions,
serde_json::{self, Value}, serde_json::{self, Value},
AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, AnyWeakViewHandle, AppContext, AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, AnyWeakViewHandle, AppContext, AppVersion,
AsyncAppContext, Entity, ModelHandle, MutableAppContext, Task, View, ViewContext, ViewHandle, AsyncAppContext, Entity, ModelHandle, MutableAppContext, Task, View, ViewContext, ViewHandle,
}; };
use http::HttpClient; use http::HttpClient;
@ -55,6 +55,11 @@ lazy_static! {
pub static ref ADMIN_API_TOKEN: Option<String> = std::env::var("ZED_ADMIN_API_TOKEN") pub static ref ADMIN_API_TOKEN: Option<String> = std::env::var("ZED_ADMIN_API_TOKEN")
.ok() .ok()
.and_then(|s| if s.is_empty() { None } else { Some(s) }); .and_then(|s| if s.is_empty() { None } else { Some(s) });
pub static ref ZED_APP_VERSION: Option<AppVersion> = std::env::var("ZED_APP_VERSION")
.ok()
.and_then(|v| v.parse().ok());
pub static ref ZED_APP_PATH: Option<PathBuf> =
std::env::var("ZED_APP_PATH").ok().map(PathBuf::from);
} }
pub const ZED_SECRET_CLIENT_TOKEN: &str = "618033988749894"; pub const ZED_SECRET_CLIENT_TOKEN: &str = "618033988749894";
@ -1319,6 +1324,10 @@ impl Client {
pub fn metrics_id(&self) -> Option<Arc<str>> { pub fn metrics_id(&self) -> Option<Arc<str>> {
self.telemetry.metrics_id() self.telemetry.metrics_id()
} }
pub fn is_staff(&self) -> Option<bool> {
self.telemetry.is_staff()
}
} }
impl WeakSubscriber { impl WeakSubscriber {

View file

@ -9,7 +9,7 @@ pub use isahc::{
Error, Error,
}; };
use smol::future::FutureExt; use smol::future::FutureExt;
use std::sync::Arc; use std::{sync::Arc, time::Duration};
pub use url::Url; pub use url::Url;
pub type Request = isahc::Request<AsyncBody>; pub type Request = isahc::Request<AsyncBody>;
@ -41,7 +41,13 @@ pub trait HttpClient: Send + Sync {
} }
pub fn client() -> Arc<dyn HttpClient> { pub fn client() -> Arc<dyn HttpClient> {
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 { impl HttpClient for isahc::HttpClient {

View file

@ -40,6 +40,7 @@ struct TelemetryState {
next_event_id: usize, next_event_id: usize,
flush_task: Option<Task<()>>, flush_task: Option<Task<()>>,
log_file: Option<NamedTempFile>, log_file: Option<NamedTempFile>,
is_staff: Option<bool>,
} }
const MIXPANEL_EVENTS_URL: &'static str = "https://api.mixpanel.com/track"; const MIXPANEL_EVENTS_URL: &'static str = "https://api.mixpanel.com/track";
@ -125,6 +126,7 @@ impl Telemetry {
flush_task: Default::default(), flush_task: Default::default(),
next_event_id: 0, next_event_id: 0,
log_file: None, log_file: None,
is_staff: None,
}), }),
}); });
@ -202,6 +204,7 @@ impl Telemetry {
let device_id = state.device_id.clone(); let device_id = state.device_id.clone();
let metrics_id: Option<Arc<str>> = metrics_id.map(|id| id.into()); let metrics_id: Option<Arc<str>> = metrics_id.map(|id| id.into());
state.metrics_id = metrics_id.clone(); state.metrics_id = metrics_id.clone();
state.is_staff = Some(is_staff);
drop(state); drop(state);
if let Some((token, device_id)) = MIXPANEL_TOKEN.as_ref().zip(device_id) { 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() self.state.lock().metrics_id.clone()
} }
pub fn is_staff(self: &Arc<Self>) -> Option<bool> {
self.state.lock().is_staff
}
fn flush(self: &Arc<Self>) { fn flush(self: &Arc<Self>) {
let mut state = self.state.lock(); let mut state = self.state.lock();
let mut events = mem::take(&mut state.queue); let mut events = mem::take(&mut state.queue);

View file

@ -7,7 +7,7 @@ use postage::{sink::Sink, watch};
use rpc::proto::{RequestMessage, UsersResponse}; use rpc::proto::{RequestMessage, UsersResponse};
use settings::Settings; use settings::Settings;
use std::sync::{Arc, Weak}; use std::sync::{Arc, Weak};
use util::TryFutureExt as _; use util::{StaffMode, TryFutureExt as _};
#[derive(Default, Debug)] #[derive(Default, Debug)]
pub struct User { pub struct User {
@ -148,6 +148,19 @@ impl UserStore {
cx.read(|cx| cx.global::<Settings>().telemetry()), cx.read(|cx| cx.global::<Settings>().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(); current_user_tx.send(user).await.ok();
} }
} }

View file

@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"]
default-run = "collab" default-run = "collab"
edition = "2021" edition = "2021"
name = "collab" name = "collab"
version = "0.5.3" version = "0.5.4"
publish = false publish = false
[[bin]] [[bin]]

View file

@ -595,7 +595,16 @@ impl Database {
.await .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<bool> {
self.transaction(|tx| async move { self.transaction(|tx| async move {
let (id_a, id_b) = if responder_id < requester_id { let (id_a, id_b) = if responder_id < requester_id {
(responder_id, requester_id) (responder_id, requester_id)
@ -603,20 +612,18 @@ impl Database {
(requester_id, responder_id) (requester_id, responder_id)
}; };
let result = contact::Entity::delete_many() let contact = contact::Entity::find()
.filter( .filter(
contact::Column::UserIdA contact::Column::UserIdA
.eq(id_a) .eq(id_a)
.and(contact::Column::UserIdB.eq(id_b)), .and(contact::Column::UserIdB.eq(id_b)),
) )
.exec(&*tx) .one(&*tx)
.await?; .await?
.ok_or_else(|| anyhow!("no such contact"))?;
if result.rows_affected == 1 { contact::Entity::delete_by_id(contact.id).exec(&*tx).await?;
Ok(()) Ok(contact.accepted)
} else {
Err(anyhow!("no such contact"))?
}
}) })
.await .await
} }
@ -1586,12 +1593,8 @@ impl Database {
.filter( .filter(
Condition::all() Condition::all()
.add( .add(
room_participant::Column::CallingConnectionId room_participant::Column::CallingUserId
.eq(connection.id as i32), .eq(leaving_participant.user_id),
)
.add(
room_participant::Column::CallingConnectionServerId
.eq(connection.owner_id as i32),
) )
.add(room_participant::Column::AnsweringConnectionId.is_null()), .add(room_participant::Column::AnsweringConnectionId.is_null()),
) )
@ -1917,7 +1920,9 @@ impl Database {
}; };
if let Some(db_worktree) = db_worktree { 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);
}
} }
} }
} }

View file

@ -1961,23 +1961,31 @@ async fn remove_contact(
let requester_id = session.user_id; let requester_id = session.user_id;
let responder_id = UserId::from_proto(request.user_id); let responder_id = UserId::from_proto(request.user_id);
let db = session.db().await; 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; let pool = session.connection_pool().await;
// Update outgoing contact requests of requester // Update outgoing contact requests of requester
let mut update = proto::UpdateContacts::default(); let mut update = proto::UpdateContacts::default();
update if contact_accepted {
.remove_outgoing_requests update.remove_contacts.push(responder_id.to_proto());
.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) { for connection_id in pool.user_connection_ids(requester_id) {
session.peer.send(connection_id, update.clone())?; session.peer.send(connection_id, update.clone())?;
} }
// Update incoming contact requests of responder // Update incoming contact requests of responder
let mut update = proto::UpdateContacts::default(); let mut update = proto::UpdateContacts::default();
update if contact_accepted {
.remove_incoming_requests update.remove_contacts.push(requester_id.to_proto());
.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) { for connection_id in pool.user_connection_ids(responder_id) {
session.peer.send(connection_id, update.clone())?; session.peer.send(connection_id, update.clone())?;
} }

View file

@ -11,7 +11,7 @@ use client::{
EstablishConnectionError, UserStore, EstablishConnectionError, UserStore,
}; };
use collections::{HashMap, HashSet}; use collections::{HashMap, HashSet};
use fs::{FakeFs, HomeDir}; use fs::FakeFs;
use futures::{channel::oneshot, StreamExt as _}; use futures::{channel::oneshot, StreamExt as _};
use gpui::{ use gpui::{
executor::Deterministic, test::EmptyView, ModelHandle, Task, TestAppContext, ViewHandle, 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 { async fn create_client(&mut self, cx: &mut TestAppContext, name: &str) -> TestClient {
cx.update(|cx| { cx.update(|cx| {
cx.set_global(HomeDir(Path::new("/tmp/").to_path_buf()));
cx.set_global(Settings::test(cx)); cx.set_global(Settings::test(cx));
}); });
@ -197,7 +196,7 @@ impl TestServer {
languages: Arc::new(LanguageRegistry::new(Task::ready(()))), languages: Arc::new(LanguageRegistry::new(Task::ready(()))),
themes: ThemeRegistry::new((), cx.font_cache()), themes: ThemeRegistry::new((), cx.font_cache()),
fs: fs.clone(), fs: fs.clone(),
build_window_options: Default::default, build_window_options: |_, _, _| Default::default(),
initialize_workspace: |_, _, _| unimplemented!(), initialize_workspace: |_, _, _| unimplemented!(),
dock_default_item_factory: |_, _| unimplemented!(), dock_default_item_factory: |_, _| unimplemented!(),
}); });

View file

@ -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 // User A shares their screen
let display = MacOSDisplay::new(); let display = MacOSDisplay::new();
let events_b = active_call_events(cx_b); let events_b = active_call_events(cx_b);
let events_c = active_call_events(cx_c);
active_call_a active_call_a
.update(cx_a, |call, cx| { .update(cx_a, |call, cx| {
call.room().unwrap().update(cx, |room, cx| { call.room().unwrap().update(cx, |room, cx| {
@ -181,9 +239,10 @@ async fn test_basic_calls(
deterministic.run_until_parked(); deterministic.run_until_parked();
// User B observes the remote screen sharing track.
assert_eq!(events_b.borrow().len(), 1); assert_eq!(events_b.borrow().len(), 1);
let event = events_b.borrow().first().unwrap().clone(); let event_b = events_b.borrow().first().unwrap().clone();
if let call::room::Event::RemoteVideoTracksChanged { participant_id } = event { if let call::room::Event::RemoteVideoTracksChanged { participant_id } = event_b {
assert_eq!(participant_id, client_a.peer_id().unwrap()); assert_eq!(participant_id, client_a.peer_id().unwrap());
room_b.read_with(cx_b, |room, _| { room_b.read_with(cx_b, |room, _| {
assert_eq!( assert_eq!(
@ -197,6 +256,23 @@ async fn test_basic_calls(
panic!("unexpected event") 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. // User A leaves the room.
active_call_a.update(cx_a, |call, cx| { active_call_a.update(cx_a, |call, cx| {
call.hang_up(cx).unwrap(); call.hang_up(cx).unwrap();
@ -213,18 +289,28 @@ async fn test_basic_calls(
assert_eq!( assert_eq!(
room_participants(&room_b, cx_b), room_participants(&room_b, cx_b),
RoomParticipants { 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() pending: Default::default()
} }
); );
// User B gets disconnected from the LiveKit server, which causes them // 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 server
.test_live_kit_server .test_live_kit_server
.disconnect_client(client_b.peer_id().unwrap().to_string()) .disconnect_client(client_b.user_id().unwrap().to_string())
.await; .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!( assert_eq!(
room_participants(&room_a, cx_a), room_participants(&room_a, cx_a),
RoomParticipants { RoomParticipants {
@ -239,6 +325,141 @@ async fn test_basic_calls(
pending: Default::default() 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<Deterministic>,
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)] #[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. // 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)); buffer_a.update(cx_a, |buf, cx| buf.edit([(0..0, "hi-a, ")], None, cx));
save_b.await.unwrap(); save_b.await.unwrap();
assert_eq!( 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.file().unwrap().path().to_str(), Some("file1.js"));
assert_eq!(&*buffer.language().unwrap().name(), "JavaScript"); 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)] #[gpui::test(iterations = 10)]
@ -2571,6 +2827,8 @@ async fn test_fs_operations(
}) })
.await .await
.unwrap(); .unwrap();
deterministic.run_until_parked();
worktree_a.read_with(cx_a, |worktree, _| { worktree_a.read_with(cx_a, |worktree, _| {
assert_eq!( assert_eq!(
worktree worktree
@ -2659,7 +2917,9 @@ async fn test_buffer_conflict_after_save(
assert!(!buf.has_conflict()); 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(); cx_a.foreground().forbid_parking();
buffer_b.read_with(cx_b, |buffer_b, _| assert!(!buffer_b.is_dirty())); buffer_b.read_with(cx_b, |buffer_b, _| assert!(!buffer_b.is_dirty()));
buffer_b.read_with(cx_b, |buf, _| { buffer_b.read_with(cx_b, |buf, _| {
@ -5291,6 +5551,27 @@ async fn test_contacts(
[("user_b".to_string(), "online", "free")] [("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( fn contacts(
client: &TestClient, client: &TestClient,
cx: &TestAppContext, cx: &TestAppContext,
@ -5602,7 +5883,6 @@ async fn test_following(
.downcast::<Editor>() .downcast::<Editor>()
.unwrap() .unwrap()
}); });
assert!(cx_b.read(|cx| editor_b2.is_focused(cx)));
assert_eq!( assert_eq!(
cx_b.read(|cx| editor_b2.project_path(cx)), cx_b.read(|cx| editor_b2.project_path(cx)),
Some((worktree_id, "2.txt").into()) Some((worktree_id, "2.txt").into())

View file

@ -397,16 +397,18 @@ async fn apply_server_operation(
log::info!("Added connection for {}", username); log::info!("Added connection for {}", username);
} }
Operation::RemoveConnection { user_id } => { Operation::RemoveConnection {
log::info!("Simulating full disconnection of user {}", user_id); user_id: removed_user_id,
} => {
log::info!("Simulating full disconnection of user {}", removed_user_id);
let client_ix = clients let client_ix = clients
.iter() .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 Some(client_ix) = client_ix else { return false };
let user_connection_ids = server let user_connection_ids = server
.connection_pool .connection_pool
.lock() .lock()
.user_connection_ids(user_id) .user_connection_ids(removed_user_id)
.collect::<Vec<_>>(); .collect::<Vec<_>>();
assert_eq!(user_connection_ids.len(), 1); assert_eq!(user_connection_ids.len(), 1);
let removed_peer_id = user_connection_ids[0].into(); let removed_peer_id = user_connection_ids[0].into();
@ -417,7 +419,7 @@ async fn apply_server_operation(
server.disconnect_client(removed_peer_id); server.disconnect_client(removed_peer_id);
deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
deterministic.start_waiting(); 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; client_task.await;
deterministic.finish_waiting(); deterministic.finish_waiting();
server.allow_connections(); server.allow_connections();
@ -441,19 +443,17 @@ async fn apply_server_operation(
.unwrap(); .unwrap();
let pool = server.connection_pool.lock(); let pool = server.connection_pool.lock();
for contact in contacts { for contact in contacts {
if let db::Contact::Accepted { user_id: id, .. } = contact { if let db::Contact::Accepted { user_id, busy, .. } = contact {
if pool.is_user_online(id) { if user_id == removed_user_id {
assert_ne!( assert!(!pool.is_user_online(user_id));
id, user_id, assert!(!busy);
"removed client is still a contact of another peer"
);
} }
} }
} }
} }
log::info!("{} removed", client.username); log::info!("{} removed", client.username);
plan.lock().user(user_id).online = false; plan.lock().user(removed_user_id).online = false;
client_cx.update(|cx| { client_cx.update(|cx| {
cx.clear_globals(); cx.clear_globals();
drop(client); drop(client);
@ -806,8 +806,8 @@ async fn apply_client_operation(
); );
ensure_project_shared(&project, client, cx).await; ensure_project_shared(&project, client, cx).await;
let (requested_version, save) = let requested_version = buffer.read_with(cx, |buffer, _| buffer.version());
buffer.update(cx, |buffer, cx| (buffer.version(), buffer.save(cx))); let save = project.update(cx, |project, cx| project.save_buffer(buffer, cx));
let save = cx.background().spawn(async move { let save = cx.background().spawn(async move {
let (saved_version, _, _) = save let (saved_version, _, _) = save
.await .await
@ -1972,15 +1972,3 @@ fn path_env_var(name: &str) -> Option<PathBuf> {
} }
Some(path) Some(path)
} }
async fn child_file_paths(client: &TestClient, dir_path: &Path) -> Vec<PathBuf> {
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
}

View file

@ -22,6 +22,7 @@ test-support = [
] ]
[dependencies] [dependencies]
auto_update = { path = "../auto_update" }
call = { path = "../call" } call = { path = "../call" }
client = { path = "../client" } client = { path = "../client" }
clock = { path = "../clock" } clock = { path = "../clock" }

View file

@ -1,4 +1,4 @@
use crate::{contact_notification::ContactNotification, contacts_popover}; use crate::{contact_notification::ContactNotification, contacts_popover, ToggleScreenSharing};
use call::{ActiveCall, ParticipantLocation}; use call::{ActiveCall, ParticipantLocation};
use client::{proto::PeerId, Authenticate, ContactEventKind, User, UserStore}; use client::{proto::PeerId, Authenticate, ContactEventKind, User, UserStore};
use clock::ReplicaId; use clock::ReplicaId;
@ -10,21 +10,17 @@ use gpui::{
geometry::{rect::RectF, vector::vec2f, PathBuilder}, geometry::{rect::RectF, vector::vec2f, PathBuilder},
json::{self, ToJson}, json::{self, ToJson},
Border, CursorStyle, Entity, ModelHandle, MouseButton, MutableAppContext, RenderContext, Border, CursorStyle, Entity, ModelHandle, MouseButton, MutableAppContext, RenderContext,
Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, Subscription, View, ViewContext, ViewHandle, WeakViewHandle,
}; };
use settings::Settings; use settings::Settings;
use std::ops::Range; use std::ops::Range;
use theme::Theme; use theme::Theme;
use workspace::{FollowNextCollaborator, JoinProject, ToggleFollow, Workspace}; use workspace::{FollowNextCollaborator, JoinProject, ToggleFollow, Workspace};
actions!( actions!(collab, [ToggleCollaborationMenu, ShareProject]);
collab,
[ToggleCollaborationMenu, ToggleScreenSharing, ShareProject]
);
pub fn init(cx: &mut MutableAppContext) { pub fn init(cx: &mut MutableAppContext) {
cx.add_action(CollabTitlebarItem::toggle_contacts_popover); cx.add_action(CollabTitlebarItem::toggle_contacts_popover);
cx.add_action(CollabTitlebarItem::toggle_screen_sharing);
cx.add_action(CollabTitlebarItem::share_project); cx.add_action(CollabTitlebarItem::share_project);
} }
@ -172,19 +168,6 @@ impl CollabTitlebarItem {
cx.notify(); cx.notify();
} }
pub fn toggle_screen_sharing(&mut self, _: &ToggleScreenSharing, cx: &mut ViewContext<Self>) {
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( fn render_toggle_contacts_button(
&self, &self,
theme: &Theme, theme: &Theme,
@ -521,7 +504,9 @@ impl CollabTitlebarItem {
workspace: &ViewHandle<Workspace>, workspace: &ViewHandle<Workspace>,
cx: &mut RenderContext<Self>, cx: &mut RenderContext<Self>,
) -> Option<ElementBox> { ) -> Option<ElementBox> {
let theme = &cx.global::<Settings>().theme; enum ConnectionStatusButton {}
let theme = &cx.global::<Settings>().theme.clone();
match &*workspace.read(cx).client().status().borrow() { match &*workspace.read(cx).client().status().borrow() {
client::Status::ConnectionError client::Status::ConnectionError
| client::Status::ConnectionLost | client::Status::ConnectionLost
@ -544,13 +529,20 @@ impl CollabTitlebarItem {
.boxed(), .boxed(),
), ),
client::Status::UpgradeRequired => Some( client::Status::UpgradeRequired => Some(
Label::new( MouseEventHandler::<ConnectionStatusButton>::new(0, cx, |_, _| {
"Please update Zed to collaborate".to_string(), Label::new(
theme.workspace.titlebar.outdated_warning.text.clone(), "Please update Zed to collaborate".to_string(),
) theme.workspace.titlebar.outdated_warning.text.clone(),
.contained() )
.with_style(theme.workspace.titlebar.outdated_warning.container) .contained()
.aligned() .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(), .boxed(),
), ),
_ => None, _ => None,

View file

@ -6,14 +6,17 @@ mod contacts_popover;
mod incoming_call_notification; mod incoming_call_notification;
mod notifications; mod notifications;
mod project_shared_notification; mod project_shared_notification;
mod sharing_status_indicator;
use anyhow::anyhow; use anyhow::anyhow;
use call::ActiveCall; use call::ActiveCall;
pub use collab_titlebar_item::{CollabTitlebarItem, ToggleCollaborationMenu}; pub use collab_titlebar_item::{CollabTitlebarItem, ToggleCollaborationMenu};
use gpui::MutableAppContext; use gpui::{actions, MutableAppContext, Task};
use std::sync::Arc; use std::sync::Arc;
use workspace::{AppState, JoinProject, ToggleFollow, Workspace}; use workspace::{AppState, JoinProject, ToggleFollow, Workspace};
actions!(collab, [ToggleScreenSharing]);
pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) { pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
collab_titlebar_item::init(cx); collab_titlebar_item::init(cx);
contact_notification::init(cx); contact_notification::init(cx);
@ -22,39 +25,60 @@ pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
contacts_popover::init(cx); contacts_popover::init(cx);
incoming_call_notification::init(cx); incoming_call_notification::init(cx);
project_shared_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| { cx.add_global_action(move |action: &JoinProject, cx| {
let project_id = action.project_id; join_project(action, app_state.clone(), cx);
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::<Workspace>(window_id))
.find(|workspace| {
workspace.read(cx).project().read(cx).remote_id() == Some(project_id)
})
});
let workspace = if let Some(existing_workspace) = existing_workspace { pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut MutableAppContext) {
existing_workspace 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 { } else {
let active_call = cx.read(ActiveCall::global); room.share_screen(cx)
let room = active_call }
.read_with(&cx, |call, _| call.room().cloned()) });
.ok_or_else(|| anyhow!("not in a call"))?; toggle_screen_sharing.detach_and_log_err(cx);
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)(), |cx| { fn join_project(action: &JoinProject, app_state: Arc<AppState>, 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::<Workspace>(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( let mut workspace = Workspace::new(
Default::default(), Default::default(),
0, 0,
@ -64,44 +88,44 @@ pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
); );
(app_state.initialize_workspace)(&mut workspace, &app_state, cx); (app_state.initialize_workspace)(&mut workspace, &app_state, cx);
workspace workspace
}); },
workspace );
}; workspace
};
cx.activate_window(workspace.window_id()); cx.activate_window(workspace.window_id());
cx.platform().activate(true); cx.platform().activate(true);
workspace.update(&mut cx, |workspace, cx| { workspace.update(&mut cx, |workspace, cx| {
if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() { if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
let follow_peer_id = room let follow_peer_id = room
.read(cx) .read(cx)
.remote_participants() .remote_participants()
.iter() .iter()
.find(|(_, participant)| participant.user.id == follow_user_id) .find(|(_, participant)| participant.user.id == follow_user_id)
.map(|(_, p)| p.peer_id) .map(|(_, p)| p.peer_id)
.or_else(|| { .or_else(|| {
// If we couldn't follow the given user, follow the host instead. // If we couldn't follow the given user, follow the host instead.
let collaborator = workspace let collaborator = workspace
.project() .project()
.read(cx) .read(cx)
.collaborators() .collaborators()
.values() .values()
.find(|collaborator| collaborator.replica_id == 0)?; .find(|collaborator| collaborator.replica_id == 0)?;
Some(collaborator.peer_id) Some(collaborator.peer_id)
}); });
if let Some(follow_peer_id) = follow_peer_id { if let Some(follow_peer_id) = follow_peer_id {
if !workspace.is_following(follow_peer_id) { if !workspace.is_following(follow_peer_id) {
workspace workspace
.toggle_follow(&ToggleFollow(follow_peer_id), cx) .toggle_follow(&ToggleFollow(follow_peer_id), cx)
.map(|follow| follow.detach_and_log_err(cx)); .map(|follow| follow.detach_and_log_err(cx));
}
} }
} }
}); }
});
anyhow::Ok(()) anyhow::Ok(())
}) })
.detach_and_log_err(cx); .detach_and_log_err(cx);
});
} }

View file

@ -1,22 +1,22 @@
use std::{mem, sync::Arc};
use crate::contacts_popover; use crate::contacts_popover;
use call::ActiveCall; use call::ActiveCall;
use client::{proto::PeerId, Contact, User, UserStore}; use client::{proto::PeerId, Contact, User, UserStore};
use editor::{Cancel, Editor}; use editor::{Cancel, Editor};
use futures::StreamExt;
use fuzzy::{match_strings, StringMatchCandidate}; use fuzzy::{match_strings, StringMatchCandidate};
use gpui::{ use gpui::{
elements::*, elements::*,
geometry::{rect::RectF, vector::vec2f}, geometry::{rect::RectF, vector::vec2f},
impl_actions, impl_internal_actions, impl_actions, impl_internal_actions,
keymap_matcher::KeymapContext, keymap_matcher::KeymapContext,
AppContext, CursorStyle, Entity, ModelHandle, MouseButton, MutableAppContext, RenderContext, AppContext, CursorStyle, Entity, ModelHandle, MouseButton, MutableAppContext, PromptLevel,
Subscription, View, ViewContext, ViewHandle, RenderContext, Subscription, View, ViewContext, ViewHandle,
}; };
use menu::{Confirm, SelectNext, SelectPrev}; use menu::{Confirm, SelectNext, SelectPrev};
use project::Project; use project::Project;
use serde::Deserialize; use serde::Deserialize;
use settings::Settings; use settings::Settings;
use std::{mem, sync::Arc};
use theme::IconButton; use theme::IconButton;
use util::ResultExt; use util::ResultExt;
use workspace::{JoinProject, OpenSharedScreen}; use workspace::{JoinProject, OpenSharedScreen};
@ -299,9 +299,19 @@ impl ContactList {
} }
fn remove_contact(&mut self, request: &RemoveContact, cx: &mut ViewContext<Self>) { fn remove_contact(&mut self, request: &RemoveContact, cx: &mut ViewContext<Self>) {
self.user_store let user_id = request.0;
.update(cx, |store, cx| store.remove_contact(request.0, cx)) let user_store = self.user_store.clone();
.detach(); 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( fn respond_to_contact_request(
@ -1051,7 +1061,7 @@ impl ContactList {
let user_id = contact.user.id; let user_id = contact.user.id;
let initial_project = project.clone(); let initial_project = project.clone();
let mut element = let mut element =
MouseEventHandler::<Contact>::new(contact.user.id as usize, cx, |_, _| { MouseEventHandler::<Contact>::new(contact.user.id as usize, cx, |_, cx| {
Flex::row() Flex::row()
.with_children(contact.user.avatar.clone().map(|avatar| { .with_children(contact.user.avatar.clone().map(|avatar| {
let status_badge = if contact.online { let status_badge = if contact.online {
@ -1093,6 +1103,27 @@ impl ContactList {
.flex(1., true) .flex(1., true)
.boxed(), .boxed(),
) )
.with_child(
MouseEventHandler::<Cancel>::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 { .with_children(if calling {
Some( Some(
Label::new("Calling".to_string(), theme.calling_indicator.text.clone()) Label::new("Calling".to_string(), theme.calling_indicator.text.clone())

View file

@ -48,7 +48,7 @@ impl View for ContactNotification {
ContactEventKind::Requested => render_user_notification( ContactEventKind::Requested => render_user_notification(
self.user.clone(), self.user.clone(),
"wants to add you as a contact", "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), Dismiss(self.user.id),
vec![ vec![
( (

View file

@ -32,11 +32,12 @@ pub fn init(cx: &mut MutableAppContext) {
}); });
for screen in cx.platform().screens() { for screen in cx.platform().screens() {
let screen_size = screen.size(); let screen_bounds = screen.bounds();
let (window_id, _) = cx.add_window( let (window_id, _) = cx.add_window(
WindowOptions { WindowOptions {
bounds: WindowBounds::Fixed(RectF::new( 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, window_size,
)), )),
titlebar: None, titlebar: None,

View file

@ -31,11 +31,11 @@ pub fn init(cx: &mut MutableAppContext) {
let window_size = vec2f(theme.window_width, theme.window_height); let window_size = vec2f(theme.window_width, theme.window_height);
for screen in cx.platform().screens() { for screen in cx.platform().screens() {
let screen_size = screen.size(); let screen_bounds = screen.bounds();
let (window_id, _) = cx.add_window( let (window_id, _) = cx.add_window(
WindowOptions { WindowOptions {
bounds: WindowBounds::Fixed(RectF::new( 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, window_size,
)), )),
titlebar: None, titlebar: None,

View file

@ -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::<Settings>().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::<Self>::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()
}
}

View file

@ -65,7 +65,7 @@ impl CommandPalette {
action, action,
keystrokes: bindings keystrokes: bindings
.iter() .iter()
.filter_map(|binding| binding.keystrokes()) .map(|binding| binding.keystrokes())
.last() .last()
.map_or(Vec::new(), |keystrokes| keystrokes.to_vec()), .map_or(Vec::new(), |keystrokes| keystrokes.to_vec()),
}) })

View file

@ -63,6 +63,7 @@ pub struct ContextMenu {
visible: bool, visible: bool,
previously_focused_view_id: Option<usize>, previously_focused_view_id: Option<usize>,
clicked: bool, clicked: bool,
parent_view_id: usize,
_actions_observation: Subscription, _actions_observation: Subscription,
} }
@ -114,6 +115,8 @@ impl View for ContextMenu {
impl ContextMenu { impl ContextMenu {
pub fn new(cx: &mut ViewContext<Self>) -> Self { pub fn new(cx: &mut ViewContext<Self>) -> Self {
let parent_view_id = cx.parent().unwrap();
Self { Self {
show_count: 0, show_count: 0,
anchor_position: Default::default(), anchor_position: Default::default(),
@ -123,6 +126,7 @@ impl ContextMenu {
visible: Default::default(), visible: Default::default(),
previously_focused_view_id: Default::default(), previously_focused_view_id: Default::default(),
clicked: false, clicked: false,
parent_view_id,
_actions_observation: cx.observe_actions(Self::action_dispatched), _actions_observation: cx.observe_actions(Self::action_dispatched),
} }
} }
@ -251,6 +255,7 @@ impl ContextMenu {
} }
fn render_menu_for_measurement(&self, cx: &mut RenderContext<Self>) -> impl Element { fn render_menu_for_measurement(&self, cx: &mut RenderContext<Self>) -> impl Element {
let window_id = cx.window_id();
let style = cx.global::<Settings>().theme.context_menu.clone(); let style = cx.global::<Settings>().theme.context_menu.clone();
Flex::row() Flex::row()
.with_child( .with_child(
@ -289,6 +294,8 @@ impl ContextMenu {
Some(ix) == self.selected_index, Some(ix) == self.selected_index,
); );
KeystrokeLabel::new( KeystrokeLabel::new(
window_id,
self.parent_view_id,
action.boxed_clone(), action.boxed_clone(),
style.keystroke.container, style.keystroke.container,
style.keystroke.text.clone(), style.keystroke.text.clone(),
@ -318,6 +325,7 @@ impl ContextMenu {
let style = cx.global::<Settings>().theme.context_menu.clone(); let style = cx.global::<Settings>().theme.context_menu.clone();
let window_id = cx.window_id();
MouseEventHandler::<Menu>::new(0, cx, |_, cx| { MouseEventHandler::<Menu>::new(0, cx, |_, cx| {
Flex::column() Flex::column()
.with_children(self.items.iter().enumerate().map(|(ix, item)| { .with_children(self.items.iter().enumerate().map(|(ix, item)| {
@ -337,6 +345,8 @@ impl ContextMenu {
) )
.with_child({ .with_child({
KeystrokeLabel::new( KeystrokeLabel::new(
window_id,
self.parent_view_id,
action.boxed_clone(), action.boxed_clone(),
style.keystroke.container, style.keystroke.container,
style.keystroke.text.clone(), style.keystroke.text.clone(),

View file

@ -21,6 +21,7 @@ use language::{
use project::{DiagnosticSummary, Project, ProjectPath}; use project::{DiagnosticSummary, Project, ProjectPath};
use serde_json::json; use serde_json::json;
use settings::Settings; use settings::Settings;
use smallvec::SmallVec;
use std::{ use std::{
any::{Any, TypeId}, any::{Any, TypeId},
cmp::Ordering, cmp::Ordering,
@ -579,7 +580,7 @@ impl Item for ProjectDiagnosticsEditor {
.update(cx, |editor, cx| editor.git_diff_recalc(project, cx)) .update(cx, |editor, cx| editor.git_diff_recalc(project, cx))
} }
fn to_item_events(event: &Self::Event) -> Vec<ItemEvent> { fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]> {
Editor::to_item_events(event) Editor::to_item_events(event)
} }

View file

@ -17,7 +17,8 @@ test-support = [
"project/test-support", "project/test-support",
"util/test-support", "util/test-support",
"workspace/test-support", "workspace/test-support",
"tree-sitter-rust" "tree-sitter-rust",
"tree-sitter-typescript"
] ]
[dependencies] [dependencies]
@ -58,6 +59,7 @@ smol = "1.2"
tree-sitter-rust = { version = "*", optional = true } tree-sitter-rust = { version = "*", optional = true }
tree-sitter-html = { version = "*", optional = true } tree-sitter-html = { version = "*", optional = true }
tree-sitter-javascript = { 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] [dev-dependencies]
text = { path = "../text", features = ["test-support"] } text = { path = "../text", features = ["test-support"] }
@ -75,4 +77,5 @@ unindent = "0.1.7"
tree-sitter = "0.20" tree-sitter = "0.20"
tree-sitter-rust = "0.20" tree-sitter-rust = "0.20"
tree-sitter-html = "0.19" tree-sitter-html = "0.19"
tree-sitter-typescript = { git = "https://github.com/tree-sitter/tree-sitter-typescript", rev = "5d20856f34315b068c41edaee2ac8a100081d259" }
tree-sitter-javascript = "0.20" tree-sitter-javascript = "0.20"

View file

@ -337,7 +337,7 @@ impl DisplaySnapshot {
.map(|h| h.text) .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<Item = &str> { pub fn reverse_text_chunks(&self, display_row: u32) -> impl Iterator<Item = &str> {
(0..=display_row).into_iter().rev().flat_map(|row| { (0..=display_row).into_iter().rev().flat_map(|row| {
self.blocks_snapshot 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<Item = DisplayPoint> + '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<Item = DisplayPoint> + 'a {
Self::find_internal(
self.reverse_chars_at(from),
target.chars().rev().collect(),
condition,
)
}
fn find_internal<'a>(
iterator: impl Iterator<Item = (char, DisplayPoint)> + 'a,
target: Vec<char>,
mut condition: impl FnMut(char, DisplayPoint) -> bool + 'a,
) -> impl Iterator<Item = DisplayPoint> + '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 { pub fn column_to_chars(&self, display_row: u32, target: u32) -> u32 {
let mut count = 0; let mut count = 0;
let mut column = 0; let mut column = 0;
@ -627,7 +688,7 @@ pub mod tests {
use smol::stream::StreamExt; use smol::stream::StreamExt;
use std::{env, sync::Arc}; use std::{env, sync::Arc};
use theme::SyntaxTheme; use theme::SyntaxTheme;
use util::test::{marked_text_ranges, sample_text}; use util::test::{marked_text_offsets, marked_text_ranges, sample_text};
use Bias::*; use Bias::*;
#[gpui::test(iterations = 100)] #[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::<Vec<_>>(),
DisplaySnapshot::find_internal(chars, target.collect(), |_, _| true)
.map(|point| point.column())
.collect::<Vec<_>>()
)
}
}
fn syntax_chunks<'a>( fn syntax_chunks<'a>(
rows: Range<u32>, rows: Range<u32>,
map: &ModelHandle<DisplayMap>, map: &ModelHandle<DisplayMap>,

View file

@ -77,14 +77,14 @@ use std::{
cmp::{self, Ordering, Reverse}, cmp::{self, Ordering, Reverse},
mem, mem,
num::NonZeroU32, num::NonZeroU32,
ops::{Deref, DerefMut, Range, RangeInclusive}, ops::{Deref, DerefMut, Range},
path::Path, path::Path,
sync::Arc, sync::Arc,
time::{Duration, Instant}, time::{Duration, Instant},
}; };
pub use sum_tree::Bias; pub use sum_tree::Bias;
use theme::{DiagnosticStyle, Theme}; use theme::{DiagnosticStyle, Theme};
use util::{post_inc, ResultExt, TryFutureExt}; use util::{post_inc, ResultExt, TryFutureExt, RangeExt};
use workspace::{ItemNavHistory, ViewId, Workspace, WorkspaceId}; use workspace::{ItemNavHistory, ViewId, Workspace, WorkspaceId};
use crate::git::diff_hunk_to_display; use crate::git::diff_hunk_to_display;
@ -154,6 +154,12 @@ pub struct ConfirmCodeAction {
pub item_ix: Option<usize>, pub item_ix: Option<usize>,
} }
#[derive(Clone, Default, Deserialize, PartialEq)]
pub struct ToggleComments {
#[serde(default)]
pub advance_downwards: bool,
}
actions!( actions!(
editor, editor,
[ [
@ -216,7 +222,6 @@ actions!(
AddSelectionBelow, AddSelectionBelow,
Tab, Tab,
TabPrev, TabPrev,
ToggleComments,
ShowCharacterPalette, ShowCharacterPalette,
SelectLargerSyntaxNode, SelectLargerSyntaxNode,
SelectSmallerSyntaxNode, SelectSmallerSyntaxNode,
@ -236,6 +241,7 @@ actions!(
RestartLanguageServer, RestartLanguageServer,
Hover, Hover,
Format, Format,
ToggleSoftWrap
] ]
); );
@ -250,6 +256,7 @@ impl_actions!(
MovePageDown, MovePageDown,
ConfirmCompletion, ConfirmCompletion,
ConfirmCodeAction, ConfirmCodeAction,
ToggleComments,
] ]
); );
@ -346,6 +353,7 @@ pub fn init(cx: &mut MutableAppContext) {
cx.add_action(Editor::toggle_code_actions); cx.add_action(Editor::toggle_code_actions);
cx.add_action(Editor::open_excerpts); cx.add_action(Editor::open_excerpts);
cx.add_action(Editor::jump); cx.add_action(Editor::jump);
cx.add_action(Editor::toggle_soft_wrap);
cx.add_async_action(Editor::format); cx.add_async_action(Editor::format);
cx.add_action(Editor::restart_language_server); cx.add_action(Editor::restart_language_server);
cx.add_action(Editor::show_character_palette); cx.add_action(Editor::show_character_palette);
@ -400,7 +408,7 @@ pub enum SelectMode {
All, All,
} }
#[derive(Copy, Clone, PartialEq, Eq)] #[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub enum EditorMode { pub enum EditorMode {
SingleLine, SingleLine,
AutoHeight { max_lines: usize }, AutoHeight { max_lines: usize },
@ -810,7 +818,7 @@ impl CompletionsMenu {
fuzzy::match_strings( fuzzy::match_strings(
&self.match_candidates, &self.match_candidates,
query, query,
false, query.chars().any(|c| c.is_uppercase()),
100, 100,
&Default::default(), &Default::default(),
executor, executor,
@ -1732,11 +1740,13 @@ impl Editor {
} }
pub fn handle_input(&mut self, text: &str, cx: &mut ViewContext<Self>) { pub fn handle_input(&mut self, text: &str, cx: &mut ViewContext<Self>) {
let text: Arc<str> = text.into();
if !self.input_enabled { if !self.input_enabled {
cx.emit(Event::InputIgnored { text });
return; return;
} }
let text: Arc<str> = text.into();
let selections = self.selections.all_adjusted(cx); let selections = self.selections.all_adjusted(cx);
let mut edits = Vec::new(); let mut edits = Vec::new();
let mut new_selections = Vec::with_capacity(selections.len()); 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 // If an opening bracket is 1 character long and is typed while
// surround that text with the bracket pair. // text is selected, then surround that text with the bracket pair.
else if is_bracket_pair_start { else if is_bracket_pair_start && bracket_pair.start.chars().count() == 1 {
edits.push((selection.start..selection.start, text.clone())); edits.push((selection.start..selection.start, text.clone()));
edits.push(( edits.push((
selection.end..selection.end, selection.end..selection.end,
@ -3800,7 +3810,7 @@ impl Editor {
} }
} }
if matches!(self.mode, EditorMode::SingleLine) { if self.mode == EditorMode::SingleLine {
cx.propagate_action(); cx.propagate_action();
return; return;
} }
@ -4462,7 +4472,7 @@ impl Editor {
} }
} }
pub fn toggle_comments(&mut self, _: &ToggleComments, cx: &mut ViewContext<Self>) { pub fn toggle_comments(&mut self, action: &ToggleComments, cx: &mut ViewContext<Self>) {
self.transact(cx, |this, cx| { self.transact(cx, |this, cx| {
let mut selections = this.selections.all::<Point>(cx); let mut selections = this.selections.all::<Point>(cx);
let mut edits = Vec::new(); let mut edits = Vec::new();
@ -4681,6 +4691,34 @@ impl Editor {
drop(snapshot); drop(snapshot);
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections)); this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections));
let selections = this.selections.all::<Point>(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, _: &MoveToEnclosingBracket,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) { ) {
let buffer = self.buffer.read(cx).snapshot(cx);
let mut selections = self.selections.all::<usize>(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| { 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()); pane.update(cx, |pane, _| pane.enable_history());
}); });
} else { } else if !definitions.is_empty() {
let replica_id = editor_handle.read(cx).replica_id(cx); let replica_id = editor_handle.read(cx).replica_id(cx);
let title = definitions let title = definitions
.iter() .iter()
@ -5810,6 +5873,19 @@ impl Editor {
.update(cx, |map, cx| map.set_wrap_width(width, cx)) .update(cx, |map, cx| map.set_wrap_width(width, cx))
} }
pub fn toggle_soft_wrap(&mut self, _: &ToggleSoftWrap, cx: &mut ViewContext<Self>) {
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<Range<u32>>) { pub fn highlight_rows(&mut self, rows: Option<Range<u32>>) {
self.highlighted_rows = rows; self.highlighted_rows = rows;
} }
@ -6187,6 +6263,9 @@ impl Deref for EditorSnapshot {
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub enum Event { pub enum Event {
InputIgnored {
text: Arc<str>,
},
ExcerptsAdded { ExcerptsAdded {
buffer: ModelHandle<Buffer>, buffer: ModelHandle<Buffer>,
predecessor: ExcerptId, predecessor: ExcerptId,
@ -6253,8 +6332,10 @@ impl View for Editor {
} }
fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) { fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
let focused_event = EditorFocused(cx.handle()); if cx.is_self_focused() {
cx.emit_global(focused_event); let focused_event = EditorFocused(cx.handle());
cx.emit_global(focused_event);
}
if let Some(rename) = self.pending_rename.as_ref() { if let Some(rename) = self.pending_rename.as_ref() {
cx.focus(&rename.editor); cx.focus(&rename.editor);
} else { } else {
@ -6393,26 +6474,29 @@ impl View for Editor {
text: &str, text: &str,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) { ) {
self.transact(cx, |this, cx| {
if this.input_enabled {
let new_selected_ranges = if let Some(range_utf16) = range_utf16 {
let range_utf16 = OffsetUtf16(range_utf16.start)..OffsetUtf16(range_utf16.end);
Some(this.selection_replacement_ranges(range_utf16, cx))
} else {
this.marked_text_ranges(cx)
};
if let Some(new_selected_ranges) = new_selected_ranges {
this.change_selections(None, cx, |selections| {
selections.select_ranges(new_selected_ranges)
});
}
}
this.handle_input(text, cx);
});
if !self.input_enabled { if !self.input_enabled {
return; return;
} }
self.transact(cx, |this, cx| {
let new_selected_ranges = if let Some(range_utf16) = range_utf16 {
let range_utf16 = OffsetUtf16(range_utf16.start)..OffsetUtf16(range_utf16.end);
Some(this.selection_replacement_ranges(range_utf16, cx))
} else {
this.marked_text_ranges(cx)
};
if let Some(new_selected_ranges) = new_selected_ranges {
this.change_selections(None, cx, |selections| {
selections.select_ranges(new_selected_ranges)
});
}
this.handle_input(text, cx);
});
if let Some(transaction) = self.ime_transaction { if let Some(transaction) = self.ime_transaction {
self.buffer.update(cx, |buffer, cx| { self.buffer.update(cx, |buffer, cx| {
buffer.group_until_transaction(transaction, cx); buffer.group_until_transaction(transaction, cx);
@ -6909,21 +6993,6 @@ pub fn split_words<'a>(text: &'a str) -> impl std::iter::Iterator<Item = &'a str
.flat_map(|word| word.split_inclusive('_')) .flat_map(|word| word.split_inclusive('_'))
} }
trait RangeExt<T> {
fn sorted(&self) -> Range<T>;
fn to_inclusive(&self) -> RangeInclusive<T>;
}
impl<T: Ord + Clone> RangeExt<T> for Range<T> {
fn sorted(&self) -> Self {
cmp::min(&self.start, &self.end).clone()..cmp::max(&self.start, &self.end).clone()
}
fn to_inclusive(&self) -> RangeInclusive<T> {
self.start.clone()..=self.end.clone()
}
}
trait RangeToAnchorExt { trait RangeToAnchorExt {
fn to_anchors(self, snapshot: &MultiBufferSnapshot) -> Range<Anchor>; fn to_anchors(self, snapshot: &MultiBufferSnapshot) -> Range<Anchor>;
} }

View file

@ -3452,12 +3452,20 @@ async fn test_surround_with_pair(cx: &mut gpui::TestAppContext) {
cx.update(|cx| cx.set_global(Settings::test(cx))); cx.update(|cx| cx.set_global(Settings::test(cx)));
let language = Arc::new(Language::new( let language = Arc::new(Language::new(
LanguageConfig { LanguageConfig {
brackets: vec![BracketPair { brackets: vec![
start: "{".to_string(), BracketPair {
end: "}".to_string(), start: "{".to_string(),
close: true, end: "}".to_string(),
newline: true, close: true,
}], newline: true,
},
BracketPair {
start: "/* ".to_string(),
end: "*/".to_string(),
close: true,
..Default::default()
},
],
..Default::default() ..Default::default()
}, },
Some(tree_sitter_rust::language()), 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) 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), DisplayPoint::new(3, 5)..DisplayPoint::new(3, 6),
]) ])
}); });
editor.toggle_comments(&ToggleComments, cx); editor.toggle_comments(&ToggleComments::default(), cx);
assert_eq!( assert_eq!(
editor.text(cx), editor.text(cx),
" "
@ -4400,7 +4469,7 @@ async fn test_toggle_comment(cx: &mut gpui::TestAppContext) {
editor.change_selections(None, cx, |s| { editor.change_selections(None, cx, |s| {
s.select_display_ranges([DisplayPoint::new(1, 3)..DisplayPoint::new(3, 6)]) 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!( assert_eq!(
editor.text(cx), editor.text(cx),
" "
@ -4417,7 +4486,7 @@ async fn test_toggle_comment(cx: &mut gpui::TestAppContext) {
editor.change_selections(None, cx, |s| { editor.change_selections(None, cx, |s| {
s.select_display_ranges([DisplayPoint::new(2, 0)..DisplayPoint::new(3, 0)]) 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!( assert_eq!(
editor.text(cx), 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] #[gpui::test]
async fn test_toggle_block_comment(cx: &mut gpui::TestAppContext) { async fn test_toggle_block_comment(cx: &mut gpui::TestAppContext) {
let mut cx = EditorTestContext::new(cx); let mut cx = EditorTestContext::new(cx);
@ -4482,7 +4684,7 @@ async fn test_toggle_block_comment(cx: &mut gpui::TestAppContext) {
"# "#
.unindent(), .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( cx.assert_editor_state(
&r#" &r#"
<!-- <p>A</p>ˇ --> <!-- <p>A</p>ˇ -->
@ -4491,7 +4693,7 @@ async fn test_toggle_block_comment(cx: &mut gpui::TestAppContext) {
"# "#
.unindent(), .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( cx.assert_editor_state(
&r#" &r#"
<p>A</p>ˇ <p>A</p>ˇ
@ -4513,7 +4715,7 @@ async fn test_toggle_block_comment(cx: &mut gpui::TestAppContext) {
.unindent(), .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( cx.assert_editor_state(
&r#" &r#"
<!-- <p>A«</p> <!-- <p>A«</p>
@ -4523,7 +4725,7 @@ async fn test_toggle_block_comment(cx: &mut gpui::TestAppContext) {
"# "#
.unindent(), .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( cx.assert_editor_state(
&r#" &r#"
<p>A«</p> <p>A«</p>
@ -4545,7 +4747,7 @@ async fn test_toggle_block_comment(cx: &mut gpui::TestAppContext) {
.unindent(), .unindent(),
); );
cx.foreground().run_until_parked(); cx.foreground().run_until_parked();
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( cx.assert_editor_state(
&r#" &r#"
<!-- ˇ<script> --> <!-- ˇ<script> -->
@ -5459,6 +5661,54 @@ fn test_split_words() {
assert_eq!(split("helloworld"), &["helloworld"]); 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<DisplayPoint> { fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
let point = DisplayPoint::new(row as u32, column as u32); let point = DisplayPoint::new(row as u32, column as u32);
point..point point..point

View file

@ -1534,15 +1534,14 @@ impl Element for EditorElement {
let snapshot = self.update_view(cx.app, |view, cx| { let snapshot = self.update_view(cx.app, |view, cx| {
view.set_visible_line_count(size.y() / line_height); 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) { let wrap_width = match view.soft_wrap_mode(cx) {
SoftWrap::None => Some((MAX_LINE_LEN / 2) as f32 * em_advance), SoftWrap::None => (MAX_LINE_LEN / 2) as f32 * em_advance,
SoftWrap::EditorWidth => { SoftWrap::EditorWidth => editor_width,
Some(text_width - gutter_margin - overscroll.x() - em_width) SoftWrap::Column(column) => editor_width.min(column as f32 * em_advance),
}
SoftWrap::Column(column) => Some(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) view.snapshot(cx)
} else { } else {
snapshot snapshot

View file

@ -17,7 +17,7 @@ pub fn refresh_matching_bracket_highlights(editor: &mut Editor, cx: &mut ViewCon
let snapshot = editor.snapshot(cx); let snapshot = editor.snapshot(cx);
if let Some((opening_range, closing_range)) = snapshot if let Some((opening_range, closing_range)) = snapshot
.buffer_snapshot .buffer_snapshot
.enclosing_bracket_ranges(head..head) .innermost_enclosing_bracket_ranges(head..head)
{ {
editor.highlight_background::<MatchingBracketHighlight>( editor.highlight_background::<MatchingBracketHighlight>(
vec![ vec![

View file

@ -331,7 +331,7 @@ impl InfoPopover {
if let Some(language) = content if let Some(language) = content
.language .language
.clone() .clone()
.and_then(|language| project.languages().get_language(&language)) .and_then(|language| project.languages().language_for_name(&language))
{ {
let runs = language let runs = language
.highlight_text(&content.text.as_str().into(), 0..content.text.len()); .highlight_text(&content.text.as_str().into(), 0..content.text.len());

View file

@ -2,12 +2,10 @@ use crate::{
display_map::ToDisplayPoint, link_go_to_definition::hide_link_definition, display_map::ToDisplayPoint, link_go_to_definition::hide_link_definition,
movement::surrounding_word, persistence::DB, scroll::ScrollAnchor, Anchor, Autoscroll, Editor, movement::surrounding_word, persistence::DB, scroll::ScrollAnchor, Anchor, Autoscroll, Editor,
Event, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, NavigationData, ToPoint as _, Event, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, NavigationData, ToPoint as _,
FORMAT_TIMEOUT,
}; };
use anyhow::{anyhow, Context, Result}; use anyhow::{anyhow, Context, Result};
use collections::HashSet; use collections::HashSet;
use futures::future::try_join_all; use futures::future::try_join_all;
use futures::FutureExt;
use gpui::{ use gpui::{
elements::*, geometry::vector::vec2f, AppContext, Entity, ModelHandle, MutableAppContext, elements::*, geometry::vector::vec2f, AppContext, Entity, ModelHandle, MutableAppContext,
RenderContext, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, RenderContext, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
@ -16,9 +14,10 @@ use language::{
proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, OffsetRangeExt, Point, proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, OffsetRangeExt, Point,
SelectionGoal, SelectionGoal,
}; };
use project::{FormatTrigger, Item as _, Project, ProjectPath}; use project::{Item as _, Project, ProjectPath};
use rpc::proto::{self, update_view}; use rpc::proto::{self, update_view};
use settings::Settings; use settings::Settings;
use smallvec::SmallVec;
use std::{ use std::{
borrow::Cow, borrow::Cow,
cmp::{self, Ordering}, cmp::{self, Ordering},
@ -609,32 +608,12 @@ impl Item for Editor {
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> Task<Result<()>> { ) -> Task<Result<()>> {
self.report_event("save editor", cx); self.report_event("save editor", cx);
let format = self.perform_format(project.clone(), cx);
let buffer = self.buffer().clone(); let buffers = self.buffer().clone().read(cx).all_buffers();
let buffers = buffer.read(cx).all_buffers(); cx.as_mut().spawn(|mut cx| async move {
let mut timeout = cx.background().timer(FORMAT_TIMEOUT).fuse(); format.await?;
let format = project.update(cx, |project, cx| { project
project.format(buffers, true, FormatTrigger::Save, cx) .update(&mut cx, |project, cx| project.save_buffers(buffers, 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)
})
.await?; .await?;
Ok(()) Ok(())
}) })
@ -693,8 +672,8 @@ impl Item for Editor {
Task::ready(Ok(())) Task::ready(Ok(()))
} }
fn to_item_events(event: &Self::Event) -> Vec<ItemEvent> { fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]> {
let mut result = Vec::new(); let mut result = SmallVec::new();
match event { match event {
Event::Closed => result.push(ItemEvent::CloseItem), Event::Closed => result.push(ItemEvent::CloseItem),
Event::Saved | Event::TitleChanged => { Event::Saved | Event::TitleChanged => {
@ -1094,7 +1073,7 @@ impl StatusItemView for CursorPosition {
active_pane_item: Option<&dyn ItemHandle>, active_pane_item: Option<&dyn ItemHandle>,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) { ) {
if let Some(editor) = active_pane_item.and_then(|item| item.downcast::<Editor>()) { if let Some(editor) = active_pane_item.and_then(|item| item.act_as::<Editor>(cx)) {
self._observe_active_editor = Some(cx.observe(&editor, Self::update_position)); self._observe_active_editor = Some(cx.observe(&editor, Self::update_position));
self.update_position(editor, cx); self.update_position(editor, cx);
} else { } else {
@ -1158,7 +1137,6 @@ fn path_for_file<'a>(
mod tests { mod tests {
use super::*; use super::*;
use gpui::MutableAppContext; use gpui::MutableAppContext;
use language::RopeFingerprint;
use std::{ use std::{
path::{Path, PathBuf}, path::{Path, PathBuf},
sync::Arc, sync::Arc,
@ -1204,17 +1182,6 @@ mod tests {
todo!() todo!()
} }
fn save(
&self,
_: u64,
_: language::Rope,
_: clock::Global,
_: project::LineEnding,
_: &mut MutableAppContext,
) -> gpui::Task<anyhow::Result<(clock::Global, RopeFingerprint, SystemTime)>> {
todo!()
}
fn as_any(&self) -> &dyn std::any::Any { fn as_any(&self) -> &dyn std::any::Any {
todo!() todo!()
} }

View file

@ -52,8 +52,8 @@ pub fn deploy_context_menu(
AnchorCorner::TopLeft, AnchorCorner::TopLeft,
vec![ vec![
ContextMenuItem::item("Rename Symbol", Rename), ContextMenuItem::item("Rename Symbol", Rename),
ContextMenuItem::item("Go To Definition", GoToDefinition), ContextMenuItem::item("Go to Definition", GoToDefinition),
ContextMenuItem::item("Go To Type Definition", GoToTypeDefinition), ContextMenuItem::item("Go to Type Definition", GoToTypeDefinition),
ContextMenuItem::item("Find All References", FindAllReferences), ContextMenuItem::item("Find All References", FindAllReferences),
ContextMenuItem::item( ContextMenuItem::item(
"Code Actions", "Code Actions",

View file

@ -1,7 +1,6 @@
mod anchor; mod anchor;
pub use anchor::{Anchor, AnchorRangeExt}; pub use anchor::{Anchor, AnchorRangeExt};
use anyhow::Result;
use clock::ReplicaId; use clock::ReplicaId;
use collections::{BTreeMap, Bound, HashMap, HashSet}; use collections::{BTreeMap, Bound, HashMap, HashSet};
use futures::{channel::mpsc, SinkExt}; use futures::{channel::mpsc, SinkExt};
@ -385,9 +384,13 @@ impl MultiBuffer {
_ => Default::default(), _ => Default::default(),
}; };
#[allow(clippy::type_complexity)] struct BufferEdit {
let mut buffer_edits: HashMap<usize, Vec<(Range<usize>, Arc<str>, bool, u32)>> = range: Range<usize>,
Default::default(); new_text: Arc<str>,
is_insertion: bool,
original_indent_column: u32,
}
let mut buffer_edits: HashMap<usize, Vec<BufferEdit>> = Default::default();
let mut cursor = snapshot.excerpts.cursor::<usize>(); let mut cursor = snapshot.excerpts.cursor::<usize>();
for (ix, (range, new_text)) in edits.enumerate() { for (ix, (range, new_text)) in edits.enumerate() {
let new_text: Arc<str> = new_text.into(); let new_text: Arc<str> = new_text.into();
@ -422,12 +425,12 @@ impl MultiBuffer {
buffer_edits buffer_edits
.entry(start_excerpt.buffer_id) .entry(start_excerpt.buffer_id)
.or_insert(Vec::new()) .or_insert(Vec::new())
.push(( .push(BufferEdit {
buffer_start..buffer_end, range: buffer_start..buffer_end,
new_text, new_text,
true, is_insertion: true,
original_indent_column, original_indent_column,
)); });
} else { } else {
let start_excerpt_range = buffer_start let start_excerpt_range = buffer_start
..start_excerpt ..start_excerpt
@ -444,21 +447,21 @@ impl MultiBuffer {
buffer_edits buffer_edits
.entry(start_excerpt.buffer_id) .entry(start_excerpt.buffer_id)
.or_insert(Vec::new()) .or_insert(Vec::new())
.push(( .push(BufferEdit {
start_excerpt_range, range: start_excerpt_range,
new_text.clone(), new_text: new_text.clone(),
true, is_insertion: true,
original_indent_column, original_indent_column,
)); });
buffer_edits buffer_edits
.entry(end_excerpt.buffer_id) .entry(end_excerpt.buffer_id)
.or_insert(Vec::new()) .or_insert(Vec::new())
.push(( .push(BufferEdit {
end_excerpt_range, range: end_excerpt_range,
new_text.clone(), new_text: new_text.clone(),
false, is_insertion: false,
original_indent_column, original_indent_column,
)); });
cursor.seek(&range.start, Bias::Right, &()); cursor.seek(&range.start, Bias::Right, &());
cursor.next(&()); cursor.next(&());
@ -469,19 +472,19 @@ impl MultiBuffer {
buffer_edits buffer_edits
.entry(excerpt.buffer_id) .entry(excerpt.buffer_id)
.or_insert(Vec::new()) .or_insert(Vec::new())
.push(( .push(BufferEdit {
excerpt.range.context.to_offset(&excerpt.buffer), range: excerpt.range.context.to_offset(&excerpt.buffer),
new_text.clone(), new_text: new_text.clone(),
false, is_insertion: false,
original_indent_column, original_indent_column,
)); });
cursor.next(&()); cursor.next(&());
} }
} }
} }
for (buffer_id, mut edits) in buffer_edits { 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] self.buffers.borrow()[&buffer_id]
.buffer .buffer
.update(cx, |buffer, cx| { .update(cx, |buffer, cx| {
@ -490,14 +493,19 @@ impl MultiBuffer {
let mut original_indent_columns = Vec::new(); let mut original_indent_columns = Vec::new();
let mut deletions = Vec::new(); let mut deletions = Vec::new();
let empty_str: Arc<str> = "".into(); let empty_str: Arc<str> = "".into();
while let Some(( while let Some(BufferEdit {
mut range, mut range,
new_text, new_text,
mut is_insertion, mut is_insertion,
original_indent_column, 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 { if range.end >= next_range.start {
range.end = cmp::max(next_range.end, range.end); range.end = cmp::max(next_range.end, range.end);
is_insertion |= *next_is_insertion; is_insertion |= *next_is_insertion;
@ -1279,20 +1287,6 @@ impl MultiBuffer {
.map(|state| state.buffer.clone()) .map(|state| state.buffer.clone())
} }
pub fn save(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
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<T>(&self, position: T, text: &str, cx: &AppContext) -> bool pub fn is_completion_trigger<T>(&self, position: T, text: &str, cx: &AppContext) -> bool
where where
T: ToOffset, T: ToOffset,
@ -2621,57 +2615,89 @@ impl MultiBufferSnapshot {
self.parse_count self.parse_count
} }
pub fn enclosing_bracket_ranges<T: ToOffset>( /// 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<T: ToOffset>(
&self, &self,
range: Range<T>, range: Range<T>,
) -> Option<(Range<usize>, Range<usize>)> { ) -> Option<(Range<usize>, Range<usize>)> {
let range = range.start.to_offset(self)..range.end.to_offset(self); let range = range.start.to_offset(self)..range.end.to_offset(self);
let mut cursor = self.excerpts.cursor::<usize>(); // Get the ranges of the innermost pair of brackets.
cursor.seek(&range.start, Bias::Right, &()); let mut result: Option<(Range<usize>, Range<usize>)> = None;
let start_excerpt = cursor.item();
cursor.seek(&range.end, Bias::Right, &()); let Some(enclosing_bracket_ranges) = self.enclosing_bracket_ranges(range.clone()) else { return None; };
let end_excerpt = cursor.item();
start_excerpt for (open, close) in enclosing_bracket_ranges {
.zip(end_excerpt) let len = close.end - open.start;
.and_then(|(start_excerpt, end_excerpt)| {
if start_excerpt.id != end_excerpt.id { if let Some((existing_open, existing_close)) = &result {
return None; let existing_len = existing_close.end - existing_open.start;
if len > existing_len {
continue;
} }
}
let excerpt_buffer_start = start_excerpt result = Some((open, close));
.range }
.context
.start
.to_offset(&start_excerpt.buffer);
let excerpt_buffer_end = excerpt_buffer_start + start_excerpt.text_summary.len;
let start_in_buffer = result
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)?;
if start_bracket_range.start >= excerpt_buffer_start /// Returns enclosing bracket ranges containing the given range or returns None if the range is
&& end_bracket_range.end <= excerpt_buffer_end /// not contained in a single excerpt
{ pub fn enclosing_bracket_ranges<'a, T: ToOffset>(
&'a self,
range: Range<T>,
) -> Option<impl Iterator<Item = (Range<usize>, Range<usize>)> + '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<T>,
) -> Option<impl Iterator<Item = (Range<usize>, Range<usize>)> + '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 = 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 = 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 = 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 = 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)) Some((start_bracket_range, end_bracket_range))
} else { })
None })
}
})
} }
pub fn diagnostics_update_count(&self) -> usize { pub fn diagnostics_update_count(&self) -> usize {
@ -2812,40 +2838,23 @@ impl MultiBufferSnapshot {
pub fn range_for_syntax_ancestor<T: ToOffset>(&self, range: Range<T>) -> Option<Range<usize>> { pub fn range_for_syntax_ancestor<T: ToOffset>(&self, range: Range<T>) -> Option<Range<usize>> {
let range = range.start.to_offset(self)..range.end.to_offset(self); let range = range.start.to_offset(self)..range.end.to_offset(self);
let mut cursor = self.excerpts.cursor::<usize>(); self.excerpt_containing(range.clone())
cursor.seek(&range.start, Bias::Right, &()); .and_then(|(excerpt, excerpt_offset)| {
let start_excerpt = cursor.item(); let excerpt_buffer_start = excerpt.range.context.start.to_offset(&excerpt.buffer);
let excerpt_buffer_end = excerpt_buffer_start + excerpt.text_summary.len;
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;
let start_in_buffer = let start_in_buffer =
excerpt_buffer_start + range.start.saturating_sub(*cursor.start()); excerpt_buffer_start + range.start.saturating_sub(excerpt_offset);
let end_in_buffer = let end_in_buffer = excerpt_buffer_start + range.end.saturating_sub(excerpt_offset);
excerpt_buffer_start + range.end.saturating_sub(*cursor.start()); let mut ancestor_buffer_range = excerpt
let mut ancestor_buffer_range = start_excerpt
.buffer .buffer
.range_for_syntax_ancestor(start_in_buffer..end_in_buffer)?; .range_for_syntax_ancestor(start_in_buffer..end_in_buffer)?;
ancestor_buffer_range.start = ancestor_buffer_range.start =
cmp::max(ancestor_buffer_range.start, excerpt_buffer_start); cmp::max(ancestor_buffer_range.start, excerpt_buffer_start);
ancestor_buffer_range.end = cmp::min(ancestor_buffer_range.end, excerpt_buffer_end); 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 start = excerpt_offset + (ancestor_buffer_range.start - excerpt_buffer_start);
let end = cursor.start() + (ancestor_buffer_range.end - excerpt_buffer_start); let end = excerpt_offset + (ancestor_buffer_range.end - excerpt_buffer_start);
Some(start..end) Some(start..end)
}) })
} }
@ -2929,6 +2938,35 @@ impl MultiBufferSnapshot {
None 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<T>,
) -> Option<(&'a Excerpt, usize)> {
let range = range.start.to_offset(self)..range.end.to_offset(self);
let mut cursor = self.excerpts.cursor::<usize>();
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>( pub fn remote_selections_in_range<'a>(
&'a self, &'a self,
range: &'a Range<Anchor>, range: &'a Range<Anchor>,

View file

@ -6,7 +6,7 @@ use db::{define_connection, query};
use workspace::{ItemId, WorkspaceDb, WorkspaceId}; use workspace::{ItemId, WorkspaceDb, WorkspaceId};
define_connection!( define_connection!(
// Current table shape using pseudo-rust syntax: // Current schema shape using pseudo-rust syntax:
// editors( // editors(
// item_id: usize, // item_id: usize,
// workspace_id: usize, // workspace_id: usize,

View file

@ -659,6 +659,31 @@ impl<'a> MutableSelectionsCollection<'a> {
} }
} }
pub fn move_offsets_with(
&mut self,
mut move_selection: impl FnMut(&MultiBufferSnapshot, &mut Selection<usize>),
) {
let mut changed = false;
let snapshot = self.buffer().clone();
let selections = self
.all::<usize>(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( pub fn move_heads_with(
&mut self, &mut self,
mut update_head: impl FnMut( mut update_head: impl FnMut(

View file

@ -1,4 +1,5 @@
use std::{ use std::{
borrow::Cow,
ops::{Deref, DerefMut, Range}, ops::{Deref, DerefMut, Range},
sync::Arc, sync::Arc,
}; };
@ -7,7 +8,8 @@ use anyhow::Result;
use futures::Future; use futures::Future;
use gpui::{json, ViewContext, ViewHandle}; 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 lsp::{notification, request};
use project::Project; use project::Project;
use smol::stream::StreamExt; use smol::stream::StreamExt;
@ -60,7 +62,7 @@ impl<'a> EditorLspTestContext<'a> {
params params
.fs .fs
.as_fake() .as_fake()
.insert_tree("/root", json!({ "dir": { file_name: "" }})) .insert_tree("/root", json!({ "dir": { file_name.clone(): "" }}))
.await; .await;
let (window_id, workspace) = cx.add_window(|cx| { let (window_id, workspace) = cx.add_window(|cx| {
@ -105,7 +107,7 @@ impl<'a> EditorLspTestContext<'a> {
}, },
lsp, lsp,
workspace, 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() ..Default::default()
}, },
Some(tree_sitter_rust::language()), 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 Self::new(language, capabilities, cx).await
} }

View file

@ -162,10 +162,13 @@ impl<'a> EditorTestContext<'a> {
/// embedded range markers that represent the ranges and directions of /// embedded range markers that represent the ranges and directions of
/// each selection. /// 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. /// See the `util::test::marked_text_ranges` function for more information.
pub fn set_state(&mut self, marked_text: &str) -> ContextHandle { pub fn set_state(&mut self, marked_text: &str) -> ContextHandle {
let _state_context = self.add_assertion_context(format!( let _state_context = self.add_assertion_context(format!(
"Editor State: \"{}\"", "Initial Editor State: \"{}\"",
marked_text.escape_debug().to_string() marked_text.escape_debug().to_string()
)); ));
let (unmarked_text, selection_ranges) = marked_text_ranges(marked_text, true); let (unmarked_text, selection_ranges) = marked_text_ranges(marked_text, true);

View file

@ -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::<Self>::new(0, cx, |state, cx| {
let theme = &cx.global::<Settings>().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<Self>) {}
}

View file

@ -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; use std::sync::Arc;
pub mod feedback_editor;
mod system_specs; mod system_specs;
use gpui::{actions, impl_actions, ClipboardItem, ViewContext}; use gpui::{actions, impl_actions, ClipboardItem, MutableAppContext, PromptLevel, ViewContext};
use serde::Deserialize; use serde::Deserialize;
use system_specs::SystemSpecs; use system_specs::SystemSpecs;
use workspace::Workspace; use workspace::{AppState, Workspace};
#[derive(Deserialize, Clone, PartialEq)] #[derive(Deserialize, Clone, PartialEq)]
pub struct OpenBrowser { pub struct OpenBrowser {
@ -16,30 +20,39 @@ impl_actions!(zed, [OpenBrowser]);
actions!( actions!(
zed, zed,
[CopySystemSpecsIntoClipboard, FileBugReport, RequestFeature,] [CopySystemSpecsIntoClipboard, FileBugReport, RequestFeature]
); );
pub fn init(cx: &mut gpui::MutableAppContext) { pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
feedback_editor::init(cx); 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)); 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( cx.add_action(
|_: &mut Workspace, _: &CopySystemSpecsIntoClipboard, cx: &mut ViewContext<Workspace>| { move |_: &mut Workspace,
let system_specs = SystemSpecs::new(cx).to_string(); _: &CopySystemSpecsIntoClipboard,
let item = ClipboardItem::new(system_specs.clone()); cx: &mut ViewContext<Workspace>| {
cx.prompt( cx.prompt(
gpui::PromptLevel::Info, PromptLevel::Info,
&format!("Copied into clipboard:\n\n{system_specs}"), &format!("Copied into clipboard:\n\n{system_specs_text}"),
&["OK"], &["OK"],
); );
let item = ClipboardItem::new(system_specs_text.clone());
cx.write_to_clipboard(item); cx.write_to_clipboard(item);
}, },
); );
cx.add_action( cx.add_action(
|_: &mut Workspace, _: &RequestFeature, cx: &mut ViewContext<Workspace>| { |_: &mut Workspace, _: &RequestFeature, cx: &mut ViewContext<Workspace>| {
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 { cx.dispatch_action(OpenBrowser {
url: url.into(), url: url.into(),
}); });
@ -47,14 +60,9 @@ pub fn init(cx: &mut gpui::MutableAppContext) {
); );
cx.add_action( cx.add_action(
|_: &mut Workspace, _: &FileBugReport, cx: &mut ViewContext<Workspace>| { move |_: &mut Workspace, _: &FileBugReport, cx: &mut ViewContext<Workspace>| {
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)
);
cx.dispatch_action(OpenBrowser { cx.dispatch_action(OpenBrowser {
url: url.into(), url: url.clone().into(),
}); });
}, },
); );

View file

@ -1,91 +1,57 @@
use std::{ops::Range, sync::Arc}; use std::{
any::TypeId,
ops::{Range, RangeInclusive},
sync::Arc,
};
use anyhow::bail; 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 editor::{Anchor, Editor};
use futures::AsyncReadExt; use futures::AsyncReadExt;
use gpui::{ use gpui::{
actions, actions,
elements::{ChildView, Flex, Label, MouseEventHandler, ParentElement, Stack, Text}, elements::{ChildView, Flex, Label, ParentElement},
serde_json, AnyViewHandle, AppContext, CursorStyle, Element, ElementBox, Entity, ModelHandle, serde_json, AnyViewHandle, AppContext, Element, ElementBox, Entity, ModelHandle,
MouseButton, MutableAppContext, PromptLevel, RenderContext, Task, View, ViewContext, MutableAppContext, PromptLevel, RenderContext, Task, View, ViewContext, ViewHandle,
ViewHandle, WeakViewHandle,
}; };
use isahc::Request; use isahc::Request;
use language::Buffer; use language::Buffer;
use postage::prelude::Stream; use postage::prelude::Stream;
use lazy_static::lazy_static;
use project::Project; use project::Project;
use serde::Serialize; use serde::Serialize;
use settings::Settings;
use workspace::{ use workspace::{
item::{Item, ItemHandle}, item::{Item, ItemHandle},
searchable::{SearchableItem, SearchableItemHandle}, searchable::{SearchableItem, SearchableItemHandle},
StatusItemView, Workspace, smallvec::SmallVec,
AppState, Workspace,
}; };
use crate::system_specs::SystemSpecs; use crate::{submit_feedback_button::SubmitFeedbackButton, system_specs::SystemSpecs};
lazy_static! { const FEEDBACK_CHAR_LIMIT: RangeInclusive<usize> = 10..=5000;
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<usize> = Range {
start: 10,
end: 1000,
};
const FEEDBACK_PLACEHOLDER_TEXT: &str = "Thanks for spending time with Zed. Enter your feedback here as Markdown. Save the tab to submit your feedback.";
const FEEDBACK_SUBMISSION_ERROR_TEXT: &str = const FEEDBACK_SUBMISSION_ERROR_TEXT: &str =
"Feedback failed to submit, see error log for details."; "Feedback failed to submit, see error log for details.";
actions!(feedback, [SubmitFeedback, GiveFeedback, DeployFeedback]); actions!(feedback, [GiveFeedback, SubmitFeedback]);
pub fn init(cx: &mut MutableAppContext) { pub fn init(system_specs: SystemSpecs, app_state: Arc<AppState>, cx: &mut MutableAppContext) {
cx.add_action(FeedbackEditor::deploy); cx.add_action({
} move |workspace: &mut Workspace, _: &GiveFeedback, cx: &mut ViewContext<Workspace>| {
FeedbackEditor::deploy(system_specs.clone(), workspace, app_state.clone(), cx);
}
});
pub struct FeedbackButton; cx.add_async_action(
|submit_feedback_button: &mut SubmitFeedbackButton, _: &SubmitFeedback, cx| {
impl Entity for FeedbackButton { if let Some(active_item) = submit_feedback_button.active_item.as_ref() {
type Event = (); Some(active_item.update(cx, |feedback_editor, cx| feedback_editor.handle_save(cx)))
} } else {
None
impl View for FeedbackButton { }
fn ui_name() -> &'static str { },
"FeedbackButton" );
}
fn render(&mut self, cx: &mut RenderContext<'_, Self>) -> ElementBox {
Stack::new()
.with_child(
MouseEventHandler::<Self>::new(0, cx, |state, cx| {
let theme = &cx.global::<Settings>().theme;
let theme = &theme.workspace.status_bar.feedback;
Text::new(
"Give Feedback".to_string(),
theme.style_for(state, true).clone(),
)
.boxed()
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, |_, cx| cx.dispatch_action(GiveFeedback))
.boxed(),
)
.boxed()
}
}
impl StatusItemView for FeedbackButton {
fn set_active_pane_item(
&mut self,
_: Option<&dyn ItemHandle>,
_: &mut gpui::ViewContext<Self>,
) {
}
} }
#[derive(Serialize)] #[derive(Serialize)]
@ -93,17 +59,20 @@ struct FeedbackRequestBody<'a> {
feedback_text: &'a str, feedback_text: &'a str,
metrics_id: Option<Arc<str>>, metrics_id: Option<Arc<str>>,
system_specs: SystemSpecs, system_specs: SystemSpecs,
is_staff: bool,
token: &'a str, token: &'a str,
} }
#[derive(Clone)] #[derive(Clone)]
struct FeedbackEditor { pub(crate) struct FeedbackEditor {
system_specs: SystemSpecs,
editor: ViewHandle<Editor>, editor: ViewHandle<Editor>,
project: ModelHandle<Project>, project: ModelHandle<Project>,
} }
impl FeedbackEditor { impl FeedbackEditor {
fn new_with_buffer( fn new(
system_specs: SystemSpecs,
project: ModelHandle<Project>, project: ModelHandle<Project>,
buffer: ModelHandle<Buffer>, buffer: ModelHandle<Buffer>,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
@ -111,46 +80,40 @@ impl FeedbackEditor {
let editor = cx.add_view(|cx| { let editor = cx.add_view(|cx| {
let mut editor = Editor::for_buffer(buffer, Some(project.clone()), cx); let mut editor = Editor::for_buffer(buffer, Some(project.clone()), cx);
editor.set_vertical_scroll_margin(5, cx); editor.set_vertical_scroll_margin(5, cx);
editor.set_placeholder_text(FEEDBACK_PLACEHOLDER_TEXT, cx);
editor editor
}); });
cx.subscribe(&editor, |_, _, e, cx| cx.emit(e.clone())) cx.subscribe(&editor, |_, _, e, cx| cx.emit(e.clone()))
.detach(); .detach();
let this = Self { editor, project }; Self {
this system_specs: system_specs.clone(),
editor,
project,
}
} }
fn new(project: ModelHandle<Project>, cx: &mut ViewContext<Self>) -> Self { fn handle_save(&mut self, cx: &mut ViewContext<Self>) -> Task<anyhow::Result<()>> {
let markdown_language = project.read(cx).languages().get_language("Markdown"); let feedback_text = self.editor.read(cx).text(cx);
let feedback_char_count = feedback_text.chars().count();
let feedback_text = feedback_text.trim().to_string();
let buffer = project let error = if feedback_char_count < *FEEDBACK_CHAR_LIMIT.start() {
.update(cx, |project, cx| { Some(format!(
project.create_buffer("", markdown_language, cx) "Feedback can't be shorter than {} characters.",
}) FEEDBACK_CHAR_LIMIT.start()
.expect("creating buffers on a local workspace always succeeds"); ))
} else if feedback_char_count > *FEEDBACK_CHAR_LIMIT.end() {
Self::new_with_buffer(project, buffer, cx) Some(format!(
} "Feedback can't be longer than {} characters.",
FEEDBACK_CHAR_LIMIT.end()
fn handle_save( ))
&mut self, } else {
_: gpui::ModelHandle<Project>, None
cx: &mut ViewContext<Self>, };
) -> Task<anyhow::Result<()>> {
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"],
);
if let Some(error) = error {
cx.prompt(PromptLevel::Critical, &error, &["OK"]);
return Task::ready(Ok(())); return Task::ready(Ok(()));
} }
@ -162,8 +125,7 @@ impl FeedbackEditor {
let this = cx.handle(); let this = cx.handle();
let client = cx.global::<Arc<Client>>().clone(); let client = cx.global::<Arc<Client>>().clone();
let feedback_text = self.editor.read(cx).text(cx); let specs = self.system_specs.clone();
let specs = SystemSpecs::new(cx);
cx.spawn(|_, mut cx| async move { cx.spawn(|_, mut cx| async move {
let answer = answer.recv().await; let answer = answer.recv().await;
@ -206,12 +168,14 @@ impl FeedbackEditor {
let feedback_endpoint = format!("{}/api/feedback", *ZED_SERVER_URL); let feedback_endpoint = format!("{}/api/feedback", *ZED_SERVER_URL);
let metrics_id = zed_client.metrics_id(); let metrics_id = zed_client.metrics_id();
let is_staff = zed_client.is_staff();
let http_client = zed_client.http_client(); let http_client = zed_client.http_client();
let request = FeedbackRequestBody { let request = FeedbackRequestBody {
feedback_text: &feedback_text, feedback_text: &feedback_text,
metrics_id, metrics_id,
system_specs, system_specs,
is_staff: is_staff.unwrap_or(false),
token: ZED_SECRET_CLIENT_TOKEN, token: ZED_SECRET_CLIENT_TOKEN,
}; };
@ -236,10 +200,26 @@ impl FeedbackEditor {
} }
impl FeedbackEditor { impl FeedbackEditor {
pub fn deploy(workspace: &mut Workspace, _: &GiveFeedback, cx: &mut ViewContext<Workspace>) { pub fn deploy(
let feedback_editor = system_specs: SystemSpecs,
cx.add_view(|cx| FeedbackEditor::new(workspace.project().clone(), cx)); workspace: &mut Workspace,
workspace.add_item(Box::new(feedback_editor), cx); app_state: Arc<AppState>,
cx: &mut ViewContext<Workspace>,
) {
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 { impl Item for FeedbackEditor {
fn tab_content( fn tab_content(&self, _: Option<usize>, style: &theme::Tab, _: &AppContext) -> ElementBox {
&self,
_: Option<usize>,
style: &theme::Tab,
_: &gpui::AppContext,
) -> ElementBox {
Flex::row() Flex::row()
.with_child( .with_child(
Label::new("Feedback".to_string(), style.label.clone()) Label::new("Feedback".to_string(), style.label.clone())
@ -284,40 +259,40 @@ impl Item for FeedbackEditor {
self.editor.for_each_project_item(cx, f) self.editor.for_each_project_item(cx, f)
} }
fn to_item_events(_: &Self::Event) -> Vec<workspace::item::ItemEvent> { fn to_item_events(_: &Self::Event) -> SmallVec<[workspace::item::ItemEvent; 2]> {
Vec::new() SmallVec::new()
} }
fn is_singleton(&self, _: &gpui::AppContext) -> bool { fn is_singleton(&self, _: &AppContext) -> bool {
true true
} }
fn set_nav_history(&mut self, _: workspace::ItemNavHistory, _: &mut ViewContext<Self>) {} fn set_nav_history(&mut self, _: workspace::ItemNavHistory, _: &mut ViewContext<Self>) {}
fn can_save(&self, _: &gpui::AppContext) -> bool { fn can_save(&self, _: &AppContext) -> bool {
true true
} }
fn save( fn save(
&mut self, &mut self,
project: gpui::ModelHandle<Project>, _: ModelHandle<Project>,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> Task<anyhow::Result<()>> { ) -> Task<anyhow::Result<()>> {
self.handle_save(project, cx) self.handle_save(cx)
} }
fn save_as( fn save_as(
&mut self, &mut self,
project: gpui::ModelHandle<Project>, _: ModelHandle<Project>,
_: std::path::PathBuf, _: std::path::PathBuf,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> Task<anyhow::Result<()>> { ) -> Task<anyhow::Result<()>> {
self.handle_save(project, cx) self.handle_save(cx)
} }
fn reload( fn reload(
&mut self, &mut self,
_: gpui::ModelHandle<Project>, _: ModelHandle<Project>,
_: &mut ViewContext<Self>, _: &mut ViewContext<Self>,
) -> Task<anyhow::Result<()>> { ) -> Task<anyhow::Result<()>> {
unreachable!("reload should not have been called") unreachable!("reload should not have been called")
@ -339,7 +314,8 @@ impl Item for FeedbackEditor {
.as_singleton() .as_singleton()
.expect("Feedback buffer is only ever singleton"); .expect("Feedback buffer is only ever singleton");
Some(Self::new_with_buffer( Some(Self::new(
self.system_specs.clone(),
self.project.clone(), self.project.clone(),
buffer.clone(), buffer.clone(),
cx, cx,
@ -351,8 +327,8 @@ impl Item for FeedbackEditor {
} }
fn deserialize( fn deserialize(
_: gpui::ModelHandle<Project>, _: ModelHandle<Project>,
_: gpui::WeakViewHandle<Workspace>, _: WeakViewHandle<Workspace>,
_: workspace::WorkspaceId, _: workspace::WorkspaceId,
_: workspace::ItemId, _: workspace::ItemId,
_: &mut ViewContext<workspace::Pane>, _: &mut ViewContext<workspace::Pane>,
@ -363,6 +339,21 @@ impl Item for FeedbackEditor {
fn as_searchable(&self, handle: &ViewHandle<Self>) -> Option<Box<dyn SearchableItemHandle>> { fn as_searchable(&self, handle: &ViewHandle<Self>) -> Option<Box<dyn SearchableItemHandle>> {
Some(Box::new(handle.clone())) Some(Box::new(handle.clone()))
} }
fn act_as_type(
&self,
type_id: TypeId,
self_handle: &ViewHandle<Self>,
_: &AppContext,
) -> Option<AnyViewHandle> {
if type_id == TypeId::of::<Self>() {
Some(self_handle.into())
} else if type_id == TypeId::of::<Editor>() {
Some((&self.editor).into())
} else {
None
}
}
} }
impl SearchableItem for FeedbackEditor { impl SearchableItem for FeedbackEditor {

View file

@ -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<ViewHandle<FeedbackEditor>>,
}
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<Self>) -> ElementBox {
let theme = cx.global::<Settings>().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<Self>,
) -> workspace::ToolbarItemLocation {
cx.notify();
if let Some(feedback_editor) = active_pane_item.and_then(|i| i.downcast::<FeedbackEditor>())
{
self.active_item = Some(feedback_editor);
ToolbarItemLocation::PrimaryLeft {
flex: Some((1., false)),
}
} else {
self.active_item = None;
ToolbarItemLocation::Hidden
}
}
}

View file

@ -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<ViewHandle<FeedbackEditor>>,
}
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<Self>) -> ElementBox {
let theme = cx.global::<Settings>().theme.clone();
enum SubmitFeedbackButton {}
MouseEventHandler::<SubmitFeedbackButton>::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::<Self, _>(
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<Self>,
) -> workspace::ToolbarItemLocation {
cx.notify();
if let Some(feedback_editor) = active_pane_item.and_then(|i| i.downcast::<FeedbackEditor>())
{
self.active_item = Some(feedback_editor);
ToolbarItemLocation::PrimaryRight { flex: None }
} else {
self.active_item = None;
ToolbarItemLocation::Hidden
}
}
}

View file

@ -1,14 +1,15 @@
use std::{env, fmt::Display}; use client::ZED_APP_VERSION;
use gpui::{AppContext, AppVersion};
use gpui::AppContext;
use human_bytes::human_bytes; use human_bytes::human_bytes;
use serde::Serialize; use serde::Serialize;
use std::{env, fmt::Display};
use sysinfo::{System, SystemExt}; use sysinfo::{System, SystemExt};
use util::channel::ReleaseChannel; use util::channel::ReleaseChannel;
#[derive(Debug, Serialize)] #[derive(Clone, Debug, Serialize)]
pub struct SystemSpecs { pub struct SystemSpecs {
app_version: &'static str, #[serde(serialize_with = "serialize_app_version")]
app_version: Option<AppVersion>,
release_channel: &'static str, release_channel: &'static str,
os_name: &'static str, os_name: &'static str,
os_version: Option<String>, os_version: Option<String>,
@ -19,18 +20,24 @@ pub struct SystemSpecs {
impl SystemSpecs { impl SystemSpecs {
pub fn new(cx: &AppContext) -> Self { pub fn new(cx: &AppContext) -> Self {
let platform = cx.platform(); let platform = cx.platform();
let app_version = ZED_APP_VERSION.or_else(|| platform.app_version().ok());
let release_channel = cx.global::<ReleaseChannel>().dev_name();
let os_name = platform.os_name();
let system = System::new_all(); 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 { SystemSpecs {
app_version: env!("CARGO_PKG_VERSION"), app_version,
release_channel: cx.global::<ReleaseChannel>().dev_name(), release_channel,
os_name: platform.os_name(), os_name,
os_version: platform os_version,
.os_version() memory,
.ok() architecture,
.map(|os_version| os_version.to_string()),
memory: system.total_memory(),
architecture: env::consts::ARCH,
} }
} }
} }
@ -41,14 +48,28 @@ impl Display for SystemSpecs {
Some(os_version) => format!("OS: {} {}", self.os_name, os_version), Some(os_version) => format!("OS: {} {}", self.os_name, os_version),
None => format!("OS: {}", self.os_name), 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 = [ let system_specs = [
format!("Zed: v{} ({})", self.app_version, self.release_channel), app_version_information,
os_information, Some(os_information),
format!("Memory: {}", human_bytes(self.memory as f64)), Some(format!("Memory: {}", human_bytes(self.memory as f64))),
format!("Architecture: {}", self.architecture), Some(format!("Architecture: {}", self.architecture)),
] ]
.into_iter()
.flatten()
.collect::<Vec<String>>()
.join("\n"); .join("\n");
write!(f, "{system_specs}") write!(f, "{system_specs}")
} }
} }
fn serialize_app_version<S>(version: &Option<AppVersion>, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
version.map(|v| v.to_string()).serialize(serializer)
}

View file

@ -13,7 +13,6 @@ use smol::io::{AsyncReadExt, AsyncWriteExt};
use std::borrow::Cow; use std::borrow::Cow;
use std::cmp; use std::cmp;
use std::io::Write; use std::io::Write;
use std::ops::Deref;
use std::sync::Arc; use std::sync::Arc;
use std::{ use std::{
io, 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] #[async_trait::async_trait]
pub trait Fs: Send + Sync { pub trait Fs: Send + Sync {
async fn create_dir(&self, path: &Path) -> Result<()>; async fn create_dir(&self, path: &Path) -> Result<()>;

View file

@ -47,6 +47,7 @@ smol = "1.2"
time = { version = "0.3", features = ["serde", "serde-well-known"] } time = { version = "0.3", features = ["serde", "serde-well-known"] }
tiny-skia = "0.5" tiny-skia = "0.5"
usvg = "0.14" usvg = "0.14"
uuid = { version = "1.1.2", features = ["v4"] }
waker-fn = "1.1.0" waker-fn = "1.1.0"
[build-dependencies] [build-dependencies]
@ -66,7 +67,7 @@ media = { path = "../media" }
anyhow = "1" anyhow = "1"
block = "0.1" block = "0.1"
cocoa = "0.24" cocoa = "0.24"
core-foundation = "0.9.3" core-foundation = { version = "0.9.3", features = ["with-uuid"] }
core-graphics = "0.22.3" core-graphics = "0.22.3"
core-text = "19.2" core-text = "19.2"
font-kit = { git = "https://github.com/zed-industries/font-kit", rev = "8eaf7a918eafa28b0a37dc759e2e0e7683fa24f1" } font-kit = { git = "https://github.com/zed-industries/font-kit", rev = "8eaf7a918eafa28b0a37dc759e2e0e7683fa24f1" }

View file

@ -1,7 +1,10 @@
pub mod action; pub mod action;
mod callback_collection; mod callback_collection;
mod menu;
pub(crate) mod ref_counts;
#[cfg(any(test, feature = "test-support"))] #[cfg(any(test, feature = "test-support"))]
pub mod test_app_context; pub mod test_app_context;
mod window_input_handler;
use std::{ use std::{
any::{type_name, Any, TypeId}, any::{type_name, Any, TypeId},
@ -19,31 +22,38 @@ use std::{
}; };
use anyhow::{anyhow, Context, Result}; use anyhow::{anyhow, Context, Result};
use lazy_static::lazy_static;
use parking_lot::Mutex; use parking_lot::Mutex;
use pathfinder_geometry::vector::Vector2F;
use postage::oneshot; use postage::oneshot;
use smallvec::SmallVec; use smallvec::SmallVec;
use smol::prelude::*; use smol::prelude::*;
use uuid::Uuid;
pub use action::*; pub use action::*;
use callback_collection::CallbackCollection; use callback_collection::CallbackCollection;
use collections::{hash_map::Entry, HashMap, HashSet, VecDeque}; use collections::{hash_map::Entry, HashMap, HashSet, VecDeque};
pub use menu::*;
use platform::Event; use platform::Event;
#[cfg(any(test, feature = "test-support"))] #[cfg(any(test, feature = "test-support"))]
use ref_counts::LeakDetector;
#[cfg(any(test, feature = "test-support"))]
pub use test_app_context::{ContextHandle, TestAppContext}; pub use test_app_context::{ContextHandle, TestAppContext};
use window_input_handler::WindowInputHandler;
use crate::{ use crate::{
elements::ElementBox, elements::ElementBox,
executor::{self, Task}, executor::{self, Task},
geometry::rect::RectF,
keymap_matcher::{self, Binding, KeymapContext, KeymapMatcher, Keystroke, MatchResult}, keymap_matcher::{self, Binding, KeymapContext, KeymapMatcher, Keystroke, MatchResult},
platform::{self, KeyDownEvent, Platform, PromptLevel, WindowOptions}, platform::{self, KeyDownEvent, Platform, PromptLevel, WindowOptions},
presenter::Presenter, presenter::Presenter,
util::post_inc, util::post_inc,
Appearance, AssetCache, AssetSource, ClipboardItem, FontCache, InputHandler, KeyUpEvent, Appearance, AssetCache, AssetSource, ClipboardItem, FontCache, KeyUpEvent,
ModifiersChangedEvent, MouseButton, MouseRegionId, PathPromptOptions, TextLayoutCache, ModifiersChangedEvent, MouseButton, MouseRegionId, PathPromptOptions, TextLayoutCache,
WindowBounds,
}; };
use self::ref_counts::RefCounts;
pub trait Entity: 'static { pub trait Entity: 'static {
type Event; type Event;
@ -171,36 +181,17 @@ pub trait UpdateView {
T: View; T: View;
} }
pub struct Menu<'a> {
pub name: &'a str,
pub items: Vec<MenuItem<'a>>,
}
pub enum MenuItem<'a> {
Separator,
Submenu(Menu<'a>),
Action {
name: &'a str,
action: Box<dyn Action>,
},
}
#[derive(Clone)] #[derive(Clone)]
pub struct App(Rc<RefCell<MutableAppContext>>); pub struct App(Rc<RefCell<MutableAppContext>>);
#[derive(Clone)] #[derive(Clone)]
pub struct AsyncAppContext(Rc<RefCell<MutableAppContext>>); pub struct AsyncAppContext(Rc<RefCell<MutableAppContext>>);
pub struct WindowInputHandler {
app: Rc<RefCell<MutableAppContext>>,
window_id: usize,
}
impl App { impl App {
pub fn new(asset_source: impl AssetSource) -> Result<Self> { pub fn new(asset_source: impl AssetSource) -> Result<Self> {
let platform = platform::current::platform(); let platform = platform::current::platform();
let foreground_platform = platform::current::foreground_platform();
let foreground = Rc::new(executor::Foreground::platform(platform.dispatcher())?); 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( let app = Self(Rc::new(RefCell::new(MutableAppContext::new(
foreground, foreground,
Arc::new(executor::Background::new()), Arc::new(executor::Background::new()),
@ -217,33 +208,7 @@ impl App {
cx.borrow_mut().quit(); cx.borrow_mut().quit();
} }
})); }));
foreground_platform.on_will_open_menu(Box::new({ setup_menu_handlers(foreground_platform.as_ref(), &app);
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);
}
}));
app.0.borrow_mut().weak_self = Some(Rc::downgrade(&app.0)); app.0.borrow_mut().weak_self = Some(Rc::downgrade(&app.0));
Ok(app) Ok(app)
@ -346,94 +311,6 @@ impl App {
} }
} }
impl WindowInputHandler {
fn read_focused_view<T, F>(&self, f: F) -> Option<T>
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<T, F>(&mut self, f: F) -> Option<T>
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<usize>) -> Option<String> {
self.read_focused_view(|view, cx| view.text_for_range(range.clone(), cx))
.flatten()
}
fn selected_text_range(&self) -> Option<Range<usize>> {
self.read_focused_view(|view, cx| view.selected_text_range(cx))
.flatten()
}
fn replace_text_in_range(&mut self, range: Option<Range<usize>>, 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<Range<usize>> {
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<Range<usize>>,
new_text: &str,
new_selected_range: Option<Range<usize>>,
) {
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<usize>) -> Option<RectF> {
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 { impl AsyncAppContext {
pub fn spawn<F, Fut, T>(&self, f: F) -> Task<T> pub fn spawn<F, Fut, T>(&self, f: F) -> Task<T>
where where
@ -593,6 +470,7 @@ type ReleaseObservationCallback = Box<dyn FnMut(&dyn Any, &mut MutableAppContext
type ActionObservationCallback = Box<dyn FnMut(TypeId, &mut MutableAppContext)>; type ActionObservationCallback = Box<dyn FnMut(TypeId, &mut MutableAppContext)>;
type WindowActivationCallback = Box<dyn FnMut(bool, &mut MutableAppContext) -> bool>; type WindowActivationCallback = Box<dyn FnMut(bool, &mut MutableAppContext) -> bool>;
type WindowFullscreenCallback = Box<dyn FnMut(bool, &mut MutableAppContext) -> bool>; type WindowFullscreenCallback = Box<dyn FnMut(bool, &mut MutableAppContext) -> bool>;
type WindowBoundsCallback = Box<dyn FnMut(WindowBounds, Uuid, &mut MutableAppContext) -> bool>;
type KeystrokeCallback = Box< type KeystrokeCallback = Box<
dyn FnMut(&Keystroke, &MatchResult, Option<&Box<dyn Action>>, &mut MutableAppContext) -> bool, dyn FnMut(&Keystroke, &MatchResult, Option<&Box<dyn Action>>, &mut MutableAppContext) -> bool,
>; >;
@ -623,6 +501,7 @@ pub struct MutableAppContext {
action_dispatch_observations: CallbackCollection<(), ActionObservationCallback>, action_dispatch_observations: CallbackCollection<(), ActionObservationCallback>,
window_activation_observations: CallbackCollection<usize, WindowActivationCallback>, window_activation_observations: CallbackCollection<usize, WindowActivationCallback>,
window_fullscreen_observations: CallbackCollection<usize, WindowFullscreenCallback>, window_fullscreen_observations: CallbackCollection<usize, WindowFullscreenCallback>,
window_bounds_observations: CallbackCollection<usize, WindowBoundsCallback>,
keystroke_observations: CallbackCollection<usize, KeystrokeCallback>, keystroke_observations: CallbackCollection<usize, KeystrokeCallback>,
#[allow(clippy::type_complexity)] #[allow(clippy::type_complexity)]
@ -680,6 +559,7 @@ impl MutableAppContext {
global_observations: Default::default(), global_observations: Default::default(),
window_activation_observations: Default::default(), window_activation_observations: Default::default(),
window_fullscreen_observations: Default::default(), window_fullscreen_observations: Default::default(),
window_bounds_observations: Default::default(),
keystroke_observations: Default::default(), keystroke_observations: Default::default(),
action_dispatch_observations: Default::default(), action_dispatch_observations: Default::default(),
presenters_and_platform_windows: 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<Item = usize> + '_ { pub fn window_ids(&self) -> impl Iterator<Item = usize> + '_ {
self.cx.windows.keys().cloned() self.cx.windows.keys().copied()
} }
pub fn activate_window(&self, window_id: usize) { pub fn activate_window(&self, window_id: usize) {
@ -896,8 +784,14 @@ impl MutableAppContext {
.map_or(false, |window| window.is_fullscreen) .map_or(false, |window| window.is_fullscreen)
} }
pub fn window_bounds(&self, window_id: usize) -> RectF { pub fn window_bounds(&self, window_id: usize) -> Option<WindowBounds> {
self.presenters_and_platform_windows[&window_id].1.bounds() let (_, window) = self.presenters_and_platform_windows.get(&window_id)?;
Some(window.bounds())
}
pub fn window_display_uuid(&self, window_id: usize) -> Option<Uuid> {
let (_, window) = self.presenters_and_platform_windows.get(&window_id)?;
window.screen().display_uuid()
} }
pub fn render_view(&mut self, params: RenderParams) -> Result<ElementBox> { pub fn render_view(&mut self, params: RenderParams) -> Result<ElementBox> {
@ -964,11 +858,6 @@ impl MutableAppContext {
result result
} }
pub fn set_menus(&mut self, menus: Vec<Menu>) {
self.foreground_platform
.set_menus(menus, &self.keystroke_matcher);
}
fn show_character_palette(&self, window_id: usize) { fn show_character_palette(&self, window_id: usize) {
let (_, window) = &self.presenters_and_platform_windows[&window_id]; let (_, window) = &self.presenters_and_platform_windows[&window_id];
window.show_character_palette(); window.show_character_palette();
@ -1011,6 +900,10 @@ impl MutableAppContext {
self.foreground_platform.prompt_for_new_path(directory) 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<E: Any>(&mut self, payload: E) { pub fn emit_global<E: Any>(&mut self, payload: E) {
self.pending_effects.push_back(Effect::GlobalEvent { self.pending_effects.push_back(Effect::GlobalEvent {
payload: Box::new(payload), payload: Box::new(payload),
@ -1231,6 +1124,23 @@ impl MutableAppContext {
) )
} }
fn observe_window_bounds<F>(&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<F>(&mut self, window_id: usize, callback: F) -> Subscription pub fn observe_keystrokes<F>(&mut self, window_id: usize, callback: F) -> Subscription
where where
F: 'static F: 'static
@ -1295,6 +1205,31 @@ impl MutableAppContext {
self.action_deserializers.keys().copied() 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<SmallVec<[Keystroke; 2]>> {
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( pub fn available_actions(
&self, &self,
window_id: usize, window_id: usize,
@ -1302,8 +1237,10 @@ impl MutableAppContext {
) -> impl Iterator<Item = (&'static str, Box<dyn Action>, SmallVec<[&Binding; 1]>)> { ) -> impl Iterator<Item = (&'static str, Box<dyn Action>, SmallVec<[&Binding; 1]>)> {
let mut action_types: HashSet<_> = self.global_actions.keys().copied().collect(); 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) { for view_id in self.ancestors(window_id, view_id) {
if let Some(view) = self.views.get(&(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(); let view_type = view.as_any().type_id();
if let Some(actions) = self.actions.get(&view_type) { if let Some(actions) = self.actions.get(&view_type) {
action_types.extend(actions.keys().copied()); action_types.extend(actions.keys().copied());
@ -1320,6 +1257,7 @@ impl MutableAppContext {
deserialize("{}").ok()?, deserialize("{}").ok()?,
self.keystroke_matcher self.keystroke_matcher
.bindings_for_action_type(*type_id) .bindings_for_action_type(*type_id)
.filter(|b| b.match_context(&contexts))
.collect(), .collect(),
)) ))
} else { } else {
@ -1347,34 +1285,6 @@ impl MutableAppContext {
self.global_actions.contains_key(&action_type) 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<SmallVec<[Keystroke; 2]>> {
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 // 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. // view calling visit with true. Then walks back up the tree calling visit with false.
// If `visit` returns false this function will immediately return. // If `visit` returns false this function will immediately return.
@ -1405,21 +1315,6 @@ impl MutableAppContext {
true 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<Item = usize> + '_ {
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( fn actions_mut(
&mut self, &mut self,
capture_phase: bool, 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(); let mut app = self.upgrade();
window.on_fullscreen(Box::new(move |is_fullscreen| { window.on_fullscreen(Box::new(move |is_fullscreen| {
@ -1886,10 +1788,11 @@ impl MutableAppContext {
{ {
self.update(|this| { self.update(|this| {
let view_id = post_inc(&mut this.next_entity_id); 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 mut cx = ViewContext::new(this, window_id, view_id);
let handle = if let Some(view) = build_view(&mut cx) { let handle = if let Some(view) = build_view(&mut cx) {
this.cx.views.insert((window_id, view_id), Box::new(view)); 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) { if let Some(window) = this.cx.windows.get_mut(&window_id) {
window window
.invalidation .invalidation
@ -1899,6 +1802,7 @@ impl MutableAppContext {
} }
Some(ViewHandle::new(window_id, view_id, &this.cx.ref_counts)) Some(ViewHandle::new(window_id, view_id, &this.cx.ref_counts))
} else { } else {
this.cx.parents.remove(&(window_id, view_id));
None None
}; };
handle handle
@ -2062,6 +1966,11 @@ impl MutableAppContext {
.invalidation .invalidation
.get_or_insert(WindowInvalidation::default()); .get_or_insert(WindowInvalidation::default());
} }
self.handle_window_moved(window_id);
}
Effect::MoveWindow { window_id } => {
self.handle_window_moved(window_id);
} }
Effect::WindowActivationObservation { Effect::WindowActivationObservation {
@ -2094,6 +2003,16 @@ impl MutableAppContext {
is_fullscreen, is_fullscreen,
} => self.handle_fullscreen_effect(window_id, 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 => { Effect::RefreshWindows => {
refreshing = true; refreshing = true;
} }
@ -2188,6 +2107,11 @@ impl MutableAppContext {
.push_back(Effect::ResizeWindow { window_id }); .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) { fn window_was_fullscreen_changed(&mut self, window_id: usize, is_fullscreen: bool) {
self.pending_effects.push_back(Effect::FullscreenWindow { self.pending_effects.push_back(Effect::FullscreenWindow {
window_id, window_id,
@ -2320,11 +2244,21 @@ impl MutableAppContext {
let window = this.cx.windows.get_mut(&window_id)?; let window = this.cx.windows.get_mut(&window_id)?;
window.is_fullscreen = is_fullscreen; window.is_fullscreen = is_fullscreen;
let mut observations = this.window_fullscreen_observations.clone(); let mut fullscreen_observations = this.window_fullscreen_observations.clone();
observations.emit(window_id, this, |callback, this| { fullscreen_observations.emit(window_id, this, |callback, this| {
callback(is_fullscreen, 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(()) 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<usize>) { pub fn focus(&mut self, window_id: usize, view_id: Option<usize>) {
self.pending_effects self.pending_effects
.push_back(Effect::Focus { window_id, view_id }); .push_back(Effect::Focus { window_id, view_id });
@ -2724,6 +2672,42 @@ impl AppContext {
panic!("no global has been added for {}", type_name::<T>()); panic!("no global has been added for {}", type_name::<T>());
} }
} }
/// 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<Item = usize> + '_ {
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<usize> {
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<AnyViewHandle>) -> 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 { impl ReadModel for AppContext {
@ -2878,9 +2862,8 @@ pub enum Effect {
ResizeWindow { ResizeWindow {
window_id: usize, window_id: usize,
}, },
FullscreenWindow { MoveWindow {
window_id: usize, window_id: usize,
is_fullscreen: bool,
}, },
ActivateWindow { ActivateWindow {
window_id: usize, window_id: usize,
@ -2891,11 +2874,20 @@ pub enum Effect {
subscription_id: usize, subscription_id: usize,
callback: WindowActivationCallback, callback: WindowActivationCallback,
}, },
FullscreenWindow {
window_id: usize,
is_fullscreen: bool,
},
WindowFullscreenObservation { WindowFullscreenObservation {
window_id: usize, window_id: usize,
subscription_id: usize, subscription_id: usize,
callback: WindowFullscreenCallback, callback: WindowFullscreenCallback,
}, },
WindowBoundsObservation {
window_id: usize,
subscription_id: usize,
callback: WindowBoundsCallback,
},
Keystroke { Keystroke {
window_id: usize, window_id: usize,
keystroke: Keystroke, keystroke: Keystroke,
@ -3006,6 +2998,10 @@ impl Debug for Effect {
.debug_struct("Effect::RefreshWindow") .debug_struct("Effect::RefreshWindow")
.field("window_id", window_id) .field("window_id", window_id)
.finish(), .finish(),
Effect::MoveWindow { window_id } => f
.debug_struct("Effect::MoveWindow")
.field("window_id", window_id)
.finish(),
Effect::WindowActivationObservation { Effect::WindowActivationObservation {
window_id, window_id,
subscription_id, subscription_id,
@ -3040,6 +3036,16 @@ impl Debug for Effect {
.field("window_id", window_id) .field("window_id", window_id)
.field("subscription_id", subscription_id) .field("subscription_id", subscription_id)
.finish(), .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::RefreshWindows => f.debug_struct("Effect::FullViewRefresh").finish(),
Effect::WindowShouldCloseSubscription { window_id, .. } => f Effect::WindowShouldCloseSubscription { window_id, .. } => f
.debug_struct("Effect::WindowShouldCloseSubscription") .debug_struct("Effect::WindowShouldCloseSubscription")
@ -3615,10 +3621,6 @@ impl<'a, T: View> ViewContext<'a, T> {
self.app.toggle_window_full_screen(self.window_id) 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( pub fn prompt(
&self, &self,
level: PromptLevel, level: PromptLevel,
@ -3639,6 +3641,10 @@ impl<'a, T: View> ViewContext<'a, T> {
self.app.prompt_for_new_path(directory) 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 { pub fn debug_elements(&self) -> crate::json::Value {
self.app.debug_elements(self.window_id).unwrap() 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) .build_and_insert_view(self.window_id, ParentId::View(self.view_id), build_view)
} }
pub fn parent(&mut self) -> Option<usize> {
self.cx.parent(self.window_id, self.view_id)
}
pub fn reparent(&mut self, view_handle: impl Into<AnyViewHandle>) { pub fn reparent(&mut self, view_handle: impl Into<AnyViewHandle>) {
let view_handle = view_handle.into(); let view_handle = view_handle.into();
if self.window_id != view_handle.window_id { if self.window_id != view_handle.window_id {
@ -3892,7 +3902,7 @@ impl<'a, T: View> ViewContext<'a, T> {
}) })
} }
pub fn observe_keystroke<F>(&mut self, mut callback: F) -> Subscription pub fn observe_keystrokes<F>(&mut self, mut callback: F) -> Subscription
where where
F: 'static F: 'static
+ FnMut( + FnMut(
@ -3919,6 +3929,24 @@ impl<'a, T: View> ViewContext<'a, T> {
) )
} }
pub fn observe_window_bounds<F>(&mut self, mut callback: F) -> Subscription
where
F: 'static + FnMut(&mut T, WindowBounds, Uuid, &mut ViewContext<T>),
{
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) { pub fn emit(&mut self, payload: T::Event) {
self.app.pending_effects.push_back(Effect::Event { self.app.pending_effects.push_back(Effect::Event {
entity_id: self.view_id, entity_id: self.view_id,
@ -4781,6 +4809,12 @@ impl<T: View> From<ViewHandle<T>> for AnyViewHandle {
} }
} }
impl<T> PartialEq<ViewHandle<T>> for AnyViewHandle {
fn eq(&self, other: &ViewHandle<T>) -> bool {
self.window_id == other.window_id && self.view_id == other.view_id
}
}
impl Drop for AnyViewHandle { impl Drop for AnyViewHandle {
fn drop(&mut self) { fn drop(&mut self) {
self.ref_counts self.ref_counts
@ -5083,6 +5117,7 @@ pub enum Subscription {
FocusObservation(callback_collection::Subscription<usize, FocusObservationCallback>), FocusObservation(callback_collection::Subscription<usize, FocusObservationCallback>),
WindowActivationObservation(callback_collection::Subscription<usize, WindowActivationCallback>), WindowActivationObservation(callback_collection::Subscription<usize, WindowActivationCallback>),
WindowFullscreenObservation(callback_collection::Subscription<usize, WindowFullscreenCallback>), WindowFullscreenObservation(callback_collection::Subscription<usize, WindowFullscreenCallback>),
WindowBoundsObservation(callback_collection::Subscription<usize, WindowBoundsCallback>),
KeystrokeObservation(callback_collection::Subscription<usize, KeystrokeCallback>), KeystrokeObservation(callback_collection::Subscription<usize, KeystrokeCallback>),
ReleaseObservation(callback_collection::Subscription<usize, ReleaseObservationCallback>), ReleaseObservation(callback_collection::Subscription<usize, ReleaseObservationCallback>),
ActionObservation(callback_collection::Subscription<(), ActionObservationCallback>), ActionObservation(callback_collection::Subscription<(), ActionObservationCallback>),
@ -5098,6 +5133,7 @@ impl Subscription {
Subscription::FocusObservation(subscription) => subscription.id(), Subscription::FocusObservation(subscription) => subscription.id(),
Subscription::WindowActivationObservation(subscription) => subscription.id(), Subscription::WindowActivationObservation(subscription) => subscription.id(),
Subscription::WindowFullscreenObservation(subscription) => subscription.id(), Subscription::WindowFullscreenObservation(subscription) => subscription.id(),
Subscription::WindowBoundsObservation(subscription) => subscription.id(),
Subscription::KeystrokeObservation(subscription) => subscription.id(), Subscription::KeystrokeObservation(subscription) => subscription.id(),
Subscription::ReleaseObservation(subscription) => subscription.id(), Subscription::ReleaseObservation(subscription) => subscription.id(),
Subscription::ActionObservation(subscription) => subscription.id(), Subscription::ActionObservation(subscription) => subscription.id(),
@ -5114,211 +5150,13 @@ impl Subscription {
Subscription::KeystrokeObservation(subscription) => subscription.detach(), Subscription::KeystrokeObservation(subscription) => subscription.detach(),
Subscription::WindowActivationObservation(subscription) => subscription.detach(), Subscription::WindowActivationObservation(subscription) => subscription.detach(),
Subscription::WindowFullscreenObservation(subscription) => subscription.detach(), Subscription::WindowFullscreenObservation(subscription) => subscription.detach(),
Subscription::WindowBoundsObservation(subscription) => subscription.detach(),
Subscription::ReleaseObservation(subscription) => subscription.detach(), Subscription::ReleaseObservation(subscription) => subscription.detach(),
Subscription::ActionObservation(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<usize, Option<backtrace::Backtrace>>,
),
>,
}
#[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<usize, usize>,
element_state_counts: HashMap<ElementStateId, ElementStateRefCount>,
dropped_models: HashSet<usize>,
dropped_views: HashSet<(usize, usize)>,
dropped_element_states: HashSet<ElementStateId>,
#[cfg(any(test, feature = "test-support"))]
leak_detector: Arc<Mutex<LeakDetector>>,
}
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<usize>,
HashSet<(usize, usize)>,
HashSet<ElementStateId>,
) {
(
std::mem::take(&mut self.dropped_models),
std::mem::take(&mut self.dropped_views),
std::mem::take(&mut self.dropped_element_states),
)
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@ -6374,6 +6212,8 @@ mod tests {
cx.focus(&view_1); cx.focus(&view_1);
cx.focus(&view_2); cx.focus(&view_2);
}); });
assert!(cx.is_child_focused(view_1.clone()));
assert!(!cx.is_child_focused(view_2.clone()));
assert_eq!( assert_eq!(
mem::take(&mut *view_events.lock()), mem::take(&mut *view_events.lock()),
[ [
@ -6398,6 +6238,8 @@ mod tests {
); );
view_1.update(cx, |_, cx| cx.focus(&view_1)); 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!( assert_eq!(
mem::take(&mut *view_events.lock()), mem::take(&mut *view_events.lock()),
["view 2 blurred", "view 1 focused"], ["view 2 blurred", "view 1 focused"],

View file

@ -0,0 +1,52 @@
use crate::{Action, App, ForegroundPlatform, MutableAppContext};
pub struct Menu<'a> {
pub name: &'a str,
pub items: Vec<MenuItem<'a>>,
}
pub enum MenuItem<'a> {
Separator,
Submenu(Menu<'a>),
Action {
name: &'a str,
action: Box<dyn Action>,
},
}
impl MutableAppContext {
pub fn set_menus(&mut self, menus: Vec<Menu>) {
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);
}
}));
}

View file

@ -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<usize, usize>,
element_state_counts: HashMap<ElementStateId, ElementStateRefCount>,
dropped_models: HashSet<usize>,
dropped_views: HashSet<(usize, usize)>,
dropped_element_states: HashSet<ElementStateId>,
#[cfg(any(test, feature = "test-support"))]
pub leak_detector: Arc<Mutex<LeakDetector>>,
}
impl RefCounts {
#[cfg(any(test, feature = "test-support"))]
pub fn new(leak_detector: Arc<Mutex<LeakDetector>>) -> 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<usize>,
HashSet<(usize, usize)>,
HashSet<ElementStateId>,
) {
(
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<usize, Option<backtrace::Backtrace>>,
),
>,
}
#[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);
}
}

View file

@ -19,13 +19,14 @@ use smol::stream::StreamExt;
use crate::{ use crate::{
executor, geometry::vector::Vector2F, keymap_matcher::Keystroke, platform, Action, executor, geometry::vector::Vector2F, keymap_matcher::Keystroke, platform, Action,
AnyViewHandle, AppContext, Appearance, Entity, Event, FontCache, InputHandler, KeyDownEvent, AnyViewHandle, AppContext, Appearance, Entity, Event, FontCache, InputHandler, KeyDownEvent,
LeakDetector, ModelContext, ModelHandle, MutableAppContext, Platform, ReadModelWith, ModelContext, ModelHandle, MutableAppContext, Platform, ReadModelWith, ReadViewWith,
ReadViewWith, RenderContext, Task, UpdateModel, UpdateView, View, ViewContext, ViewHandle, RenderContext, Task, UpdateModel, UpdateView, View, ViewContext, ViewHandle, WeakHandle,
WeakHandle, WindowInputHandler,
}; };
use collections::BTreeMap; use collections::BTreeMap;
use super::{AsyncAppContext, RefCounts}; use super::{
ref_counts::LeakDetector, window_input_handler::WindowInputHandler, AsyncAppContext, RefCounts,
};
#[derive(Clone)] #[derive(Clone)]
pub struct TestAppContext { pub struct TestAppContext {
@ -53,11 +54,7 @@ impl TestAppContext {
platform, platform,
foreground_platform.clone(), foreground_platform.clone(),
font_cache, font_cache,
RefCounts { RefCounts::new(leak_detector),
#[cfg(any(test, feature = "test-support"))]
leak_detector,
..Default::default()
},
(), (),
); );
cx.next_entity_id = first_entity_id; cx.next_entity_id = first_entity_id;
@ -625,6 +622,8 @@ impl<T: View> ViewHandle<T> {
} }
} }
/// 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)] #[derive(Clone)]
pub struct AssertionContextManager { pub struct AssertionContextManager {
id: Arc<AtomicUsize>, id: Arc<AtomicUsize>,
@ -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 { pub struct ContextHandle {
id: usize, id: usize,
manager: AssertionContextManager, manager: AssertionContextManager,

View file

@ -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<RefCell<MutableAppContext>>,
pub window_id: usize,
}
impl WindowInputHandler {
fn read_focused_view<T, F>(&self, f: F) -> Option<T>
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<T, F>(&mut self, f: F) -> Option<T>
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<usize>) -> Option<String> {
self.read_focused_view(|view, cx| view.text_for_range(range.clone(), cx))
.flatten()
}
fn selected_text_range(&self) -> Option<Range<usize>> {
self.read_focused_view(|view, cx| view.selected_text_range(cx))
.flatten()
}
fn replace_text_in_range(&mut self, range: Option<Range<usize>>, 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<Range<usize>> {
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<Range<usize>>,
new_text: &str,
new_selected_range: Option<Range<usize>>,
) {
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<usize>) -> Option<RectF> {
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)
}
}

View file

@ -1,5 +1,6 @@
mod align; mod align;
mod canvas; mod canvas;
mod clipped;
mod constrained_box; mod constrained_box;
mod container; mod container;
mod empty; mod empty;
@ -19,12 +20,12 @@ mod text;
mod tooltip; mod tooltip;
mod uniform_list; mod uniform_list;
use self::expanded::Expanded;
pub use self::{ pub use self::{
align::*, canvas::*, constrained_box::*, container::*, empty::*, flex::*, hook::*, image::*, align::*, canvas::*, constrained_box::*, container::*, empty::*, flex::*, hook::*, image::*,
keystroke_label::*, label::*, list::*, mouse_event_handler::*, overlay::*, resizable::*, keystroke_label::*, label::*, list::*, mouse_event_handler::*, overlay::*, resizable::*,
stack::*, svg::*, text::*, tooltip::*, uniform_list::*, stack::*, svg::*, text::*, tooltip::*, uniform_list::*,
}; };
use self::{clipped::Clipped, expanded::Expanded};
pub use crate::presenter::ChildView; pub use crate::presenter::ChildView;
use crate::{ use crate::{
geometry::{ geometry::{
@ -135,6 +136,13 @@ pub trait Element {
Align::new(self.boxed()) Align::new(self.boxed())
} }
fn clipped(self) -> Clipped
where
Self: 'static + Sized,
{
Clipped::new(self.boxed())
}
fn contained(self) -> Container fn contained(self) -> Container
where where
Self: 'static + Sized, Self: 'static + Sized,

View file

@ -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<usize>,
_: RectF,
_: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
cx: &MeasurementContext,
) -> Option<RectF> {
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)
})
}
}

View file

@ -12,15 +12,21 @@ pub struct KeystrokeLabel {
action: Box<dyn Action>, action: Box<dyn Action>,
container_style: ContainerStyle, container_style: ContainerStyle,
text_style: TextStyle, text_style: TextStyle,
window_id: usize,
view_id: usize,
} }
impl KeystrokeLabel { impl KeystrokeLabel {
pub fn new( pub fn new(
window_id: usize,
view_id: usize,
action: Box<dyn Action>, action: Box<dyn Action>,
container_style: ContainerStyle, container_style: ContainerStyle,
text_style: TextStyle, text_style: TextStyle,
) -> Self { ) -> Self {
Self { Self {
window_id,
view_id,
action, action,
container_style, container_style,
text_style, text_style,
@ -37,7 +43,10 @@ impl Element for KeystrokeLabel {
constraint: SizeConstraint, constraint: SizeConstraint,
cx: &mut LayoutContext, cx: &mut LayoutContext,
) -> (Vector2F, ElementBox) { ) -> (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() Flex::row()
.with_children(keystrokes.iter().map(|keystroke| { .with_children(keystrokes.iter().map(|keystroke| {
Label::new(keystroke.to_string(), self.text_style.clone()) Label::new(keystroke.to_string(), self.text_style.clone())

View file

@ -61,11 +61,14 @@ impl Tooltip {
) -> Self { ) -> Self {
struct ElementState<Tag>(Tag); struct ElementState<Tag>(Tag);
struct MouseEventHandlerState<Tag>(Tag); struct MouseEventHandlerState<Tag>(Tag);
let focused_view_id = cx.focused_view_id(cx.window_id);
let state_handle = cx.default_element_state::<ElementState<Tag>, Rc<TooltipState>>(id); let state_handle = cx.default_element_state::<ElementState<Tag>, Rc<TooltipState>>(id);
let state = state_handle.read(cx).clone(); let state = state_handle.read(cx).clone();
let tooltip = if state.visible.get() { let tooltip = if state.visible.get() {
let mut collapsed_tooltip = Self::render_tooltip( let mut collapsed_tooltip = Self::render_tooltip(
cx.window_id,
focused_view_id,
text.clone(), text.clone(),
style.clone(), style.clone(),
action.as_ref().map(|a| a.boxed_clone()), action.as_ref().map(|a| a.boxed_clone()),
@ -74,7 +77,7 @@ impl Tooltip {
.boxed(); .boxed();
Some( Some(
Overlay::new( Overlay::new(
Self::render_tooltip(text, style, action, false) Self::render_tooltip(cx.window_id, focused_view_id, text, style, action, false)
.constrained() .constrained()
.dynamically(move |constraint, cx| { .dynamically(move |constraint, cx| {
SizeConstraint::strict_along( SizeConstraint::strict_along(
@ -128,6 +131,8 @@ impl Tooltip {
} }
pub fn render_tooltip( pub fn render_tooltip(
window_id: usize,
focused_view_id: Option<usize>,
text: String, text: String,
style: TooltipStyle, style: TooltipStyle,
action: Option<Box<dyn Action>>, action: Option<Box<dyn Action>>,
@ -144,13 +149,18 @@ impl Tooltip {
text.flex(1., false).aligned().boxed() text.flex(1., false).aligned().boxed()
} }
}) })
.with_children(action.map(|action| { .with_children(action.and_then(|action| {
let keystroke_label = let keystroke_label = KeystrokeLabel::new(
KeystrokeLabel::new(action, style.keystroke.container, style.keystroke.text); window_id,
focused_view_id?,
action,
style.keystroke.container,
style.keystroke.text,
);
if measure { if measure {
keystroke_label.boxed() Some(keystroke_label.boxed())
} else { } else {
keystroke_label.aligned().boxed() Some(keystroke_label.aligned().boxed())
} }
})) }))
.contained() .contained()

View file

@ -5,25 +5,16 @@ mod keystroke;
use std::{any::TypeId, fmt::Debug}; use std::{any::TypeId, fmt::Debug};
use collections::HashMap; use collections::{BTreeMap, HashMap};
use serde::Deserialize;
use smallvec::SmallVec; use smallvec::SmallVec;
use crate::{impl_actions, Action}; use crate::Action;
pub use binding::{Binding, BindingMatchResult}; pub use binding::{Binding, BindingMatchResult};
pub use keymap::Keymap; pub use keymap::Keymap;
pub use keymap_context::{KeymapContext, KeymapContextPredicate}; pub use keymap_context::{KeymapContext, KeymapContextPredicate};
pub use keystroke::Keystroke; 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 struct KeymapMatcher {
pub contexts: Vec<KeymapContext>, pub contexts: Vec<KeymapContext>,
pending_views: HashMap<usize, KeymapContext>, pending_views: HashMap<usize, KeymapContext>,
@ -69,13 +60,28 @@ impl KeymapMatcher {
!self.pending_keystrokes.is_empty() !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( pub fn push_keystroke(
&mut self, &mut self,
keystroke: Keystroke, keystroke: Keystroke,
mut dispatch_path: Vec<(usize, KeymapContext)>, mut dispatch_path: Vec<(usize, KeymapContext)>,
) -> MatchResult { ) -> MatchResult {
let mut any_pending = false; let mut any_pending = false;
let mut matched_bindings: Vec<(usize, Box<dyn Action>)> = 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<usize, Vec<(usize, Box<dyn Action>)>> =
Default::default();
let first_keystroke = self.pending_keystrokes.is_empty(); let first_keystroke = self.pending_keystrokes.is_empty();
self.pending_keystrokes.push(keystroke.clone()); self.pending_keystrokes.push(keystroke.clone());
@ -84,35 +90,33 @@ impl KeymapMatcher {
self.contexts self.contexts
.extend(dispatch_path.iter_mut().map(|e| std::mem::take(&mut e.1))); .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 // 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; continue;
} }
// If there is a previous view context, invalidate that view if it // If there is a previous view context, invalidate that view if it
// has changed // 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] { if previous_view_context != self.contexts[i] {
continue; continue;
} }
} }
// Find the bindings which map the pending keystrokes and current context for (order, binding) in self.keymap.bindings().iter().rev().enumerate() {
for binding in self.keymap.bindings().iter().rev() {
match binding.match_keys_and_context(&self.pending_keystrokes, &self.contexts[i..]) match binding.match_keys_and_context(&self.pending_keystrokes, &self.contexts[i..])
{ {
BindingMatchResult::Complete(mut action) => { BindingMatchResult::Complete(action) => {
// Swap in keystroke for special KeyPressed action matched_bindings
if action.name() == "KeyPressed" && action.namespace() == "gpui" { .entry(order)
action = Box::new(KeyPressed { .or_default()
keystroke: keystroke.clone(), .push((*view_id, action));
});
}
matched_bindings.push((view_id, action))
} }
BindingMatchResult::Partial => { 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; any_pending = true;
} }
_ => {} _ => {}
@ -125,7 +129,9 @@ impl KeymapMatcher {
} }
if !matched_bindings.is_empty() { 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 { } else if any_pending {
MatchResult::Pending MatchResult::Pending
} else { } else {

View file

@ -7,7 +7,7 @@ use super::{KeymapContext, KeymapContextPredicate, Keystroke};
pub struct Binding { pub struct Binding {
action: Box<dyn Action>, action: Box<dyn Action>,
keystrokes: Option<SmallVec<[Keystroke; 2]>>, keystrokes: SmallVec<[Keystroke; 2]>,
context_predicate: Option<KeymapContextPredicate>, context_predicate: Option<KeymapContextPredicate>,
} }
@ -23,16 +23,10 @@ impl Binding {
None None
}; };
let keystrokes = if keystrokes == "*" { let keystrokes = keystrokes
None // Catch all context .split_whitespace()
} else { .map(Keystroke::parse)
Some( .collect::<Result<_>>()?;
keystrokes
.split_whitespace()
.map(Keystroke::parse)
.collect::<Result<_>>()?,
)
};
Ok(Self { Ok(Self {
keystrokes, 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 self.context_predicate
.as_ref() .as_ref()
.map(|predicate| predicate.eval(contexts)) .map(|predicate| predicate.eval(contexts))
@ -53,20 +47,10 @@ impl Binding {
pending_keystrokes: &Vec<Keystroke>, pending_keystrokes: &Vec<Keystroke>,
contexts: &[KeymapContext], contexts: &[KeymapContext],
) -> BindingMatchResult { ) -> BindingMatchResult {
if self if self.keystrokes.as_ref().starts_with(&pending_keystrokes) && self.match_context(contexts)
.keystrokes
.as_ref()
.map(|keystrokes| keystrokes.starts_with(&pending_keystrokes))
.unwrap_or(true)
&& self.match_context(contexts)
{ {
// If the binding is completed, push it onto the matches list // If the binding is completed, push it onto the matches list
if self if self.keystrokes.as_ref().len() == pending_keystrokes.len() {
.keystrokes
.as_ref()
.map(|keystrokes| keystrokes.len() == pending_keystrokes.len())
.unwrap_or(true)
{
BindingMatchResult::Complete(self.action.boxed_clone()) BindingMatchResult::Complete(self.action.boxed_clone())
} else { } else {
BindingMatchResult::Partial BindingMatchResult::Partial
@ -82,14 +66,14 @@ impl Binding {
contexts: &[KeymapContext], contexts: &[KeymapContext],
) -> Option<SmallVec<[Keystroke; 2]>> { ) -> Option<SmallVec<[Keystroke; 2]>> {
if self.action.eq(action) && self.match_context(contexts) { if self.action.eq(action) && self.match_context(contexts) {
self.keystrokes.clone() Some(self.keystrokes.clone())
} else { } else {
None None
} }
} }
pub fn keystrokes(&self) -> Option<&[Keystroke]> { pub fn keystrokes(&self) -> &[Keystroke] {
self.keystrokes.as_deref() self.keystrokes.as_slice()
} }
pub fn action(&self) -> &dyn Action { pub fn action(&self) -> &dyn Action {

View file

@ -43,7 +43,7 @@ impl KeymapContextPredicate {
pub fn eval(&self, contexts: &[KeymapContext]) -> bool { pub fn eval(&self, contexts: &[KeymapContext]) -> bool {
let Some(context) = contexts.first() else { return false }; let Some(context) = contexts.first() else { return false };
match self { 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 Self::Equal(left, right) => context
.map .map
.get(left) .get(left)

View file

@ -18,11 +18,15 @@ use crate::{
text_layout::{LineLayout, RunStyle}, text_layout::{LineLayout, RunStyle},
Action, ClipboardItem, Menu, Scene, Action, ClipboardItem, Menu, Scene,
}; };
use anyhow::{anyhow, Result}; use anyhow::{anyhow, bail, Result};
use async_task::Runnable; use async_task::Runnable;
pub use event::*; pub use event::*;
use postage::oneshot; use postage::oneshot;
use serde::Deserialize; use serde::Deserialize;
use sqlez::{
bindable::{Bind, Column, StaticColumnCount},
statement::Statement,
};
use std::{ use std::{
any::Any, any::Any,
fmt::{self, Debug, Display}, fmt::{self, Debug, Display},
@ -33,6 +37,7 @@ use std::{
sync::Arc, sync::Arc,
}; };
use time::UtcOffset; use time::UtcOffset;
use uuid::Uuid;
pub trait Platform: Send + Sync { pub trait Platform: Send + Sync {
fn dispatcher(&self) -> Arc<dyn Dispatcher>; fn dispatcher(&self) -> Arc<dyn Dispatcher>;
@ -44,6 +49,7 @@ pub trait Platform: Send + Sync {
fn unhide_other_apps(&self); fn unhide_other_apps(&self);
fn quit(&self); fn quit(&self);
fn screen_by_id(&self, id: Uuid) -> Option<Rc<dyn Screen>>;
fn screens(&self) -> Vec<Rc<dyn Screen>>; fn screens(&self) -> Vec<Rc<dyn Screen>>;
fn open_window( fn open_window(
@ -74,6 +80,7 @@ pub trait Platform: Send + Sync {
fn app_version(&self) -> Result<AppVersion>; fn app_version(&self) -> Result<AppVersion>;
fn os_name(&self) -> &'static str; fn os_name(&self) -> &'static str;
fn os_version(&self) -> Result<AppVersion>; fn os_version(&self) -> Result<AppVersion>;
fn restart(&self);
} }
pub(crate) trait ForegroundPlatform { pub(crate) trait ForegroundPlatform {
@ -93,6 +100,7 @@ pub(crate) trait ForegroundPlatform {
options: PathPromptOptions, options: PathPromptOptions,
) -> oneshot::Receiver<Option<Vec<PathBuf>>>; ) -> oneshot::Receiver<Option<Vec<PathBuf>>>;
fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver<Option<PathBuf>>; fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver<Option<PathBuf>>;
fn reveal_path(&self, path: &Path);
} }
pub trait Dispatcher: Send + Sync { pub trait Dispatcher: Send + Sync {
@ -117,17 +125,19 @@ pub trait InputHandler {
pub trait Screen: Debug { pub trait Screen: Debug {
fn as_any(&self) -> &dyn Any; fn as_any(&self) -> &dyn Any;
fn size(&self) -> Vector2F; fn bounds(&self) -> RectF;
fn display_uuid(&self) -> Option<Uuid>;
} }
pub trait Window { 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<dyn Screen>;
fn as_any_mut(&mut self) -> &mut dyn Any; fn as_any_mut(&mut self) -> &mut dyn Any;
fn on_event(&mut self, callback: Box<dyn FnMut(Event) -> bool>);
fn on_active_status_change(&mut self, callback: Box<dyn FnMut(bool)>);
fn on_resize(&mut self, callback: Box<dyn FnMut()>);
fn on_fullscreen(&mut self, callback: Box<dyn FnMut(bool)>);
fn on_should_close(&mut self, callback: Box<dyn FnMut() -> bool>);
fn on_close(&mut self, callback: Box<dyn FnOnce()>);
fn set_input_handler(&mut self, input_handler: Box<dyn InputHandler>); fn set_input_handler(&mut self, input_handler: Box<dyn InputHandler>);
fn prompt(&self, level: PromptLevel, msg: &str, answers: &[&str]) -> oneshot::Receiver<usize>; fn prompt(&self, level: PromptLevel, msg: &str, answers: &[&str]) -> oneshot::Receiver<usize>;
fn activate(&self); fn activate(&self);
@ -136,15 +146,18 @@ pub trait Window {
fn show_character_palette(&self); fn show_character_palette(&self);
fn minimize(&self); fn minimize(&self);
fn zoom(&self); fn zoom(&self);
fn present_scene(&mut self, scene: Scene);
fn toggle_full_screen(&self); fn toggle_full_screen(&self);
fn bounds(&self) -> RectF; fn on_event(&mut self, callback: Box<dyn FnMut(Event) -> bool>);
fn content_size(&self) -> Vector2F; fn on_active_status_change(&mut self, callback: Box<dyn FnMut(bool)>);
fn scale_factor(&self) -> f32; fn on_resize(&mut self, callback: Box<dyn FnMut()>);
fn titlebar_height(&self) -> f32; fn on_fullscreen(&mut self, callback: Box<dyn FnMut(bool)>);
fn present_scene(&mut self, scene: Scene); fn on_moved(&mut self, callback: Box<dyn FnMut()>);
fn appearance(&self) -> Appearance; fn on_should_close(&mut self, callback: Box<dyn FnMut() -> bool>);
fn on_close(&mut self, callback: Box<dyn FnOnce()>);
fn on_appearance_changed(&mut self, callback: Box<dyn FnMut()>); fn on_appearance_changed(&mut self, callback: Box<dyn FnMut()>);
fn is_topmost_for_position(&self, position: Vector2F) -> bool;
} }
#[derive(Debug)] #[derive(Debug)]
@ -185,12 +198,70 @@ pub enum WindowKind {
PopUp, PopUp,
} }
#[derive(Debug)] #[derive(Copy, Clone, Debug, PartialEq)]
pub enum WindowBounds { pub enum WindowBounds {
Fullscreen,
Maximized, Maximized,
Fixed(RectF), Fixed(RectF),
} }
impl StaticColumnCount for WindowBounds {
fn column_count() -> usize {
5
}
}
impl Bind for WindowBounds {
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
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 struct PathPromptOptions {
pub files: bool, pub files: bool,
pub directories: bool, pub directories: bool,

View file

@ -12,20 +12,27 @@ mod sprite_cache;
mod status_item; mod status_item;
mod window; 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 dispatcher::Dispatcher;
pub use fonts::FontSystem; pub use fonts::FontSystem;
use platform::{MacForegroundPlatform, MacPlatform}; use platform::{MacForegroundPlatform, MacPlatform};
pub use renderer::Surface; pub use renderer::Surface;
use std::{rc::Rc, sync::Arc}; use std::{ops::Range, rc::Rc, sync::Arc};
use window::Window; use window::Window;
use crate::executor;
pub(crate) fn platform() -> Arc<dyn super::Platform> { pub(crate) fn platform() -> Arc<dyn super::Platform> {
Arc::new(MacPlatform::new()) Arc::new(MacPlatform::new())
} }
pub(crate) fn foreground_platform() -> Rc<dyn super::ForegroundPlatform> { pub(crate) fn foreground_platform(
Rc::new(MacForegroundPlatform::default()) foreground: Rc<executor::Foreground>,
) -> Rc<dyn super::ForegroundPlatform> {
Rc::new(MacForegroundPlatform::new(foreground))
} }
trait BoolExt { 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<Range<usize>> {
if self.is_valid() {
let start = self.location as usize;
let end = start + self.length as usize;
Some(start..end)
} else {
None
}
}
}
impl From<Range<usize>> for NSRange {
fn from(range: Range<usize>) -> 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()
}

View file

@ -125,6 +125,7 @@ impl Event {
button, button,
position: vec2f( position: vec2f(
native_event.locationInWindow().x as f32, native_event.locationInWindow().x as f32,
// MacOS screen coordinates are relative to bottom left
window_height - native_event.locationInWindow().y as f32, window_height - native_event.locationInWindow().y as f32,
), ),
modifiers: read_modifiers(native_event), modifiers: read_modifiers(native_event),
@ -150,6 +151,7 @@ impl Event {
button, button,
position: vec2f( position: vec2f(
native_event.locationInWindow().x as f32, native_event.locationInWindow().x as f32,
// MacOS view coordinates are relative to bottom left
window_height - native_event.locationInWindow().y as f32, window_height - native_event.locationInWindow().y as f32,
), ),
modifiers: read_modifiers(native_event), modifiers: read_modifiers(native_event),

View file

@ -1,27 +1,97 @@
use cocoa::foundation::{NSPoint, NSRect, NSSize}; use cocoa::{
use pathfinder_geometry::{rect::RectF, vector::Vector2F}; 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 { pub trait Vector2FExt {
fn to_ns_point(&self) -> NSPoint; /// Converts self to an NSPoint with y axis pointing up.
fn to_ns_size(&self) -> NSSize; 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 { 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; fn to_ns_rect(&self) -> NSRect;
} }
impl Vector2FExt for Vector2F {
fn to_ns_point(&self) -> NSPoint {
NSPoint::new(self.x() as f64, self.y() as f64)
}
fn to_ns_size(&self) -> NSSize {
NSSize::new(self.x() as f64, self.y() as f64)
}
}
impl RectFExt for RectF { impl RectFExt for RectF {
fn to_screen_ns_rect(&self, native_window: id) -> NSRect {
unsafe { native_window.convertRectToScreen_(self.to_ns_rect()) }
}
fn to_ns_rect(&self) -> NSRect { fn to_ns_rect(&self) -> NSRect {
NSRect::new(self.origin().to_ns_point(), self.size().to_ns_size()) NSRect::new(
NSPoint::new(
self.origin_x() as f64,
-(self.origin_y() + self.height()) as f64,
),
NSSize::new(self.width() as f64, self.height() as f64),
)
}
}
pub trait NSRectExt {
/// Converts self to a RectF with y axis pointing down.
/// The resulting RectF will have an origin at the top left of the rectangle.
/// Also takes care of converting from screen scale coordinates to window coordinates
fn to_window_rectf(&self, native_window: id) -> RectF;
/// Converts self to a RectF with y axis pointing down.
/// The resulting RectF will have an origin at the top left of the rectangle.
/// Unlike to_screen_ns_rect, coordinates are not converted and are assumed to already be in screen scale
fn to_rectf(&self) -> RectF;
fn intersects(&self, other: Self) -> bool;
}
impl NSRectExt for NSRect {
fn to_window_rectf(&self, native_window: id) -> RectF {
unsafe {
self.origin.x;
let rect: NSRect = native_window.convertRectFromScreen_(*self);
rect.to_rectf()
}
}
fn to_rectf(&self) -> RectF {
RectF::new(
vec2f(
self.origin.x as f32,
-(self.origin.y + self.size.height) as f32,
),
vec2f(self.size.width as f32, self.size.height as f32),
)
}
fn intersects(&self, other: Self) -> bool {
self.size.width > 0.
&& self.size.height > 0.
&& other.size.width > 0.
&& other.size.height > 0.
&& self.origin.x <= other.origin.x + other.size.width
&& self.origin.x + self.size.width >= other.origin.x
&& self.origin.y <= other.origin.y + other.size.height
&& self.origin.y + self.size.height >= other.origin.y
} }
} }

View file

@ -16,7 +16,7 @@ use cocoa::{
NSEventModifierFlags, NSMenu, NSMenuItem, NSModalResponse, NSOpenPanel, NSPasteboard, NSEventModifierFlags, NSMenu, NSMenuItem, NSModalResponse, NSOpenPanel, NSPasteboard,
NSPasteboardTypeString, NSSavePanel, NSWindow, NSPasteboardTypeString, NSSavePanel, NSWindow,
}, },
base::{id, nil, selector, YES}, base::{id, nil, selector, BOOL, YES},
foundation::{ foundation::{
NSArray, NSAutoreleasePool, NSBundle, NSData, NSInteger, NSProcessInfo, NSString, NSArray, NSAutoreleasePool, NSBundle, NSData, NSInteger, NSProcessInfo, NSString,
NSUInteger, NSURL, NSUInteger, NSURL,
@ -45,6 +45,7 @@ use std::{
ffi::{c_void, CStr, OsStr}, ffi::{c_void, CStr, OsStr},
os::{raw::c_char, unix::ffi::OsStrExt}, os::{raw::c_char, unix::ffi::OsStrExt},
path::{Path, PathBuf}, path::{Path, PathBuf},
process::Command,
ptr, ptr,
rc::Rc, rc::Rc,
slice, str, slice, str,
@ -113,10 +114,8 @@ unsafe fn build_classes() {
} }
} }
#[derive(Default)]
pub struct MacForegroundPlatform(RefCell<MacForegroundPlatformState>); pub struct MacForegroundPlatform(RefCell<MacForegroundPlatformState>);
#[derive(Default)]
pub struct MacForegroundPlatformState { pub struct MacForegroundPlatformState {
become_active: Option<Box<dyn FnMut()>>, become_active: Option<Box<dyn FnMut()>>,
resign_active: Option<Box<dyn FnMut()>>, resign_active: Option<Box<dyn FnMut()>>,
@ -128,9 +127,26 @@ pub struct MacForegroundPlatformState {
open_urls: Option<Box<dyn FnMut(Vec<String>)>>, open_urls: Option<Box<dyn FnMut(Vec<String>)>>,
finish_launching: Option<Box<dyn FnOnce()>>, finish_launching: Option<Box<dyn FnOnce()>>,
menu_actions: Vec<Box<dyn Action>>, menu_actions: Vec<Box<dyn Action>>,
foreground: Rc<executor::Foreground>,
} }
impl MacForegroundPlatform { impl MacForegroundPlatform {
pub fn new(foreground: Rc<executor::Foreground>) -> 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( unsafe fn create_menu_bar(
&self, &self,
menus: Vec<Menu>, menus: Vec<Menu>,
@ -184,7 +200,7 @@ impl MacForegroundPlatform {
.map(|binding| binding.keystrokes()); .map(|binding| binding.keystrokes());
let item; let item;
if let Some(keystrokes) = keystrokes.flatten() { if let Some(keystrokes) = keystrokes {
if keystrokes.len() == 1 { if keystrokes.len() == 1 {
let keystroke = &keystrokes[0]; let keystroke = &keystrokes[0];
let mut mask = NSEventModifierFlags::empty(); let mut mask = NSEventModifierFlags::empty();
@ -398,6 +414,26 @@ impl platform::ForegroundPlatform for MacForegroundPlatform {
done_rx 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 { pub struct MacPlatform {
@ -440,6 +476,10 @@ impl platform::Platform for MacPlatform {
self.dispatcher.clone() self.dispatcher.clone()
} }
fn fonts(&self) -> Arc<dyn platform::FontSystem> {
self.fonts.clone()
}
fn activate(&self, ignoring_other_apps: bool) { fn activate(&self, ignoring_other_apps: bool) {
unsafe { unsafe {
let app = NSApplication::sharedApplication(nil); let app = NSApplication::sharedApplication(nil);
@ -488,6 +528,10 @@ impl platform::Platform for MacPlatform {
} }
} }
fn screen_by_id(&self, id: uuid::Uuid) -> Option<Rc<dyn crate::Screen>> {
Screen::find_by_id(id).map(|screen| Rc::new(screen) as Rc<_>)
}
fn screens(&self) -> Vec<Rc<dyn platform::Screen>> { fn screens(&self) -> Vec<Rc<dyn platform::Screen>> {
Screen::all() Screen::all()
.into_iter() .into_iter()
@ -512,10 +556,6 @@ impl platform::Platform for MacPlatform {
Box::new(StatusItem::add(self.fonts())) Box::new(StatusItem::add(self.fonts()))
} }
fn fonts(&self) -> Arc<dyn platform::FontSystem> {
self.fonts.clone()
}
fn write_to_clipboard(&self, item: ClipboardItem) { fn write_to_clipboard(&self, item: ClipboardItem) {
unsafe { unsafe {
self.pasteboard.clearContents(); self.pasteboard.clearContents();
@ -699,7 +739,9 @@ impl platform::Platform for MacPlatform {
unsafe { unsafe {
let cursor: id = match style { let cursor: id = match style {
CursorStyle::Arrow => msg_send![class!(NSCursor), arrowCursor], 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::ResizeUpDown => msg_send![class!(NSCursor), resizeUpDownCursor],
CursorStyle::PointingHand => msg_send![class!(NSCursor), pointingHandCursor], CursorStyle::PointingHand => msg_send![class!(NSCursor), pointingHandCursor],
CursorStyle::IBeam => msg_send![class!(NSCursor), IBeamCursor], 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 { 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()) (0..urls.count())
.into_iter() .into_iter()
.filter_map(|i| { .filter_map(|i| {
let path = urls.objectAtIndex(i); let url = urls.objectAtIndex(i);
match CStr::from_ptr(path.absoluteString().UTF8String() as *mut c_char).to_str() { match CStr::from_ptr(url.absoluteString().UTF8String() as *mut c_char).to_str() {
Ok(string) => Some(string.to_string()), Ok(string) => Some(string.to_string()),
Err(err) => { Err(err) => {
log::error!("error converting path to string: {}", err); log::error!("error converting path to string: {}", err);

View file

@ -1,14 +1,25 @@
use std::any::Any; use std::{any::Any, ffi::c_void};
use crate::{ use crate::platform;
geometry::vector::{vec2f, Vector2F},
platform,
};
use cocoa::{ use cocoa::{
appkit::NSScreen, appkit::NSScreen,
base::{id, nil}, 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)] #[derive(Debug)]
pub struct Screen { pub struct Screen {
@ -16,11 +27,23 @@ pub struct Screen {
} }
impl Screen { impl Screen {
pub fn find_by_id(uuid: Uuid) -> Option<Self> {
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<Self> { pub fn all() -> Vec<Self> {
let mut screens = Vec::new(); let mut screens = Vec::new();
unsafe { unsafe {
let native_screens = NSScreen::screens(nil); let native_screens = NSScreen::screens(nil);
for ix in 0..native_screens.count() { for ix in 0..NSArray::count(native_screens) {
screens.push(Screen { screens.push(Screen {
native_screen: native_screens.objectAtIndex(ix), native_screen: native_screens.objectAtIndex(ix),
}); });
@ -35,10 +58,52 @@ impl platform::Screen for Screen {
self self
} }
fn size(&self) -> Vector2F { fn display_uuid(&self) -> Option<uuid::Uuid> {
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 { unsafe {
let frame = self.native_screen.frame(); let frame = self.native_screen.frame();
vec2f(frame.size.width as f32, frame.size.height as f32) frame.to_rectf()
} }
} }
} }

View file

@ -7,7 +7,7 @@ use crate::{
self, self,
mac::{platform::NSViewLayerContentsRedrawDuringViewResize, renderer::Renderer}, mac::{platform::NSViewLayerContentsRedrawDuringViewResize, renderer::Renderer},
}, },
Event, FontSystem, Scene, Event, FontSystem, Scene, WindowBounds,
}; };
use cocoa::{ use cocoa::{
appkit::{NSScreen, NSSquareStatusItemLength, NSStatusBar, NSStatusItem, NSView, NSWindow}, appkit::{NSScreen, NSSquareStatusItemLength, NSStatusBar, NSStatusItem, NSView, NSWindow},
@ -32,6 +32,8 @@ use std::{
sync::Arc, sync::Arc,
}; };
use super::screen::Screen;
static mut VIEW_CLASS: *const Class = ptr::null(); static mut VIEW_CLASS: *const Class = ptr::null();
const STATE_IVAR: &str = "state"; const STATE_IVAR: &str = "state";
@ -167,28 +169,42 @@ impl StatusItem {
} }
impl platform::Window for StatusItem { impl platform::Window for StatusItem {
fn bounds(&self) -> WindowBounds {
self.0.borrow().bounds()
}
fn content_size(&self) -> Vector2F {
self.0.borrow().content_size()
}
fn scale_factor(&self) -> f32 {
self.0.borrow().scale_factor()
}
fn titlebar_height(&self) -> f32 {
0.
}
fn appearance(&self) -> crate::Appearance {
unsafe {
let appearance: id =
msg_send![self.0.borrow().native_item.button(), effectiveAppearance];
crate::Appearance::from_native(appearance)
}
}
fn screen(&self) -> Rc<dyn crate::Screen> {
unsafe {
Rc::new(Screen {
native_screen: self.0.borrow().native_window().screen(),
})
}
}
fn as_any_mut(&mut self) -> &mut dyn std::any::Any { fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
self self
} }
fn on_event(&mut self, callback: Box<dyn FnMut(crate::Event) -> bool>) {
self.0.borrow_mut().event_callback = Some(callback);
}
fn on_appearance_changed(&mut self, callback: Box<dyn FnMut()>) {
self.0.borrow_mut().appearance_changed_callback = Some(callback);
}
fn on_active_status_change(&mut self, _: Box<dyn FnMut(bool)>) {}
fn on_resize(&mut self, _: Box<dyn FnMut()>) {}
fn on_fullscreen(&mut self, _: Box<dyn FnMut(bool)>) {}
fn on_should_close(&mut self, _: Box<dyn FnMut() -> bool>) {}
fn on_close(&mut self, _: Box<dyn FnOnce()>) {}
fn set_input_handler(&mut self, _: Box<dyn crate::InputHandler>) {} fn set_input_handler(&mut self, _: Box<dyn crate::InputHandler>) {}
fn prompt( fn prompt(
@ -224,26 +240,6 @@ impl platform::Window for StatusItem {
unimplemented!() unimplemented!()
} }
fn toggle_full_screen(&self) {
unimplemented!()
}
fn bounds(&self) -> RectF {
self.0.borrow().bounds()
}
fn content_size(&self) -> Vector2F {
self.0.borrow().content_size()
}
fn scale_factor(&self) -> f32 {
self.0.borrow().scale_factor()
}
fn titlebar_height(&self) -> f32 {
0.
}
fn present_scene(&mut self, scene: Scene) { fn present_scene(&mut self, scene: Scene) {
self.0.borrow_mut().scene = Some(scene); self.0.borrow_mut().scene = Some(scene);
unsafe { unsafe {
@ -251,19 +247,39 @@ impl platform::Window for StatusItem {
} }
} }
fn appearance(&self) -> crate::Appearance { fn toggle_full_screen(&self) {
unsafe { unimplemented!()
let appearance: id = }
msg_send![self.0.borrow().native_item.button(), effectiveAppearance];
crate::Appearance::from_native(appearance) fn on_event(&mut self, callback: Box<dyn FnMut(crate::Event) -> bool>) {
} self.0.borrow_mut().event_callback = Some(callback);
}
fn on_active_status_change(&mut self, _: Box<dyn FnMut(bool)>) {}
fn on_resize(&mut self, _: Box<dyn FnMut()>) {}
fn on_fullscreen(&mut self, _: Box<dyn FnMut(bool)>) {}
fn on_moved(&mut self, _: Box<dyn FnMut()>) {}
fn on_should_close(&mut self, _: Box<dyn FnMut() -> bool>) {}
fn on_close(&mut self, _: Box<dyn FnOnce()>) {}
fn on_appearance_changed(&mut self, callback: Box<dyn FnMut()>) {
self.0.borrow_mut().appearance_changed_callback = Some(callback);
}
fn is_topmost_for_position(&self, _: Vector2F) -> bool {
true
} }
} }
impl StatusItemState { impl StatusItemState {
fn bounds(&self) -> RectF { fn bounds(&self) -> WindowBounds {
unsafe { unsafe {
let window: id = msg_send![self.native_item.button(), window]; let window: id = self.native_window();
let screen_frame = window.screen().visibleFrame(); let screen_frame = window.screen().visibleFrame();
let window_frame = NSWindow::frame(window); let window_frame = NSWindow::frame(window);
let origin = vec2f( let origin = vec2f(
@ -275,7 +291,7 @@ impl StatusItemState {
window_frame.size.width as f32, window_frame.size.width as f32,
window_frame.size.height 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 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) { extern "C" fn dealloc_view(this: &Object, _: Sel) {

View file

@ -19,12 +19,10 @@ use cocoa::{
appkit::{ appkit::{
CGPoint, NSApplication, NSBackingStoreBuffered, NSScreen, NSView, NSViewHeightSizable, CGPoint, NSApplication, NSBackingStoreBuffered, NSScreen, NSView, NSViewHeightSizable,
NSViewWidthSizable, NSWindow, NSWindowButton, NSWindowCollectionBehavior, NSViewWidthSizable, NSWindow, NSWindowButton, NSWindowCollectionBehavior,
NSWindowStyleMask, NSWindowStyleMask, NSWindowTitleVisibility,
}, },
base::{id, nil}, base::{id, nil},
foundation::{ foundation::{NSAutoreleasePool, NSInteger, NSPoint, NSRect, NSSize, NSString, NSUInteger},
NSAutoreleasePool, NSInteger, NSNotFound, NSPoint, NSRect, NSSize, NSString, NSUInteger,
},
}; };
use core_graphics::display::CGRect; use core_graphics::display::CGRect;
use ctor::ctor; use ctor::ctor;
@ -52,6 +50,11 @@ use std::{
time::Duration, time::Duration,
}; };
use super::{
geometry::{NSRectExt, Vector2FExt},
ns_string, NSRange,
};
const WINDOW_STATE_IVAR: &str = "windowState"; const WINDOW_STATE_IVAR: &str = "windowState";
static mut WINDOW_CLASS: *const Class = ptr::null(); static mut WINDOW_CLASS: *const Class = ptr::null();
@ -76,56 +79,6 @@ const NSTrackingInVisibleRect: NSUInteger = 0x200;
#[allow(non_upper_case_globals)] #[allow(non_upper_case_globals)]
const NSWindowAnimationBehaviorUtilityWindow: NSInteger = 4; 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<Range<usize>> {
if self.is_valid() {
let start = self.location as usize;
let end = start + self.length as usize;
Some(start..end)
} else {
None
}
}
}
impl From<Range<usize>> for NSRange {
fn from(range: Range<usize>) -> 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] #[ctor]
unsafe fn build_classes() { unsafe fn build_classes() {
WINDOW_CLASS = build_window_class("GPUIWindow", class!(NSWindow)); 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:), sel!(windowWillExitFullScreen:),
window_will_exit_fullscreen as extern "C" fn(&Object, Sel, id), 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( decl.add_method(
sel!(windowDidBecomeKey:), sel!(windowDidBecomeKey:),
window_did_change_key_status as extern "C" fn(&Object, Sel, id), 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() decl.register()
} }
pub struct Window(Rc<RefCell<WindowState>>);
///Used to track what the IME does when we send it a keystroke. ///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 ///This is only used to handle the case where the IME mysteriously
///swallows certain keys. ///swallows certain keys.
@ -325,6 +280,11 @@ enum ImeState {
None, None,
} }
struct InsertText {
replacement_range: Option<Range<usize>>,
text: String,
}
struct WindowState { struct WindowState {
id: usize, id: usize,
native_window: id, native_window: id,
@ -333,6 +293,7 @@ struct WindowState {
activate_callback: Option<Box<dyn FnMut(bool)>>, activate_callback: Option<Box<dyn FnMut(bool)>>,
resize_callback: Option<Box<dyn FnMut()>>, resize_callback: Option<Box<dyn FnMut()>>,
fullscreen_callback: Option<Box<dyn FnMut(bool)>>, fullscreen_callback: Option<Box<dyn FnMut(bool)>>,
moved_callback: Option<Box<dyn FnMut()>>,
should_close_callback: Option<Box<dyn FnMut() -> bool>>, should_close_callback: Option<Box<dyn FnMut() -> bool>>,
close_callback: Option<Box<dyn FnOnce()>>, close_callback: Option<Box<dyn FnOnce()>>,
appearance_changed_callback: Option<Box<dyn FnMut()>>, appearance_changed_callback: Option<Box<dyn FnMut()>>,
@ -352,11 +313,109 @@ struct WindowState {
ime_text: Option<String>, ime_text: Option<String>,
} }
struct InsertText { impl WindowState {
replacement_range: Option<Range<usize>>, fn move_traffic_light(&self) {
text: String, 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<RefCell<WindowState>>);
impl Window { impl Window {
pub fn open( pub fn open(
id: usize, id: usize,
@ -390,7 +449,7 @@ impl Window {
} }
}; };
let native_window = native_window.initWithContentRect_styleMask_backing_defer_screen_( 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, style_mask,
NSBackingStoreBuffered, NSBackingStoreBuffered,
NO, NO,
@ -405,30 +464,26 @@ impl Window {
let screen = native_window.screen(); let screen = native_window.screen();
match options.bounds { match options.bounds {
WindowBounds::Fullscreen => {
native_window.toggleFullScreen_(nil);
}
WindowBounds::Maximized => { WindowBounds::Maximized => {
native_window.setFrame_display_(screen.visibleFrame(), YES); native_window.setFrame_display_(screen.visibleFrame(), YES);
} }
WindowBounds::Fixed(top_left_bounds) => { WindowBounds::Fixed(rect) => {
let frame = screen.visibleFrame(); let screen_frame = screen.visibleFrame();
let bottom_left_bounds = RectF::new( let ns_rect = rect.to_ns_rect();
vec2f( if ns_rect.intersects(screen_frame) {
top_left_bounds.origin_x(), native_window.setFrame_display_(ns_rect, YES);
frame.size.height as f32 } else {
- top_left_bounds.origin_y() native_window.setFrame_display_(screen_frame, YES);
- top_left_bounds.height(), }
),
top_left_bounds.size(),
)
.to_ns_rect();
native_window.setFrame_display_(
native_window.convertRectToScreen_(bottom_left_bounds),
YES,
);
} }
} }
let native_view: id = msg_send![VIEW_CLASS, alloc]; let native_view: id = msg_send![VIEW_CLASS, alloc];
let native_view = NSView::init(native_view); let native_view = NSView::init(native_view);
assert!(!native_view.is_null()); assert!(!native_view.is_null());
let window = Self(Rc::new(RefCell::new(WindowState { let window = Self(Rc::new(RefCell::new(WindowState {
@ -441,6 +496,7 @@ impl Window {
close_callback: None, close_callback: None,
activate_callback: None, activate_callback: None,
fullscreen_callback: None, fullscreen_callback: None,
moved_callback: None,
appearance_changed_callback: None, appearance_changed_callback: None,
input_handler: None, input_handler: None,
pending_key_down: None, pending_key_down: None,
@ -480,6 +536,7 @@ impl Window {
.map_or(true, |titlebar| titlebar.appears_transparent) .map_or(true, |titlebar| titlebar.appears_transparent)
{ {
native_window.setTitlebarAppearsTransparent_(YES); native_window.setTitlebarAppearsTransparent_(YES);
native_window.setTitleVisibility_(NSWindowTitleVisibility::NSWindowTitleHidden);
} }
native_view.setAutoresizingMask_(NSViewWidthSizable | NSViewHeightSizable); native_view.setAutoresizingMask_(NSViewWidthSizable | NSViewHeightSizable);
@ -576,34 +633,41 @@ impl Drop for Window {
} }
impl platform::Window for Window { impl platform::Window for Window {
fn bounds(&self) -> WindowBounds {
self.0.as_ref().borrow().bounds()
}
fn content_size(&self) -> Vector2F {
self.0.as_ref().borrow().content_size()
}
fn scale_factor(&self) -> f32 {
self.0.as_ref().borrow().scale_factor()
}
fn titlebar_height(&self) -> f32 {
self.0.as_ref().borrow().titlebar_height()
}
fn appearance(&self) -> crate::Appearance {
unsafe {
let appearance: id = msg_send![self.0.borrow().native_window, effectiveAppearance];
crate::Appearance::from_native(appearance)
}
}
fn screen(&self) -> Rc<dyn crate::Screen> {
unsafe {
Rc::new(Screen {
native_screen: self.0.as_ref().borrow().native_window.screen(),
})
}
}
fn as_any_mut(&mut self) -> &mut dyn Any { fn as_any_mut(&mut self) -> &mut dyn Any {
self self
} }
fn on_event(&mut self, callback: Box<dyn FnMut(Event) -> bool>) {
self.0.as_ref().borrow_mut().event_callback = Some(callback);
}
fn on_resize(&mut self, callback: Box<dyn FnMut()>) {
self.0.as_ref().borrow_mut().resize_callback = Some(callback);
}
fn on_fullscreen(&mut self, callback: Box<dyn FnMut(bool)>) {
self.0.as_ref().borrow_mut().fullscreen_callback = Some(callback);
}
fn on_should_close(&mut self, callback: Box<dyn FnMut() -> bool>) {
self.0.as_ref().borrow_mut().should_close_callback = Some(callback);
}
fn on_close(&mut self, callback: Box<dyn FnOnce()>) {
self.0.as_ref().borrow_mut().close_callback = Some(callback);
}
fn on_active_status_change(&mut self, callback: Box<dyn FnMut(bool)>) {
self.0.as_ref().borrow_mut().activate_callback = Some(callback);
}
fn set_input_handler(&mut self, input_handler: Box<dyn InputHandler>) { fn set_input_handler(&mut self, input_handler: Box<dyn InputHandler>) {
self.0.as_ref().borrow_mut().input_handler = Some(input_handler); self.0.as_ref().borrow_mut().input_handler = Some(input_handler);
} }
@ -671,7 +735,8 @@ impl platform::Window for Window {
let app = NSApplication::sharedApplication(nil); let app = NSApplication::sharedApplication(nil);
let window = self.0.borrow().native_window; let window = self.0.borrow().native_window;
let title = ns_string(title); 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(); .detach();
} }
fn present_scene(&mut self, scene: Scene) {
self.0.as_ref().borrow_mut().present_scene(scene);
}
fn toggle_full_screen(&self) { fn toggle_full_screen(&self) {
let this = self.0.borrow(); let this = self.0.borrow();
let window = this.native_window; let window = this.native_window;
@ -725,124 +794,65 @@ impl platform::Window for Window {
.detach(); .detach();
} }
fn bounds(&self) -> RectF { fn on_event(&mut self, callback: Box<dyn FnMut(Event) -> bool>) {
self.0.as_ref().borrow().bounds() self.0.as_ref().borrow_mut().event_callback = Some(callback);
} }
fn content_size(&self) -> Vector2F { fn on_active_status_change(&mut self, callback: Box<dyn FnMut(bool)>) {
self.0.as_ref().borrow().content_size() self.0.as_ref().borrow_mut().activate_callback = Some(callback);
} }
fn scale_factor(&self) -> f32 { fn on_resize(&mut self, callback: Box<dyn FnMut()>) {
self.0.as_ref().borrow().scale_factor() self.0.as_ref().borrow_mut().resize_callback = Some(callback);
} }
fn present_scene(&mut self, scene: Scene) { fn on_fullscreen(&mut self, callback: Box<dyn FnMut(bool)>) {
self.0.as_ref().borrow_mut().present_scene(scene); self.0.as_ref().borrow_mut().fullscreen_callback = Some(callback);
} }
fn titlebar_height(&self) -> f32 { fn on_moved(&mut self, callback: Box<dyn FnMut()>) {
self.0.as_ref().borrow().titlebar_height() self.0.as_ref().borrow_mut().moved_callback = Some(callback);
} }
fn appearance(&self) -> crate::Appearance { fn on_should_close(&mut self, callback: Box<dyn FnMut() -> bool>) {
unsafe { self.0.as_ref().borrow_mut().should_close_callback = Some(callback);
let appearance: id = msg_send![self.0.borrow().native_window, effectiveAppearance]; }
crate::Appearance::from_native(appearance)
} fn on_close(&mut self, callback: Box<dyn FnOnce()>) {
self.0.as_ref().borrow_mut().close_callback = Some(callback);
} }
fn on_appearance_changed(&mut self, callback: Box<dyn FnMut()>) { fn on_appearance_changed(&mut self, callback: Box<dyn FnMut()>) {
self.0.borrow_mut().appearance_changed_callback = Some(callback); self.0.borrow_mut().appearance_changed_callback = Some(callback);
} }
}
impl WindowState { fn is_topmost_for_position(&self, position: Vector2F) -> bool {
fn move_traffic_light(&self) { let self_borrow = self.0.borrow();
if let Some(traffic_light_position) = self.traffic_light_position { let self_id = self_borrow.id;
let titlebar_height = self.titlebar_height();
unsafe { unsafe {
let close_button: id = msg_send![ let app = NSApplication::sharedApplication(nil);
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]; // Convert back to screen coordinates
let mut min_button_frame: CGRect = msg_send![min_button, frame]; let screen_point = position.to_screen_ns_point(
let mut zoom_button_frame: CGRect = msg_send![zoom_button, frame]; self_borrow.native_window,
let mut origin = vec2f( self_borrow.content_size().y() as f64,
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 window_number: NSInteger = msg_send![class!(NSWindow), windowNumberAtPoint:screen_point belowWindowWithWindowNumber:0];
let _: () = msg_send![close_button, setFrame: close_button_frame]; let top_most_window: id = msg_send![app, windowWithWindowNumber: window_number];
origin.set_x(origin.x() + button_spacing);
min_button_frame.origin = CGPoint::new(origin.x() as f64, origin.y() as f64); let is_panel: BOOL = msg_send![top_most_window, isKindOfClass: PANEL_CLASS];
let _: () = msg_send![min_button, setFrame: min_button_frame]; let is_window: BOOL = msg_send![top_most_window, isKindOfClass: WINDOW_CLASS];
origin.set_x(origin.x() + button_spacing); if is_panel == YES || is_window == YES {
let topmost_window_id = get_window_state(&*top_most_window).borrow().id;
zoom_button_frame.origin = CGPoint::new(origin.x() as f64, origin.y() as f64); topmost_window_id == self_id
let _: () = msg_send![zoom_button, setFrame: zoom_button_frame]; } else {
// Someone else's window is on top
false
} }
} }
} }
fn bounds(&self) -> RectF {
unsafe {
let screen_frame = self.native_window.screen().visibleFrame();
let window_frame = NSWindow::frame(self.native_window);
let origin = vec2f(
window_frame.origin.x as f32,
(window_frame.origin.y - screen_frame.size.height - window_frame.size.height)
as f32,
);
let size = vec2f(
window_frame.size.width as f32,
window_frame.size.height as f32,
);
RectF::new(origin, size)
}
}
fn content_size(&self) -> Vector2F {
let NSSize { width, height, .. } =
unsafe { NSView::frame(self.native_window.contentView()) }.size;
vec2f(width as f32, height as f32)
}
fn scale_factor(&self) -> f32 {
get_scale_factor(self.native_window)
}
fn titlebar_height(&self) -> f32 {
unsafe {
let frame = NSWindow::frame(self.native_window);
let content_layout_rect: CGRect = msg_send![self.native_window, contentLayoutRect];
(frame.size.height - content_layout_rect.size.height) as f32
}
}
fn present_scene(&mut self, scene: Scene) {
self.scene_to_render = Some(scene);
unsafe {
let _: () = msg_send![self.native_window.contentView(), setNeedsDisplay: YES];
}
}
} }
fn get_scale_factor(native_window: id) -> f32 { fn get_scale_factor(native_window: id) -> f32 {
@ -1106,6 +1116,16 @@ fn window_fullscreen_changed(this: &Object, is_fullscreen: bool) {
} }
} }
extern "C" fn window_did_move(this: &Object, _: Sel, _: id) {
let window_state = unsafe { get_window_state(this) };
let mut window_state_borrow = window_state.as_ref().borrow_mut();
if let Some(mut callback) = window_state_borrow.moved_callback.take() {
drop(window_state_borrow);
callback();
window_state.borrow_mut().moved_callback = Some(callback);
}
}
extern "C" fn window_did_change_key_status(this: &Object, selector: Sel, _: id) { extern "C" fn window_did_change_key_status(this: &Object, selector: Sel, _: id) {
let window_state = unsafe { get_window_state(this) }; let window_state = unsafe { get_window_state(this) };
let window_state_borrow = window_state.borrow(); 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<F, R>(window: &Object, f: F) -> Option<R> fn with_input_handler<F, R>(window: &Object, f: F) -> Option<R>
where where
F: FnOnce(&mut dyn InputHandler) -> R, F: FnOnce(&mut dyn InputHandler) -> R,

View file

@ -5,7 +5,7 @@ use crate::{
vector::{vec2f, Vector2F}, vector::{vec2f, Vector2F},
}, },
keymap_matcher::KeymapMatcher, keymap_matcher::KeymapMatcher,
Action, ClipboardItem, Action, ClipboardItem, Menu,
}; };
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use collections::VecDeque; use collections::VecDeque;
@ -20,11 +20,20 @@ use std::{
}; };
use time::UtcOffset; use time::UtcOffset;
pub struct Platform { struct Dispatcher;
dispatcher: Arc<dyn super::Dispatcher>,
fonts: Arc<dyn super::FontSystem>, impl super::Dispatcher for Dispatcher {
current_clipboard_item: Mutex<Option<ClipboardItem>>, fn is_main_thread(&self) -> bool {
cursor: Mutex<CursorStyle>, true
}
fn run_on_main_thread(&self, task: async_task::Runnable) {
task.run();
}
}
pub fn foreground_platform() -> ForegroundPlatform {
ForegroundPlatform::default()
} }
#[derive(Default)] #[derive(Default)]
@ -32,23 +41,6 @@ pub struct ForegroundPlatform {
last_prompt_for_new_path_args: RefCell<Option<(PathBuf, oneshot::Sender<Option<PathBuf>>)>>, last_prompt_for_new_path_args: RefCell<Option<(PathBuf, oneshot::Sender<Option<PathBuf>>)>>,
} }
struct Dispatcher;
pub struct Window {
pub(crate) size: Vector2F,
scale_factor: f32,
current_scene: Option<crate::Scene>,
event_handlers: Vec<Box<dyn FnMut(super::Event) -> bool>>,
pub(crate) resize_handlers: Vec<Box<dyn FnMut()>>,
close_handlers: Vec<Box<dyn FnOnce()>>,
fullscreen_handlers: Vec<Box<dyn FnMut(bool)>>,
pub(crate) active_status_change_handlers: Vec<Box<dyn FnMut(bool)>>,
pub(crate) should_close_handler: Option<Box<dyn FnMut() -> bool>>,
pub(crate) title: Option<String>,
pub(crate) edited: bool,
pub(crate) pending_prompts: RefCell<VecDeque<oneshot::Sender<usize>>>,
}
#[cfg(any(test, feature = "test-support"))] #[cfg(any(test, feature = "test-support"))]
impl ForegroundPlatform { impl ForegroundPlatform {
pub(crate) fn simulate_new_path_selection( pub(crate) fn simulate_new_path_selection(
@ -85,7 +77,7 @@ impl super::ForegroundPlatform for ForegroundPlatform {
fn on_menu_command(&self, _: Box<dyn FnMut(&dyn Action)>) {} fn on_menu_command(&self, _: Box<dyn FnMut(&dyn Action)>) {}
fn on_validate_menu_command(&self, _: Box<dyn FnMut(&dyn Action) -> bool>) {} fn on_validate_menu_command(&self, _: Box<dyn FnMut(&dyn Action) -> bool>) {}
fn on_will_open_menu(&self, _: Box<dyn FnMut()>) {} fn on_will_open_menu(&self, _: Box<dyn FnMut()>) {}
fn set_menus(&self, _: Vec<crate::Menu>, _: &KeymapMatcher) {} fn set_menus(&self, _: Vec<Menu>, _: &KeymapMatcher) {}
fn prompt_for_paths( fn prompt_for_paths(
&self, &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)); *self.last_prompt_for_new_path_args.borrow_mut() = Some((path.to_path_buf(), done_tx));
done_rx done_rx
} }
fn reveal_path(&self, _: &Path) {}
}
pub fn platform() -> Platform {
Platform::new()
}
pub struct Platform {
dispatcher: Arc<dyn super::Dispatcher>,
fonts: Arc<dyn super::FontSystem>,
current_clipboard_item: Mutex<Option<ClipboardItem>>,
cursor: Mutex<CursorStyle>,
} }
impl Platform { impl Platform {
@ -132,6 +137,10 @@ impl super::Platform for Platform {
fn quit(&self) {} fn quit(&self) {}
fn screen_by_id(&self, _id: uuid::Uuid) -> Option<Rc<dyn crate::Screen>> {
None
}
fn screens(&self) -> Vec<Rc<dyn crate::Screen>> { fn screens(&self) -> Vec<Rc<dyn crate::Screen>> {
Default::default() Default::default()
} }
@ -143,7 +152,7 @@ impl super::Platform for Platform {
_executor: Rc<super::executor::Foreground>, _executor: Rc<super::executor::Foreground>,
) -> Box<dyn super::Window> { ) -> Box<dyn super::Window> {
Box::new(Window::new(match options.bounds { Box::new(Window::new(match options.bounds {
WindowBounds::Maximized => vec2f(1024., 768.), WindowBounds::Maximized | WindowBounds::Fullscreen => vec2f(1024., 768.),
WindowBounds::Fixed(rect) => rect.size(), WindowBounds::Fixed(rect) => rect.size(),
})) }))
} }
@ -217,6 +226,41 @@ impl super::Platform for Platform {
patch: 0, 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<uuid::Uuid> {
Some(uuid::Uuid::new_v4())
}
}
pub struct Window {
pub(crate) size: Vector2F,
scale_factor: f32,
current_scene: Option<crate::Scene>,
event_handlers: Vec<Box<dyn FnMut(super::Event) -> bool>>,
pub(crate) resize_handlers: Vec<Box<dyn FnMut()>>,
pub(crate) moved_handlers: Vec<Box<dyn FnMut()>>,
close_handlers: Vec<Box<dyn FnOnce()>>,
fullscreen_handlers: Vec<Box<dyn FnMut(bool)>>,
pub(crate) active_status_change_handlers: Vec<Box<dyn FnMut(bool)>>,
pub(crate) should_close_handler: Option<Box<dyn FnMut() -> bool>>,
pub(crate) title: Option<String>,
pub(crate) edited: bool,
pub(crate) pending_prompts: RefCell<VecDeque<oneshot::Sender<usize>>>,
} }
impl Window { impl Window {
@ -225,6 +269,7 @@ impl Window {
size, size,
event_handlers: Default::default(), event_handlers: Default::default(),
resize_handlers: Default::default(), resize_handlers: Default::default(),
moved_handlers: Default::default(),
close_handlers: Default::default(), close_handlers: Default::default(),
should_close_handler: Default::default(), should_close_handler: Default::default(),
active_status_change_handlers: Default::default(), active_status_change_handlers: Default::default(),
@ -242,41 +287,35 @@ impl Window {
} }
} }
impl super::Dispatcher for Dispatcher {
fn is_main_thread(&self) -> bool {
true
}
fn run_on_main_thread(&self, task: async_task::Runnable) {
task.run();
}
}
impl super::Window for Window { impl super::Window for Window {
fn bounds(&self) -> WindowBounds {
WindowBounds::Fixed(RectF::new(Vector2F::zero(), self.size))
}
fn content_size(&self) -> Vector2F {
self.size
}
fn scale_factor(&self) -> f32 {
self.scale_factor
}
fn titlebar_height(&self) -> f32 {
24.
}
fn appearance(&self) -> crate::Appearance {
crate::Appearance::Light
}
fn screen(&self) -> Rc<dyn crate::Screen> {
Rc::new(Screen)
}
fn as_any_mut(&mut self) -> &mut dyn Any { fn as_any_mut(&mut self) -> &mut dyn Any {
self self
} }
fn on_event(&mut self, callback: Box<dyn FnMut(crate::Event) -> bool>) {
self.event_handlers.push(callback);
}
fn on_active_status_change(&mut self, callback: Box<dyn FnMut(bool)>) {
self.active_status_change_handlers.push(callback);
}
fn on_fullscreen(&mut self, callback: Box<dyn FnMut(bool)>) {
self.fullscreen_handlers.push(callback)
}
fn on_resize(&mut self, callback: Box<dyn FnMut()>) {
self.resize_handlers.push(callback);
}
fn on_close(&mut self, callback: Box<dyn FnOnce()>) {
self.close_handlers.push(callback);
}
fn set_input_handler(&mut self, _: Box<dyn crate::InputHandler>) {} fn set_input_handler(&mut self, _: Box<dyn crate::InputHandler>) {}
fn prompt(&self, _: crate::PromptLevel, _: &str, _: &[&str]) -> oneshot::Receiver<usize> { fn prompt(&self, _: crate::PromptLevel, _: &str, _: &[&str]) -> oneshot::Receiver<usize> {
@ -295,49 +334,49 @@ impl super::Window for Window {
self.edited = edited; self.edited = edited;
} }
fn on_should_close(&mut self, callback: Box<dyn FnMut() -> bool>) {
self.should_close_handler = Some(callback);
}
fn show_character_palette(&self) {} fn show_character_palette(&self) {}
fn minimize(&self) {} fn minimize(&self) {}
fn zoom(&self) {} fn zoom(&self) {}
fn toggle_full_screen(&self) {}
fn bounds(&self) -> RectF {
RectF::new(Default::default(), self.size)
}
fn content_size(&self) -> Vector2F {
self.size
}
fn scale_factor(&self) -> f32 {
self.scale_factor
}
fn titlebar_height(&self) -> f32 {
24.
}
fn present_scene(&mut self, scene: crate::Scene) { fn present_scene(&mut self, scene: crate::Scene) {
self.current_scene = Some(scene); self.current_scene = Some(scene);
} }
fn appearance(&self) -> crate::Appearance { fn toggle_full_screen(&self) {}
crate::Appearance::Light
fn on_event(&mut self, callback: Box<dyn FnMut(crate::Event) -> bool>) {
self.event_handlers.push(callback);
}
fn on_active_status_change(&mut self, callback: Box<dyn FnMut(bool)>) {
self.active_status_change_handlers.push(callback);
}
fn on_resize(&mut self, callback: Box<dyn FnMut()>) {
self.resize_handlers.push(callback);
}
fn on_fullscreen(&mut self, callback: Box<dyn FnMut(bool)>) {
self.fullscreen_handlers.push(callback)
}
fn on_moved(&mut self, callback: Box<dyn FnMut()>) {
self.moved_handlers.push(callback);
}
fn on_should_close(&mut self, callback: Box<dyn FnMut() -> bool>) {
self.should_close_handler = Some(callback);
}
fn on_close(&mut self, callback: Box<dyn FnOnce()>) {
self.close_handlers.push(callback);
} }
fn on_appearance_changed(&mut self, _: Box<dyn FnMut()>) {} fn on_appearance_changed(&mut self, _: Box<dyn FnMut()>) {}
}
pub fn platform() -> Platform { fn is_topmost_for_position(&self, _position: Vector2F) -> bool {
Platform::new() true
} }
pub fn foreground_platform() -> ForegroundPlatform {
ForegroundPlatform::default()
} }

View file

@ -4,7 +4,6 @@ use crate::{
font_cache::FontCache, font_cache::FontCache,
geometry::rect::RectF, geometry::rect::RectF,
json::{self, ToJson}, json::{self, ToJson},
keymap_matcher::Keystroke,
platform::{CursorStyle, Event}, platform::{CursorStyle, Event},
scene::{ scene::{
CursorRegion, MouseClick, MouseDown, MouseDownOut, MouseDrag, MouseEvent, MouseHover, CursorRegion, MouseClick, MouseDown, MouseDownOut, MouseDrag, MouseEvent, MouseHover,
@ -23,7 +22,7 @@ use pathfinder_geometry::vector::{vec2f, Vector2F};
use serde_json::json; use serde_json::json;
use smallvec::SmallVec; use smallvec::SmallVec;
use sqlez::{ use sqlez::{
bindable::{Bind, Column}, bindable::{Bind, Column, StaticColumnCount},
statement::Statement, statement::Statement,
}; };
use std::{ use std::{
@ -316,7 +315,10 @@ impl Presenter {
break; 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 !event_reused {
if pressed_button.is_some() { if pressed_button.is_some() {
@ -601,14 +603,6 @@ pub struct LayoutContext<'a> {
} }
impl<'a> LayoutContext<'a> { impl<'a> LayoutContext<'a> {
pub(crate) fn keystrokes_for_action(
&mut self,
action: &dyn Action,
) -> Option<SmallVec<[Keystroke; 2]>> {
self.app
.keystrokes_for_action(self.window_id, &self.view_stack, action)
}
fn layout(&mut self, view_id: usize, constraint: SizeConstraint) -> Vector2F { fn layout(&mut self, view_id: usize, constraint: SizeConstraint) -> Vector2F {
let print_error = |view_id| { let print_error = |view_id| {
format!( format!(
@ -929,6 +923,7 @@ impl ToJson for Axis {
} }
} }
impl StaticColumnCount for Axis {}
impl Bind for Axis { impl Bind for Axis {
fn bind(&self, statement: &Statement, start_index: i32) -> anyhow::Result<i32> { fn bind(&self, statement: &Statement, start_index: i32) -> anyhow::Result<i32> {
match self { match self {

View file

@ -209,6 +209,7 @@ impl EventDispatcher {
break; break;
} }
} }
cx.platform().set_cursor_style(style_to_assign); cx.platform().set_cursor_style(style_to_assign);
if !event_reused { if !event_reused {

View file

@ -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::{ use std::{
fmt::Write, fmt::Write,
panic::{self, RefUnwindSafe}, 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)] #[cfg(test)]
#[ctor::ctor] #[ctor::ctor]
fn init_logger() { fn init_logger() {

View file

@ -54,6 +54,7 @@ smol = "1.2"
tree-sitter = "0.20" tree-sitter = "0.20"
tree-sitter-rust = { version = "*", optional = true } tree-sitter-rust = { version = "*", optional = true }
tree-sitter-typescript = { version = "*", optional = true } tree-sitter-typescript = { version = "*", optional = true }
unicase = "2.6"
[dev-dependencies] [dev-dependencies]
client = { path = "../client", features = ["test-support"] } client = { path = "../client", features = ["test-support"] }
@ -65,13 +66,15 @@ settings = { path = "../settings", features = ["test-support"] }
util = { path = "../util", features = ["test-support"] } util = { path = "../util", features = ["test-support"] }
ctor = "0.1" ctor = "0.1"
env_logger = "0.9" env_logger = "0.9"
indoc = "1.0.4"
rand = "0.8.3" rand = "0.8.3"
tree-sitter-embedded-template = "*"
tree-sitter-html = "*" tree-sitter-html = "*"
tree-sitter-javascript = "*" tree-sitter-javascript = "*"
tree-sitter-json = "*" tree-sitter-json = "*"
tree-sitter-markdown = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "330ecab87a3e3a7211ac69bbadc19eabecdb1cca" }
tree-sitter-rust = "*" tree-sitter-rust = "*"
tree-sitter-python = "*" tree-sitter-python = "*"
tree-sitter-typescript = "*" tree-sitter-typescript = "*"
tree-sitter-ruby = "*" tree-sitter-ruby = "*"
tree-sitter-embedded-template = "*"
unindent = "0.1.7" unindent = "0.1.7"

View file

@ -41,7 +41,7 @@ pub use text::{Buffer as TextBuffer, BufferSnapshot as TextBufferSnapshot, Opera
use theme::SyntaxTheme; use theme::SyntaxTheme;
#[cfg(any(test, feature = "test-support"))] #[cfg(any(test, feature = "test-support"))]
use util::RandomCharIter; use util::RandomCharIter;
use util::TryFutureExt as _; use util::{RangeExt, TryFutureExt as _};
#[cfg(any(test, feature = "test-support"))] #[cfg(any(test, feature = "test-support"))]
pub use {tree_sitter_rust, tree_sitter_typescript}; pub use {tree_sitter_rust, tree_sitter_typescript};
@ -214,15 +214,6 @@ pub trait File: Send + Sync {
fn is_deleted(&self) -> bool; fn is_deleted(&self) -> bool;
fn save(
&self,
buffer_id: u64,
text: Rope,
version: clock::Global,
line_ending: LineEnding,
cx: &mut MutableAppContext,
) -> Task<Result<(clock::Global, RopeFingerprint, SystemTime)>>;
fn as_any(&self) -> &dyn Any; fn as_any(&self) -> &dyn Any;
fn to_proto(&self) -> rpc::proto::File; fn to_proto(&self) -> rpc::proto::File;
@ -529,33 +520,6 @@ impl Buffer {
self.file.as_ref() self.file.as_ref()
} }
pub fn save(
&mut self,
cx: &mut ModelContext<Self>,
) -> Task<Result<(clock::Global, RopeFingerprint, SystemTime)>> {
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 { pub fn saved_version(&self) -> &clock::Global {
&self.saved_version &self.saved_version
} }
@ -585,16 +549,11 @@ impl Buffer {
version: clock::Global, version: clock::Global,
fingerprint: RopeFingerprint, fingerprint: RopeFingerprint,
mtime: SystemTime, mtime: SystemTime,
new_file: Option<Arc<dyn File>>,
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
) { ) {
self.saved_version = version; self.saved_version = version;
self.saved_version_fingerprint = fingerprint; self.saved_version_fingerprint = fingerprint;
self.saved_mtime = mtime; 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.emit(Event::Saved);
cx.notify(); cx.notify();
} }
@ -661,36 +620,35 @@ impl Buffer {
new_file: Arc<dyn File>, new_file: Arc<dyn File>,
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
) -> Task<()> { ) -> Task<()> {
let old_file = if let Some(file) = self.file.as_ref() {
file
} else {
return Task::ready(());
};
let mut file_changed = false; let mut file_changed = false;
let mut task = Task::ready(()); let mut task = Task::ready(());
if new_file.path() != old_file.path() { if let Some(old_file) = self.file.as_ref() {
file_changed = true; if new_file.path() != old_file.path() {
}
if new_file.is_deleted() {
if !old_file.is_deleted() {
file_changed = true; file_changed = true;
if !self.is_dirty() { }
cx.emit(Event::DirtyChanged);
if new_file.is_deleted() {
if !old_file.is_deleted() {
file_changed = true;
if !self.is_dirty() {
cx.emit(Event::DirtyChanged);
}
}
} else {
let new_mtime = new_file.mtime();
if new_mtime != old_file.mtime() {
file_changed = true;
if !self.is_dirty() {
let reload = self.reload(cx).log_err().map(drop);
task = cx.foreground().spawn(reload);
}
} }
} }
} else { } else {
let new_mtime = new_file.mtime(); file_changed = true;
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 file_changed { if file_changed {
self.file_update_count += 1; self.file_update_count += 1;
@ -797,6 +755,10 @@ impl Buffer {
self.parsing_in_background self.parsing_in_background
} }
pub fn contains_unknown_injections(&self) -> bool {
self.syntax_map.lock().contains_unknown_injections()
}
#[cfg(test)] #[cfg(test)]
pub fn set_sync_parse_timeout(&mut self, timeout: Duration) { pub fn set_sync_parse_timeout(&mut self, timeout: Duration) {
self.sync_parse_timeout = timeout; self.sync_parse_timeout = timeout;
@ -825,7 +787,7 @@ impl Buffer {
/// initiate an additional reparse recursively. To avoid concurrent parses /// initiate an additional reparse recursively. To avoid concurrent parses
/// for the same buffer, we only initiate a new parse if we are not already /// for the same buffer, we only initiate a new parse if we are not already
/// parsing in the background. /// parsing in the background.
fn reparse(&mut self, cx: &mut ModelContext<Self>) { pub fn reparse(&mut self, cx: &mut ModelContext<Self>) {
if self.parsing_in_background { if self.parsing_in_background {
return; return;
} }
@ -842,13 +804,13 @@ impl Buffer {
syntax_map.interpolate(&text); syntax_map.interpolate(&text);
let language_registry = syntax_map.language_registry(); let language_registry = syntax_map.language_registry();
let mut syntax_snapshot = syntax_map.snapshot(); let mut syntax_snapshot = syntax_map.snapshot();
let syntax_map_version = syntax_map.parsed_version();
drop(syntax_map); drop(syntax_map);
let parse_task = cx.background().spawn({ let parse_task = cx.background().spawn({
let language = language.clone(); let language = language.clone();
let language_registry = language_registry.clone();
async move { async move {
syntax_snapshot.reparse(&syntax_map_version, &text, language_registry, language); syntax_snapshot.reparse(&text, language_registry, language);
syntax_snapshot syntax_snapshot
} }
}); });
@ -858,7 +820,7 @@ impl Buffer {
.block_with_timeout(self.sync_parse_timeout, parse_task) .block_with_timeout(self.sync_parse_timeout, parse_task)
{ {
Ok(new_syntax_snapshot) => { Ok(new_syntax_snapshot) => {
self.did_finish_parsing(new_syntax_snapshot, parsed_version, cx); self.did_finish_parsing(new_syntax_snapshot, cx);
return; return;
} }
Err(parse_task) => { Err(parse_task) => {
@ -870,9 +832,15 @@ impl Buffer {
this.language.as_ref().map_or(true, |current_language| { this.language.as_ref().map_or(true, |current_language| {
!Arc::ptr_eq(&language, current_language) !Arc::ptr_eq(&language, current_language)
}); });
let parse_again = let language_registry_changed = new_syntax_map
this.version.changed_since(&parsed_version) || grammar_changed; .contains_unknown_injections()
this.did_finish_parsing(new_syntax_map, parsed_version, cx); && 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; this.parsing_in_background = false;
if parse_again { if parse_again {
this.reparse(cx); this.reparse(cx);
@ -884,14 +852,9 @@ impl Buffer {
} }
} }
fn did_finish_parsing( fn did_finish_parsing(&mut self, syntax_snapshot: SyntaxSnapshot, cx: &mut ModelContext<Self>) {
&mut self,
syntax_snapshot: SyntaxSnapshot,
version: clock::Global,
cx: &mut ModelContext<Self>,
) {
self.parse_count += 1; 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); self.request_autoindent(cx);
cx.emit(Event::Reparsed); cx.emit(Event::Reparsed);
cx.notify(); cx.notify();
@ -1384,12 +1347,12 @@ impl Buffer {
.enumerate() .enumerate()
.zip(&edit_operation.as_edit().unwrap().new_text) .zip(&edit_operation.as_edit().unwrap().new_text)
.map(|((ix, (range, _)), 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 old_start = range.start.to_point(&before_edit);
let new_start = (delta + range.start as isize) as usize; 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 first_line_is_new = false;
let mut original_indent_column = None; let mut original_indent_column = None;
@ -2242,7 +2205,6 @@ impl BufferSnapshot {
.map(|g| g.outline_config.as_ref().unwrap()) .map(|g| g.outline_config.as_ref().unwrap())
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let mut chunks = self.chunks(0..self.len(), true);
let mut stack = Vec::<Range<usize>>::new(); let mut stack = Vec::<Range<usize>>::new();
let mut items = Vec::new(); let mut items = Vec::new();
while let Some(mat) = matches.peek() { while let Some(mat) = matches.peek() {
@ -2261,9 +2223,7 @@ impl BufferSnapshot {
continue; continue;
} }
let mut text = String::new(); let mut buffer_ranges = Vec::new();
let mut name_ranges = Vec::new();
let mut highlight_ranges = Vec::new();
for capture in mat.captures { for capture in mat.captures {
let node_is_name; let node_is_name;
if capture.index == config.name_capture_ix { 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; 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() { if !text.is_empty() {
text.push(' '); text.push(' ');
} }
if node_is_name { if is_name {
let mut start = text.len(); 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 // When multiple names are captured, then the matcheable text
// includes the whitespace in between the names. // includes the whitespace in between the names.
@ -2297,12 +2272,12 @@ impl BufferSnapshot {
name_ranges.push(start..end); name_ranges.push(start..end);
} }
let mut offset = range.start; let mut offset = buffer_range.start;
chunks.seek(offset); chunks.seek(offset);
for mut chunk in chunks.by_ref() { for mut chunk in chunks.by_ref() {
if chunk.text.len() > range.end - offset { if chunk.text.len() > buffer_range.end - offset {
chunk.text = &chunk.text[0..(range.end - offset)]; chunk.text = &chunk.text[0..(buffer_range.end - offset)];
offset = range.end; offset = buffer_range.end;
} else { } else {
offset += chunk.text.len(); offset += chunk.text.len();
} }
@ -2316,7 +2291,7 @@ impl BufferSnapshot {
highlight_ranges.push((start..end, style)); highlight_ranges.push((start..end, style));
} }
text.push_str(chunk.text); text.push_str(chunk.text);
if offset >= range.end { if offset >= buffer_range.end {
break; break;
} }
} }
@ -2341,56 +2316,50 @@ impl BufferSnapshot {
Some(items) Some(items)
} }
pub fn enclosing_bracket_ranges<T: ToOffset>( /// Returns bracket range pairs overlapping or adjacent to `range`
&self, pub fn bracket_ranges<'a, T: ToOffset>(
&'a self,
range: Range<T>, range: Range<T>,
) -> Option<(Range<usize>, Range<usize>)> { ) -> impl Iterator<Item = (Range<usize>, Range<usize>)> + 'a {
// Find bracket pairs that *inclusively* contain the given range. // Find bracket pairs that *inclusively* contain the given range.
let range = range.start.to_offset(self)..range.end.to_offset(self); let range = range.start.to_offset(self).saturating_sub(1)
let mut matches = self.syntax.matches( ..self.len().min(range.end.to_offset(self) + 1);
range.start.saturating_sub(1)..self.len().min(range.end + 1),
&self.text, let mut matches = self.syntax.matches(range.clone(), &self.text, |grammar| {
|grammar| grammar.brackets_config.as_ref().map(|c| &c.query), grammar.brackets_config.as_ref().map(|c| &c.query)
); });
let configs = matches let configs = matches
.grammars() .grammars()
.iter() .iter()
.map(|grammar| grammar.brackets_config.as_ref().unwrap()) .map(|grammar| grammar.brackets_config.as_ref().unwrap())
.collect::<Vec<_>>(); .collect::<Vec<_>>();
// Get the ranges of the innermost pair of brackets. iter::from_fn(move || {
let mut result: Option<(Range<usize>, Range<usize>)> = None; while let Some(mat) = matches.peek() {
while let Some(mat) = matches.peek() { let mut open = None;
let mut open = None; let mut close = None;
let mut close = None; let config = &configs[mat.grammar_index];
let config = &configs[mat.grammar_index]; for capture in mat.captures {
for capture in mat.captures { if capture.index == config.open_capture_ix {
if capture.index == config.open_capture_ix { open = Some(capture.node.byte_range());
open = Some(capture.node.byte_range()); } else if capture.index == config.close_capture_ix {
} else if capture.index == config.close_capture_ix { close = Some(capture.node.byte_range());
close = Some(capture.node.byte_range()); }
} }
}
matches.advance(); matches.advance();
let Some((open, close)) = open.zip(close) else { continue }; 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;
if let Some((existing_open, existing_close)) = &result { let bracket_range = open.start..=close.end;
let existing_len = existing_close.end - existing_open.start; if !bracket_range.overlaps(&range) {
if len > existing_len {
continue; continue;
} }
return Some((open, close));
} }
None
result = Some((open, close)); })
}
result
} }
#[allow(clippy::type_complexity)] #[allow(clippy::type_complexity)]

View file

@ -3,6 +3,7 @@ use clock::ReplicaId;
use collections::BTreeMap; use collections::BTreeMap;
use fs::LineEnding; use fs::LineEnding;
use gpui::{ModelHandle, MutableAppContext}; use gpui::{ModelHandle, MutableAppContext};
use indoc::indoc;
use proto::deserialize_operation; use proto::deserialize_operation;
use rand::prelude::*; use rand::prelude::*;
use settings::Settings; use settings::Settings;
@ -15,7 +16,7 @@ use std::{
}; };
use text::network::Network; use text::network::Network;
use unindent::Unindent as _; 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)] #[cfg(test)]
#[ctor::ctor] #[ctor::ctor]
@ -51,7 +52,7 @@ fn test_line_endings(cx: &mut gpui::MutableAppContext) {
#[gpui::test] #[gpui::test]
fn test_select_language() { fn test_select_language() {
let registry = LanguageRegistry::test(); let registry = Arc::new(LanguageRegistry::test());
registry.add(Arc::new(Language::new( registry.add(Arc::new(Language::new(
LanguageConfig { LanguageConfig {
name: "Rust".into(), name: "Rust".into(),
@ -71,27 +72,33 @@ fn test_select_language() {
// matching file extension // matching file extension
assert_eq!( 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()) Some("Rust".into())
); );
assert_eq!( 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()) Some("Make".into())
); );
// matching filename // matching filename
assert_eq!( assert_eq!(
registry.select_language("zed/Makefile").map(|l| l.name()), registry.language_for_path("zed/Makefile").map(|l| l.name()),
Some("Make".into()) Some("Make".into())
); );
// matching suffix that is not the full file extension or filename // 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!( 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 None
); );
assert_eq!(registry.select_language("zed/sumk").map(|l| l.name()), None);
} }
#[gpui::test] #[gpui::test]
@ -570,53 +577,117 @@ async fn test_symbols_containing(cx: &mut gpui::TestAppContext) {
#[gpui::test] #[gpui::test]
fn test_enclosing_bracket_ranges(cx: &mut MutableAppContext) { fn test_enclosing_bracket_ranges(cx: &mut MutableAppContext) {
cx.set_global(Settings::test(cx)); let mut assert = |selection_text, range_markers| {
let buffer = cx.add_model(|cx| { assert_bracket_pairs(selection_text, range_markers, rust_lang(), cx)
let text = " };
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 ˇ{
}
}
let foo = 1;"},
vec![
indoc! {"
mod x «{»
mod y {
}
«}»
let foo = 1;"},
indoc! {"
mod x {
mod y «{»
«}»
}
let foo = 1;"},
],
);
assert(
indoc! {"
mod x {
mod y {
}ˇ
}
let foo = 1;"},
vec![
indoc! {"
mod x «{»
mod y {
}
«}»
let foo = 1;"},
indoc! {"
mod x {
mod y «{»
«}»
}
let foo = 1;"},
],
);
assert(
indoc! {"
mod x {
mod y {
}
ˇ}
let foo = 1;"},
vec![indoc! {"
mod x «{»
mod y {
}
«}»
let foo = 1;"}],
);
assert(
indoc! {"
mod x { mod x {
mod y { mod y {
} }
} }
" let fˇoo = 1;"},
.unindent(); vec![],
Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx)
});
let buffer = buffer.read(cx);
assert_eq!(
buffer.enclosing_bracket_point_ranges(Point::new(1, 6)..Point::new(1, 6)),
Some((
Point::new(0, 6)..Point::new(0, 7),
Point::new(4, 0)..Point::new(4, 1)
))
);
assert_eq!(
buffer.enclosing_bracket_point_ranges(Point::new(1, 10)..Point::new(1, 10)),
Some((
Point::new(1, 10)..Point::new(1, 11),
Point::new(3, 4)..Point::new(3, 5)
))
);
assert_eq!(
buffer.enclosing_bracket_point_ranges(Point::new(3, 5)..Point::new(3, 5)),
Some((
Point::new(1, 10)..Point::new(1, 11),
Point::new(3, 4)..Point::new(3, 5)
))
);
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)
))
); );
// Regression test: avoid crash when querying at the end of the buffer. // Regression test: avoid crash when querying at the end of the buffer.
assert_eq!( assert(
buffer.enclosing_bracket_point_ranges(Point::new(4, 1)..Point::new(5, 0)), indoc! {"
None 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( fn test_enclosing_bracket_ranges_where_brackets_are_not_outermost_children(
cx: &mut MutableAppContext, cx: &mut MutableAppContext,
) { ) {
let javascript_language = Arc::new( let mut assert = |selection_text, bracket_pair_texts| {
Language::new( assert_bracket_pairs(selection_text, bracket_pair_texts, javascript_lang(), cx)
LanguageConfig { };
name: "JavaScript".into(),
..Default::default() assert(
}, indoc! {"
Some(tree_sitter_javascript::language()), for (const a in b)ˇ {
) // a comment that's longer than the for-loop header
.with_brackets_query( }"},
r#" vec![indoc! {"
("{" @open "}" @close) for «(»const a in b«)» {
("(" @open ")" @close) // a comment that's longer than the for-loop header
"#, }"}],
)
.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)
))
); );
eprintln!("-----------------------");
// Regression test: even though the parent node of the parentheses (the for loop) does // 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 // 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. // they should not be returned. Only the curly braces contain the range.
assert_eq!( assert(
buffer.enclosing_bracket_point_ranges(Point::new(0, 20)..Point::new(0, 20)), indoc! {"
Some(( for (const a in b) {ˇ
Point::new(0, 19)..Point::new(0, 20), // a comment that's longer than the for-loop header
Point::new(2, 0)..Point::new(2, 1) }"},
)) 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<T: ToOffset>(
&self,
range: Range<T>,
) -> Option<(Range<Point>, Range<Point>)> {
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 { fn ruby_lang() -> Language {
Language::new( Language::new(
LanguageConfig { 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<Buffer>, cx: &gpui::TestAppContext) -> String { fn get_tree_sexp(buffer: &ModelHandle<Buffer>, cx: &gpui::TestAppContext) -> String {
buffer.read_with(cx, |buffer, _| { buffer.read_with(cx, |buffer, _| {
let snapshot = buffer.snapshot(); let snapshot = buffer.snapshot();
@ -1991,3 +2046,34 @@ fn get_tree_sexp(buffer: &ModelHandle<Buffer>, cx: &gpui::TestAppContext) -> Str
layers[0].node.to_sexp() 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::<Vec<_>>();
assert_set_eq!(
buffer.bracket_ranges(selection_range).collect::<Vec<_>>(),
bracket_pairs
);
}

View file

@ -16,7 +16,7 @@ use futures::{
future::{BoxFuture, Shared}, future::{BoxFuture, Shared},
FutureExt, TryFutureExt, FutureExt, TryFutureExt,
}; };
use gpui::{MutableAppContext, Task}; use gpui::{executor::Background, MutableAppContext, Task};
use highlight_map::HighlightMap; use highlight_map::HighlightMap;
use lazy_static::lazy_static; use lazy_static::lazy_static;
use parking_lot::{Mutex, RwLock}; use parking_lot::{Mutex, RwLock};
@ -26,6 +26,7 @@ use serde::{de, Deserialize, Deserializer};
use serde_json::Value; use serde_json::Value;
use std::{ use std::{
any::Any, any::Any,
borrow::Cow,
cell::RefCell, cell::RefCell,
fmt::Debug, fmt::Debug,
hash::Hash, hash::Hash,
@ -41,6 +42,7 @@ use std::{
use syntax_map::SyntaxSnapshot; use syntax_map::SyntaxSnapshot;
use theme::{SyntaxTheme, Theme}; use theme::{SyntaxTheme, Theme};
use tree_sitter::{self, Query}; use tree_sitter::{self, Query};
use unicase::UniCase;
use util::ResultExt; use util::ResultExt;
#[cfg(any(test, feature = "test-support"))] #[cfg(any(test, feature = "test-support"))]
@ -88,8 +90,7 @@ pub struct CachedLspAdapter {
} }
impl CachedLspAdapter { impl CachedLspAdapter {
pub async fn new<T: LspAdapter>(adapter: T) -> Arc<Self> { pub async fn new(adapter: Box<dyn LspAdapter>) -> Arc<Self> {
let adapter = Box::new(adapter);
let name = adapter.name().await; let name = adapter.name().await;
let server_args = adapter.server_args().await; let server_args = adapter.server_args().await;
let initialization_options = adapter.initialization_options().await; let initialization_options = adapter.initialization_options().await;
@ -247,6 +248,16 @@ pub struct LanguageConfig {
pub overrides: HashMap<String, LanguageConfigOverride>, pub overrides: HashMap<String, LanguageConfigOverride>,
} }
#[derive(Debug, Default)]
pub struct LanguageQueries {
pub highlights: Option<Cow<'static, str>>,
pub brackets: Option<Cow<'static, str>>,
pub indents: Option<Cow<'static, str>>,
pub outline: Option<Cow<'static, str>>,
pub injections: Option<Cow<'static, str>>,
pub overrides: Option<Cow<'static, str>>,
}
#[derive(Clone)] #[derive(Clone)]
pub struct LanguageScope { pub struct LanguageScope {
language: Arc<Language>, language: Arc<Language>,
@ -406,8 +417,17 @@ pub enum LanguageServerBinaryStatus {
Failed { error: String }, Failed { error: String },
} }
struct AvailableLanguage {
path: &'static str,
config: LanguageConfig,
grammar: tree_sitter::Language,
lsp_adapter: Option<Box<dyn LspAdapter>>,
get_queries: fn(&str) -> LanguageQueries,
}
pub struct LanguageRegistry { pub struct LanguageRegistry {
languages: RwLock<Vec<Arc<Language>>>, languages: RwLock<Vec<Arc<Language>>>,
available_languages: RwLock<Vec<AvailableLanguage>>,
language_server_download_dir: Option<Arc<Path>>, language_server_download_dir: Option<Arc<Path>>,
lsp_binary_statuses_tx: async_broadcast::Sender<(Arc<Language>, LanguageServerBinaryStatus)>, lsp_binary_statuses_tx: async_broadcast::Sender<(Arc<Language>, LanguageServerBinaryStatus)>,
lsp_binary_statuses_rx: async_broadcast::Receiver<(Arc<Language>, LanguageServerBinaryStatus)>, lsp_binary_statuses_rx: async_broadcast::Receiver<(Arc<Language>, LanguageServerBinaryStatus)>,
@ -421,6 +441,8 @@ pub struct LanguageRegistry {
>, >,
subscription: RwLock<(watch::Sender<()>, watch::Receiver<()>)>, subscription: RwLock<(watch::Sender<()>, watch::Receiver<()>)>,
theme: RwLock<Option<Arc<Theme>>>, theme: RwLock<Option<Arc<Theme>>>,
executor: Option<Arc<Background>>,
version: AtomicUsize,
} }
impl LanguageRegistry { impl LanguageRegistry {
@ -429,12 +451,15 @@ impl LanguageRegistry {
Self { Self {
language_server_download_dir: None, language_server_download_dir: None,
languages: Default::default(), languages: Default::default(),
available_languages: Default::default(),
lsp_binary_statuses_tx, lsp_binary_statuses_tx,
lsp_binary_statuses_rx, lsp_binary_statuses_rx,
login_shell_env_loaded: login_shell_env_loaded.shared(), login_shell_env_loaded: login_shell_env_loaded.shared(),
lsp_binary_paths: Default::default(), lsp_binary_paths: Default::default(),
subscription: RwLock::new(watch::channel()), subscription: RwLock::new(watch::channel()),
theme: Default::default(), theme: Default::default(),
version: Default::default(),
executor: None,
} }
} }
@ -443,11 +468,50 @@ impl LanguageRegistry {
Self::new(Task::ready(())) Self::new(Task::ready(()))
} }
pub fn set_executor(&mut self, executor: Arc<Background>) {
self.executor = Some(executor);
}
pub fn register(
&self,
path: &'static str,
config: LanguageConfig,
grammar: tree_sitter::Language,
lsp_adapter: Option<Box<dyn LspAdapter>>,
get_queries: fn(&str) -> LanguageQueries,
) {
self.available_languages.write().push(AvailableLanguage {
path,
config,
grammar,
lsp_adapter,
get_queries,
});
}
pub fn language_names(&self) -> Vec<String> {
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::<Vec<_>>();
result.sort_unstable();
result
}
pub fn add(&self, language: Arc<Language>) { pub fn add(&self, language: Arc<Language>) {
if let Some(theme) = self.theme.read().clone() { if let Some(theme) = self.theme.read().clone() {
language.set_theme(&theme.editor.syntax); language.set_theme(&theme.editor.syntax);
} }
self.languages.write().push(language); self.languages.write().push(language);
self.version.fetch_add(1, SeqCst);
*self.subscription.write().0.borrow_mut() = (); *self.subscription.write().0.borrow_mut() = ();
} }
@ -455,6 +519,10 @@ impl LanguageRegistry {
self.subscription.read().1.clone() self.subscription.read().1.clone()
} }
pub fn version(&self) -> usize {
self.version.load(SeqCst)
}
pub fn set_theme(&self, theme: Arc<Theme>) { pub fn set_theme(&self, theme: Arc<Theme>) {
*self.theme.write() = Some(theme.clone()); *self.theme.write() = Some(theme.clone());
for language in self.languages.read().iter() { for language in self.languages.read().iter() {
@ -466,42 +534,79 @@ impl LanguageRegistry {
self.language_server_download_dir = Some(path.into()); self.language_server_download_dir = Some(path.into());
} }
pub fn get_language(&self, name: &str) -> Option<Arc<Language>> { pub fn language_for_name(self: &Arc<Self>, name: &str) -> Option<Arc<Language>> {
self.languages let name = UniCase::new(name);
.read() self.get_or_load_language(|config| UniCase::new(config.name.as_ref()) == name)
.iter()
.find(|language| language.name().to_lowercase() == name.to_lowercase())
.cloned()
} }
pub fn to_vec(&self) -> Vec<Arc<Language>> { pub fn language_for_name_or_extension(self: &Arc<Self>, string: &str) -> Option<Arc<Language>> {
self.languages.read().iter().cloned().collect() let string = UniCase::new(string);
self.get_or_load_language(|config| {
UniCase::new(config.name.as_ref()) == string
|| config
.path_suffixes
.iter()
.any(|suffix| UniCase::new(suffix) == string)
})
} }
pub fn language_names(&self) -> Vec<String> { pub fn language_for_path(self: &Arc<Self>, path: impl AsRef<Path>) -> Option<Arc<Language>> {
self.languages
.read()
.iter()
.map(|language| language.name().to_string())
.collect()
}
pub fn select_language(&self, path: impl AsRef<Path>) -> Option<Arc<Language>> {
let path = path.as_ref(); let path = path.as_ref();
let filename = path.file_name().and_then(|name| name.to_str()); let filename = path.file_name().and_then(|name| name.to_str());
let extension = path.extension().and_then(|name| name.to_str()); let extension = path.extension().and_then(|name| name.to_str());
let path_suffixes = [extension, filename]; 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<Self>,
callback: impl Fn(&LanguageConfig) -> bool,
) -> Option<Arc<Language>> {
if let Some(language) = self
.languages
.read() .read()
.iter() .iter()
.find(|language| { .find(|language| callback(&language.config))
language {
.config return Some(language.clone());
.path_suffixes }
.iter()
.any(|suffix| path_suffixes.contains(&Some(suffix.as_str()))) if let Some(executor) = self.executor.clone() {
}) let mut available_languages = self.available_languages.write();
.cloned()
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<Arc<Language>> {
self.languages.read().iter().cloned().collect()
} }
pub fn start_language_server( pub fn start_language_server(
@ -705,12 +810,70 @@ impl Language {
self.grammar.as_ref().map(|g| g.id) self.grammar.as_ref().map(|g| g.id)
} }
pub fn with_queries(mut self, queries: LanguageQueries) -> Result<Self> {
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<Self> { pub fn with_highlights_query(mut self, source: &str) -> Result<Self> {
let grammar = self.grammar_mut(); let grammar = self.grammar_mut();
grammar.highlights_query = Some(Query::new(grammar.ts_language, source)?); grammar.highlights_query = Some(Query::new(grammar.ts_language, source)?);
Ok(self) Ok(self)
} }
pub fn with_outline_query(mut self, source: &str) -> Result<Self> {
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<Self> { pub fn with_brackets_query(mut self, source: &str) -> Result<Self> {
let grammar = self.grammar_mut(); let grammar = self.grammar_mut();
let query = Query::new(grammar.ts_language, source)?; let query = Query::new(grammar.ts_language, source)?;
@ -761,31 +924,6 @@ impl Language {
Ok(self) Ok(self)
} }
pub fn with_outline_query(mut self, source: &str) -> Result<Self> {
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<Self> { pub fn with_injection_query(mut self, source: &str) -> Result<Self> {
let grammar = self.grammar_mut(); let grammar = self.grammar_mut();
let query = Query::new(grammar.ts_language, source)?; let query = Query::new(grammar.ts_language, source)?;
@ -858,8 +996,10 @@ impl Language {
Arc::get_mut(self.grammar.as_mut().unwrap()).unwrap() Arc::get_mut(self.grammar.as_mut().unwrap()).unwrap()
} }
pub fn with_lsp_adapter(mut self, lsp_adapter: Arc<CachedLspAdapter>) -> Self { pub async fn with_lsp_adapter(mut self, lsp_adapter: Option<Box<dyn LspAdapter>>) -> Self {
self.adapter = Some(lsp_adapter); if let Some(adapter) = lsp_adapter {
self.adapter = Some(CachedLspAdapter::new(adapter).await);
}
self self
} }
@ -870,7 +1010,7 @@ impl Language {
) -> mpsc::UnboundedReceiver<lsp::FakeLanguageServer> { ) -> mpsc::UnboundedReceiver<lsp::FakeLanguageServer> {
let (servers_tx, servers_rx) = mpsc::unbounded(); let (servers_tx, servers_rx) = mpsc::unbounded();
self.fake_adapter = Some((servers_tx, fake_lsp_adapter.clone())); 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); self.adapter = Some(adapter);
servers_rx servers_rx
} }

View file

@ -5,8 +5,9 @@ use parking_lot::Mutex;
use std::{ use std::{
borrow::Cow, borrow::Cow,
cell::RefCell, cell::RefCell,
cmp::{Ordering, Reverse}, cmp::{self, Ordering, Reverse},
collections::BinaryHeap, collections::BinaryHeap,
iter,
ops::{Deref, DerefMut, Range}, ops::{Deref, DerefMut, Range},
sync::Arc, sync::Arc,
}; };
@ -26,8 +27,6 @@ lazy_static! {
#[derive(Default)] #[derive(Default)]
pub struct SyntaxMap { pub struct SyntaxMap {
parsed_version: clock::Global,
interpolated_version: clock::Global,
snapshot: SyntaxSnapshot, snapshot: SyntaxSnapshot,
language_registry: Option<Arc<LanguageRegistry>>, language_registry: Option<Arc<LanguageRegistry>>,
} }
@ -35,6 +34,9 @@ pub struct SyntaxMap {
#[derive(Clone, Default)] #[derive(Clone, Default)]
pub struct SyntaxSnapshot { pub struct SyntaxSnapshot {
layers: SumTree<SyntaxLayer>, layers: SumTree<SyntaxLayer>,
parsed_version: clock::Global,
interpolated_version: clock::Global,
language_registry_version: usize,
} }
#[derive(Default)] #[derive(Default)]
@ -89,8 +91,34 @@ struct SyntaxMapMatchesLayer<'a> {
struct SyntaxLayer { struct SyntaxLayer {
depth: usize, depth: usize,
range: Range<Anchor>, range: Range<Anchor>,
tree: tree_sitter::Tree, content: SyntaxLayerContent,
language: Arc<Language>, }
#[derive(Clone)]
enum SyntaxLayerContent {
Parsed {
tree: tree_sitter::Tree,
language: Arc<Language>,
},
Pending {
language_name: Arc<str>,
},
}
impl SyntaxLayerContent {
fn language_id(&self) -> Option<usize> {
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)] #[derive(Debug)]
@ -107,6 +135,7 @@ struct SyntaxLayerSummary {
range: Range<Anchor>, range: Range<Anchor>,
last_layer_range: Range<Anchor>, last_layer_range: Range<Anchor>,
last_layer_language: Option<usize>, last_layer_language: Option<usize>,
contains_unknown_injections: bool,
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@ -130,12 +159,26 @@ struct SyntaxLayerPositionBeforeChange {
struct ParseStep { struct ParseStep {
depth: usize, depth: usize,
language: Arc<Language>, language: ParseStepLanguage,
range: Range<Anchor>, range: Range<Anchor>,
included_ranges: Vec<tree_sitter::Range>, included_ranges: Vec<tree_sitter::Range>,
mode: ParseMode, mode: ParseMode,
} }
enum ParseStepLanguage {
Loaded { language: Arc<Language> },
Pending { name: Arc<str> },
}
impl ParseStepLanguage {
fn id(&self) -> Option<usize> {
match self {
ParseStepLanguage::Loaded { language } => language.id(),
ParseStepLanguage::Pending { .. } => None,
}
}
}
enum ParseMode { enum ParseMode {
Single, Single,
Combined { Combined {
@ -176,30 +219,17 @@ impl SyntaxMap {
self.language_registry.clone() self.language_registry.clone()
} }
pub fn parsed_version(&self) -> clock::Global {
self.parsed_version.clone()
}
pub fn interpolate(&mut self, text: &BufferSnapshot) { pub fn interpolate(&mut self, text: &BufferSnapshot) {
self.snapshot.interpolate(&self.interpolated_version, text); self.snapshot.interpolate(text);
self.interpolated_version = text.version.clone();
} }
#[cfg(test)] #[cfg(test)]
pub fn reparse(&mut self, language: Arc<Language>, text: &BufferSnapshot) { pub fn reparse(&mut self, language: Arc<Language>, text: &BufferSnapshot) {
self.snapshot.reparse( self.snapshot
&self.parsed_version, .reparse(text, self.language_registry.clone(), language);
text,
self.language_registry.clone(),
language,
);
self.parsed_version = text.version.clone();
self.interpolated_version = text.version.clone();
} }
pub fn did_parse(&mut self, snapshot: SyntaxSnapshot, version: clock::Global) { pub fn did_parse(&mut self, snapshot: SyntaxSnapshot) {
self.interpolated_version = version.clone();
self.parsed_version = version;
self.snapshot = snapshot; self.snapshot = snapshot;
} }
@ -213,10 +243,12 @@ impl SyntaxSnapshot {
self.layers.is_empty() self.layers.is_empty()
} }
pub fn interpolate(&mut self, from_version: &clock::Global, text: &BufferSnapshot) { fn interpolate(&mut self, text: &BufferSnapshot) {
let edits = text let edits = text
.anchored_edits_since::<(usize, Point)>(&from_version) .anchored_edits_since::<(usize, Point)>(&self.interpolated_version)
.collect::<Vec<_>>(); .collect::<Vec<_>>();
self.interpolated_version = text.version().clone();
if edits.is_empty() { if edits.is_empty() {
return; return;
} }
@ -276,47 +308,49 @@ impl SyntaxSnapshot {
} }
let mut layer = layer.clone(); let mut layer = layer.clone();
for (edit, edit_range) in &edits[first_edit_ix_for_depth..] { if let SyntaxLayerContent::Parsed { tree, .. } = &mut layer.content {
// Ignore any edits that follow this layer. for (edit, edit_range) in &edits[first_edit_ix_for_depth..] {
if edit_range.start.cmp(&layer.range.end, text).is_ge() { // Ignore any edits that follow this layer.
break; if edit_range.start.cmp(&layer.range.end, text).is_ge() {
break;
}
// Apply any edits that intersect this layer to the layer's syntax tree.
let tree_edit = if edit_range.start.cmp(&layer.range.start, text).is_ge() {
tree_sitter::InputEdit {
start_byte: edit.new.start.0 - start_byte,
old_end_byte: edit.new.start.0 - start_byte
+ (edit.old.end.0 - edit.old.start.0),
new_end_byte: edit.new.end.0 - start_byte,
start_position: (edit.new.start.1 - start_point).to_ts_point(),
old_end_position: (edit.new.start.1 - start_point
+ (edit.old.end.1 - edit.old.start.1))
.to_ts_point(),
new_end_position: (edit.new.end.1 - start_point).to_ts_point(),
}
} else {
let node = tree.root_node();
tree_sitter::InputEdit {
start_byte: 0,
old_end_byte: node.end_byte(),
new_end_byte: 0,
start_position: Default::default(),
old_end_position: node.end_position(),
new_end_position: Default::default(),
}
};
tree.edit(&tree_edit);
} }
// Apply any edits that intersect this layer to the layer's syntax tree. debug_assert!(
let tree_edit = if edit_range.start.cmp(&layer.range.start, text).is_ge() { tree.root_node().end_byte() <= text.len(),
tree_sitter::InputEdit { "tree's size {}, is larger than text size {}",
start_byte: edit.new.start.0 - start_byte, tree.root_node().end_byte(),
old_end_byte: edit.new.start.0 - start_byte text.len(),
+ (edit.old.end.0 - edit.old.start.0), );
new_end_byte: edit.new.end.0 - start_byte,
start_position: (edit.new.start.1 - start_point).to_ts_point(),
old_end_position: (edit.new.start.1 - start_point
+ (edit.old.end.1 - edit.old.start.1))
.to_ts_point(),
new_end_position: (edit.new.end.1 - start_point).to_ts_point(),
}
} else {
let node = layer.tree.root_node();
tree_sitter::InputEdit {
start_byte: 0,
old_end_byte: node.end_byte(),
new_end_byte: 0,
start_position: Default::default(),
old_end_position: node.end_position(),
new_end_position: Default::default(),
}
};
layer.tree.edit(&tree_edit);
} }
debug_assert!(
layer.tree.root_node().end_byte() <= text.len(),
"tree's size {}, is larger than text size {}",
layer.tree.root_node().end_byte(),
text.len(),
);
layers.push(layer, text); layers.push(layer, text);
cursor.next(text); cursor.next(text);
} }
@ -328,12 +362,58 @@ impl SyntaxSnapshot {
pub fn reparse( pub fn reparse(
&mut self, &mut self,
from_version: &clock::Global,
text: &BufferSnapshot, text: &BufferSnapshot,
registry: Option<Arc<LanguageRegistry>>, registry: Option<Arc<LanguageRegistry>>,
root_language: Arc<Language>, root_language: Arc<Language>,
) { ) {
let edits = text.edits_since::<usize>(from_version).collect::<Vec<_>>(); let edit_ranges = text
.edits_since::<usize>(&self.parsed_version)
.map(|edit| edit.new)
.collect::<Vec<_>>();
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 = &registry;
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(&registry),
);
}
self.language_registry_version = registry.version();
}
}
}
fn reparse_with_ranges(
&mut self,
text: &BufferSnapshot,
root_language: Arc<Language>,
invalidated_ranges: Vec<Range<usize>>,
registry: Option<&Arc<LanguageRegistry>>,
) {
let max_depth = self.layers.summary().max_depth; let max_depth = self.layers.summary().max_depth;
let mut cursor = self.layers.cursor::<SyntaxLayerSummary>(); let mut cursor = self.layers.cursor::<SyntaxLayerSummary>();
cursor.next(&text); cursor.next(&text);
@ -344,7 +424,9 @@ impl SyntaxSnapshot {
let mut combined_injection_ranges = HashMap::default(); let mut combined_injection_ranges = HashMap::default();
queue.push(ParseStep { queue.push(ParseStep {
depth: 0, depth: 0,
language: root_language.clone(), language: ParseStepLanguage::Loaded {
language: root_language,
},
included_ranges: vec![tree_sitter::Range { included_ranges: vec![tree_sitter::Range {
start_byte: 0, start_byte: 0,
end_byte: text.len(), end_byte: text.len(),
@ -415,12 +497,11 @@ impl SyntaxSnapshot {
let (step_start_byte, step_start_point) = let (step_start_byte, step_start_point) =
step.range.start.summary::<(usize, Point)>(text); step.range.start.summary::<(usize, Point)>(text);
let step_end_byte = step.range.end.to_offset(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(); let mut old_layer = cursor.item();
if let Some(layer) = old_layer { if let Some(layer) = old_layer {
if layer.range.to_offset(text) == (step_start_byte..step_end_byte) 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); cursor.next(&text);
} else { } else {
@ -428,89 +509,130 @@ impl SyntaxSnapshot {
} }
} }
let tree; let content = match step.language {
let changed_ranges; ParseStepLanguage::Loaded { language } => {
let mut included_ranges = step.included_ranges; let Some(grammar) = language.grammar() else { continue };
if let Some(old_layer) = old_layer { let tree;
if let ParseMode::Combined { let changed_ranges;
parent_layer_changed_ranges, let mut included_ranges = step.included_ranges;
.. if let Some(SyntaxLayerContent::Parsed { tree: old_tree, .. }) =
} = step.mode old_layer.map(|layer| &layer.content)
{ {
included_ranges = splice_included_ranges( if let ParseMode::Combined {
old_layer.tree.included_ranges(), parent_layer_changed_ranges,
&parent_layer_changed_ranges, ..
&included_ranges, } = step.mode
); {
} included_ranges = splice_included_ranges(
old_tree.included_ranges(),
&parent_layer_changed_ranges,
&included_ranges,
);
}
tree = parse_text( tree = parse_text(
grammar, grammar,
text.as_rope(), text.as_rope(),
step_start_byte, step_start_byte,
step_start_point, step_start_point,
included_ranges, included_ranges,
Some(old_layer.tree.clone()), Some(old_tree.clone()),
); );
changed_ranges = join_ranges( changed_ranges = join_ranges(
edits.iter().map(|e| e.new.clone()).filter(|range| { invalidated_ranges.iter().cloned().filter(|range| {
range.start <= step_end_byte && range.end >= step_start_byte range.start <= step_end_byte && range.end >= step_start_byte
}), }),
old_layer old_tree.changed_ranges(&tree).map(|r| {
.tree step_start_byte + r.start_byte..step_start_byte + r.end_byte
.changed_ranges(&tree) }),
.map(|r| step_start_byte + r.start_byte..step_start_byte + r.end_byte), );
); } else {
} else { tree = parse_text(
tree = parse_text( grammar,
grammar, text.as_rope(),
text.as_rope(), step_start_byte,
step_start_byte, step_start_point,
step_start_point, included_ranges,
included_ranges, None,
None, );
); changed_ranges = vec![step_start_byte..step_end_byte];
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( layers.push(
SyntaxLayer { SyntaxLayer {
depth: step.depth, depth: step.depth,
range: step.range, range: step.range,
tree: tree.clone(), content,
language: step.language.clone(),
}, },
&text, &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); drop(cursor);
self.layers = layers; 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<Range<Anchor>> = 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>( pub fn single_tree_captures<'a>(
@ -585,23 +707,34 @@ impl SyntaxSnapshot {
}); });
cursor.next(buffer); cursor.next(buffer);
std::iter::from_fn(move || { iter::from_fn(move || {
if let Some(layer) = cursor.item() { while let Some(layer) = cursor.item() {
let info = SyntaxLayerInfo { if let SyntaxLayerContent::Parsed { tree, language } = &layer.content {
language: &layer.language, let info = SyntaxLayerInfo {
depth: layer.depth, language,
node: layer.tree.root_node_with_offset( depth: layer.depth,
layer.range.start.to_offset(buffer), node: tree.root_node_with_offset(
layer.range.start.to_point(buffer).to_ts_point(), layer.range.start.to_offset(buffer),
), layer.range.start.to_point(buffer).to_ts_point(),
}; ),
cursor.next(buffer); };
Some(info) cursor.next(buffer);
} else { return Some(info);
None } 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> { impl<'a> SyntaxMapCaptures<'a> {
@ -963,20 +1096,20 @@ fn get_injections(
config: &InjectionConfig, config: &InjectionConfig,
text: &BufferSnapshot, text: &BufferSnapshot,
node: Node, node: Node,
language_registry: &LanguageRegistry, language_registry: &Arc<LanguageRegistry>,
depth: usize, depth: usize,
changed_ranges: &[Range<usize>], changed_ranges: &[Range<usize>],
combined_injection_ranges: &mut HashMap<Arc<Language>, Vec<tree_sitter::Range>>, combined_injection_ranges: &mut HashMap<Arc<Language>, Vec<tree_sitter::Range>>,
queue: &mut BinaryHeap<ParseStep>, queue: &mut BinaryHeap<ParseStep>,
) -> bool { ) {
let mut result = false;
let mut query_cursor = QueryCursorHandle::new(); let mut query_cursor = QueryCursorHandle::new();
let mut prev_match = None; let mut prev_match = None;
combined_injection_ranges.clear(); combined_injection_ranges.clear();
for pattern in &config.patterns { for pattern in &config.patterns {
if let (Some(language_name), true) = (pattern.language.as_ref(), pattern.combined) { 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()); combined_injection_ranges.insert(language, Vec::new());
} }
} }
@ -1004,21 +1137,29 @@ fn get_injections(
prev_match = Some((mat.pattern_index, content_range.clone())); prev_match = Some((mat.pattern_index, content_range.clone()));
let combined = config.patterns[mat.pattern_index].combined; let combined = config.patterns[mat.pattern_index].combined;
let language_name = config.patterns[mat.pattern_index]
.language let mut language_name = None;
.as_ref() let mut step_range = content_range.clone();
.map(|s| Cow::Borrowed(s.as_ref())) if let Some(name) = config.patterns[mat.pattern_index].language.as_ref() {
.or_else(|| { language_name = Some(Cow::Borrowed(name.as_ref()))
let ix = config.language_capture_ix?; } else if let Some(language_node) = config
let node = mat.nodes_for_capture_index(ix).next()?; .language_capture_ix
Some(Cow::Owned(text.text_for_range(node.byte_range()).collect())) .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_name) = language_name {
if let Some(language) = language_registry.get_language(language_name.as_ref()) { let language = {
result = true; let language_name: &str = &language_name;
let range = text.anchor_before(content_range.start) language_registry.language_for_name_or_extension(language_name)
..text.anchor_after(content_range.end); };
let range = text.anchor_before(step_range.start)..text.anchor_after(step_range.end);
if let Some(language) = language {
if combined { if combined {
combined_injection_ranges combined_injection_ranges
.get_mut(&language.clone()) .get_mut(&language.clone())
@ -1027,12 +1168,22 @@ fn get_injections(
} else { } else {
queue.push(ParseStep { queue.push(ParseStep {
depth, depth,
language, language: ParseStepLanguage::Loaded { language },
included_ranges: content_ranges, included_ranges: content_ranges,
range, range,
mode: ParseMode::Single, 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()); let range = text.anchor_before(node.start_byte())..text.anchor_after(node.end_byte());
queue.push(ParseStep { queue.push(ParseStep {
depth, depth,
language, language: ParseStepLanguage::Loaded { language },
range, range,
included_ranges, included_ranges,
mode: ParseMode::Combined { mode: ParseMode::Combined {
@ -1052,8 +1203,6 @@ fn get_injections(
}, },
}) })
} }
result
} }
fn splice_included_ranges( fn splice_included_ranges(
@ -1282,6 +1431,7 @@ impl Default for SyntaxLayerSummary {
range: Anchor::MAX..Anchor::MIN, range: Anchor::MAX..Anchor::MIN,
last_layer_range: Anchor::MIN..Anchor::MAX, last_layer_range: Anchor::MIN..Anchor::MAX,
last_layer_language: None, 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.max_depth = other.max_depth;
self.range = other.range.clone(); self.range = other.range.clone();
} else { } 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; self.range.start = other.range.start;
} }
if other.range.end.cmp(&self.range.end, buffer).is_gt() { 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_range = other.last_layer_range.clone();
self.last_layer_language = other.last_layer_language; 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, max_depth: self.depth,
range: self.range.clone(), range: self.range.clone(),
last_layer_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") f.debug_struct("SyntaxLayer")
.field("depth", &self.depth) .field("depth", &self.depth)
.field("range", &self.range) .field("range", &self.range)
.field("tree", &self.tree) .field("tree", &self.content.tree())
.finish() .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] #[gpui::test]
fn test_typing_multiple_new_injections() { fn test_typing_multiple_new_injections() {
let (buffer, syntax_map) = test_edit_sequence( let (buffer, syntax_map) = test_edit_sequence(
@ -2157,16 +2387,14 @@ mod tests {
.zip(new_syntax_map.layers.iter()) .zip(new_syntax_map.layers.iter())
{ {
assert_eq!(old_layer.range, new_layer.range); 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 old_start_byte = old_layer.range.start.to_offset(old_buffer);
let new_start_byte = new_layer.range.start.to_offset(new_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 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 new_start_point = new_layer.range.start.to_point(new_buffer).to_ts_point();
let old_node = old_layer let old_node = old_tree.root_node_with_offset(old_start_byte, old_start_point);
.tree let new_node = new_tree.root_node_with_offset(new_start_byte, new_start_point);
.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);
check_node_edits( check_node_edits(
old_layer.depth, old_layer.depth,
&old_layer.range, &old_layer.range,
@ -2254,7 +2482,8 @@ mod tests {
registry.add(Arc::new(ruby_lang())); registry.add(Arc::new(ruby_lang()));
registry.add(Arc::new(html_lang())); registry.add(Arc::new(html_lang()));
registry.add(Arc::new(erb_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 buffer = Buffer::new(0, 0, Default::default());
let mut mutated_syntax_map = SyntaxMap::new(); let mut mutated_syntax_map = SyntaxMap::new();
@ -2392,6 +2621,26 @@ mod tests {
.unwrap() .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<usize> { fn range_for_text(buffer: &Buffer, text: &str) -> Range<usize> {
let start = buffer.as_rope().to_string().find(text).unwrap(); let start = buffer.as_rope().to_string().find(text).unwrap();
start..start + text.len() start..start + text.len()

View file

@ -128,14 +128,9 @@ impl Room {
let url = url.to_string(); let url = url.to_string();
let token = token.to_string(); let token = token.to_string();
async move { async move {
match rx.await.unwrap().context("error connecting to room") { rx.await.unwrap().context("error connecting to room")?;
Ok(()) => { *this.connection.lock().0.borrow_mut() = ConnectionState::Connected { url, token };
*this.connection.lock().0.borrow_mut() = Ok(())
ConnectionState::Connected { url, token };
Ok(())
}
Err(err) => Err(err),
}
} }
} }

View file

@ -1,3 +1,4 @@
use log::warn;
pub use lsp_types::request::*; pub use lsp_types::request::*;
pub use lsp_types::*; pub use lsp_types::*;
@ -64,6 +65,7 @@ struct Request<'a, T> {
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
struct AnyResponse<'a> { struct AnyResponse<'a> {
jsonrpc: &'a str,
id: usize, id: usize,
#[serde(default)] #[serde(default)]
error: Option<Error>, error: Option<Error>,
@ -203,8 +205,9 @@ impl LanguageServer {
} else { } else {
on_unhandled_notification(msg); on_unhandled_notification(msg);
} }
} else if let Ok(AnyResponse { id, error, result }) = } else if let Ok(AnyResponse {
serde_json::from_slice(&buffer) id, error, result, ..
}) = serde_json::from_slice(&buffer)
{ {
if let Some(handler) = response_handlers if let Some(handler) = response_handlers
.lock() .lock()
@ -220,10 +223,10 @@ impl LanguageServer {
} }
} }
} else { } else {
return Err(anyhow!( warn!(
"failed to deserialize message:\n{}", "Failed to deserialize message:\n{}",
std::str::from_utf8(&buffer)? std::str::from_utf8(&buffer)?
)); );
} }
// Don't starve the main thread when receiving lots of messages at once. // Don't starve the main thread when receiving lots of messages at once.
@ -460,35 +463,57 @@ impl LanguageServer {
method, method,
Box::new(move |id, params, cx| { Box::new(move |id, params, cx| {
if let Some(id) = id { if let Some(id) = id {
if let Some(params) = serde_json::from_str(params).log_err() { match serde_json::from_str(params) {
let response = f(params, cx.clone()); Ok(params) => {
cx.foreground() let response = f(params, cx.clone());
.spawn({ cx.foreground()
let outbound_tx = outbound_tx.clone(); .spawn({
async move { let outbound_tx = outbound_tx.clone();
let response = match response.await { async move {
Ok(result) => Response { let response = match response.await {
jsonrpc: JSON_RPC_VERSION, Ok(result) => Response {
id, jsonrpc: JSON_RPC_VERSION,
result: Some(result), id,
error: None, result: Some(result),
}, error: None,
Err(error) => Response { },
jsonrpc: JSON_RPC_VERSION, Err(error) => Response {
id, jsonrpc: JSON_RPC_VERSION,
result: None, id,
error: Some(Error { result: None,
message: error.to_string(), error: Some(Error {
}), message: error.to_string(),
}, }),
}; },
if let Some(response) = serde_json::to_vec(&response).log_err() };
{ if let Some(response) =
outbound_tx.try_send(response).ok(); 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();
}
}
} }
} }
}), }),

21
crates/pando/Cargo.toml Normal file
View file

@ -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" }

15
crates/pando/src/pando.rs Normal file
View file

@ -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

View file

@ -12,7 +12,7 @@ use clock::ReplicaId;
use collections::{hash_map, BTreeMap, HashMap, HashSet}; use collections::{hash_map, BTreeMap, HashMap, HashSet};
use futures::{ use futures::{
channel::{mpsc, oneshot}, channel::{mpsc, oneshot},
future::Shared, future::{try_join_all, Shared},
AsyncWriteExt, Future, FutureExt, StreamExt, TryFutureExt, AsyncWriteExt, Future, FutureExt, StreamExt, TryFutureExt,
}; };
use gpui::{ use gpui::{
@ -28,8 +28,8 @@ use language::{
range_from_lsp, range_to_lsp, Anchor, Bias, Buffer, CachedLspAdapter, CharKind, CodeAction, range_from_lsp, range_to_lsp, Anchor, Bias, Buffer, CachedLspAdapter, CharKind, CodeAction,
CodeLabel, Completion, Diagnostic, DiagnosticEntry, DiagnosticSet, Event as BufferEvent, CodeLabel, Completion, Diagnostic, DiagnosticEntry, DiagnosticSet, Event as BufferEvent,
File as _, Language, LanguageRegistry, LanguageServerName, LocalFile, OffsetRangeExt, File as _, Language, LanguageRegistry, LanguageServerName, LocalFile, OffsetRangeExt,
Operation, Patch, PointUtf16, TextBufferSnapshot, ToOffset, ToPointUtf16, Transaction, Operation, Patch, PointUtf16, RopeFingerprint, TextBufferSnapshot, ToOffset, ToPointUtf16,
Unclipped, Transaction, Unclipped,
}; };
use lsp::{ use lsp::{
DiagnosticSeverity, DiagnosticTag, DocumentHighlightKind, LanguageServer, LanguageString, DiagnosticSeverity, DiagnosticTag, DocumentHighlightKind, LanguageServer, LanguageString,
@ -59,7 +59,7 @@ use std::{
atomic::{AtomicUsize, Ordering::SeqCst}, atomic::{AtomicUsize, Ordering::SeqCst},
Arc, Arc,
}, },
time::Instant, time::{Duration, Instant, SystemTime},
}; };
use terminal::{Terminal, TerminalBuilder}; use terminal::{Terminal, TerminalBuilder};
use util::{debug_panic, defer, post_inc, ResultExt, TryFutureExt as _}; use util::{debug_panic, defer, post_inc, ResultExt, TryFutureExt as _};
@ -185,6 +185,7 @@ pub enum LanguageServerState {
language: Arc<Language>, language: Arc<Language>,
adapter: Arc<CachedLspAdapter>, adapter: Arc<CachedLspAdapter>,
server: Arc<LanguageServer>, server: Arc<LanguageServer>,
simulate_disk_based_diagnostics_completion: Option<Task<()>>,
}, },
} }
@ -550,15 +551,16 @@ impl Project {
if !cx.read(|cx| cx.has_global::<Settings>()) { if !cx.read(|cx| cx.has_global::<Settings>()) {
cx.update(|cx| { cx.update(|cx| {
cx.set_global(Settings::test(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 http_client = client::test::FakeHttpClient::with_404_response();
let client = cx.update(|cx| client::Client::new(http_client.clone(), cx)); 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 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 { for path in root_paths {
let (tree, _) = project let (tree, _) = project
.update(cx, |project, cx| { .update(cx, |project, cx| {
@ -1426,11 +1428,41 @@ impl Project {
} }
} }
pub fn save_buffers(
&self,
buffers: HashSet<ModelHandle<Buffer>>,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
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<Buffer>,
cx: &mut ModelContext<Self>,
) -> Task<Result<(clock::Global, RopeFingerprint, SystemTime)>> {
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( pub fn save_buffer_as(
&mut self, &mut self,
buffer: ModelHandle<Buffer>, buffer: ModelHandle<Buffer>,
abs_path: PathBuf, abs_path: PathBuf,
cx: &mut ModelContext<Project>, cx: &mut ModelContext<Self>,
) -> Task<Result<()>> { ) -> Task<Result<()>> {
let worktree_task = self.find_or_create_local_worktree(&abs_path, true, cx); let worktree_task = self.find_or_create_local_worktree(&abs_path, true, cx);
let old_path = let old_path =
@ -1443,11 +1475,11 @@ impl Project {
} }
let (worktree, path) = worktree_task.await?; let (worktree, path) = worktree_task.await?;
worktree worktree
.update(&mut cx, |worktree, cx| { .update(&mut cx, |worktree, cx| match worktree {
worktree Worktree::Local(worktree) => {
.as_local_mut() worktree.save_buffer(buffer.clone(), path.into(), true, cx)
.unwrap() }
.save_buffer_as(buffer.clone(), path, cx) Worktree::Remote(_) => panic!("cannot remote buffers as new files"),
}) })
.await?; .await?;
this.update(&mut cx, |this, cx| { this.update(&mut cx, |this, cx| {
@ -1480,6 +1512,10 @@ impl Project {
buffer: &ModelHandle<Buffer>, buffer: &ModelHandle<Buffer>,
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
) -> Result<()> { ) -> Result<()> {
buffer.update(cx, |buffer, _| {
buffer.set_language_registry(self.languages.clone())
});
let remote_id = buffer.read(cx).remote_id(); let remote_id = buffer.read(cx).remote_id();
let open_buffer = if self.is_remote() || self.is_shared() { let open_buffer = if self.is_remote() || self.is_shared() {
OpenBuffer::Strong(buffer.clone()) OpenBuffer::Strong(buffer.clone())
@ -1713,19 +1749,39 @@ impl Project {
.log_err(); .log_err();
} }
// After saving a buffer, simulate disk-based diagnostics being finished for languages let language_server_id = self.language_server_id_for_buffer(buffer.read(cx), cx)?;
// that don't support a disk-based progress token. if let Some(LanguageServerState::Running {
let (lsp_adapter, language_server) = adapter,
self.language_server_for_buffer(buffer.read(cx), cx)?; simulate_disk_based_diagnostics_completion,
if lsp_adapter.disk_based_diagnostics_progress_token.is_none() { ..
let server_id = language_server.server_id(); }) = self.language_servers.get_mut(&language_server_id)
self.disk_based_diagnostics_finished(server_id, cx); {
self.broadcast_language_server_update( // After saving a buffer using a language server that doesn't provide
server_id, // a disk-based progress token, kick off a timer that will reset every
proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated( // time the buffer is saved. If the timer eventually fires, simulate
proto::LspDiskBasedDiagnosticsUpdated {}, // 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, adapter,
language, language,
server, server,
..
}) = self.language_servers.get(id) }) = self.language_servers.get(id)
{ {
return Some((adapter, language, server)); return Some((adapter, language, server));
@ -1764,19 +1821,29 @@ impl Project {
while let Some(()) = subscription.next().await { while let Some(()) = subscription.next().await {
if let Some(project) = project.upgrade(&cx) { if let Some(project) = project.upgrade(&cx) {
project.update(&mut cx, |project, 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() { for buffer in project.opened_buffers.values() {
if let Some(buffer) = buffer.upgrade(cx) { if let Some(handle) = buffer.upgrade(cx) {
if buffer.read(cx).language().is_none() { let buffer = &handle.read(cx);
buffers_without_language.push(buffer); 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.assign_language_to_buffer(&buffer, cx);
project.register_buffer_with_language_server(&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<()> { ) -> Option<()> {
// If the buffer has a language, set it and start the language server if we haven't already. // 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 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| { buffer.update(cx, |buffer, cx| {
if buffer.language().map_or(true, |old_language| { if buffer.language().map_or(true, |old_language| {
!Arc::ptr_eq(old_language, &new_language) !Arc::ptr_eq(old_language, &new_language)
}) { }) {
buffer.set_language_registry(self.languages.clone());
buffer.set_language(Some(new_language.clone()), cx); buffer.set_language(Some(new_language.clone()), cx);
} }
}); });
@ -2025,6 +2091,7 @@ impl Project {
adapter: adapter.clone(), adapter: adapter.clone(),
language, language,
server: language_server.clone(), server: language_server.clone(),
simulate_disk_based_diagnostics_completion: None,
}, },
); );
this.language_server_statuses.insert( this.language_server_statuses.insert(
@ -2200,7 +2267,7 @@ impl Project {
}) })
.collect(); .collect();
for (worktree_id, worktree_abs_path, full_path) in language_server_lookup_info { 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); self.restart_language_server(worktree_id, worktree_abs_path, language, cx);
} }
@ -2785,126 +2852,126 @@ impl Project {
trigger: FormatTrigger, trigger: FormatTrigger,
cx: &mut ModelContext<Project>, cx: &mut ModelContext<Project>,
) -> Task<Result<ProjectTransaction>> { ) -> Task<Result<ProjectTransaction>> {
let mut local_buffers = Vec::new(); if self.is_local() {
let mut remote_buffers = None; let mut buffers_with_paths_and_servers = buffers
for buffer_handle in buffers { .into_iter()
let buffer = buffer_handle.read(cx); .filter_map(|buffer_handle| {
if let Some(file) = File::from_dyn(buffer.file()) { let buffer = buffer_handle.read(cx);
if let Some(buffer_abs_path) = file.as_local().map(|f| f.abs_path(cx)) { let file = File::from_dyn(buffer.file())?;
if let Some((_, server)) = self.language_server_for_buffer(buffer, cx) { let buffer_abs_path = file.as_local()?.abs_path(cx);
local_buffers.push((buffer_handle, buffer_abs_path, server.clone())); let (_, server) = self.language_server_for_buffer(buffer, cx)?;
} Some((buffer_handle, buffer_abs_path, server.clone()))
} else { })
remote_buffers.get_or_insert(Vec::new()).push(buffer_handle); .collect::<Vec<_>>();
}
} else {
return Task::ready(Ok(Default::default()));
}
}
let remote_buffers = self.remote_id().zip(remote_buffers); cx.spawn(|this, mut cx| async move {
let client = self.client.clone(); // Do not allow multiple concurrent formatting requests for the
// same buffer.
cx.spawn(|this, mut cx| async move { this.update(&mut cx, |this, _| {
let mut project_transaction = ProjectTransaction::default(); buffers_with_paths_and_servers
.retain(|(buffer, _, _)| this.buffers_being_formatted.insert(buffer.id()));
if let Some((project_id, remote_buffers)) = remote_buffers {
let response = client
.request(proto::FormatBuffers {
project_id,
trigger: trigger as i32,
buffer_ids: remote_buffers
.iter()
.map(|buffer| buffer.read_with(&cx, |buffer, _| buffer.remote_id()))
.collect(),
})
.await?
.transaction
.ok_or_else(|| anyhow!("missing transaction"))?;
project_transaction = this
.update(&mut cx, |this, cx| {
this.deserialize_project_transaction(response, push_to_history, cx)
})
.await?;
}
// Do not allow multiple concurrent formatting requests for the
// same buffer.
this.update(&mut cx, |this, _| {
local_buffers
.retain(|(buffer, _, _)| this.buffers_being_formatted.insert(buffer.id()));
});
let _cleanup = defer({
let this = this.clone();
let mut cx = cx.clone();
let local_buffers = &local_buffers;
move || {
this.update(&mut cx, |this, _| {
for (buffer, _, _) in local_buffers {
this.buffers_being_formatted.remove(&buffer.id());
}
});
}
});
for (buffer, buffer_abs_path, language_server) in &local_buffers {
let (format_on_save, formatter, tab_size) = buffer.read_with(&cx, |buffer, cx| {
let settings = cx.global::<Settings>();
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 transaction = match (formatter, format_on_save) { let _cleanup = defer({
(_, FormatOnSave::Off) if trigger == FormatTrigger::Save => continue, let this = this.clone();
let mut cx = cx.clone();
let local_buffers = &buffers_with_paths_and_servers;
move || {
this.update(&mut cx, |this, _| {
for (buffer, _, _) in local_buffers {
this.buffers_being_formatted.remove(&buffer.id());
}
});
}
});
(Formatter::LanguageServer, FormatOnSave::On | FormatOnSave::Off) let mut project_transaction = ProjectTransaction::default();
| (_, FormatOnSave::LanguageServer) => Self::format_via_lsp( for (buffer, buffer_abs_path, language_server) in &buffers_with_paths_and_servers {
&this, let (format_on_save, formatter, tab_size) =
&buffer, buffer.read_with(&cx, |buffer, cx| {
&buffer_abs_path, let settings = cx.global::<Settings>();
&language_server, let language_name = buffer.language().map(|language| language.name());
tab_size, (
&mut cx, settings.format_on_save(language_name.as_deref()),
) settings.formatter(language_name.as_deref()),
.await settings.tab_size(language_name.as_deref()),
.context("failed to format via language server")?, )
});
( let transaction = match (formatter, format_on_save) {
Formatter::External { command, arguments }, (_, FormatOnSave::Off) if trigger == FormatTrigger::Save => continue,
FormatOnSave::On | FormatOnSave::Off,
) (Formatter::LanguageServer, FormatOnSave::On | FormatOnSave::Off)
| (_, FormatOnSave::External { command, arguments }) => { | (_, FormatOnSave::LanguageServer) => Self::format_via_lsp(
Self::format_via_external_command( &this,
&buffer, &buffer,
&buffer_abs_path, &buffer_abs_path,
&command, &language_server,
&arguments, tab_size,
&mut cx, &mut cx,
) )
.await .await
.context(format!( .context("failed to format via language server")?,
"failed to format via external command {:?}",
command
))?
}
};
if let Some(transaction) = transaction { (
if !push_to_history { Formatter::External { command, arguments },
buffer.update(&mut cx, |buffer, _| { FormatOnSave::On | FormatOnSave::Off,
buffer.forget_transaction(transaction.id) )
}); | (_, 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( async fn format_via_lsp(
@ -3095,6 +3162,7 @@ impl Project {
adapter, adapter,
language, language,
server, server,
..
}) = self.language_servers.get(server_id) }) = self.language_servers.get(server_id)
{ {
let adapter = adapter.clone(); let adapter = adapter.clone();
@ -3160,7 +3228,7 @@ impl Project {
let signature = this.symbol_signature(&project_path); let signature = this.symbol_signature(&project_path);
let language = this let language = this
.languages .languages
.select_language(&project_path.path) .language_for_path(&project_path.path)
.unwrap_or(adapter_language.clone()); .unwrap_or(adapter_language.clone());
let language_server_name = adapter.name.clone(); let language_server_name = adapter.name.clone();
Some(async move { Some(async move {
@ -4395,16 +4463,19 @@ impl Project {
renamed_buffers.push((cx.handle(), old_path)); renamed_buffers.push((cx.handle(), old_path));
} }
if let Some(project_id) = self.remote_id() { if new_file != *old_file {
self.client if let Some(project_id) = self.remote_id() {
.send(proto::UpdateBufferFile { self.client
project_id, .send(proto::UpdateBufferFile {
buffer_id: *buffer_id as u64, project_id,
file: Some(new_file.to_proto()), buffer_id: *buffer_id as u64,
}) file: Some(new_file.to_proto()),
.log_err(); })
.log_err();
}
buffer.file_updated(Arc::new(new_file), cx).detach();
} }
buffer.file_updated(Arc::new(new_file), cx).detach();
} }
}); });
} else { } else {
@ -5117,8 +5188,9 @@ impl Project {
}) })
.await; .await;
let (saved_version, fingerprint, mtime) = let (saved_version, fingerprint, mtime) = this
buffer.update(&mut cx, |buffer, cx| buffer.save(cx)).await?; .update(&mut cx, |this, cx| this.save_buffer(buffer, cx))
.await?;
Ok(proto::BufferSaved { Ok(proto::BufferSaved {
project_id, project_id,
buffer_id, buffer_id,
@ -5936,7 +6008,7 @@ impl Project {
worktree_id, worktree_id,
path: PathBuf::from(serialized_symbol.path).into(), path: PathBuf::from(serialized_symbol.path).into(),
}; };
let language = languages.select_language(&path.path); let language = languages.language_for_path(&path.path);
Ok(Symbol { Ok(Symbol {
language_server_name: LanguageServerName( language_server_name: LanguageServerName(
serialized_symbol.language_server_name.into(), serialized_symbol.language_server_name.into(),
@ -5988,7 +6060,7 @@ impl Project {
.and_then(|buffer| buffer.upgrade(cx)); .and_then(|buffer| buffer.upgrade(cx));
if let Some(buffer) = buffer { if let Some(buffer) = buffer {
buffer.update(cx, |buffer, cx| { buffer.update(cx, |buffer, cx| {
buffer.did_save(version, fingerprint, mtime, None, cx); buffer.did_save(version, fingerprint, mtime, cx);
}); });
} }
Ok(()) Ok(())
@ -6168,22 +6240,27 @@ impl Project {
buffer: &Buffer, buffer: &Buffer,
cx: &AppContext, cx: &AppContext,
) -> Option<(&Arc<CachedLspAdapter>, &Arc<LanguageServer>)> { ) -> Option<(&Arc<CachedLspAdapter>, &Arc<LanguageServer>)> {
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<usize> {
if let Some((file, language)) = File::from_dyn(buffer.file()).zip(buffer.language()) { if let Some((file, language)) = File::from_dyn(buffer.file()).zip(buffer.language()) {
let name = language.lsp_adapter()?.name.clone(); let name = language.lsp_adapter()?.name.clone();
let worktree_id = file.worktree_id(cx); let worktree_id = file.worktree_id(cx);
let key = (worktree_id, name); let key = (worktree_id, name);
self.language_server_ids.get(&key).copied()
if let Some(server_id) = self.language_server_ids.get(&key) { } else {
if let Some(LanguageServerState::Running { None
adapter, server, ..
}) = self.language_servers.get(server_id)
{
return Some((adapter, server));
}
}
} }
None
} }
} }

View file

@ -243,8 +243,8 @@ async fn test_managing_language_servers(
); );
// Save notifications are reported to all servers. // Save notifications are reported to all servers.
toml_buffer project
.update(cx, |buffer, cx| buffer.save(cx)) .update(cx, |project, cx| project.save_buffer(toml_buffer, cx))
.await .await
.unwrap(); .unwrap();
assert_eq!( 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)) .update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx))
.await .await
.unwrap(); .unwrap();
buffer buffer.update(cx, |buffer, cx| {
.update(cx, |buffer, cx| { assert_eq!(buffer.text(), "the old contents");
assert_eq!(buffer.text(), "the old contents"); buffer.edit([(0..0, "a line of text.\n".repeat(10 * 1024))], None, cx);
buffer.edit([(0..0, "a line of text.\n".repeat(10 * 1024))], None, cx); });
buffer.save(cx)
}) project
.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
.await .await
.unwrap(); .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)) .update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx))
.await .await
.unwrap(); .unwrap();
buffer buffer.update(cx, |buffer, cx| {
.update(cx, |buffer, cx| { buffer.edit([(0..0, "a line of text.\n".repeat(10 * 1024))], None, cx);
buffer.edit([(0..0, "a line of text.\n".repeat(10 * 1024))], None, cx); });
buffer.save(cx)
}) project
.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
.await .await
.unwrap(); .unwrap();
@ -2130,6 +2132,20 @@ async fn test_save_as(cx: &mut gpui::TestAppContext) {
fs.insert_tree("/dir", json!({})).await; fs.insert_tree("/dir", json!({})).await;
let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).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| { let buffer = project.update(cx, |project, cx| {
project.create_buffer("", None, cx).unwrap() 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); buffer.edit([(0..0, "abc")], None, cx);
assert!(buffer.is_dirty()); assert!(buffer.is_dirty());
assert!(!buffer.has_conflict()); assert!(!buffer.has_conflict());
assert_eq!(buffer.language().unwrap().name().as_ref(), "Plain Text");
}); });
project project
.update(cx, |project, cx| { .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 .await
.unwrap(); .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| { 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.is_dirty());
assert!(!buffer.has_conflict()); assert!(!buffer.has_conflict());
assert_eq!(buffer.language().unwrap().name().as_ref(), "Rust");
}); });
let opened_buffer = project let opened_buffer = project
.update(cx, |project, cx| { .update(cx, |project, cx| {
project.open_local_buffer("/dir/file1", cx) project.open_local_buffer("/dir/file1.rs", cx)
}) })
.await .await
.unwrap(); .unwrap();
@ -2462,7 +2485,6 @@ async fn test_buffer_is_dirty(cx: &mut gpui::TestAppContext) {
buffer.version(), buffer.version(),
buffer.as_rope().fingerprint(), buffer.as_rope().fingerprint(),
buffer.file().unwrap().mtime(), buffer.file().unwrap().mtime(),
None,
cx, 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. // Save a file with windows line endings. The file is written correctly.
buffer2 buffer2.update(cx, |buffer, cx| {
.update(cx, |buffer, cx| { buffer.set_text("one\ntwo\nthree\nfour\n", cx);
buffer.set_text("one\ntwo\nthree\nfour\n", cx); });
buffer.save(cx) project
}) .update(cx, |project, cx| project.save_buffer(buffer2, cx))
.await .await
.unwrap(); .unwrap();
assert_eq!( assert_eq!(

View file

@ -5,8 +5,8 @@ use anyhow::{anyhow, Context, Result};
use client::{proto, Client}; use client::{proto, Client};
use clock::ReplicaId; use clock::ReplicaId;
use collections::{HashMap, VecDeque}; use collections::{HashMap, VecDeque};
use fs::LineEnding;
use fs::{repository::GitRepository, Fs}; use fs::{repository::GitRepository, Fs};
use fs::{HomeDir, LineEnding};
use futures::{ use futures::{
channel::{ channel::{
mpsc::{self, UnboundedSender}, mpsc::{self, UnboundedSender},
@ -20,6 +20,7 @@ use gpui::{
executor, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, executor, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext,
Task, Task,
}; };
use language::File as _;
use language::{ use language::{
proto::{ proto::{
deserialize_fingerprint, deserialize_version, serialize_fingerprint, serialize_line_ending, deserialize_fingerprint, deserialize_version, serialize_fingerprint, serialize_line_ending,
@ -49,6 +50,7 @@ use std::{
time::{Duration, SystemTime}, time::{Duration, SystemTime},
}; };
use sum_tree::{Bias, Edit, SeekTarget, SumTree, TreeMap, TreeSet}; use sum_tree::{Bias, Edit, SeekTarget, SumTree, TreeMap, TreeSet};
use util::paths::HOME;
use util::{ResultExt, TryFutureExt}; use util::{ResultExt, TryFutureExt};
#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, PartialOrd, Ord)] #[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, &self,
buffer_handle: ModelHandle<Buffer>, buffer_handle: ModelHandle<Buffer>,
path: impl Into<Arc<Path>>, path: Arc<Path>,
has_changed_file: bool,
cx: &mut ModelContext<Worktree>, cx: &mut ModelContext<Worktree>,
) -> Task<Result<()>> { ) -> Task<Result<(clock::Global, RopeFingerprint, SystemTime)>> {
let handle = cx.handle();
let buffer = buffer_handle.read(cx); 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 text = buffer.as_rope().clone();
let fingerprint = text.fingerprint(); let fingerprint = text.fingerprint();
let version = buffer.version(); let version = buffer.version();
let save = self.write_file(path, text, buffer.line_ending(), cx); let save = self.write_file(path, text, buffer.line_ending(), cx);
let handle = cx.handle();
cx.as_mut().spawn(|mut cx| async move { cx.as_mut().spawn(|mut cx| async move {
let entry = save.await?; let entry = save.await?;
let file = File {
entry_id: entry.id, if has_changed_file {
worktree: handle, let new_file = Arc::new(File {
path: entry.path, entry_id: entry.id,
mtime: entry.mtime, worktree: handle,
is_local: true, path: entry.path,
is_deleted: false, 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_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; self.disconnected = true;
} }
pub fn save_buffer(
&self,
buffer_handle: ModelHandle<Buffer>,
cx: &mut ModelContext<Worktree>,
) -> Task<Result<(clock::Global, RopeFingerprint, SystemTime)>> {
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) { pub fn update_from_remote(&mut self, update: proto::UpdateWorktree) {
if let Some(updates_tx) = &self.updates_tx { if let Some(updates_tx) = &self.updates_tx {
updates_tx updates_tx
@ -1831,9 +1901,9 @@ impl language::File for File {
} else { } else {
let path = worktree.abs_path(); let path = worktree.abs_path();
if worktree.is_local() && path.starts_with(cx.global::<HomeDir>().as_path()) { if worktree.is_local() && path.starts_with(HOME.as_path()) {
full_path.push("~"); full_path.push("~");
full_path.push(path.strip_prefix(cx.global::<HomeDir>().as_path()).unwrap()); full_path.push(path.strip_prefix(HOME.as_path()).unwrap());
} else { } else {
full_path.push(path) full_path.push(path)
} }
@ -1858,57 +1928,6 @@ impl language::File for File {
self.is_deleted self.is_deleted
} }
fn save(
&self,
buffer_id: u64,
text: Rope,
version: clock::Global,
line_ending: LineEnding,
cx: &mut MutableAppContext,
) -> Task<Result<(clock::Global, RopeFingerprint, SystemTime)>> {
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 { fn as_any(&self) -> &dyn Any {
self self
} }

View file

@ -119,6 +119,7 @@ actions!(
AddFile, AddFile,
Copy, Copy,
CopyPath, CopyPath,
RevealInFinder,
Cut, Cut,
Paste, Paste,
Delete, Delete,
@ -147,6 +148,7 @@ pub fn init(cx: &mut MutableAppContext) {
cx.add_action(ProjectPanel::cancel); cx.add_action(ProjectPanel::cancel);
cx.add_action(ProjectPanel::copy); cx.add_action(ProjectPanel::copy);
cx.add_action(ProjectPanel::copy_path); cx.add_action(ProjectPanel::copy_path);
cx.add_action(ProjectPanel::reveal_in_finder);
cx.add_action(ProjectPanel::cut); cx.add_action(ProjectPanel::cut);
cx.add_action( cx.add_action(
|this: &mut ProjectPanel, action: &Paste, cx: &mut ViewContext<ProjectPanel>| { |this: &mut ProjectPanel, action: &Paste, cx: &mut ViewContext<ProjectPanel>| {
@ -305,6 +307,7 @@ impl ProjectPanel {
} }
menu_entries.push(ContextMenuItem::item("New File", AddFile)); menu_entries.push(ContextMenuItem::item("New File", AddFile));
menu_entries.push(ContextMenuItem::item("New Folder", AddDirectory)); 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::Separator);
menu_entries.push(ContextMenuItem::item("Copy", Copy)); menu_entries.push(ContextMenuItem::item("Copy", Copy));
menu_entries.push(ContextMenuItem::item("Copy Path", CopyPath)); menu_entries.push(ContextMenuItem::item("Copy Path", CopyPath));
@ -787,6 +790,12 @@ impl ProjectPanel {
} }
} }
fn reveal_in_finder(&mut self, _: &RevealInFinder, cx: &mut ViewContext<Self>) {
if let Some((worktree, entry)) = self.selected_entry(cx) {
cx.reveal_path(&worktree.abs_path().join(&entry.path));
}
}
fn move_entry( fn move_entry(
&mut self, &mut self,
&MoveProjectEntry { &MoveProjectEntry {

View file

@ -11,9 +11,12 @@ use highlighted_workspace_location::HighlightedWorkspaceLocation;
use ordered_float::OrderedFloat; use ordered_float::OrderedFloat;
use picker::{Picker, PickerDelegate}; use picker::{Picker, PickerDelegate};
use settings::Settings; 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) { pub fn init(cx: &mut MutableAppContext) {
cx.add_action(RecentProjectsView::toggle); cx.add_action(RecentProjectsView::toggle);
@ -40,9 +43,9 @@ impl RecentProjectsView {
} }
} }
fn toggle(_: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) { fn toggle(_: &mut Workspace, _: &OpenRecent, cx: &mut ViewContext<Workspace>) {
cx.spawn(|workspace, mut cx| async move { cx.spawn(|workspace, mut cx| async move {
let workspace_locations = cx let workspace_locations: Vec<_> = cx
.background() .background()
.spawn(async { .spawn(async {
WORKSPACE_DB WORKSPACE_DB
@ -56,12 +59,20 @@ impl RecentProjectsView {
.await; .await;
workspace.update(&mut cx, |workspace, cx| { workspace.update(&mut cx, |workspace, cx| {
workspace.toggle_modal(cx, |_, cx| { if !workspace_locations.is_empty() {
let view = cx.add_view(|cx| Self::new(workspace_locations, cx)); workspace.toggle_modal(cx, |_, cx| {
cx.subscribe(&view, Self::on_event).detach(); let view = cx.add_view(|cx| Self::new(workspace_locations, cx));
view 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(); .detach();
} }

View file

@ -9,7 +9,7 @@ use std::fmt;
use std::{ use std::{
cmp, cmp,
fmt::Debug, fmt::Debug,
io, iter, mem, io, iter,
time::{Duration, SystemTime, UNIX_EPOCH}, time::{Duration, SystemTime, UNIX_EPOCH},
}; };
@ -489,16 +489,26 @@ pub fn split_worktree_update(
return None; return None;
} }
let chunk_size = cmp::min(message.updated_entries.len(), max_chunk_size); let updated_entries_chunk_size = cmp::min(message.updated_entries.len(), max_chunk_size);
let updated_entries = message.updated_entries.drain(..chunk_size).collect(); let updated_entries = message
done = message.updated_entries.is_empty(); .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 { Some(UpdateWorktree {
project_id: message.project_id, project_id: message.project_id,
worktree_id: message.worktree_id, worktree_id: message.worktree_id,
root_name: message.root_name.clone(), root_name: message.root_name.clone(),
abs_path: message.abs_path.clone(), abs_path: message.abs_path.clone(),
updated_entries, updated_entries,
removed_entries: mem::take(&mut message.removed_entries), removed_entries,
scan_id: message.scan_id, scan_id: message.scan_id,
is_last_update: done && message.is_last_update, is_last_update: done && message.is_last_update,
}) })

Some files were not shown because too many files have changed in this diff Show more