Compare commits
11 Commits
v0.87.2-pr
...
v0.85.2-pr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2fcf186b17 | ||
|
|
5faceaf659 | ||
|
|
8ff3f6569d | ||
|
|
41ef9cc279 | ||
|
|
08e5e5a43d | ||
|
|
96dffc06e7 | ||
|
|
dc8f348cdb | ||
|
|
fcbdfe849f | ||
|
|
e23ca8d20e | ||
|
|
3407b16ec8 | ||
|
|
cc62945c6b |
5
.github/pull_request_template.md
vendored
5
.github/pull_request_template.md
vendored
@@ -1,5 +0,0 @@
|
||||
[[PR Description]]
|
||||
|
||||
Release Notes:
|
||||
|
||||
* [[Added foo / Fixed bar / No notes]]
|
||||
54
.github/workflows/build_dmg.yml
vendored
54
.github/workflows/build_dmg.yml
vendored
@@ -1,54 +0,0 @@
|
||||
name: Build Zed.dmg
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- "v[0-9]+.[0-9]+.x"
|
||||
pull_request:
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash -euxo pipefail {0}
|
||||
|
||||
concurrency:
|
||||
# Allow only one workflow per any non-`main` branch.
|
||||
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name == 'main' && github.sha || 'anysha' }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
RUST_BACKTRACE: 1
|
||||
COPT: '-Werror'
|
||||
|
||||
jobs:
|
||||
build-dmg:
|
||||
if: github.ref_name == 'main' || contains(github.event.pull_request.labels.*.name, 'run-build-dmg')
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- test
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
clean: false
|
||||
submodules: 'recursive'
|
||||
|
||||
- name: Install Rust
|
||||
run: |
|
||||
rustup set profile minimal
|
||||
rustup update stable
|
||||
|
||||
- name: Install node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
|
||||
- name: Build dmg bundle
|
||||
run: ./script/bundle
|
||||
|
||||
- name: Upload the build artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: Zed_${{ github.event.pull_request.head.sha || github.sha }}.dmg
|
||||
path: ./target/release/Zed.dmg
|
||||
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
@@ -62,9 +62,6 @@ jobs:
|
||||
clean: false
|
||||
submodules: 'recursive'
|
||||
|
||||
- name: Limit target directory size
|
||||
run: script/clear-target-dir-if-larger-than 70
|
||||
|
||||
- name: Run check
|
||||
run: cargo check --workspace
|
||||
|
||||
@@ -113,9 +110,6 @@ jobs:
|
||||
clean: false
|
||||
submodules: 'recursive'
|
||||
|
||||
- name: Limit target directory size
|
||||
run: script/clear-target-dir-if-larger-than 70
|
||||
|
||||
- name: Determine version and release channel
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||
run: |
|
||||
|
||||
2
.github/workflows/release_actions.yml
vendored
2
.github/workflows/release_actions.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
||||
content: |
|
||||
📣 Zed ${{ github.event.release.tag_name }} was just released!
|
||||
|
||||
Restart your Zed or head to https://zed.dev/releases/stable/latest to grab it.
|
||||
Restart your Zed or head to https://zed.dev/releases/latest to grab it.
|
||||
|
||||
```md
|
||||
# Changelog
|
||||
|
||||
40
Cargo.lock
generated
40
Cargo.lock
generated
@@ -1097,7 +1097,6 @@ dependencies = [
|
||||
"plist",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1190,7 +1189,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "collab"
|
||||
version = "0.12.2"
|
||||
version = "0.10.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-tungstenite",
|
||||
@@ -1962,6 +1961,12 @@ version = "1.0.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4f94fa09c2aeea5b8839e414b7b841bf429fd25b9c522116ac97ee87856d88b2"
|
||||
|
||||
[[package]]
|
||||
name = "easy-parallel"
|
||||
version = "3.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6907e25393cdcc1f4f3f513d9aac1e840eb1cc341a0fccb01171f7d14d10b946"
|
||||
|
||||
[[package]]
|
||||
name = "editor"
|
||||
version = "0.1.0"
|
||||
@@ -2186,7 +2191,6 @@ dependencies = [
|
||||
"project",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"text",
|
||||
"theme",
|
||||
"util",
|
||||
"workspace",
|
||||
@@ -2352,7 +2356,6 @@ dependencies = [
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"smol",
|
||||
"sum_tree",
|
||||
"tempfile",
|
||||
"util",
|
||||
]
|
||||
@@ -2676,7 +2679,6 @@ dependencies = [
|
||||
"postage",
|
||||
"settings",
|
||||
"text",
|
||||
"util",
|
||||
"workspace",
|
||||
]
|
||||
|
||||
@@ -4720,11 +4722,9 @@ dependencies = [
|
||||
"futures 0.3.25",
|
||||
"fuzzy",
|
||||
"git",
|
||||
"git2",
|
||||
"globset",
|
||||
"glob",
|
||||
"gpui",
|
||||
"ignore",
|
||||
"itertools",
|
||||
"language",
|
||||
"lazy_static",
|
||||
"log",
|
||||
@@ -5777,7 +5777,6 @@ dependencies = [
|
||||
"collections",
|
||||
"editor",
|
||||
"futures 0.3.25",
|
||||
"globset",
|
||||
"gpui",
|
||||
"language",
|
||||
"log",
|
||||
@@ -5895,6 +5894,15 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_path_to_error"
|
||||
version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "184c643044780f7ceb59104cef98a5a6f12cb2288a7bc701ab93a362b49fd47d"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_repr"
|
||||
version = "0.1.9"
|
||||
@@ -5970,6 +5978,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"serde_path_to_error",
|
||||
"sqlez",
|
||||
"staff_mode",
|
||||
"theme",
|
||||
@@ -6540,12 +6549,6 @@ dependencies = [
|
||||
"winx",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "take-until"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8bdb6fa0dfa67b38c1e66b7041ba9dcf23b99d8121907cd31c807a332f7a0bbb"
|
||||
|
||||
[[package]]
|
||||
name = "target-lexicon"
|
||||
version = "0.12.5"
|
||||
@@ -6691,13 +6694,13 @@ name = "theme"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"fs",
|
||||
"gpui",
|
||||
"indexmap",
|
||||
"parking_lot 0.11.2",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"serde_path_to_error",
|
||||
"toml",
|
||||
]
|
||||
|
||||
@@ -7606,7 +7609,6 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"smol",
|
||||
"take-until",
|
||||
"tempdir",
|
||||
"url",
|
||||
]
|
||||
@@ -8544,7 +8546,7 @@ checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"
|
||||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.87.2"
|
||||
version = "0.85.2"
|
||||
dependencies = [
|
||||
"activity_indicator",
|
||||
"anyhow",
|
||||
@@ -8570,6 +8572,7 @@ dependencies = [
|
||||
"ctor",
|
||||
"db",
|
||||
"diagnostics",
|
||||
"easy-parallel",
|
||||
"editor",
|
||||
"env_logger",
|
||||
"feedback",
|
||||
@@ -8612,6 +8615,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"serde_path_to_error",
|
||||
"settings",
|
||||
"simplelog",
|
||||
"smallvec",
|
||||
|
||||
@@ -77,8 +77,7 @@ async-trait = { version = "0.1" }
|
||||
ctor = { version = "0.1" }
|
||||
env_logger = { version = "0.9" }
|
||||
futures = { version = "0.3" }
|
||||
glob = { version = "0.3" }
|
||||
globset = { version = "0.4" }
|
||||
glob = { version = "0.3.1" }
|
||||
lazy_static = { version = "1.4.0" }
|
||||
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
|
||||
ordered-float = { version = "2.1.1" }
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4.75 1.875C4.75 2.71406 4.19922 3.42422 3.4375 3.66328V5.97891C3.9086 5.64609 4.4711 5.4375 5.125 5.4375H7.375C8.30782 5.4375 9.0625 4.68281 9.0625 3.75V3.66328C8.30078 3.42422 7.75 2.71406 7.75 1.875C7.75 0.839531 8.58907 0 9.625 0C10.6609 0 11.5 0.839531 11.5 1.875C11.5 2.71406 10.9492 3.42422 10.1875 3.66328V3.75C10.1875 5.30391 8.92891 6.5625 7.375 6.5625H5.125C4.19219 6.5625 3.4375 7.31719 3.4375 8.25V8.33672C4.19922 8.57578 4.75 9.28594 4.75 10.125C4.75 11.1609 3.91094 12 2.875 12C1.83953 12 1 11.1609 1 10.125C1 9.28594 1.55172 8.57578 2.3125 8.33672V3.66328C1.55172 3.42422 1 2.71406 1 1.875C1 0.839531 1.83953 0 2.875 0C3.91094 0 4.75 0.839531 4.75 1.875ZM2.875 2.625C3.28914 2.625 3.625 2.28914 3.625 1.875C3.625 1.46086 3.28914 1.125 2.875 1.125C2.46086 1.125 2.125 1.46086 2.125 1.875C2.125 2.28914 2.46086 2.625 2.875 2.625ZM9.625 1.125C9.21016 1.125 8.875 1.46086 8.875 1.875C8.875 2.28914 9.21016 2.625 9.625 2.625C10.0398 2.625 10.375 2.28914 10.375 1.875C10.375 1.46086 10.0398 1.125 9.625 1.125ZM2.875 10.875C3.28914 10.875 3.625 10.5398 3.625 10.125C3.625 9.71016 3.28914 9.375 2.875 9.375C2.46086 9.375 2.125 9.71016 2.125 10.125C2.125 10.5398 2.46086 10.875 2.875 10.875Z" fill="white"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB |
@@ -191,7 +191,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "BufferSearchBar",
|
||||
"context": "BufferSearchBar > Editor",
|
||||
"bindings": {
|
||||
"escape": "buffer_search::Dismiss",
|
||||
"tab": "buffer_search::FocusEditor",
|
||||
@@ -199,18 +199,6 @@
|
||||
"shift-enter": "search::SelectPrevMatch"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ProjectSearchBar",
|
||||
"bindings": {
|
||||
"escape": "project_search::ToggleFocus"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ProjectSearchView",
|
||||
"bindings": {
|
||||
"escape": "project_search::ToggleFocus"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Pane",
|
||||
"bindings": {
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
"ctrl->": "zed::IncreaseBufferFontSize",
|
||||
"ctrl-<": "zed::DecreaseBufferFontSize",
|
||||
"cmd-d": "editor::DuplicateLine",
|
||||
"cmd-backspace": "editor::DeleteLine",
|
||||
"cmd-pagedown": "editor::MovePageDown",
|
||||
"cmd-pageup": "editor::MovePageUp",
|
||||
"ctrl-alt-shift-b": "editor::SelectToPreviousWordStart",
|
||||
@@ -34,7 +33,6 @@
|
||||
],
|
||||
"shift-alt-up": "editor::MoveLineUp",
|
||||
"shift-alt-down": "editor::MoveLineDown",
|
||||
"cmd-alt-l": "editor::Format",
|
||||
"cmd-[": "pane::GoBack",
|
||||
"cmd-]": "pane::GoForward",
|
||||
"alt-f7": "editor::FindAllReferences",
|
||||
@@ -65,7 +63,6 @@
|
||||
{
|
||||
"context": "Workspace",
|
||||
"bindings": {
|
||||
"cmd-shift-o": "file_finder::Toggle",
|
||||
"cmd-shift-a": "command_palette::Toggle",
|
||||
"cmd-alt-o": "project_symbols::Toggle",
|
||||
"cmd-1": "workspace::ToggleLeftSidebar",
|
||||
|
||||
@@ -33,29 +33,6 @@
|
||||
// Controls whether copilot provides suggestion immediately
|
||||
// or waits for a `copilot::Toggle`
|
||||
"show_copilot_suggestions": true,
|
||||
// Whether to show tabs and spaces in the editor.
|
||||
// This setting can take two values:
|
||||
//
|
||||
// 1. Draw tabs and spaces only for the selected text (default):
|
||||
// "selection"
|
||||
// 2. Do not draw any tabs or spaces:
|
||||
// "none"
|
||||
// 3. Draw all invisible symbols:
|
||||
// "all"
|
||||
"show_whitespaces": "selection",
|
||||
// Whether to show the scrollbar in the editor.
|
||||
// This setting can take four values:
|
||||
//
|
||||
// 1. Show the scrollbar if there's important information or
|
||||
// follow the system's configured behavior (default):
|
||||
// "auto"
|
||||
// 2. Match the system's configured behavior:
|
||||
// "system"
|
||||
// 3. Always show the scrollbar:
|
||||
// "always"
|
||||
// 4. Never show the scrollbar:
|
||||
// "never"
|
||||
"show_scrollbars": "auto",
|
||||
// Whether the screen sharing icon is shown in the os status bar.
|
||||
"show_call_status_icon": true,
|
||||
// Whether to use language servers to provide code intelligence.
|
||||
|
||||
@@ -102,7 +102,7 @@ fn view_release_notes(_: &ViewReleaseNotes, cx: &mut AppContext) {
|
||||
{
|
||||
format!("{server_url}/releases/preview/latest")
|
||||
} else {
|
||||
format!("{server_url}/releases/stable/latest")
|
||||
format!("{server_url}/releases/latest")
|
||||
};
|
||||
cx.platform().open_url(&latest_release_url);
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ dirs = "3.0"
|
||||
ipc-channel = "0.16"
|
||||
serde.workspace = true
|
||||
serde_derive.workspace = true
|
||||
util = { path = "../util" }
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
core-foundation = "0.9"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
pub use ipc_channel::ipc;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct IpcHandshake {
|
||||
@@ -9,12 +10,7 @@ pub struct IpcHandshake {
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub enum CliRequest {
|
||||
// The filed is named `path` for compatibility, but now CLI can request
|
||||
// opening a path at a certain row and/or column: `some/path:123` and `some/path:123:456`.
|
||||
//
|
||||
// Since Zed CLI has to be installed separately, there can be situations when old CLI is
|
||||
// querying new Zed editors, support both formats by using `String` here and parsing it on Zed side later.
|
||||
Open { paths: Vec<String>, wait: bool },
|
||||
Open { paths: Vec<PathBuf>, wait: bool },
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
@@ -24,7 +20,3 @@ pub enum CliResponse {
|
||||
Stderr { message: String },
|
||||
Exit { status: i32 },
|
||||
}
|
||||
|
||||
/// When Zed started not as an *.app but as a binary (e.g. local development),
|
||||
/// there's a possibility to tell it to behave "regularly".
|
||||
pub const FORCE_CLI_MODE_ENV_VAR_NAME: &str = "ZED_FORCE_CLI_MODE";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use anyhow::{anyhow, Result};
|
||||
use clap::Parser;
|
||||
use cli::{CliRequest, CliResponse, IpcHandshake, FORCE_CLI_MODE_ENV_VAR_NAME};
|
||||
use cli::{CliRequest, CliResponse, IpcHandshake};
|
||||
use core_foundation::{
|
||||
array::{CFArray, CFIndex},
|
||||
string::kCFStringEncodingUTF8,
|
||||
@@ -16,20 +16,16 @@ use std::{
|
||||
path::{Path, PathBuf},
|
||||
ptr,
|
||||
};
|
||||
use util::paths::PathLikeWithPosition;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[clap(name = "zed", global_setting(clap::AppSettings::NoAutoVersion))]
|
||||
struct Args {
|
||||
/// Wait for all of the given paths to be opened/closed before exiting.
|
||||
/// Wait for all of the given paths to be closed before exiting.
|
||||
#[clap(short, long)]
|
||||
wait: bool,
|
||||
/// A sequence of space-separated paths that you want to open.
|
||||
///
|
||||
/// Use `path:line:row` syntax to open a file at a specific location.
|
||||
/// Non-existing paths and directories will ignore `:line:row` suffix.
|
||||
#[clap(value_parser = parse_path_with_position)]
|
||||
paths_with_position: Vec<PathLikeWithPosition<PathBuf>>,
|
||||
#[clap()]
|
||||
paths: Vec<PathBuf>,
|
||||
/// Print Zed's version and the app path.
|
||||
#[clap(short, long)]
|
||||
version: bool,
|
||||
@@ -38,14 +34,6 @@ struct Args {
|
||||
bundle_path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
fn parse_path_with_position(
|
||||
argument_str: &str,
|
||||
) -> Result<PathLikeWithPosition<PathBuf>, std::convert::Infallible> {
|
||||
PathLikeWithPosition::parse_str(argument_str, |path_str| {
|
||||
Ok(Path::new(path_str).to_path_buf())
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct InfoPlist {
|
||||
#[serde(rename = "CFBundleShortVersionString")]
|
||||
@@ -55,37 +43,37 @@ struct InfoPlist {
|
||||
fn main() -> Result<()> {
|
||||
let args = Args::parse();
|
||||
|
||||
let bundle = Bundle::detect(args.bundle_path.as_deref()).context("Bundle detection")?;
|
||||
let bundle_path = if let Some(bundle_path) = args.bundle_path {
|
||||
bundle_path.canonicalize()?
|
||||
} else {
|
||||
locate_bundle()?
|
||||
};
|
||||
|
||||
if args.version {
|
||||
println!("{}", bundle.zed_version_string());
|
||||
let plist_path = bundle_path.join("Contents/Info.plist");
|
||||
let plist = plist::from_file::<_, InfoPlist>(plist_path)?;
|
||||
println!(
|
||||
"Zed {} – {}",
|
||||
plist.bundle_short_version_string,
|
||||
bundle_path.to_string_lossy()
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
for path in args
|
||||
.paths_with_position
|
||||
.iter()
|
||||
.map(|path_with_position| &path_with_position.path_like)
|
||||
{
|
||||
for path in args.paths.iter() {
|
||||
if !path.exists() {
|
||||
touch(path.as_path())?;
|
||||
}
|
||||
}
|
||||
|
||||
let (tx, rx) = bundle.launch()?;
|
||||
let (tx, rx) = launch_app(bundle_path)?;
|
||||
|
||||
tx.send(CliRequest::Open {
|
||||
paths: args
|
||||
.paths_with_position
|
||||
.paths
|
||||
.into_iter()
|
||||
.map(|path_with_position| {
|
||||
let path_with_position = path_with_position.map_path_like(|path| {
|
||||
fs::canonicalize(&path)
|
||||
.with_context(|| format!("path {path:?} canonicalization"))
|
||||
})?;
|
||||
Ok(path_with_position.to_string(|path| path.display().to_string()))
|
||||
})
|
||||
.collect::<Result<_>>()?,
|
||||
.map(|path| fs::canonicalize(path).map_err(|error| anyhow!(error)))
|
||||
.collect::<Result<Vec<PathBuf>>>()?,
|
||||
wait: args.wait,
|
||||
})?;
|
||||
|
||||
@@ -101,148 +89,6 @@ fn main() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
enum Bundle {
|
||||
App {
|
||||
app_bundle: PathBuf,
|
||||
plist: InfoPlist,
|
||||
},
|
||||
LocalPath {
|
||||
executable: PathBuf,
|
||||
plist: InfoPlist,
|
||||
},
|
||||
}
|
||||
|
||||
impl Bundle {
|
||||
fn detect(args_bundle_path: Option<&Path>) -> anyhow::Result<Self> {
|
||||
let bundle_path = if let Some(bundle_path) = args_bundle_path {
|
||||
bundle_path
|
||||
.canonicalize()
|
||||
.with_context(|| format!("Args bundle path {bundle_path:?} canonicalization"))?
|
||||
} else {
|
||||
locate_bundle().context("bundle autodiscovery")?
|
||||
};
|
||||
|
||||
match bundle_path.extension().and_then(|ext| ext.to_str()) {
|
||||
Some("app") => {
|
||||
let plist_path = bundle_path.join("Contents/Info.plist");
|
||||
let plist = plist::from_file::<_, InfoPlist>(&plist_path).with_context(|| {
|
||||
format!("Reading *.app bundle plist file at {plist_path:?}")
|
||||
})?;
|
||||
Ok(Self::App {
|
||||
app_bundle: bundle_path,
|
||||
plist,
|
||||
})
|
||||
}
|
||||
_ => {
|
||||
println!("Bundle path {bundle_path:?} has no *.app extension, attempting to locate a dev build");
|
||||
let plist_path = bundle_path
|
||||
.parent()
|
||||
.with_context(|| format!("Bundle path {bundle_path:?} has no parent"))?
|
||||
.join("WebRTC.framework/Resources/Info.plist");
|
||||
let plist = plist::from_file::<_, InfoPlist>(&plist_path)
|
||||
.with_context(|| format!("Reading dev bundle plist file at {plist_path:?}"))?;
|
||||
Ok(Self::LocalPath {
|
||||
executable: bundle_path,
|
||||
plist,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn plist(&self) -> &InfoPlist {
|
||||
match self {
|
||||
Self::App { plist, .. } => plist,
|
||||
Self::LocalPath { plist, .. } => plist,
|
||||
}
|
||||
}
|
||||
|
||||
fn path(&self) -> &Path {
|
||||
match self {
|
||||
Self::App { app_bundle, .. } => app_bundle,
|
||||
Self::LocalPath {
|
||||
executable: excutable,
|
||||
..
|
||||
} => excutable,
|
||||
}
|
||||
}
|
||||
|
||||
fn launch(&self) -> anyhow::Result<(IpcSender<CliRequest>, IpcReceiver<CliResponse>)> {
|
||||
let (server, server_name) =
|
||||
IpcOneShotServer::<IpcHandshake>::new().context("Handshake before Zed spawn")?;
|
||||
let url = format!("zed-cli://{server_name}");
|
||||
|
||||
match self {
|
||||
Self::App { app_bundle, .. } => {
|
||||
let app_path = app_bundle;
|
||||
|
||||
let status = unsafe {
|
||||
let app_url = CFURL::from_path(app_path, true)
|
||||
.with_context(|| format!("invalid app path {app_path:?}"))?;
|
||||
let url_to_open = CFURL::wrap_under_create_rule(CFURLCreateWithBytes(
|
||||
ptr::null(),
|
||||
url.as_ptr(),
|
||||
url.len() as CFIndex,
|
||||
kCFStringEncodingUTF8,
|
||||
ptr::null(),
|
||||
));
|
||||
let urls_to_open = CFArray::from_copyable(&[url_to_open.as_concrete_TypeRef()]);
|
||||
LSOpenFromURLSpec(
|
||||
&LSLaunchURLSpec {
|
||||
appURL: app_url.as_concrete_TypeRef(),
|
||||
itemURLs: urls_to_open.as_concrete_TypeRef(),
|
||||
passThruParams: ptr::null(),
|
||||
launchFlags: kLSLaunchDefaults,
|
||||
asyncRefCon: ptr::null_mut(),
|
||||
},
|
||||
ptr::null_mut(),
|
||||
)
|
||||
};
|
||||
|
||||
anyhow::ensure!(
|
||||
status == 0,
|
||||
"cannot start app bundle {}",
|
||||
self.zed_version_string()
|
||||
);
|
||||
}
|
||||
Self::LocalPath { executable, .. } => {
|
||||
let executable_parent = executable
|
||||
.parent()
|
||||
.with_context(|| format!("Executable {executable:?} path has no parent"))?;
|
||||
let subprocess_stdout_file =
|
||||
fs::File::create(executable_parent.join("zed_dev.log"))
|
||||
.with_context(|| format!("Log file creation in {executable_parent:?}"))?;
|
||||
let subprocess_stdin_file =
|
||||
subprocess_stdout_file.try_clone().with_context(|| {
|
||||
format!("Cloning descriptor for file {subprocess_stdout_file:?}")
|
||||
})?;
|
||||
let mut command = std::process::Command::new(executable);
|
||||
let command = command
|
||||
.env(FORCE_CLI_MODE_ENV_VAR_NAME, "")
|
||||
.stderr(subprocess_stdout_file)
|
||||
.stdout(subprocess_stdin_file)
|
||||
.arg(url);
|
||||
|
||||
command
|
||||
.spawn()
|
||||
.with_context(|| format!("Spawning {command:?}"))?;
|
||||
}
|
||||
}
|
||||
|
||||
let (_, handshake) = server.accept().context("Handshake after Zed spawn")?;
|
||||
Ok((handshake.requests, handshake.responses))
|
||||
}
|
||||
|
||||
fn zed_version_string(&self) -> String {
|
||||
let is_dev = matches!(self, Self::LocalPath { .. });
|
||||
format!(
|
||||
"Zed {}{} – {}",
|
||||
self.plist().bundle_short_version_string,
|
||||
if is_dev { " (dev)" } else { "" },
|
||||
self.path().display(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn touch(path: &Path) -> io::Result<()> {
|
||||
match OpenOptions::new().create(true).write(true).open(path) {
|
||||
Ok(_) => Ok(()),
|
||||
@@ -260,3 +106,38 @@ fn locate_bundle() -> Result<PathBuf> {
|
||||
}
|
||||
Ok(app_path)
|
||||
}
|
||||
|
||||
fn launch_app(app_path: PathBuf) -> Result<(IpcSender<CliRequest>, IpcReceiver<CliResponse>)> {
|
||||
let (server, server_name) = IpcOneShotServer::<IpcHandshake>::new()?;
|
||||
let url = format!("zed-cli://{server_name}");
|
||||
|
||||
let status = unsafe {
|
||||
let app_url =
|
||||
CFURL::from_path(&app_path, true).ok_or_else(|| anyhow!("invalid app path"))?;
|
||||
let url_to_open = CFURL::wrap_under_create_rule(CFURLCreateWithBytes(
|
||||
ptr::null(),
|
||||
url.as_ptr(),
|
||||
url.len() as CFIndex,
|
||||
kCFStringEncodingUTF8,
|
||||
ptr::null(),
|
||||
));
|
||||
let urls_to_open = CFArray::from_copyable(&[url_to_open.as_concrete_TypeRef()]);
|
||||
LSOpenFromURLSpec(
|
||||
&LSLaunchURLSpec {
|
||||
appURL: app_url.as_concrete_TypeRef(),
|
||||
itemURLs: urls_to_open.as_concrete_TypeRef(),
|
||||
passThruParams: ptr::null(),
|
||||
launchFlags: kLSLaunchDefaults,
|
||||
asyncRefCon: ptr::null_mut(),
|
||||
},
|
||||
ptr::null_mut(),
|
||||
)
|
||||
};
|
||||
|
||||
if status == 0 {
|
||||
let (_, handshake) = server.accept()?;
|
||||
Ok((handshake.requests, handshake.responses))
|
||||
} else {
|
||||
Err(anyhow!("cannot start {:?}", app_path))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,11 +86,6 @@ pub enum ClickhouseEvent {
|
||||
copilot_enabled: bool,
|
||||
copilot_enabled_for_language: bool,
|
||||
},
|
||||
Copilot {
|
||||
suggestion_id: Option<String>,
|
||||
suggestion_accepted: bool,
|
||||
file_extension: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
|
||||
@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"]
|
||||
default-run = "collab"
|
||||
edition = "2021"
|
||||
name = "collab"
|
||||
version = "0.12.2"
|
||||
version = "0.10.0"
|
||||
publish = false
|
||||
|
||||
[[bin]]
|
||||
|
||||
@@ -82,37 +82,6 @@ CREATE TABLE "worktree_entries" (
|
||||
CREATE INDEX "index_worktree_entries_on_project_id" ON "worktree_entries" ("project_id");
|
||||
CREATE INDEX "index_worktree_entries_on_project_id_and_worktree_id" ON "worktree_entries" ("project_id", "worktree_id");
|
||||
|
||||
CREATE TABLE "worktree_repositories" (
|
||||
"project_id" INTEGER NOT NULL,
|
||||
"worktree_id" INTEGER NOT NULL,
|
||||
"work_directory_id" INTEGER NOT NULL,
|
||||
"branch" VARCHAR,
|
||||
"scan_id" INTEGER NOT NULL,
|
||||
"is_deleted" BOOL NOT NULL,
|
||||
PRIMARY KEY(project_id, worktree_id, work_directory_id),
|
||||
FOREIGN KEY(project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE,
|
||||
FOREIGN KEY(project_id, worktree_id, work_directory_id) REFERENCES worktree_entries (project_id, worktree_id, id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX "index_worktree_repositories_on_project_id" ON "worktree_repositories" ("project_id");
|
||||
CREATE INDEX "index_worktree_repositories_on_project_id_and_worktree_id" ON "worktree_repositories" ("project_id", "worktree_id");
|
||||
|
||||
CREATE TABLE "worktree_repository_statuses" (
|
||||
"project_id" INTEGER NOT NULL,
|
||||
"worktree_id" INTEGER NOT NULL,
|
||||
"work_directory_id" INTEGER NOT NULL,
|
||||
"repo_path" VARCHAR NOT NULL,
|
||||
"status" INTEGER NOT NULL,
|
||||
"scan_id" INTEGER NOT NULL,
|
||||
"is_deleted" BOOL NOT NULL,
|
||||
PRIMARY KEY(project_id, worktree_id, work_directory_id, repo_path),
|
||||
FOREIGN KEY(project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE,
|
||||
FOREIGN KEY(project_id, worktree_id, work_directory_id) REFERENCES worktree_entries (project_id, worktree_id, id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX "index_worktree_repository_statuses_on_project_id" ON "worktree_repository_statuses" ("project_id");
|
||||
CREATE INDEX "index_worktree_repository_statuses_on_project_id_and_worktree_id" ON "worktree_repository_statuses" ("project_id", "worktree_id");
|
||||
CREATE INDEX "index_worktree_repository_statuses_on_project_id_and_worktree_id_and_work_directory_id" ON "worktree_repository_statuses" ("project_id", "worktree_id", "work_directory_id");
|
||||
|
||||
|
||||
CREATE TABLE "worktree_diagnostic_summaries" (
|
||||
"project_id" INTEGER NOT NULL,
|
||||
"worktree_id" INTEGER NOT NULL,
|
||||
@@ -184,7 +153,7 @@ CREATE TABLE "followers" (
|
||||
"follower_connection_server_id" INTEGER NOT NULL REFERENCES servers (id) ON DELETE CASCADE,
|
||||
"follower_connection_id" INTEGER NOT NULL
|
||||
);
|
||||
CREATE UNIQUE INDEX
|
||||
CREATE UNIQUE INDEX
|
||||
"index_followers_on_project_id_and_leader_connection_server_id_and_leader_connection_id_and_follower_connection_server_id_and_follower_connection_id"
|
||||
ON "followers" ("project_id", "leader_connection_server_id", "leader_connection_id", "follower_connection_server_id", "follower_connection_id");
|
||||
CREATE INDEX "index_followers_on_room_id" ON "followers" ("room_id");
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
CREATE TABLE "worktree_repositories" (
|
||||
"project_id" INTEGER NOT NULL,
|
||||
"worktree_id" INT8 NOT NULL,
|
||||
"work_directory_id" INT8 NOT NULL,
|
||||
"scan_id" INT8 NOT NULL,
|
||||
"branch" VARCHAR,
|
||||
"is_deleted" BOOL NOT NULL,
|
||||
PRIMARY KEY(project_id, worktree_id, work_directory_id),
|
||||
FOREIGN KEY(project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE,
|
||||
FOREIGN KEY(project_id, worktree_id, work_directory_id) REFERENCES worktree_entries (project_id, worktree_id, id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX "index_worktree_repositories_on_project_id" ON "worktree_repositories" ("project_id");
|
||||
CREATE INDEX "index_worktree_repositories_on_project_id_and_worktree_id" ON "worktree_repositories" ("project_id", "worktree_id");
|
||||
@@ -1,15 +0,0 @@
|
||||
CREATE TABLE "worktree_repository_statuses" (
|
||||
"project_id" INTEGER NOT NULL,
|
||||
"worktree_id" INT8 NOT NULL,
|
||||
"work_directory_id" INT8 NOT NULL,
|
||||
"repo_path" VARCHAR NOT NULL,
|
||||
"status" INT8 NOT NULL,
|
||||
"scan_id" INT8 NOT NULL,
|
||||
"is_deleted" BOOL NOT NULL,
|
||||
PRIMARY KEY(project_id, worktree_id, work_directory_id, repo_path),
|
||||
FOREIGN KEY(project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE,
|
||||
FOREIGN KEY(project_id, worktree_id, work_directory_id) REFERENCES worktree_entries (project_id, worktree_id, id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX "index_wt_repos_statuses_on_project_id" ON "worktree_repository_statuses" ("project_id");
|
||||
CREATE INDEX "index_wt_repos_statuses_on_project_id_and_wt_id" ON "worktree_repository_statuses" ("project_id", "worktree_id");
|
||||
CREATE INDEX "index_wt_repos_statuses_on_project_id_and_wt_id_and_wd_id" ON "worktree_repository_statuses" ("project_id", "worktree_id", "work_directory_id");
|
||||
@@ -14,8 +14,6 @@ mod user;
|
||||
mod worktree;
|
||||
mod worktree_diagnostic_summary;
|
||||
mod worktree_entry;
|
||||
mod worktree_repository;
|
||||
mod worktree_repository_statuses;
|
||||
|
||||
use crate::executor::Executor;
|
||||
use crate::{Error, Result};
|
||||
@@ -1491,8 +1489,6 @@ impl Database {
|
||||
visible: db_worktree.visible,
|
||||
updated_entries: Default::default(),
|
||||
removed_entries: Default::default(),
|
||||
updated_repositories: Default::default(),
|
||||
removed_repositories: Default::default(),
|
||||
diagnostic_summaries: Default::default(),
|
||||
scan_id: db_worktree.scan_id as u64,
|
||||
completed_scan_id: db_worktree.completed_scan_id as u64,
|
||||
@@ -1502,119 +1498,38 @@ impl Database {
|
||||
.worktrees
|
||||
.iter()
|
||||
.find(|worktree| worktree.id == db_worktree.id as u64);
|
||||
let entry_filter = if let Some(rejoined_worktree) = rejoined_worktree {
|
||||
worktree_entry::Column::ScanId.gt(rejoined_worktree.scan_id)
|
||||
} else {
|
||||
worktree_entry::Column::IsDeleted.eq(false)
|
||||
};
|
||||
|
||||
// File entries
|
||||
{
|
||||
let entry_filter = if let Some(rejoined_worktree) = rejoined_worktree {
|
||||
worktree_entry::Column::ScanId.gt(rejoined_worktree.scan_id)
|
||||
let mut db_entries = worktree_entry::Entity::find()
|
||||
.filter(
|
||||
Condition::all()
|
||||
.add(worktree_entry::Column::WorktreeId.eq(worktree.id))
|
||||
.add(entry_filter),
|
||||
)
|
||||
.stream(&*tx)
|
||||
.await?;
|
||||
|
||||
while let Some(db_entry) = db_entries.next().await {
|
||||
let db_entry = db_entry?;
|
||||
if db_entry.is_deleted {
|
||||
worktree.removed_entries.push(db_entry.id as u64);
|
||||
} else {
|
||||
worktree_entry::Column::IsDeleted.eq(false)
|
||||
};
|
||||
|
||||
let mut db_entries = worktree_entry::Entity::find()
|
||||
.filter(
|
||||
Condition::all()
|
||||
.add(worktree_entry::Column::WorktreeId.eq(worktree.id))
|
||||
.add(entry_filter),
|
||||
)
|
||||
.stream(&*tx)
|
||||
.await?;
|
||||
|
||||
while let Some(db_entry) = db_entries.next().await {
|
||||
let db_entry = db_entry?;
|
||||
if db_entry.is_deleted {
|
||||
worktree.removed_entries.push(db_entry.id as u64);
|
||||
} else {
|
||||
worktree.updated_entries.push(proto::Entry {
|
||||
id: db_entry.id as u64,
|
||||
is_dir: db_entry.is_dir,
|
||||
path: db_entry.path,
|
||||
inode: db_entry.inode as u64,
|
||||
mtime: Some(proto::Timestamp {
|
||||
seconds: db_entry.mtime_seconds as u64,
|
||||
nanos: db_entry.mtime_nanos as u32,
|
||||
}),
|
||||
is_symlink: db_entry.is_symlink,
|
||||
is_ignored: db_entry.is_ignored,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Repository Entries
|
||||
{
|
||||
let repository_entry_filter =
|
||||
if let Some(rejoined_worktree) = rejoined_worktree {
|
||||
worktree_repository::Column::ScanId.gt(rejoined_worktree.scan_id)
|
||||
} else {
|
||||
worktree_repository::Column::IsDeleted.eq(false)
|
||||
};
|
||||
|
||||
let mut db_repositories = worktree_repository::Entity::find()
|
||||
.filter(
|
||||
Condition::all()
|
||||
.add(worktree_repository::Column::WorktreeId.eq(worktree.id))
|
||||
.add(repository_entry_filter),
|
||||
)
|
||||
.stream(&*tx)
|
||||
.await?;
|
||||
|
||||
while let Some(db_repository) = db_repositories.next().await {
|
||||
let db_repository = db_repository?;
|
||||
if db_repository.is_deleted {
|
||||
worktree
|
||||
.removed_repositories
|
||||
.push(db_repository.work_directory_id as u64);
|
||||
} else {
|
||||
worktree.updated_repositories.push(proto::RepositoryEntry {
|
||||
work_directory_id: db_repository.work_directory_id as u64,
|
||||
branch: db_repository.branch,
|
||||
removed_repo_paths: Default::default(),
|
||||
updated_statuses: Default::default(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Repository Status Entries
|
||||
for repository in worktree.updated_repositories.iter_mut() {
|
||||
let repository_status_entry_filter =
|
||||
if let Some(rejoined_worktree) = rejoined_worktree {
|
||||
worktree_repository_statuses::Column::ScanId
|
||||
.gt(rejoined_worktree.scan_id)
|
||||
} else {
|
||||
worktree_repository_statuses::Column::IsDeleted.eq(false)
|
||||
};
|
||||
|
||||
let mut db_repository_statuses =
|
||||
worktree_repository_statuses::Entity::find()
|
||||
.filter(
|
||||
Condition::all()
|
||||
.add(
|
||||
worktree_repository_statuses::Column::WorktreeId
|
||||
.eq(worktree.id),
|
||||
)
|
||||
.add(
|
||||
worktree_repository_statuses::Column::WorkDirectoryId
|
||||
.eq(repository.work_directory_id),
|
||||
)
|
||||
.add(repository_status_entry_filter),
|
||||
)
|
||||
.stream(&*tx)
|
||||
.await?;
|
||||
|
||||
while let Some(db_status_entry) = db_repository_statuses.next().await {
|
||||
let db_status_entry = db_status_entry?;
|
||||
if db_status_entry.is_deleted {
|
||||
repository
|
||||
.removed_repo_paths
|
||||
.push(db_status_entry.repo_path);
|
||||
} else {
|
||||
repository.updated_statuses.push(proto::StatusEntry {
|
||||
repo_path: db_status_entry.repo_path,
|
||||
status: db_status_entry.status as i32,
|
||||
});
|
||||
}
|
||||
worktree.updated_entries.push(proto::Entry {
|
||||
id: db_entry.id as u64,
|
||||
is_dir: db_entry.is_dir,
|
||||
path: db_entry.path,
|
||||
inode: db_entry.inode as u64,
|
||||
mtime: Some(proto::Timestamp {
|
||||
seconds: db_entry.mtime_seconds as u64,
|
||||
nanos: db_entry.mtime_nanos as u32,
|
||||
}),
|
||||
is_symlink: db_entry.is_symlink,
|
||||
is_ignored: db_entry.is_ignored,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2415,115 +2330,6 @@ impl Database {
|
||||
.await?;
|
||||
}
|
||||
|
||||
if !update.updated_repositories.is_empty() {
|
||||
worktree_repository::Entity::insert_many(update.updated_repositories.iter().map(
|
||||
|repository| worktree_repository::ActiveModel {
|
||||
project_id: ActiveValue::set(project_id),
|
||||
worktree_id: ActiveValue::set(worktree_id),
|
||||
work_directory_id: ActiveValue::set(repository.work_directory_id as i64),
|
||||
scan_id: ActiveValue::set(update.scan_id as i64),
|
||||
branch: ActiveValue::set(repository.branch.clone()),
|
||||
is_deleted: ActiveValue::set(false),
|
||||
},
|
||||
))
|
||||
.on_conflict(
|
||||
OnConflict::columns([
|
||||
worktree_repository::Column::ProjectId,
|
||||
worktree_repository::Column::WorktreeId,
|
||||
worktree_repository::Column::WorkDirectoryId,
|
||||
])
|
||||
.update_columns([
|
||||
worktree_repository::Column::ScanId,
|
||||
worktree_repository::Column::Branch,
|
||||
])
|
||||
.to_owned(),
|
||||
)
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
|
||||
for repository in update.updated_repositories.iter() {
|
||||
if !repository.updated_statuses.is_empty() {
|
||||
worktree_repository_statuses::Entity::insert_many(
|
||||
repository.updated_statuses.iter().map(|status_entry| {
|
||||
worktree_repository_statuses::ActiveModel {
|
||||
project_id: ActiveValue::set(project_id),
|
||||
worktree_id: ActiveValue::set(worktree_id),
|
||||
work_directory_id: ActiveValue::set(
|
||||
repository.work_directory_id as i64,
|
||||
),
|
||||
repo_path: ActiveValue::set(status_entry.repo_path.clone()),
|
||||
status: ActiveValue::set(status_entry.status as i64),
|
||||
scan_id: ActiveValue::set(update.scan_id as i64),
|
||||
is_deleted: ActiveValue::set(false),
|
||||
}
|
||||
}),
|
||||
)
|
||||
.on_conflict(
|
||||
OnConflict::columns([
|
||||
worktree_repository_statuses::Column::ProjectId,
|
||||
worktree_repository_statuses::Column::WorktreeId,
|
||||
worktree_repository_statuses::Column::WorkDirectoryId,
|
||||
worktree_repository_statuses::Column::RepoPath,
|
||||
])
|
||||
.update_columns([
|
||||
worktree_repository_statuses::Column::ScanId,
|
||||
worktree_repository_statuses::Column::Status,
|
||||
worktree_repository_statuses::Column::IsDeleted,
|
||||
])
|
||||
.to_owned(),
|
||||
)
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
}
|
||||
|
||||
if !repository.removed_repo_paths.is_empty() {
|
||||
worktree_repository_statuses::Entity::update_many()
|
||||
.filter(
|
||||
worktree_repository_statuses::Column::ProjectId
|
||||
.eq(project_id)
|
||||
.and(
|
||||
worktree_repository_statuses::Column::WorktreeId
|
||||
.eq(worktree_id),
|
||||
)
|
||||
.and(
|
||||
worktree_repository_statuses::Column::WorkDirectoryId
|
||||
.eq(repository.work_directory_id as i64),
|
||||
)
|
||||
.and(worktree_repository_statuses::Column::RepoPath.is_in(
|
||||
repository.removed_repo_paths.iter().map(String::as_str),
|
||||
)),
|
||||
)
|
||||
.set(worktree_repository_statuses::ActiveModel {
|
||||
is_deleted: ActiveValue::Set(true),
|
||||
scan_id: ActiveValue::Set(update.scan_id as i64),
|
||||
..Default::default()
|
||||
})
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !update.removed_repositories.is_empty() {
|
||||
worktree_repository::Entity::update_many()
|
||||
.filter(
|
||||
worktree_repository::Column::ProjectId
|
||||
.eq(project_id)
|
||||
.and(worktree_repository::Column::WorktreeId.eq(worktree_id))
|
||||
.and(
|
||||
worktree_repository::Column::WorkDirectoryId
|
||||
.is_in(update.removed_repositories.iter().map(|id| *id as i64)),
|
||||
),
|
||||
)
|
||||
.set(worktree_repository::ActiveModel {
|
||||
is_deleted: ActiveValue::Set(true),
|
||||
scan_id: ActiveValue::Set(update.scan_id as i64),
|
||||
..Default::default()
|
||||
})
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let connection_ids = self.project_guest_connection_ids(project_id, &tx).await?;
|
||||
Ok(connection_ids)
|
||||
})
|
||||
@@ -2699,7 +2505,6 @@ impl Database {
|
||||
root_name: db_worktree.root_name,
|
||||
visible: db_worktree.visible,
|
||||
entries: Default::default(),
|
||||
repository_entries: Default::default(),
|
||||
diagnostic_summaries: Default::default(),
|
||||
scan_id: db_worktree.scan_id as u64,
|
||||
completed_scan_id: db_worktree.completed_scan_id as u64,
|
||||
@@ -2737,61 +2542,6 @@ impl Database {
|
||||
}
|
||||
}
|
||||
|
||||
// Populate repository entries.
|
||||
{
|
||||
let mut db_repository_entries = worktree_repository::Entity::find()
|
||||
.filter(
|
||||
Condition::all()
|
||||
.add(worktree_repository::Column::ProjectId.eq(project_id))
|
||||
.add(worktree_repository::Column::IsDeleted.eq(false)),
|
||||
)
|
||||
.stream(&*tx)
|
||||
.await?;
|
||||
while let Some(db_repository_entry) = db_repository_entries.next().await {
|
||||
let db_repository_entry = db_repository_entry?;
|
||||
if let Some(worktree) =
|
||||
worktrees.get_mut(&(db_repository_entry.worktree_id as u64))
|
||||
{
|
||||
worktree.repository_entries.insert(
|
||||
db_repository_entry.work_directory_id as u64,
|
||||
proto::RepositoryEntry {
|
||||
work_directory_id: db_repository_entry.work_directory_id as u64,
|
||||
branch: db_repository_entry.branch,
|
||||
removed_repo_paths: Default::default(),
|
||||
updated_statuses: Default::default(),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
let mut db_status_entries = worktree_repository_statuses::Entity::find()
|
||||
.filter(
|
||||
Condition::all()
|
||||
.add(worktree_repository_statuses::Column::ProjectId.eq(project_id))
|
||||
.add(worktree_repository_statuses::Column::IsDeleted.eq(false)),
|
||||
)
|
||||
.stream(&*tx)
|
||||
.await?;
|
||||
|
||||
while let Some(db_status_entry) = db_status_entries.next().await {
|
||||
let db_status_entry = db_status_entry?;
|
||||
if let Some(worktree) = worktrees.get_mut(&(db_status_entry.worktree_id as u64))
|
||||
{
|
||||
if let Some(repository_entry) = worktree
|
||||
.repository_entries
|
||||
.get_mut(&(db_status_entry.work_directory_id as u64))
|
||||
{
|
||||
repository_entry.updated_statuses.push(proto::StatusEntry {
|
||||
repo_path: db_status_entry.repo_path,
|
||||
status: db_status_entry.status as i32,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Populate worktree diagnostic summaries.
|
||||
{
|
||||
let mut db_summaries = worktree_diagnostic_summary::Entity::find()
|
||||
@@ -3473,8 +3223,6 @@ pub struct RejoinedWorktree {
|
||||
pub visible: bool,
|
||||
pub updated_entries: Vec<proto::Entry>,
|
||||
pub removed_entries: Vec<u64>,
|
||||
pub updated_repositories: Vec<proto::RepositoryEntry>,
|
||||
pub removed_repositories: Vec<u64>,
|
||||
pub diagnostic_summaries: Vec<proto::DiagnosticSummary>,
|
||||
pub scan_id: u64,
|
||||
pub completed_scan_id: u64,
|
||||
@@ -3529,7 +3277,6 @@ pub struct Worktree {
|
||||
pub root_name: String,
|
||||
pub visible: bool,
|
||||
pub entries: Vec<proto::Entry>,
|
||||
pub repository_entries: BTreeMap<u64, proto::RepositoryEntry>,
|
||||
pub diagnostic_summaries: Vec<proto::DiagnosticSummary>,
|
||||
pub scan_id: u64,
|
||||
pub completed_scan_id: u64,
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
use super::ProjectId;
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "worktree_repositories")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub project_id: ProjectId,
|
||||
#[sea_orm(primary_key)]
|
||||
pub worktree_id: i64,
|
||||
#[sea_orm(primary_key)]
|
||||
pub work_directory_id: i64,
|
||||
pub scan_id: i64,
|
||||
pub branch: Option<String>,
|
||||
pub is_deleted: bool,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
@@ -1,23 +0,0 @@
|
||||
use super::ProjectId;
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "worktree_repository_statuses")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub project_id: ProjectId,
|
||||
#[sea_orm(primary_key)]
|
||||
pub worktree_id: i64,
|
||||
#[sea_orm(primary_key)]
|
||||
pub work_directory_id: i64,
|
||||
#[sea_orm(primary_key)]
|
||||
pub repo_path: String,
|
||||
pub status: i64,
|
||||
pub scan_id: i64,
|
||||
pub is_deleted: bool,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
@@ -1063,8 +1063,6 @@ async fn rejoin_room(
|
||||
removed_entries: worktree.removed_entries,
|
||||
scan_id: worktree.scan_id,
|
||||
is_last_update: worktree.completed_scan_id == worktree.scan_id,
|
||||
updated_repositories: worktree.updated_repositories,
|
||||
removed_repositories: worktree.removed_repositories,
|
||||
};
|
||||
for update in proto::split_worktree_update(message, MAX_CHUNK_SIZE) {
|
||||
session.peer.send(session.connection_id, update.clone())?;
|
||||
@@ -1385,8 +1383,6 @@ async fn join_project(
|
||||
removed_entries: Default::default(),
|
||||
scan_id: worktree.scan_id,
|
||||
is_last_update: worktree.scan_id == worktree.completed_scan_id,
|
||||
updated_repositories: worktree.repository_entries.into_values().collect(),
|
||||
removed_repositories: Default::default(),
|
||||
};
|
||||
for update in proto::split_worktree_update(message, MAX_CHUNK_SIZE) {
|
||||
session.peer.send(session.connection_id, update.clone())?;
|
||||
|
||||
@@ -12,10 +12,7 @@ use client::{
|
||||
use collections::{HashMap, HashSet};
|
||||
use fs::FakeFs;
|
||||
use futures::{channel::oneshot, StreamExt as _};
|
||||
use gpui::{
|
||||
elements::*, executor::Deterministic, AnyElement, Entity, ModelHandle, TestAppContext, View,
|
||||
ViewContext, ViewHandle, WeakViewHandle,
|
||||
};
|
||||
use gpui::{executor::Deterministic, test::EmptyView, ModelHandle, TestAppContext, ViewHandle};
|
||||
use language::LanguageRegistry;
|
||||
use parking_lot::Mutex;
|
||||
use project::{Project, WorktreeId};
|
||||
@@ -465,41 +462,8 @@ impl TestClient {
|
||||
project: &ModelHandle<Project>,
|
||||
cx: &mut TestAppContext,
|
||||
) -> ViewHandle<Workspace> {
|
||||
struct WorkspaceContainer {
|
||||
workspace: Option<WeakViewHandle<Workspace>>,
|
||||
}
|
||||
|
||||
impl Entity for WorkspaceContainer {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
impl View for WorkspaceContainer {
|
||||
fn ui_name() -> &'static str {
|
||||
"WorkspaceContainer"
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
if let Some(workspace) = self
|
||||
.workspace
|
||||
.as_ref()
|
||||
.and_then(|workspace| workspace.upgrade(cx))
|
||||
{
|
||||
ChildView::new(&workspace, cx).into_any()
|
||||
} else {
|
||||
Empty::new().into_any()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We use a workspace container so that we don't need to remove the window in order to
|
||||
// drop the workspace and we can use a ViewHandle instead.
|
||||
let (window_id, container) = cx.add_window(|_| WorkspaceContainer { workspace: None });
|
||||
let workspace = cx.add_view(window_id, |cx| Workspace::test_new(project.clone(), cx));
|
||||
container.update(cx, |container, cx| {
|
||||
container.workspace = Some(workspace.downgrade());
|
||||
cx.notify();
|
||||
});
|
||||
workspace
|
||||
let (_, root_view) = cx.add_window(|_| EmptyView);
|
||||
cx.add_view(&root_view, |cx| Workspace::test_new(project.clone(), cx))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,11 +10,11 @@ use editor::{
|
||||
ConfirmRename, Editor, ExcerptRange, MultiBuffer, Redo, Rename, ToOffset, ToggleCodeActions,
|
||||
Undo,
|
||||
};
|
||||
use fs::{repository::GitFileStatus, FakeFs, Fs as _, LineEnding, RemoveOptions};
|
||||
use fs::{FakeFs, Fs as _, LineEnding, RemoveOptions};
|
||||
use futures::StreamExt as _;
|
||||
use gpui::{
|
||||
executor::Deterministic, geometry::vector::vec2f, test::EmptyView, AppContext, ModelHandle,
|
||||
TestAppContext, ViewHandle,
|
||||
executor::Deterministic, geometry::vector::vec2f, test::EmptyView, ModelHandle, TestAppContext,
|
||||
ViewHandle,
|
||||
};
|
||||
use indoc::indoc;
|
||||
use language::{
|
||||
@@ -1202,7 +1202,7 @@ async fn test_share_project(
|
||||
cx_c: &mut TestAppContext,
|
||||
) {
|
||||
deterministic.forbid_parking();
|
||||
let (window_b, _) = cx_b.add_window(|_| EmptyView);
|
||||
let (_, window_b) = cx_b.add_window(|_| EmptyView);
|
||||
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;
|
||||
@@ -1289,7 +1289,7 @@ async fn test_share_project(
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let editor_b = cx_b.add_view(window_b, |cx| Editor::for_buffer(buffer_b, None, cx));
|
||||
let editor_b = cx_b.add_view(&window_b, |cx| Editor::for_buffer(buffer_b, None, cx));
|
||||
|
||||
// Client A sees client B's selection
|
||||
deterministic.run_until_parked();
|
||||
@@ -2604,240 +2604,6 @@ async fn test_git_diff_base_change(
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_git_branch_name(
|
||||
deterministic: Arc<Deterministic>,
|
||||
cx_a: &mut TestAppContext,
|
||||
cx_b: &mut TestAppContext,
|
||||
cx_c: &mut TestAppContext,
|
||||
) {
|
||||
deterministic.forbid_parking();
|
||||
let mut server = TestServer::start(&deterministic).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
let client_c = server.create_client(cx_c, "user_c").await;
|
||||
server
|
||||
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
|
||||
.await;
|
||||
let active_call_a = cx_a.read(ActiveCall::global);
|
||||
|
||||
client_a
|
||||
.fs
|
||||
.insert_tree(
|
||||
"/dir",
|
||||
json!({
|
||||
".git": {},
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let (project_local, _worktree_id) = client_a.build_local_project("/dir", cx_a).await;
|
||||
let project_id = active_call_a
|
||||
.update(cx_a, |call, cx| {
|
||||
call.share_project(project_local.clone(), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let project_remote = client_b.build_remote_project(project_id, cx_b).await;
|
||||
client_a
|
||||
.fs
|
||||
.as_fake()
|
||||
.set_branch_name(Path::new("/dir/.git"), Some("branch-1"))
|
||||
.await;
|
||||
|
||||
// Wait for it to catch up to the new branch
|
||||
deterministic.run_until_parked();
|
||||
|
||||
#[track_caller]
|
||||
fn assert_branch(branch_name: Option<impl Into<String>>, project: &Project, cx: &AppContext) {
|
||||
let branch_name = branch_name.map(Into::into);
|
||||
let worktrees = project.visible_worktrees(cx).collect::<Vec<_>>();
|
||||
assert_eq!(worktrees.len(), 1);
|
||||
let worktree = worktrees[0].clone();
|
||||
let root_entry = worktree.read(cx).snapshot().root_git_entry().unwrap();
|
||||
assert_eq!(root_entry.branch(), branch_name.map(Into::into));
|
||||
}
|
||||
|
||||
// Smoke test branch reading
|
||||
project_local.read_with(cx_a, |project, cx| {
|
||||
assert_branch(Some("branch-1"), project, cx)
|
||||
});
|
||||
project_remote.read_with(cx_b, |project, cx| {
|
||||
assert_branch(Some("branch-1"), project, cx)
|
||||
});
|
||||
|
||||
client_a
|
||||
.fs
|
||||
.as_fake()
|
||||
.set_branch_name(Path::new("/dir/.git"), Some("branch-2"))
|
||||
.await;
|
||||
|
||||
// Wait for buffer_local_a to receive it
|
||||
deterministic.run_until_parked();
|
||||
|
||||
// Smoke test branch reading
|
||||
project_local.read_with(cx_a, |project, cx| {
|
||||
assert_branch(Some("branch-2"), project, cx)
|
||||
});
|
||||
project_remote.read_with(cx_b, |project, cx| {
|
||||
assert_branch(Some("branch-2"), project, cx)
|
||||
});
|
||||
|
||||
let project_remote_c = client_c.build_remote_project(project_id, cx_c).await;
|
||||
project_remote_c.read_with(cx_c, |project, cx| {
|
||||
assert_branch(Some("branch-2"), project, cx)
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_git_status_sync(
|
||||
deterministic: Arc<Deterministic>,
|
||||
cx_a: &mut TestAppContext,
|
||||
cx_b: &mut TestAppContext,
|
||||
cx_c: &mut TestAppContext,
|
||||
) {
|
||||
deterministic.forbid_parking();
|
||||
let mut server = TestServer::start(&deterministic).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
let client_c = server.create_client(cx_c, "user_c").await;
|
||||
server
|
||||
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
|
||||
.await;
|
||||
let active_call_a = cx_a.read(ActiveCall::global);
|
||||
|
||||
client_a
|
||||
.fs
|
||||
.insert_tree(
|
||||
"/dir",
|
||||
json!({
|
||||
".git": {},
|
||||
"a.txt": "a",
|
||||
"b.txt": "b",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
const A_TXT: &'static str = "a.txt";
|
||||
const B_TXT: &'static str = "b.txt";
|
||||
|
||||
client_a
|
||||
.fs
|
||||
.as_fake()
|
||||
.set_status_for_repo(
|
||||
Path::new("/dir/.git"),
|
||||
&[
|
||||
(&Path::new(A_TXT), GitFileStatus::Added),
|
||||
(&Path::new(B_TXT), GitFileStatus::Added),
|
||||
],
|
||||
)
|
||||
.await;
|
||||
|
||||
let (project_local, _worktree_id) = client_a.build_local_project("/dir", cx_a).await;
|
||||
let project_id = active_call_a
|
||||
.update(cx_a, |call, cx| {
|
||||
call.share_project(project_local.clone(), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let project_remote = client_b.build_remote_project(project_id, cx_b).await;
|
||||
|
||||
// Wait for it to catch up to the new status
|
||||
deterministic.run_until_parked();
|
||||
|
||||
#[track_caller]
|
||||
fn assert_status(
|
||||
file: &impl AsRef<Path>,
|
||||
status: Option<GitFileStatus>,
|
||||
project: &Project,
|
||||
cx: &AppContext,
|
||||
) {
|
||||
let file = file.as_ref();
|
||||
let worktrees = project.visible_worktrees(cx).collect::<Vec<_>>();
|
||||
assert_eq!(worktrees.len(), 1);
|
||||
let worktree = worktrees[0].clone();
|
||||
let snapshot = worktree.read(cx).snapshot();
|
||||
let root_entry = snapshot.root_git_entry().unwrap();
|
||||
assert_eq!(root_entry.status_for_file(&snapshot, file), status);
|
||||
}
|
||||
|
||||
// Smoke test status reading
|
||||
project_local.read_with(cx_a, |project, cx| {
|
||||
assert_status(&Path::new(A_TXT), Some(GitFileStatus::Added), project, cx);
|
||||
assert_status(&Path::new(B_TXT), Some(GitFileStatus::Added), project, cx);
|
||||
});
|
||||
project_remote.read_with(cx_b, |project, cx| {
|
||||
assert_status(&Path::new(A_TXT), Some(GitFileStatus::Added), project, cx);
|
||||
assert_status(&Path::new(B_TXT), Some(GitFileStatus::Added), project, cx);
|
||||
});
|
||||
|
||||
client_a
|
||||
.fs
|
||||
.as_fake()
|
||||
.set_status_for_repo(
|
||||
Path::new("/dir/.git"),
|
||||
&[
|
||||
(&Path::new(A_TXT), GitFileStatus::Modified),
|
||||
(&Path::new(B_TXT), GitFileStatus::Modified),
|
||||
],
|
||||
)
|
||||
.await;
|
||||
|
||||
// Wait for buffer_local_a to receive it
|
||||
deterministic.run_until_parked();
|
||||
|
||||
// Smoke test status reading
|
||||
project_local.read_with(cx_a, |project, cx| {
|
||||
assert_status(
|
||||
&Path::new(A_TXT),
|
||||
Some(GitFileStatus::Modified),
|
||||
project,
|
||||
cx,
|
||||
);
|
||||
assert_status(
|
||||
&Path::new(B_TXT),
|
||||
Some(GitFileStatus::Modified),
|
||||
project,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
project_remote.read_with(cx_b, |project, cx| {
|
||||
assert_status(
|
||||
&Path::new(A_TXT),
|
||||
Some(GitFileStatus::Modified),
|
||||
project,
|
||||
cx,
|
||||
);
|
||||
assert_status(
|
||||
&Path::new(B_TXT),
|
||||
Some(GitFileStatus::Modified),
|
||||
project,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
// And synchronization while joining
|
||||
let project_remote_c = client_c.build_remote_project(project_id, cx_c).await;
|
||||
deterministic.run_until_parked();
|
||||
|
||||
project_remote_c.read_with(cx_c, |project, cx| {
|
||||
assert_status(
|
||||
&Path::new(A_TXT),
|
||||
Some(GitFileStatus::Modified),
|
||||
project,
|
||||
cx,
|
||||
);
|
||||
assert_status(
|
||||
&Path::new(B_TXT),
|
||||
Some(GitFileStatus::Modified),
|
||||
project,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_fs_operations(
|
||||
deterministic: Arc<Deterministic>,
|
||||
@@ -3310,13 +3076,13 @@ async fn test_newline_above_or_below_does_not_move_guest_cursor(
|
||||
.update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
let (window_a, _) = cx_a.add_window(|_| EmptyView);
|
||||
let editor_a = cx_a.add_view(window_a, |cx| {
|
||||
let (_, window_a) = cx_a.add_window(|_| EmptyView);
|
||||
let editor_a = cx_a.add_view(&window_a, |cx| {
|
||||
Editor::for_buffer(buffer_a, Some(project_a), cx)
|
||||
});
|
||||
let mut editor_cx_a = EditorTestContext {
|
||||
cx: cx_a,
|
||||
window_id: window_a,
|
||||
window_id: window_a.id(),
|
||||
editor: editor_a,
|
||||
};
|
||||
|
||||
@@ -3325,13 +3091,13 @@ async fn test_newline_above_or_below_does_not_move_guest_cursor(
|
||||
.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
let (window_b, _) = cx_b.add_window(|_| EmptyView);
|
||||
let editor_b = cx_b.add_view(window_b, |cx| {
|
||||
let (_, window_b) = cx_b.add_window(|_| EmptyView);
|
||||
let editor_b = cx_b.add_view(&window_b, |cx| {
|
||||
Editor::for_buffer(buffer_b, Some(project_b), cx)
|
||||
});
|
||||
let mut editor_cx_b = EditorTestContext {
|
||||
cx: cx_b,
|
||||
window_id: window_b,
|
||||
window_id: window_b.id(),
|
||||
editor: editor_b,
|
||||
};
|
||||
|
||||
@@ -3456,18 +3222,14 @@ async fn test_canceling_buffer_opening(
|
||||
.unwrap();
|
||||
|
||||
// Open a buffer as client B but cancel after a random amount of time.
|
||||
let buffer_b = project_b.update(cx_b, |p, cx| {
|
||||
p.open_buffer_by_id(buffer_a.read_with(cx_a, |a, _| a.remote_id()), cx)
|
||||
});
|
||||
let buffer_b = project_b.update(cx_b, |p, cx| p.open_buffer_by_id(buffer_a.id() as u64, cx));
|
||||
deterministic.simulate_random_delay().await;
|
||||
drop(buffer_b);
|
||||
|
||||
// Try opening the same buffer again as client B, and ensure we can
|
||||
// still do it despite the cancellation above.
|
||||
let buffer_b = project_b
|
||||
.update(cx_b, |p, cx| {
|
||||
p.open_buffer_by_id(buffer_a.read_with(cx_a, |a, _| a.remote_id()), cx)
|
||||
})
|
||||
.update(cx_b, |p, cx| p.open_buffer_by_id(buffer_a.id() as u64, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
buffer_b.read_with(cx_b, |buf, _| assert_eq!(buf.text(), "abc"));
|
||||
@@ -4070,8 +3832,8 @@ async fn test_collaborating_with_completion(
|
||||
.update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
let (window_b, _) = cx_b.add_window(|_| EmptyView);
|
||||
let editor_b = cx_b.add_view(window_b, |cx| {
|
||||
let (_, window_b) = cx_b.add_window(|_| EmptyView);
|
||||
let editor_b = cx_b.add_view(&window_b, |cx| {
|
||||
Editor::for_buffer(buffer_b.clone(), Some(project_b.clone()), cx)
|
||||
});
|
||||
|
||||
@@ -4696,10 +4458,7 @@ async fn test_project_search(
|
||||
// Perform a search as the guest.
|
||||
let results = project_b
|
||||
.update(cx_b, |project, cx| {
|
||||
project.search(
|
||||
SearchQuery::text("world", false, false, Vec::new(), Vec::new()),
|
||||
cx,
|
||||
)
|
||||
project.search(SearchQuery::text("world", false, false), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -7045,10 +6804,13 @@ async fn test_peers_following_each_other(
|
||||
// Clients A and B follow each other in split panes
|
||||
workspace_a.update(cx_a, |workspace, cx| {
|
||||
workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
|
||||
let pane_a1 = pane_a1.clone();
|
||||
cx.defer(move |workspace, _| {
|
||||
assert_ne!(*workspace.active_pane(), pane_a1);
|
||||
});
|
||||
});
|
||||
workspace_a
|
||||
.update(cx_a, |workspace, cx| {
|
||||
assert_ne!(*workspace.active_pane(), pane_a1);
|
||||
let leader_id = *project_a.read(cx).collaborators().keys().next().unwrap();
|
||||
workspace.toggle_follow(leader_id, cx).unwrap()
|
||||
})
|
||||
@@ -7056,10 +6818,13 @@ async fn test_peers_following_each_other(
|
||||
.unwrap();
|
||||
workspace_b.update(cx_b, |workspace, cx| {
|
||||
workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
|
||||
let pane_b1 = pane_b1.clone();
|
||||
cx.defer(move |workspace, _| {
|
||||
assert_ne!(*workspace.active_pane(), pane_b1);
|
||||
});
|
||||
});
|
||||
workspace_b
|
||||
.update(cx_b, |workspace, cx| {
|
||||
assert_ne!(*workspace.active_pane(), pane_b1);
|
||||
let leader_id = *project_b.read(cx).collaborators().keys().next().unwrap();
|
||||
workspace.toggle_follow(leader_id, cx).unwrap()
|
||||
})
|
||||
|
||||
@@ -8,13 +8,12 @@ use call::ActiveCall;
|
||||
use client::RECEIVE_TIMEOUT;
|
||||
use collections::BTreeMap;
|
||||
use editor::Bias;
|
||||
use fs::{repository::GitFileStatus, FakeFs, Fs as _};
|
||||
use fs::{FakeFs, Fs as _};
|
||||
use futures::StreamExt as _;
|
||||
use gpui::{executor::Deterministic, ModelHandle, Task, TestAppContext};
|
||||
use language::{range_to_lsp, FakeLspAdapter, Language, LanguageConfig, PointUtf16};
|
||||
use lsp::FakeLanguageServer;
|
||||
use parking_lot::Mutex;
|
||||
use pretty_assertions::assert_eq;
|
||||
use project::{search::SearchQuery, Project, ProjectPath};
|
||||
use rand::{
|
||||
distributions::{Alphanumeric, DistString},
|
||||
@@ -717,10 +716,7 @@ async fn apply_client_operation(
|
||||
);
|
||||
|
||||
let search = project.update(cx, |project, cx| {
|
||||
project.search(
|
||||
SearchQuery::text(query, false, false, Vec::new(), Vec::new()),
|
||||
cx,
|
||||
)
|
||||
project.search(SearchQuery::text(query, false, false), cx)
|
||||
});
|
||||
drop(project);
|
||||
let search = cx.background().spawn(async move {
|
||||
@@ -764,85 +760,31 @@ async fn apply_client_operation(
|
||||
}
|
||||
}
|
||||
|
||||
ClientOperation::GitOperation { operation } => match operation {
|
||||
GitOperation::WriteGitIndex {
|
||||
repo_path,
|
||||
contents,
|
||||
} => {
|
||||
if !client.fs.directories().contains(&repo_path) {
|
||||
return Err(TestError::Inapplicable);
|
||||
}
|
||||
|
||||
log::info!(
|
||||
"{}: writing git index for repo {:?}: {:?}",
|
||||
client.username,
|
||||
repo_path,
|
||||
contents
|
||||
);
|
||||
|
||||
let dot_git_dir = repo_path.join(".git");
|
||||
let contents = contents
|
||||
.iter()
|
||||
.map(|(path, contents)| (path.as_path(), contents.clone()))
|
||||
.collect::<Vec<_>>();
|
||||
if client.fs.metadata(&dot_git_dir).await?.is_none() {
|
||||
client.fs.create_dir(&dot_git_dir).await?;
|
||||
}
|
||||
client.fs.set_index_for_repo(&dot_git_dir, &contents).await;
|
||||
ClientOperation::WriteGitIndex {
|
||||
repo_path,
|
||||
contents,
|
||||
} => {
|
||||
if !client.fs.directories().contains(&repo_path) {
|
||||
return Err(TestError::Inapplicable);
|
||||
}
|
||||
GitOperation::WriteGitBranch {
|
||||
|
||||
log::info!(
|
||||
"{}: writing git index for repo {:?}: {:?}",
|
||||
client.username,
|
||||
repo_path,
|
||||
new_branch,
|
||||
} => {
|
||||
if !client.fs.directories().contains(&repo_path) {
|
||||
return Err(TestError::Inapplicable);
|
||||
}
|
||||
contents
|
||||
);
|
||||
|
||||
log::info!(
|
||||
"{}: writing git branch for repo {:?}: {:?}",
|
||||
client.username,
|
||||
repo_path,
|
||||
new_branch
|
||||
);
|
||||
|
||||
let dot_git_dir = repo_path.join(".git");
|
||||
if client.fs.metadata(&dot_git_dir).await?.is_none() {
|
||||
client.fs.create_dir(&dot_git_dir).await?;
|
||||
}
|
||||
client.fs.set_branch_name(&dot_git_dir, new_branch).await;
|
||||
let dot_git_dir = repo_path.join(".git");
|
||||
let contents = contents
|
||||
.iter()
|
||||
.map(|(path, contents)| (path.as_path(), contents.clone()))
|
||||
.collect::<Vec<_>>();
|
||||
if client.fs.metadata(&dot_git_dir).await?.is_none() {
|
||||
client.fs.create_dir(&dot_git_dir).await?;
|
||||
}
|
||||
GitOperation::WriteGitStatuses {
|
||||
repo_path,
|
||||
statuses,
|
||||
} => {
|
||||
if !client.fs.directories().contains(&repo_path) {
|
||||
return Err(TestError::Inapplicable);
|
||||
}
|
||||
|
||||
log::info!(
|
||||
"{}: writing git statuses for repo {:?}: {:?}",
|
||||
client.username,
|
||||
repo_path,
|
||||
statuses
|
||||
);
|
||||
|
||||
let dot_git_dir = repo_path.join(".git");
|
||||
|
||||
let statuses = statuses
|
||||
.iter()
|
||||
.map(|(path, val)| (path.as_path(), val.clone()))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if client.fs.metadata(&dot_git_dir).await?.is_none() {
|
||||
client.fs.create_dir(&dot_git_dir).await?;
|
||||
}
|
||||
|
||||
client
|
||||
.fs
|
||||
.set_status_for_repo(&dot_git_dir, statuses.as_slice())
|
||||
.await;
|
||||
}
|
||||
},
|
||||
client.fs.set_index_for_repo(&dot_git_dir, &contents).await;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -917,12 +859,6 @@ fn check_consistency_between_clients(clients: &[(Rc<TestClient>, TestAppContext)
|
||||
host_snapshot.abs_path(),
|
||||
guest_project.remote_id(),
|
||||
);
|
||||
assert_eq!(guest_snapshot.repositories().collect::<Vec<_>>(), host_snapshot.repositories().collect::<Vec<_>>(),
|
||||
"{} has different repositories than the host for worktree {:?} and project {:?}",
|
||||
client.username,
|
||||
host_snapshot.abs_path(),
|
||||
guest_project.remote_id(),
|
||||
);
|
||||
assert_eq!(guest_snapshot.scan_id(), host_snapshot.scan_id(),
|
||||
"{} has different scan id than the host for worktree {:?} and project {:?}",
|
||||
client.username,
|
||||
@@ -1211,25 +1147,10 @@ enum ClientOperation {
|
||||
is_dir: bool,
|
||||
content: String,
|
||||
},
|
||||
GitOperation {
|
||||
operation: GitOperation,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
enum GitOperation {
|
||||
WriteGitIndex {
|
||||
repo_path: PathBuf,
|
||||
contents: Vec<(PathBuf, String)>,
|
||||
},
|
||||
WriteGitBranch {
|
||||
repo_path: PathBuf,
|
||||
new_branch: Option<String>,
|
||||
},
|
||||
WriteGitStatuses {
|
||||
repo_path: PathBuf,
|
||||
statuses: Vec<(PathBuf, GitFileStatus)>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
@@ -1742,10 +1663,38 @@ impl TestPlan {
|
||||
}
|
||||
}
|
||||
|
||||
// Update a git related action
|
||||
// Update a git index
|
||||
91..=95 => {
|
||||
break ClientOperation::GitOperation {
|
||||
operation: self.generate_git_operation(client),
|
||||
let repo_path = client
|
||||
.fs
|
||||
.directories()
|
||||
.choose(&mut self.rng)
|
||||
.unwrap()
|
||||
.clone();
|
||||
|
||||
let mut file_paths = client
|
||||
.fs
|
||||
.files()
|
||||
.into_iter()
|
||||
.filter(|path| path.starts_with(&repo_path))
|
||||
.collect::<Vec<_>>();
|
||||
let count = self.rng.gen_range(0..=file_paths.len());
|
||||
file_paths.shuffle(&mut self.rng);
|
||||
file_paths.truncate(count);
|
||||
|
||||
let mut contents = Vec::new();
|
||||
for abs_child_file_path in &file_paths {
|
||||
let child_file_path = abs_child_file_path
|
||||
.strip_prefix(&repo_path)
|
||||
.unwrap()
|
||||
.to_path_buf();
|
||||
let new_base = Alphanumeric.sample_string(&mut self.rng, 16);
|
||||
contents.push((child_file_path, new_base));
|
||||
}
|
||||
|
||||
break ClientOperation::WriteGitIndex {
|
||||
repo_path,
|
||||
contents,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1783,86 +1732,6 @@ impl TestPlan {
|
||||
})
|
||||
}
|
||||
|
||||
fn generate_git_operation(&mut self, client: &TestClient) -> GitOperation {
|
||||
fn generate_file_paths(
|
||||
repo_path: &Path,
|
||||
rng: &mut StdRng,
|
||||
client: &TestClient,
|
||||
) -> Vec<PathBuf> {
|
||||
let mut paths = client
|
||||
.fs
|
||||
.files()
|
||||
.into_iter()
|
||||
.filter(|path| path.starts_with(repo_path))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let count = rng.gen_range(0..=paths.len());
|
||||
paths.shuffle(rng);
|
||||
paths.truncate(count);
|
||||
|
||||
paths
|
||||
.iter()
|
||||
.map(|path| path.strip_prefix(repo_path).unwrap().to_path_buf())
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
let repo_path = client
|
||||
.fs
|
||||
.directories()
|
||||
.choose(&mut self.rng)
|
||||
.unwrap()
|
||||
.clone();
|
||||
|
||||
match self.rng.gen_range(0..100_u32) {
|
||||
0..=25 => {
|
||||
let file_paths = generate_file_paths(&repo_path, &mut self.rng, client);
|
||||
|
||||
let contents = file_paths
|
||||
.into_iter()
|
||||
.map(|path| (path, Alphanumeric.sample_string(&mut self.rng, 16)))
|
||||
.collect();
|
||||
|
||||
GitOperation::WriteGitIndex {
|
||||
repo_path,
|
||||
contents,
|
||||
}
|
||||
}
|
||||
26..=63 => {
|
||||
let new_branch = (self.rng.gen_range(0..10) > 3)
|
||||
.then(|| Alphanumeric.sample_string(&mut self.rng, 8));
|
||||
|
||||
GitOperation::WriteGitBranch {
|
||||
repo_path,
|
||||
new_branch,
|
||||
}
|
||||
}
|
||||
64..=100 => {
|
||||
let file_paths = generate_file_paths(&repo_path, &mut self.rng, client);
|
||||
|
||||
let statuses = file_paths
|
||||
.into_iter()
|
||||
.map(|paths| {
|
||||
(
|
||||
paths,
|
||||
match self.rng.gen_range(0..3_u32) {
|
||||
0 => GitFileStatus::Added,
|
||||
1 => GitFileStatus::Modified,
|
||||
2 => GitFileStatus::Conflict,
|
||||
_ => unreachable!(),
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
GitOperation::WriteGitStatuses {
|
||||
repo_path,
|
||||
statuses,
|
||||
}
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn next_root_dir_name(&mut self, user_id: UserId) -> String {
|
||||
let user_ix = self
|
||||
.users
|
||||
|
||||
@@ -14,8 +14,8 @@ use gpui::{
|
||||
geometry::{rect::RectF, vector::vec2f, PathBuilder},
|
||||
json::{self, ToJson},
|
||||
platform::{CursorStyle, MouseButton},
|
||||
AppContext, Entity, ImageData, LayoutContext, ModelHandle, SceneBuilder, Subscription, View,
|
||||
ViewContext, ViewHandle, WeakViewHandle,
|
||||
AppContext, Entity, ImageData, ModelHandle, SceneBuilder, Subscription, View, ViewContext,
|
||||
ViewHandle, WeakViewHandle,
|
||||
};
|
||||
use project::Project;
|
||||
use settings::Settings;
|
||||
@@ -24,8 +24,6 @@ use theme::{AvatarStyle, Theme};
|
||||
use util::ResultExt;
|
||||
use workspace::{FollowNextCollaborator, Workspace};
|
||||
|
||||
const MAX_TITLE_LENGTH: usize = 75;
|
||||
|
||||
actions!(
|
||||
collab,
|
||||
[
|
||||
@@ -70,11 +68,29 @@ impl View for CollabTitlebarItem {
|
||||
};
|
||||
|
||||
let project = self.project.read(cx);
|
||||
let mut project_title = String::new();
|
||||
for (i, name) in project.worktree_root_names(cx).enumerate() {
|
||||
if i > 0 {
|
||||
project_title.push_str(", ");
|
||||
}
|
||||
project_title.push_str(name);
|
||||
}
|
||||
if project_title.is_empty() {
|
||||
project_title = "empty project".to_owned();
|
||||
}
|
||||
|
||||
let theme = cx.global::<Settings>().theme.clone();
|
||||
|
||||
let mut left_container = Flex::row();
|
||||
let mut right_container = Flex::row().align_children_center();
|
||||
|
||||
left_container.add_child(self.collect_title_root_names(&project, theme.clone(), cx));
|
||||
left_container.add_child(
|
||||
Label::new(project_title, theme.workspace.titlebar.title.clone())
|
||||
.contained()
|
||||
.with_margin_right(theme.workspace.titlebar.item_spacing)
|
||||
.aligned()
|
||||
.left(),
|
||||
);
|
||||
|
||||
let user = self.user_store.read(cx).current_user();
|
||||
let peer_id = self.client.peer_id();
|
||||
@@ -104,21 +120,7 @@ impl View for CollabTitlebarItem {
|
||||
|
||||
Stack::new()
|
||||
.with_child(left_container)
|
||||
.with_child(
|
||||
Flex::row()
|
||||
.with_child(
|
||||
right_container.contained().with_background_color(
|
||||
theme
|
||||
.workspace
|
||||
.titlebar
|
||||
.container
|
||||
.background_color
|
||||
.unwrap_or_else(|| Color::transparent_black()),
|
||||
),
|
||||
)
|
||||
.aligned()
|
||||
.right(),
|
||||
)
|
||||
.with_child(right_container.aligned().right())
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
@@ -135,7 +137,6 @@ impl CollabTitlebarItem {
|
||||
let active_call = ActiveCall::global(cx);
|
||||
let mut subscriptions = Vec::new();
|
||||
subscriptions.push(cx.observe(workspace_handle, |_, _, cx| cx.notify()));
|
||||
subscriptions.push(cx.observe(&project, |_, _, cx| cx.notify()));
|
||||
subscriptions.push(cx.observe(&active_call, |this, _, cx| this.active_call_changed(cx)));
|
||||
subscriptions.push(cx.observe_window_activation(|this, active, cx| {
|
||||
this.window_activation_changed(active, cx)
|
||||
@@ -164,7 +165,6 @@ impl CollabTitlebarItem {
|
||||
}),
|
||||
);
|
||||
|
||||
let view_id = cx.view_id();
|
||||
Self {
|
||||
workspace: workspace.weak_handle(),
|
||||
project,
|
||||
@@ -172,7 +172,7 @@ impl CollabTitlebarItem {
|
||||
client,
|
||||
contacts_popover: None,
|
||||
user_menu: cx.add_view(|cx| {
|
||||
let mut menu = ContextMenu::new(view_id, cx);
|
||||
let mut menu = ContextMenu::new(cx);
|
||||
menu.set_position_mode(OverlayPositionMode::Local);
|
||||
menu
|
||||
}),
|
||||
@@ -180,63 +180,6 @@ impl CollabTitlebarItem {
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_title_root_names(
|
||||
&self,
|
||||
project: &Project,
|
||||
theme: Arc<Theme>,
|
||||
cx: &ViewContext<Self>,
|
||||
) -> AnyElement<Self> {
|
||||
let names_and_branches = project.visible_worktrees(cx).map(|worktree| {
|
||||
let worktree = worktree.read(cx);
|
||||
(worktree.root_name(), worktree.root_git_entry())
|
||||
});
|
||||
|
||||
fn push_str(buffer: &mut String, index: &mut usize, str: &str) {
|
||||
buffer.push_str(str);
|
||||
*index += str.chars().count();
|
||||
}
|
||||
|
||||
let mut indices = Vec::new();
|
||||
let mut index = 0;
|
||||
let mut title = String::new();
|
||||
let mut names_and_branches = names_and_branches.peekable();
|
||||
while let Some((name, entry)) = names_and_branches.next() {
|
||||
let pre_index = index;
|
||||
push_str(&mut title, &mut index, name);
|
||||
indices.extend((pre_index..index).into_iter());
|
||||
if let Some(branch) = entry.and_then(|entry| entry.branch()) {
|
||||
push_str(&mut title, &mut index, "/");
|
||||
push_str(&mut title, &mut index, &branch);
|
||||
}
|
||||
if names_and_branches.peek().is_some() {
|
||||
push_str(&mut title, &mut index, ", ");
|
||||
if index >= MAX_TITLE_LENGTH {
|
||||
title.push_str(" …");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let text_style = theme.workspace.titlebar.title.clone();
|
||||
let item_spacing = theme.workspace.titlebar.item_spacing;
|
||||
|
||||
let mut highlight = text_style.clone();
|
||||
highlight.color = theme.workspace.titlebar.highlight_color;
|
||||
|
||||
let style = LabelStyle {
|
||||
text: text_style,
|
||||
highlight_text: Some(highlight),
|
||||
};
|
||||
|
||||
Label::new(title, style)
|
||||
.with_highlights(indices)
|
||||
.contained()
|
||||
.with_margin_right(item_spacing)
|
||||
.aligned()
|
||||
.left()
|
||||
.into_any_named("title-with-git-information")
|
||||
}
|
||||
|
||||
fn window_activation_changed(&mut self, active: bool, cx: &mut ViewContext<Self>) {
|
||||
let project = if active {
|
||||
Some(self.project.clone())
|
||||
@@ -922,7 +865,7 @@ impl Element<CollabTitlebarItem> for AvatarRibbon {
|
||||
&mut self,
|
||||
constraint: gpui::SizeConstraint,
|
||||
_: &mut CollabTitlebarItem,
|
||||
_: &mut LayoutContext<CollabTitlebarItem>,
|
||||
_: &mut ViewContext<CollabTitlebarItem>,
|
||||
) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) {
|
||||
(constraint.max, ())
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ use gpui::{
|
||||
},
|
||||
json::ToJson,
|
||||
serde_json::{self, json},
|
||||
AnyElement, Axis, Element, LayoutContext, SceneBuilder, ViewContext,
|
||||
AnyElement, Axis, Element, SceneBuilder, ViewContext,
|
||||
};
|
||||
|
||||
use crate::CollabTitlebarItem;
|
||||
@@ -34,7 +34,7 @@ impl Element<CollabTitlebarItem> for FacePile {
|
||||
&mut self,
|
||||
constraint: gpui::SizeConstraint,
|
||||
view: &mut CollabTitlebarItem,
|
||||
cx: &mut LayoutContext<CollabTitlebarItem>,
|
||||
cx: &mut ViewContext<CollabTitlebarItem>,
|
||||
) -> (Vector2F, Self::LayoutState) {
|
||||
debug_assert!(constraint.max_along(Axis::Horizontal) == f32::INFINITY);
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ use collections::CommandPaletteFilter;
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use gpui::{
|
||||
actions, elements::*, keymap_matcher::Keystroke, Action, AppContext, Element, MouseState,
|
||||
ViewContext,
|
||||
ViewContext, WindowContext,
|
||||
};
|
||||
use picker::{Picker, PickerDelegate, PickerEvent};
|
||||
use settings::Settings;
|
||||
@@ -41,17 +41,47 @@ struct Command {
|
||||
keystrokes: Vec<Keystroke>,
|
||||
}
|
||||
|
||||
fn toggle_command_palette(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
|
||||
let focused_view_id = cx.focused_view_id().unwrap_or_else(|| cx.view_id());
|
||||
workspace.toggle_modal(cx, |_, cx| {
|
||||
cx.add_view(|cx| Picker::new(CommandPaletteDelegate::new(focused_view_id), cx))
|
||||
fn toggle_command_palette(_: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
|
||||
let workspace = cx.handle();
|
||||
let focused_view_id = cx.focused_view_id().unwrap_or_else(|| workspace.id());
|
||||
|
||||
cx.window_context().defer(move |cx| {
|
||||
// Build the delegate before the workspace is put on the stack so we can find it when
|
||||
// computing the actions. We should really not allow available_actions to be called
|
||||
// if it's not reliable however.
|
||||
let delegate = CommandPaletteDelegate::new(focused_view_id, cx);
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.toggle_modal(cx, |_, cx| cx.add_view(|cx| Picker::new(delegate, cx)));
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
impl CommandPaletteDelegate {
|
||||
pub fn new(focused_view_id: usize) -> Self {
|
||||
pub fn new(focused_view_id: usize, cx: &mut WindowContext) -> Self {
|
||||
let actions = cx
|
||||
.available_actions(focused_view_id)
|
||||
.filter_map(|(name, action, bindings)| {
|
||||
if cx.has_global::<CommandPaletteFilter>() {
|
||||
let filter = cx.global::<CommandPaletteFilter>();
|
||||
if filter.filtered_namespaces.contains(action.namespace()) {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
Some(Command {
|
||||
name: humanize_action_name(name),
|
||||
action,
|
||||
keystrokes: bindings
|
||||
.iter()
|
||||
.map(|binding| binding.keystrokes())
|
||||
.last()
|
||||
.map_or(Vec::new(), |keystrokes| keystrokes.to_vec()),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
Self {
|
||||
actions: Default::default(),
|
||||
actions,
|
||||
matches: vec![],
|
||||
selected_ix: 0,
|
||||
focused_view_id,
|
||||
@@ -81,46 +111,17 @@ impl PickerDelegate for CommandPaletteDelegate {
|
||||
query: String,
|
||||
cx: &mut ViewContext<Picker<Self>>,
|
||||
) -> gpui::Task<()> {
|
||||
let window_id = cx.window_id();
|
||||
let view_id = self.focused_view_id;
|
||||
let candidates = self
|
||||
.actions
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(ix, command)| StringMatchCandidate {
|
||||
id: ix,
|
||||
string: command.name.to_string(),
|
||||
char_bag: command.name.chars().collect(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
cx.spawn(move |picker, mut cx| async move {
|
||||
let actions = cx
|
||||
.available_actions(window_id, view_id)
|
||||
.into_iter()
|
||||
.filter_map(|(name, action, bindings)| {
|
||||
let filtered = cx.read(|cx| {
|
||||
if cx.has_global::<CommandPaletteFilter>() {
|
||||
let filter = cx.global::<CommandPaletteFilter>();
|
||||
filter.filtered_namespaces.contains(action.namespace())
|
||||
} else {
|
||||
false
|
||||
}
|
||||
});
|
||||
|
||||
if filtered {
|
||||
None
|
||||
} else {
|
||||
Some(Command {
|
||||
name: humanize_action_name(name),
|
||||
action,
|
||||
keystrokes: bindings
|
||||
.iter()
|
||||
.map(|binding| binding.keystrokes())
|
||||
.last()
|
||||
.map_or(Vec::new(), |keystrokes| keystrokes.to_vec()),
|
||||
})
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let candidates = actions
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(ix, command)| StringMatchCandidate {
|
||||
id: ix,
|
||||
string: command.name.to_string(),
|
||||
char_bag: command.name.chars().collect(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let matches = if query.is_empty() {
|
||||
candidates
|
||||
.into_iter()
|
||||
@@ -146,7 +147,6 @@ impl PickerDelegate for CommandPaletteDelegate {
|
||||
picker
|
||||
.update(&mut cx, |picker, _| {
|
||||
let delegate = picker.delegate_mut();
|
||||
delegate.actions = actions;
|
||||
delegate.matches = matches;
|
||||
if delegate.matches.is_empty() {
|
||||
delegate.selected_ix = 0;
|
||||
@@ -304,8 +304,8 @@ mod tests {
|
||||
});
|
||||
|
||||
let project = Project::test(app_state.fs.clone(), [], cx).await;
|
||||
let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
|
||||
let editor = cx.add_view(window_id, |cx| {
|
||||
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
|
||||
let editor = cx.add_view(&workspace, |cx| {
|
||||
let mut editor = Editor::single_line(None, cx);
|
||||
editor.set_text("abc", cx);
|
||||
editor
|
||||
|
||||
@@ -126,6 +126,7 @@ pub struct ContextMenu {
|
||||
selected_index: Option<usize>,
|
||||
visible: bool,
|
||||
previously_focused_view_id: Option<usize>,
|
||||
clicked: bool,
|
||||
parent_view_id: usize,
|
||||
_actions_observation: Subscription,
|
||||
}
|
||||
@@ -176,7 +177,9 @@ impl View for ContextMenu {
|
||||
}
|
||||
|
||||
impl ContextMenu {
|
||||
pub fn new(parent_view_id: usize, cx: &mut ViewContext<Self>) -> Self {
|
||||
pub fn new(cx: &mut ViewContext<Self>) -> Self {
|
||||
let parent_view_id = cx.parent().unwrap();
|
||||
|
||||
Self {
|
||||
show_count: 0,
|
||||
anchor_position: Default::default(),
|
||||
@@ -186,6 +189,7 @@ impl ContextMenu {
|
||||
selected_index: Default::default(),
|
||||
visible: Default::default(),
|
||||
previously_focused_view_id: Default::default(),
|
||||
clicked: false,
|
||||
parent_view_id,
|
||||
_actions_observation: cx.observe_actions(Self::action_dispatched),
|
||||
}
|
||||
@@ -201,14 +205,18 @@ impl ContextMenu {
|
||||
.iter()
|
||||
.position(|item| item.action_id() == Some(action_id))
|
||||
{
|
||||
self.selected_index = Some(ix);
|
||||
cx.notify();
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
cx.background().timer(Duration::from_millis(50)).await;
|
||||
this.update(&mut cx, |this, cx| this.cancel(&Default::default(), cx))?;
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
if self.clicked {
|
||||
self.cancel(&Default::default(), cx);
|
||||
} else {
|
||||
self.selected_index = Some(ix);
|
||||
cx.notify();
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
cx.background().timer(Duration::from_millis(50)).await;
|
||||
this.update(&mut cx, |this, cx| this.cancel(&Default::default(), cx))?;
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -248,6 +256,7 @@ impl ContextMenu {
|
||||
self.items.clear();
|
||||
self.visible = false;
|
||||
self.selected_index.take();
|
||||
self.clicked = false;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -447,7 +456,7 @@ impl ContextMenu {
|
||||
.on_up(MouseButton::Left, |_, _, _| {}) // Capture these events
|
||||
.on_down(MouseButton::Left, |_, _, _| {}) // Capture these events
|
||||
.on_click(MouseButton::Left, move |_, menu, cx| {
|
||||
menu.cancel(&Default::default(), cx);
|
||||
menu.clicked = true;
|
||||
let window_id = cx.window_id();
|
||||
match &action {
|
||||
ContextMenuItemAction::Action(action) => {
|
||||
|
||||
@@ -126,7 +126,7 @@ impl CopilotServer {
|
||||
struct RunningCopilotServer {
|
||||
lsp: Arc<LanguageServer>,
|
||||
sign_in_status: SignInStatus,
|
||||
registered_buffers: HashMap<u64, RegisteredBuffer>,
|
||||
registered_buffers: HashMap<usize, RegisteredBuffer>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -162,7 +162,7 @@ impl Status {
|
||||
}
|
||||
|
||||
struct RegisteredBuffer {
|
||||
id: u64,
|
||||
id: usize,
|
||||
uri: lsp::Url,
|
||||
language_id: String,
|
||||
snapshot: BufferSnapshot,
|
||||
@@ -258,7 +258,7 @@ impl RegisteredBuffer {
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Completion {
|
||||
pub uuid: String,
|
||||
uuid: String,
|
||||
pub range: Range<Anchor>,
|
||||
pub text: String,
|
||||
}
|
||||
@@ -267,7 +267,7 @@ pub struct Copilot {
|
||||
http: Arc<dyn HttpClient>,
|
||||
node_runtime: Arc<NodeRuntime>,
|
||||
server: CopilotServer,
|
||||
buffers: HashMap<u64, WeakModelHandle<Buffer>>,
|
||||
buffers: HashMap<usize, WeakModelHandle<Buffer>>,
|
||||
}
|
||||
|
||||
impl Entity for Copilot {
|
||||
@@ -580,7 +580,7 @@ impl Copilot {
|
||||
}
|
||||
|
||||
pub fn register_buffer(&mut self, buffer: &ModelHandle<Buffer>, cx: &mut ModelContext<Self>) {
|
||||
let buffer_id = buffer.read(cx).remote_id();
|
||||
let buffer_id = buffer.id();
|
||||
self.buffers.insert(buffer_id, buffer.downgrade());
|
||||
|
||||
if let CopilotServer::Running(RunningCopilotServer {
|
||||
@@ -594,8 +594,7 @@ impl Copilot {
|
||||
return;
|
||||
}
|
||||
|
||||
let buffer_id = buffer.read(cx).remote_id();
|
||||
registered_buffers.entry(buffer_id).or_insert_with(|| {
|
||||
registered_buffers.entry(buffer.id()).or_insert_with(|| {
|
||||
let uri: lsp::Url = uri_for_buffer(buffer, cx);
|
||||
let language_id = id_for_language(buffer.read(cx).language());
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
@@ -640,8 +639,7 @@ impl Copilot {
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Result<()> {
|
||||
if let Ok(server) = self.server.as_running() {
|
||||
let buffer_id = buffer.read(cx).remote_id();
|
||||
if let Some(registered_buffer) = server.registered_buffers.get_mut(&buffer_id) {
|
||||
if let Some(registered_buffer) = server.registered_buffers.get_mut(&buffer.id()) {
|
||||
match event {
|
||||
language::Event::Edited => {
|
||||
let _ = registered_buffer.report_changes(&buffer, cx);
|
||||
@@ -695,7 +693,7 @@ impl Copilot {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn unregister_buffer(&mut self, buffer_id: u64) {
|
||||
fn unregister_buffer(&mut self, buffer_id: usize) {
|
||||
if let Ok(server) = self.server.as_running() {
|
||||
if let Some(buffer) = server.registered_buffers.remove(&buffer_id) {
|
||||
server
|
||||
@@ -800,8 +798,7 @@ impl Copilot {
|
||||
Err(error) => return Task::ready(Err(error)),
|
||||
};
|
||||
let lsp = server.lsp.clone();
|
||||
let buffer_id = buffer.read(cx).remote_id();
|
||||
let registered_buffer = server.registered_buffers.get_mut(&buffer_id).unwrap();
|
||||
let registered_buffer = server.registered_buffers.get_mut(&buffer.id()).unwrap();
|
||||
let snapshot = registered_buffer.report_changes(buffer, cx);
|
||||
let buffer = buffer.read(cx);
|
||||
let uri = registered_buffer.uri.clone();
|
||||
@@ -920,9 +917,7 @@ fn uri_for_buffer(buffer: &ModelHandle<Buffer>, cx: &AppContext) -> lsp::Url {
|
||||
if let Some(file) = buffer.read(cx).file().and_then(|file| file.as_local()) {
|
||||
lsp::Url::from_file_path(file.abs_path(cx)).unwrap()
|
||||
} else {
|
||||
format!("buffer://{}", buffer.read(cx).remote_id())
|
||||
.parse()
|
||||
.unwrap()
|
||||
format!("buffer://{}", buffer.id()).parse().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1170,7 +1165,7 @@ mod tests {
|
||||
}
|
||||
|
||||
fn mtime(&self) -> std::time::SystemTime {
|
||||
unimplemented!()
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn path(&self) -> &Arc<Path> {
|
||||
@@ -1178,23 +1173,23 @@ mod tests {
|
||||
}
|
||||
|
||||
fn full_path(&self, _: &AppContext) -> PathBuf {
|
||||
unimplemented!()
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn file_name<'a>(&'a self, _: &'a AppContext) -> &'a std::ffi::OsStr {
|
||||
unimplemented!()
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn is_deleted(&self) -> bool {
|
||||
unimplemented!()
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
unimplemented!()
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn to_proto(&self) -> rpc::proto::File {
|
||||
unimplemented!()
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1204,7 +1199,7 @@ mod tests {
|
||||
}
|
||||
|
||||
fn load(&self, _: &AppContext) -> Task<Result<String>> {
|
||||
unimplemented!()
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn buffer_reloaded(
|
||||
@@ -1216,7 +1211,7 @@ mod tests {
|
||||
_: std::time::SystemTime,
|
||||
_: &mut AppContext,
|
||||
) {
|
||||
unimplemented!()
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,9 +144,8 @@ impl View for CopilotButton {
|
||||
|
||||
impl CopilotButton {
|
||||
pub fn new(cx: &mut ViewContext<Self>) -> Self {
|
||||
let button_view_id = cx.view_id();
|
||||
let menu = cx.add_view(|cx| {
|
||||
let mut menu = ContextMenu::new(button_view_id, cx);
|
||||
let mut menu = ContextMenu::new(cx);
|
||||
menu.set_position_mode(OverlayPositionMode::Local);
|
||||
menu
|
||||
});
|
||||
@@ -328,9 +327,10 @@ async fn configure_disabled_globs(
|
||||
cx.global::<Settings>()
|
||||
.copilot
|
||||
.disabled_globs
|
||||
.clone()
|
||||
.iter()
|
||||
.map(|glob| glob.as_str().to_string())
|
||||
.collect()
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
|
||||
if let Some(path_to_disable) = &path_to_disable {
|
||||
|
||||
@@ -852,7 +852,7 @@ mod tests {
|
||||
|
||||
let language_server_id = LanguageServerId(0);
|
||||
let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
|
||||
let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
|
||||
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
|
||||
|
||||
// Create some diagnostics
|
||||
project.update(cx, |project, cx| {
|
||||
@@ -939,7 +939,7 @@ mod tests {
|
||||
});
|
||||
|
||||
// Open the project diagnostics view while there are already diagnostics.
|
||||
let view = cx.add_view(window_id, |cx| {
|
||||
let view = cx.add_view(&workspace, |cx| {
|
||||
ProjectDiagnosticsEditor::new(project.clone(), workspace.downgrade(), cx)
|
||||
});
|
||||
|
||||
@@ -1244,9 +1244,9 @@ mod tests {
|
||||
let server_id_1 = LanguageServerId(100);
|
||||
let server_id_2 = LanguageServerId(101);
|
||||
let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
|
||||
let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
|
||||
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
|
||||
|
||||
let view = cx.add_view(window_id, |cx| {
|
||||
let view = cx.add_view(&workspace, |cx| {
|
||||
ProjectDiagnosticsEditor::new(project.clone(), workspace.downgrade(), cx)
|
||||
});
|
||||
|
||||
|
||||
@@ -199,7 +199,7 @@ impl<V: View> DragAndDrop<V> {
|
||||
return None;
|
||||
}
|
||||
|
||||
let position = (position - region_offset).round();
|
||||
let position = position - region_offset;
|
||||
Some(
|
||||
Overlay::new(
|
||||
MouseEventHandler::<DraggedElementHandler, V>::new(
|
||||
|
||||
@@ -833,7 +833,10 @@ impl<'a> Iterator for BlockChunks<'a> {
|
||||
|
||||
return Some(Chunk {
|
||||
text: unsafe { std::str::from_utf8_unchecked(&NEWLINES[..line_count as usize]) },
|
||||
..Default::default()
|
||||
syntax_highlight_id: None,
|
||||
highlight_style: None,
|
||||
diagnostic_severity: None,
|
||||
is_unnecessary: false,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1065,11 +1065,13 @@ impl<'a> Iterator for FoldChunks<'a> {
|
||||
self.output_offset += output_text.len();
|
||||
return Some(Chunk {
|
||||
text: output_text,
|
||||
syntax_highlight_id: None,
|
||||
highlight_style: self.ellipses_color.map(|color| HighlightStyle {
|
||||
color: Some(color),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
diagnostic_severity: None,
|
||||
is_unnecessary: false,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -531,8 +531,10 @@ impl<'a> Iterator for SuggestionChunks<'a> {
|
||||
if let Some(chunk) = chunks.next() {
|
||||
return Some(Chunk {
|
||||
text: chunk,
|
||||
syntax_highlight_id: None,
|
||||
highlight_style: self.highlight_style,
|
||||
..Default::default()
|
||||
diagnostic_severity: None,
|
||||
is_unnecessary: false,
|
||||
});
|
||||
} else {
|
||||
self.suggestion_chunks = None;
|
||||
|
||||
@@ -268,7 +268,6 @@ impl TabSnapshot {
|
||||
tab_size: self.tab_size,
|
||||
chunk: Chunk {
|
||||
text: &SPACES[0..(to_next_stop as usize)],
|
||||
is_tab: true,
|
||||
..Default::default()
|
||||
},
|
||||
inside_leading_tab: to_next_stop > 0,
|
||||
@@ -546,7 +545,6 @@ impl<'a> Iterator for TabChunks<'a> {
|
||||
self.output_position = next_output_position;
|
||||
return Some(Chunk {
|
||||
text: &SPACES[..len as usize],
|
||||
is_tab: true,
|
||||
..self.chunk
|
||||
});
|
||||
}
|
||||
@@ -656,56 +654,6 @@ mod tests {
|
||||
assert_eq!(tab_snapshot.text(), input);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_marking_tabs(cx: &mut gpui::AppContext) {
|
||||
let input = "\t \thello";
|
||||
|
||||
let buffer = MultiBuffer::build_simple(&input, cx);
|
||||
let buffer_snapshot = buffer.read(cx).snapshot(cx);
|
||||
let (_, fold_snapshot) = FoldMap::new(buffer_snapshot.clone());
|
||||
let (_, suggestion_snapshot) = SuggestionMap::new(fold_snapshot);
|
||||
let (_, tab_snapshot) = TabMap::new(suggestion_snapshot, 4.try_into().unwrap());
|
||||
|
||||
assert_eq!(
|
||||
chunks(&tab_snapshot, TabPoint::zero()),
|
||||
vec![
|
||||
(" ".to_string(), true),
|
||||
(" ".to_string(), false),
|
||||
(" ".to_string(), true),
|
||||
("hello".to_string(), false),
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
chunks(&tab_snapshot, TabPoint::new(0, 2)),
|
||||
vec![
|
||||
(" ".to_string(), true),
|
||||
(" ".to_string(), false),
|
||||
(" ".to_string(), true),
|
||||
("hello".to_string(), false),
|
||||
]
|
||||
);
|
||||
|
||||
fn chunks(snapshot: &TabSnapshot, start: TabPoint) -> Vec<(String, bool)> {
|
||||
let mut chunks = Vec::new();
|
||||
let mut was_tab = false;
|
||||
let mut text = String::new();
|
||||
for chunk in snapshot.chunks(start..snapshot.max_point(), false, None, None) {
|
||||
if chunk.is_tab != was_tab {
|
||||
if !text.is_empty() {
|
||||
chunks.push((mem::take(&mut text), was_tab));
|
||||
}
|
||||
was_tab = chunk.is_tab;
|
||||
}
|
||||
text.push_str(chunk.text);
|
||||
}
|
||||
|
||||
if !text.is_empty() {
|
||||
chunks.push((text, was_tab));
|
||||
}
|
||||
chunks
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 100)]
|
||||
fn test_random_tabs(cx: &mut gpui::AppContext, mut rng: StdRng) {
|
||||
let tab_size = NonZeroU32::new(rng.gen_range(1..=4)).unwrap();
|
||||
|
||||
@@ -516,15 +516,6 @@ pub struct EditorSnapshot {
|
||||
ongoing_scroll: OngoingScroll,
|
||||
}
|
||||
|
||||
impl EditorSnapshot {
|
||||
fn has_scrollbar_info(&self) -> bool {
|
||||
self.buffer_snapshot
|
||||
.git_diff_hunks_in_range(0..self.max_point().row(), false)
|
||||
.next()
|
||||
.is_some()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct SelectionHistoryEntry {
|
||||
selections: Arc<[Selection<Anchor>]>,
|
||||
@@ -1236,7 +1227,6 @@ impl Editor {
|
||||
get_field_editor_theme: Option<Arc<GetFieldEditorTheme>>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let editor_view_id = cx.view_id();
|
||||
let display_map = cx.add_model(|cx| {
|
||||
let settings = cx.global::<Settings>();
|
||||
let style = build_style(&*settings, get_field_editor_theme.as_deref(), None, cx);
|
||||
@@ -1257,16 +1247,6 @@ impl Editor {
|
||||
|
||||
let soft_wrap_mode_override =
|
||||
(mode == EditorMode::SingleLine).then(|| settings::SoftWrap::None);
|
||||
|
||||
let mut project_subscription = None;
|
||||
if mode == EditorMode::Full && buffer.read(cx).is_singleton() {
|
||||
if let Some(project) = project.as_ref() {
|
||||
project_subscription = Some(cx.observe(project, |_, _, cx| {
|
||||
cx.emit(Event::TitleChanged);
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
let mut this = Self {
|
||||
handle: cx.weak_handle(),
|
||||
buffer: buffer.clone(),
|
||||
@@ -1294,8 +1274,7 @@ impl Editor {
|
||||
background_highlights: Default::default(),
|
||||
nav_history: None,
|
||||
context_menu: None,
|
||||
mouse_context_menu: cx
|
||||
.add_view(|cx| context_menu::ContextMenu::new(editor_view_id, cx)),
|
||||
mouse_context_menu: cx.add_view(context_menu::ContextMenu::new),
|
||||
completion_tasks: Default::default(),
|
||||
next_completion_id: 0,
|
||||
available_code_actions: Default::default(),
|
||||
@@ -1323,11 +1302,6 @@ impl Editor {
|
||||
cx.observe_global::<Settings, _>(Self::settings_changed),
|
||||
],
|
||||
};
|
||||
|
||||
if let Some(project_subscription) = project_subscription {
|
||||
this._subscriptions.push(project_subscription);
|
||||
}
|
||||
|
||||
this.end_selection(cx);
|
||||
this.scroll_manager.show_scrollbar(cx);
|
||||
|
||||
@@ -1339,7 +1313,7 @@ impl Editor {
|
||||
cx.set_global(ScrollbarAutoHide(should_auto_hide_scrollbars));
|
||||
}
|
||||
|
||||
this.report_editor_event("open", None, cx);
|
||||
this.report_editor_event("open", cx);
|
||||
this
|
||||
}
|
||||
|
||||
@@ -2620,7 +2594,7 @@ impl Editor {
|
||||
let old_text = buffer.text_for_range(old_range.clone()).collect::<String>();
|
||||
|
||||
let newest_selection = self.selections.newest_anchor();
|
||||
if newest_selection.start.buffer_id != Some(buffer_handle.read(cx).remote_id()) {
|
||||
if newest_selection.start.buffer_id != Some(buffer_handle.id()) {
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -2828,7 +2802,7 @@ impl Editor {
|
||||
),
|
||||
);
|
||||
}
|
||||
multibuffer.push_transaction(entries.iter().map(|(b, t)| (b, t)), cx);
|
||||
multibuffer.push_transaction(entries.iter().map(|(b, t)| (b, t)));
|
||||
multibuffer
|
||||
});
|
||||
|
||||
@@ -3106,8 +3080,6 @@ impl Editor {
|
||||
copilot
|
||||
.update(cx, |copilot, cx| copilot.accept_completion(completion, cx))
|
||||
.detach_and_log_err(cx);
|
||||
|
||||
self.report_copilot_event(Some(completion.uuid.clone()), true, cx)
|
||||
}
|
||||
self.insert_with_autoindent_mode(&suggestion.text.to_string(), None, cx);
|
||||
cx.notify();
|
||||
@@ -3125,8 +3097,6 @@ impl Editor {
|
||||
copilot.discard_completions(&self.copilot_state.completions, cx)
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
|
||||
self.report_copilot_event(None, false, cx)
|
||||
}
|
||||
|
||||
self.display_map
|
||||
@@ -5794,7 +5764,7 @@ impl Editor {
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) {
|
||||
// If there are multiple definitions, open them in a multibuffer
|
||||
locations.sort_by_key(|location| location.buffer.read(cx).remote_id());
|
||||
locations.sort_by_key(|location| location.buffer.id());
|
||||
let mut locations = locations.into_iter().peekable();
|
||||
let mut ranges_to_highlight = Vec::new();
|
||||
|
||||
@@ -6089,7 +6059,7 @@ impl Editor {
|
||||
buffer.update(&mut cx, |buffer, cx| {
|
||||
if let Some(transaction) = transaction {
|
||||
if !buffer.is_singleton() {
|
||||
buffer.push_transaction(&transaction.0, cx);
|
||||
buffer.push_transaction(&transaction.0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6889,84 +6859,44 @@ impl Editor {
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn report_copilot_event(
|
||||
&self,
|
||||
suggestion_id: Option<String>,
|
||||
suggestion_accepted: bool,
|
||||
cx: &AppContext,
|
||||
) {
|
||||
let Some(project) = &self.project else {
|
||||
return
|
||||
};
|
||||
fn report_editor_event(&self, name: &'static str, cx: &AppContext) {
|
||||
if let Some((project, file)) = self.project.as_ref().zip(
|
||||
self.buffer
|
||||
.read(cx)
|
||||
.as_singleton()
|
||||
.and_then(|b| b.read(cx).file()),
|
||||
) {
|
||||
let settings = cx.global::<Settings>();
|
||||
|
||||
// If None, we are either getting suggestions in a new, unsaved file, or in a file without an extension
|
||||
let file_extension = self
|
||||
.buffer
|
||||
.read(cx)
|
||||
.as_singleton()
|
||||
.and_then(|b| b.read(cx).file())
|
||||
.and_then(|file| Path::new(file.file_name(cx)).extension())
|
||||
.and_then(|e| e.to_str())
|
||||
.map(|a| a.to_string());
|
||||
|
||||
let telemetry = project.read(cx).client().telemetry().clone();
|
||||
let telemetry_settings = cx.global::<Settings>().telemetry();
|
||||
|
||||
let event = ClickhouseEvent::Copilot {
|
||||
suggestion_id,
|
||||
suggestion_accepted,
|
||||
file_extension,
|
||||
};
|
||||
telemetry.report_clickhouse_event(event, telemetry_settings);
|
||||
}
|
||||
|
||||
fn report_editor_event(
|
||||
&self,
|
||||
name: &'static str,
|
||||
file_extension: Option<String>,
|
||||
cx: &AppContext,
|
||||
) {
|
||||
let Some(project) = &self.project else {
|
||||
return
|
||||
};
|
||||
|
||||
// If None, we are in a file without an extension
|
||||
let file_extension = file_extension.or(self
|
||||
.buffer
|
||||
.read(cx)
|
||||
.as_singleton()
|
||||
.and_then(|b| b.read(cx).file())
|
||||
.and_then(|file| Path::new(file.file_name(cx)).extension())
|
||||
.and_then(|e| e.to_str())
|
||||
.map(|a| a.to_string()));
|
||||
|
||||
let settings = cx.global::<Settings>();
|
||||
|
||||
let telemetry = project.read(cx).client().telemetry().clone();
|
||||
telemetry.report_mixpanel_event(
|
||||
let extension = Path::new(file.file_name(cx))
|
||||
.extension()
|
||||
.and_then(|e| e.to_str());
|
||||
let telemetry = project.read(cx).client().telemetry().clone();
|
||||
telemetry.report_mixpanel_event(
|
||||
match name {
|
||||
"open" => "open editor",
|
||||
"save" => "save editor",
|
||||
_ => name,
|
||||
},
|
||||
json!({ "File Extension": file_extension, "Vim Mode": settings.vim_mode, "In Clickhouse": true }),
|
||||
json!({ "File Extension": extension, "Vim Mode": settings.vim_mode, "In Clickhouse": true }),
|
||||
settings.telemetry(),
|
||||
);
|
||||
let event = ClickhouseEvent::Editor {
|
||||
file_extension,
|
||||
vim_mode: settings.vim_mode,
|
||||
operation: name,
|
||||
copilot_enabled: settings.features.copilot,
|
||||
copilot_enabled_for_language: settings.show_copilot_suggestions(
|
||||
self.language_at(0, cx)
|
||||
.map(|language| language.name())
|
||||
.as_deref(),
|
||||
self.file_at(0, cx)
|
||||
.map(|file| file.path().clone())
|
||||
.as_deref(),
|
||||
),
|
||||
};
|
||||
telemetry.report_clickhouse_event(event, settings.telemetry())
|
||||
let event = ClickhouseEvent::Editor {
|
||||
file_extension: extension.map(ToString::to_string),
|
||||
vim_mode: settings.vim_mode,
|
||||
operation: name,
|
||||
copilot_enabled: settings.features.copilot,
|
||||
copilot_enabled_for_language: settings.show_copilot_suggestions(
|
||||
self.language_at(0, cx)
|
||||
.map(|language| language.name())
|
||||
.as_deref(),
|
||||
self.file_at(0, cx)
|
||||
.map(|file| file.path().clone())
|
||||
.as_deref(),
|
||||
),
|
||||
};
|
||||
telemetry.report_clickhouse_event(event, settings.telemetry())
|
||||
}
|
||||
}
|
||||
|
||||
/// Copy the highlighted chunks to the clipboard as JSON. The format is an array of lines,
|
||||
|
||||
@@ -493,9 +493,9 @@ async fn test_navigation_history(cx: &mut TestAppContext) {
|
||||
|
||||
let fs = FakeFs::new(cx.background());
|
||||
let project = Project::test(fs, [], cx).await;
|
||||
let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
|
||||
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
|
||||
let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
|
||||
cx.add_view(window_id, |cx| {
|
||||
cx.add_view(&pane, |cx| {
|
||||
let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
|
||||
let mut editor = build_editor(buffer.clone(), cx);
|
||||
let handle = cx.handle();
|
||||
@@ -5459,12 +5459,10 @@ async fn test_following(cx: &mut gpui::TestAppContext) {
|
||||
});
|
||||
|
||||
let is_still_following = Rc::new(RefCell::new(true));
|
||||
let follower_edit_event_count = Rc::new(RefCell::new(0));
|
||||
let pending_update = Rc::new(RefCell::new(None));
|
||||
follower.update(cx, {
|
||||
let update = pending_update.clone();
|
||||
let is_still_following = is_still_following.clone();
|
||||
let follower_edit_event_count = follower_edit_event_count.clone();
|
||||
|_, cx| {
|
||||
cx.subscribe(&leader, move |_, leader, event, cx| {
|
||||
leader
|
||||
@@ -5477,9 +5475,6 @@ async fn test_following(cx: &mut gpui::TestAppContext) {
|
||||
if Editor::should_unfollow_on_event(event, cx) {
|
||||
*is_still_following.borrow_mut() = false;
|
||||
}
|
||||
if let Event::BufferEdited = event {
|
||||
*follower_edit_event_count.borrow_mut() += 1;
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
@@ -5499,7 +5494,6 @@ async fn test_following(cx: &mut gpui::TestAppContext) {
|
||||
assert_eq!(follower.selections.ranges(cx), vec![1..1]);
|
||||
});
|
||||
assert_eq!(*is_still_following.borrow(), true);
|
||||
assert_eq!(*follower_edit_event_count.borrow(), 0);
|
||||
|
||||
// Update the scroll position only
|
||||
leader.update(cx, |leader, cx| {
|
||||
@@ -5516,7 +5510,6 @@ async fn test_following(cx: &mut gpui::TestAppContext) {
|
||||
vec2f(1.5, 3.5)
|
||||
);
|
||||
assert_eq!(*is_still_following.borrow(), true);
|
||||
assert_eq!(*follower_edit_event_count.borrow(), 0);
|
||||
|
||||
// Update the selections and scroll position. The follower's scroll position is updated
|
||||
// via autoscroll, not via the leader's exact scroll position.
|
||||
|
||||
@@ -21,7 +21,7 @@ use git::diff::DiffHunkStatus;
|
||||
use gpui::{
|
||||
color::Color,
|
||||
elements::*,
|
||||
fonts::{HighlightStyle, TextStyle, Underline},
|
||||
fonts::{HighlightStyle, Underline},
|
||||
geometry::{
|
||||
rect::RectF,
|
||||
vector::{vec2f, Vector2F},
|
||||
@@ -30,17 +30,16 @@ use gpui::{
|
||||
json::{self, ToJson},
|
||||
platform::{CursorStyle, Modifiers, MouseButton, MouseButtonEvent, MouseMovedEvent},
|
||||
text_layout::{self, Line, RunStyle, TextLayoutCache},
|
||||
AnyElement, Axis, Border, CursorRegion, Element, EventContext, FontCache, LayoutContext,
|
||||
MouseRegion, Quad, SceneBuilder, SizeConstraint, ViewContext, WindowContext,
|
||||
AnyElement, Axis, Border, CursorRegion, Element, EventContext, MouseRegion, Quad, SceneBuilder,
|
||||
SizeConstraint, ViewContext, WindowContext,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use json::json;
|
||||
use language::{Bias, CursorShape, DiagnosticSeverity, OffsetUtf16, Selection};
|
||||
use project::ProjectPath;
|
||||
use settings::{GitGutter, Settings, ShowWhitespaces};
|
||||
use settings::{GitGutter, Settings};
|
||||
use smallvec::SmallVec;
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
cmp::{self, Ordering},
|
||||
fmt::Write,
|
||||
iter,
|
||||
@@ -784,19 +783,11 @@ impl EditorElement {
|
||||
|
||||
let mut cursors = SmallVec::<[Cursor; 32]>::new();
|
||||
let corner_radius = 0.15 * layout.position_map.line_height;
|
||||
let mut invisible_display_ranges = SmallVec::<[Range<DisplayPoint>; 32]>::new();
|
||||
|
||||
for (replica_id, selections) in &layout.selections {
|
||||
let replica_id = *replica_id;
|
||||
let selection_style = style.replica_selection_style(replica_id);
|
||||
let selection_style = style.replica_selection_style(*replica_id);
|
||||
|
||||
for selection in selections {
|
||||
if !selection.range.is_empty()
|
||||
&& (replica_id == local_replica_id
|
||||
|| Some(replica_id) == editor.leader_replica_id)
|
||||
{
|
||||
invisible_display_ranges.push(selection.range.clone());
|
||||
}
|
||||
self.paint_highlighted_range(
|
||||
scene,
|
||||
selection.range.clone(),
|
||||
@@ -810,15 +801,14 @@ impl EditorElement {
|
||||
bounds,
|
||||
);
|
||||
|
||||
if editor.show_local_cursors(cx) || replica_id != local_replica_id {
|
||||
if editor.show_local_cursors(cx) || *replica_id != local_replica_id {
|
||||
let cursor_position = selection.head;
|
||||
if layout
|
||||
.visible_display_row_range
|
||||
.contains(&cursor_position.row())
|
||||
{
|
||||
let cursor_row_layout = &layout.position_map.line_layouts
|
||||
[(cursor_position.row() - start_row) as usize]
|
||||
.line;
|
||||
[(cursor_position.row() - start_row) as usize];
|
||||
let cursor_column = cursor_position.column() as usize;
|
||||
|
||||
let cursor_character_x = cursor_row_layout.x_for_index(cursor_column);
|
||||
@@ -872,20 +862,20 @@ impl EditorElement {
|
||||
}
|
||||
|
||||
if let Some(visible_text_bounds) = bounds.intersection(visible_bounds) {
|
||||
for (ix, line_with_invisibles) in layout.position_map.line_layouts.iter().enumerate() {
|
||||
// Draw glyphs
|
||||
for (ix, line) in layout.position_map.line_layouts.iter().enumerate() {
|
||||
let row = start_row + ix as u32;
|
||||
line_with_invisibles.draw(
|
||||
layout,
|
||||
row,
|
||||
scroll_top,
|
||||
line.paint(
|
||||
scene,
|
||||
content_origin,
|
||||
scroll_left,
|
||||
content_origin
|
||||
+ vec2f(
|
||||
-scroll_left,
|
||||
row as f32 * layout.position_map.line_height - scroll_top,
|
||||
),
|
||||
visible_text_bounds,
|
||||
layout.position_map.line_height,
|
||||
cx,
|
||||
&invisible_display_ranges,
|
||||
visible_bounds,
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -898,7 +888,7 @@ impl EditorElement {
|
||||
if let Some((position, context_menu)) = layout.context_menu.as_mut() {
|
||||
scene.push_stacking_context(None, None);
|
||||
let cursor_row_layout =
|
||||
&layout.position_map.line_layouts[(position.row() - start_row) as usize].line;
|
||||
&layout.position_map.line_layouts[(position.row() - start_row) as usize];
|
||||
let x = cursor_row_layout.x_for_index(position.column() as usize) - scroll_left;
|
||||
let y = (position.row() + 1) as f32 * layout.position_map.line_height - scroll_top;
|
||||
let mut list_origin = content_origin + vec2f(x, y);
|
||||
@@ -931,7 +921,7 @@ impl EditorElement {
|
||||
|
||||
// This is safe because we check on layout whether the required row is available
|
||||
let hovered_row_layout =
|
||||
&layout.position_map.line_layouts[(position.row() - start_row) as usize].line;
|
||||
&layout.position_map.line_layouts[(position.row() - start_row) as usize];
|
||||
|
||||
// Minimum required size: Take the first popover, and add 1.5 times the minimum popover
|
||||
// height. This is the size we will use to decide whether to render popovers above or below
|
||||
@@ -1022,16 +1012,15 @@ impl EditorElement {
|
||||
let mut first_row_y_offset = 0.0;
|
||||
|
||||
// Impose a minimum height on the scrollbar thumb
|
||||
let row_height = height / max_row;
|
||||
let min_thumb_height =
|
||||
style.min_height_factor * cx.font_cache.line_height(self.style.text.font_size);
|
||||
let thumb_height = (row_range.end - row_range.start) * row_height;
|
||||
let thumb_height = (row_range.end - row_range.start) * height / max_row;
|
||||
if thumb_height < min_thumb_height {
|
||||
first_row_y_offset = (min_thumb_height - thumb_height) / 2.0;
|
||||
height -= min_thumb_height - thumb_height;
|
||||
}
|
||||
|
||||
let y_for_row = |row: f32| -> f32 { top + first_row_y_offset + row * row_height };
|
||||
let y_for_row = |row: f32| -> f32 { top + first_row_y_offset + row * height / max_row };
|
||||
|
||||
let thumb_top = y_for_row(row_range.start) - first_row_y_offset;
|
||||
let thumb_bottom = y_for_row(row_range.end) + first_row_y_offset;
|
||||
@@ -1045,50 +1034,6 @@ impl EditorElement {
|
||||
background: style.track.background_color,
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let diff_style = cx.global::<Settings>().theme.editor.diff.clone();
|
||||
for hunk in layout
|
||||
.position_map
|
||||
.snapshot
|
||||
.buffer_snapshot
|
||||
.git_diff_hunks_in_range(0..(max_row.floor() as u32), false)
|
||||
{
|
||||
let start_y = y_for_row(hunk.buffer_range.start as f32);
|
||||
let mut end_y = if hunk.buffer_range.start == hunk.buffer_range.end {
|
||||
y_for_row((hunk.buffer_range.end + 1) as f32)
|
||||
} else {
|
||||
y_for_row((hunk.buffer_range.end) as f32)
|
||||
};
|
||||
|
||||
if end_y - start_y < 1. {
|
||||
end_y = start_y + 1.;
|
||||
}
|
||||
let bounds = RectF::from_points(vec2f(left, start_y), vec2f(right, end_y));
|
||||
|
||||
let color = match hunk.status() {
|
||||
DiffHunkStatus::Added => diff_style.inserted,
|
||||
DiffHunkStatus::Modified => diff_style.modified,
|
||||
DiffHunkStatus::Removed => diff_style.deleted,
|
||||
};
|
||||
|
||||
let border = Border {
|
||||
width: 1.,
|
||||
color: style.thumb.border.color,
|
||||
overlay: false,
|
||||
top: false,
|
||||
right: true,
|
||||
bottom: false,
|
||||
left: true,
|
||||
};
|
||||
|
||||
scene.push_quad(Quad {
|
||||
bounds,
|
||||
background: Some(color),
|
||||
border,
|
||||
corner_radius: style.thumb.corner_radius,
|
||||
})
|
||||
}
|
||||
|
||||
scene.push_quad(Quad {
|
||||
bounds: thumb_bounds,
|
||||
border: style.thumb.border,
|
||||
@@ -1173,7 +1118,7 @@ impl EditorElement {
|
||||
.into_iter()
|
||||
.map(|row| {
|
||||
let line_layout =
|
||||
&layout.position_map.line_layouts[(row - start_row) as usize].line;
|
||||
&layout.position_map.line_layouts[(row - start_row) as usize];
|
||||
HighlightedRangeLine {
|
||||
start_x: if row == range.start.row() {
|
||||
content_origin.x()
|
||||
@@ -1335,10 +1280,9 @@ impl EditorElement {
|
||||
fn layout_lines(
|
||||
&mut self,
|
||||
rows: Range<u32>,
|
||||
line_number_layouts: &[Option<Line>],
|
||||
snapshot: &EditorSnapshot,
|
||||
cx: &ViewContext<Editor>,
|
||||
) -> Vec<LineWithInvisibles> {
|
||||
) -> Vec<text_layout::Line> {
|
||||
if rows.start >= rows.end {
|
||||
return Vec::new();
|
||||
}
|
||||
@@ -1373,10 +1317,6 @@ impl EditorElement {
|
||||
)],
|
||||
)
|
||||
})
|
||||
.map(|line| LineWithInvisibles {
|
||||
line,
|
||||
invisibles: Vec::new(),
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
let style = &self.style;
|
||||
@@ -1419,22 +1359,15 @@ impl EditorElement {
|
||||
highlight_style = Some(diagnostic_highlight);
|
||||
}
|
||||
|
||||
HighlightedChunk {
|
||||
chunk: chunk.text,
|
||||
style: highlight_style,
|
||||
is_tab: chunk.is_tab,
|
||||
}
|
||||
(chunk.text, highlight_style)
|
||||
});
|
||||
|
||||
LineWithInvisibles::from_chunks(
|
||||
layout_highlighted_chunks(
|
||||
chunks,
|
||||
&style.text,
|
||||
cx.text_layout_cache(),
|
||||
cx.font_cache(),
|
||||
MAX_LINE_LEN,
|
||||
rows.len() as usize,
|
||||
line_number_layouts,
|
||||
snapshot.mode,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1452,10 +1385,10 @@ impl EditorElement {
|
||||
text_x: f32,
|
||||
line_height: f32,
|
||||
style: &EditorStyle,
|
||||
line_layouts: &[LineWithInvisibles],
|
||||
line_layouts: &[text_layout::Line],
|
||||
include_root: bool,
|
||||
editor: &mut Editor,
|
||||
cx: &mut LayoutContext<Editor>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) -> (f32, Vec<BlockLayout>) {
|
||||
let tooltip_style = cx.global::<Settings>().theme.tooltip.clone();
|
||||
let scroll_x = snapshot.scroll_anchor.offset.x();
|
||||
@@ -1475,7 +1408,6 @@ impl EditorElement {
|
||||
let anchor_x = text_x
|
||||
+ if rows.contains(&align_to.row()) {
|
||||
line_layouts[(align_to.row() - rows.start) as usize]
|
||||
.line
|
||||
.x_for_index(align_to.column() as usize)
|
||||
} else {
|
||||
layout_line(align_to.row(), snapshot, style, cx.text_layout_cache())
|
||||
@@ -1654,220 +1586,6 @@ impl EditorElement {
|
||||
}
|
||||
}
|
||||
|
||||
struct HighlightedChunk<'a> {
|
||||
chunk: &'a str,
|
||||
style: Option<HighlightStyle>,
|
||||
is_tab: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct LineWithInvisibles {
|
||||
pub line: Line,
|
||||
invisibles: Vec<Invisible>,
|
||||
}
|
||||
|
||||
impl LineWithInvisibles {
|
||||
fn from_chunks<'a>(
|
||||
chunks: impl Iterator<Item = HighlightedChunk<'a>>,
|
||||
text_style: &TextStyle,
|
||||
text_layout_cache: &TextLayoutCache,
|
||||
font_cache: &Arc<FontCache>,
|
||||
max_line_len: usize,
|
||||
max_line_count: usize,
|
||||
line_number_layouts: &[Option<Line>],
|
||||
editor_mode: EditorMode,
|
||||
) -> Vec<Self> {
|
||||
let mut layouts = Vec::with_capacity(max_line_count);
|
||||
let mut line = String::new();
|
||||
let mut invisibles = Vec::new();
|
||||
let mut styles = Vec::new();
|
||||
let mut non_whitespace_added = false;
|
||||
let mut row = 0;
|
||||
let mut line_exceeded_max_len = false;
|
||||
for highlighted_chunk in chunks.chain([HighlightedChunk {
|
||||
chunk: "\n",
|
||||
style: None,
|
||||
is_tab: false,
|
||||
}]) {
|
||||
for (ix, mut line_chunk) in highlighted_chunk.chunk.split('\n').enumerate() {
|
||||
if ix > 0 {
|
||||
layouts.push(Self {
|
||||
line: text_layout_cache.layout_str(&line, text_style.font_size, &styles),
|
||||
invisibles: invisibles.drain(..).collect(),
|
||||
});
|
||||
|
||||
line.clear();
|
||||
styles.clear();
|
||||
row += 1;
|
||||
line_exceeded_max_len = false;
|
||||
non_whitespace_added = false;
|
||||
if row == max_line_count {
|
||||
return layouts;
|
||||
}
|
||||
}
|
||||
|
||||
if !line_chunk.is_empty() && !line_exceeded_max_len {
|
||||
let text_style = if let Some(style) = highlighted_chunk.style {
|
||||
text_style
|
||||
.clone()
|
||||
.highlight(style, font_cache)
|
||||
.map(Cow::Owned)
|
||||
.unwrap_or_else(|_| Cow::Borrowed(text_style))
|
||||
} else {
|
||||
Cow::Borrowed(text_style)
|
||||
};
|
||||
|
||||
if line.len() + line_chunk.len() > max_line_len {
|
||||
let mut chunk_len = max_line_len - line.len();
|
||||
while !line_chunk.is_char_boundary(chunk_len) {
|
||||
chunk_len -= 1;
|
||||
}
|
||||
line_chunk = &line_chunk[..chunk_len];
|
||||
line_exceeded_max_len = true;
|
||||
}
|
||||
|
||||
styles.push((
|
||||
line_chunk.len(),
|
||||
RunStyle {
|
||||
font_id: text_style.font_id,
|
||||
color: text_style.color,
|
||||
underline: text_style.underline,
|
||||
},
|
||||
));
|
||||
|
||||
if editor_mode == EditorMode::Full {
|
||||
// Line wrap pads its contents with fake whitespaces,
|
||||
// avoid printing them
|
||||
let inside_wrapped_string = line_number_layouts
|
||||
.get(row)
|
||||
.and_then(|layout| layout.as_ref())
|
||||
.is_none();
|
||||
if highlighted_chunk.is_tab {
|
||||
if non_whitespace_added || !inside_wrapped_string {
|
||||
invisibles.push(Invisible::Tab {
|
||||
line_start_offset: line.len(),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
invisibles.extend(
|
||||
line_chunk
|
||||
.chars()
|
||||
.enumerate()
|
||||
.filter(|(_, line_char)| {
|
||||
let is_whitespace = line_char.is_whitespace();
|
||||
non_whitespace_added |= !is_whitespace;
|
||||
is_whitespace
|
||||
&& (non_whitespace_added || !inside_wrapped_string)
|
||||
})
|
||||
.map(|(whitespace_index, _)| Invisible::Whitespace {
|
||||
line_offset: line.len() + whitespace_index,
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
line.push_str(line_chunk);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
layouts
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
layout: &LayoutState,
|
||||
row: u32,
|
||||
scroll_top: f32,
|
||||
scene: &mut SceneBuilder,
|
||||
content_origin: Vector2F,
|
||||
scroll_left: f32,
|
||||
visible_text_bounds: RectF,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
selection_ranges: &[Range<DisplayPoint>],
|
||||
visible_bounds: RectF,
|
||||
) {
|
||||
let line_height = layout.position_map.line_height;
|
||||
let line_y = row as f32 * line_height - scroll_top;
|
||||
|
||||
self.line.paint(
|
||||
scene,
|
||||
content_origin + vec2f(-scroll_left, line_y),
|
||||
visible_text_bounds,
|
||||
line_height,
|
||||
cx,
|
||||
);
|
||||
|
||||
self.draw_invisibles(
|
||||
cx,
|
||||
&selection_ranges,
|
||||
layout,
|
||||
content_origin,
|
||||
scroll_left,
|
||||
line_y,
|
||||
row,
|
||||
scene,
|
||||
visible_bounds,
|
||||
line_height,
|
||||
);
|
||||
}
|
||||
|
||||
fn draw_invisibles(
|
||||
&self,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
selection_ranges: &[Range<DisplayPoint>],
|
||||
layout: &LayoutState,
|
||||
content_origin: Vector2F,
|
||||
scroll_left: f32,
|
||||
line_y: f32,
|
||||
row: u32,
|
||||
scene: &mut SceneBuilder,
|
||||
visible_bounds: RectF,
|
||||
line_height: f32,
|
||||
) {
|
||||
let settings = cx.global::<Settings>();
|
||||
let allowed_invisibles_regions = match settings
|
||||
.editor_overrides
|
||||
.show_whitespaces
|
||||
.or(settings.editor_defaults.show_whitespaces)
|
||||
.unwrap_or_default()
|
||||
{
|
||||
ShowWhitespaces::None => return,
|
||||
ShowWhitespaces::Selection => Some(selection_ranges),
|
||||
ShowWhitespaces::All => None,
|
||||
};
|
||||
|
||||
for invisible in &self.invisibles {
|
||||
let (&token_offset, invisible_symbol) = match invisible {
|
||||
Invisible::Tab { line_start_offset } => (line_start_offset, &layout.tab_invisible),
|
||||
Invisible::Whitespace { line_offset } => (line_offset, &layout.space_invisible),
|
||||
};
|
||||
|
||||
let x_offset = self.line.x_for_index(token_offset);
|
||||
let invisible_offset =
|
||||
(layout.position_map.em_width - invisible_symbol.width()).max(0.0) / 2.0;
|
||||
let origin = content_origin + vec2f(-scroll_left + x_offset + invisible_offset, line_y);
|
||||
|
||||
if let Some(allowed_regions) = allowed_invisibles_regions {
|
||||
let invisible_point = DisplayPoint::new(row, token_offset as u32);
|
||||
if !allowed_regions
|
||||
.iter()
|
||||
.any(|region| region.start <= invisible_point && invisible_point < region.end)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
invisible_symbol.paint(scene, origin, visible_bounds, line_height, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum Invisible {
|
||||
Tab { line_start_offset: usize },
|
||||
Whitespace { line_offset: usize },
|
||||
}
|
||||
|
||||
impl Element<Editor> for EditorElement {
|
||||
type LayoutState = LayoutState;
|
||||
type PaintState = ();
|
||||
@@ -1876,7 +1594,7 @@ impl Element<Editor> for EditorElement {
|
||||
&mut self,
|
||||
constraint: SizeConstraint,
|
||||
editor: &mut Editor,
|
||||
cx: &mut LayoutContext<Editor>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) -> (Vector2F, Self::LayoutState) {
|
||||
let mut size = constraint.max;
|
||||
if size.x().is_infinite() {
|
||||
@@ -1891,8 +1609,7 @@ impl Element<Editor> for EditorElement {
|
||||
let gutter_width;
|
||||
let gutter_margin;
|
||||
if snapshot.mode == EditorMode::Full {
|
||||
let em_width = style.text.em_width(cx.font_cache());
|
||||
gutter_padding = (em_width * style.gutter_padding_factor).round();
|
||||
gutter_padding = style.text.em_width(cx.font_cache()) * style.gutter_padding_factor;
|
||||
gutter_width = self.max_line_number_width(&snapshot, cx) + gutter_padding * 2.0;
|
||||
gutter_margin = -style.text.descent(cx.font_cache());
|
||||
} else {
|
||||
@@ -2058,15 +1775,7 @@ impl Element<Editor> for EditorElement {
|
||||
));
|
||||
}
|
||||
|
||||
let show_scrollbars = match cx.global::<Settings>().show_scrollbars {
|
||||
settings::ShowScrollbars::Auto => {
|
||||
snapshot.has_scrollbar_info() || editor.scroll_manager.scrollbars_visible()
|
||||
}
|
||||
settings::ShowScrollbars::System => editor.scroll_manager.scrollbars_visible(),
|
||||
settings::ShowScrollbars::Always => true,
|
||||
settings::ShowScrollbars::Never => false,
|
||||
};
|
||||
|
||||
let show_scrollbars = editor.scroll_manager.scrollbars_visible();
|
||||
let include_root = editor
|
||||
.project
|
||||
.as_ref()
|
||||
@@ -2101,11 +1810,10 @@ impl Element<Editor> for EditorElement {
|
||||
let scrollbar_row_range = scroll_position.y()..(scroll_position.y() + height_in_lines);
|
||||
|
||||
let mut max_visible_line_width = 0.0;
|
||||
let line_layouts =
|
||||
self.layout_lines(start_row..end_row, &line_number_layouts, &snapshot, cx);
|
||||
for line_with_invisibles in &line_layouts {
|
||||
if line_with_invisibles.line.width() > max_visible_line_width {
|
||||
max_visible_line_width = line_with_invisibles.line.width();
|
||||
let line_layouts = self.layout_lines(start_row..end_row, &snapshot, cx);
|
||||
for line in &line_layouts {
|
||||
if line.width() > max_visible_line_width {
|
||||
max_visible_line_width = line.width();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2257,13 +1965,6 @@ impl Element<Editor> for EditorElement {
|
||||
}
|
||||
}
|
||||
|
||||
let invisible_symbol_font_size = self.style.text.font_size / 2.0;
|
||||
let invisible_symbol_style = RunStyle {
|
||||
color: self.style.whitespace,
|
||||
font_id: self.style.text.font_id,
|
||||
underline: Default::default(),
|
||||
};
|
||||
|
||||
(
|
||||
size,
|
||||
LayoutState {
|
||||
@@ -2296,16 +1997,6 @@ impl Element<Editor> for EditorElement {
|
||||
context_menu,
|
||||
code_actions_indicator,
|
||||
fold_indicators,
|
||||
tab_invisible: cx.text_layout_cache().layout_str(
|
||||
"→",
|
||||
invisible_symbol_font_size,
|
||||
&[("→".len(), invisible_symbol_style)],
|
||||
),
|
||||
space_invisible: cx.text_layout_cache().layout_str(
|
||||
"•",
|
||||
invisible_symbol_font_size,
|
||||
&[("•".len(), invisible_symbol_style)],
|
||||
),
|
||||
hover_popovers: hover,
|
||||
},
|
||||
)
|
||||
@@ -2382,11 +2073,10 @@ impl Element<Editor> for EditorElement {
|
||||
return None;
|
||||
}
|
||||
|
||||
let line = &layout
|
||||
let line = layout
|
||||
.position_map
|
||||
.line_layouts
|
||||
.get((range_start.row() - start_row) as usize)?
|
||||
.line;
|
||||
.get((range_start.row() - start_row) as usize)?;
|
||||
let range_start_x = line.x_for_index(range_start.column() as usize);
|
||||
let range_start_y = range_start.row() as f32 * layout.position_map.line_height;
|
||||
Some(RectF::new(
|
||||
@@ -2443,17 +2133,15 @@ pub struct LayoutState {
|
||||
code_actions_indicator: Option<(u32, AnyElement<Editor>)>,
|
||||
hover_popovers: Option<(DisplayPoint, Vec<AnyElement<Editor>>)>,
|
||||
fold_indicators: Vec<Option<AnyElement<Editor>>>,
|
||||
tab_invisible: Line,
|
||||
space_invisible: Line,
|
||||
}
|
||||
|
||||
struct PositionMap {
|
||||
pub struct PositionMap {
|
||||
size: Vector2F,
|
||||
line_height: f32,
|
||||
scroll_max: Vector2F,
|
||||
em_width: f32,
|
||||
em_advance: f32,
|
||||
line_layouts: Vec<LineWithInvisibles>,
|
||||
line_layouts: Vec<text_layout::Line>,
|
||||
snapshot: EditorSnapshot,
|
||||
}
|
||||
|
||||
@@ -2475,7 +2163,6 @@ impl PositionMap {
|
||||
let (column, x_overshoot) = if let Some(line) = self
|
||||
.line_layouts
|
||||
.get(row as usize - scroll_position.y() as usize)
|
||||
.map(|line_with_spaces| &line_with_spaces.line)
|
||||
{
|
||||
if let Some(ix) = line.index_for_x(x) {
|
||||
(ix as u32, 0.0)
|
||||
@@ -2744,7 +2431,7 @@ impl HighlightedRange {
|
||||
}
|
||||
}
|
||||
|
||||
fn position_to_display_point(
|
||||
pub fn position_to_display_point(
|
||||
position: Vector2F,
|
||||
text_bounds: RectF,
|
||||
position_map: &PositionMap,
|
||||
@@ -2761,7 +2448,7 @@ fn position_to_display_point(
|
||||
}
|
||||
}
|
||||
|
||||
fn range_to_bounds(
|
||||
pub fn range_to_bounds(
|
||||
range: &Range<DisplayPoint>,
|
||||
content_origin: Vector2F,
|
||||
scroll_left: f32,
|
||||
@@ -2789,7 +2476,7 @@ fn range_to_bounds(
|
||||
content_origin.y() + row_range.start as f32 * position_map.line_height - scroll_top;
|
||||
|
||||
for (idx, row) in row_range.enumerate() {
|
||||
let line_layout = &position_map.line_layouts[(row - start_row) as usize].line;
|
||||
let line_layout = &position_map.line_layouts[(row - start_row) as usize];
|
||||
|
||||
let start_x = if row == range.start.row() {
|
||||
content_origin.x() + line_layout.x_for_index(range.start.column() as usize)
|
||||
@@ -2829,9 +2516,8 @@ mod tests {
|
||||
Editor, MultiBuffer,
|
||||
};
|
||||
use gpui::TestAppContext;
|
||||
use log::info;
|
||||
use settings::Settings;
|
||||
use std::{num::NonZeroU32, sync::Arc};
|
||||
use std::sync::Arc;
|
||||
use util::test::sample_text;
|
||||
|
||||
#[gpui::test]
|
||||
@@ -2879,18 +2565,10 @@ mod tests {
|
||||
|
||||
let mut element = EditorElement::new(editor.read_with(cx, |editor, cx| editor.style(cx)));
|
||||
let (size, mut state) = editor.update(cx, |editor, cx| {
|
||||
let mut new_parents = Default::default();
|
||||
let mut notify_views_if_parents_change = Default::default();
|
||||
let mut layout_cx = LayoutContext::new(
|
||||
cx,
|
||||
&mut new_parents,
|
||||
&mut notify_views_if_parents_change,
|
||||
false,
|
||||
);
|
||||
element.layout(
|
||||
SizeConstraint::new(vec2f(500., 500.), vec2f(500., 500.)),
|
||||
editor,
|
||||
&mut layout_cx,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
@@ -2911,194 +2589,4 @@ mod tests {
|
||||
element.paint(&mut scene, bounds, bounds, &mut state, editor, cx);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_all_invisibles_drawing(cx: &mut TestAppContext) {
|
||||
let tab_size = 4;
|
||||
let input_text = "\t \t|\t| a b";
|
||||
let expected_invisibles = vec![
|
||||
Invisible::Tab {
|
||||
line_start_offset: 0,
|
||||
},
|
||||
Invisible::Whitespace {
|
||||
line_offset: tab_size as usize,
|
||||
},
|
||||
Invisible::Tab {
|
||||
line_start_offset: tab_size as usize + 1,
|
||||
},
|
||||
Invisible::Tab {
|
||||
line_start_offset: tab_size as usize * 2 + 1,
|
||||
},
|
||||
Invisible::Whitespace {
|
||||
line_offset: tab_size as usize * 3 + 1,
|
||||
},
|
||||
Invisible::Whitespace {
|
||||
line_offset: tab_size as usize * 3 + 3,
|
||||
},
|
||||
];
|
||||
assert_eq!(
|
||||
expected_invisibles.len(),
|
||||
input_text
|
||||
.chars()
|
||||
.filter(|initial_char| initial_char.is_whitespace())
|
||||
.count(),
|
||||
"Hardcoded expected invisibles differ from the actual ones in '{input_text}'"
|
||||
);
|
||||
|
||||
cx.update(|cx| {
|
||||
let mut test_settings = Settings::test(cx);
|
||||
test_settings.editor_defaults.show_whitespaces = Some(ShowWhitespaces::All);
|
||||
test_settings.editor_defaults.tab_size = Some(NonZeroU32::new(tab_size).unwrap());
|
||||
cx.set_global(test_settings);
|
||||
});
|
||||
let actual_invisibles =
|
||||
collect_invisibles_from_new_editor(cx, EditorMode::Full, &input_text, 500.0);
|
||||
|
||||
assert_eq!(expected_invisibles, actual_invisibles);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_invisibles_dont_appear_in_certain_editors(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
let mut test_settings = Settings::test(cx);
|
||||
test_settings.editor_defaults.show_whitespaces = Some(ShowWhitespaces::All);
|
||||
test_settings.editor_defaults.tab_size = Some(NonZeroU32::new(4).unwrap());
|
||||
cx.set_global(test_settings);
|
||||
});
|
||||
|
||||
for editor_mode_without_invisibles in [
|
||||
EditorMode::SingleLine,
|
||||
EditorMode::AutoHeight { max_lines: 100 },
|
||||
] {
|
||||
let invisibles = collect_invisibles_from_new_editor(
|
||||
cx,
|
||||
editor_mode_without_invisibles,
|
||||
"\t\t\t| | a b",
|
||||
500.0,
|
||||
);
|
||||
assert!(invisibles.is_empty(),
|
||||
"For editor mode {editor_mode_without_invisibles:?} no invisibles was expected but got {invisibles:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_wrapped_invisibles_drawing(cx: &mut TestAppContext) {
|
||||
let tab_size = 4;
|
||||
let input_text = "a\tbcd ".repeat(9);
|
||||
let repeated_invisibles = [
|
||||
Invisible::Tab {
|
||||
line_start_offset: 1,
|
||||
},
|
||||
Invisible::Whitespace {
|
||||
line_offset: tab_size as usize + 3,
|
||||
},
|
||||
Invisible::Whitespace {
|
||||
line_offset: tab_size as usize + 4,
|
||||
},
|
||||
Invisible::Whitespace {
|
||||
line_offset: tab_size as usize + 5,
|
||||
},
|
||||
];
|
||||
let expected_invisibles = std::iter::once(repeated_invisibles)
|
||||
.cycle()
|
||||
.take(9)
|
||||
.flatten()
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
expected_invisibles.len(),
|
||||
input_text
|
||||
.chars()
|
||||
.filter(|initial_char| initial_char.is_whitespace())
|
||||
.count(),
|
||||
"Hardcoded expected invisibles differ from the actual ones in '{input_text}'"
|
||||
);
|
||||
info!("Expected invisibles: {expected_invisibles:?}");
|
||||
|
||||
// Put the same string with repeating whitespace pattern into editors of various size,
|
||||
// take deliberately small steps during resizing, to put all whitespace kinds near the wrap point.
|
||||
let resize_step = 10.0;
|
||||
let mut editor_width = 200.0;
|
||||
while editor_width <= 1000.0 {
|
||||
cx.update(|cx| {
|
||||
let mut test_settings = Settings::test(cx);
|
||||
test_settings.editor_defaults.tab_size = Some(NonZeroU32::new(tab_size).unwrap());
|
||||
test_settings.editor_defaults.show_whitespaces = Some(ShowWhitespaces::All);
|
||||
test_settings.editor_defaults.preferred_line_length = Some(editor_width as u32);
|
||||
test_settings.editor_defaults.soft_wrap =
|
||||
Some(settings::SoftWrap::PreferredLineLength);
|
||||
cx.set_global(test_settings);
|
||||
});
|
||||
|
||||
let actual_invisibles =
|
||||
collect_invisibles_from_new_editor(cx, EditorMode::Full, &input_text, editor_width);
|
||||
|
||||
// Whatever the editor size is, ensure it has the same invisible kinds in the same order
|
||||
// (no good guarantees about the offsets: wrapping could trigger padding and its tests should check the offsets).
|
||||
let mut i = 0;
|
||||
for (actual_index, actual_invisible) in actual_invisibles.iter().enumerate() {
|
||||
i = actual_index;
|
||||
match expected_invisibles.get(i) {
|
||||
Some(expected_invisible) => match (expected_invisible, actual_invisible) {
|
||||
(Invisible::Whitespace { .. }, Invisible::Whitespace { .. })
|
||||
| (Invisible::Tab { .. }, Invisible::Tab { .. }) => {}
|
||||
_ => {
|
||||
panic!("At index {i}, expected invisible {expected_invisible:?} does not match actual {actual_invisible:?} by kind. Actual invisibles: {actual_invisibles:?}")
|
||||
}
|
||||
},
|
||||
None => panic!("Unexpected extra invisible {actual_invisible:?} at index {i}"),
|
||||
}
|
||||
}
|
||||
let missing_expected_invisibles = &expected_invisibles[i + 1..];
|
||||
assert!(
|
||||
missing_expected_invisibles.is_empty(),
|
||||
"Missing expected invisibles after index {i}: {missing_expected_invisibles:?}"
|
||||
);
|
||||
|
||||
editor_width += resize_step;
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_invisibles_from_new_editor(
|
||||
cx: &mut TestAppContext,
|
||||
editor_mode: EditorMode,
|
||||
input_text: &str,
|
||||
editor_width: f32,
|
||||
) -> Vec<Invisible> {
|
||||
info!(
|
||||
"Creating editor with mode {editor_mode:?}, witdh {editor_width} and text '{input_text}'"
|
||||
);
|
||||
let (_, editor) = cx.add_window(|cx| {
|
||||
let buffer = MultiBuffer::build_simple(&input_text, cx);
|
||||
Editor::new(editor_mode, buffer, None, None, cx)
|
||||
});
|
||||
|
||||
let mut element = EditorElement::new(editor.read_with(cx, |editor, cx| editor.style(cx)));
|
||||
let (_, layout_state) = editor.update(cx, |editor, cx| {
|
||||
editor.set_soft_wrap_mode(settings::SoftWrap::EditorWidth, cx);
|
||||
editor.set_wrap_width(Some(editor_width), cx);
|
||||
|
||||
let mut new_parents = Default::default();
|
||||
let mut notify_views_if_parents_change = Default::default();
|
||||
let mut layout_cx = LayoutContext::new(
|
||||
cx,
|
||||
&mut new_parents,
|
||||
&mut notify_views_if_parents_change,
|
||||
false,
|
||||
);
|
||||
element.layout(
|
||||
SizeConstraint::new(vec2f(editor_width, 500.), vec2f(editor_width, 500.)),
|
||||
editor,
|
||||
&mut layout_cx,
|
||||
)
|
||||
});
|
||||
|
||||
layout_state
|
||||
.position_map
|
||||
.line_layouts
|
||||
.iter()
|
||||
.map(|line_with_invisibles| &line_with_invisibles.invisibles)
|
||||
.flatten()
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1006,7 +1006,8 @@ mod tests {
|
||||
.zip(expected_styles.iter().cloned())
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
rendered.text, expected_text,
|
||||
rendered.text,
|
||||
dbg!(expected_text),
|
||||
"wrong text for input {blocks:?}"
|
||||
);
|
||||
assert_eq!(
|
||||
|
||||
@@ -3,7 +3,7 @@ use crate::{
|
||||
movement::surrounding_word, persistence::DB, scroll::ScrollAnchor, Anchor, Autoscroll, Editor,
|
||||
Event, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, NavigationData, ToPoint as _,
|
||||
};
|
||||
use anyhow::{Context, Result};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use collections::HashSet;
|
||||
use futures::future::try_join_all;
|
||||
use gpui::{
|
||||
@@ -27,7 +27,7 @@ use std::{
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
use text::Selection;
|
||||
use util::{paths::FILE_ROW_COLUMN_DELIMITER, ResultExt, TryFutureExt};
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
use workspace::item::{BreadcrumbText, FollowableItemHandle};
|
||||
use workspace::{
|
||||
item::{FollowableItem, Item, ItemEvent, ItemHandle, ProjectItem},
|
||||
@@ -566,7 +566,7 @@ impl Item for Editor {
|
||||
cx: &AppContext,
|
||||
) -> AnyElement<T> {
|
||||
Flex::row()
|
||||
.with_child(Label::new(self.title(cx).to_string(), style.label.clone()).into_any())
|
||||
.with_child(Label::new(self.title(cx).to_string(), style.label.clone()).aligned())
|
||||
.with_children(detail.and_then(|detail| {
|
||||
let path = path_for_buffer(&self.buffer, detail, false, cx)?;
|
||||
let description = path.to_string_lossy();
|
||||
@@ -580,7 +580,6 @@ impl Item for Editor {
|
||||
.aligned(),
|
||||
)
|
||||
}))
|
||||
.align_children_center()
|
||||
.into_any()
|
||||
}
|
||||
|
||||
@@ -637,7 +636,7 @@ impl Item for Editor {
|
||||
project: ModelHandle<Project>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
self.report_editor_event("save", None, cx);
|
||||
self.report_editor_event("save", cx);
|
||||
let format = self.perform_format(project.clone(), FormatTrigger::Save, cx);
|
||||
let buffers = self.buffer().clone().read(cx).all_buffers();
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
@@ -686,11 +685,6 @@ impl Item for Editor {
|
||||
.as_singleton()
|
||||
.expect("cannot call save_as on an excerpt list");
|
||||
|
||||
let file_extension = abs_path
|
||||
.extension()
|
||||
.map(|a| a.to_string_lossy().to_string());
|
||||
self.report_editor_event("save", file_extension, cx);
|
||||
|
||||
project.update(cx, |project, cx| {
|
||||
project.save_buffer_as(buffer, abs_path, cx)
|
||||
})
|
||||
@@ -710,10 +704,10 @@ impl Item for Editor {
|
||||
this.update(&mut cx, |editor, cx| {
|
||||
editor.request_autoscroll(Autoscroll::fit(), cx)
|
||||
})?;
|
||||
buffer.update(&mut cx, |buffer, cx| {
|
||||
buffer.update(&mut cx, |buffer, _| {
|
||||
if let Some(transaction) = transaction {
|
||||
if !buffer.is_singleton() {
|
||||
buffer.push_transaction(&transaction.0, cx);
|
||||
buffer.push_transaction(&transaction.0);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -870,13 +864,16 @@ impl Item for Editor {
|
||||
let buffer = project_item
|
||||
.downcast::<Buffer>()
|
||||
.context("Project item at stored path was not a buffer")?;
|
||||
Ok(pane.update(&mut cx, |_, cx| {
|
||||
cx.add_view(|cx| {
|
||||
let pane = pane
|
||||
.upgrade(&cx)
|
||||
.ok_or_else(|| anyhow!("pane was dropped"))?;
|
||||
Ok(cx.update(|cx| {
|
||||
cx.add_view(&pane, |cx| {
|
||||
let mut editor = Editor::for_buffer(buffer, Some(project), cx);
|
||||
editor.read_scroll_position_from_db(item_id, workspace_id, cx);
|
||||
editor
|
||||
})
|
||||
})?)
|
||||
}))
|
||||
})
|
||||
})
|
||||
.unwrap_or_else(|error| Task::ready(Err(error)))
|
||||
@@ -1117,11 +1114,7 @@ impl View for CursorPosition {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
if let Some(position) = self.position {
|
||||
let theme = &cx.global::<Settings>().theme.workspace.status_bar;
|
||||
let mut text = format!(
|
||||
"{}{FILE_ROW_COLUMN_DELIMITER}{}",
|
||||
position.row + 1,
|
||||
position.column + 1
|
||||
);
|
||||
let mut text = format!("{},{}", position.row + 1, position.column + 1);
|
||||
if self.selected_count > 0 {
|
||||
write!(text, " ({} selected)", self.selected_count).unwrap();
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ pub struct ExcerptId(usize);
|
||||
|
||||
pub struct MultiBuffer {
|
||||
snapshot: RefCell<MultiBufferSnapshot>,
|
||||
buffers: RefCell<HashMap<u64, BufferState>>,
|
||||
buffers: RefCell<HashMap<usize, BufferState>>,
|
||||
next_excerpt_id: usize,
|
||||
subscriptions: Topic,
|
||||
singleton: bool,
|
||||
@@ -85,7 +85,7 @@ struct History {
|
||||
#[derive(Clone)]
|
||||
struct Transaction {
|
||||
id: TransactionId,
|
||||
buffer_transactions: HashMap<u64, text::TransactionId>,
|
||||
buffer_transactions: HashMap<usize, text::TransactionId>,
|
||||
first_edit_at: Instant,
|
||||
last_edit_at: Instant,
|
||||
suppress_grouping: bool,
|
||||
@@ -145,7 +145,7 @@ pub struct ExcerptBoundary {
|
||||
struct Excerpt {
|
||||
id: ExcerptId,
|
||||
locator: Locator,
|
||||
buffer_id: u64,
|
||||
buffer_id: usize,
|
||||
buffer: BufferSnapshot,
|
||||
range: ExcerptRange<text::Anchor>,
|
||||
max_buffer_row: u32,
|
||||
@@ -337,7 +337,7 @@ impl MultiBuffer {
|
||||
offset: T,
|
||||
theme: Option<&SyntaxTheme>,
|
||||
cx: &AppContext,
|
||||
) -> Option<(u64, Vec<OutlineItem<Anchor>>)> {
|
||||
) -> Option<(usize, Vec<OutlineItem<Anchor>>)> {
|
||||
self.read(cx).symbols_containing(offset, theme)
|
||||
}
|
||||
|
||||
@@ -394,7 +394,7 @@ impl MultiBuffer {
|
||||
is_insertion: bool,
|
||||
original_indent_column: u32,
|
||||
}
|
||||
let mut buffer_edits: HashMap<u64, Vec<BufferEdit>> = Default::default();
|
||||
let mut buffer_edits: HashMap<usize, Vec<BufferEdit>> = Default::default();
|
||||
let mut cursor = snapshot.excerpts.cursor::<usize>();
|
||||
for (ix, (range, new_text)) in edits.enumerate() {
|
||||
let new_text: Arc<str> = new_text.into();
|
||||
@@ -593,7 +593,7 @@ impl MultiBuffer {
|
||||
if let Some(transaction_id) =
|
||||
buffer.update(cx, |buffer, cx| buffer.end_transaction_at(now, cx))
|
||||
{
|
||||
buffer_transactions.insert(buffer.read(cx).remote_id(), transaction_id);
|
||||
buffer_transactions.insert(buffer.id(), transaction_id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -614,12 +614,12 @@ impl MultiBuffer {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push_transaction<'a, T>(&mut self, buffer_transactions: T, cx: &mut ModelContext<Self>)
|
||||
pub fn push_transaction<'a, T>(&mut self, buffer_transactions: T)
|
||||
where
|
||||
T: IntoIterator<Item = (&'a ModelHandle<Buffer>, &'a language::Transaction)>,
|
||||
{
|
||||
self.history
|
||||
.push_transaction(buffer_transactions, Instant::now(), cx);
|
||||
.push_transaction(buffer_transactions, Instant::now());
|
||||
self.history.finalize_last_transaction();
|
||||
}
|
||||
|
||||
@@ -644,7 +644,7 @@ impl MultiBuffer {
|
||||
cursor_shape: CursorShape,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
let mut selections_by_buffer: HashMap<u64, Vec<Selection<text::Anchor>>> =
|
||||
let mut selections_by_buffer: HashMap<usize, Vec<Selection<text::Anchor>>> =
|
||||
Default::default();
|
||||
let snapshot = self.read(cx);
|
||||
let mut cursor = snapshot.excerpts.cursor::<Option<&Locator>>();
|
||||
@@ -785,8 +785,8 @@ impl MultiBuffer {
|
||||
let (mut tx, rx) = mpsc::channel(256);
|
||||
let task = cx.spawn(|this, mut cx| async move {
|
||||
for (buffer, ranges) in excerpts {
|
||||
let (buffer_id, buffer_snapshot) =
|
||||
buffer.read_with(&cx, |buffer, _| (buffer.remote_id(), buffer.snapshot()));
|
||||
let buffer_id = buffer.id();
|
||||
let buffer_snapshot = buffer.read_with(&cx, |buffer, _| buffer.snapshot());
|
||||
|
||||
let mut excerpt_ranges = Vec::new();
|
||||
let mut range_counts = Vec::new();
|
||||
@@ -855,7 +855,7 @@ impl MultiBuffer {
|
||||
where
|
||||
O: text::ToPoint + text::ToOffset,
|
||||
{
|
||||
let buffer_id = buffer.read(cx).remote_id();
|
||||
let buffer_id = buffer.id();
|
||||
let buffer_snapshot = buffer.read(cx).snapshot();
|
||||
let (excerpt_ranges, range_counts) =
|
||||
build_excerpt_ranges(&buffer_snapshot, &ranges, context_line_count);
|
||||
@@ -924,7 +924,7 @@ impl MultiBuffer {
|
||||
|
||||
self.sync(cx);
|
||||
|
||||
let buffer_id = buffer.read(cx).remote_id();
|
||||
let buffer_id = buffer.id();
|
||||
let buffer_snapshot = buffer.read(cx).snapshot();
|
||||
|
||||
let mut buffers = self.buffers.borrow_mut();
|
||||
@@ -1051,7 +1051,7 @@ impl MultiBuffer {
|
||||
let buffers = self.buffers.borrow();
|
||||
let mut cursor = snapshot.excerpts.cursor::<Option<&Locator>>();
|
||||
for locator in buffers
|
||||
.get(&buffer.read(cx).remote_id())
|
||||
.get(&buffer.id())
|
||||
.map(|state| &state.excerpts)
|
||||
.into_iter()
|
||||
.flatten()
|
||||
@@ -1165,9 +1165,6 @@ impl MultiBuffer {
|
||||
) {
|
||||
self.sync(cx);
|
||||
let ids = excerpt_ids.into_iter().collect::<Vec<_>>();
|
||||
if ids.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut buffers = self.buffers.borrow_mut();
|
||||
let mut snapshot = self.snapshot.borrow_mut();
|
||||
@@ -1324,7 +1321,7 @@ impl MultiBuffer {
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn buffer(&self, buffer_id: u64) -> Option<ModelHandle<Buffer>> {
|
||||
pub fn buffer(&self, buffer_id: usize) -> Option<ModelHandle<Buffer>> {
|
||||
self.buffers
|
||||
.borrow()
|
||||
.get(&buffer_id)
|
||||
@@ -1481,8 +1478,8 @@ impl MultiBuffer {
|
||||
for (locator, buffer, buffer_edited) in excerpts_to_edit {
|
||||
new_excerpts.push_tree(cursor.slice(&Some(locator), Bias::Left, &()), &());
|
||||
let old_excerpt = cursor.item().unwrap();
|
||||
let buffer_id = buffer.id();
|
||||
let buffer = buffer.read(cx);
|
||||
let buffer_id = buffer.remote_id();
|
||||
|
||||
let mut new_excerpt;
|
||||
if buffer_edited {
|
||||
@@ -1608,11 +1605,11 @@ impl MultiBuffer {
|
||||
let buffer_handle = if rng.gen() || self.buffers.borrow().is_empty() {
|
||||
let text = RandomCharIter::new(&mut *rng).take(10).collect::<String>();
|
||||
buffers.push(cx.add_model(|cx| Buffer::new(0, text, cx)));
|
||||
let buffer = buffers.last().unwrap().read(cx);
|
||||
let buffer = buffers.last().unwrap();
|
||||
log::info!(
|
||||
"Creating new buffer {} with text: {:?}",
|
||||
buffer.remote_id(),
|
||||
buffer.text()
|
||||
buffer.id(),
|
||||
buffer.read(cx).text()
|
||||
);
|
||||
buffers.last().unwrap().clone()
|
||||
} else {
|
||||
@@ -1640,7 +1637,7 @@ impl MultiBuffer {
|
||||
.collect::<Vec<_>>();
|
||||
log::info!(
|
||||
"Inserting excerpts from buffer {} and ranges {:?}: {:?}",
|
||||
buffer_handle.read(cx).remote_id(),
|
||||
buffer_handle.id(),
|
||||
ranges.iter().map(|r| &r.context).collect::<Vec<_>>(),
|
||||
ranges
|
||||
.iter()
|
||||
@@ -1833,7 +1830,7 @@ impl MultiBufferSnapshot {
|
||||
(start..end, word_kind)
|
||||
}
|
||||
|
||||
pub fn as_singleton(&self) -> Option<(&ExcerptId, u64, &BufferSnapshot)> {
|
||||
pub fn as_singleton(&self) -> Option<(&ExcerptId, usize, &BufferSnapshot)> {
|
||||
if self.singleton {
|
||||
self.excerpts
|
||||
.iter()
|
||||
@@ -2941,7 +2938,7 @@ impl MultiBufferSnapshot {
|
||||
&self,
|
||||
offset: T,
|
||||
theme: Option<&SyntaxTheme>,
|
||||
) -> Option<(u64, Vec<OutlineItem<Anchor>>)> {
|
||||
) -> Option<(usize, Vec<OutlineItem<Anchor>>)> {
|
||||
let anchor = self.anchor_before(offset);
|
||||
let excerpt_id = anchor.excerpt_id();
|
||||
let excerpt = self.excerpt(excerpt_id)?;
|
||||
@@ -2981,7 +2978,7 @@ impl MultiBufferSnapshot {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn buffer_id_for_excerpt(&self, excerpt_id: ExcerptId) -> Option<u64> {
|
||||
pub fn buffer_id_for_excerpt(&self, excerpt_id: ExcerptId) -> Option<usize> {
|
||||
Some(self.excerpt(excerpt_id)?.buffer_id)
|
||||
}
|
||||
|
||||
@@ -3119,7 +3116,7 @@ impl History {
|
||||
fn end_transaction(
|
||||
&mut self,
|
||||
now: Instant,
|
||||
buffer_transactions: HashMap<u64, TransactionId>,
|
||||
buffer_transactions: HashMap<usize, TransactionId>,
|
||||
) -> bool {
|
||||
assert_ne!(self.transaction_depth, 0);
|
||||
self.transaction_depth -= 1;
|
||||
@@ -3144,12 +3141,8 @@ impl History {
|
||||
}
|
||||
}
|
||||
|
||||
fn push_transaction<'a, T>(
|
||||
&mut self,
|
||||
buffer_transactions: T,
|
||||
now: Instant,
|
||||
cx: &mut ModelContext<MultiBuffer>,
|
||||
) where
|
||||
fn push_transaction<'a, T>(&mut self, buffer_transactions: T, now: Instant)
|
||||
where
|
||||
T: IntoIterator<Item = (&'a ModelHandle<Buffer>, &'a language::Transaction)>,
|
||||
{
|
||||
assert_eq!(self.transaction_depth, 0);
|
||||
@@ -3157,7 +3150,7 @@ impl History {
|
||||
id: self.next_transaction_id.tick(),
|
||||
buffer_transactions: buffer_transactions
|
||||
.into_iter()
|
||||
.map(|(buffer, transaction)| (buffer.read(cx).remote_id(), transaction.id))
|
||||
.map(|(buffer, transaction)| (buffer.id(), transaction.id))
|
||||
.collect(),
|
||||
first_edit_at: now,
|
||||
last_edit_at: now,
|
||||
@@ -3254,7 +3247,7 @@ impl Excerpt {
|
||||
fn new(
|
||||
id: ExcerptId,
|
||||
locator: Locator,
|
||||
buffer_id: u64,
|
||||
buffer_id: usize,
|
||||
buffer: BufferSnapshot,
|
||||
range: ExcerptRange<text::Anchor>,
|
||||
has_trailing_newline: bool,
|
||||
@@ -4083,25 +4076,19 @@ mod tests {
|
||||
|
||||
let leader_multibuffer = cx.add_model(|_| MultiBuffer::new(0));
|
||||
let follower_multibuffer = cx.add_model(|_| MultiBuffer::new(0));
|
||||
let follower_edit_event_count = Rc::new(RefCell::new(0));
|
||||
|
||||
follower_multibuffer.update(cx, |_, cx| {
|
||||
let follower_edit_event_count = follower_edit_event_count.clone();
|
||||
cx.subscribe(
|
||||
&leader_multibuffer,
|
||||
move |follower, _, event, cx| match event.clone() {
|
||||
cx.subscribe(&leader_multibuffer, |follower, _, event, cx| {
|
||||
match event.clone() {
|
||||
Event::ExcerptsAdded {
|
||||
buffer,
|
||||
predecessor,
|
||||
excerpts,
|
||||
} => follower.insert_excerpts_with_ids_after(predecessor, buffer, excerpts, cx),
|
||||
Event::ExcerptsRemoved { ids } => follower.remove_excerpts(ids, cx),
|
||||
Event::Edited => {
|
||||
*follower_edit_event_count.borrow_mut() += 1;
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
)
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
});
|
||||
|
||||
@@ -4140,7 +4127,6 @@ mod tests {
|
||||
leader_multibuffer.read(cx).snapshot(cx).text(),
|
||||
follower_multibuffer.read(cx).snapshot(cx).text(),
|
||||
);
|
||||
assert_eq!(*follower_edit_event_count.borrow(), 2);
|
||||
|
||||
leader_multibuffer.update(cx, |leader, cx| {
|
||||
let excerpt_ids = leader.excerpt_ids();
|
||||
@@ -4150,27 +4136,6 @@ mod tests {
|
||||
leader_multibuffer.read(cx).snapshot(cx).text(),
|
||||
follower_multibuffer.read(cx).snapshot(cx).text(),
|
||||
);
|
||||
assert_eq!(*follower_edit_event_count.borrow(), 3);
|
||||
|
||||
// Removing an empty set of excerpts is a noop.
|
||||
leader_multibuffer.update(cx, |leader, cx| {
|
||||
leader.remove_excerpts([], cx);
|
||||
});
|
||||
assert_eq!(
|
||||
leader_multibuffer.read(cx).snapshot(cx).text(),
|
||||
follower_multibuffer.read(cx).snapshot(cx).text(),
|
||||
);
|
||||
assert_eq!(*follower_edit_event_count.borrow(), 3);
|
||||
|
||||
// Adding an empty set of excerpts is a noop.
|
||||
leader_multibuffer.update(cx, |leader, cx| {
|
||||
leader.push_excerpts::<usize>(buffer_2.clone(), [], cx);
|
||||
});
|
||||
assert_eq!(
|
||||
leader_multibuffer.read(cx).snapshot(cx).text(),
|
||||
follower_multibuffer.read(cx).snapshot(cx).text(),
|
||||
);
|
||||
assert_eq!(*follower_edit_event_count.borrow(), 3);
|
||||
|
||||
leader_multibuffer.update(cx, |leader, cx| {
|
||||
leader.clear(cx);
|
||||
@@ -4179,7 +4144,6 @@ mod tests {
|
||||
leader_multibuffer.read(cx).snapshot(cx).text(),
|
||||
follower_multibuffer.read(cx).snapshot(cx).text(),
|
||||
);
|
||||
assert_eq!(*follower_edit_event_count.borrow(), 4);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
@@ -4751,7 +4715,7 @@ mod tests {
|
||||
"Inserting excerpt at {} of {} for buffer {}: {:?}[{:?}] = {:?}",
|
||||
excerpt_ix,
|
||||
expected_excerpts.len(),
|
||||
buffer_handle.read(cx).remote_id(),
|
||||
buffer_handle.id(),
|
||||
buffer.text(),
|
||||
start_ix..end_ix,
|
||||
&buffer.text()[start_ix..end_ix]
|
||||
@@ -4837,8 +4801,8 @@ mod tests {
|
||||
|
||||
let mut excerpt_starts = excerpt_starts.into_iter();
|
||||
for (buffer, range) in &expected_excerpts {
|
||||
let buffer_id = buffer.id();
|
||||
let buffer = buffer.read(cx);
|
||||
let buffer_id = buffer.remote_id();
|
||||
let buffer_range = range.to_offset(buffer);
|
||||
let buffer_start_point = buffer.offset_to_point(buffer_range.start);
|
||||
let buffer_start_point_utf16 =
|
||||
|
||||
@@ -8,7 +8,7 @@ use sum_tree::Bias;
|
||||
|
||||
#[derive(Clone, Copy, Eq, PartialEq, Debug, Hash)]
|
||||
pub struct Anchor {
|
||||
pub(crate) buffer_id: Option<u64>,
|
||||
pub(crate) buffer_id: Option<usize>,
|
||||
pub(crate) excerpt_id: ExcerptId,
|
||||
pub(crate) text_anchor: text::Anchor,
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use std::cmp;
|
||||
|
||||
use gpui::ViewContext;
|
||||
use gpui::{text_layout, ViewContext};
|
||||
use language::Point;
|
||||
|
||||
use crate::{display_map::ToDisplayPoint, Editor, EditorMode, LineWithInvisibles};
|
||||
use crate::{display_map::ToDisplayPoint, Editor, EditorMode};
|
||||
|
||||
#[derive(PartialEq, Eq)]
|
||||
pub enum Autoscroll {
|
||||
@@ -172,7 +172,7 @@ impl Editor {
|
||||
viewport_width: f32,
|
||||
scroll_width: f32,
|
||||
max_glyph_width: f32,
|
||||
layouts: &[LineWithInvisibles],
|
||||
layouts: &[text_layout::Line],
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> bool {
|
||||
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
|
||||
@@ -194,13 +194,10 @@ impl Editor {
|
||||
let end_column = cmp::min(display_map.line_len(head.row()), head.column() + 3);
|
||||
target_left = target_left.min(
|
||||
layouts[(head.row() - start_row) as usize]
|
||||
.line
|
||||
.x_for_index(start_column as usize),
|
||||
);
|
||||
target_right = target_right.max(
|
||||
layouts[(head.row() - start_row) as usize]
|
||||
.line
|
||||
.x_for_index(end_column as usize)
|
||||
layouts[(head.row() - start_row) as usize].x_for_index(end_column as usize)
|
||||
+ max_glyph_width,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ menu = { path = "../menu" }
|
||||
picker = { path = "../picker" }
|
||||
project = { path = "../project" }
|
||||
settings = { path = "../settings" }
|
||||
text = { path = "../text" }
|
||||
util = { path = "../util" }
|
||||
theme = { path = "../theme" }
|
||||
workspace = { path = "../workspace" }
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use editor::{scroll::autoscroll::Autoscroll, Bias, Editor};
|
||||
use fuzzy::PathMatch;
|
||||
use gpui::{
|
||||
actions, elements::*, AppContext, ModelHandle, MouseState, Task, ViewContext, WeakViewHandle,
|
||||
@@ -13,8 +12,7 @@ use std::{
|
||||
Arc,
|
||||
},
|
||||
};
|
||||
use text::Point;
|
||||
use util::{paths::PathLikeWithPosition, post_inc, ResultExt};
|
||||
use util::{post_inc, ResultExt};
|
||||
use workspace::Workspace;
|
||||
|
||||
pub type FileFinder = Picker<FileFinderDelegate>;
|
||||
@@ -25,7 +23,7 @@ pub struct FileFinderDelegate {
|
||||
search_count: usize,
|
||||
latest_search_id: usize,
|
||||
latest_search_did_cancel: bool,
|
||||
latest_search_query: Option<PathLikeWithPosition<FileSearchQuery>>,
|
||||
latest_search_query: String,
|
||||
relative_to: Option<Arc<Path>>,
|
||||
matches: Vec<PathMatch>,
|
||||
selected: Option<(usize, Arc<Path>)>,
|
||||
@@ -62,21 +60,6 @@ pub enum Event {
|
||||
Dismissed,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct FileSearchQuery {
|
||||
raw_query: String,
|
||||
file_query_end: Option<usize>,
|
||||
}
|
||||
|
||||
impl FileSearchQuery {
|
||||
fn path_query(&self) -> &str {
|
||||
match self.file_query_end {
|
||||
Some(file_path_end) => &self.raw_query[..file_path_end],
|
||||
None => &self.raw_query,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FileFinderDelegate {
|
||||
fn labels_for_match(&self, path_match: &PathMatch) -> (String, Vec<usize>, String, Vec<usize>) {
|
||||
let path = &path_match.path;
|
||||
@@ -120,7 +103,7 @@ impl FileFinderDelegate {
|
||||
search_count: 0,
|
||||
latest_search_id: 0,
|
||||
latest_search_did_cancel: false,
|
||||
latest_search_query: None,
|
||||
latest_search_query: String::new(),
|
||||
relative_to,
|
||||
matches: Vec::new(),
|
||||
selected: None,
|
||||
@@ -128,11 +111,7 @@ impl FileFinderDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
fn spawn_search(
|
||||
&mut self,
|
||||
query: PathLikeWithPosition<FileSearchQuery>,
|
||||
cx: &mut ViewContext<FileFinder>,
|
||||
) -> Task<()> {
|
||||
fn spawn_search(&mut self, query: String, cx: &mut ViewContext<FileFinder>) -> Task<()> {
|
||||
let relative_to = self.relative_to.clone();
|
||||
let worktrees = self
|
||||
.project
|
||||
@@ -161,7 +140,7 @@ impl FileFinderDelegate {
|
||||
cx.spawn(|picker, mut cx| async move {
|
||||
let matches = fuzzy::match_path_sets(
|
||||
candidate_sets.as_slice(),
|
||||
query.path_like.path_query(),
|
||||
&query,
|
||||
relative_to,
|
||||
false,
|
||||
100,
|
||||
@@ -184,24 +163,18 @@ impl FileFinderDelegate {
|
||||
&mut self,
|
||||
search_id: usize,
|
||||
did_cancel: bool,
|
||||
query: PathLikeWithPosition<FileSearchQuery>,
|
||||
query: String,
|
||||
matches: Vec<PathMatch>,
|
||||
cx: &mut ViewContext<FileFinder>,
|
||||
) {
|
||||
if search_id >= self.latest_search_id {
|
||||
self.latest_search_id = search_id;
|
||||
if self.latest_search_did_cancel
|
||||
&& Some(query.path_like.path_query())
|
||||
== self
|
||||
.latest_search_query
|
||||
.as_ref()
|
||||
.map(|query| query.path_like.path_query())
|
||||
{
|
||||
if self.latest_search_did_cancel && query == self.latest_search_query {
|
||||
util::extend_sorted(&mut self.matches, matches.into_iter(), 100, |a, b| b.cmp(a));
|
||||
} else {
|
||||
self.matches = matches;
|
||||
}
|
||||
self.latest_search_query = Some(query);
|
||||
self.latest_search_query = query;
|
||||
self.latest_search_did_cancel = did_cancel;
|
||||
cx.notify();
|
||||
}
|
||||
@@ -236,25 +209,13 @@ impl PickerDelegate for FileFinderDelegate {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn update_matches(&mut self, raw_query: String, cx: &mut ViewContext<FileFinder>) -> Task<()> {
|
||||
if raw_query.is_empty() {
|
||||
fn update_matches(&mut self, query: String, cx: &mut ViewContext<FileFinder>) -> Task<()> {
|
||||
if query.is_empty() {
|
||||
self.latest_search_id = post_inc(&mut self.search_count);
|
||||
self.matches.clear();
|
||||
cx.notify();
|
||||
Task::ready(())
|
||||
} else {
|
||||
let raw_query = &raw_query;
|
||||
let query = PathLikeWithPosition::parse_str(raw_query, |path_like_str| {
|
||||
Ok::<_, std::convert::Infallible>(FileSearchQuery {
|
||||
raw_query: raw_query.to_owned(),
|
||||
file_query_end: if path_like_str == raw_query {
|
||||
None
|
||||
} else {
|
||||
Some(path_like_str.len())
|
||||
},
|
||||
})
|
||||
})
|
||||
.expect("infallible");
|
||||
self.spawn_search(query, cx)
|
||||
}
|
||||
}
|
||||
@@ -267,49 +228,12 @@ impl PickerDelegate for FileFinderDelegate {
|
||||
path: m.path.clone(),
|
||||
};
|
||||
|
||||
let open_task = workspace.update(cx, |workspace, cx| {
|
||||
workspace.open_path(project_path.clone(), None, true, cx)
|
||||
});
|
||||
|
||||
let workspace = workspace.downgrade();
|
||||
|
||||
let row = self
|
||||
.latest_search_query
|
||||
.as_ref()
|
||||
.and_then(|query| query.row)
|
||||
.map(|row| row.saturating_sub(1));
|
||||
let col = self
|
||||
.latest_search_query
|
||||
.as_ref()
|
||||
.and_then(|query| query.column)
|
||||
.unwrap_or(0)
|
||||
.saturating_sub(1);
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
let item = open_task.await.log_err()?;
|
||||
if let Some(row) = row {
|
||||
if let Some(active_editor) = item.downcast::<Editor>() {
|
||||
active_editor
|
||||
.downgrade()
|
||||
.update(&mut cx, |editor, cx| {
|
||||
let snapshot = editor.snapshot(cx).display_snapshot;
|
||||
let point = snapshot
|
||||
.buffer_snapshot
|
||||
.clip_point(Point::new(row, col), Bias::Left);
|
||||
editor.change_selections(Some(Autoscroll::center()), cx, |s| {
|
||||
s.select_ranges([point..point])
|
||||
});
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace
|
||||
.update(&mut cx, |workspace, cx| workspace.dismiss_modal(cx))
|
||||
.log_err();
|
||||
|
||||
Some(())
|
||||
.open_path(project_path.clone(), None, true, cx)
|
||||
.detach_and_log_err(cx);
|
||||
workspace.dismiss_modal(cx);
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -346,7 +270,6 @@ impl PickerDelegate for FileFinderDelegate {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use editor::Editor;
|
||||
use gpui::executor::Deterministic;
|
||||
use menu::{Confirm, SelectNext};
|
||||
use serde_json::json;
|
||||
use workspace::{AppState, Workspace};
|
||||
@@ -414,186 +337,6 @@ mod tests {
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_row_column_numbers_query_inside_file(
|
||||
deterministic: Arc<Deterministic>,
|
||||
cx: &mut gpui::TestAppContext,
|
||||
) {
|
||||
let app_state = cx.update(|cx| {
|
||||
super::init(cx);
|
||||
editor::init(cx);
|
||||
AppState::test(cx)
|
||||
});
|
||||
|
||||
let first_file_name = "first.rs";
|
||||
let first_file_contents = "// First Rust file";
|
||||
app_state
|
||||
.fs
|
||||
.as_fake()
|
||||
.insert_tree(
|
||||
"/src",
|
||||
json!({
|
||||
"test": {
|
||||
first_file_name: first_file_contents,
|
||||
"second.rs": "// Second Rust file",
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
|
||||
let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
|
||||
cx.dispatch_action(window_id, Toggle);
|
||||
let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap());
|
||||
|
||||
let file_query = &first_file_name[..3];
|
||||
let file_row = 1;
|
||||
let file_column = 3;
|
||||
assert!(file_column <= first_file_contents.len());
|
||||
let query_inside_file = format!("{file_query}:{file_row}:{file_column}");
|
||||
finder
|
||||
.update(cx, |finder, cx| {
|
||||
finder
|
||||
.delegate_mut()
|
||||
.update_matches(query_inside_file.to_string(), cx)
|
||||
})
|
||||
.await;
|
||||
finder.read_with(cx, |finder, _| {
|
||||
let finder = finder.delegate();
|
||||
assert_eq!(finder.matches.len(), 1);
|
||||
let latest_search_query = finder
|
||||
.latest_search_query
|
||||
.as_ref()
|
||||
.expect("Finder should have a query after the update_matches call");
|
||||
assert_eq!(latest_search_query.path_like.raw_query, query_inside_file);
|
||||
assert_eq!(
|
||||
latest_search_query.path_like.file_query_end,
|
||||
Some(file_query.len())
|
||||
);
|
||||
assert_eq!(latest_search_query.row, Some(file_row));
|
||||
assert_eq!(latest_search_query.column, Some(file_column as u32));
|
||||
});
|
||||
|
||||
let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone());
|
||||
cx.dispatch_action(window_id, SelectNext);
|
||||
cx.dispatch_action(window_id, Confirm);
|
||||
active_pane
|
||||
.condition(cx, |pane, _| pane.active_item().is_some())
|
||||
.await;
|
||||
let editor = cx.update(|cx| {
|
||||
let active_item = active_pane.read(cx).active_item().unwrap();
|
||||
active_item.downcast::<Editor>().unwrap()
|
||||
});
|
||||
deterministic.advance_clock(std::time::Duration::from_secs(2));
|
||||
deterministic.start_waiting();
|
||||
deterministic.finish_waiting();
|
||||
editor.update(cx, |editor, cx| {
|
||||
let all_selections = editor.selections.all_adjusted(cx);
|
||||
assert_eq!(
|
||||
all_selections.len(),
|
||||
1,
|
||||
"Expected to have 1 selection (caret) after file finder confirm, but got: {all_selections:?}"
|
||||
);
|
||||
let caret_selection = all_selections.into_iter().next().unwrap();
|
||||
assert_eq!(caret_selection.start, caret_selection.end,
|
||||
"Caret selection should have its start and end at the same position");
|
||||
assert_eq!(file_row, caret_selection.start.row + 1,
|
||||
"Query inside file should get caret with the same focus row");
|
||||
assert_eq!(file_column, caret_selection.start.column as usize + 1,
|
||||
"Query inside file should get caret with the same focus column");
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_row_column_numbers_query_outside_file(
|
||||
deterministic: Arc<Deterministic>,
|
||||
cx: &mut gpui::TestAppContext,
|
||||
) {
|
||||
let app_state = cx.update(|cx| {
|
||||
super::init(cx);
|
||||
editor::init(cx);
|
||||
AppState::test(cx)
|
||||
});
|
||||
|
||||
let first_file_name = "first.rs";
|
||||
let first_file_contents = "// First Rust file";
|
||||
app_state
|
||||
.fs
|
||||
.as_fake()
|
||||
.insert_tree(
|
||||
"/src",
|
||||
json!({
|
||||
"test": {
|
||||
first_file_name: first_file_contents,
|
||||
"second.rs": "// Second Rust file",
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
|
||||
let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
|
||||
cx.dispatch_action(window_id, Toggle);
|
||||
let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap());
|
||||
|
||||
let file_query = &first_file_name[..3];
|
||||
let file_row = 200;
|
||||
let file_column = 300;
|
||||
assert!(file_column > first_file_contents.len());
|
||||
let query_outside_file = format!("{file_query}:{file_row}:{file_column}");
|
||||
finder
|
||||
.update(cx, |finder, cx| {
|
||||
finder
|
||||
.delegate_mut()
|
||||
.update_matches(query_outside_file.to_string(), cx)
|
||||
})
|
||||
.await;
|
||||
finder.read_with(cx, |finder, _| {
|
||||
let finder = finder.delegate();
|
||||
assert_eq!(finder.matches.len(), 1);
|
||||
let latest_search_query = finder
|
||||
.latest_search_query
|
||||
.as_ref()
|
||||
.expect("Finder should have a query after the update_matches call");
|
||||
assert_eq!(latest_search_query.path_like.raw_query, query_outside_file);
|
||||
assert_eq!(
|
||||
latest_search_query.path_like.file_query_end,
|
||||
Some(file_query.len())
|
||||
);
|
||||
assert_eq!(latest_search_query.row, Some(file_row));
|
||||
assert_eq!(latest_search_query.column, Some(file_column as u32));
|
||||
});
|
||||
|
||||
let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone());
|
||||
cx.dispatch_action(window_id, SelectNext);
|
||||
cx.dispatch_action(window_id, Confirm);
|
||||
active_pane
|
||||
.condition(cx, |pane, _| pane.active_item().is_some())
|
||||
.await;
|
||||
let editor = cx.update(|cx| {
|
||||
let active_item = active_pane.read(cx).active_item().unwrap();
|
||||
active_item.downcast::<Editor>().unwrap()
|
||||
});
|
||||
deterministic.advance_clock(std::time::Duration::from_secs(2));
|
||||
deterministic.start_waiting();
|
||||
deterministic.finish_waiting();
|
||||
editor.update(cx, |editor, cx| {
|
||||
let all_selections = editor.selections.all_adjusted(cx);
|
||||
assert_eq!(
|
||||
all_selections.len(),
|
||||
1,
|
||||
"Expected to have 1 selection (caret) after file finder confirm, but got: {all_selections:?}"
|
||||
);
|
||||
let caret_selection = all_selections.into_iter().next().unwrap();
|
||||
assert_eq!(caret_selection.start, caret_selection.end,
|
||||
"Caret selection should have its start and end at the same position");
|
||||
assert_eq!(0, caret_selection.start.row,
|
||||
"Excessive rows (as in query outside file borders) should get trimmed to last file row");
|
||||
assert_eq!(first_file_contents.len(), caret_selection.start.column as usize,
|
||||
"Excessive columns (as in query outside file borders) should get trimmed to selected row's last column");
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_matching_cancellation(cx: &mut gpui::TestAppContext) {
|
||||
let app_state = cx.update(AppState::test);
|
||||
@@ -628,7 +371,7 @@ mod tests {
|
||||
)
|
||||
});
|
||||
|
||||
let query = test_path_like("hi");
|
||||
let query = "hi".to_string();
|
||||
finder
|
||||
.update(cx, |f, cx| f.delegate_mut().spawn_search(query.clone(), cx))
|
||||
.await;
|
||||
@@ -712,9 +455,7 @@ mod tests {
|
||||
)
|
||||
});
|
||||
finder
|
||||
.update(cx, |f, cx| {
|
||||
f.delegate_mut().spawn_search(test_path_like("hi"), cx)
|
||||
})
|
||||
.update(cx, |f, cx| f.delegate_mut().spawn_search("hi".into(), cx))
|
||||
.await;
|
||||
finder.read_with(cx, |f, _| assert_eq!(f.delegate().matches.len(), 7));
|
||||
}
|
||||
@@ -750,9 +491,7 @@ mod tests {
|
||||
// Even though there is only one worktree, that worktree's filename
|
||||
// is included in the matching, because the worktree is a single file.
|
||||
finder
|
||||
.update(cx, |f, cx| {
|
||||
f.delegate_mut().spawn_search(test_path_like("thf"), cx)
|
||||
})
|
||||
.update(cx, |f, cx| f.delegate_mut().spawn_search("thf".into(), cx))
|
||||
.await;
|
||||
cx.read(|cx| {
|
||||
let finder = finder.read(cx);
|
||||
@@ -770,9 +509,7 @@ mod tests {
|
||||
// Since the worktree root is a file, searching for its name followed by a slash does
|
||||
// not match anything.
|
||||
finder
|
||||
.update(cx, |f, cx| {
|
||||
f.delegate_mut().spawn_search(test_path_like("thf/"), cx)
|
||||
})
|
||||
.update(cx, |f, cx| f.delegate_mut().spawn_search("thf/".into(), cx))
|
||||
.await;
|
||||
finder.read_with(cx, |f, _| assert_eq!(f.delegate().matches.len(), 0));
|
||||
}
|
||||
@@ -816,9 +553,7 @@ mod tests {
|
||||
|
||||
// Run a search that matches two files with the same relative path.
|
||||
finder
|
||||
.update(cx, |f, cx| {
|
||||
f.delegate_mut().spawn_search(test_path_like("a.t"), cx)
|
||||
})
|
||||
.update(cx, |f, cx| f.delegate_mut().spawn_search("a.t".into(), cx))
|
||||
.await;
|
||||
|
||||
// Can switch between different matches with the same relative path.
|
||||
@@ -874,7 +609,7 @@ mod tests {
|
||||
|
||||
finder
|
||||
.update(cx, |f, cx| {
|
||||
f.delegate_mut().spawn_search(test_path_like("a.txt"), cx)
|
||||
f.delegate_mut().spawn_search("a.txt".into(), cx)
|
||||
})
|
||||
.await;
|
||||
|
||||
@@ -916,27 +651,11 @@ mod tests {
|
||||
)
|
||||
});
|
||||
finder
|
||||
.update(cx, |f, cx| {
|
||||
f.delegate_mut().spawn_search(test_path_like("dir"), cx)
|
||||
})
|
||||
.update(cx, |f, cx| f.delegate_mut().spawn_search("dir".into(), cx))
|
||||
.await;
|
||||
cx.read(|cx| {
|
||||
let finder = finder.read(cx);
|
||||
assert_eq!(finder.delegate().matches.len(), 0);
|
||||
});
|
||||
}
|
||||
|
||||
fn test_path_like(test_str: &str) -> PathLikeWithPosition<FileSearchQuery> {
|
||||
PathLikeWithPosition::parse_str(test_str, |path_like_str| {
|
||||
Ok::<_, std::convert::Infallible>(FileSearchQuery {
|
||||
raw_query: test_str.to_owned(),
|
||||
file_query_end: if path_like_str == test_str {
|
||||
None
|
||||
} else {
|
||||
Some(path_like_str.len())
|
||||
},
|
||||
})
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ gpui = { path = "../gpui" }
|
||||
lsp = { path = "../lsp" }
|
||||
rope = { path = "../rope" }
|
||||
util = { path = "../util" }
|
||||
sum_tree = { path = "../sum_tree" }
|
||||
anyhow.workspace = true
|
||||
async-trait.workspace = true
|
||||
futures.workspace = true
|
||||
|
||||
@@ -27,7 +27,7 @@ use util::ResultExt;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
use collections::{btree_map, BTreeMap};
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
use repository::{FakeGitRepositoryState, GitFileStatus};
|
||||
use repository::FakeGitRepositoryState;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
use std::sync::Weak;
|
||||
|
||||
@@ -572,15 +572,15 @@ impl FakeFs {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn pause_events(&self) {
|
||||
pub async fn pause_events(&self) {
|
||||
self.state.lock().events_paused = true;
|
||||
}
|
||||
|
||||
pub fn buffered_event_count(&self) -> usize {
|
||||
pub async fn buffered_event_count(&self) -> usize {
|
||||
self.state.lock().buffered_events.len()
|
||||
}
|
||||
|
||||
pub fn flush_events(&self, count: usize) {
|
||||
pub async fn flush_events(&self, count: usize) {
|
||||
self.state.lock().flush_events(count);
|
||||
}
|
||||
|
||||
@@ -619,10 +619,7 @@ impl FakeFs {
|
||||
.boxed()
|
||||
}
|
||||
|
||||
pub fn with_git_state<F>(&self, dot_git: &Path, f: F)
|
||||
where
|
||||
F: FnOnce(&mut FakeGitRepositoryState),
|
||||
{
|
||||
pub async fn set_index_for_repo(&self, dot_git: &Path, head_state: &[(&Path, String)]) {
|
||||
let mut state = self.state.lock();
|
||||
let entry = state.read_path(dot_git).unwrap();
|
||||
let mut entry = entry.lock();
|
||||
@@ -631,7 +628,12 @@ impl FakeFs {
|
||||
let repo_state = git_repo_state.get_or_insert_with(Default::default);
|
||||
let mut repo_state = repo_state.lock();
|
||||
|
||||
f(&mut repo_state);
|
||||
repo_state.index_contents.clear();
|
||||
repo_state.index_contents.extend(
|
||||
head_state
|
||||
.iter()
|
||||
.map(|(path, content)| (path.to_path_buf(), content.clone())),
|
||||
);
|
||||
|
||||
state.emit_event([dot_git]);
|
||||
} else {
|
||||
@@ -639,32 +641,6 @@ impl FakeFs {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn set_branch_name(&self, dot_git: &Path, branch: Option<impl Into<String>>) {
|
||||
self.with_git_state(dot_git, |state| state.branch_name = branch.map(Into::into))
|
||||
}
|
||||
|
||||
pub async fn set_index_for_repo(&self, dot_git: &Path, head_state: &[(&Path, String)]) {
|
||||
self.with_git_state(dot_git, |state| {
|
||||
state.index_contents.clear();
|
||||
state.index_contents.extend(
|
||||
head_state
|
||||
.iter()
|
||||
.map(|(path, content)| (path.to_path_buf(), content.clone())),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
pub async fn set_status_for_repo(&self, dot_git: &Path, statuses: &[(&Path, GitFileStatus)]) {
|
||||
self.with_git_state(dot_git, |state| {
|
||||
state.worktree_statuses.clear();
|
||||
state.worktree_statuses.extend(
|
||||
statuses
|
||||
.iter()
|
||||
.map(|(path, content)| ((**path).into(), content.clone())),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn paths(&self) -> Vec<PathBuf> {
|
||||
let mut result = Vec::new();
|
||||
let mut queue = collections::VecDeque::new();
|
||||
@@ -832,16 +808,14 @@ impl Fs for FakeFs {
|
||||
|
||||
let old_path = normalize_path(old_path);
|
||||
let new_path = normalize_path(new_path);
|
||||
|
||||
let mut state = self.state.lock();
|
||||
let moved_entry = state.write_path(&old_path, |e| {
|
||||
if let btree_map::Entry::Occupied(e) = e {
|
||||
Ok(e.get().clone())
|
||||
Ok(e.remove())
|
||||
} else {
|
||||
Err(anyhow!("path does not exist: {}", &old_path.display()))
|
||||
}
|
||||
})?;
|
||||
|
||||
state.write_path(&new_path, |e| {
|
||||
match e {
|
||||
btree_map::Entry::Occupied(mut e) => {
|
||||
@@ -857,17 +831,6 @@ impl Fs for FakeFs {
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
state
|
||||
.write_path(&old_path, |e| {
|
||||
if let btree_map::Entry::Occupied(e) = e {
|
||||
Ok(e.remove())
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
state.emit_event(&[old_path, new_path]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,16 +1,10 @@
|
||||
use anyhow::Result;
|
||||
use collections::HashMap;
|
||||
use parking_lot::Mutex;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use std::{
|
||||
cmp::Ordering,
|
||||
ffi::OsStr,
|
||||
os::unix::prelude::OsStrExt,
|
||||
path::{Component, Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
use sum_tree::{MapSeekTarget, TreeMap};
|
||||
use util::ResultExt;
|
||||
|
||||
pub use git2::Repository as LibGitRepository;
|
||||
|
||||
@@ -19,18 +13,6 @@ pub trait GitRepository: Send {
|
||||
fn reload_index(&self);
|
||||
|
||||
fn load_index_text(&self, relative_file_path: &Path) -> Option<String>;
|
||||
|
||||
fn branch_name(&self) -> Option<String>;
|
||||
|
||||
fn statuses(&self) -> Option<TreeMap<RepoPath, GitFileStatus>>;
|
||||
|
||||
fn status(&self, path: &RepoPath) -> Option<GitFileStatus>;
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for dyn GitRepository {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("dyn GitRepository<...>").finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
@@ -64,54 +46,6 @@ impl GitRepository for LibGitRepository {
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn branch_name(&self) -> Option<String> {
|
||||
let head = self.head().log_err()?;
|
||||
let branch = String::from_utf8_lossy(head.shorthand_bytes());
|
||||
Some(branch.to_string())
|
||||
}
|
||||
|
||||
fn statuses(&self) -> Option<TreeMap<RepoPath, GitFileStatus>> {
|
||||
let statuses = self.statuses(None).log_err()?;
|
||||
|
||||
let mut map = TreeMap::default();
|
||||
|
||||
for status in statuses
|
||||
.iter()
|
||||
.filter(|status| !status.status().contains(git2::Status::IGNORED))
|
||||
{
|
||||
let path = RepoPath(PathBuf::from(OsStr::from_bytes(status.path_bytes())));
|
||||
let Some(status) = read_status(status.status()) else {
|
||||
continue
|
||||
};
|
||||
|
||||
map.insert(path, status)
|
||||
}
|
||||
|
||||
Some(map)
|
||||
}
|
||||
|
||||
fn status(&self, path: &RepoPath) -> Option<GitFileStatus> {
|
||||
let status = self.status_file(path).log_err()?;
|
||||
read_status(status)
|
||||
}
|
||||
}
|
||||
|
||||
fn read_status(status: git2::Status) -> Option<GitFileStatus> {
|
||||
if status.contains(git2::Status::CONFLICTED) {
|
||||
Some(GitFileStatus::Conflict)
|
||||
} else if status.intersects(
|
||||
git2::Status::WT_MODIFIED
|
||||
| git2::Status::WT_RENAMED
|
||||
| git2::Status::INDEX_MODIFIED
|
||||
| git2::Status::INDEX_RENAMED,
|
||||
) {
|
||||
Some(GitFileStatus::Modified)
|
||||
} else if status.intersects(git2::Status::WT_NEW | git2::Status::INDEX_NEW) {
|
||||
Some(GitFileStatus::Added)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
@@ -122,8 +56,6 @@ pub struct FakeGitRepository {
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct FakeGitRepositoryState {
|
||||
pub index_contents: HashMap<PathBuf, String>,
|
||||
pub worktree_statuses: HashMap<RepoPath, GitFileStatus>,
|
||||
pub branch_name: Option<String>,
|
||||
}
|
||||
|
||||
impl FakeGitRepository {
|
||||
@@ -140,25 +72,6 @@ impl GitRepository for FakeGitRepository {
|
||||
let state = self.state.lock();
|
||||
state.index_contents.get(path).cloned()
|
||||
}
|
||||
|
||||
fn branch_name(&self) -> Option<String> {
|
||||
let state = self.state.lock();
|
||||
state.branch_name.clone()
|
||||
}
|
||||
|
||||
fn statuses(&self) -> Option<TreeMap<RepoPath, GitFileStatus>> {
|
||||
let state = self.state.lock();
|
||||
let mut map = TreeMap::default();
|
||||
for (repo_path, status) in state.worktree_statuses.iter() {
|
||||
map.insert(repo_path.to_owned(), status.to_owned());
|
||||
}
|
||||
Some(map)
|
||||
}
|
||||
|
||||
fn status(&self, path: &RepoPath) -> Option<GitFileStatus> {
|
||||
let state = self.state.lock();
|
||||
state.worktree_statuses.get(path).cloned()
|
||||
}
|
||||
}
|
||||
|
||||
fn check_path_to_repo_path_errors(relative_file_path: &Path) -> Result<()> {
|
||||
@@ -189,66 +102,3 @@ fn check_path_to_repo_path_errors(relative_file_path: &Path) -> Result<()> {
|
||||
_ => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum GitFileStatus {
|
||||
Added,
|
||||
Modified,
|
||||
Conflict,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Ord, Hash, PartialOrd, Eq, PartialEq)]
|
||||
pub struct RepoPath(PathBuf);
|
||||
|
||||
impl RepoPath {
|
||||
pub fn new(path: PathBuf) -> Self {
|
||||
debug_assert!(path.is_relative(), "Repo paths must be relative");
|
||||
|
||||
RepoPath(path)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Path> for RepoPath {
|
||||
fn from(value: &Path) -> Self {
|
||||
RepoPath::new(value.to_path_buf())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PathBuf> for RepoPath {
|
||||
fn from(value: PathBuf) -> Self {
|
||||
RepoPath::new(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for RepoPath {
|
||||
fn default() -> Self {
|
||||
RepoPath(PathBuf::new())
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<Path> for RepoPath {
|
||||
fn as_ref(&self) -> &Path {
|
||||
self.0.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Deref for RepoPath {
|
||||
type Target = PathBuf;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct RepoPathDescendants<'a>(pub &'a Path);
|
||||
|
||||
impl<'a> MapSeekTarget<RepoPath> for RepoPathDescendants<'a> {
|
||||
fn cmp_cursor(&self, key: &RepoPath) -> Ordering {
|
||||
if key.starts_with(&self.0) {
|
||||
Ordering::Greater
|
||||
} else {
|
||||
self.0.cmp(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,4 +16,3 @@ settings = { path = "../settings" }
|
||||
text = { path = "../text" }
|
||||
workspace = { path = "../workspace" }
|
||||
postage.workspace = true
|
||||
util = { path = "../util" }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use editor::{display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Editor};
|
||||
use editor::{display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, DisplayPoint, Editor};
|
||||
use gpui::{
|
||||
actions, elements::*, geometry::vector::Vector2F, AnyViewHandle, AppContext, Axis, Entity,
|
||||
View, ViewContext, ViewHandle,
|
||||
@@ -8,7 +8,6 @@ use gpui::{
|
||||
use menu::{Cancel, Confirm};
|
||||
use settings::Settings;
|
||||
use text::{Bias, Point};
|
||||
use util::paths::FILE_ROW_COLUMN_DELIMITER;
|
||||
use workspace::{Modal, Workspace};
|
||||
|
||||
actions!(go_to_line, [Toggle]);
|
||||
@@ -76,16 +75,15 @@ impl GoToLine {
|
||||
|
||||
fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
|
||||
self.prev_scroll_position.take();
|
||||
if let Some(point) = self.point_from_query(cx) {
|
||||
self.active_editor.update(cx, |active_editor, cx| {
|
||||
self.active_editor.update(cx, |active_editor, cx| {
|
||||
if let Some(rows) = active_editor.highlighted_rows() {
|
||||
let snapshot = active_editor.snapshot(cx).display_snapshot;
|
||||
let point = snapshot.buffer_snapshot.clip_point(point, Bias::Left);
|
||||
let position = DisplayPoint::new(rows.start, 0).to_point(&snapshot);
|
||||
active_editor.change_selections(Some(Autoscroll::center()), cx, |s| {
|
||||
s.select_ranges([point..point])
|
||||
s.select_ranges([position..position])
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
cx.emit(Event::Dismissed);
|
||||
}
|
||||
|
||||
@@ -98,7 +96,16 @@ impl GoToLine {
|
||||
match event {
|
||||
editor::Event::Blurred => cx.emit(Event::Dismissed),
|
||||
editor::Event::BufferEdited { .. } => {
|
||||
if let Some(point) = self.point_from_query(cx) {
|
||||
let line_editor = self.line_editor.read(cx).text(cx);
|
||||
let mut components = line_editor.trim().split(&[',', ':'][..]);
|
||||
let row = components.next().and_then(|row| row.parse::<u32>().ok());
|
||||
let column = components.next().and_then(|row| row.parse::<u32>().ok());
|
||||
if let Some(point) = row.map(|row| {
|
||||
Point::new(
|
||||
row.saturating_sub(1),
|
||||
column.map(|column| column.saturating_sub(1)).unwrap_or(0),
|
||||
)
|
||||
}) {
|
||||
self.active_editor.update(cx, |active_editor, cx| {
|
||||
let snapshot = active_editor.snapshot(cx).display_snapshot;
|
||||
let point = snapshot.buffer_snapshot.clip_point(point, Bias::Left);
|
||||
@@ -113,20 +120,6 @@ impl GoToLine {
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn point_from_query(&self, cx: &ViewContext<Self>) -> Option<Point> {
|
||||
let line_editor = self.line_editor.read(cx).text(cx);
|
||||
let mut components = line_editor
|
||||
.splitn(2, FILE_ROW_COLUMN_DELIMITER)
|
||||
.map(str::trim)
|
||||
.fuse();
|
||||
let row = components.next().and_then(|row| row.parse::<u32>().ok())?;
|
||||
let column = components.next().and_then(|col| col.parse::<u32>().ok());
|
||||
Some(Point::new(
|
||||
row.saturating_sub(1),
|
||||
column.unwrap_or(0).saturating_sub(1),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for GoToLine {
|
||||
@@ -154,7 +147,7 @@ impl View for GoToLine {
|
||||
let theme = &cx.global::<Settings>().theme.picker;
|
||||
|
||||
let label = format!(
|
||||
"{}{FILE_ROW_COLUMN_DELIMITER}{} of {} lines",
|
||||
"{},{} of {} lines",
|
||||
self.cursor_point.row + 1,
|
||||
self.cursor_point.column + 1,
|
||||
self.max_point.row + 1
|
||||
|
||||
@@ -48,7 +48,7 @@ smallvec.workspace = true
|
||||
smol.workspace = true
|
||||
time.workspace = true
|
||||
tiny-skia = "0.5"
|
||||
usvg = { version = "0.14", features = [] }
|
||||
usvg = "0.14"
|
||||
uuid = { version = "1.1.2", features = ["v4"] }
|
||||
waker-fn = "1.1.0"
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -81,7 +81,7 @@ pub(crate) fn setup_menu_handlers(foreground_platform: &dyn ForegroundPlatform,
|
||||
let dispatched = cx
|
||||
.update_window(main_window_id, |cx| {
|
||||
if let Some(view_id) = cx.focused_view_id() {
|
||||
cx.dispatch_action(Some(view_id), action);
|
||||
cx.handle_dispatch_action_from_effect(Some(view_id), action);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
use crate::{
|
||||
executor,
|
||||
geometry::vector::Vector2F,
|
||||
keymap_matcher::{Binding, Keystroke},
|
||||
keymap_matcher::Keystroke,
|
||||
platform,
|
||||
platform::{Event, InputHandler, KeyDownEvent, Platform},
|
||||
Action, AppContext, BorrowAppContext, BorrowWindowContext, Entity, FontCache, Handle,
|
||||
ModelContext, ModelHandle, Subscription, Task, View, ViewContext, ViewHandle, WeakHandle,
|
||||
WindowContext,
|
||||
Action, AnyViewHandle, AppContext, BorrowAppContext, BorrowWindowContext, Entity, FontCache,
|
||||
Handle, ModelContext, ModelHandle, Subscription, Task, View, ViewContext, ViewHandle,
|
||||
WeakHandle, WindowContext,
|
||||
};
|
||||
use collections::BTreeMap;
|
||||
use futures::Future;
|
||||
use itertools::Itertools;
|
||||
use parking_lot::{Mutex, RwLock};
|
||||
use smallvec::SmallVec;
|
||||
use smol::stream::StreamExt;
|
||||
use std::{
|
||||
any::Any,
|
||||
@@ -72,24 +71,17 @@ impl TestAppContext {
|
||||
cx
|
||||
}
|
||||
|
||||
pub fn dispatch_action<A: Action>(&mut self, window_id: usize, action: A) {
|
||||
self.update_window(window_id, |window| {
|
||||
window.dispatch_action(window.focused_view_id(), &action);
|
||||
})
|
||||
.expect("window not found");
|
||||
pub fn dispatch_action<A: Action>(&self, window_id: usize, action: A) {
|
||||
self.cx
|
||||
.borrow_mut()
|
||||
.update_window(window_id, |window| {
|
||||
window.handle_dispatch_action_from_effect(window.focused_view_id(), &action);
|
||||
})
|
||||
.expect("window not found");
|
||||
}
|
||||
|
||||
pub fn available_actions(
|
||||
&self,
|
||||
window_id: usize,
|
||||
view_id: usize,
|
||||
) -> Vec<(&'static str, Box<dyn Action>, SmallVec<[Binding; 1]>)> {
|
||||
self.read_window(window_id, |cx| cx.available_actions(view_id))
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn dispatch_global_action<A: Action>(&mut self, action: A) {
|
||||
self.update(|cx| cx.dispatch_global_action_any(&action));
|
||||
pub fn dispatch_global_action<A: Action>(&self, action: A) {
|
||||
self.cx.borrow_mut().dispatch_global_action_any(&action);
|
||||
}
|
||||
|
||||
pub fn dispatch_keystroke(&mut self, window_id: usize, keystroke: Keystroke, is_held: bool) {
|
||||
@@ -161,13 +153,12 @@ impl TestAppContext {
|
||||
(window_id, view)
|
||||
}
|
||||
|
||||
pub fn add_view<T, F>(&mut self, window_id: usize, build_view: F) -> ViewHandle<T>
|
||||
pub fn add_view<T, F>(&mut self, parent_handle: &AnyViewHandle, build_view: F) -> ViewHandle<T>
|
||||
where
|
||||
T: View,
|
||||
F: FnOnce(&mut ViewContext<T>) -> T,
|
||||
{
|
||||
self.update_window(window_id, |cx| cx.add_view(build_view))
|
||||
.expect("window not found")
|
||||
self.cx.borrow_mut().add_view(parent_handle, build_view)
|
||||
}
|
||||
|
||||
pub fn observe_global<E, F>(&mut self, callback: F) -> Subscription
|
||||
|
||||
@@ -14,7 +14,7 @@ use crate::{
|
||||
text_layout::TextLayoutCache,
|
||||
util::post_inc,
|
||||
Action, AnyView, AnyViewHandle, AppContext, BorrowAppContext, BorrowWindowContext, Effect,
|
||||
Element, Entity, Handle, LayoutContext, MouseRegion, MouseRegionId, SceneBuilder, Subscription,
|
||||
Element, Entity, Handle, MouseRegion, MouseRegionId, ParentId, SceneBuilder, Subscription,
|
||||
View, ViewContext, ViewHandle, WindowInvalidation,
|
||||
};
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
@@ -39,7 +39,6 @@ use super::{Reference, ViewMetadata};
|
||||
pub struct Window {
|
||||
pub(crate) root_view: Option<AnyViewHandle>,
|
||||
pub(crate) focused_view_id: Option<usize>,
|
||||
pub(crate) parents: HashMap<usize, usize>,
|
||||
pub(crate) is_active: bool,
|
||||
pub(crate) is_fullscreen: bool,
|
||||
pub(crate) invalidation: Option<WindowInvalidation>,
|
||||
@@ -73,7 +72,6 @@ impl Window {
|
||||
let mut window = Self {
|
||||
root_view: None,
|
||||
focused_view_id: None,
|
||||
parents: Default::default(),
|
||||
is_active: false,
|
||||
invalidation: None,
|
||||
is_fullscreen: false,
|
||||
@@ -92,9 +90,11 @@ impl Window {
|
||||
};
|
||||
|
||||
let mut window_context = WindowContext::mutable(cx, &mut window, window_id);
|
||||
let root_view = window_context.add_view(|cx| build_view(cx));
|
||||
if let Some(invalidation) = window_context.window.invalidation.take() {
|
||||
window_context.invalidate(invalidation, appearance);
|
||||
let root_view = window_context
|
||||
.build_and_insert_view(ParentId::Root, |cx| Some(build_view(cx)))
|
||||
.unwrap();
|
||||
if let Some(mut invalidation) = window_context.window.invalidation.take() {
|
||||
window_context.invalidate(&mut invalidation, appearance);
|
||||
}
|
||||
window.focused_view_id = Some(root_view.id());
|
||||
window.root_view = Some(root_view.into_any());
|
||||
@@ -113,6 +113,7 @@ pub struct WindowContext<'a> {
|
||||
pub(crate) app_context: Reference<'a, AppContext>,
|
||||
pub(crate) window: Reference<'a, Window>,
|
||||
pub(crate) window_id: usize,
|
||||
pub(crate) refreshing: bool,
|
||||
pub(crate) removed: bool,
|
||||
}
|
||||
|
||||
@@ -168,6 +169,7 @@ impl<'a> WindowContext<'a> {
|
||||
app_context: Reference::Mutable(app_context),
|
||||
window: Reference::Mutable(window),
|
||||
window_id,
|
||||
refreshing: false,
|
||||
removed: false,
|
||||
}
|
||||
}
|
||||
@@ -177,6 +179,7 @@ impl<'a> WindowContext<'a> {
|
||||
app_context: Reference::Immutable(app_context),
|
||||
window: Reference::Immutable(window),
|
||||
window_id,
|
||||
refreshing: false,
|
||||
removed: false,
|
||||
}
|
||||
}
|
||||
@@ -356,10 +359,49 @@ impl<'a> WindowContext<'a> {
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn available_actions(
|
||||
/// Return keystrokes that would dispatch the given action on the given view.
|
||||
pub(crate) fn keystrokes_for_action(
|
||||
&mut self,
|
||||
view_id: usize,
|
||||
action: &dyn Action,
|
||||
) -> Option<SmallVec<[Keystroke; 2]>> {
|
||||
let window_id = self.window_id;
|
||||
let mut contexts = Vec::new();
|
||||
let mut handler_depth = None;
|
||||
for (i, view_id) in self.ancestors(view_id).enumerate() {
|
||||
if let Some(view_metadata) = self.views_metadata.get(&(window_id, view_id)) {
|
||||
if let Some(actions) = self.actions.get(&view_metadata.type_id) {
|
||||
if actions.contains_key(&action.as_any().type_id()) {
|
||||
handler_depth = Some(i);
|
||||
}
|
||||
}
|
||||
contexts.push(view_metadata.keymap_context.clone());
|
||||
}
|
||||
}
|
||||
|
||||
if self.global_actions.contains_key(&action.as_any().type_id()) {
|
||||
handler_depth = Some(contexts.len())
|
||||
}
|
||||
|
||||
self.keystroke_matcher
|
||||
.bindings_for_action_type(action.as_any().type_id())
|
||||
.find_map(|b| {
|
||||
handler_depth
|
||||
.map(|highest_handler| {
|
||||
if (0..=highest_handler).any(|depth| b.match_context(&contexts[depth..])) {
|
||||
Some(b.keystrokes().into())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.flatten()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn available_actions(
|
||||
&self,
|
||||
view_id: usize,
|
||||
) -> Vec<(&'static str, Box<dyn Action>, SmallVec<[Binding; 1]>)> {
|
||||
) -> impl Iterator<Item = (&'static str, Box<dyn Action>, SmallVec<[&Binding; 1]>)> {
|
||||
let window_id = self.window_id;
|
||||
let mut contexts = Vec::new();
|
||||
let mut handler_depths_by_action_type = HashMap::<TypeId, usize>::default();
|
||||
@@ -401,17 +443,15 @@ impl<'a> WindowContext<'a> {
|
||||
.filter(|b| {
|
||||
(0..=action_depth).any(|depth| b.match_context(&contexts[depth..]))
|
||||
})
|
||||
.cloned()
|
||||
.collect(),
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn dispatch_keystroke(&mut self, keystroke: &Keystroke) -> bool {
|
||||
pub fn dispatch_keystroke(&mut self, keystroke: &Keystroke) -> bool {
|
||||
let window_id = self.window_id;
|
||||
if let Some(focused_view_id) = self.focused_view_id() {
|
||||
let dispatch_path = self
|
||||
@@ -433,7 +473,8 @@ impl<'a> WindowContext<'a> {
|
||||
MatchResult::Pending => true,
|
||||
MatchResult::Matches(matches) => {
|
||||
for (view_id, action) in matches {
|
||||
if self.dispatch_action(Some(*view_id), action.as_ref()) {
|
||||
if self.handle_dispatch_action_from_effect(Some(*view_id), action.as_ref())
|
||||
{
|
||||
self.keystroke_matcher.clear_pending();
|
||||
handled_by = Some(action.boxed_clone());
|
||||
break;
|
||||
@@ -456,7 +497,7 @@ impl<'a> WindowContext<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn dispatch_event(&mut self, event: Event, event_reused: bool) -> bool {
|
||||
pub fn dispatch_event(&mut self, event: Event, event_reused: bool) -> bool {
|
||||
let mut mouse_events = SmallVec::<[_; 2]>::new();
|
||||
let mut notified_views: HashSet<usize> = Default::default();
|
||||
let window_id = self.window_id;
|
||||
@@ -792,7 +833,7 @@ impl<'a> WindowContext<'a> {
|
||||
any_event_handled
|
||||
}
|
||||
|
||||
pub(crate) fn dispatch_key_down(&mut self, event: &KeyDownEvent) -> bool {
|
||||
pub fn dispatch_key_down(&mut self, event: &KeyDownEvent) -> bool {
|
||||
let window_id = self.window_id;
|
||||
if let Some(focused_view_id) = self.window.focused_view_id {
|
||||
for view_id in self.ancestors(focused_view_id).collect::<Vec<_>>() {
|
||||
@@ -811,7 +852,7 @@ impl<'a> WindowContext<'a> {
|
||||
false
|
||||
}
|
||||
|
||||
pub(crate) fn dispatch_key_up(&mut self, event: &KeyUpEvent) -> bool {
|
||||
pub fn dispatch_key_up(&mut self, event: &KeyUpEvent) -> bool {
|
||||
let window_id = self.window_id;
|
||||
if let Some(focused_view_id) = self.window.focused_view_id {
|
||||
for view_id in self.ancestors(focused_view_id).collect::<Vec<_>>() {
|
||||
@@ -830,7 +871,7 @@ impl<'a> WindowContext<'a> {
|
||||
false
|
||||
}
|
||||
|
||||
pub(crate) fn dispatch_modifiers_changed(&mut self, event: &ModifiersChangedEvent) -> bool {
|
||||
pub fn dispatch_modifiers_changed(&mut self, event: &ModifiersChangedEvent) -> bool {
|
||||
let window_id = self.window_id;
|
||||
if let Some(focused_view_id) = self.window.focused_view_id {
|
||||
for view_id in self.ancestors(focused_view_id).collect::<Vec<_>>() {
|
||||
@@ -849,7 +890,7 @@ impl<'a> WindowContext<'a> {
|
||||
false
|
||||
}
|
||||
|
||||
pub fn invalidate(&mut self, mut invalidation: WindowInvalidation, appearance: Appearance) {
|
||||
pub fn invalidate(&mut self, invalidation: &mut WindowInvalidation, appearance: Appearance) {
|
||||
self.start_frame();
|
||||
self.window.appearance = appearance;
|
||||
for view_id in &invalidation.removed {
|
||||
@@ -890,52 +931,13 @@ impl<'a> WindowContext<'a> {
|
||||
Ok(element)
|
||||
}
|
||||
|
||||
pub(crate) fn layout(&mut self, refreshing: bool) -> Result<()> {
|
||||
let window_size = self.window.platform_window.content_size();
|
||||
let root_view_id = self.window.root_view().id();
|
||||
let mut rendered_root = self.window.rendered_views.remove(&root_view_id).unwrap();
|
||||
let mut new_parents = HashMap::default();
|
||||
let mut views_to_notify_if_ancestors_change = HashMap::default();
|
||||
rendered_root.layout(
|
||||
SizeConstraint::strict(window_size),
|
||||
&mut new_parents,
|
||||
&mut views_to_notify_if_ancestors_change,
|
||||
refreshing,
|
||||
self,
|
||||
)?;
|
||||
|
||||
for (view_id, view_ids_to_notify) in views_to_notify_if_ancestors_change {
|
||||
let mut current_view_id = view_id;
|
||||
loop {
|
||||
let old_parent_id = self.window.parents.get(¤t_view_id);
|
||||
let new_parent_id = new_parents.get(¤t_view_id);
|
||||
if old_parent_id.is_none() && new_parent_id.is_none() {
|
||||
break;
|
||||
} else if old_parent_id == new_parent_id {
|
||||
current_view_id = *old_parent_id.unwrap();
|
||||
} else {
|
||||
let window_id = self.window_id;
|
||||
for view_id_to_notify in view_ids_to_notify {
|
||||
self.notify_view(window_id, view_id_to_notify);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.window.parents = new_parents;
|
||||
self.window
|
||||
.rendered_views
|
||||
.insert(root_view_id, rendered_root);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn paint(&mut self) -> Result<Scene> {
|
||||
pub fn build_scene(&mut self) -> Result<Scene> {
|
||||
let window_size = self.window.platform_window.content_size();
|
||||
let scale_factor = self.window.platform_window.scale_factor();
|
||||
|
||||
let root_view_id = self.window.root_view().id();
|
||||
let mut rendered_root = self.window.rendered_views.remove(&root_view_id).unwrap();
|
||||
rendered_root.layout(SizeConstraint::strict(window_size), self)?;
|
||||
|
||||
let mut scene_builder = SceneBuilder::new(scale_factor);
|
||||
rendered_root.paint(
|
||||
@@ -998,7 +1000,11 @@ impl<'a> WindowContext<'a> {
|
||||
self.window.is_fullscreen
|
||||
}
|
||||
|
||||
pub(crate) fn dispatch_action(&mut self, view_id: Option<usize>, action: &dyn Action) -> bool {
|
||||
pub(crate) fn handle_dispatch_action_from_effect(
|
||||
&mut self,
|
||||
view_id: Option<usize>,
|
||||
action: &dyn Action,
|
||||
) -> bool {
|
||||
if let Some(view_id) = view_id {
|
||||
self.halt_action_dispatch = false;
|
||||
self.visit_dispatch_path(view_id, |view_id, capture_phase, cx| {
|
||||
@@ -1044,7 +1050,9 @@ impl<'a> WindowContext<'a> {
|
||||
std::iter::once(view_id)
|
||||
.into_iter()
|
||||
.chain(std::iter::from_fn(move || {
|
||||
if let Some(parent_id) = self.window.parents.get(&view_id) {
|
||||
if let Some(ParentId::View(parent_id)) =
|
||||
self.parents.get(&(self.window_id, view_id))
|
||||
{
|
||||
view_id = *parent_id;
|
||||
Some(view_id)
|
||||
} else {
|
||||
@@ -1053,6 +1061,16 @@ impl<'a> WindowContext<'a> {
|
||||
}))
|
||||
}
|
||||
|
||||
/// Returns the id of the parent of the given view, or none if the given
|
||||
/// view is the root.
|
||||
pub(crate) fn parent(&self, view_id: usize) -> Option<usize> {
|
||||
if let Some(ParentId::View(view_id)) = self.parents.get(&(self.window_id, view_id)) {
|
||||
Some(*view_id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
// Traverses the parent tree. Walks down the tree toward the passed
|
||||
// view calling visit with true. Then walks back up the tree calling visit with false.
|
||||
// If `visit` returns false this function will immediately return.
|
||||
@@ -1083,6 +1101,16 @@ impl<'a> WindowContext<'a> {
|
||||
self.window.focused_view_id
|
||||
}
|
||||
|
||||
pub fn is_child_focused(&self, view: &AnyViewHandle) -> bool {
|
||||
if let Some(focused_view_id) = self.focused_view_id() {
|
||||
self.ancestors(focused_view_id)
|
||||
.skip(1) // Skip self id
|
||||
.any(|parent| parent == view.view_id)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn window_bounds(&self) -> WindowBounds {
|
||||
self.window.platform_window.bounds()
|
||||
}
|
||||
@@ -1125,27 +1153,27 @@ impl<'a> WindowContext<'a> {
|
||||
V: View,
|
||||
F: FnOnce(&mut ViewContext<V>) -> V,
|
||||
{
|
||||
let root_view = self.add_view(|cx| build_root_view(cx));
|
||||
let root_view = self
|
||||
.build_and_insert_view(ParentId::Root, |cx| Some(build_root_view(cx)))
|
||||
.unwrap();
|
||||
self.window.root_view = Some(root_view.clone().into_any());
|
||||
self.window.focused_view_id = Some(root_view.id());
|
||||
root_view
|
||||
}
|
||||
|
||||
pub fn add_view<T, F>(&mut self, build_view: F) -> ViewHandle<T>
|
||||
where
|
||||
T: View,
|
||||
F: FnOnce(&mut ViewContext<T>) -> T,
|
||||
{
|
||||
self.add_option_view(|cx| Some(build_view(cx))).unwrap()
|
||||
}
|
||||
|
||||
pub fn add_option_view<T, F>(&mut self, build_view: F) -> Option<ViewHandle<T>>
|
||||
pub(crate) fn build_and_insert_view<T, F>(
|
||||
&mut self,
|
||||
parent_id: ParentId,
|
||||
build_view: F,
|
||||
) -> Option<ViewHandle<T>>
|
||||
where
|
||||
T: View,
|
||||
F: FnOnce(&mut ViewContext<T>) -> Option<T>,
|
||||
{
|
||||
let window_id = self.window_id;
|
||||
let view_id = post_inc(&mut self.next_entity_id);
|
||||
// Make sure we can tell child views about their parentu
|
||||
self.parents.insert((window_id, view_id), parent_id);
|
||||
let mut cx = ViewContext::mutable(self, view_id);
|
||||
let handle = if let Some(view) = build_view(&mut cx) {
|
||||
let mut keymap_context = KeymapContext::default();
|
||||
@@ -1165,6 +1193,7 @@ impl<'a> WindowContext<'a> {
|
||||
.insert(view_id);
|
||||
Some(ViewHandle::new(window_id, view_id, &self.ref_counts))
|
||||
} else {
|
||||
self.parents.remove(&(window_id, view_id));
|
||||
None
|
||||
};
|
||||
handle
|
||||
@@ -1345,18 +1374,11 @@ impl<V: View> Element<V> for ChildView {
|
||||
&mut self,
|
||||
constraint: SizeConstraint,
|
||||
_: &mut V,
|
||||
cx: &mut LayoutContext<V>,
|
||||
cx: &mut ViewContext<V>,
|
||||
) -> (Vector2F, Self::LayoutState) {
|
||||
if let Some(mut rendered_view) = cx.window.rendered_views.remove(&self.view_id) {
|
||||
cx.new_parents.insert(self.view_id, cx.view_id());
|
||||
let size = rendered_view
|
||||
.layout(
|
||||
constraint,
|
||||
cx.new_parents,
|
||||
cx.views_to_notify_if_ancestors_change,
|
||||
cx.refreshing,
|
||||
cx.view_context,
|
||||
)
|
||||
.layout(constraint, cx)
|
||||
.log_err()
|
||||
.unwrap_or(Vector2F::zero());
|
||||
cx.window.rendered_views.insert(self.view_id, rendered_view);
|
||||
|
||||
@@ -42,7 +42,7 @@ impl Color {
|
||||
}
|
||||
|
||||
pub fn yellow() -> Self {
|
||||
Self(ColorU::from_u32(0xffff00ff))
|
||||
Self(ColorU::from_u32(0x00ffffff))
|
||||
}
|
||||
|
||||
pub fn new(r: u8, g: u8, b: u8, a: u8) -> Self {
|
||||
|
||||
@@ -33,14 +33,11 @@ use crate::{
|
||||
rect::RectF,
|
||||
vector::{vec2f, Vector2F},
|
||||
},
|
||||
json, Action, LayoutContext, SceneBuilder, SizeConstraint, View, ViewContext, WeakViewHandle,
|
||||
WindowContext,
|
||||
json, Action, SceneBuilder, SizeConstraint, View, ViewContext, WeakViewHandle, WindowContext,
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
use collections::HashMap;
|
||||
use core::panic;
|
||||
use json::ToJson;
|
||||
use smallvec::SmallVec;
|
||||
use std::{
|
||||
any::Any,
|
||||
borrow::Cow,
|
||||
@@ -57,7 +54,7 @@ pub trait Element<V: View>: 'static {
|
||||
&mut self,
|
||||
constraint: SizeConstraint,
|
||||
view: &mut V,
|
||||
cx: &mut LayoutContext<V>,
|
||||
cx: &mut ViewContext<V>,
|
||||
) -> (Vector2F, Self::LayoutState);
|
||||
|
||||
fn paint(
|
||||
@@ -214,7 +211,7 @@ trait AnyElementState<V: View> {
|
||||
&mut self,
|
||||
constraint: SizeConstraint,
|
||||
view: &mut V,
|
||||
cx: &mut LayoutContext<V>,
|
||||
cx: &mut ViewContext<V>,
|
||||
) -> Vector2F;
|
||||
|
||||
fn paint(
|
||||
@@ -266,7 +263,7 @@ impl<V: View, E: Element<V>> AnyElementState<V> for ElementState<V, E> {
|
||||
&mut self,
|
||||
constraint: SizeConstraint,
|
||||
view: &mut V,
|
||||
cx: &mut LayoutContext<V>,
|
||||
cx: &mut ViewContext<V>,
|
||||
) -> Vector2F {
|
||||
let result;
|
||||
*self = match mem::take(self) {
|
||||
@@ -447,7 +444,7 @@ impl<V: View> AnyElement<V> {
|
||||
&mut self,
|
||||
constraint: SizeConstraint,
|
||||
view: &mut V,
|
||||
cx: &mut LayoutContext<V>,
|
||||
cx: &mut ViewContext<V>,
|
||||
) -> Vector2F {
|
||||
self.state.layout(constraint, view, cx)
|
||||
}
|
||||
@@ -508,7 +505,7 @@ impl<V: View> Element<V> for AnyElement<V> {
|
||||
&mut self,
|
||||
constraint: SizeConstraint,
|
||||
view: &mut V,
|
||||
cx: &mut LayoutContext<V>,
|
||||
cx: &mut ViewContext<V>,
|
||||
) -> (Vector2F, Self::LayoutState) {
|
||||
let size = self.layout(constraint, view, cx);
|
||||
(size, ())
|
||||
@@ -578,15 +575,6 @@ pub struct ComponentHost<V: View, C: Component<V>> {
|
||||
view_type: PhantomData<V>,
|
||||
}
|
||||
|
||||
impl<V: View, C: Component<V>> ComponentHost<V, C> {
|
||||
pub fn new(c: C) -> Self {
|
||||
Self {
|
||||
component: c,
|
||||
view_type: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: View, C: Component<V>> Deref for ComponentHost<V, C> {
|
||||
type Target = C;
|
||||
|
||||
@@ -609,7 +597,7 @@ impl<V: View, C: Component<V>> Element<V> for ComponentHost<V, C> {
|
||||
&mut self,
|
||||
constraint: SizeConstraint,
|
||||
view: &mut V,
|
||||
cx: &mut LayoutContext<V>,
|
||||
cx: &mut ViewContext<V>,
|
||||
) -> (Vector2F, AnyElement<V>) {
|
||||
let mut element = self.component.render(view, cx);
|
||||
let size = element.layout(constraint, view, cx);
|
||||
@@ -654,14 +642,7 @@ impl<V: View, C: Component<V>> Element<V> for ComponentHost<V, C> {
|
||||
}
|
||||
|
||||
pub trait AnyRootElement {
|
||||
fn layout(
|
||||
&mut self,
|
||||
constraint: SizeConstraint,
|
||||
new_parents: &mut HashMap<usize, usize>,
|
||||
views_to_notify_if_ancestors_change: &mut HashMap<usize, SmallVec<[usize; 2]>>,
|
||||
refreshing: bool,
|
||||
cx: &mut WindowContext,
|
||||
) -> Result<Vector2F>;
|
||||
fn layout(&mut self, constraint: SizeConstraint, cx: &mut WindowContext) -> Result<Vector2F>;
|
||||
fn paint(
|
||||
&mut self,
|
||||
scene: &mut SceneBuilder,
|
||||
@@ -679,27 +660,12 @@ pub trait AnyRootElement {
|
||||
}
|
||||
|
||||
impl<V: View> AnyRootElement for RootElement<V> {
|
||||
fn layout(
|
||||
&mut self,
|
||||
constraint: SizeConstraint,
|
||||
new_parents: &mut HashMap<usize, usize>,
|
||||
views_to_notify_if_ancestors_change: &mut HashMap<usize, SmallVec<[usize; 2]>>,
|
||||
refreshing: bool,
|
||||
cx: &mut WindowContext,
|
||||
) -> Result<Vector2F> {
|
||||
fn layout(&mut self, constraint: SizeConstraint, cx: &mut WindowContext) -> Result<Vector2F> {
|
||||
let view = self
|
||||
.view
|
||||
.upgrade(cx)
|
||||
.ok_or_else(|| anyhow!("layout called on a root element for a dropped view"))?;
|
||||
view.update(cx, |view, cx| {
|
||||
let mut cx = LayoutContext::new(
|
||||
cx,
|
||||
new_parents,
|
||||
views_to_notify_if_ancestors_change,
|
||||
refreshing,
|
||||
);
|
||||
Ok(self.element.layout(constraint, view, &mut cx))
|
||||
})
|
||||
view.update(cx, |view, cx| Ok(self.element.layout(constraint, view, cx)))
|
||||
}
|
||||
|
||||
fn paint(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
geometry::{rect::RectF, vector::Vector2F},
|
||||
json, AnyElement, Element, LayoutContext, SceneBuilder, SizeConstraint, View, ViewContext,
|
||||
json, AnyElement, Element, SceneBuilder, SizeConstraint, View, ViewContext,
|
||||
};
|
||||
use json::ToJson;
|
||||
|
||||
@@ -48,7 +48,7 @@ impl<V: View> Element<V> for Align<V> {
|
||||
&mut self,
|
||||
mut constraint: SizeConstraint,
|
||||
view: &mut V,
|
||||
cx: &mut LayoutContext<V>,
|
||||
cx: &mut ViewContext<V>,
|
||||
) -> (Vector2F, Self::LayoutState) {
|
||||
let mut size = constraint.max;
|
||||
constraint.min = Vector2F::zero();
|
||||
|
||||
@@ -34,7 +34,7 @@ where
|
||||
&mut self,
|
||||
constraint: crate::SizeConstraint,
|
||||
_: &mut V,
|
||||
_: &mut crate::LayoutContext<V>,
|
||||
_: &mut crate::ViewContext<V>,
|
||||
) -> (Vector2F, Self::LayoutState) {
|
||||
let x = if constraint.max.x().is_finite() {
|
||||
constraint.max.x()
|
||||
|
||||
@@ -3,9 +3,7 @@ use std::ops::Range;
|
||||
use pathfinder_geometry::{rect::RectF, vector::Vector2F};
|
||||
use serde_json::json;
|
||||
|
||||
use crate::{
|
||||
json, AnyElement, Element, LayoutContext, SceneBuilder, SizeConstraint, View, ViewContext,
|
||||
};
|
||||
use crate::{json, AnyElement, Element, SceneBuilder, SizeConstraint, View, ViewContext};
|
||||
|
||||
pub struct Clipped<V: View> {
|
||||
child: AnyElement<V>,
|
||||
@@ -25,7 +23,7 @@ impl<V: View> Element<V> for Clipped<V> {
|
||||
&mut self,
|
||||
constraint: SizeConstraint,
|
||||
view: &mut V,
|
||||
cx: &mut LayoutContext<V>,
|
||||
cx: &mut ViewContext<V>,
|
||||
) -> (Vector2F, Self::LayoutState) {
|
||||
(self.child.layout(constraint, view, cx), ())
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ use serde_json::json;
|
||||
|
||||
use crate::{
|
||||
geometry::{rect::RectF, vector::Vector2F},
|
||||
json, AnyElement, Element, LayoutContext, SceneBuilder, SizeConstraint, View, ViewContext,
|
||||
json, AnyElement, Element, SceneBuilder, SizeConstraint, View, ViewContext,
|
||||
};
|
||||
|
||||
pub struct ConstrainedBox<V: View> {
|
||||
@@ -15,7 +15,7 @@ pub struct ConstrainedBox<V: View> {
|
||||
|
||||
pub enum Constraint<V: View> {
|
||||
Static(SizeConstraint),
|
||||
Dynamic(Box<dyn FnMut(SizeConstraint, &mut V, &mut LayoutContext<V>) -> SizeConstraint>),
|
||||
Dynamic(Box<dyn FnMut(SizeConstraint, &mut V, &mut ViewContext<V>) -> SizeConstraint>),
|
||||
}
|
||||
|
||||
impl<V: View> ToJson for Constraint<V> {
|
||||
@@ -37,8 +37,7 @@ impl<V: View> ConstrainedBox<V> {
|
||||
|
||||
pub fn dynamically(
|
||||
mut self,
|
||||
constraint: impl 'static
|
||||
+ FnMut(SizeConstraint, &mut V, &mut LayoutContext<V>) -> SizeConstraint,
|
||||
constraint: impl 'static + FnMut(SizeConstraint, &mut V, &mut ViewContext<V>) -> SizeConstraint,
|
||||
) -> Self {
|
||||
self.constraint = Constraint::Dynamic(Box::new(constraint));
|
||||
self
|
||||
@@ -120,7 +119,7 @@ impl<V: View> ConstrainedBox<V> {
|
||||
&mut self,
|
||||
input_constraint: SizeConstraint,
|
||||
view: &mut V,
|
||||
cx: &mut LayoutContext<V>,
|
||||
cx: &mut ViewContext<V>,
|
||||
) -> SizeConstraint {
|
||||
match &mut self.constraint {
|
||||
Constraint::Static(constraint) => *constraint,
|
||||
@@ -139,7 +138,7 @@ impl<V: View> Element<V> for ConstrainedBox<V> {
|
||||
&mut self,
|
||||
mut parent_constraint: SizeConstraint,
|
||||
view: &mut V,
|
||||
cx: &mut LayoutContext<V>,
|
||||
cx: &mut ViewContext<V>,
|
||||
) -> (Vector2F, Self::LayoutState) {
|
||||
let constraint = self.constraint(parent_constraint, view, cx);
|
||||
parent_constraint.min = parent_constraint.min.max(constraint.min);
|
||||
|
||||
@@ -10,7 +10,7 @@ use crate::{
|
||||
json::ToJson,
|
||||
platform::CursorStyle,
|
||||
scene::{self, Border, CursorRegion, Quad},
|
||||
AnyElement, Element, LayoutContext, SceneBuilder, SizeConstraint, View, ViewContext,
|
||||
AnyElement, Element, SceneBuilder, SizeConstraint, View, ViewContext,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use serde_json::json;
|
||||
@@ -192,7 +192,7 @@ impl<V: View> Element<V> for Container<V> {
|
||||
&mut self,
|
||||
constraint: SizeConstraint,
|
||||
view: &mut V,
|
||||
cx: &mut LayoutContext<V>,
|
||||
cx: &mut ViewContext<V>,
|
||||
) -> (Vector2F, Self::LayoutState) {
|
||||
let mut size_buffer = self.margin_size() + self.padding_size();
|
||||
if !self.style.border.overlay {
|
||||
|
||||
@@ -6,7 +6,7 @@ use crate::{
|
||||
vector::{vec2f, Vector2F},
|
||||
},
|
||||
json::{json, ToJson},
|
||||
LayoutContext, SceneBuilder, View, ViewContext,
|
||||
SceneBuilder, View, ViewContext,
|
||||
};
|
||||
use crate::{Element, SizeConstraint};
|
||||
|
||||
@@ -34,7 +34,7 @@ impl<V: View> Element<V> for Empty {
|
||||
&mut self,
|
||||
constraint: SizeConstraint,
|
||||
_: &mut V,
|
||||
_: &mut LayoutContext<V>,
|
||||
_: &mut ViewContext<V>,
|
||||
) -> (Vector2F, Self::LayoutState) {
|
||||
let x = if constraint.max.x().is_finite() && !self.collapsed {
|
||||
constraint.max.x()
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::ops::Range;
|
||||
|
||||
use crate::{
|
||||
geometry::{rect::RectF, vector::Vector2F},
|
||||
json, AnyElement, Element, LayoutContext, SceneBuilder, SizeConstraint, View, ViewContext,
|
||||
json, AnyElement, Element, SceneBuilder, SizeConstraint, View, ViewContext,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
@@ -42,7 +42,7 @@ impl<V: View> Element<V> for Expanded<V> {
|
||||
&mut self,
|
||||
mut constraint: SizeConstraint,
|
||||
view: &mut V,
|
||||
cx: &mut LayoutContext<V>,
|
||||
cx: &mut ViewContext<V>,
|
||||
) -> (Vector2F, Self::LayoutState) {
|
||||
if self.full_width {
|
||||
constraint.min.set_x(constraint.max.x());
|
||||
|
||||
@@ -2,8 +2,8 @@ use std::{any::Any, cell::Cell, f32::INFINITY, ops::Range, rc::Rc};
|
||||
|
||||
use crate::{
|
||||
json::{self, ToJson, Value},
|
||||
AnyElement, Axis, Element, ElementStateHandle, LayoutContext, SceneBuilder, SizeConstraint,
|
||||
Vector2FExt, View, ViewContext,
|
||||
AnyElement, Axis, Element, ElementStateHandle, SceneBuilder, SizeConstraint, Vector2FExt, View,
|
||||
ViewContext,
|
||||
};
|
||||
use pathfinder_geometry::{
|
||||
rect::RectF,
|
||||
@@ -66,10 +66,6 @@ impl<V: View> Flex<V> {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.children.is_empty()
|
||||
}
|
||||
|
||||
fn layout_flex_children(
|
||||
&mut self,
|
||||
layout_expanded: bool,
|
||||
@@ -78,7 +74,7 @@ impl<V: View> Flex<V> {
|
||||
remaining_flex: &mut f32,
|
||||
cross_axis_max: &mut f32,
|
||||
view: &mut V,
|
||||
cx: &mut LayoutContext<V>,
|
||||
cx: &mut ViewContext<V>,
|
||||
) {
|
||||
let cross_axis = self.axis.invert();
|
||||
for child in &mut self.children {
|
||||
@@ -129,7 +125,7 @@ impl<V: View> Element<V> for Flex<V> {
|
||||
&mut self,
|
||||
constraint: SizeConstraint,
|
||||
view: &mut V,
|
||||
cx: &mut LayoutContext<V>,
|
||||
cx: &mut ViewContext<V>,
|
||||
) -> (Vector2F, Self::LayoutState) {
|
||||
let mut total_flex = None;
|
||||
let mut fixed_space = 0.0;
|
||||
@@ -218,7 +214,7 @@ impl<V: View> Element<V> for Flex<V> {
|
||||
}
|
||||
|
||||
if let Some(scroll_state) = self.scroll_state.as_ref() {
|
||||
scroll_state.0.update(cx.view_context(), |scroll_state, _| {
|
||||
scroll_state.0.update(cx, |scroll_state, _| {
|
||||
if let Some(scroll_to) = scroll_state.scroll_to.take() {
|
||||
let visible_start = scroll_state.scroll_position.get();
|
||||
let visible_end = visible_start + size.along(self.axis);
|
||||
@@ -436,7 +432,7 @@ impl<V: View> Element<V> for FlexItem<V> {
|
||||
&mut self,
|
||||
constraint: SizeConstraint,
|
||||
view: &mut V,
|
||||
cx: &mut LayoutContext<V>,
|
||||
cx: &mut ViewContext<V>,
|
||||
) -> (Vector2F, Self::LayoutState) {
|
||||
let size = self.child.layout(constraint, view, cx);
|
||||
(size, ())
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::ops::Range;
|
||||
use crate::{
|
||||
geometry::{rect::RectF, vector::Vector2F},
|
||||
json::json,
|
||||
AnyElement, Element, LayoutContext, SceneBuilder, SizeConstraint, View, ViewContext,
|
||||
AnyElement, Element, SceneBuilder, SizeConstraint, View, ViewContext,
|
||||
};
|
||||
|
||||
pub struct Hook<V: View> {
|
||||
@@ -36,7 +36,7 @@ impl<V: View> Element<V> for Hook<V> {
|
||||
&mut self,
|
||||
constraint: SizeConstraint,
|
||||
view: &mut V,
|
||||
cx: &mut LayoutContext<V>,
|
||||
cx: &mut ViewContext<V>,
|
||||
) -> (Vector2F, Self::LayoutState) {
|
||||
let size = self.child.layout(constraint, view, cx);
|
||||
if let Some(handler) = self.after_layout.as_mut() {
|
||||
|
||||
@@ -5,8 +5,7 @@ use crate::{
|
||||
vector::{vec2f, Vector2F},
|
||||
},
|
||||
json::{json, ToJson},
|
||||
scene, Border, Element, ImageData, LayoutContext, SceneBuilder, SizeConstraint, View,
|
||||
ViewContext,
|
||||
scene, Border, Element, ImageData, SceneBuilder, SizeConstraint, View, ViewContext,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use std::{ops::Range, sync::Arc};
|
||||
@@ -64,7 +63,7 @@ impl<V: View> Element<V> for Image {
|
||||
&mut self,
|
||||
constraint: SizeConstraint,
|
||||
_: &mut V,
|
||||
cx: &mut LayoutContext<V>,
|
||||
cx: &mut ViewContext<V>,
|
||||
) -> (Vector2F, Self::LayoutState) {
|
||||
let data = match &self.source {
|
||||
ImageSource::Path(path) => match cx.asset_cache.png(path) {
|
||||
|
||||
@@ -39,7 +39,7 @@ impl<V: View> Element<V> for KeystrokeLabel {
|
||||
&mut self,
|
||||
constraint: SizeConstraint,
|
||||
view: &mut V,
|
||||
cx: &mut LayoutContext<V>,
|
||||
cx: &mut ViewContext<V>,
|
||||
) -> (Vector2F, AnyElement<V>) {
|
||||
let mut element = if let Some(keystrokes) =
|
||||
cx.keystrokes_for_action(self.view_id, self.action.as_ref())
|
||||
|
||||
@@ -8,7 +8,7 @@ use crate::{
|
||||
},
|
||||
json::{ToJson, Value},
|
||||
text_layout::{Line, RunStyle},
|
||||
Element, LayoutContext, SceneBuilder, SizeConstraint, View, ViewContext,
|
||||
Element, SceneBuilder, SizeConstraint, View, ViewContext,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use serde_json::json;
|
||||
@@ -135,7 +135,7 @@ impl<V: View> Element<V> for Label {
|
||||
&mut self,
|
||||
constraint: SizeConstraint,
|
||||
_: &mut V,
|
||||
cx: &mut LayoutContext<V>,
|
||||
cx: &mut ViewContext<V>,
|
||||
) -> (Vector2F, Self::LayoutState) {
|
||||
let runs = self.compute_runs();
|
||||
let line = cx.text_layout_cache().layout_str(
|
||||
|
||||
@@ -4,8 +4,7 @@ use crate::{
|
||||
vector::{vec2f, Vector2F},
|
||||
},
|
||||
json::json,
|
||||
AnyElement, Element, LayoutContext, MouseRegion, SceneBuilder, SizeConstraint, View,
|
||||
ViewContext,
|
||||
AnyElement, Element, MouseRegion, SceneBuilder, SizeConstraint, View, ViewContext,
|
||||
};
|
||||
use std::{cell::RefCell, collections::VecDeque, fmt::Debug, ops::Range, rc::Rc};
|
||||
use sum_tree::{Bias, SumTree};
|
||||
@@ -100,7 +99,7 @@ impl<V: View> Element<V> for List<V> {
|
||||
&mut self,
|
||||
constraint: SizeConstraint,
|
||||
view: &mut V,
|
||||
cx: &mut LayoutContext<V>,
|
||||
cx: &mut ViewContext<V>,
|
||||
) -> (Vector2F, Self::LayoutState) {
|
||||
let state = &mut *self.state.0.borrow_mut();
|
||||
let size = constraint.max;
|
||||
@@ -453,7 +452,7 @@ impl<V: View> StateInner<V> {
|
||||
existing_element: Option<&ListItem<V>>,
|
||||
constraint: SizeConstraint,
|
||||
view: &mut V,
|
||||
cx: &mut LayoutContext<V>,
|
||||
cx: &mut ViewContext<V>,
|
||||
) -> Option<Rc<RefCell<AnyElement<V>>>> {
|
||||
if let Some(ListItem::Rendered(element)) = existing_element {
|
||||
Some(element.clone())
|
||||
@@ -666,15 +665,7 @@ mod tests {
|
||||
});
|
||||
|
||||
let mut list = List::new(state.clone());
|
||||
let mut new_parents = Default::default();
|
||||
let mut notify_views_if_parents_change = Default::default();
|
||||
let mut layout_cx = LayoutContext::new(
|
||||
cx,
|
||||
&mut new_parents,
|
||||
&mut notify_views_if_parents_change,
|
||||
false,
|
||||
);
|
||||
let (size, _) = list.layout(constraint, &mut view, &mut layout_cx);
|
||||
let (size, _) = list.layout(constraint, &mut view, cx);
|
||||
assert_eq!(size, vec2f(100., 40.));
|
||||
assert_eq!(
|
||||
state.0.borrow().items.summary().clone(),
|
||||
@@ -698,13 +689,7 @@ mod tests {
|
||||
cx,
|
||||
);
|
||||
|
||||
let mut layout_cx = LayoutContext::new(
|
||||
cx,
|
||||
&mut new_parents,
|
||||
&mut notify_views_if_parents_change,
|
||||
false,
|
||||
);
|
||||
let (_, logical_scroll_top) = list.layout(constraint, &mut view, &mut layout_cx);
|
||||
let (_, logical_scroll_top) = list.layout(constraint, &mut view, cx);
|
||||
assert_eq!(
|
||||
logical_scroll_top,
|
||||
ListOffset {
|
||||
@@ -728,13 +713,7 @@ mod tests {
|
||||
}
|
||||
);
|
||||
|
||||
let mut layout_cx = LayoutContext::new(
|
||||
cx,
|
||||
&mut new_parents,
|
||||
&mut notify_views_if_parents_change,
|
||||
false,
|
||||
);
|
||||
let (size, logical_scroll_top) = list.layout(constraint, &mut view, &mut layout_cx);
|
||||
let (size, logical_scroll_top) = list.layout(constraint, &mut view, cx);
|
||||
assert_eq!(size, vec2f(100., 40.));
|
||||
assert_eq!(
|
||||
state.0.borrow().items.summary().clone(),
|
||||
@@ -852,18 +831,10 @@ mod tests {
|
||||
|
||||
let mut list = List::new(state.clone());
|
||||
let window_size = vec2f(width, height);
|
||||
let mut new_parents = Default::default();
|
||||
let mut notify_views_if_parents_change = Default::default();
|
||||
let mut layout_cx = LayoutContext::new(
|
||||
cx,
|
||||
&mut new_parents,
|
||||
&mut notify_views_if_parents_change,
|
||||
false,
|
||||
);
|
||||
let (size, logical_scroll_top) = list.layout(
|
||||
SizeConstraint::new(vec2f(0., 0.), window_size),
|
||||
&mut view,
|
||||
&mut layout_cx,
|
||||
cx,
|
||||
);
|
||||
assert_eq!(size, window_size);
|
||||
last_logical_scroll_top = Some(logical_scroll_top);
|
||||
@@ -976,7 +947,7 @@ mod tests {
|
||||
&mut self,
|
||||
_: SizeConstraint,
|
||||
_: &mut V,
|
||||
_: &mut LayoutContext<V>,
|
||||
_: &mut ViewContext<V>,
|
||||
) -> (Vector2F, ()) {
|
||||
(self.size, ())
|
||||
}
|
||||
|
||||
@@ -10,8 +10,8 @@ use crate::{
|
||||
CursorRegion, HandlerSet, MouseClick, MouseDown, MouseDownOut, MouseDrag, MouseHover,
|
||||
MouseMove, MouseMoveOut, MouseScrollWheel, MouseUp, MouseUpOut,
|
||||
},
|
||||
AnyElement, Element, EventContext, LayoutContext, MouseRegion, MouseState, SceneBuilder,
|
||||
SizeConstraint, View, ViewContext,
|
||||
AnyElement, Element, EventContext, MouseRegion, MouseState, SceneBuilder, SizeConstraint, View,
|
||||
ViewContext,
|
||||
};
|
||||
use serde_json::json;
|
||||
use std::{marker::PhantomData, ops::Range};
|
||||
@@ -220,7 +220,7 @@ impl<Tag, V: View> Element<V> for MouseEventHandler<Tag, V> {
|
||||
&mut self,
|
||||
constraint: SizeConstraint,
|
||||
view: &mut V,
|
||||
cx: &mut LayoutContext<V>,
|
||||
cx: &mut ViewContext<V>,
|
||||
) -> (Vector2F, Self::LayoutState) {
|
||||
(self.child.layout(constraint, view, cx), ())
|
||||
}
|
||||
|
||||
@@ -3,8 +3,7 @@ use std::ops::Range;
|
||||
use crate::{
|
||||
geometry::{rect::RectF, vector::Vector2F},
|
||||
json::ToJson,
|
||||
AnyElement, Axis, Element, LayoutContext, MouseRegion, SceneBuilder, SizeConstraint, View,
|
||||
ViewContext,
|
||||
AnyElement, Axis, Element, MouseRegion, SceneBuilder, SizeConstraint, View, ViewContext,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
@@ -125,7 +124,7 @@ impl<V: View> Element<V> for Overlay<V> {
|
||||
&mut self,
|
||||
constraint: SizeConstraint,
|
||||
view: &mut V,
|
||||
cx: &mut LayoutContext<V>,
|
||||
cx: &mut ViewContext<V>,
|
||||
) -> (Vector2F, Self::LayoutState) {
|
||||
let constraint = if self.anchor_position.is_some() {
|
||||
SizeConstraint::new(Vector2F::zero(), cx.window_size())
|
||||
|
||||
@@ -7,8 +7,7 @@ use crate::{
|
||||
geometry::rect::RectF,
|
||||
platform::{CursorStyle, MouseButton},
|
||||
scene::MouseDrag,
|
||||
AnyElement, Axis, Element, ElementStateHandle, LayoutContext, MouseRegion, SceneBuilder, View,
|
||||
ViewContext,
|
||||
AnyElement, Axis, Element, ElementStateHandle, MouseRegion, SceneBuilder, View, ViewContext,
|
||||
};
|
||||
|
||||
use super::{ConstrainedBox, Hook};
|
||||
@@ -140,7 +139,7 @@ impl<V: View> Element<V> for Resizable<V> {
|
||||
&mut self,
|
||||
constraint: crate::SizeConstraint,
|
||||
view: &mut V,
|
||||
cx: &mut LayoutContext<V>,
|
||||
cx: &mut ViewContext<V>,
|
||||
) -> (Vector2F, Self::LayoutState) {
|
||||
(self.child.layout(constraint, view, cx), ())
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::ops::Range;
|
||||
use crate::{
|
||||
geometry::{rect::RectF, vector::Vector2F},
|
||||
json::{self, json, ToJson},
|
||||
AnyElement, Element, LayoutContext, SceneBuilder, SizeConstraint, View, ViewContext,
|
||||
AnyElement, Element, SceneBuilder, SizeConstraint, View, ViewContext,
|
||||
};
|
||||
|
||||
/// Element which renders it's children in a stack on top of each other.
|
||||
@@ -34,7 +34,7 @@ impl<V: View> Element<V> for Stack<V> {
|
||||
&mut self,
|
||||
mut constraint: SizeConstraint,
|
||||
view: &mut V,
|
||||
cx: &mut LayoutContext<V>,
|
||||
cx: &mut ViewContext<V>,
|
||||
) -> (Vector2F, Self::LayoutState) {
|
||||
let mut size = constraint.min;
|
||||
let mut children = self.children.iter_mut();
|
||||
|
||||
@@ -8,7 +8,7 @@ use crate::{
|
||||
rect::RectF,
|
||||
vector::{vec2f, Vector2F},
|
||||
},
|
||||
scene, Element, LayoutContext, SceneBuilder, SizeConstraint, View, ViewContext,
|
||||
scene, Element, SceneBuilder, SizeConstraint, View, ViewContext,
|
||||
};
|
||||
|
||||
pub struct Svg {
|
||||
@@ -38,7 +38,7 @@ impl<V: View> Element<V> for Svg {
|
||||
&mut self,
|
||||
constraint: SizeConstraint,
|
||||
_: &mut V,
|
||||
cx: &mut LayoutContext<V>,
|
||||
cx: &mut ViewContext<V>,
|
||||
) -> (Vector2F, Self::LayoutState) {
|
||||
match cx.asset_cache.svg(&self.path) {
|
||||
Ok(tree) => {
|
||||
|
||||
@@ -7,8 +7,8 @@ use crate::{
|
||||
},
|
||||
json::{ToJson, Value},
|
||||
text_layout::{Line, RunStyle, ShapedBoundary},
|
||||
AppContext, Element, FontCache, LayoutContext, SceneBuilder, SizeConstraint, TextLayoutCache,
|
||||
View, ViewContext,
|
||||
AppContext, Element, FontCache, SceneBuilder, SizeConstraint, TextLayoutCache, View,
|
||||
ViewContext,
|
||||
};
|
||||
use log::warn;
|
||||
use serde_json::json;
|
||||
@@ -78,7 +78,7 @@ impl<V: View> Element<V> for Text {
|
||||
&mut self,
|
||||
constraint: SizeConstraint,
|
||||
_: &mut V,
|
||||
cx: &mut LayoutContext<V>,
|
||||
cx: &mut ViewContext<V>,
|
||||
) -> (Vector2F, Self::LayoutState) {
|
||||
// Convert the string and highlight ranges into an iterator of highlighted chunks.
|
||||
|
||||
@@ -338,7 +338,7 @@ impl<V: View> Element<V> for Text {
|
||||
}
|
||||
|
||||
/// Perform text layout on a series of highlighted chunks of text.
|
||||
fn layout_highlighted_chunks<'a>(
|
||||
pub fn layout_highlighted_chunks<'a>(
|
||||
chunks: impl Iterator<Item = (&'a str, Option<HighlightStyle>)>,
|
||||
text_style: &TextStyle,
|
||||
text_layout_cache: &TextLayoutCache,
|
||||
@@ -411,18 +411,10 @@ mod tests {
|
||||
let mut view = TestView;
|
||||
fonts::with_font_cache(cx.font_cache().clone(), || {
|
||||
let mut text = Text::new("Hello\r\n", Default::default()).with_soft_wrap(true);
|
||||
let mut new_parents = Default::default();
|
||||
let mut notify_views_if_parents_change = Default::default();
|
||||
let mut layout_cx = LayoutContext::new(
|
||||
cx,
|
||||
&mut new_parents,
|
||||
&mut notify_views_if_parents_change,
|
||||
false,
|
||||
);
|
||||
let (_, state) = text.layout(
|
||||
SizeConstraint::new(Default::default(), vec2f(f32::INFINITY, f32::INFINITY)),
|
||||
&mut view,
|
||||
&mut layout_cx,
|
||||
cx,
|
||||
);
|
||||
assert_eq!(state.shaped_lines.len(), 2);
|
||||
assert_eq!(state.wrap_boundaries.len(), 2);
|
||||
|
||||
@@ -6,8 +6,7 @@ use crate::{
|
||||
fonts::TextStyle,
|
||||
geometry::{rect::RectF, vector::Vector2F},
|
||||
json::json,
|
||||
Action, Axis, ElementStateHandle, LayoutContext, SceneBuilder, SizeConstraint, Task, View,
|
||||
ViewContext,
|
||||
Action, Axis, ElementStateHandle, SceneBuilder, SizeConstraint, Task, View, ViewContext,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use std::{
|
||||
@@ -173,7 +172,7 @@ impl<V: View> Element<V> for Tooltip<V> {
|
||||
&mut self,
|
||||
constraint: SizeConstraint,
|
||||
view: &mut V,
|
||||
cx: &mut LayoutContext<V>,
|
||||
cx: &mut ViewContext<V>,
|
||||
) -> (Vector2F, Self::LayoutState) {
|
||||
let size = self.child.layout(constraint, view, cx);
|
||||
if let Some(tooltip) = self.tooltip.as_mut() {
|
||||
|
||||
@@ -6,7 +6,7 @@ use crate::{
|
||||
},
|
||||
json::{self, json},
|
||||
platform::ScrollWheelEvent,
|
||||
AnyElement, LayoutContext, MouseRegion, SceneBuilder, View, ViewContext,
|
||||
AnyElement, MouseRegion, SceneBuilder, View, ViewContext,
|
||||
};
|
||||
use json::ToJson;
|
||||
use std::{cell::RefCell, cmp, ops::Range, rc::Rc};
|
||||
@@ -159,7 +159,7 @@ impl<V: View> Element<V> for UniformList<V> {
|
||||
&mut self,
|
||||
constraint: SizeConstraint,
|
||||
view: &mut V,
|
||||
cx: &mut LayoutContext<V>,
|
||||
cx: &mut ViewContext<V>,
|
||||
) -> (Vector2F, Self::LayoutState) {
|
||||
if constraint.max.y().is_infinite() {
|
||||
unimplemented!(
|
||||
|
||||
@@ -11,29 +11,6 @@ pub struct Binding {
|
||||
context_predicate: Option<KeymapContextPredicate>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Binding {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"Binding {{ keystrokes: {:?}, action: {}::{}, context_predicate: {:?} }}",
|
||||
self.keystrokes,
|
||||
self.action.namespace(),
|
||||
self.action.name(),
|
||||
self.context_predicate
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for Binding {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
action: self.action.boxed_clone(),
|
||||
keystrokes: self.keystrokes.clone(),
|
||||
context_predicate: self.context_predicate.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Binding {
|
||||
pub fn new<A: Action>(keystrokes: &str, action: A, context: Option<&str>) -> Self {
|
||||
Self::load(keystrokes, Box::new(action), context).unwrap()
|
||||
|
||||
@@ -44,7 +44,7 @@ impl KeymapContext {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub enum KeymapContextPredicate {
|
||||
Identifier(String),
|
||||
Equal(String, String),
|
||||
|
||||
@@ -755,7 +755,7 @@ impl platform::Window for Window {
|
||||
let _ = postage::sink::Sink::try_send(&mut done_tx, answer.try_into().unwrap());
|
||||
}
|
||||
});
|
||||
let block = block.copy();
|
||||
|
||||
let native_window = self.0.borrow().native_window;
|
||||
self.0
|
||||
.borrow()
|
||||
|
||||
@@ -223,41 +223,41 @@ impl HandlerSet {
|
||||
|
||||
set.insert(
|
||||
HandlerKey::new(MouseEvent::move_disc(), None),
|
||||
SmallVec::from_buf([Rc::new(|_, _, _, _| true)]),
|
||||
SmallVec::from_buf([Rc::new(|_, _, _, _| false)]),
|
||||
);
|
||||
set.insert(
|
||||
HandlerKey::new(MouseEvent::hover_disc(), None),
|
||||
SmallVec::from_buf([Rc::new(|_, _, _, _| true)]),
|
||||
SmallVec::from_buf([Rc::new(|_, _, _, _| false)]),
|
||||
);
|
||||
for button in MouseButton::all() {
|
||||
set.insert(
|
||||
HandlerKey::new(MouseEvent::drag_disc(), Some(button)),
|
||||
SmallVec::from_buf([Rc::new(|_, _, _, _| true)]),
|
||||
SmallVec::from_buf([Rc::new(|_, _, _, _| false)]),
|
||||
);
|
||||
set.insert(
|
||||
HandlerKey::new(MouseEvent::down_disc(), Some(button)),
|
||||
SmallVec::from_buf([Rc::new(|_, _, _, _| true)]),
|
||||
SmallVec::from_buf([Rc::new(|_, _, _, _| false)]),
|
||||
);
|
||||
set.insert(
|
||||
HandlerKey::new(MouseEvent::up_disc(), Some(button)),
|
||||
SmallVec::from_buf([Rc::new(|_, _, _, _| true)]),
|
||||
SmallVec::from_buf([Rc::new(|_, _, _, _| false)]),
|
||||
);
|
||||
set.insert(
|
||||
HandlerKey::new(MouseEvent::click_disc(), Some(button)),
|
||||
SmallVec::from_buf([Rc::new(|_, _, _, _| true)]),
|
||||
SmallVec::from_buf([Rc::new(|_, _, _, _| false)]),
|
||||
);
|
||||
set.insert(
|
||||
HandlerKey::new(MouseEvent::down_out_disc(), Some(button)),
|
||||
SmallVec::from_buf([Rc::new(|_, _, _, _| true)]),
|
||||
SmallVec::from_buf([Rc::new(|_, _, _, _| false)]),
|
||||
);
|
||||
set.insert(
|
||||
HandlerKey::new(MouseEvent::up_out_disc(), Some(button)),
|
||||
SmallVec::from_buf([Rc::new(|_, _, _, _| true)]),
|
||||
SmallVec::from_buf([Rc::new(|_, _, _, _| false)]),
|
||||
);
|
||||
}
|
||||
set.insert(
|
||||
HandlerKey::new(MouseEvent::scroll_wheel_disc(), None),
|
||||
SmallVec::from_buf([Rc::new(|_, _, _, _| true)]),
|
||||
SmallVec::from_buf([Rc::new(|_, _, _, _| false)]),
|
||||
);
|
||||
|
||||
HandlerSet { set }
|
||||
|
||||
@@ -39,7 +39,7 @@ use std::{
|
||||
};
|
||||
use sum_tree::TreeMap;
|
||||
use text::operation_queue::OperationQueue;
|
||||
pub use text::{Buffer as TextBuffer, BufferSnapshot as TextBufferSnapshot, *};
|
||||
pub use text::{Buffer as TextBuffer, BufferSnapshot as TextBufferSnapshot, Operation as _, *};
|
||||
use theme::SyntaxTheme;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
use util::RandomCharIter;
|
||||
@@ -311,7 +311,6 @@ pub struct Chunk<'a> {
|
||||
pub highlight_style: Option<HighlightStyle>,
|
||||
pub diagnostic_severity: Option<DiagnosticSeverity>,
|
||||
pub is_unnecessary: bool,
|
||||
pub is_tab: bool,
|
||||
}
|
||||
|
||||
pub struct Diff {
|
||||
@@ -358,6 +357,20 @@ impl Buffer {
|
||||
)
|
||||
}
|
||||
|
||||
pub fn from_file<T: Into<String>>(
|
||||
replica_id: ReplicaId,
|
||||
base_text: T,
|
||||
diff_base: Option<T>,
|
||||
file: Arc<dyn File>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Self {
|
||||
Self::build(
|
||||
TextBuffer::new(replica_id, cx.model_id() as u64, base_text.into()),
|
||||
diff_base.map(|h| h.into().into_boxed_str().into()),
|
||||
Some(file),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn from_proto(
|
||||
replica_id: ReplicaId,
|
||||
message: proto::BufferState,
|
||||
@@ -447,11 +460,7 @@ impl Buffer {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(
|
||||
buffer: TextBuffer,
|
||||
diff_base: Option<String>,
|
||||
file: Option<Arc<dyn File>>,
|
||||
) -> Self {
|
||||
fn build(buffer: TextBuffer, diff_base: Option<String>, file: Option<Arc<dyn File>>) -> Self {
|
||||
let saved_mtime = if let Some(file) = file.as_ref() {
|
||||
file.mtime()
|
||||
} else {
|
||||
@@ -2841,9 +2850,9 @@ impl<'a> Iterator for BufferChunks<'a> {
|
||||
Some(Chunk {
|
||||
text: slice,
|
||||
syntax_highlight_id: highlight_id,
|
||||
highlight_style: None,
|
||||
diagnostic_severity: self.current_diagnostic_severity(),
|
||||
is_unnecessary: self.current_code_is_unnecessary(),
|
||||
..Default::default()
|
||||
})
|
||||
} else {
|
||||
None
|
||||
|
||||
@@ -42,7 +42,7 @@ anyhow.workspace = true
|
||||
async-trait.workspace = true
|
||||
backtrace = "0.3"
|
||||
futures.workspace = true
|
||||
globset.workspace = true
|
||||
glob.workspace = true
|
||||
ignore = "0.4"
|
||||
lazy_static.workspace = true
|
||||
log.workspace = true
|
||||
@@ -58,7 +58,6 @@ similar = "1.3"
|
||||
smol.workspace = true
|
||||
thiserror.workspace = true
|
||||
toml = "0.5"
|
||||
itertools = "0.10"
|
||||
|
||||
[dev-dependencies]
|
||||
ctor.workspace = true
|
||||
@@ -74,6 +73,5 @@ lsp = { path = "../lsp", features = ["test-support"] }
|
||||
settings = { path = "../settings", features = ["test-support"] }
|
||||
util = { path = "../util", features = ["test-support"] }
|
||||
rpc = { path = "../rpc", features = ["test-support"] }
|
||||
git2 = { version = "0.15", default-features = false }
|
||||
tempdir.workspace = true
|
||||
unindent.workspace = true
|
||||
|
||||
121
crates/project/src/lsp_glob_set.rs
Normal file
121
crates/project/src/lsp_glob_set.rs
Normal file
@@ -0,0 +1,121 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use std::path::Path;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct LspGlobSet {
|
||||
patterns: Vec<glob::Pattern>,
|
||||
}
|
||||
|
||||
impl LspGlobSet {
|
||||
pub fn clear(&mut self) {
|
||||
self.patterns.clear();
|
||||
}
|
||||
|
||||
/// Add a pattern to the glob set.
|
||||
///
|
||||
/// LSP's glob syntax supports bash-style brace expansion. For example,
|
||||
/// the pattern '*.{js,ts}' would match all JavaScript or TypeScript files.
|
||||
/// This is not a part of the standard libc glob syntax, and isn't supported
|
||||
/// by the `glob` crate. So we pre-process the glob patterns, producing a
|
||||
/// separate glob `Pattern` object for each part of a brace expansion.
|
||||
pub fn add_pattern(&mut self, pattern: &str) -> Result<()> {
|
||||
// Find all of the ranges of `pattern` that contain matched curly braces.
|
||||
let mut expansion_ranges = Vec::new();
|
||||
let mut expansion_start_ix = None;
|
||||
for (ix, c) in pattern.match_indices(|c| ['{', '}'].contains(&c)) {
|
||||
match c {
|
||||
"{" => {
|
||||
if expansion_start_ix.is_some() {
|
||||
return Err(anyhow!("nested braces in glob patterns aren't supported"));
|
||||
}
|
||||
expansion_start_ix = Some(ix);
|
||||
}
|
||||
"}" => {
|
||||
if let Some(start_ix) = expansion_start_ix {
|
||||
expansion_ranges.push(start_ix..ix + 1);
|
||||
}
|
||||
expansion_start_ix = None;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
// Starting with a single pattern, process each brace expansion by cloning
|
||||
// the pattern once per element of the expansion.
|
||||
let mut unexpanded_patterns = vec![];
|
||||
let mut expanded_patterns = vec![pattern.to_string()];
|
||||
|
||||
for outer_range in expansion_ranges.into_iter().rev() {
|
||||
let inner_range = (outer_range.start + 1)..(outer_range.end - 1);
|
||||
std::mem::swap(&mut unexpanded_patterns, &mut expanded_patterns);
|
||||
for unexpanded_pattern in unexpanded_patterns.drain(..) {
|
||||
for part in unexpanded_pattern[inner_range.clone()].split(',') {
|
||||
let mut expanded_pattern = unexpanded_pattern.clone();
|
||||
expanded_pattern.replace_range(outer_range.clone(), part);
|
||||
expanded_patterns.push(expanded_pattern);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse the final glob patterns and add them to the set.
|
||||
for pattern in expanded_patterns {
|
||||
let pattern = glob::Pattern::new(&pattern)?;
|
||||
self.patterns.push(pattern);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn matches(&self, path: &Path) -> bool {
|
||||
self.patterns
|
||||
.iter()
|
||||
.any(|pattern| pattern.matches_path(path))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_glob_set() {
|
||||
let mut watch = LspGlobSet::default();
|
||||
watch.add_pattern("/a/**/*.rs").unwrap();
|
||||
watch.add_pattern("/a/**/Cargo.toml").unwrap();
|
||||
|
||||
assert!(watch.matches("/a/b.rs".as_ref()));
|
||||
assert!(watch.matches("/a/b/c.rs".as_ref()));
|
||||
|
||||
assert!(!watch.matches("/b/c.rs".as_ref()));
|
||||
assert!(!watch.matches("/a/b.ts".as_ref()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_brace_expansion() {
|
||||
let mut watch = LspGlobSet::default();
|
||||
watch.add_pattern("/a/*.{ts,js,tsx}").unwrap();
|
||||
|
||||
assert!(watch.matches("/a/one.js".as_ref()));
|
||||
assert!(watch.matches("/a/two.ts".as_ref()));
|
||||
assert!(watch.matches("/a/three.tsx".as_ref()));
|
||||
|
||||
assert!(!watch.matches("/a/one.j".as_ref()));
|
||||
assert!(!watch.matches("/a/two.s".as_ref()));
|
||||
assert!(!watch.matches("/a/three.t".as_ref()));
|
||||
assert!(!watch.matches("/a/four.t".as_ref()));
|
||||
assert!(!watch.matches("/a/five.xt".as_ref()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiple_brace_expansion() {
|
||||
let mut watch = LspGlobSet::default();
|
||||
watch.add_pattern("/a/{one,two,three}.{b*c,d*e}").unwrap();
|
||||
|
||||
assert!(watch.matches("/a/one.bic".as_ref()));
|
||||
assert!(watch.matches("/a/two.dole".as_ref()));
|
||||
assert!(watch.matches("/a/three.deeee".as_ref()));
|
||||
|
||||
assert!(!watch.matches("/a/four.bic".as_ref()));
|
||||
assert!(!watch.matches("/a/one.be".as_ref()));
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
mod ignore;
|
||||
mod lsp_command;
|
||||
mod lsp_glob_set;
|
||||
pub mod search;
|
||||
pub mod terminals;
|
||||
pub mod worktree;
|
||||
@@ -17,7 +18,6 @@ use futures::{
|
||||
future::{try_join_all, Shared},
|
||||
AsyncWriteExt, Future, FutureExt, StreamExt, TryFutureExt,
|
||||
};
|
||||
use globset::{Glob, GlobSet, GlobSetBuilder};
|
||||
use gpui::{
|
||||
AnyModelHandle, AppContext, AsyncAppContext, BorrowAppContext, Entity, ModelContext,
|
||||
ModelHandle, Task, WeakModelHandle,
|
||||
@@ -39,6 +39,7 @@ use lsp::{
|
||||
DocumentHighlightKind, LanguageServer, LanguageServerId,
|
||||
};
|
||||
use lsp_command::*;
|
||||
use lsp_glob_set::LspGlobSet;
|
||||
use postage::watch;
|
||||
use rand::prelude::*;
|
||||
use search::SearchQuery;
|
||||
@@ -63,7 +64,6 @@ use std::{
|
||||
},
|
||||
time::{Duration, Instant, SystemTime},
|
||||
};
|
||||
|
||||
use terminals::Terminals;
|
||||
|
||||
use util::{debug_panic, defer, merge_json_value_into, post_inc, ResultExt, TryFutureExt as _};
|
||||
@@ -109,7 +109,6 @@ pub struct Project {
|
||||
collaborators: HashMap<proto::PeerId, Collaborator>,
|
||||
client_subscriptions: Vec<client::Subscription>,
|
||||
_subscriptions: Vec<gpui::Subscription>,
|
||||
next_buffer_id: u64,
|
||||
opened_buffer: (watch::Sender<()>, watch::Receiver<()>),
|
||||
shared_buffers: HashMap<proto::PeerId, HashSet<u64>>,
|
||||
#[allow(clippy::type_complexity)]
|
||||
@@ -121,13 +120,11 @@ pub struct Project {
|
||||
loading_local_worktrees:
|
||||
HashMap<Arc<Path>, Shared<Task<Result<ModelHandle<Worktree>, Arc<anyhow::Error>>>>>,
|
||||
opened_buffers: HashMap<u64, OpenBuffer>,
|
||||
local_buffer_ids_by_path: HashMap<ProjectPath, u64>,
|
||||
local_buffer_ids_by_entry_id: HashMap<ProjectEntryId, u64>,
|
||||
/// A mapping from a buffer ID to None means that we've started waiting for an ID but haven't finished loading it.
|
||||
/// Used for re-issuing buffer requests when peers temporarily disconnect
|
||||
incomplete_remote_buffers: HashMap<u64, Option<ModelHandle<Buffer>>>,
|
||||
buffer_snapshots: HashMap<u64, HashMap<LanguageServerId, Vec<LspBufferSnapshot>>>, // buffer_id -> server_id -> vec of snapshots
|
||||
buffers_being_formatted: HashSet<u64>,
|
||||
buffers_being_formatted: HashSet<usize>,
|
||||
nonce: u128,
|
||||
_maintain_buffer_languages: Task<()>,
|
||||
_maintain_workspace_config: Task<()>,
|
||||
@@ -224,7 +221,7 @@ pub enum LanguageServerState {
|
||||
language: Arc<Language>,
|
||||
adapter: Arc<CachedLspAdapter>,
|
||||
server: Arc<LanguageServer>,
|
||||
watched_paths: HashMap<WorktreeId, GlobSet>,
|
||||
watched_paths: LspGlobSet,
|
||||
simulate_disk_based_diagnostics_completion: Option<Task<()>>,
|
||||
},
|
||||
}
|
||||
@@ -444,14 +441,11 @@ impl Project {
|
||||
worktrees: Default::default(),
|
||||
buffer_ordered_messages_tx: tx,
|
||||
collaborators: Default::default(),
|
||||
next_buffer_id: 0,
|
||||
opened_buffers: Default::default(),
|
||||
shared_buffers: Default::default(),
|
||||
incomplete_remote_buffers: Default::default(),
|
||||
loading_buffers_by_path: Default::default(),
|
||||
loading_local_worktrees: Default::default(),
|
||||
local_buffer_ids_by_path: Default::default(),
|
||||
local_buffer_ids_by_entry_id: Default::default(),
|
||||
buffer_snapshots: Default::default(),
|
||||
join_project_response_message_id: 0,
|
||||
client_state: None,
|
||||
@@ -515,13 +509,10 @@ impl Project {
|
||||
worktrees: Vec::new(),
|
||||
buffer_ordered_messages_tx: tx,
|
||||
loading_buffers_by_path: Default::default(),
|
||||
next_buffer_id: 0,
|
||||
opened_buffer: watch::channel(),
|
||||
shared_buffers: Default::default(),
|
||||
incomplete_remote_buffers: Default::default(),
|
||||
loading_local_worktrees: Default::default(),
|
||||
local_buffer_ids_by_path: Default::default(),
|
||||
local_buffer_ids_by_entry_id: Default::default(),
|
||||
active_entry: None,
|
||||
collaborators: Default::default(),
|
||||
join_project_response_message_id: response.message_id,
|
||||
@@ -1410,10 +1401,9 @@ impl Project {
|
||||
worktree: &ModelHandle<Worktree>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<ModelHandle<Buffer>>> {
|
||||
let buffer_id = post_inc(&mut self.next_buffer_id);
|
||||
let load_buffer = worktree.update(cx, |worktree, cx| {
|
||||
let worktree = worktree.as_local_mut().unwrap();
|
||||
worktree.load_buffer(buffer_id, path, cx)
|
||||
worktree.load_buffer(path, cx)
|
||||
});
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let buffer = load_buffer.await?;
|
||||
@@ -1633,21 +1623,6 @@ impl Project {
|
||||
})
|
||||
.detach();
|
||||
|
||||
if let Some(file) = File::from_dyn(buffer.read(cx).file()) {
|
||||
if file.is_local {
|
||||
self.local_buffer_ids_by_path.insert(
|
||||
ProjectPath {
|
||||
worktree_id: file.worktree_id(cx),
|
||||
path: file.path.clone(),
|
||||
},
|
||||
remote_id,
|
||||
);
|
||||
|
||||
self.local_buffer_ids_by_entry_id
|
||||
.insert(file.entry_id, remote_id);
|
||||
}
|
||||
}
|
||||
|
||||
self.detect_language_for_buffer(buffer, cx);
|
||||
self.register_buffer_with_language_servers(buffer, cx);
|
||||
self.register_buffer_with_copilot(buffer, cx);
|
||||
@@ -2858,39 +2833,10 @@ impl Project {
|
||||
if let Some(LanguageServerState::Running { watched_paths, .. }) =
|
||||
self.language_servers.get_mut(&language_server_id)
|
||||
{
|
||||
eprintln!("change watch");
|
||||
let mut builders = HashMap::default();
|
||||
for watcher in params.watchers {
|
||||
eprintln!(" {}", watcher.glob_pattern);
|
||||
for worktree in &self.worktrees {
|
||||
if let Some(worktree) = worktree.upgrade(cx) {
|
||||
let worktree = worktree.read(cx);
|
||||
if let Some(abs_path) = worktree.abs_path().to_str() {
|
||||
if let Some(suffix) = watcher
|
||||
.glob_pattern
|
||||
.strip_prefix(abs_path)
|
||||
.and_then(|s| s.strip_prefix(std::path::MAIN_SEPARATOR))
|
||||
{
|
||||
if let Some(glob) = Glob::new(suffix).log_err() {
|
||||
builders
|
||||
.entry(worktree.id())
|
||||
.or_insert_with(|| GlobSetBuilder::new())
|
||||
.add(glob);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
watched_paths.clear();
|
||||
for (worktree_id, builder) in builders {
|
||||
if let Ok(globset) = builder.build() {
|
||||
watched_paths.insert(worktree_id, globset);
|
||||
}
|
||||
for watcher in params.watchers {
|
||||
watched_paths.add_pattern(&watcher.glob_pattern).log_err();
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
@@ -3254,11 +3200,9 @@ impl Project {
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
// Do not allow multiple concurrent formatting requests for the
|
||||
// same buffer.
|
||||
this.update(&mut cx, |this, cx| {
|
||||
buffers_with_paths_and_servers.retain(|(buffer, _, _)| {
|
||||
this.buffers_being_formatted
|
||||
.insert(buffer.read(cx).remote_id())
|
||||
});
|
||||
this.update(&mut cx, |this, _| {
|
||||
buffers_with_paths_and_servers
|
||||
.retain(|(buffer, _, _)| this.buffers_being_formatted.insert(buffer.id()));
|
||||
});
|
||||
|
||||
let _cleanup = defer({
|
||||
@@ -3266,10 +3210,9 @@ impl Project {
|
||||
let mut cx = cx.clone();
|
||||
let buffers = &buffers_with_paths_and_servers;
|
||||
move || {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.update(&mut cx, |this, _| {
|
||||
for (buffer, _, _) in buffers {
|
||||
this.buffers_being_formatted
|
||||
.remove(&buffer.read(cx).remote_id());
|
||||
this.buffers_being_formatted.remove(&buffer.id());
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -4257,19 +4200,14 @@ impl Project {
|
||||
if matching_paths_tx.is_closed() {
|
||||
break;
|
||||
}
|
||||
let matches = if query
|
||||
.file_matches(Some(&entry.path))
|
||||
|
||||
abs_path.clear();
|
||||
abs_path.push(&snapshot.abs_path());
|
||||
abs_path.push(&entry.path);
|
||||
let matches = if let Some(file) =
|
||||
fs.open_sync(&abs_path).await.log_err()
|
||||
{
|
||||
abs_path.clear();
|
||||
abs_path.push(&snapshot.abs_path());
|
||||
abs_path.push(&entry.path);
|
||||
if let Some(file) =
|
||||
fs.open_sync(&abs_path).await.log_err()
|
||||
{
|
||||
query.detect(file).unwrap_or(false)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
query.detect(file).unwrap_or(false)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
@@ -4353,21 +4291,15 @@ impl Project {
|
||||
let mut buffers_rx = buffers_rx.clone();
|
||||
scope.spawn(async move {
|
||||
while let Some((buffer, snapshot)) = buffers_rx.next().await {
|
||||
let buffer_matches = if query.file_matches(
|
||||
snapshot.file().map(|file| file.path().as_ref()),
|
||||
) {
|
||||
query
|
||||
.search(snapshot.as_rope())
|
||||
.await
|
||||
.iter()
|
||||
.map(|range| {
|
||||
snapshot.anchor_before(range.start)
|
||||
..snapshot.anchor_after(range.end)
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
let buffer_matches = query
|
||||
.search(snapshot.as_rope())
|
||||
.await
|
||||
.iter()
|
||||
.map(|range| {
|
||||
snapshot.anchor_before(range.start)
|
||||
..snapshot.anchor_after(range.end)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
if !buffer_matches.is_empty() {
|
||||
worker_matched_buffers
|
||||
.insert(buffer.clone(), buffer_matches);
|
||||
@@ -4585,7 +4517,7 @@ impl Project {
|
||||
if worktree.read(cx).is_local() {
|
||||
cx.subscribe(worktree, |this, worktree, event, cx| match event {
|
||||
worktree::Event::UpdatedEntries(changes) => {
|
||||
this.update_local_worktree_buffers(&worktree, &changes, cx);
|
||||
this.update_local_worktree_buffers(&worktree, cx);
|
||||
this.update_local_worktree_language_servers(&worktree, changes, cx);
|
||||
}
|
||||
worktree::Event::UpdatedGitRepositories(updated_repos) => {
|
||||
@@ -4619,106 +4551,80 @@ impl Project {
|
||||
fn update_local_worktree_buffers(
|
||||
&mut self,
|
||||
worktree_handle: &ModelHandle<Worktree>,
|
||||
changes: &HashMap<(Arc<Path>, ProjectEntryId), PathChange>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
let snapshot = worktree_handle.read(cx).snapshot();
|
||||
|
||||
let mut buffers_to_delete = Vec::new();
|
||||
let mut renamed_buffers = Vec::new();
|
||||
for (path, entry_id) in changes.keys() {
|
||||
let worktree_id = worktree_handle.read(cx).id();
|
||||
let project_path = ProjectPath {
|
||||
worktree_id,
|
||||
path: path.clone(),
|
||||
};
|
||||
|
||||
let buffer_id = match self.local_buffer_ids_by_entry_id.get(entry_id) {
|
||||
Some(&buffer_id) => buffer_id,
|
||||
None => match self.local_buffer_ids_by_path.get(&project_path) {
|
||||
Some(&buffer_id) => buffer_id,
|
||||
None => continue,
|
||||
},
|
||||
};
|
||||
for (buffer_id, buffer) in &self.opened_buffers {
|
||||
if let Some(buffer) = buffer.upgrade(cx) {
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
if let Some(old_file) = File::from_dyn(buffer.file()) {
|
||||
if old_file.worktree != *worktree_handle {
|
||||
return;
|
||||
}
|
||||
|
||||
let open_buffer = self.opened_buffers.get(&buffer_id);
|
||||
let buffer = if let Some(buffer) = open_buffer.and_then(|buffer| buffer.upgrade(cx)) {
|
||||
buffer
|
||||
let new_file = if let Some(entry) = snapshot.entry_for_id(old_file.entry_id)
|
||||
{
|
||||
File {
|
||||
is_local: true,
|
||||
entry_id: entry.id,
|
||||
mtime: entry.mtime,
|
||||
path: entry.path.clone(),
|
||||
worktree: worktree_handle.clone(),
|
||||
is_deleted: false,
|
||||
}
|
||||
} else if let Some(entry) =
|
||||
snapshot.entry_for_path(old_file.path().as_ref())
|
||||
{
|
||||
File {
|
||||
is_local: true,
|
||||
entry_id: entry.id,
|
||||
mtime: entry.mtime,
|
||||
path: entry.path.clone(),
|
||||
worktree: worktree_handle.clone(),
|
||||
is_deleted: false,
|
||||
}
|
||||
} else {
|
||||
File {
|
||||
is_local: true,
|
||||
entry_id: old_file.entry_id,
|
||||
path: old_file.path().clone(),
|
||||
mtime: old_file.mtime(),
|
||||
worktree: worktree_handle.clone(),
|
||||
is_deleted: true,
|
||||
}
|
||||
};
|
||||
|
||||
let old_path = old_file.abs_path(cx);
|
||||
if new_file.abs_path(cx) != old_path {
|
||||
renamed_buffers.push((cx.handle(), old_file.clone()));
|
||||
}
|
||||
|
||||
if new_file != *old_file {
|
||||
if let Some(project_id) = self.remote_id() {
|
||||
self.client
|
||||
.send(proto::UpdateBufferFile {
|
||||
project_id,
|
||||
buffer_id: *buffer_id as u64,
|
||||
file: Some(new_file.to_proto()),
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
|
||||
buffer.file_updated(Arc::new(new_file), cx).detach();
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
self.opened_buffers.remove(&buffer_id);
|
||||
self.local_buffer_ids_by_path.remove(&project_path);
|
||||
self.local_buffer_ids_by_entry_id.remove(entry_id);
|
||||
continue;
|
||||
};
|
||||
buffers_to_delete.push(*buffer_id);
|
||||
}
|
||||
}
|
||||
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
if let Some(old_file) = File::from_dyn(buffer.file()) {
|
||||
if old_file.worktree != *worktree_handle {
|
||||
return;
|
||||
}
|
||||
|
||||
let new_file = if let Some(entry) = snapshot.entry_for_id(old_file.entry_id) {
|
||||
File {
|
||||
is_local: true,
|
||||
entry_id: entry.id,
|
||||
mtime: entry.mtime,
|
||||
path: entry.path.clone(),
|
||||
worktree: worktree_handle.clone(),
|
||||
is_deleted: false,
|
||||
}
|
||||
} else if let Some(entry) = snapshot.entry_for_path(old_file.path().as_ref()) {
|
||||
File {
|
||||
is_local: true,
|
||||
entry_id: entry.id,
|
||||
mtime: entry.mtime,
|
||||
path: entry.path.clone(),
|
||||
worktree: worktree_handle.clone(),
|
||||
is_deleted: false,
|
||||
}
|
||||
} else {
|
||||
File {
|
||||
is_local: true,
|
||||
entry_id: old_file.entry_id,
|
||||
path: old_file.path().clone(),
|
||||
mtime: old_file.mtime(),
|
||||
worktree: worktree_handle.clone(),
|
||||
is_deleted: true,
|
||||
}
|
||||
};
|
||||
|
||||
let old_path = old_file.abs_path(cx);
|
||||
if new_file.abs_path(cx) != old_path {
|
||||
renamed_buffers.push((cx.handle(), old_file.clone()));
|
||||
self.local_buffer_ids_by_path.remove(&project_path);
|
||||
self.local_buffer_ids_by_path.insert(
|
||||
ProjectPath {
|
||||
worktree_id,
|
||||
path: path.clone(),
|
||||
},
|
||||
buffer_id,
|
||||
);
|
||||
}
|
||||
|
||||
if new_file.entry_id != *entry_id {
|
||||
self.local_buffer_ids_by_entry_id.remove(entry_id);
|
||||
self.local_buffer_ids_by_entry_id
|
||||
.insert(new_file.entry_id, buffer_id);
|
||||
}
|
||||
|
||||
if new_file != *old_file {
|
||||
if let Some(project_id) = self.remote_id() {
|
||||
self.client
|
||||
.send(proto::UpdateBufferFile {
|
||||
project_id,
|
||||
buffer_id: buffer_id as u64,
|
||||
file: Some(new_file.to_proto()),
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
|
||||
buffer.file_updated(Arc::new(new_file), cx).detach();
|
||||
}
|
||||
}
|
||||
});
|
||||
for buffer_id in buffers_to_delete {
|
||||
self.opened_buffers.remove(&buffer_id);
|
||||
}
|
||||
|
||||
for (buffer, old_file) in renamed_buffers {
|
||||
@@ -4731,42 +4637,28 @@ impl Project {
|
||||
fn update_local_worktree_language_servers(
|
||||
&mut self,
|
||||
worktree_handle: &ModelHandle<Worktree>,
|
||||
changes: &HashMap<(Arc<Path>, ProjectEntryId), PathChange>,
|
||||
changes: &HashMap<Arc<Path>, PathChange>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
if changes.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let worktree_id = worktree_handle.read(cx).id();
|
||||
let mut language_server_ids = self
|
||||
.language_server_ids
|
||||
.iter()
|
||||
.filter_map(|((server_worktree_id, _), server_id)| {
|
||||
(*server_worktree_id == worktree_id).then_some(*server_id)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
language_server_ids.sort();
|
||||
language_server_ids.dedup();
|
||||
|
||||
let abs_path = worktree_handle.read(cx).abs_path();
|
||||
for server_id in &language_server_ids {
|
||||
if let Some(server) = self.language_servers.get(server_id) {
|
||||
if let LanguageServerState::Running {
|
||||
server,
|
||||
watched_paths,
|
||||
..
|
||||
} = server
|
||||
{
|
||||
if let Some(watched_paths) = watched_paths.get(&worktree_id) {
|
||||
for ((server_worktree_id, _), server_id) in &self.language_server_ids {
|
||||
if *server_worktree_id == worktree_id {
|
||||
if let Some(server) = self.language_servers.get(server_id) {
|
||||
if let LanguageServerState::Running {
|
||||
server,
|
||||
watched_paths,
|
||||
..
|
||||
} = server
|
||||
{
|
||||
let params = lsp::DidChangeWatchedFilesParams {
|
||||
changes: changes
|
||||
.iter()
|
||||
.filter_map(|((path, _), change)| {
|
||||
if watched_paths.is_match(&path) {
|
||||
.filter_map(|(path, change)| {
|
||||
let path = abs_path.join(path);
|
||||
if watched_paths.matches(&path) {
|
||||
Some(lsp::FileEvent {
|
||||
uri: lsp::Url::from_file_path(abs_path.join(path))
|
||||
.unwrap(),
|
||||
uri: lsp::Url::from_file_path(path).unwrap(),
|
||||
typ: match change {
|
||||
PathChange::Added => lsp::FileChangeType::CREATED,
|
||||
PathChange::Removed => lsp::FileChangeType::DELETED,
|
||||
@@ -4796,50 +4688,40 @@ impl Project {
|
||||
|
||||
fn update_local_worktree_buffers_git_repos(
|
||||
&mut self,
|
||||
worktree_handle: ModelHandle<Worktree>,
|
||||
repos: &HashMap<Arc<Path>, LocalRepositoryEntry>,
|
||||
worktree: ModelHandle<Worktree>,
|
||||
repos: &[GitRepositoryEntry],
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
debug_assert!(worktree_handle.read(cx).is_local());
|
||||
|
||||
for (_, buffer) in &self.opened_buffers {
|
||||
if let Some(buffer) = buffer.upgrade(cx) {
|
||||
let file = match File::from_dyn(buffer.read(cx).file()) {
|
||||
Some(file) => file,
|
||||
None => continue,
|
||||
};
|
||||
if file.worktree != worktree_handle {
|
||||
if file.worktree != worktree {
|
||||
continue;
|
||||
}
|
||||
|
||||
let path = file.path().clone();
|
||||
|
||||
let worktree = worktree_handle.read(cx);
|
||||
|
||||
let (work_directory, repo) = match repos
|
||||
.iter()
|
||||
.find(|(work_directory, _)| path.starts_with(work_directory))
|
||||
{
|
||||
let repo = match repos.iter().find(|repo| repo.manages(&path)) {
|
||||
Some(repo) => repo.clone(),
|
||||
None => return,
|
||||
};
|
||||
|
||||
let relative_repo = match path.strip_prefix(work_directory).log_err() {
|
||||
Some(relative_repo) => relative_repo.to_owned(),
|
||||
None => return,
|
||||
let relative_repo = match path.strip_prefix(repo.content_path) {
|
||||
Ok(relative_repo) => relative_repo.to_owned(),
|
||||
Err(_) => return,
|
||||
};
|
||||
|
||||
drop(worktree);
|
||||
|
||||
let remote_id = self.remote_id();
|
||||
let client = self.client.clone();
|
||||
let git_ptr = repo.repo_ptr.clone();
|
||||
let diff_base_task = cx
|
||||
.background()
|
||||
.spawn(async move { git_ptr.lock().load_index_text(&relative_repo) });
|
||||
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
let diff_base = diff_base_task.await;
|
||||
let diff_base = cx
|
||||
.background()
|
||||
.spawn(async move { repo.repo.lock().load_index_text(&relative_repo) })
|
||||
.await;
|
||||
|
||||
let buffer_id = buffer.update(&mut cx, |buffer, cx| {
|
||||
buffer.set_diff_base(diff_base.clone(), cx);
|
||||
|
||||
@@ -2,8 +2,8 @@ use crate::{worktree::WorktreeHandle, Event, *};
|
||||
use fs::LineEnding;
|
||||
use fs::{FakeFs, RealFs};
|
||||
use futures::{future, StreamExt};
|
||||
use globset::Glob;
|
||||
use gpui::{executor::Deterministic, test::subscribe, AppContext};
|
||||
use gpui::AppContext;
|
||||
use gpui::{executor::Deterministic, test::subscribe};
|
||||
use language::{
|
||||
tree_sitter_rust, tree_sitter_typescript, Diagnostic, FakeLspAdapter, LanguageConfig,
|
||||
OffsetRangeExt, Point, ToPoint,
|
||||
@@ -503,7 +503,7 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon
|
||||
register_options: serde_json::to_value(
|
||||
lsp::DidChangeWatchedFilesRegistrationOptions {
|
||||
watchers: vec![lsp::FileSystemWatcher {
|
||||
glob_pattern: "/the-root/*.{rs,c}".to_string(),
|
||||
glob_pattern: "*.{rs,c}".to_string(),
|
||||
kind: None,
|
||||
}],
|
||||
},
|
||||
@@ -3297,13 +3297,9 @@ async fn test_search(cx: &mut gpui::TestAppContext) {
|
||||
.await;
|
||||
let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
|
||||
assert_eq!(
|
||||
search(
|
||||
&project,
|
||||
SearchQuery::text("TWO", false, true, Vec::new(), Vec::new()),
|
||||
cx
|
||||
)
|
||||
.await
|
||||
.unwrap(),
|
||||
search(&project, SearchQuery::text("TWO", false, true), cx)
|
||||
.await
|
||||
.unwrap(),
|
||||
HashMap::from_iter([
|
||||
("two.rs".to_string(), vec![6..9]),
|
||||
("three.rs".to_string(), vec![37..40])
|
||||
@@ -3322,361 +3318,37 @@ async fn test_search(cx: &mut gpui::TestAppContext) {
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
search(
|
||||
&project,
|
||||
SearchQuery::text("TWO", false, true, Vec::new(), Vec::new()),
|
||||
cx
|
||||
)
|
||||
.await
|
||||
.unwrap(),
|
||||
search(&project, SearchQuery::text("TWO", false, true), cx)
|
||||
.await
|
||||
.unwrap(),
|
||||
HashMap::from_iter([
|
||||
("two.rs".to_string(), vec![6..9]),
|
||||
("three.rs".to_string(), vec![37..40]),
|
||||
("four.rs".to_string(), vec![25..28, 36..39])
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) {
|
||||
let search_query = "file";
|
||||
async fn search(
|
||||
project: &ModelHandle<Project>,
|
||||
query: SearchQuery,
|
||||
cx: &mut gpui::TestAppContext,
|
||||
) -> Result<HashMap<String, Vec<Range<usize>>>> {
|
||||
let results = project
|
||||
.update(cx, |project, cx| project.search(query, cx))
|
||||
.await?;
|
||||
|
||||
let fs = FakeFs::new(cx.background());
|
||||
fs.insert_tree(
|
||||
"/dir",
|
||||
json!({
|
||||
"one.rs": r#"// Rust file one"#,
|
||||
"one.ts": r#"// TypeScript file one"#,
|
||||
"two.rs": r#"// Rust file two"#,
|
||||
"two.ts": r#"// TypeScript file two"#,
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
|
||||
|
||||
assert!(
|
||||
search(
|
||||
&project,
|
||||
SearchQuery::text(
|
||||
search_query,
|
||||
false,
|
||||
true,
|
||||
vec![Glob::new("*.odd").unwrap().compile_matcher()],
|
||||
Vec::new()
|
||||
),
|
||||
cx
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.is_empty(),
|
||||
"If no inclusions match, no files should be returned"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
search(
|
||||
&project,
|
||||
SearchQuery::text(
|
||||
search_query,
|
||||
false,
|
||||
true,
|
||||
vec![Glob::new("*.rs").unwrap().compile_matcher()],
|
||||
Vec::new()
|
||||
),
|
||||
cx
|
||||
)
|
||||
.await
|
||||
.unwrap(),
|
||||
HashMap::from_iter([
|
||||
("one.rs".to_string(), vec![8..12]),
|
||||
("two.rs".to_string(), vec![8..12]),
|
||||
]),
|
||||
"Rust only search should give only Rust files"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
search(
|
||||
&project,
|
||||
SearchQuery::text(
|
||||
search_query,
|
||||
false,
|
||||
true,
|
||||
vec![
|
||||
Glob::new("*.ts").unwrap().compile_matcher(),
|
||||
Glob::new("*.odd").unwrap().compile_matcher(),
|
||||
],
|
||||
Vec::new()
|
||||
),
|
||||
cx
|
||||
)
|
||||
.await
|
||||
.unwrap(),
|
||||
HashMap::from_iter([
|
||||
("one.ts".to_string(), vec![14..18]),
|
||||
("two.ts".to_string(), vec![14..18]),
|
||||
]),
|
||||
"TypeScript only search should give only TypeScript files, even if other inclusions don't match anything"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
search(
|
||||
&project,
|
||||
SearchQuery::text(
|
||||
search_query,
|
||||
false,
|
||||
true,
|
||||
vec![
|
||||
Glob::new("*.rs").unwrap().compile_matcher(),
|
||||
Glob::new("*.ts").unwrap().compile_matcher(),
|
||||
Glob::new("*.odd").unwrap().compile_matcher(),
|
||||
],
|
||||
Vec::new()
|
||||
),
|
||||
cx
|
||||
)
|
||||
.await
|
||||
.unwrap(),
|
||||
HashMap::from_iter([
|
||||
("one.rs".to_string(), vec![8..12]),
|
||||
("one.ts".to_string(), vec![14..18]),
|
||||
("two.rs".to_string(), vec![8..12]),
|
||||
("two.ts".to_string(), vec![14..18]),
|
||||
]),
|
||||
"Rust and typescript search should give both Rust and TypeScript files, even if other inclusions don't match anything"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) {
|
||||
let search_query = "file";
|
||||
|
||||
let fs = FakeFs::new(cx.background());
|
||||
fs.insert_tree(
|
||||
"/dir",
|
||||
json!({
|
||||
"one.rs": r#"// Rust file one"#,
|
||||
"one.ts": r#"// TypeScript file one"#,
|
||||
"two.rs": r#"// Rust file two"#,
|
||||
"two.ts": r#"// TypeScript file two"#,
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
|
||||
|
||||
assert_eq!(
|
||||
search(
|
||||
&project,
|
||||
SearchQuery::text(
|
||||
search_query,
|
||||
false,
|
||||
true,
|
||||
Vec::new(),
|
||||
vec![Glob::new("*.odd").unwrap().compile_matcher()],
|
||||
),
|
||||
cx
|
||||
)
|
||||
.await
|
||||
.unwrap(),
|
||||
HashMap::from_iter([
|
||||
("one.rs".to_string(), vec![8..12]),
|
||||
("one.ts".to_string(), vec![14..18]),
|
||||
("two.rs".to_string(), vec![8..12]),
|
||||
("two.ts".to_string(), vec![14..18]),
|
||||
]),
|
||||
"If no exclusions match, all files should be returned"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
search(
|
||||
&project,
|
||||
SearchQuery::text(
|
||||
search_query,
|
||||
false,
|
||||
true,
|
||||
Vec::new(),
|
||||
vec![Glob::new("*.rs").unwrap().compile_matcher()],
|
||||
),
|
||||
cx
|
||||
)
|
||||
.await
|
||||
.unwrap(),
|
||||
HashMap::from_iter([
|
||||
("one.ts".to_string(), vec![14..18]),
|
||||
("two.ts".to_string(), vec![14..18]),
|
||||
]),
|
||||
"Rust exclusion search should give only TypeScript files"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
search(
|
||||
&project,
|
||||
SearchQuery::text(
|
||||
search_query,
|
||||
false,
|
||||
true,
|
||||
Vec::new(),
|
||||
vec![
|
||||
Glob::new("*.ts").unwrap().compile_matcher(),
|
||||
Glob::new("*.odd").unwrap().compile_matcher(),
|
||||
],
|
||||
),
|
||||
cx
|
||||
)
|
||||
.await
|
||||
.unwrap(),
|
||||
HashMap::from_iter([
|
||||
("one.rs".to_string(), vec![8..12]),
|
||||
("two.rs".to_string(), vec![8..12]),
|
||||
]),
|
||||
"TypeScript exclusion search should give only Rust files, even if other exclusions don't match anything"
|
||||
);
|
||||
|
||||
assert!(
|
||||
search(
|
||||
&project,
|
||||
SearchQuery::text(
|
||||
search_query,
|
||||
false,
|
||||
true,
|
||||
Vec::new(),
|
||||
vec![
|
||||
Glob::new("*.rs").unwrap().compile_matcher(),
|
||||
Glob::new("*.ts").unwrap().compile_matcher(),
|
||||
Glob::new("*.odd").unwrap().compile_matcher(),
|
||||
],
|
||||
),
|
||||
cx
|
||||
)
|
||||
.await
|
||||
.unwrap().is_empty(),
|
||||
"Rust and typescript exclusion should give no files, even if other exclusions don't match anything"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContext) {
|
||||
let search_query = "file";
|
||||
|
||||
let fs = FakeFs::new(cx.background());
|
||||
fs.insert_tree(
|
||||
"/dir",
|
||||
json!({
|
||||
"one.rs": r#"// Rust file one"#,
|
||||
"one.ts": r#"// TypeScript file one"#,
|
||||
"two.rs": r#"// Rust file two"#,
|
||||
"two.ts": r#"// TypeScript file two"#,
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
|
||||
|
||||
assert!(
|
||||
search(
|
||||
&project,
|
||||
SearchQuery::text(
|
||||
search_query,
|
||||
false,
|
||||
true,
|
||||
vec![Glob::new("*.odd").unwrap().compile_matcher()],
|
||||
vec![Glob::new("*.odd").unwrap().compile_matcher()],
|
||||
),
|
||||
cx
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.is_empty(),
|
||||
"If both no exclusions and inclusions match, exclusions should win and return nothing"
|
||||
);
|
||||
|
||||
assert!(
|
||||
search(
|
||||
&project,
|
||||
SearchQuery::text(
|
||||
search_query,
|
||||
false,
|
||||
true,
|
||||
vec![Glob::new("*.ts").unwrap().compile_matcher()],
|
||||
vec![Glob::new("*.ts").unwrap().compile_matcher()],
|
||||
),
|
||||
cx
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.is_empty(),
|
||||
"If both TypeScript exclusions and inclusions match, exclusions should win and return nothing files."
|
||||
);
|
||||
|
||||
assert!(
|
||||
search(
|
||||
&project,
|
||||
SearchQuery::text(
|
||||
search_query,
|
||||
false,
|
||||
true,
|
||||
vec![
|
||||
Glob::new("*.ts").unwrap().compile_matcher(),
|
||||
Glob::new("*.odd").unwrap().compile_matcher()
|
||||
],
|
||||
vec![
|
||||
Glob::new("*.ts").unwrap().compile_matcher(),
|
||||
Glob::new("*.odd").unwrap().compile_matcher()
|
||||
],
|
||||
),
|
||||
cx
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.is_empty(),
|
||||
"Non-matching inclusions and exclusions should not change that."
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
search(
|
||||
&project,
|
||||
SearchQuery::text(
|
||||
search_query,
|
||||
false,
|
||||
true,
|
||||
vec![
|
||||
Glob::new("*.ts").unwrap().compile_matcher(),
|
||||
Glob::new("*.odd").unwrap().compile_matcher()
|
||||
],
|
||||
vec![
|
||||
Glob::new("*.rs").unwrap().compile_matcher(),
|
||||
Glob::new("*.odd").unwrap().compile_matcher()
|
||||
],
|
||||
),
|
||||
cx
|
||||
)
|
||||
.await
|
||||
.unwrap(),
|
||||
HashMap::from_iter([
|
||||
("one.ts".to_string(), vec![14..18]),
|
||||
("two.ts".to_string(), vec![14..18]),
|
||||
]),
|
||||
"Non-intersecting TypeScript inclusions and Rust exclusions should return TypeScript files"
|
||||
);
|
||||
}
|
||||
|
||||
async fn search(
|
||||
project: &ModelHandle<Project>,
|
||||
query: SearchQuery,
|
||||
cx: &mut gpui::TestAppContext,
|
||||
) -> Result<HashMap<String, Vec<Range<usize>>>> {
|
||||
let results = project
|
||||
.update(cx, |project, cx| project.search(query, cx))
|
||||
.await?;
|
||||
|
||||
Ok(results
|
||||
.into_iter()
|
||||
.map(|(buffer, ranges)| {
|
||||
buffer.read_with(cx, |buffer, _| {
|
||||
let path = buffer.file().unwrap().path().to_string_lossy().to_string();
|
||||
let ranges = ranges
|
||||
.into_iter()
|
||||
.map(|range| range.to_offset(buffer))
|
||||
.collect::<Vec<_>>();
|
||||
(path, ranges)
|
||||
Ok(results
|
||||
.into_iter()
|
||||
.map(|(buffer, ranges)| {
|
||||
buffer.read_with(cx, |buffer, _| {
|
||||
let path = buffer.file().unwrap().path().to_string_lossy().to_string();
|
||||
let ranges = ranges
|
||||
.into_iter()
|
||||
.map(|range| range.to_offset(buffer))
|
||||
.collect::<Vec<_>>();
|
||||
(path, ranges)
|
||||
})
|
||||
})
|
||||
})
|
||||
.collect())
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,27 +1,22 @@
|
||||
use aho_corasick::{AhoCorasick, AhoCorasickBuilder};
|
||||
use anyhow::Result;
|
||||
use client::proto;
|
||||
use globset::{Glob, GlobMatcher};
|
||||
use itertools::Itertools;
|
||||
use language::{char_kind, Rope};
|
||||
use regex::{Regex, RegexBuilder};
|
||||
use smol::future::yield_now;
|
||||
use std::{
|
||||
io::{BufRead, BufReader, Read},
|
||||
ops::Range,
|
||||
path::Path,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone)]
|
||||
pub enum SearchQuery {
|
||||
Text {
|
||||
search: Arc<AhoCorasick<usize>>,
|
||||
query: Arc<str>,
|
||||
whole_word: bool,
|
||||
case_sensitive: bool,
|
||||
files_to_include: Vec<GlobMatcher>,
|
||||
files_to_exclude: Vec<GlobMatcher>,
|
||||
},
|
||||
Regex {
|
||||
regex: Regex,
|
||||
@@ -29,19 +24,11 @@ pub enum SearchQuery {
|
||||
multiline: bool,
|
||||
whole_word: bool,
|
||||
case_sensitive: bool,
|
||||
files_to_include: Vec<GlobMatcher>,
|
||||
files_to_exclude: Vec<GlobMatcher>,
|
||||
},
|
||||
}
|
||||
|
||||
impl SearchQuery {
|
||||
pub fn text(
|
||||
query: impl ToString,
|
||||
whole_word: bool,
|
||||
case_sensitive: bool,
|
||||
files_to_include: Vec<GlobMatcher>,
|
||||
files_to_exclude: Vec<GlobMatcher>,
|
||||
) -> Self {
|
||||
pub fn text(query: impl ToString, whole_word: bool, case_sensitive: bool) -> Self {
|
||||
let query = query.to_string();
|
||||
let search = AhoCorasickBuilder::new()
|
||||
.auto_configure(&[&query])
|
||||
@@ -52,18 +39,10 @@ impl SearchQuery {
|
||||
query: Arc::from(query),
|
||||
whole_word,
|
||||
case_sensitive,
|
||||
files_to_include,
|
||||
files_to_exclude,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn regex(
|
||||
query: impl ToString,
|
||||
whole_word: bool,
|
||||
case_sensitive: bool,
|
||||
files_to_include: Vec<GlobMatcher>,
|
||||
files_to_exclude: Vec<GlobMatcher>,
|
||||
) -> Result<Self> {
|
||||
pub fn regex(query: impl ToString, whole_word: bool, case_sensitive: bool) -> Result<Self> {
|
||||
let mut query = query.to_string();
|
||||
let initial_query = Arc::from(query.as_str());
|
||||
if whole_word {
|
||||
@@ -85,27 +64,17 @@ impl SearchQuery {
|
||||
multiline,
|
||||
whole_word,
|
||||
case_sensitive,
|
||||
files_to_include,
|
||||
files_to_exclude,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn from_proto(message: proto::SearchProject) -> Result<Self> {
|
||||
if message.regex {
|
||||
Self::regex(
|
||||
message.query,
|
||||
message.whole_word,
|
||||
message.case_sensitive,
|
||||
deserialize_globs(&message.files_to_include)?,
|
||||
deserialize_globs(&message.files_to_exclude)?,
|
||||
)
|
||||
Self::regex(message.query, message.whole_word, message.case_sensitive)
|
||||
} else {
|
||||
Ok(Self::text(
|
||||
message.query,
|
||||
message.whole_word,
|
||||
message.case_sensitive,
|
||||
deserialize_globs(&message.files_to_include)?,
|
||||
deserialize_globs(&message.files_to_exclude)?,
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -117,16 +86,6 @@ impl SearchQuery {
|
||||
regex: self.is_regex(),
|
||||
whole_word: self.whole_word(),
|
||||
case_sensitive: self.case_sensitive(),
|
||||
files_to_include: self
|
||||
.files_to_include()
|
||||
.iter()
|
||||
.map(|g| g.glob().to_string())
|
||||
.join(","),
|
||||
files_to_exclude: self
|
||||
.files_to_exclude()
|
||||
.iter()
|
||||
.map(|g| g.glob().to_string())
|
||||
.join(","),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -265,52 +224,4 @@ impl SearchQuery {
|
||||
pub fn is_regex(&self) -> bool {
|
||||
matches!(self, Self::Regex { .. })
|
||||
}
|
||||
|
||||
pub fn files_to_include(&self) -> &[GlobMatcher] {
|
||||
match self {
|
||||
Self::Text {
|
||||
files_to_include, ..
|
||||
} => files_to_include,
|
||||
Self::Regex {
|
||||
files_to_include, ..
|
||||
} => files_to_include,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn files_to_exclude(&self) -> &[GlobMatcher] {
|
||||
match self {
|
||||
Self::Text {
|
||||
files_to_exclude, ..
|
||||
} => files_to_exclude,
|
||||
Self::Regex {
|
||||
files_to_exclude, ..
|
||||
} => files_to_exclude,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn file_matches(&self, file_path: Option<&Path>) -> bool {
|
||||
match file_path {
|
||||
Some(file_path) => {
|
||||
!self
|
||||
.files_to_exclude()
|
||||
.iter()
|
||||
.any(|exclude_glob| exclude_glob.is_match(file_path))
|
||||
&& (self.files_to_include().is_empty()
|
||||
|| self
|
||||
.files_to_include()
|
||||
.iter()
|
||||
.any(|include_glob| include_glob.is_match(file_path)))
|
||||
}
|
||||
None => self.files_to_include().is_empty(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_globs(glob_set: &str) -> Result<Vec<GlobMatcher>> {
|
||||
glob_set
|
||||
.split(',')
|
||||
.map(str::trim)
|
||||
.filter(|glob_str| !glob_str.is_empty())
|
||||
.map(|glob_str| Ok(Glob::new(glob_str)?.compile_matcher()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,7 @@ use gpui::{
|
||||
actions,
|
||||
anyhow::{anyhow, Result},
|
||||
elements::{
|
||||
AnchorCorner, ChildView, ComponentHost, ContainerStyle, Empty, Flex, MouseEventHandler,
|
||||
AnchorCorner, ChildView, ContainerStyle, Empty, Flex, Label, MouseEventHandler,
|
||||
ParentElement, ScrollTarget, Stack, Svg, UniformList, UniformListState,
|
||||
},
|
||||
geometry::vector::Vector2F,
|
||||
@@ -16,10 +16,7 @@ use gpui::{
|
||||
ViewHandle, WeakViewHandle,
|
||||
};
|
||||
use menu::{Confirm, SelectNext, SelectPrev};
|
||||
use project::{
|
||||
repository::GitFileStatus, Entry, EntryKind, Project, ProjectEntryId, ProjectPath, Worktree,
|
||||
WorktreeId,
|
||||
};
|
||||
use project::{Entry, EntryKind, Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId};
|
||||
use settings::Settings;
|
||||
use std::{
|
||||
cmp::Ordering,
|
||||
@@ -29,7 +26,7 @@ use std::{
|
||||
path::Path,
|
||||
sync::Arc,
|
||||
};
|
||||
use theme::{ui::FileName, ProjectPanelEntry};
|
||||
use theme::ProjectPanelEntry;
|
||||
use unicase::UniCase;
|
||||
use workspace::Workspace;
|
||||
|
||||
@@ -89,7 +86,6 @@ pub struct EntryDetails {
|
||||
is_editing: bool,
|
||||
is_processing: bool,
|
||||
is_cut: bool,
|
||||
git_status: Option<GitFileStatus>,
|
||||
}
|
||||
|
||||
actions!(
|
||||
@@ -200,7 +196,6 @@ impl ProjectPanel {
|
||||
})
|
||||
.detach();
|
||||
|
||||
let view_id = cx.view_id();
|
||||
let mut this = Self {
|
||||
project: project.clone(),
|
||||
list: Default::default(),
|
||||
@@ -211,7 +206,7 @@ impl ProjectPanel {
|
||||
edit_state: None,
|
||||
filename_editor,
|
||||
clipboard_entry: None,
|
||||
context_menu: cx.add_view(|cx| ContextMenu::new(view_id, cx)),
|
||||
context_menu: cx.add_view(ContextMenu::new),
|
||||
dragged_entry_destination: None,
|
||||
workspace: workspace.weak_handle(),
|
||||
};
|
||||
@@ -1012,15 +1007,6 @@ impl ProjectPanel {
|
||||
|
||||
let entry_range = range.start.saturating_sub(ix)..end_ix - ix;
|
||||
for entry in &visible_worktree_entries[entry_range] {
|
||||
let path = &entry.path;
|
||||
let status = (entry.path.parent().is_some() && !entry.is_ignored)
|
||||
.then(|| {
|
||||
snapshot
|
||||
.repo_for(path)
|
||||
.and_then(|entry| entry.status_for_path(&snapshot, path))
|
||||
})
|
||||
.flatten();
|
||||
|
||||
let mut details = EntryDetails {
|
||||
filename: entry
|
||||
.path
|
||||
@@ -1041,7 +1027,6 @@ impl ProjectPanel {
|
||||
is_cut: self
|
||||
.clipboard_entry
|
||||
.map_or(false, |e| e.is_cut() && e.entry_id() == entry.id),
|
||||
git_status: status,
|
||||
};
|
||||
|
||||
if let Some(edit_state) = &self.edit_state {
|
||||
@@ -1110,16 +1095,12 @@ impl ProjectPanel {
|
||||
.flex(1.0, true)
|
||||
.into_any()
|
||||
} else {
|
||||
ComponentHost::new(FileName::new(
|
||||
details.filename.clone(),
|
||||
details.git_status,
|
||||
FileName::style(style.text.clone(), &cx.global::<Settings>().theme),
|
||||
))
|
||||
.contained()
|
||||
.with_margin_left(style.icon_spacing)
|
||||
.aligned()
|
||||
.left()
|
||||
.into_any()
|
||||
Label::new(details.filename.clone(), style.text.clone())
|
||||
.contained()
|
||||
.with_margin_left(style.icon_spacing)
|
||||
.aligned()
|
||||
.left()
|
||||
.into_any()
|
||||
})
|
||||
.constrained()
|
||||
.with_height(style.height)
|
||||
|
||||
@@ -318,10 +318,10 @@ mod tests {
|
||||
},
|
||||
);
|
||||
|
||||
let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
|
||||
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
|
||||
|
||||
// Create the project symbols view.
|
||||
let symbols = cx.add_view(window_id, |cx| {
|
||||
let symbols = cx.add_view(&workspace, |cx| {
|
||||
ProjectSymbols::new(
|
||||
ProjectSymbolsDelegate::new(workspace.downgrade(), project.clone()),
|
||||
cx,
|
||||
|
||||
@@ -329,11 +329,9 @@ message UpdateWorktree {
|
||||
string root_name = 3;
|
||||
repeated Entry updated_entries = 4;
|
||||
repeated uint64 removed_entries = 5;
|
||||
repeated RepositoryEntry updated_repositories = 6;
|
||||
repeated uint64 removed_repositories = 7;
|
||||
uint64 scan_id = 8;
|
||||
bool is_last_update = 9;
|
||||
string abs_path = 10;
|
||||
uint64 scan_id = 6;
|
||||
bool is_last_update = 7;
|
||||
string abs_path = 8;
|
||||
}
|
||||
|
||||
message CreateProjectEntry {
|
||||
@@ -680,8 +678,6 @@ message SearchProject {
|
||||
bool regex = 3;
|
||||
bool whole_word = 4;
|
||||
bool case_sensitive = 5;
|
||||
string files_to_include = 6;
|
||||
string files_to_exclude = 7;
|
||||
}
|
||||
|
||||
message SearchProjectResponse {
|
||||
@@ -983,25 +979,6 @@ message Entry {
|
||||
bool is_ignored = 7;
|
||||
}
|
||||
|
||||
message RepositoryEntry {
|
||||
uint64 work_directory_id = 1;
|
||||
optional string branch = 2;
|
||||
repeated string removed_repo_paths = 3;
|
||||
repeated StatusEntry updated_statuses = 4;
|
||||
}
|
||||
|
||||
message StatusEntry {
|
||||
string repo_path = 1;
|
||||
GitStatus status = 2;
|
||||
}
|
||||
|
||||
enum GitStatus {
|
||||
Added = 0;
|
||||
Modified = 1;
|
||||
Conflict = 2;
|
||||
}
|
||||
|
||||
|
||||
message BufferState {
|
||||
uint64 id = 1;
|
||||
optional File file = 2;
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
use super::{entity_messages, messages, request_messages, ConnectionId, TypedEnvelope};
|
||||
use anyhow::{anyhow, Result};
|
||||
use async_tungstenite::tungstenite::Message as WebSocketMessage;
|
||||
use collections::HashMap;
|
||||
use futures::{SinkExt as _, StreamExt as _};
|
||||
use prost::Message as _;
|
||||
use serde::Serialize;
|
||||
use std::any::{Any, TypeId};
|
||||
use std::fmt;
|
||||
use std::{
|
||||
cmp,
|
||||
fmt::Debug,
|
||||
io, iter,
|
||||
time::{Duration, SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
use std::{fmt, mem};
|
||||
|
||||
include!(concat!(env!("OUT_DIR"), "/zed.messages.rs"));
|
||||
|
||||
@@ -485,21 +484,14 @@ pub fn split_worktree_update(
|
||||
mut message: UpdateWorktree,
|
||||
max_chunk_size: usize,
|
||||
) -> impl Iterator<Item = UpdateWorktree> {
|
||||
let mut done_files = false;
|
||||
|
||||
let mut repository_map = message
|
||||
.updated_repositories
|
||||
.into_iter()
|
||||
.map(|repo| (repo.work_directory_id, repo))
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
let mut done = false;
|
||||
iter::from_fn(move || {
|
||||
if done_files {
|
||||
if done {
|
||||
return None;
|
||||
}
|
||||
|
||||
let updated_entries_chunk_size = cmp::min(message.updated_entries.len(), max_chunk_size);
|
||||
let updated_entries: Vec<_> = message
|
||||
let updated_entries = message
|
||||
.updated_entries
|
||||
.drain(..updated_entries_chunk_size)
|
||||
.collect();
|
||||
@@ -510,28 +502,7 @@ pub fn split_worktree_update(
|
||||
.drain(..removed_entries_chunk_size)
|
||||
.collect();
|
||||
|
||||
done_files = message.updated_entries.is_empty() && message.removed_entries.is_empty();
|
||||
|
||||
let mut updated_repositories = Vec::new();
|
||||
|
||||
if !repository_map.is_empty() {
|
||||
for entry in &updated_entries {
|
||||
if let Some(repo) = repository_map.remove(&entry.id) {
|
||||
updated_repositories.push(repo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let removed_repositories = if done_files {
|
||||
mem::take(&mut message.removed_repositories)
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
|
||||
if done_files {
|
||||
updated_repositories.extend(mem::take(&mut repository_map).into_values());
|
||||
}
|
||||
|
||||
done = message.updated_entries.is_empty() && message.removed_entries.is_empty();
|
||||
Some(UpdateWorktree {
|
||||
project_id: message.project_id,
|
||||
worktree_id: message.worktree_id,
|
||||
@@ -540,9 +511,7 @@ pub fn split_worktree_update(
|
||||
updated_entries,
|
||||
removed_entries,
|
||||
scan_id: message.scan_id,
|
||||
is_last_update: done_files && message.is_last_update,
|
||||
updated_repositories,
|
||||
removed_repositories,
|
||||
is_last_update: done && message.is_last_update,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -6,4 +6,4 @@ pub use conn::Connection;
|
||||
pub use peer::*;
|
||||
mod macros;
|
||||
|
||||
pub const PROTOCOL_VERSION: u32 = 55;
|
||||
pub const PROTOCOL_VERSION: u32 = 53;
|
||||
|
||||
@@ -27,7 +27,6 @@ serde.workspace = true
|
||||
serde_derive.workspace = true
|
||||
smallvec.workspace = true
|
||||
smol.workspace = true
|
||||
globset.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
editor = { path = "../editor", features = ["test-support"] }
|
||||
|
||||
@@ -573,13 +573,7 @@ impl BufferSearchBar {
|
||||
active_searchable_item.clear_matches(cx);
|
||||
} else {
|
||||
let query = if self.regex {
|
||||
match SearchQuery::regex(
|
||||
query,
|
||||
self.whole_word,
|
||||
self.case_sensitive,
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
) {
|
||||
match SearchQuery::regex(query, self.whole_word, self.case_sensitive) {
|
||||
Ok(query) => query,
|
||||
Err(_) => {
|
||||
self.query_contains_error = true;
|
||||
@@ -588,13 +582,7 @@ impl BufferSearchBar {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
SearchQuery::text(
|
||||
query,
|
||||
self.whole_word,
|
||||
self.case_sensitive,
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
)
|
||||
SearchQuery::text(query, self.whole_word, self.case_sensitive)
|
||||
};
|
||||
|
||||
let matches = active_searchable_item.find_matches(query, cx);
|
||||
@@ -682,11 +670,13 @@ mod tests {
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let (window_id, _root_view) = cx.add_window(|_| EmptyView);
|
||||
let (_, root_view) = cx.add_window(|_| EmptyView);
|
||||
|
||||
let editor = cx.add_view(window_id, |cx| Editor::for_buffer(buffer.clone(), None, cx));
|
||||
let editor = cx.add_view(&root_view, |cx| {
|
||||
Editor::for_buffer(buffer.clone(), None, cx)
|
||||
});
|
||||
|
||||
let search_bar = cx.add_view(window_id, |cx| {
|
||||
let search_bar = cx.add_view(&root_view, |cx| {
|
||||
let mut search_bar = BufferSearchBar::new(cx);
|
||||
search_bar.set_active_pane_item(Some(&editor), cx);
|
||||
search_bar.show(false, true, cx);
|
||||
|
||||
@@ -2,14 +2,12 @@ use crate::{
|
||||
SearchOption, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleRegex,
|
||||
ToggleWholeWord,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use collections::HashMap;
|
||||
use editor::{
|
||||
items::active_match_index, scroll::autoscroll::Autoscroll, Anchor, Editor, MultiBuffer,
|
||||
SelectAll, MAX_TAB_TITLE_LEN,
|
||||
};
|
||||
use futures::StreamExt;
|
||||
use globset::{Glob, GlobMatcher};
|
||||
use gpui::{
|
||||
actions,
|
||||
elements::*,
|
||||
@@ -24,7 +22,6 @@ use smallvec::SmallVec;
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
borrow::Cow,
|
||||
collections::HashSet,
|
||||
mem,
|
||||
ops::Range,
|
||||
path::PathBuf,
|
||||
@@ -37,7 +34,7 @@ use workspace::{
|
||||
ItemNavHistory, Pane, ToolbarItemLocation, ToolbarItemView, Workspace, WorkspaceId,
|
||||
};
|
||||
|
||||
actions!(project_search, [SearchInNew, ToggleFocus, NextField]);
|
||||
actions!(project_search, [SearchInNew, ToggleFocus]);
|
||||
|
||||
#[derive(Default)]
|
||||
struct ActiveSearches(HashMap<WeakModelHandle<Project>, WeakViewHandle<ProjectSearchView>>);
|
||||
@@ -51,7 +48,6 @@ pub fn init(cx: &mut AppContext) {
|
||||
cx.add_action(ProjectSearchBar::select_prev_match);
|
||||
cx.add_action(ProjectSearchBar::toggle_focus);
|
||||
cx.capture_action(ProjectSearchBar::tab);
|
||||
cx.capture_action(ProjectSearchBar::tab_previous);
|
||||
add_toggle_option_action::<ToggleCaseSensitive>(SearchOption::CaseSensitive, cx);
|
||||
add_toggle_option_action::<ToggleWholeWord>(SearchOption::WholeWord, cx);
|
||||
add_toggle_option_action::<ToggleRegex>(SearchOption::Regex, cx);
|
||||
@@ -79,13 +75,6 @@ struct ProjectSearch {
|
||||
search_id: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
enum InputPanel {
|
||||
Query,
|
||||
Exclude,
|
||||
Include,
|
||||
}
|
||||
|
||||
pub struct ProjectSearchView {
|
||||
model: ModelHandle<ProjectSearch>,
|
||||
query_editor: ViewHandle<Editor>,
|
||||
@@ -93,12 +82,10 @@ pub struct ProjectSearchView {
|
||||
case_sensitive: bool,
|
||||
whole_word: bool,
|
||||
regex: bool,
|
||||
panels_with_errors: HashSet<InputPanel>,
|
||||
query_contains_error: bool,
|
||||
active_match_index: Option<usize>,
|
||||
search_id: usize,
|
||||
query_editor_was_focused: bool,
|
||||
included_files_editor: ViewHandle<Editor>,
|
||||
excluded_files_editor: ViewHandle<Editor>,
|
||||
}
|
||||
|
||||
pub struct ProjectSearchBar {
|
||||
@@ -213,7 +200,7 @@ impl View for ProjectSearchView {
|
||||
.flex(1., true)
|
||||
})
|
||||
.on_down(MouseButton::Left, |_, _, cx| {
|
||||
cx.focus_parent();
|
||||
cx.focus_parent_view();
|
||||
})
|
||||
.into_any_named("project search view")
|
||||
} else {
|
||||
@@ -438,7 +425,7 @@ impl ProjectSearchView {
|
||||
editor.set_text(query_text, cx);
|
||||
editor
|
||||
});
|
||||
// Subscribe to query_editor in order to reraise editor events for workspace item activation purposes
|
||||
// Subcribe to query_editor in order to reraise editor events for workspace item activation purposes
|
||||
cx.subscribe(&query_editor, |_, _, event, cx| {
|
||||
cx.emit(ViewEvent::EditorEvent(event.clone()))
|
||||
})
|
||||
@@ -461,40 +448,6 @@ impl ProjectSearchView {
|
||||
})
|
||||
.detach();
|
||||
|
||||
let included_files_editor = cx.add_view(|cx| {
|
||||
let mut editor = Editor::single_line(
|
||||
Some(Arc::new(|theme| {
|
||||
theme.search.include_exclude_editor.input.clone()
|
||||
})),
|
||||
cx,
|
||||
);
|
||||
editor.set_placeholder_text("Include: crates/**/*.toml", cx);
|
||||
|
||||
editor
|
||||
});
|
||||
// Subscribe to include_files_editor in order to reraise editor events for workspace item activation purposes
|
||||
cx.subscribe(&included_files_editor, |_, _, event, cx| {
|
||||
cx.emit(ViewEvent::EditorEvent(event.clone()))
|
||||
})
|
||||
.detach();
|
||||
|
||||
let excluded_files_editor = cx.add_view(|cx| {
|
||||
let mut editor = Editor::single_line(
|
||||
Some(Arc::new(|theme| {
|
||||
theme.search.include_exclude_editor.input.clone()
|
||||
})),
|
||||
cx,
|
||||
);
|
||||
editor.set_placeholder_text("Exclude: vendor/*, *.lock", cx);
|
||||
|
||||
editor
|
||||
});
|
||||
// Subscribe to excluded_files_editor in order to reraise editor events for workspace item activation purposes
|
||||
cx.subscribe(&excluded_files_editor, |_, _, event, cx| {
|
||||
cx.emit(ViewEvent::EditorEvent(event.clone()))
|
||||
})
|
||||
.detach();
|
||||
|
||||
let mut this = ProjectSearchView {
|
||||
search_id: model.read(cx).search_id,
|
||||
model,
|
||||
@@ -503,11 +456,9 @@ impl ProjectSearchView {
|
||||
case_sensitive,
|
||||
whole_word,
|
||||
regex,
|
||||
panels_with_errors: HashSet::new(),
|
||||
query_contains_error: false,
|
||||
active_match_index: None,
|
||||
query_editor_was_focused: false,
|
||||
included_files_editor,
|
||||
excluded_files_editor,
|
||||
};
|
||||
this.model_changed(cx);
|
||||
this
|
||||
@@ -574,44 +525,11 @@ impl ProjectSearchView {
|
||||
|
||||
fn build_search_query(&mut self, cx: &mut ViewContext<Self>) -> Option<SearchQuery> {
|
||||
let text = self.query_editor.read(cx).text(cx);
|
||||
let included_files =
|
||||
match Self::load_glob_set(&self.included_files_editor.read(cx).text(cx)) {
|
||||
Ok(included_files) => {
|
||||
self.panels_with_errors.remove(&InputPanel::Include);
|
||||
included_files
|
||||
}
|
||||
Err(_e) => {
|
||||
self.panels_with_errors.insert(InputPanel::Include);
|
||||
cx.notify();
|
||||
return None;
|
||||
}
|
||||
};
|
||||
let excluded_files =
|
||||
match Self::load_glob_set(&self.excluded_files_editor.read(cx).text(cx)) {
|
||||
Ok(excluded_files) => {
|
||||
self.panels_with_errors.remove(&InputPanel::Exclude);
|
||||
excluded_files
|
||||
}
|
||||
Err(_e) => {
|
||||
self.panels_with_errors.insert(InputPanel::Exclude);
|
||||
cx.notify();
|
||||
return None;
|
||||
}
|
||||
};
|
||||
if self.regex {
|
||||
match SearchQuery::regex(
|
||||
text,
|
||||
self.whole_word,
|
||||
self.case_sensitive,
|
||||
included_files,
|
||||
excluded_files,
|
||||
) {
|
||||
Ok(query) => {
|
||||
self.panels_with_errors.remove(&InputPanel::Query);
|
||||
Some(query)
|
||||
}
|
||||
Err(_e) => {
|
||||
self.panels_with_errors.insert(InputPanel::Query);
|
||||
match SearchQuery::regex(text, self.whole_word, self.case_sensitive) {
|
||||
Ok(query) => Some(query),
|
||||
Err(_) => {
|
||||
self.query_contains_error = true;
|
||||
cx.notify();
|
||||
None
|
||||
}
|
||||
@@ -621,20 +539,10 @@ impl ProjectSearchView {
|
||||
text,
|
||||
self.whole_word,
|
||||
self.case_sensitive,
|
||||
included_files,
|
||||
excluded_files,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn load_glob_set(text: &str) -> Result<Vec<GlobMatcher>> {
|
||||
text.split(',')
|
||||
.map(str::trim)
|
||||
.filter(|glob_str| !glob_str.is_empty())
|
||||
.map(|glob_str| anyhow::Ok(Glob::new(glob_str)?.compile_matcher()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn select_match(&mut self, direction: Direction, cx: &mut ViewContext<Self>) {
|
||||
if let Some(index) = self.active_match_index {
|
||||
let match_ranges = self.model.read(cx).match_ranges.clone();
|
||||
@@ -815,50 +723,19 @@ impl ProjectSearchBar {
|
||||
}
|
||||
|
||||
fn tab(&mut self, _: &editor::Tab, cx: &mut ViewContext<Self>) {
|
||||
self.cycle_field(Direction::Next, cx);
|
||||
}
|
||||
|
||||
fn tab_previous(&mut self, _: &editor::TabPrev, cx: &mut ViewContext<Self>) {
|
||||
self.cycle_field(Direction::Prev, cx);
|
||||
}
|
||||
|
||||
fn cycle_field(&mut self, direction: Direction, cx: &mut ViewContext<Self>) {
|
||||
let active_project_search = match &self.active_project_search {
|
||||
Some(active_project_search) => active_project_search,
|
||||
|
||||
None => {
|
||||
cx.propagate_action();
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
active_project_search.update(cx, |project_view, cx| {
|
||||
let views = &[
|
||||
&project_view.query_editor,
|
||||
&project_view.included_files_editor,
|
||||
&project_view.excluded_files_editor,
|
||||
];
|
||||
|
||||
let current_index = match views
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find(|(_, view)| view.is_focused(cx))
|
||||
{
|
||||
Some((index, _)) => index,
|
||||
|
||||
None => {
|
||||
if let Some(search_view) = self.active_project_search.as_ref() {
|
||||
search_view.update(cx, |search_view, cx| {
|
||||
if search_view.query_editor.is_focused(cx) {
|
||||
if !search_view.model.read(cx).match_ranges.is_empty() {
|
||||
search_view.focus_results_editor(cx);
|
||||
}
|
||||
} else {
|
||||
cx.propagate_action();
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let new_index = match direction {
|
||||
Direction::Next => (current_index + 1) % views.len(),
|
||||
Direction::Prev if current_index == 0 => views.len() - 1,
|
||||
Direction::Prev => (current_index - 1) % views.len(),
|
||||
};
|
||||
cx.focus(views[new_index]);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
cx.propagate_action();
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle_search_option(&mut self, option: SearchOption, cx: &mut ViewContext<Self>) -> bool {
|
||||
@@ -987,121 +864,59 @@ impl View for ProjectSearchBar {
|
||||
if let Some(search) = self.active_project_search.as_ref() {
|
||||
let search = search.read(cx);
|
||||
let theme = cx.global::<Settings>().theme.clone();
|
||||
let query_container_style = if search.panels_with_errors.contains(&InputPanel::Query) {
|
||||
let editor_container = if search.query_contains_error {
|
||||
theme.search.invalid_editor
|
||||
} else {
|
||||
theme.search.editor.input.container
|
||||
};
|
||||
let include_container_style =
|
||||
if search.panels_with_errors.contains(&InputPanel::Include) {
|
||||
theme.search.invalid_include_exclude_editor
|
||||
} else {
|
||||
theme.search.include_exclude_editor.input.container
|
||||
};
|
||||
let exclude_container_style =
|
||||
if search.panels_with_errors.contains(&InputPanel::Exclude) {
|
||||
theme.search.invalid_include_exclude_editor
|
||||
} else {
|
||||
theme.search.include_exclude_editor.input.container
|
||||
};
|
||||
|
||||
let included_files_view = ChildView::new(&search.included_files_editor, cx)
|
||||
.aligned()
|
||||
.left()
|
||||
.flex(1.0, true);
|
||||
let excluded_files_view = ChildView::new(&search.excluded_files_editor, cx)
|
||||
.aligned()
|
||||
.right()
|
||||
.flex(1.0, true);
|
||||
|
||||
let row_spacing = theme.workspace.toolbar.container.padding.bottom;
|
||||
|
||||
Flex::column()
|
||||
Flex::row()
|
||||
.with_child(
|
||||
Flex::row()
|
||||
.with_child(
|
||||
Flex::row()
|
||||
.with_child(
|
||||
ChildView::new(&search.query_editor, cx)
|
||||
.aligned()
|
||||
.left()
|
||||
.flex(1., true),
|
||||
)
|
||||
.with_children(search.active_match_index.map(|match_ix| {
|
||||
Label::new(
|
||||
format!(
|
||||
"{}/{}",
|
||||
match_ix + 1,
|
||||
search.model.read(cx).match_ranges.len()
|
||||
),
|
||||
theme.search.match_index.text.clone(),
|
||||
)
|
||||
.contained()
|
||||
.with_style(theme.search.match_index.container)
|
||||
.aligned()
|
||||
}))
|
||||
.contained()
|
||||
.with_style(query_container_style)
|
||||
ChildView::new(&search.query_editor, cx)
|
||||
.aligned()
|
||||
.constrained()
|
||||
.with_min_width(theme.search.editor.min_width)
|
||||
.with_max_width(theme.search.editor.max_width)
|
||||
.flex(1., false),
|
||||
)
|
||||
.with_child(
|
||||
Flex::row()
|
||||
.with_child(self.render_nav_button("<", Direction::Prev, cx))
|
||||
.with_child(self.render_nav_button(">", Direction::Next, cx))
|
||||
.aligned(),
|
||||
)
|
||||
.with_child(
|
||||
Flex::row()
|
||||
.with_child(self.render_option_button(
|
||||
"Case",
|
||||
SearchOption::CaseSensitive,
|
||||
cx,
|
||||
))
|
||||
.with_child(self.render_option_button(
|
||||
"Word",
|
||||
SearchOption::WholeWord,
|
||||
cx,
|
||||
))
|
||||
.with_child(self.render_option_button(
|
||||
"Regex",
|
||||
SearchOption::Regex,
|
||||
cx,
|
||||
))
|
||||
.contained()
|
||||
.with_style(theme.search.option_button_group)
|
||||
.aligned(),
|
||||
.left()
|
||||
.flex(1., true),
|
||||
)
|
||||
.with_children(search.active_match_index.map(|match_ix| {
|
||||
Label::new(
|
||||
format!(
|
||||
"{}/{}",
|
||||
match_ix + 1,
|
||||
search.model.read(cx).match_ranges.len()
|
||||
),
|
||||
theme.search.match_index.text.clone(),
|
||||
)
|
||||
.contained()
|
||||
.with_style(theme.search.match_index.container)
|
||||
.aligned()
|
||||
}))
|
||||
.contained()
|
||||
.with_margin_bottom(row_spacing),
|
||||
.with_style(editor_container)
|
||||
.aligned()
|
||||
.constrained()
|
||||
.with_min_width(theme.search.editor.min_width)
|
||||
.with_max_width(theme.search.editor.max_width)
|
||||
.flex(1., false),
|
||||
)
|
||||
.with_child(
|
||||
Flex::row()
|
||||
.with_child(
|
||||
Flex::row()
|
||||
.with_child(included_files_view)
|
||||
.contained()
|
||||
.with_style(include_container_style)
|
||||
.aligned()
|
||||
.constrained()
|
||||
.with_min_width(theme.search.include_exclude_editor.min_width)
|
||||
.with_max_width(theme.search.include_exclude_editor.max_width)
|
||||
.flex(1., false),
|
||||
)
|
||||
.with_child(
|
||||
Flex::row()
|
||||
.with_child(excluded_files_view)
|
||||
.contained()
|
||||
.with_style(exclude_container_style)
|
||||
.aligned()
|
||||
.constrained()
|
||||
.with_min_width(theme.search.include_exclude_editor.min_width)
|
||||
.with_max_width(theme.search.include_exclude_editor.max_width)
|
||||
.flex(1., false),
|
||||
),
|
||||
.with_child(self.render_nav_button("<", Direction::Prev, cx))
|
||||
.with_child(self.render_nav_button(">", Direction::Next, cx))
|
||||
.aligned(),
|
||||
)
|
||||
.with_child(
|
||||
Flex::row()
|
||||
.with_child(self.render_option_button(
|
||||
"Case",
|
||||
SearchOption::CaseSensitive,
|
||||
cx,
|
||||
))
|
||||
.with_child(self.render_option_button("Word", SearchOption::WholeWord, cx))
|
||||
.with_child(self.render_option_button("Regex", SearchOption::Regex, cx))
|
||||
.contained()
|
||||
.with_style(theme.search.option_button_group)
|
||||
.aligned(),
|
||||
)
|
||||
.contained()
|
||||
.with_style(theme.search.container)
|
||||
@@ -1124,6 +939,8 @@ impl ToolbarItemView for ProjectSearchBar {
|
||||
self.subscription = None;
|
||||
self.active_project_search = None;
|
||||
if let Some(search) = active_pane_item.and_then(|i| i.downcast::<ProjectSearchView>()) {
|
||||
let query_editor = search.read(cx).query_editor.clone();
|
||||
cx.reparent(&query_editor);
|
||||
self.subscription = Some(cx.observe(&search, |_, _, cx| cx.notify()));
|
||||
self.active_project_search = Some(search);
|
||||
ToolbarItemLocation::PrimaryLeft {
|
||||
@@ -1133,10 +950,6 @@ impl ToolbarItemView for ProjectSearchBar {
|
||||
ToolbarItemLocation::Hidden
|
||||
}
|
||||
}
|
||||
|
||||
fn row_count(&self) -> usize {
|
||||
2
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user