Compare commits
41 Commits
testing-in
...
tab-bar-se
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e19f6416d4 | ||
|
|
393b16d226 | ||
|
|
7bd18fa653 | ||
|
|
11dc3c2582 | ||
|
|
268cb948a7 | ||
|
|
6a915e349c | ||
|
|
70d03e4841 | ||
|
|
b1eb0291dc | ||
|
|
e0644de90e | ||
|
|
9329ef1d78 | ||
|
|
664f779eb4 | ||
|
|
314b723292 | ||
|
|
1af1a9e8b3 | ||
|
|
8006f69513 | ||
|
|
bacc92333a | ||
|
|
eb7bd0b98a | ||
|
|
7f229dc202 | ||
|
|
03d0b68f0c | ||
|
|
5c2f27a501 | ||
|
|
d9d509a2bb | ||
|
|
a4ad3bcc08 | ||
|
|
6d7332e80c | ||
|
|
1b614ef63b | ||
|
|
604857ed2e | ||
|
|
d9eb3c4b35 | ||
|
|
f8beda0704 | ||
|
|
40fe5275cf | ||
|
|
6fb6cd3c5c | ||
|
|
0875257852 | ||
|
|
eb97c311c8 | ||
|
|
6422fdea9b | ||
|
|
9d684d7856 | ||
|
|
93978f6017 | ||
|
|
120ead9429 | ||
|
|
99a0356a56 | ||
|
|
94858caff5 | ||
|
|
ca9645c2bf | ||
|
|
b81e8971df | ||
|
|
ed2651d62a | ||
|
|
b8a6fc316f | ||
|
|
2fa9220f44 |
61
Cargo.lock
generated
61
Cargo.lock
generated
@@ -382,6 +382,7 @@ dependencies = [
|
||||
"editor",
|
||||
"env_logger",
|
||||
"feature_flags",
|
||||
"fs",
|
||||
"futures 0.3.28",
|
||||
"gpui",
|
||||
"language",
|
||||
@@ -3181,13 +3182,17 @@ dependencies = [
|
||||
"anyhow",
|
||||
"client",
|
||||
"collections",
|
||||
"ctor",
|
||||
"editor",
|
||||
"env_logger",
|
||||
"futures 0.3.28",
|
||||
"gpui",
|
||||
"language",
|
||||
"log",
|
||||
"lsp",
|
||||
"pretty_assertions",
|
||||
"project",
|
||||
"rand 0.8.5",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -3433,6 +3438,20 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "embed-resource"
|
||||
version = "2.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c6985554d0688b687c5cb73898a34fbe3ad6c24c58c238a4d91d5e840670ee9d"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"memchr",
|
||||
"rustc_version",
|
||||
"toml 0.8.10",
|
||||
"vswhom",
|
||||
"winreg 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "emojis"
|
||||
version = "0.6.1"
|
||||
@@ -3810,6 +3829,7 @@ dependencies = [
|
||||
"ctor",
|
||||
"editor",
|
||||
"env_logger",
|
||||
"futures 0.3.28",
|
||||
"fuzzy",
|
||||
"gpui",
|
||||
"itertools 0.11.0",
|
||||
@@ -4530,6 +4550,7 @@ dependencies = [
|
||||
"cosmic-text",
|
||||
"ctor",
|
||||
"derive_more",
|
||||
"embed-resource",
|
||||
"env_logger",
|
||||
"etagere",
|
||||
"filedescriptor",
|
||||
@@ -5896,6 +5917,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion 1.0.5",
|
||||
"collections",
|
||||
"editor",
|
||||
"gpui",
|
||||
"language",
|
||||
@@ -5952,9 +5974,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.6.3"
|
||||
version = "2.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c"
|
||||
checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d"
|
||||
|
||||
[[package]]
|
||||
name = "memfd"
|
||||
@@ -7963,7 +7985,7 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
"winreg",
|
||||
"winreg 0.50.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -9492,7 +9514,6 @@ dependencies = [
|
||||
"strum",
|
||||
"theme",
|
||||
"ui",
|
||||
"winresource",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -10620,7 +10641,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "tree-sitter-jsdoc"
|
||||
version = "0.20.0"
|
||||
source = "git+https://github.com/tree-sitter/tree-sitter-jsdoc#6a6cf9e7341af32d8e2b2e24a37fbfebefc3dc55"
|
||||
source = "git+https://github.com/tree-sitter/tree-sitter-jsdoc?rev=6a6cf9e7341af32d8e2b2e24a37fbfebefc3dc55#6a6cf9e7341af32d8e2b2e24a37fbfebefc3dc55"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"tree-sitter",
|
||||
@@ -11126,6 +11147,26 @@ version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64"
|
||||
|
||||
[[package]]
|
||||
name = "vswhom"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"vswhom-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vswhom-sys"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3b17ae1f6c8a2b28506cd96d412eebf83b4a0ff2cbefeeb952f2f9dfa44ba18"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vte"
|
||||
version = "0.13.0"
|
||||
@@ -12199,6 +12240,16 @@ dependencies = [
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winreg"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winresource"
|
||||
version = "0.1.17"
|
||||
|
||||
@@ -283,6 +283,7 @@ itertools = "0.11.0"
|
||||
lazy_static = "1.4.0"
|
||||
linkify = "0.10.0"
|
||||
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
|
||||
nanoid = "0.4"
|
||||
ordered-float = "2.1.1"
|
||||
palette = { version = "0.7.5", default-features = false, features = ["std"] }
|
||||
parking_lot = "0.12.1"
|
||||
@@ -341,7 +342,7 @@ tree-sitter-gowork = { git = "https://github.com/d1y/tree-sitter-go-work" }
|
||||
rustc-demangle = "0.1.23"
|
||||
tree-sitter-heex = { git = "https://github.com/phoenixframework/tree-sitter-heex", rev = "2e1348c3cf2c9323e87c2744796cf3f3868aa82a" }
|
||||
tree-sitter-html = "0.19.0"
|
||||
tree-sitter-jsdoc = { git = "https://github.com/tree-sitter/tree-sitter-jsdoc", ref = "6a6cf9e7341af32d8e2b2e24a37fbfebefc3dc55" }
|
||||
tree-sitter-jsdoc = { git = "https://github.com/tree-sitter/tree-sitter-jsdoc", rev = "6a6cf9e7341af32d8e2b2e24a37fbfebefc3dc55" }
|
||||
tree-sitter-json = { git = "https://github.com/tree-sitter/tree-sitter-json", rev = "40a81c01a40ac48744e0c8ccabbaba1920441199" }
|
||||
tree-sitter-markdown = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "330ecab87a3e3a7211ac69bbadc19eabecdb1cca" }
|
||||
tree-sitter-proto = { git = "https://github.com/rewinfrey/tree-sitter-proto", rev = "36d54f288aee112f13a67b550ad32634d0c2cb52" }
|
||||
|
||||
102
README.md
102
README.md
@@ -1,51 +1,51 @@
|
||||
# Zed
|
||||
|
||||
[](https://github.com/zed-industries/zed/actions/workflows/ci.yml)
|
||||
|
||||
Welcome to Zed, a high-performance, multiplayer code editor from the creators of [Atom](https://github.com/atom/atom) and [Tree-sitter](https://github.com/tree-sitter/tree-sitter).
|
||||
|
||||
## Installation
|
||||
|
||||
You can [download](https://zed.dev/download) Zed today for macOS (v10.15+).
|
||||
|
||||
Support for additional platforms is on our [roadmap](https://zed.dev/roadmap):
|
||||
|
||||
- Linux ([tracking issue](https://github.com/zed-industries/zed/issues/7015))
|
||||
- Windows ([tracking issue](https://github.com/zed-industries/zed/issues/5394))
|
||||
- Web ([tracking issue](https://github.com/zed-industries/zed/issues/5396))
|
||||
|
||||
For macOS users, you can also install Zed using [Homebrew](https://brew.sh/):
|
||||
|
||||
```sh
|
||||
brew install --cask zed
|
||||
```
|
||||
|
||||
Alternatively, to install the Preview release:
|
||||
|
||||
```sh
|
||||
brew tap homebrew/cask-versions
|
||||
brew install zed-preview
|
||||
```
|
||||
|
||||
## Developing Zed
|
||||
|
||||
- [Building Zed for macOS](./docs/src/developing_zed__building_zed_macos.md)
|
||||
- [Building Zed for Linux](./docs/src/developing_zed__building_zed_linux.md)
|
||||
- [Building Zed for Windows](./docs/src/developing_zed__building_zed_windows.md)
|
||||
- [Running Collaboration Locally](./docs/src/developing_zed__local_collaboration.md)
|
||||
|
||||
## Contributing
|
||||
|
||||
See [CONTRIBUTING.md](./CONTRIBUTING.md) for ways you can contribute to Zed.
|
||||
|
||||
Also... we're hiring! Check out our [jobs](https://zed.dev/jobs) page for open roles.
|
||||
|
||||
## Licensing
|
||||
|
||||
License information for third party dependencies must be correctly provided for CI to pass.
|
||||
|
||||
We use [`cargo-about`](https://github.com/EmbarkStudios/cargo-about) to automatically comply with open source licenses. If CI is failing, check the following:
|
||||
|
||||
- Is it showing a `no license specified` error for a crate you've created? If so, add `publish = false` under `[package]` in your crate's Cargo.toml.
|
||||
- Is the error `failed to satisfy license requirements` for a dependency? If so, first determine what license the project has and whether this system is sufficient to comply with this license's requirements. If you're unsure, ask a lawyer. Once you've verified that this system is acceptable add the license's SPDX identifier to the `accepted` array in `script/licenses/zed-licenses.toml`.
|
||||
- Is `cargo-about` unable to find the license for a dependency? If so, add a clarification field at the end of `script/licenses/zed-licenses.toml`, as specified in the [cargo-about book](https://embarkstudios.github.io/cargo-about/cli/generate/config.html#crate-configuration).
|
||||
# Zed
|
||||
|
||||
[](https://github.com/zed-industries/zed/actions/workflows/ci.yml)
|
||||
|
||||
Welcome to Zed, a high-performance, multiplayer code editor from the creators of [Atom](https://github.com/atom/atom) and [Tree-sitter](https://github.com/tree-sitter/tree-sitter).
|
||||
|
||||
## Installation
|
||||
|
||||
You can [download](https://zed.dev/download) Zed today for macOS (v10.15+).
|
||||
|
||||
Support for additional platforms is on our [roadmap](https://zed.dev/roadmap):
|
||||
|
||||
- Linux ([tracking issue](https://github.com/zed-industries/zed/issues/7015))
|
||||
- Windows ([tracking issue](https://github.com/zed-industries/zed/issues/5394))
|
||||
- Web ([tracking issue](https://github.com/zed-industries/zed/issues/5396))
|
||||
|
||||
For macOS users, you can also install Zed using [Homebrew](https://brew.sh/):
|
||||
|
||||
```sh
|
||||
brew install --cask zed
|
||||
```
|
||||
|
||||
Alternatively, to install the Preview release:
|
||||
|
||||
```sh
|
||||
brew tap homebrew/cask-versions
|
||||
brew install zed-preview
|
||||
```
|
||||
|
||||
## Developing Zed
|
||||
|
||||
- [Building Zed for macOS](./docs/src/developing_zed__building_zed_macos.md)
|
||||
- [Building Zed for Linux](./docs/src/developing_zed__building_zed_linux.md)
|
||||
- [Building Zed for Windows](./docs/src/developing_zed__building_zed_windows.md)
|
||||
- [Running Collaboration Locally](./docs/src/developing_zed__local_collaboration.md)
|
||||
|
||||
## Contributing
|
||||
|
||||
See [CONTRIBUTING.md](./CONTRIBUTING.md) for ways you can contribute to Zed.
|
||||
|
||||
Also... we're hiring! Check out our [jobs](https://zed.dev/jobs) page for open roles.
|
||||
|
||||
## Licensing
|
||||
|
||||
License information for third party dependencies must be correctly provided for CI to pass.
|
||||
|
||||
We use [`cargo-about`](https://github.com/EmbarkStudios/cargo-about) to automatically comply with open source licenses. If CI is failing, check the following:
|
||||
|
||||
- Is it showing a `no license specified` error for a crate you've created? If so, add `publish = false` under `[package]` in your crate's Cargo.toml.
|
||||
- Is the error `failed to satisfy license requirements` for a dependency? If so, first determine what license the project has and whether this system is sufficient to comply with this license's requirements. If you're unsure, ask a lawyer. Once you've verified that this system is acceptable add the license's SPDX identifier to the `accepted` array in `script/licenses/zed-licenses.toml`.
|
||||
- Is `cargo-about` unable to find the license for a dependency? If so, add a clarification field at the end of `script/licenses/zed-licenses.toml`, as specified in the [cargo-about book](https://embarkstudios.github.io/cargo-about/cli/generate/config.html#crate-configuration).
|
||||
|
||||
@@ -312,6 +312,16 @@
|
||||
"autosave": "off",
|
||||
// Settings related to the editor's tab bar.
|
||||
"tab_bar": {
|
||||
// Where to show the tab bar in the editor.
|
||||
// This setting can take three values:
|
||||
//
|
||||
// 1. Show tab bar at the top of the editor (default):
|
||||
// "top"
|
||||
// 2. Show tab bar at the bottom of the editor:
|
||||
// "bottom"
|
||||
// 3. Don't show the tab bar:
|
||||
// "no"
|
||||
"placement": "top",
|
||||
// Whether or not to show the navigation history buttons.
|
||||
"show_nav_history_buttons": true
|
||||
},
|
||||
|
||||
@@ -2873,7 +2873,7 @@ impl InlineAssistant {
|
||||
cx.theme().colors().text
|
||||
},
|
||||
font_family: settings.ui_font.family.clone(),
|
||||
font_features: settings.ui_font.features,
|
||||
font_features: settings.ui_font.features.clone(),
|
||||
font_size: rems(0.875).into(),
|
||||
font_weight: FontWeight::NORMAL,
|
||||
font_style: FontStyle::Normal,
|
||||
|
||||
@@ -241,7 +241,7 @@ impl AuthenticationPrompt {
|
||||
let text_style = TextStyle {
|
||||
color: cx.theme().colors().text,
|
||||
font_family: settings.ui_font.family.clone(),
|
||||
font_features: settings.ui_font.features,
|
||||
font_features: settings.ui_font.features.clone(),
|
||||
font_size: rems(0.875).into(),
|
||||
font_weight: FontWeight::NORMAL,
|
||||
font_style: FontStyle::Normal,
|
||||
|
||||
@@ -19,15 +19,17 @@ assistant_tooling.workspace = true
|
||||
client.workspace = true
|
||||
editor.workspace = true
|
||||
feature_flags.workspace = true
|
||||
fs.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
language.workspace = true
|
||||
log.workspace = true
|
||||
nanoid.workspace = true
|
||||
open_ai.workspace = true
|
||||
project.workspace = true
|
||||
rich_text.workspace = true
|
||||
semantic_index.workspace = true
|
||||
schemars.workspace = true
|
||||
semantic_index.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
@@ -35,7 +37,6 @@ theme.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
workspace.workspace = true
|
||||
nanoid = "0.4"
|
||||
|
||||
[dev-dependencies]
|
||||
assets.workspace = true
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
/// This example creates a basic Chat UI with a function for rolling a die.
|
||||
//! This example creates a basic Chat UI with a function for rolling a die.
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use assets::Assets;
|
||||
use assistant2::AssistantPanel;
|
||||
221
crates/assistant2/examples/file_interactions.rs
Normal file
221
crates/assistant2/examples/file_interactions.rs
Normal file
@@ -0,0 +1,221 @@
|
||||
//! This example creates a basic Chat UI for interacting with the filesystem.
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use assets::Assets;
|
||||
use assistant2::AssistantPanel;
|
||||
use assistant_tooling::{LanguageModelTool, ToolRegistry};
|
||||
use client::Client;
|
||||
use fs::Fs;
|
||||
use futures::StreamExt;
|
||||
use gpui::{actions, App, AppContext, KeyBinding, Task, View, WindowOptions};
|
||||
use language::LanguageRegistry;
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{KeymapFile, DEFAULT_KEYMAP_PATH};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use theme::LoadThemes;
|
||||
use ui::{div, prelude::*, Render};
|
||||
use util::ResultExt as _;
|
||||
|
||||
actions!(example, [Quit]);
|
||||
|
||||
struct FileBrowserTool {
|
||||
fs: Arc<dyn Fs>,
|
||||
root_dir: PathBuf,
|
||||
}
|
||||
|
||||
impl FileBrowserTool {
|
||||
fn new(fs: Arc<dyn Fs>, root_dir: PathBuf) -> Self {
|
||||
Self { fs, root_dir }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
struct FileBrowserParams {
|
||||
command: FileBrowserCommand,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
enum FileBrowserCommand {
|
||||
Ls { path: PathBuf },
|
||||
Cat { path: PathBuf },
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
enum FileBrowserOutput {
|
||||
Ls { entries: Vec<String> },
|
||||
Cat { content: String },
|
||||
}
|
||||
|
||||
pub struct FileBrowserView {
|
||||
result: Result<FileBrowserOutput>,
|
||||
}
|
||||
|
||||
impl Render for FileBrowserView {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let Ok(output) = self.result.as_ref() else {
|
||||
return h_flex().child("Failed to perform operation");
|
||||
};
|
||||
|
||||
match output {
|
||||
FileBrowserOutput::Ls { entries } => v_flex().children(
|
||||
entries
|
||||
.into_iter()
|
||||
.map(|entry| h_flex().text_ui(cx).child(entry.clone())),
|
||||
),
|
||||
FileBrowserOutput::Cat { content } => h_flex().child(content.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LanguageModelTool for FileBrowserTool {
|
||||
type Input = FileBrowserParams;
|
||||
type Output = FileBrowserOutput;
|
||||
type View = FileBrowserView;
|
||||
|
||||
fn name(&self) -> String {
|
||||
"file_browser".to_string()
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"A tool for browsing the filesystem.".to_string()
|
||||
}
|
||||
|
||||
fn execute(&self, input: &Self::Input, cx: &AppContext) -> Task<gpui::Result<Self::Output>> {
|
||||
cx.spawn({
|
||||
let fs = self.fs.clone();
|
||||
let root_dir = self.root_dir.clone();
|
||||
let input = input.clone();
|
||||
|_cx| async move {
|
||||
match input.command {
|
||||
FileBrowserCommand::Ls { path } => {
|
||||
let path = root_dir.join(path);
|
||||
|
||||
let mut output = fs.read_dir(&path).await?;
|
||||
|
||||
let mut entries = Vec::new();
|
||||
while let Some(entry) = output.next().await {
|
||||
let entry = entry?;
|
||||
entries.push(entry.display().to_string());
|
||||
}
|
||||
|
||||
Ok(FileBrowserOutput::Ls { entries })
|
||||
}
|
||||
FileBrowserCommand::Cat { path } => {
|
||||
let path = root_dir.join(path);
|
||||
|
||||
let output = fs.load(&path).await?;
|
||||
|
||||
Ok(FileBrowserOutput::Cat { content: output })
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn new_view(
|
||||
_tool_call_id: String,
|
||||
_input: Self::Input,
|
||||
result: Result<Self::Output>,
|
||||
cx: &mut WindowContext,
|
||||
) -> gpui::View<Self::View> {
|
||||
cx.new_view(|_cx| FileBrowserView { result })
|
||||
}
|
||||
|
||||
fn format(_input: &Self::Input, output: &Result<Self::Output>) -> String {
|
||||
let Ok(output) = output else {
|
||||
return "Failed to perform command: {input:?}".to_string();
|
||||
};
|
||||
|
||||
match output {
|
||||
FileBrowserOutput::Ls { entries } => entries.join("\n"),
|
||||
FileBrowserOutput::Cat { content } => content.to_owned(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
env_logger::init();
|
||||
App::new().with_assets(Assets).run(|cx| {
|
||||
cx.bind_keys(Some(KeyBinding::new("cmd-q", Quit, None)));
|
||||
cx.on_action(|_: &Quit, cx: &mut AppContext| {
|
||||
cx.quit();
|
||||
});
|
||||
|
||||
settings::init(cx);
|
||||
language::init(cx);
|
||||
Project::init_settings(cx);
|
||||
editor::init(cx);
|
||||
theme::init(LoadThemes::JustBase, cx);
|
||||
Assets.load_fonts(cx).unwrap();
|
||||
KeymapFile::load_asset(DEFAULT_KEYMAP_PATH, cx).unwrap();
|
||||
client::init_settings(cx);
|
||||
release_channel::init("0.130.0", cx);
|
||||
|
||||
let client = Client::production(cx);
|
||||
{
|
||||
let client = client.clone();
|
||||
cx.spawn(|cx| async move { client.authenticate_and_connect(false, &cx).await })
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
assistant2::init(client.clone(), cx);
|
||||
|
||||
let language_registry = Arc::new(LanguageRegistry::new(
|
||||
Task::ready(()),
|
||||
cx.background_executor().clone(),
|
||||
));
|
||||
let node_runtime = node_runtime::RealNodeRuntime::new(client.http_client());
|
||||
languages::init(language_registry.clone(), node_runtime, cx);
|
||||
|
||||
cx.spawn(|cx| async move {
|
||||
cx.update(|cx| {
|
||||
let fs = Arc::new(fs::RealFs::new(None));
|
||||
let cwd = std::env::current_dir().expect("Failed to get current working directory");
|
||||
|
||||
let mut tool_registry = ToolRegistry::new();
|
||||
tool_registry
|
||||
.register(FileBrowserTool::new(fs, cwd))
|
||||
.context("failed to register FileBrowserTool")
|
||||
.log_err();
|
||||
|
||||
let tool_registry = Arc::new(tool_registry);
|
||||
|
||||
println!("Tools registered");
|
||||
for definition in tool_registry.definitions() {
|
||||
println!("{}", definition);
|
||||
}
|
||||
|
||||
cx.open_window(WindowOptions::default(), |cx| {
|
||||
cx.new_view(|cx| Example::new(language_registry, tool_registry, cx))
|
||||
});
|
||||
cx.activate(true);
|
||||
})
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
})
|
||||
}
|
||||
|
||||
struct Example {
|
||||
assistant_panel: View<AssistantPanel>,
|
||||
}
|
||||
|
||||
impl Example {
|
||||
fn new(
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
tool_registry: Arc<ToolRegistry>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
Self {
|
||||
assistant_panel: cx
|
||||
.new_view(|cx| AssistantPanel::new(language_registry, tool_registry, cx)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for Example {
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl ui::prelude::IntoElement {
|
||||
div().size_full().child(self.assistant_panel.clone())
|
||||
}
|
||||
}
|
||||
@@ -37,7 +37,7 @@ google_ai.workspace = true
|
||||
hex.workspace = true
|
||||
live_kit_server.workspace = true
|
||||
log.workspace = true
|
||||
nanoid = "0.4"
|
||||
nanoid.workspace = true
|
||||
open_ai.workspace = true
|
||||
parking_lot.workspace = true
|
||||
prometheus = "0.13"
|
||||
|
||||
@@ -136,6 +136,13 @@ pub async fn post_crash(
|
||||
.get("x-zed-panicked-on")
|
||||
.and_then(|h| h.to_str().ok())
|
||||
.and_then(|s| s.parse().ok());
|
||||
|
||||
let installation_id = headers
|
||||
.get("x-zed-installation-id")
|
||||
.and_then(|h| h.to_str().ok())
|
||||
.map(|s| s.to_string())
|
||||
.unwrap_or_default();
|
||||
|
||||
let mut recent_panic = None;
|
||||
|
||||
if let Some(recent_panic_on) = recent_panic_on {
|
||||
@@ -160,6 +167,7 @@ pub async fn post_crash(
|
||||
os_version = %report.header.os_version,
|
||||
bundle_id = %report.header.bundle_id,
|
||||
incident_id = %report.header.incident_id,
|
||||
installation_id = %installation_id,
|
||||
description = %description,
|
||||
backtrace = %summary,
|
||||
"crash report");
|
||||
|
||||
@@ -70,6 +70,7 @@ async fn test_dev_server(cx: &mut gpui::TestAppContext, cx2: &mut gpui::TestAppC
|
||||
workspace::join_remote_project(
|
||||
projects[0].project_id.unwrap(),
|
||||
client.app_state.clone(),
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
@@ -205,7 +206,12 @@ async fn create_remote_project(
|
||||
let projects = store.remote_projects();
|
||||
assert_eq!(projects.len(), 1);
|
||||
assert_eq!(projects[0].path, "/remote");
|
||||
workspace::join_remote_project(projects[0].project_id.unwrap(), client_app_state, cx)
|
||||
workspace::join_remote_project(
|
||||
projects[0].project_id.unwrap(),
|
||||
client_app_state,
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -301,6 +307,7 @@ async fn test_dev_server_reconnect(
|
||||
workspace::join_remote_project(
|
||||
projects[0].project_id.unwrap(),
|
||||
client2.app_state.clone(),
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
@@ -359,3 +366,35 @@ async fn test_create_remote_project_path_validation(
|
||||
ErrorCode::RemoteProjectPathDoesNotExist
|
||||
));
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_save_as_remote(cx1: &mut gpui::TestAppContext, cx2: &mut gpui::TestAppContext) {
|
||||
let (server, client1) = TestServer::start1(cx1).await;
|
||||
|
||||
// Creating a project with a path that does exist should not fail
|
||||
let (dev_server, remote_workspace) =
|
||||
create_remote_project(&server, client1.app_state.clone(), cx1, cx2).await;
|
||||
|
||||
let mut cx = VisualTestContext::from_window(remote_workspace.into(), cx1);
|
||||
|
||||
cx.simulate_keystrokes("cmd-p 1 enter");
|
||||
cx.simulate_keystrokes("cmd-shift-s");
|
||||
cx.simulate_input("2.txt");
|
||||
cx.simulate_keystrokes("enter");
|
||||
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
let title = remote_workspace
|
||||
.update(&mut cx, |ws, cx| {
|
||||
ws.active_item(cx).unwrap().tab_description(0, &cx).unwrap()
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(title, "2.txt");
|
||||
|
||||
let path = Path::new("/remote/2.txt");
|
||||
assert_eq!(
|
||||
dev_server.fs().load(&path).await.unwrap(),
|
||||
"remote\nremote\nremote"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2468,7 +2468,12 @@ async fn test_propagate_saves_and_fs_changes(
|
||||
});
|
||||
project_a
|
||||
.update(cx_a, |project, cx| {
|
||||
project.save_buffer_as(new_buffer_a.clone(), "/a/file3.rs".into(), cx)
|
||||
let path = ProjectPath {
|
||||
path: Arc::from(Path::new("file3.rs")),
|
||||
worktree_id: worktree_a.read(cx).id(),
|
||||
};
|
||||
|
||||
project.save_buffer_as(new_buffer_a.clone(), path, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -522,7 +522,7 @@ impl Render for MessageEditor {
|
||||
cx.theme().colors().text
|
||||
},
|
||||
font_family: settings.ui_font.family.clone(),
|
||||
font_features: settings.ui_font.features,
|
||||
font_features: settings.ui_font.features.clone(),
|
||||
font_size: TextSize::Small.rems(cx).into(),
|
||||
font_weight: FontWeight::NORMAL,
|
||||
font_style: FontStyle::Normal,
|
||||
@@ -630,6 +630,7 @@ mod tests {
|
||||
let http = FakeHttpClient::with_404_response();
|
||||
let client = Client::new(clock, http.clone(), cx);
|
||||
let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx));
|
||||
workspace::init_settings(cx);
|
||||
theme::init(theme::LoadThemes::JustBase, cx);
|
||||
Project::init_settings(cx);
|
||||
language::init(cx);
|
||||
|
||||
@@ -2171,7 +2171,7 @@ impl CollabPanel {
|
||||
cx.theme().colors().text
|
||||
},
|
||||
font_family: settings.ui_font.family.clone(),
|
||||
font_features: settings.ui_font.features,
|
||||
font_features: settings.ui_font.features.clone(),
|
||||
font_size: rems(0.875).into(),
|
||||
font_weight: FontWeight::NORMAL,
|
||||
font_style: FontStyle::Normal,
|
||||
@@ -2970,6 +2970,7 @@ impl Render for DraggedChannelView {
|
||||
struct JoinChannelTooltip {
|
||||
channel_store: Model<ChannelStore>,
|
||||
channel_id: ChannelId,
|
||||
#[allow(unused)]
|
||||
has_notes_notification: bool,
|
||||
}
|
||||
|
||||
@@ -2983,12 +2984,6 @@ impl Render for JoinChannelTooltip {
|
||||
|
||||
container
|
||||
.child(Label::new("Join channel"))
|
||||
.children(self.has_notes_notification.then(|| {
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(Indicator::dot().color(Color::Info))
|
||||
.child(Label::new("Unread notes"))
|
||||
}))
|
||||
.children(participants.iter().map(|participant| {
|
||||
h_flex()
|
||||
.gap_2()
|
||||
|
||||
@@ -15,13 +15,16 @@ doctest = false
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
collections.workspace = true
|
||||
ctor.workspace = true
|
||||
editor.workspace = true
|
||||
env_logger.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
language.workspace = true
|
||||
log.workspace = true
|
||||
lsp.workspace = true
|
||||
project.workspace = true
|
||||
rand.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
settings.workspace = true
|
||||
@@ -40,3 +43,4 @@ serde_json.workspace = true
|
||||
theme = { workspace = true, features = ["test-support"] }
|
||||
unindent.workspace = true
|
||||
workspace = { workspace = true, features = ["test-support"] }
|
||||
pretty_assertions.workspace = true
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
1011
crates/diagnostics/src/diagnostics_tests.rs
Normal file
1011
crates/diagnostics/src/diagnostics_tests.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,13 +1,11 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use collections::HashSet;
|
||||
use editor::Editor;
|
||||
use gpui::{
|
||||
percentage, rems, Animation, AnimationExt, EventEmitter, IntoElement, ParentElement, Render,
|
||||
Styled, Subscription, Transformation, View, ViewContext, WeakView,
|
||||
};
|
||||
use language::Diagnostic;
|
||||
use lsp::LanguageServerId;
|
||||
use ui::{h_flex, prelude::*, Button, ButtonLike, Color, Icon, IconName, Label, Tooltip};
|
||||
use workspace::{item::ItemHandle, StatusItemView, ToolbarItemEvent, Workspace};
|
||||
|
||||
@@ -18,7 +16,6 @@ pub struct DiagnosticIndicator {
|
||||
active_editor: Option<WeakView<Editor>>,
|
||||
workspace: WeakView<Workspace>,
|
||||
current_diagnostic: Option<Diagnostic>,
|
||||
in_progress_checks: HashSet<LanguageServerId>,
|
||||
_observe_active_editor: Option<Subscription>,
|
||||
}
|
||||
|
||||
@@ -64,7 +61,20 @@ impl Render for DiagnosticIndicator {
|
||||
.child(Label::new(warning_count.to_string()).size(LabelSize::Small)),
|
||||
};
|
||||
|
||||
let status = if !self.in_progress_checks.is_empty() {
|
||||
let has_in_progress_checks = self
|
||||
.workspace
|
||||
.upgrade()
|
||||
.and_then(|workspace| {
|
||||
workspace
|
||||
.read(cx)
|
||||
.project()
|
||||
.read(cx)
|
||||
.language_servers_running_disk_based_diagnostics()
|
||||
.next()
|
||||
})
|
||||
.is_some();
|
||||
|
||||
let status = if has_in_progress_checks {
|
||||
Some(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
@@ -126,15 +136,13 @@ impl DiagnosticIndicator {
|
||||
pub fn new(workspace: &Workspace, cx: &mut ViewContext<Self>) -> Self {
|
||||
let project = workspace.project();
|
||||
cx.subscribe(project, |this, project, event, cx| match event {
|
||||
project::Event::DiskBasedDiagnosticsStarted { language_server_id } => {
|
||||
this.in_progress_checks.insert(*language_server_id);
|
||||
project::Event::DiskBasedDiagnosticsStarted { .. } => {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
project::Event::DiskBasedDiagnosticsFinished { language_server_id }
|
||||
| project::Event::LanguageServerRemoved(language_server_id) => {
|
||||
project::Event::DiskBasedDiagnosticsFinished { .. }
|
||||
| project::Event::LanguageServerRemoved(_) => {
|
||||
this.summary = project.read(cx).diagnostic_summary(false, cx);
|
||||
this.in_progress_checks.remove(language_server_id);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -149,10 +157,6 @@ impl DiagnosticIndicator {
|
||||
|
||||
Self {
|
||||
summary: project.read(cx).diagnostic_summary(false, cx),
|
||||
in_progress_checks: project
|
||||
.read(cx)
|
||||
.language_servers_running_disk_based_diagnostics()
|
||||
.collect(),
|
||||
active_editor: None,
|
||||
workspace: workspace.weak_handle(),
|
||||
current_diagnostic: None,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::ProjectDiagnosticsEditor;
|
||||
use gpui::{div, EventEmitter, ParentElement, Render, ViewContext, WeakView};
|
||||
use gpui::{EventEmitter, ParentElement, Render, ViewContext, WeakView};
|
||||
use ui::prelude::*;
|
||||
use ui::{IconButton, IconName, Tooltip};
|
||||
use workspace::{item::ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView};
|
||||
@@ -10,12 +10,23 @@ pub struct ToolbarControls {
|
||||
|
||||
impl Render for ToolbarControls {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let include_warnings = self
|
||||
.editor
|
||||
.as_ref()
|
||||
.and_then(|editor| editor.upgrade())
|
||||
.map(|editor| editor.read(cx).include_warnings)
|
||||
.unwrap_or(false);
|
||||
let mut include_warnings = false;
|
||||
let mut has_stale_excerpts = false;
|
||||
let mut is_updating = false;
|
||||
|
||||
if let Some(editor) = self.editor.as_ref().and_then(|editor| editor.upgrade()) {
|
||||
let editor = editor.read(cx);
|
||||
|
||||
include_warnings = editor.include_warnings;
|
||||
has_stale_excerpts = !editor.paths_to_update.is_empty();
|
||||
is_updating = editor.update_paths_tx.len() > 0
|
||||
|| editor
|
||||
.project
|
||||
.read(cx)
|
||||
.language_servers_running_disk_based_diagnostics()
|
||||
.next()
|
||||
.is_some();
|
||||
}
|
||||
|
||||
let tooltip = if include_warnings {
|
||||
"Exclude Warnings"
|
||||
@@ -23,17 +34,37 @@ impl Render for ToolbarControls {
|
||||
"Include Warnings"
|
||||
};
|
||||
|
||||
div().child(
|
||||
IconButton::new("toggle-warnings", IconName::ExclamationTriangle)
|
||||
.tooltip(move |cx| Tooltip::text(tooltip, cx))
|
||||
.on_click(cx.listener(|this, _, cx| {
|
||||
if let Some(editor) = this.editor.as_ref().and_then(|editor| editor.upgrade()) {
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.toggle_warnings(&Default::default(), cx);
|
||||
});
|
||||
}
|
||||
})),
|
||||
)
|
||||
h_flex()
|
||||
.when(has_stale_excerpts, |div| {
|
||||
div.child(
|
||||
IconButton::new("update-excerpts", IconName::Update)
|
||||
.icon_color(Color::Info)
|
||||
.disabled(is_updating)
|
||||
.tooltip(move |cx| Tooltip::text("Update excerpts", cx))
|
||||
.on_click(cx.listener(|this, _, cx| {
|
||||
if let Some(editor) =
|
||||
this.editor.as_ref().and_then(|editor| editor.upgrade())
|
||||
{
|
||||
editor.update(cx, |editor, _| {
|
||||
editor.enqueue_update_stale_excerpts(None);
|
||||
});
|
||||
}
|
||||
})),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
IconButton::new("toggle-warnings", IconName::ExclamationTriangle)
|
||||
.tooltip(move |cx| Tooltip::text(tooltip, cx))
|
||||
.on_click(cx.listener(|this, _, cx| {
|
||||
if let Some(editor) =
|
||||
this.editor.as_ref().and_then(|editor| editor.upgrade())
|
||||
{
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.toggle_warnings(&Default::default(), cx);
|
||||
});
|
||||
}
|
||||
})),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1821,6 +1821,7 @@ pub mod tests {
|
||||
cx.set_global(settings);
|
||||
language::init(cx);
|
||||
crate::init(cx);
|
||||
workspace::init_settings(cx);
|
||||
Project::init_settings(cx);
|
||||
theme::init(LoadThemes::JustBase, cx);
|
||||
cx.update_global::<SettingsStore, _>(|store, cx| {
|
||||
|
||||
@@ -130,12 +130,11 @@ use ui::{
|
||||
Tooltip,
|
||||
};
|
||||
use util::{defer, maybe, post_inc, RangeExt, ResultExt, TryFutureExt};
|
||||
use workspace::item::ItemHandle;
|
||||
use workspace::notifications::NotificationId;
|
||||
use workspace::{
|
||||
searchable::SearchEvent, ItemNavHistory, SplitDirection, ViewId, Workspace, WorkspaceId,
|
||||
item::ItemHandle, notifications::NotificationId, searchable::SearchEvent, ItemNavHistory,
|
||||
OpenInTerminal, OpenTerminal, SplitDirection, TabBarPlacement, TabBarSettings, Toast, ViewId,
|
||||
Workspace, WorkspaceId,
|
||||
};
|
||||
use workspace::{OpenInTerminal, OpenTerminal, Toast};
|
||||
|
||||
use crate::hover_links::find_url;
|
||||
|
||||
@@ -419,6 +418,7 @@ pub struct Editor {
|
||||
hovered_cursors: HashMap<HoveredCursor, Task<()>>,
|
||||
pub show_local_selections: bool,
|
||||
mode: EditorMode,
|
||||
tab_bar_placement: TabBarPlacement,
|
||||
show_breadcrumbs: bool,
|
||||
show_gutter: bool,
|
||||
show_wrap_guides: Option<bool>,
|
||||
@@ -1453,6 +1453,7 @@ impl Editor {
|
||||
blink_manager: blink_manager.clone(),
|
||||
show_local_selections: true,
|
||||
mode,
|
||||
tab_bar_placement: TabBarSettings::get_global(cx).placement,
|
||||
show_breadcrumbs: EditorSettings::get_global(cx).toolbar.breadcrumbs,
|
||||
show_gutter: mode == EditorMode::Full,
|
||||
show_wrap_guides: None,
|
||||
@@ -8206,9 +8207,13 @@ impl Editor {
|
||||
cursor_offset_in_rename_range_end..cursor_offset_in_rename_range
|
||||
}
|
||||
};
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.select_ranges([rename_selection_range]);
|
||||
});
|
||||
if rename_selection_range.end > old_name.len() {
|
||||
editor.select_all(&SelectAll, cx);
|
||||
} else {
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.select_ranges([rename_selection_range]);
|
||||
});
|
||||
}
|
||||
editor
|
||||
});
|
||||
|
||||
@@ -9616,6 +9621,7 @@ impl Editor {
|
||||
let editor_settings = EditorSettings::get_global(cx);
|
||||
self.scroll_manager.vertical_scroll_margin = editor_settings.vertical_scroll_margin;
|
||||
self.show_breadcrumbs = editor_settings.toolbar.breadcrumbs;
|
||||
self.tab_bar_placement = TabBarSettings::get_global(cx).placement;
|
||||
|
||||
if self.mode == EditorMode::Full {
|
||||
let inline_blame_enabled = ProjectSettings::get_global(cx).git.inline_blame_enabled();
|
||||
@@ -10336,7 +10342,7 @@ impl Render for Editor {
|
||||
EditorMode::SingleLine | EditorMode::AutoHeight { .. } => TextStyle {
|
||||
color: cx.theme().colors().editor_foreground,
|
||||
font_family: settings.ui_font.family.clone(),
|
||||
font_features: settings.ui_font.features,
|
||||
font_features: settings.ui_font.features.clone(),
|
||||
font_size: rems(0.875).into(),
|
||||
font_weight: FontWeight::NORMAL,
|
||||
font_style: FontStyle::Normal,
|
||||
@@ -10349,7 +10355,7 @@ impl Render for Editor {
|
||||
EditorMode::Full => TextStyle {
|
||||
color: cx.theme().colors().editor_foreground,
|
||||
font_family: settings.buffer_font.family.clone(),
|
||||
font_features: settings.buffer_font.features,
|
||||
font_features: settings.buffer_font.features.clone(),
|
||||
font_size: settings.buffer_font_size(cx).into(),
|
||||
font_weight: FontWeight::NORMAL,
|
||||
font_style: FontStyle::Normal,
|
||||
@@ -10774,7 +10780,7 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, _is_valid: bool) -> Ren
|
||||
let theme_settings = ThemeSettings::get_global(cx);
|
||||
text_style.font_family = theme_settings.buffer_font.family.clone();
|
||||
text_style.font_style = theme_settings.buffer_font.style;
|
||||
text_style.font_features = theme_settings.buffer_font.features;
|
||||
text_style.font_features = theme_settings.buffer_font.features.clone();
|
||||
text_style.font_weight = theme_settings.buffer_font.weight;
|
||||
|
||||
let multi_line_diagnostic = diagnostic.message.contains('\n');
|
||||
|
||||
@@ -89,7 +89,10 @@ impl SelectionLayout {
|
||||
}
|
||||
|
||||
// any vim visual mode (including line mode)
|
||||
if cursor_shape == CursorShape::Block && !range.is_empty() && !selection.reversed {
|
||||
if (cursor_shape == CursorShape::Block || cursor_shape == CursorShape::Hollow)
|
||||
&& !range.is_empty()
|
||||
&& !selection.reversed
|
||||
{
|
||||
if head.column() > 0 {
|
||||
head = map.clip_point(DisplayPoint::new(head.row(), head.column() - 1), Bias::Left)
|
||||
} else if head.row() > 0 && head != map.max_point() {
|
||||
|
||||
@@ -19,14 +19,17 @@ use language::{
|
||||
use project::{search::SearchQuery, FormatTrigger, Item as _, Project, ProjectPath};
|
||||
use rpc::proto::{self, update_view, PeerId};
|
||||
use settings::Settings;
|
||||
use workspace::item::{ItemSettings, TabContentParams};
|
||||
use workspace::{
|
||||
item::{TabContentParams, TabsSettings},
|
||||
TabBarPlacement,
|
||||
};
|
||||
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
cmp::{self, Ordering},
|
||||
iter,
|
||||
ops::Range,
|
||||
path::{Path, PathBuf},
|
||||
path::Path,
|
||||
sync::Arc,
|
||||
};
|
||||
use text::{BufferId, Selection};
|
||||
@@ -596,7 +599,7 @@ impl Item for Editor {
|
||||
}
|
||||
|
||||
fn tab_content(&self, params: TabContentParams, cx: &WindowContext) -> AnyElement {
|
||||
let label_color = if ItemSettings::get_global(cx).git_status {
|
||||
let label_color = if TabsSettings::get_global(cx).git_status {
|
||||
self.buffer()
|
||||
.read(cx)
|
||||
.as_singleton()
|
||||
@@ -750,7 +753,7 @@ impl Item for Editor {
|
||||
fn save_as(
|
||||
&mut self,
|
||||
project: Model<Project>,
|
||||
abs_path: PathBuf,
|
||||
path: ProjectPath,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let buffer = self
|
||||
@@ -759,14 +762,13 @@ impl Item for Editor {
|
||||
.as_singleton()
|
||||
.expect("cannot call save_as on an excerpt list");
|
||||
|
||||
let file_extension = abs_path
|
||||
let file_extension = path
|
||||
.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)
|
||||
})
|
||||
project.update(cx, |project, cx| project.save_buffer_as(buffer, path, cx))
|
||||
}
|
||||
|
||||
fn reload(&mut self, project: Model<Project>, cx: &mut ViewContext<Self>) -> Task<Result<()>> {
|
||||
@@ -800,6 +802,10 @@ impl Item for Editor {
|
||||
self.pixel_position_of_newest_cursor
|
||||
}
|
||||
|
||||
fn tab_bar_placement(&self) -> TabBarPlacement {
|
||||
self.tab_bar_placement
|
||||
}
|
||||
|
||||
fn breadcrumb_location(&self) -> ToolbarItemLocation {
|
||||
if self.show_breadcrumbs {
|
||||
ToolbarItemLocation::PrimaryLeft
|
||||
|
||||
@@ -1032,6 +1032,7 @@ mod tests {
|
||||
theme::init(theme::LoadThemes::JustBase, cx);
|
||||
language::init(cx);
|
||||
crate::init(cx);
|
||||
workspace::init_settings(cx);
|
||||
Project::init_settings(cx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -739,7 +739,7 @@ impl ExtensionsPage {
|
||||
cx.theme().colors().text
|
||||
},
|
||||
font_family: settings.ui_font.family.clone(),
|
||||
font_features: settings.ui_font.features,
|
||||
font_features: settings.ui_font.features.clone(),
|
||||
font_size: rems(0.875).into(),
|
||||
font_weight: FontWeight::NORMAL,
|
||||
font_style: FontStyle::Normal,
|
||||
|
||||
@@ -16,6 +16,7 @@ doctest = false
|
||||
anyhow.workspace = true
|
||||
collections.workspace = true
|
||||
editor.workspace = true
|
||||
futures.workspace = true
|
||||
fuzzy.workspace = true
|
||||
gpui.workspace = true
|
||||
itertools = "0.11"
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#[cfg(test)]
|
||||
mod file_finder_tests;
|
||||
|
||||
mod new_path_prompt;
|
||||
|
||||
use collections::{HashMap, HashSet};
|
||||
use editor::{scroll::Autoscroll, Bias, Editor};
|
||||
use fuzzy::{CharBag, PathMatch, PathMatchCandidate};
|
||||
@@ -10,6 +12,7 @@ use gpui::{
|
||||
ViewContext, VisualContext, WeakView,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use new_path_prompt::NewPathPrompt;
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use project::{PathMatchCandidateSet, Project, ProjectPath, WorktreeId};
|
||||
use settings::Settings;
|
||||
@@ -37,6 +40,7 @@ pub struct FileFinder {
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.observe_new_views(FileFinder::register).detach();
|
||||
cx.observe_new_views(NewPathPrompt::register).detach();
|
||||
}
|
||||
|
||||
impl FileFinder {
|
||||
@@ -454,6 +458,7 @@ impl FileFinderDelegate {
|
||||
.root_entry()
|
||||
.map_or(false, |entry| entry.is_ignored),
|
||||
include_root_name,
|
||||
directories_only: false,
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
463
crates/file_finder/src/new_path_prompt.rs
Normal file
463
crates/file_finder/src/new_path_prompt.rs
Normal file
@@ -0,0 +1,463 @@
|
||||
use futures::channel::oneshot;
|
||||
use fuzzy::PathMatch;
|
||||
use gpui::{HighlightStyle, Model, StyledText};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use project::{Entry, PathMatchCandidateSet, Project, ProjectPath, WorktreeId};
|
||||
use std::{
|
||||
path::PathBuf,
|
||||
sync::{
|
||||
atomic::{self, AtomicBool},
|
||||
Arc,
|
||||
},
|
||||
};
|
||||
use ui::{highlight_ranges, prelude::*, LabelLike, ListItemSpacing};
|
||||
use ui::{ListItem, ViewContext};
|
||||
use util::ResultExt;
|
||||
use workspace::Workspace;
|
||||
|
||||
pub(crate) struct NewPathPrompt;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct Match {
|
||||
path_match: Option<PathMatch>,
|
||||
suffix: Option<String>,
|
||||
}
|
||||
|
||||
impl Match {
|
||||
fn entry<'a>(&'a self, project: &'a Project, cx: &'a WindowContext) -> Option<&'a Entry> {
|
||||
if let Some(suffix) = &self.suffix {
|
||||
let (worktree, path) = if let Some(path_match) = &self.path_match {
|
||||
(
|
||||
project.worktree_for_id(WorktreeId::from_usize(path_match.worktree_id), cx),
|
||||
path_match.path.join(suffix),
|
||||
)
|
||||
} else {
|
||||
(project.worktrees().next(), PathBuf::from(suffix))
|
||||
};
|
||||
|
||||
worktree.and_then(|worktree| worktree.read(cx).entry_for_path(path))
|
||||
} else if let Some(path_match) = &self.path_match {
|
||||
let worktree =
|
||||
project.worktree_for_id(WorktreeId::from_usize(path_match.worktree_id), cx)?;
|
||||
worktree.read(cx).entry_for_path(path_match.path.as_ref())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn is_dir(&self, project: &Project, cx: &WindowContext) -> bool {
|
||||
self.entry(project, cx).is_some_and(|e| e.is_dir())
|
||||
|| self.suffix.as_ref().is_some_and(|s| s.ends_with('/'))
|
||||
}
|
||||
|
||||
fn relative_path(&self) -> String {
|
||||
if let Some(path_match) = &self.path_match {
|
||||
if let Some(suffix) = &self.suffix {
|
||||
format!(
|
||||
"{}/{}",
|
||||
path_match.path.to_string_lossy(),
|
||||
suffix.trim_end_matches('/')
|
||||
)
|
||||
} else {
|
||||
path_match.path.to_string_lossy().to_string()
|
||||
}
|
||||
} else if let Some(suffix) = &self.suffix {
|
||||
suffix.trim_end_matches('/').to_string()
|
||||
} else {
|
||||
"".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn project_path(&self, project: &Project, cx: &WindowContext) -> Option<ProjectPath> {
|
||||
let worktree_id = if let Some(path_match) = &self.path_match {
|
||||
WorktreeId::from_usize(path_match.worktree_id)
|
||||
} else {
|
||||
project.worktrees().next()?.read(cx).id()
|
||||
};
|
||||
|
||||
let path = PathBuf::from(self.relative_path());
|
||||
|
||||
Some(ProjectPath {
|
||||
worktree_id,
|
||||
path: Arc::from(path),
|
||||
})
|
||||
}
|
||||
|
||||
fn existing_prefix(&self, project: &Project, cx: &WindowContext) -> Option<PathBuf> {
|
||||
let worktree = project.worktrees().next()?.read(cx);
|
||||
let mut prefix = PathBuf::new();
|
||||
let parts = self.suffix.as_ref()?.split('/');
|
||||
for part in parts {
|
||||
if worktree.entry_for_path(prefix.join(&part)).is_none() {
|
||||
return Some(prefix);
|
||||
}
|
||||
prefix = prefix.join(part);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn styled_text(&self, project: &Project, cx: &WindowContext) -> StyledText {
|
||||
let mut text = "./".to_string();
|
||||
let mut highlights = Vec::new();
|
||||
let mut offset = text.as_bytes().len();
|
||||
|
||||
let separator = '/';
|
||||
let dir_indicator = "[…]";
|
||||
|
||||
if let Some(path_match) = &self.path_match {
|
||||
text.push_str(&path_match.path.to_string_lossy());
|
||||
for (range, style) in highlight_ranges(
|
||||
&path_match.path.to_string_lossy(),
|
||||
&path_match.positions,
|
||||
gpui::HighlightStyle::color(Color::Accent.color(cx)),
|
||||
) {
|
||||
highlights.push((range.start + offset..range.end + offset, style))
|
||||
}
|
||||
text.push(separator);
|
||||
offset = text.as_bytes().len();
|
||||
|
||||
if let Some(suffix) = &self.suffix {
|
||||
text.push_str(suffix);
|
||||
let entry = self.entry(project, cx);
|
||||
let color = if let Some(entry) = entry {
|
||||
if entry.is_dir() {
|
||||
Color::Accent
|
||||
} else {
|
||||
Color::Conflict
|
||||
}
|
||||
} else {
|
||||
Color::Created
|
||||
};
|
||||
highlights.push((
|
||||
offset..offset + suffix.as_bytes().len(),
|
||||
HighlightStyle::color(color.color(cx)),
|
||||
));
|
||||
offset += suffix.as_bytes().len();
|
||||
if entry.is_some_and(|e| e.is_dir()) {
|
||||
text.push(separator);
|
||||
offset += separator.len_utf8();
|
||||
|
||||
text.push_str(dir_indicator);
|
||||
highlights.push((
|
||||
offset..offset + dir_indicator.bytes().len(),
|
||||
HighlightStyle::color(Color::Muted.color(cx)),
|
||||
));
|
||||
}
|
||||
} else {
|
||||
text.push_str(dir_indicator);
|
||||
highlights.push((
|
||||
offset..offset + dir_indicator.bytes().len(),
|
||||
HighlightStyle::color(Color::Muted.color(cx)),
|
||||
))
|
||||
}
|
||||
} else if let Some(suffix) = &self.suffix {
|
||||
text.push_str(suffix);
|
||||
let existing_prefix_len = self
|
||||
.existing_prefix(project, cx)
|
||||
.map(|prefix| prefix.to_string_lossy().as_bytes().len())
|
||||
.unwrap_or(0);
|
||||
|
||||
if existing_prefix_len > 0 {
|
||||
highlights.push((
|
||||
offset..offset + existing_prefix_len,
|
||||
HighlightStyle::color(Color::Accent.color(cx)),
|
||||
));
|
||||
}
|
||||
highlights.push((
|
||||
offset + existing_prefix_len..offset + suffix.as_bytes().len(),
|
||||
HighlightStyle::color(if self.entry(project, cx).is_some() {
|
||||
Color::Conflict.color(cx)
|
||||
} else {
|
||||
Color::Created.color(cx)
|
||||
}),
|
||||
));
|
||||
offset += suffix.as_bytes().len();
|
||||
if suffix.ends_with('/') {
|
||||
text.push_str(dir_indicator);
|
||||
highlights.push((
|
||||
offset..offset + dir_indicator.bytes().len(),
|
||||
HighlightStyle::color(Color::Muted.color(cx)),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
StyledText::new(text).with_highlights(&cx.text_style().clone(), highlights)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct NewPathDelegate {
|
||||
project: Model<Project>,
|
||||
tx: Option<oneshot::Sender<Option<ProjectPath>>>,
|
||||
selected_index: usize,
|
||||
matches: Vec<Match>,
|
||||
last_selected_dir: Option<String>,
|
||||
cancel_flag: Arc<AtomicBool>,
|
||||
should_dismiss: bool,
|
||||
}
|
||||
|
||||
impl NewPathPrompt {
|
||||
pub(crate) fn register(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
|
||||
if workspace.project().read(cx).is_remote() {
|
||||
workspace.set_prompt_for_new_path(Box::new(|workspace, cx| {
|
||||
let (tx, rx) = futures::channel::oneshot::channel();
|
||||
Self::prompt_for_new_path(workspace, tx, cx);
|
||||
rx
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_for_new_path(
|
||||
workspace: &mut Workspace,
|
||||
tx: oneshot::Sender<Option<ProjectPath>>,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) {
|
||||
let project = workspace.project().clone();
|
||||
workspace.toggle_modal(cx, |cx| {
|
||||
let delegate = NewPathDelegate {
|
||||
project,
|
||||
tx: Some(tx),
|
||||
selected_index: 0,
|
||||
matches: vec![],
|
||||
cancel_flag: Arc::new(AtomicBool::new(false)),
|
||||
last_selected_dir: None,
|
||||
should_dismiss: true,
|
||||
};
|
||||
|
||||
Picker::uniform_list(delegate, cx).width(rems(34.))
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl PickerDelegate for NewPathDelegate {
|
||||
type ListItem = ui::ListItem;
|
||||
|
||||
fn match_count(&self) -> usize {
|
||||
self.matches.len()
|
||||
}
|
||||
|
||||
fn selected_index(&self) -> usize {
|
||||
self.selected_index
|
||||
}
|
||||
|
||||
fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<picker::Picker<Self>>) {
|
||||
self.selected_index = ix;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn update_matches(
|
||||
&mut self,
|
||||
query: String,
|
||||
cx: &mut ViewContext<picker::Picker<Self>>,
|
||||
) -> gpui::Task<()> {
|
||||
let query = query.trim().trim_start_matches('/');
|
||||
let (dir, suffix) = if let Some(index) = query.rfind('/') {
|
||||
let suffix = if index + 1 < query.len() {
|
||||
Some(query[index + 1..].to_string())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
(query[0..index].to_string(), suffix)
|
||||
} else {
|
||||
(query.to_string(), None)
|
||||
};
|
||||
|
||||
let worktrees = self
|
||||
.project
|
||||
.read(cx)
|
||||
.visible_worktrees(cx)
|
||||
.collect::<Vec<_>>();
|
||||
let include_root_name = worktrees.len() > 1;
|
||||
let candidate_sets = worktrees
|
||||
.into_iter()
|
||||
.map(|worktree| {
|
||||
let worktree = worktree.read(cx);
|
||||
PathMatchCandidateSet {
|
||||
snapshot: worktree.snapshot(),
|
||||
include_ignored: worktree
|
||||
.root_entry()
|
||||
.map_or(false, |entry| entry.is_ignored),
|
||||
include_root_name,
|
||||
directories_only: true,
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
self.cancel_flag.store(true, atomic::Ordering::Relaxed);
|
||||
self.cancel_flag = Arc::new(AtomicBool::new(false));
|
||||
|
||||
let cancel_flag = self.cancel_flag.clone();
|
||||
let query = query.to_string();
|
||||
let prefix = dir.clone();
|
||||
cx.spawn(|picker, mut cx| async move {
|
||||
let matches = fuzzy::match_path_sets(
|
||||
candidate_sets.as_slice(),
|
||||
&dir,
|
||||
None,
|
||||
false,
|
||||
100,
|
||||
&cancel_flag,
|
||||
cx.background_executor().clone(),
|
||||
)
|
||||
.await;
|
||||
let did_cancel = cancel_flag.load(atomic::Ordering::Relaxed);
|
||||
if did_cancel {
|
||||
return;
|
||||
}
|
||||
picker
|
||||
.update(&mut cx, |picker, cx| {
|
||||
picker
|
||||
.delegate
|
||||
.set_search_matches(query, prefix, suffix, matches, cx)
|
||||
})
|
||||
.log_err();
|
||||
})
|
||||
}
|
||||
|
||||
fn confirm_update_query(&mut self, cx: &mut ViewContext<Picker<Self>>) -> Option<String> {
|
||||
let m = self.matches.get(self.selected_index)?;
|
||||
if m.is_dir(self.project.read(cx), cx) {
|
||||
let path = m.relative_path();
|
||||
self.last_selected_dir = Some(path.clone());
|
||||
Some(format!("{}/", path))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _: bool, cx: &mut ViewContext<picker::Picker<Self>>) {
|
||||
let Some(m) = self.matches.get(self.selected_index) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let exists = m.entry(self.project.read(cx), cx).is_some();
|
||||
if exists {
|
||||
self.should_dismiss = false;
|
||||
let answer = cx.prompt(
|
||||
gpui::PromptLevel::Destructive,
|
||||
&format!("{} already exists. Do you want to replace it?", m.relative_path()),
|
||||
Some(
|
||||
"A file or folder with the same name already eixsts. Replacing it will overwrite its current contents.",
|
||||
),
|
||||
&["Replace", "Cancel"],
|
||||
);
|
||||
let m = m.clone();
|
||||
cx.spawn(|picker, mut cx| async move {
|
||||
let answer = answer.await.ok();
|
||||
picker
|
||||
.update(&mut cx, |picker, cx| {
|
||||
picker.delegate.should_dismiss = true;
|
||||
if answer != Some(0) {
|
||||
return;
|
||||
}
|
||||
if let Some(path) = m.project_path(picker.delegate.project.read(cx), cx) {
|
||||
if let Some(tx) = picker.delegate.tx.take() {
|
||||
tx.send(Some(path)).ok();
|
||||
}
|
||||
}
|
||||
cx.emit(gpui::DismissEvent);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.detach();
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(path) = m.project_path(self.project.read(cx), cx) {
|
||||
if let Some(tx) = self.tx.take() {
|
||||
tx.send(Some(path)).ok();
|
||||
}
|
||||
}
|
||||
cx.emit(gpui::DismissEvent);
|
||||
}
|
||||
|
||||
fn should_dismiss(&self) -> bool {
|
||||
self.should_dismiss
|
||||
}
|
||||
|
||||
fn dismissed(&mut self, cx: &mut ViewContext<picker::Picker<Self>>) {
|
||||
if let Some(tx) = self.tx.take() {
|
||||
tx.send(None).ok();
|
||||
}
|
||||
cx.emit(gpui::DismissEvent)
|
||||
}
|
||||
|
||||
fn render_match(
|
||||
&self,
|
||||
ix: usize,
|
||||
selected: bool,
|
||||
cx: &mut ViewContext<picker::Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
let m = self.matches.get(ix)?;
|
||||
|
||||
Some(
|
||||
ListItem::new(ix)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.inset(true)
|
||||
.selected(selected)
|
||||
.child(LabelLike::new().child(m.styled_text(self.project.read(cx), cx))),
|
||||
)
|
||||
}
|
||||
|
||||
fn no_matches_text(&self, _cx: &mut WindowContext) -> SharedString {
|
||||
"Type a path...".into()
|
||||
}
|
||||
|
||||
fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
|
||||
Arc::from("[directory/]filename.ext")
|
||||
}
|
||||
}
|
||||
|
||||
impl NewPathDelegate {
|
||||
fn set_search_matches(
|
||||
&mut self,
|
||||
query: String,
|
||||
prefix: String,
|
||||
suffix: Option<String>,
|
||||
matches: Vec<PathMatch>,
|
||||
cx: &mut ViewContext<Picker<Self>>,
|
||||
) {
|
||||
cx.notify();
|
||||
if query.is_empty() {
|
||||
self.matches = vec![];
|
||||
return;
|
||||
}
|
||||
|
||||
let mut directory_exists = false;
|
||||
|
||||
self.matches = matches
|
||||
.into_iter()
|
||||
.map(|m| {
|
||||
if m.path.as_ref().to_string_lossy() == prefix {
|
||||
directory_exists = true
|
||||
}
|
||||
Match {
|
||||
path_match: Some(m),
|
||||
suffix: suffix.clone(),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
if !directory_exists {
|
||||
if suffix.is_none()
|
||||
|| self
|
||||
.last_selected_dir
|
||||
.as_ref()
|
||||
.is_some_and(|d| query.starts_with(d))
|
||||
{
|
||||
self.matches.insert(
|
||||
0,
|
||||
Match {
|
||||
path_match: None,
|
||||
suffix: Some(query.clone()),
|
||||
},
|
||||
)
|
||||
} else {
|
||||
self.matches.push(Match {
|
||||
path_match: None,
|
||||
suffix: Some(query.clone()),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -714,6 +714,15 @@ impl FakeFs {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn read_file_sync(&self, path: impl AsRef<Path>) -> Result<Vec<u8>> {
|
||||
let path = path.as_ref();
|
||||
let path = normalize_path(path);
|
||||
let state = self.state.lock();
|
||||
let entry = state.read_path(&path)?;
|
||||
let entry = entry.lock();
|
||||
entry.file_content(&path).cloned()
|
||||
}
|
||||
|
||||
async fn load_internal(&self, path: impl AsRef<Path>) -> Result<Vec<u8>> {
|
||||
let path = path.as_ref();
|
||||
let path = normalize_path(path);
|
||||
|
||||
@@ -120,6 +120,9 @@ xkbcommon = { version = "0.7", features = ["wayland", "x11"] }
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows.workspace = true
|
||||
|
||||
[target.'cfg(windows)'.build-dependencies]
|
||||
embed-resource = "2.4"
|
||||
|
||||
[[example]]
|
||||
name = "hello_world"
|
||||
path = "examples/hello_world.rs"
|
||||
|
||||
@@ -6,6 +6,15 @@
|
||||
fn main() {
|
||||
#[cfg(target_os = "macos")]
|
||||
macos::build();
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let manifest = std::path::Path::new("resources/windows/gpui.manifest.xml");
|
||||
let rc_file = std::path::Path::new("resources/windows/gpui.rc");
|
||||
println!("cargo:rerun-if-changed={}", manifest.display());
|
||||
println!("cargo:rerun-if-changed={}", rc_file.display());
|
||||
embed_resource::compile(rc_file, embed_resource::NONE);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
|
||||
2
crates/gpui/resources/windows/gpui.rc
Normal file
2
crates/gpui/resources/windows/gpui.rc
Normal file
@@ -0,0 +1,2 @@
|
||||
#define RT_MANIFEST 24
|
||||
1 RT_MANIFEST "resources/windows/gpui.manifest.xml"
|
||||
@@ -693,7 +693,7 @@ pub struct PathPromptOptions {
|
||||
}
|
||||
|
||||
/// What kind of prompt styling to show
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||
pub enum PromptLevel {
|
||||
/// A prompt that is shown when the user should be notified of something
|
||||
Info,
|
||||
@@ -703,6 +703,10 @@ pub enum PromptLevel {
|
||||
|
||||
/// A prompt that is shown when a critical problem has occurred
|
||||
Critical,
|
||||
|
||||
/// A prompt that is shown when asking the user to confirm a potentially destructive action
|
||||
/// (overwriting a file for example)
|
||||
Destructive,
|
||||
}
|
||||
|
||||
/// The style of the cursor (pointer)
|
||||
|
||||
@@ -90,7 +90,7 @@ impl PlatformTextSystem for CosmicTextSystem {
|
||||
let candidates = if let Some(font_ids) = state.font_ids_by_family_cache.get(&font.family) {
|
||||
font_ids.as_slice()
|
||||
} else {
|
||||
let font_ids = state.load_family(&font.family, font.features)?;
|
||||
let font_ids = state.load_family(&font.family, &font.features)?;
|
||||
state
|
||||
.font_ids_by_family_cache
|
||||
.insert(font.family.clone(), font_ids);
|
||||
@@ -211,7 +211,7 @@ impl CosmicTextSystemState {
|
||||
fn load_family(
|
||||
&mut self,
|
||||
name: &str,
|
||||
_features: FontFeatures,
|
||||
_features: &FontFeatures,
|
||||
) -> Result<SmallVec<[FontId; 4]>> {
|
||||
// TODO: Determine the proper system UI font.
|
||||
let name = if name == ".SystemUIFont" {
|
||||
|
||||
@@ -843,7 +843,9 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr {
|
||||
keystroke: Keystroke::from_xkb(keymap_state, state.modifiers, keycode),
|
||||
});
|
||||
|
||||
state.repeat.current_keysym = None;
|
||||
if state.repeat.current_keysym == Some(keysym) {
|
||||
state.repeat.current_keysym = None;
|
||||
}
|
||||
|
||||
drop(state);
|
||||
focused_window.handle_input(input);
|
||||
|
||||
@@ -322,7 +322,7 @@ impl WaylandWindowStatePtr {
|
||||
self.resize(width, height);
|
||||
self.set_fullscreen(fullscreen);
|
||||
let mut state = self.state.borrow_mut();
|
||||
state.maximized = true;
|
||||
state.maximized = maximized;
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::cell::RefCell;
|
||||
use std::ops::Deref;
|
||||
use std::rc::Rc;
|
||||
use std::rc::{Rc, Weak};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use calloop::{EventLoop, LoopHandle};
|
||||
@@ -23,10 +23,10 @@ use crate::platform::linux::LinuxClient;
|
||||
use crate::platform::{LinuxCommon, PlatformWindow};
|
||||
use crate::{
|
||||
px, AnyWindowHandle, Bounds, CursorStyle, DisplayId, Modifiers, ModifiersChangedEvent, Pixels,
|
||||
PlatformDisplay, PlatformInput, Point, ScrollDelta, Size, TouchPhase, WindowParams,
|
||||
PlatformDisplay, PlatformInput, Point, ScrollDelta, Size, TouchPhase, WindowParams, X11Window,
|
||||
};
|
||||
|
||||
use super::{super::SCROLL_LINES, X11Display, X11Window, XcbAtoms};
|
||||
use super::{super::SCROLL_LINES, X11Display, X11WindowStatePtr, XcbAtoms};
|
||||
use super::{button_of_key, modifiers_from_state};
|
||||
use crate::platform::linux::is_within_click_distance;
|
||||
use crate::platform::linux::platform::DOUBLE_CLICK_INTERVAL;
|
||||
@@ -36,12 +36,12 @@ use calloop::{
|
||||
};
|
||||
|
||||
pub(crate) struct WindowRef {
|
||||
window: X11Window,
|
||||
window: X11WindowStatePtr,
|
||||
refresh_event_token: RegistrationToken,
|
||||
}
|
||||
|
||||
impl Deref for WindowRef {
|
||||
type Target = X11Window;
|
||||
type Target = X11WindowStatePtr;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.window
|
||||
@@ -68,6 +68,24 @@ pub struct X11ClientState {
|
||||
pub(crate) primary: X11ClipboardContext<Primary>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct X11ClientStatePtr(pub Weak<RefCell<X11ClientState>>);
|
||||
|
||||
impl X11ClientStatePtr {
|
||||
pub fn drop_window(&self, x_window: u32) {
|
||||
let client = X11Client(self.0.upgrade().expect("client already dropped"));
|
||||
let mut state = client.0.borrow_mut();
|
||||
|
||||
if let Some(window_ref) = state.windows.remove(&x_window) {
|
||||
state.loop_handle.remove(window_ref.refresh_event_token);
|
||||
}
|
||||
|
||||
if state.windows.is_empty() {
|
||||
state.common.signal.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct X11Client(Rc<RefCell<X11ClientState>>);
|
||||
|
||||
@@ -171,7 +189,7 @@ impl X11Client {
|
||||
})))
|
||||
}
|
||||
|
||||
fn get_window(&self, win: xproto::Window) -> Option<X11Window> {
|
||||
fn get_window(&self, win: xproto::Window) -> Option<X11WindowStatePtr> {
|
||||
let state = self.0.borrow();
|
||||
state
|
||||
.windows
|
||||
@@ -182,18 +200,16 @@ impl X11Client {
|
||||
fn handle_event(&self, event: Event) -> Option<()> {
|
||||
match event {
|
||||
Event::ClientMessage(event) => {
|
||||
let window = self.get_window(event.window)?;
|
||||
let [atom, ..] = event.data.as_data32();
|
||||
let mut state = self.0.borrow_mut();
|
||||
|
||||
if atom == state.atoms.WM_DELETE_WINDOW {
|
||||
// window "x" button clicked by user, we gracefully exit
|
||||
let window_ref = state.windows.remove(&event.window)?;
|
||||
|
||||
state.loop_handle.remove(window_ref.refresh_event_token);
|
||||
window_ref.window.destroy();
|
||||
|
||||
if state.windows.is_empty() {
|
||||
state.common.signal.stop();
|
||||
// window "x" button clicked by user
|
||||
if window.should_close() {
|
||||
let window_ref = state.windows.remove(&event.window)?;
|
||||
state.loop_handle.remove(window_ref.refresh_event_token);
|
||||
// Rest of the close logic is handled in drop_window()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -424,6 +440,8 @@ impl LinuxClient for X11Client {
|
||||
let x_window = state.xcb_connection.generate_id().unwrap();
|
||||
|
||||
let window = X11Window::new(
|
||||
X11ClientStatePtr(Rc::downgrade(&self.0)),
|
||||
state.common.foreground_executor.clone(),
|
||||
params,
|
||||
&state.xcb_connection,
|
||||
state.x_root_index,
|
||||
@@ -492,7 +510,7 @@ impl LinuxClient for X11Client {
|
||||
.expect("Failed to initialize refresh timer");
|
||||
|
||||
let window_ref = WindowRef {
|
||||
window: window.clone(),
|
||||
window: window.0.clone(),
|
||||
refresh_event_token,
|
||||
};
|
||||
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
#![allow(unused)]
|
||||
|
||||
use crate::{
|
||||
platform::blade::BladeRenderer, size, Bounds, DevicePixels, Modifiers, Pixels, PlatformAtlas,
|
||||
PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow, Point, PromptLevel,
|
||||
Scene, Size, WindowAppearance, WindowBackgroundAppearance, WindowOptions, WindowParams,
|
||||
X11Client, X11ClientState,
|
||||
platform::blade::BladeRenderer, size, Bounds, DevicePixels, ForegroundExecutor, Modifiers,
|
||||
Pixels, Platform, PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler,
|
||||
PlatformWindow, Point, PromptLevel, Scene, Size, WindowAppearance, WindowBackgroundAppearance,
|
||||
WindowOptions, WindowParams, X11Client, X11ClientState, X11ClientStatePtr,
|
||||
};
|
||||
use blade_graphics as gpu;
|
||||
use parking_lot::Mutex;
|
||||
@@ -77,6 +77,8 @@ pub struct Callbacks {
|
||||
}
|
||||
|
||||
pub(crate) struct X11WindowState {
|
||||
client: X11ClientStatePtr,
|
||||
executor: ForegroundExecutor,
|
||||
atoms: XcbAtoms,
|
||||
raw: RawWindow,
|
||||
bounds: Bounds<i32>,
|
||||
@@ -88,7 +90,7 @@ pub(crate) struct X11WindowState {
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct X11Window {
|
||||
pub(crate) struct X11WindowStatePtr {
|
||||
pub(crate) state: Rc<RefCell<X11WindowState>>,
|
||||
pub(crate) callbacks: Rc<RefCell<Callbacks>>,
|
||||
xcb_connection: Rc<XCBConnection>,
|
||||
@@ -124,6 +126,8 @@ impl rwh::HasDisplayHandle for X11Window {
|
||||
|
||||
impl X11WindowState {
|
||||
pub fn new(
|
||||
client: X11ClientStatePtr,
|
||||
executor: ForegroundExecutor,
|
||||
params: WindowParams,
|
||||
xcb_connection: &Rc<XCBConnection>,
|
||||
x_main_screen_index: usize,
|
||||
@@ -224,6 +228,8 @@ impl X11WindowState {
|
||||
let gpu_extent = query_render_extent(xcb_connection, x_window);
|
||||
|
||||
Self {
|
||||
client,
|
||||
executor,
|
||||
display: Rc::new(X11Display::new(xcb_connection, x_screen_index).unwrap()),
|
||||
raw,
|
||||
bounds: params.bounds.map(|v| v.0),
|
||||
@@ -244,16 +250,47 @@ impl X11WindowState {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct X11Window(pub X11WindowStatePtr);
|
||||
|
||||
impl Drop for X11Window {
|
||||
fn drop(&mut self) {
|
||||
let mut state = self.0.state.borrow_mut();
|
||||
state.renderer.destroy();
|
||||
|
||||
self.0.xcb_connection.unmap_window(self.0.x_window).unwrap();
|
||||
self.0
|
||||
.xcb_connection
|
||||
.destroy_window(self.0.x_window)
|
||||
.unwrap();
|
||||
self.0.xcb_connection.flush().unwrap();
|
||||
|
||||
let this_ptr = self.0.clone();
|
||||
let client_ptr = state.client.clone();
|
||||
state
|
||||
.executor
|
||||
.spawn(async move {
|
||||
this_ptr.close();
|
||||
client_ptr.drop_window(this_ptr.x_window);
|
||||
})
|
||||
.detach();
|
||||
drop(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl X11Window {
|
||||
pub fn new(
|
||||
client: X11ClientStatePtr,
|
||||
executor: ForegroundExecutor,
|
||||
params: WindowParams,
|
||||
xcb_connection: &Rc<XCBConnection>,
|
||||
x_main_screen_index: usize,
|
||||
x_window: xproto::Window,
|
||||
atoms: &XcbAtoms,
|
||||
) -> Self {
|
||||
X11Window {
|
||||
Self(X11WindowStatePtr {
|
||||
state: Rc::new(RefCell::new(X11WindowState::new(
|
||||
client,
|
||||
executor,
|
||||
params,
|
||||
xcb_connection,
|
||||
x_main_screen_index,
|
||||
@@ -263,20 +300,27 @@ impl X11Window {
|
||||
callbacks: Rc::new(RefCell::new(Callbacks::default())),
|
||||
xcb_connection: xcb_connection.clone(),
|
||||
x_window,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl X11WindowStatePtr {
|
||||
pub fn should_close(&self) -> bool {
|
||||
let mut cb = self.callbacks.borrow_mut();
|
||||
if let Some(mut should_close) = cb.should_close.take() {
|
||||
let result = (should_close)();
|
||||
cb.should_close = Some(should_close);
|
||||
result
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
pub fn destroy(&self) {
|
||||
let mut state = self.state.borrow_mut();
|
||||
state.renderer.destroy();
|
||||
drop(state);
|
||||
|
||||
self.xcb_connection.unmap_window(self.x_window).unwrap();
|
||||
self.xcb_connection.destroy_window(self.x_window).unwrap();
|
||||
if let Some(fun) = self.callbacks.borrow_mut().close.take() {
|
||||
fun();
|
||||
pub fn close(&self) {
|
||||
let mut callbacks = self.callbacks.borrow_mut();
|
||||
if let Some(fun) = callbacks.close.take() {
|
||||
fun()
|
||||
}
|
||||
self.xcb_connection.flush().unwrap();
|
||||
}
|
||||
|
||||
pub fn refresh(&self) {
|
||||
@@ -345,7 +389,7 @@ impl X11Window {
|
||||
|
||||
impl PlatformWindow for X11Window {
|
||||
fn bounds(&self) -> Bounds<DevicePixels> {
|
||||
self.state.borrow_mut().bounds.map(|v| v.into())
|
||||
self.0.state.borrow_mut().bounds.map(|v| v.into())
|
||||
}
|
||||
|
||||
// todo(linux)
|
||||
@@ -359,11 +403,11 @@ impl PlatformWindow for X11Window {
|
||||
}
|
||||
|
||||
fn content_size(&self) -> Size<Pixels> {
|
||||
self.state.borrow_mut().content_size()
|
||||
self.0.state.borrow_mut().content_size()
|
||||
}
|
||||
|
||||
fn scale_factor(&self) -> f32 {
|
||||
self.state.borrow_mut().scale_factor
|
||||
self.0.state.borrow_mut().scale_factor
|
||||
}
|
||||
|
||||
// todo(linux)
|
||||
@@ -372,13 +416,14 @@ impl PlatformWindow for X11Window {
|
||||
}
|
||||
|
||||
fn display(&self) -> Rc<dyn PlatformDisplay> {
|
||||
self.state.borrow().display.clone()
|
||||
self.0.state.borrow().display.clone()
|
||||
}
|
||||
|
||||
fn mouse_position(&self) -> Point<Pixels> {
|
||||
let reply = self
|
||||
.0
|
||||
.xcb_connection
|
||||
.query_pointer(self.x_window)
|
||||
.query_pointer(self.0.x_window)
|
||||
.unwrap()
|
||||
.reply()
|
||||
.unwrap();
|
||||
@@ -395,11 +440,11 @@ impl PlatformWindow for X11Window {
|
||||
}
|
||||
|
||||
fn set_input_handler(&mut self, input_handler: PlatformInputHandler) {
|
||||
self.state.borrow_mut().input_handler = Some(input_handler);
|
||||
self.0.state.borrow_mut().input_handler = Some(input_handler);
|
||||
}
|
||||
|
||||
fn take_input_handler(&mut self) -> Option<PlatformInputHandler> {
|
||||
self.state.borrow_mut().input_handler.take()
|
||||
self.0.state.borrow_mut().input_handler.take()
|
||||
}
|
||||
|
||||
fn prompt(
|
||||
@@ -414,8 +459,9 @@ impl PlatformWindow for X11Window {
|
||||
|
||||
fn activate(&self) {
|
||||
let win_aux = xproto::ConfigureWindowAux::new().stack_mode(xproto::StackMode::ABOVE);
|
||||
self.xcb_connection
|
||||
.configure_window(self.x_window, &win_aux)
|
||||
self.0
|
||||
.xcb_connection
|
||||
.configure_window(self.0.x_window, &win_aux)
|
||||
.log_err();
|
||||
}
|
||||
|
||||
@@ -425,22 +471,24 @@ impl PlatformWindow for X11Window {
|
||||
}
|
||||
|
||||
fn set_title(&mut self, title: &str) {
|
||||
self.xcb_connection
|
||||
self.0
|
||||
.xcb_connection
|
||||
.change_property8(
|
||||
xproto::PropMode::REPLACE,
|
||||
self.x_window,
|
||||
self.0.x_window,
|
||||
xproto::AtomEnum::WM_NAME,
|
||||
xproto::AtomEnum::STRING,
|
||||
title.as_bytes(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
self.xcb_connection
|
||||
self.0
|
||||
.xcb_connection
|
||||
.change_property8(
|
||||
xproto::PropMode::REPLACE,
|
||||
self.x_window,
|
||||
self.state.borrow().atoms._NET_WM_NAME,
|
||||
self.state.borrow().atoms.UTF8_STRING,
|
||||
self.0.x_window,
|
||||
self.0.state.borrow().atoms._NET_WM_NAME,
|
||||
self.0.state.borrow().atoms.UTF8_STRING,
|
||||
title.as_bytes(),
|
||||
)
|
||||
.unwrap();
|
||||
@@ -484,39 +532,39 @@ impl PlatformWindow for X11Window {
|
||||
}
|
||||
|
||||
fn on_request_frame(&self, callback: Box<dyn FnMut()>) {
|
||||
self.callbacks.borrow_mut().request_frame = Some(callback);
|
||||
self.0.callbacks.borrow_mut().request_frame = Some(callback);
|
||||
}
|
||||
|
||||
fn on_input(&self, callback: Box<dyn FnMut(PlatformInput) -> crate::DispatchEventResult>) {
|
||||
self.callbacks.borrow_mut().input = Some(callback);
|
||||
self.0.callbacks.borrow_mut().input = Some(callback);
|
||||
}
|
||||
|
||||
fn on_active_status_change(&self, callback: Box<dyn FnMut(bool)>) {
|
||||
self.callbacks.borrow_mut().active_status_change = Some(callback);
|
||||
self.0.callbacks.borrow_mut().active_status_change = Some(callback);
|
||||
}
|
||||
|
||||
fn on_resize(&self, callback: Box<dyn FnMut(Size<Pixels>, f32)>) {
|
||||
self.callbacks.borrow_mut().resize = Some(callback);
|
||||
self.0.callbacks.borrow_mut().resize = Some(callback);
|
||||
}
|
||||
|
||||
fn on_fullscreen(&self, callback: Box<dyn FnMut(bool)>) {
|
||||
self.callbacks.borrow_mut().fullscreen = Some(callback);
|
||||
self.0.callbacks.borrow_mut().fullscreen = Some(callback);
|
||||
}
|
||||
|
||||
fn on_moved(&self, callback: Box<dyn FnMut()>) {
|
||||
self.callbacks.borrow_mut().moved = Some(callback);
|
||||
self.0.callbacks.borrow_mut().moved = Some(callback);
|
||||
}
|
||||
|
||||
fn on_should_close(&self, callback: Box<dyn FnMut() -> bool>) {
|
||||
self.callbacks.borrow_mut().should_close = Some(callback);
|
||||
self.0.callbacks.borrow_mut().should_close = Some(callback);
|
||||
}
|
||||
|
||||
fn on_close(&self, callback: Box<dyn FnOnce()>) {
|
||||
self.callbacks.borrow_mut().close = Some(callback);
|
||||
self.0.callbacks.borrow_mut().close = Some(callback);
|
||||
}
|
||||
|
||||
fn on_appearance_changed(&self, callback: Box<dyn FnMut()>) {
|
||||
self.callbacks.borrow_mut().appearance_changed = Some(callback);
|
||||
self.0.callbacks.borrow_mut().appearance_changed = Some(callback);
|
||||
}
|
||||
|
||||
// todo(linux)
|
||||
@@ -525,12 +573,12 @@ impl PlatformWindow for X11Window {
|
||||
}
|
||||
|
||||
fn draw(&self, scene: &Scene) {
|
||||
let mut inner = self.state.borrow_mut();
|
||||
let mut inner = self.0.state.borrow_mut();
|
||||
inner.renderer.draw(scene);
|
||||
}
|
||||
|
||||
fn sprite_atlas(&self) -> sync::Arc<dyn PlatformAtlas> {
|
||||
let inner = self.state.borrow();
|
||||
let inner = self.0.state.borrow();
|
||||
inner.renderer.sprite_atlas().clone()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,7 +107,7 @@ const kTypographicExtrasType: i32 = 14;
|
||||
const kVerticalFractionsSelector: i32 = 1;
|
||||
const kVerticalPositionType: i32 = 10;
|
||||
|
||||
pub fn apply_features(font: &mut Font, features: FontFeatures) {
|
||||
pub fn apply_features(font: &mut Font, features: &FontFeatures) {
|
||||
// See https://chromium.googlesource.com/chromium/src/+/66.0.3359.158/third_party/harfbuzz-ng/src/hb-coretext.cc
|
||||
// for a reference implementation.
|
||||
toggle_open_type_feature(
|
||||
|
||||
@@ -123,12 +123,12 @@ impl PlatformTextSystem for MacTextSystem {
|
||||
let mut lock = RwLockUpgradableReadGuard::upgrade(lock);
|
||||
let font_key = FontKey {
|
||||
font_family: font.family.clone(),
|
||||
font_features: font.features,
|
||||
font_features: font.features.clone(),
|
||||
};
|
||||
let candidates = if let Some(font_ids) = lock.font_ids_by_font_key.get(&font_key) {
|
||||
font_ids.as_slice()
|
||||
} else {
|
||||
let font_ids = lock.load_family(&font.family, font.features)?;
|
||||
let font_ids = lock.load_family(&font.family, &font.features)?;
|
||||
lock.font_ids_by_font_key.insert(font_key.clone(), font_ids);
|
||||
lock.font_ids_by_font_key[&font_key].as_ref()
|
||||
};
|
||||
@@ -219,7 +219,11 @@ impl MacTextSystemState {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn load_family(&mut self, name: &str, features: FontFeatures) -> Result<SmallVec<[FontId; 4]>> {
|
||||
fn load_family(
|
||||
&mut self,
|
||||
name: &str,
|
||||
features: &FontFeatures,
|
||||
) -> Result<SmallVec<[FontId; 4]>> {
|
||||
let name = if name == ".SystemUIFont" {
|
||||
".AppleSystemUIFont"
|
||||
} else {
|
||||
|
||||
@@ -904,7 +904,7 @@ impl PlatformWindow for MacWindow {
|
||||
let alert_style = match level {
|
||||
PromptLevel::Info => 1,
|
||||
PromptLevel::Warning => 0,
|
||||
PromptLevel::Critical => 2,
|
||||
PromptLevel::Critical | PromptLevel::Destructive => 2,
|
||||
};
|
||||
let _: () = msg_send![alert, setAlertStyle: alert_style];
|
||||
let _: () = msg_send![alert, setMessageText: ns_string(msg)];
|
||||
@@ -919,10 +919,17 @@ impl PlatformWindow for MacWindow {
|
||||
{
|
||||
let button: id = msg_send![alert, addButtonWithTitle: ns_string(answer)];
|
||||
let _: () = msg_send![button, setTag: ix as NSInteger];
|
||||
if level == PromptLevel::Destructive && answer != &"Cancel" {
|
||||
let _: () = msg_send![button, setHasDestructiveAction: YES];
|
||||
}
|
||||
}
|
||||
if let Some((ix, answer)) = latest_non_cancel_label {
|
||||
let button: id = msg_send![alert, addButtonWithTitle: ns_string(answer)];
|
||||
let _: () = msg_send![button, setTag: ix as NSInteger];
|
||||
let _: () = msg_send![button, setHasDestructiveAction: YES];
|
||||
if level == PromptLevel::Destructive {
|
||||
let _: () = msg_send![button, setHasDestructiveAction: YES];
|
||||
}
|
||||
}
|
||||
|
||||
let (done_tx, done_rx) = oneshot::channel();
|
||||
|
||||
@@ -25,7 +25,7 @@ use windows::{
|
||||
Win32::{
|
||||
Foundation::*,
|
||||
Graphics::Gdi::*,
|
||||
System::{Com::*, Ole::*, SystemServices::*},
|
||||
System::{Com::*, LibraryLoader::*, Ole::*, SystemServices::*},
|
||||
UI::{
|
||||
Controls::*,
|
||||
HiDpi::*,
|
||||
@@ -82,7 +82,9 @@ impl WindowsWindowInner {
|
||||
fn window_handle(&self) -> Result<rwh::WindowHandle<'_>, rwh::HandleError> {
|
||||
Ok(unsafe {
|
||||
let hwnd = NonZeroIsize::new_unchecked(self.hwnd);
|
||||
let handle = rwh::Win32WindowHandle::new(hwnd);
|
||||
let mut handle = rwh::Win32WindowHandle::new(hwnd);
|
||||
let hinstance = get_window_long(HWND(self.hwnd), GWLP_HINSTANCE);
|
||||
handle.hinstance = NonZeroIsize::new(hinstance);
|
||||
rwh::WindowHandle::borrow_raw(handle.into())
|
||||
})
|
||||
}
|
||||
@@ -1269,7 +1271,7 @@ impl WindowsWindow {
|
||||
let nheight = options.bounds.size.height.0;
|
||||
let hwndparent = HWND::default();
|
||||
let hmenu = HMENU::default();
|
||||
let hinstance = HINSTANCE::default();
|
||||
let hinstance = get_module_handle();
|
||||
let mut context = WindowCreateContext {
|
||||
inner: None,
|
||||
platform_inner: platform_inner.clone(),
|
||||
@@ -1455,7 +1457,7 @@ impl PlatformWindow for WindowsWindow {
|
||||
title = windows::core::w!("Warning");
|
||||
main_icon = TD_WARNING_ICON;
|
||||
}
|
||||
crate::PromptLevel::Critical => {
|
||||
crate::PromptLevel::Critical | crate::PromptLevel::Destructive => {
|
||||
title = windows::core::w!("Critical");
|
||||
main_icon = TD_ERROR_ICON;
|
||||
}
|
||||
@@ -1767,6 +1769,7 @@ fn register_wnd_class(icon_handle: HICON) -> PCWSTR {
|
||||
hIcon: icon_handle,
|
||||
lpszClassName: PCWSTR(CLASS_NAME.as_ptr()),
|
||||
style: CS_HREDRAW | CS_VREDRAW,
|
||||
hInstance: get_module_handle().into(),
|
||||
..Default::default()
|
||||
};
|
||||
unsafe { RegisterClassW(&wc) };
|
||||
@@ -1907,6 +1910,20 @@ struct StyleAndBounds {
|
||||
cy: i32,
|
||||
}
|
||||
|
||||
fn get_module_handle() -> HMODULE {
|
||||
unsafe {
|
||||
let mut h_module = std::mem::zeroed();
|
||||
GetModuleHandleExW(
|
||||
GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
|
||||
windows::core::w!("ZedModule"),
|
||||
&mut h_module,
|
||||
)
|
||||
.expect("Unable to get module handle"); // this should never fail
|
||||
|
||||
h_module
|
||||
}
|
||||
}
|
||||
|
||||
// https://learn.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-dragqueryfilew
|
||||
const DRAGDROP_GET_FILES_COUNT: u32 = 0xFFFFFFFF;
|
||||
// https://learn.microsoft.com/en-us/windows/win32/controls/ttm-setdelaytime?redirectedfrom=MSDN
|
||||
|
||||
@@ -262,7 +262,7 @@ impl TextStyle {
|
||||
pub fn font(&self) -> Font {
|
||||
Font {
|
||||
family: self.font_family.clone(),
|
||||
features: self.font_features,
|
||||
features: self.font_features.clone(),
|
||||
weight: self.font_weight,
|
||||
style: self.font_style,
|
||||
}
|
||||
@@ -628,6 +628,13 @@ impl From<&TextStyle> for HighlightStyle {
|
||||
}
|
||||
|
||||
impl HighlightStyle {
|
||||
/// Create a highlight style with just a color
|
||||
pub fn color(color: Hsla) -> Self {
|
||||
Self {
|
||||
color: Some(color),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
/// Blend this highlight style with another.
|
||||
/// Non-continuous properties, like font_weight and font_style, are overwritten.
|
||||
pub fn highlight(&mut self, other: HighlightStyle) {
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
#[cfg(target_os = "windows")]
|
||||
use crate::SharedString;
|
||||
#[cfg(target_os = "windows")]
|
||||
use itertools::Itertools;
|
||||
use schemars::{
|
||||
schema::{InstanceType, Schema, SchemaObject, SingleOrVec},
|
||||
JsonSchema,
|
||||
@@ -7,10 +11,14 @@ macro_rules! create_definitions {
|
||||
($($(#[$meta:meta])* ($name:ident, $idx:expr)),* $(,)?) => {
|
||||
|
||||
/// The OpenType features that can be configured for a given font.
|
||||
#[derive(Default, Copy, Clone, Eq, PartialEq, Hash)]
|
||||
#[derive(Default, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct FontFeatures {
|
||||
enabled: u64,
|
||||
disabled: u64,
|
||||
#[cfg(target_os = "windows")]
|
||||
other_enabled: SharedString,
|
||||
#[cfg(target_os = "windows")]
|
||||
other_disabled: SharedString,
|
||||
}
|
||||
|
||||
impl FontFeatures {
|
||||
@@ -47,6 +55,14 @@ macro_rules! create_definitions {
|
||||
}
|
||||
}
|
||||
)*
|
||||
{
|
||||
for name in self.other_enabled.as_ref().chars().chunks(4).into_iter() {
|
||||
result.push((name.collect::<String>(), true));
|
||||
}
|
||||
for name in self.other_disabled.as_ref().chars().chunks(4).into_iter() {
|
||||
result.push((name.collect::<String>(), false));
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
@@ -59,6 +75,15 @@ macro_rules! create_definitions {
|
||||
debug.field(stringify!($name), &value);
|
||||
};
|
||||
)*
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
for name in self.other_enabled.as_ref().chars().chunks(4).into_iter() {
|
||||
debug.field(name.collect::<String>().as_str(), &true);
|
||||
}
|
||||
for name in self.other_disabled.as_ref().chars().chunks(4).into_iter() {
|
||||
debug.field(name.collect::<String>().as_str(), &false);
|
||||
}
|
||||
}
|
||||
debug.finish()
|
||||
}
|
||||
}
|
||||
@@ -80,6 +105,7 @@ macro_rules! create_definitions {
|
||||
formatter.write_str("a map of font features")
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
fn visit_map<M>(self, mut access: M) -> Result<Self::Value, M::Error>
|
||||
where
|
||||
M: MapAccess<'de>,
|
||||
@@ -100,6 +126,54 @@ macro_rules! create_definitions {
|
||||
}
|
||||
Ok(FontFeatures { enabled, disabled })
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn visit_map<M>(self, mut access: M) -> Result<Self::Value, M::Error>
|
||||
where
|
||||
M: MapAccess<'de>,
|
||||
{
|
||||
let mut enabled: u64 = 0;
|
||||
let mut disabled: u64 = 0;
|
||||
let mut other_enabled = "".to_owned();
|
||||
let mut other_disabled = "".to_owned();
|
||||
|
||||
while let Some((key, value)) = access.next_entry::<String, Option<bool>>()? {
|
||||
let idx = match key.as_str() {
|
||||
$(stringify!($name) => Some($idx),)*
|
||||
other_feature => {
|
||||
if other_feature.len() != 4 || !other_feature.is_ascii() {
|
||||
log::error!("Incorrect feature name: {}", other_feature);
|
||||
continue;
|
||||
}
|
||||
None
|
||||
},
|
||||
};
|
||||
if let Some(idx) = idx {
|
||||
match value {
|
||||
Some(true) => enabled |= 1 << idx,
|
||||
Some(false) => disabled |= 1 << idx,
|
||||
None => {}
|
||||
};
|
||||
} else {
|
||||
match value {
|
||||
Some(true) => other_enabled.push_str(key.as_str()),
|
||||
Some(false) => other_disabled.push_str(key.as_str()),
|
||||
None => {}
|
||||
};
|
||||
}
|
||||
}
|
||||
let other_enabled = if other_enabled.is_empty() {
|
||||
"".into()
|
||||
} else {
|
||||
other_enabled.into()
|
||||
};
|
||||
let other_disabled = if other_disabled.is_empty() {
|
||||
"".into()
|
||||
} else {
|
||||
other_disabled.into()
|
||||
};
|
||||
Ok(FontFeatures { enabled, disabled, other_enabled, other_disabled })
|
||||
}
|
||||
}
|
||||
|
||||
let features = deserializer.deserialize_map(FontFeaturesVisitor)?;
|
||||
@@ -125,6 +199,16 @@ macro_rules! create_definitions {
|
||||
}
|
||||
)*
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
for name in self.other_enabled.as_ref().chars().chunks(4).into_iter() {
|
||||
map.serialize_entry(name.collect::<String>().as_str(), &true)?;
|
||||
}
|
||||
for name in self.other_disabled.as_ref().chars().chunks(4).into_iter() {
|
||||
map.serialize_entry(name.collect::<String>().as_str(), &false)?;
|
||||
}
|
||||
}
|
||||
|
||||
map.end()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,6 +108,7 @@ fn init_test(cx: &mut gpui::TestAppContext) {
|
||||
release_channel::init("0.0.0", cx);
|
||||
language::init(cx);
|
||||
client::init_settings(cx);
|
||||
workspace::init_settings(cx);
|
||||
Project::init_settings(cx);
|
||||
editor::init_settings(cx);
|
||||
});
|
||||
|
||||
@@ -35,7 +35,7 @@ gpui = { workspace = true, optional = true }
|
||||
live_kit_server = { workspace = true, optional = true }
|
||||
log.workspace = true
|
||||
media.workspace = true
|
||||
nanoid = { version = "0.4", optional = true}
|
||||
nanoid = { workspace = true, optional = true}
|
||||
parking_lot.workspace = true
|
||||
postage.workspace = true
|
||||
|
||||
@@ -47,14 +47,14 @@ async-trait = { workspace = true }
|
||||
collections = { workspace = true }
|
||||
gpui = { workspace = true }
|
||||
live_kit_server.workspace = true
|
||||
nanoid = "0.4"
|
||||
nanoid.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
async-trait.workspace = true
|
||||
collections = { workspace = true, features = ["test-support"] }
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
live_kit_server.workspace = true
|
||||
nanoid = "0.4"
|
||||
nanoid.workspace = true
|
||||
sha2.workspace = true
|
||||
simplelog = "0.9"
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ test-support = []
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
async-recursion.workspace = true
|
||||
collections.workspace = true
|
||||
editor.workspace = true
|
||||
gpui.workspace = true
|
||||
language.workspace = true
|
||||
|
||||
@@ -8,8 +8,7 @@ use std::{fmt::Display, ops::Range, path::PathBuf};
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
pub enum ParsedMarkdownElement {
|
||||
Heading(ParsedMarkdownHeading),
|
||||
/// An ordered or unordered list of items.
|
||||
List(ParsedMarkdownList),
|
||||
ListItem(ParsedMarkdownListItem),
|
||||
Table(ParsedMarkdownTable),
|
||||
BlockQuote(ParsedMarkdownBlockQuote),
|
||||
CodeBlock(ParsedMarkdownCodeBlock),
|
||||
@@ -22,7 +21,7 @@ impl ParsedMarkdownElement {
|
||||
pub fn source_range(&self) -> Range<usize> {
|
||||
match self {
|
||||
Self::Heading(heading) => heading.source_range.clone(),
|
||||
Self::List(list) => list.source_range.clone(),
|
||||
Self::ListItem(list_item) => list_item.source_range.clone(),
|
||||
Self::Table(table) => table.source_range.clone(),
|
||||
Self::BlockQuote(block_quote) => block_quote.source_range.clone(),
|
||||
Self::CodeBlock(code_block) => code_block.source_range.clone(),
|
||||
@@ -30,6 +29,10 @@ impl ParsedMarkdownElement {
|
||||
Self::HorizontalRule(range) => range.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_list_item(&self) -> bool {
|
||||
matches!(self, Self::ListItem(_))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -38,20 +41,14 @@ pub struct ParsedMarkdown {
|
||||
pub children: Vec<ParsedMarkdownElement>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
pub struct ParsedMarkdownList {
|
||||
pub source_range: Range<usize>,
|
||||
pub children: Vec<ParsedMarkdownListItem>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
pub struct ParsedMarkdownListItem {
|
||||
pub source_range: Range<usize>,
|
||||
/// How many indentations deep this item is.
|
||||
pub depth: u16,
|
||||
pub item_type: ParsedMarkdownListItemType,
|
||||
pub contents: Vec<Box<ParsedMarkdownElement>>,
|
||||
pub content: Vec<ParsedMarkdownElement>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -129,7 +126,7 @@ impl ParsedMarkdownTableRow {
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
pub struct ParsedMarkdownBlockQuote {
|
||||
pub source_range: Range<usize>,
|
||||
pub children: Vec<Box<ParsedMarkdownElement>>,
|
||||
pub children: Vec<ParsedMarkdownElement>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::markdown_elements::*;
|
||||
use async_recursion::async_recursion;
|
||||
use collections::FxHashMap;
|
||||
use gpui::FontWeight;
|
||||
use language::LanguageRegistry;
|
||||
use pulldown_cmark::{Alignment, Event, Options, Parser, Tag, TagEnd};
|
||||
@@ -98,20 +99,22 @@ impl<'a> MarkdownParser<'a> {
|
||||
async fn parse_document(mut self) -> Self {
|
||||
while !self.eof() {
|
||||
if let Some(block) = self.parse_block().await {
|
||||
self.parsed.push(block);
|
||||
self.parsed.extend(block);
|
||||
}
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
async fn parse_block(&mut self) -> Option<ParsedMarkdownElement> {
|
||||
#[async_recursion]
|
||||
async fn parse_block(&mut self) -> Option<Vec<ParsedMarkdownElement>> {
|
||||
let (current, source_range) = self.current().unwrap();
|
||||
let source_range = source_range.clone();
|
||||
match current {
|
||||
Event::Start(tag) => match tag {
|
||||
Tag::Paragraph => {
|
||||
self.cursor += 1;
|
||||
let text = self.parse_text(false);
|
||||
Some(ParsedMarkdownElement::Paragraph(text))
|
||||
let text = self.parse_text(false, Some(source_range));
|
||||
Some(vec![ParsedMarkdownElement::Paragraph(text)])
|
||||
}
|
||||
Tag::Heading {
|
||||
level,
|
||||
@@ -122,24 +125,24 @@ impl<'a> MarkdownParser<'a> {
|
||||
let level = *level;
|
||||
self.cursor += 1;
|
||||
let heading = self.parse_heading(level);
|
||||
Some(ParsedMarkdownElement::Heading(heading))
|
||||
Some(vec![ParsedMarkdownElement::Heading(heading)])
|
||||
}
|
||||
Tag::Table(alignment) => {
|
||||
let alignment = alignment.clone();
|
||||
self.cursor += 1;
|
||||
let table = self.parse_table(alignment);
|
||||
Some(ParsedMarkdownElement::Table(table))
|
||||
Some(vec![ParsedMarkdownElement::Table(table)])
|
||||
}
|
||||
Tag::List(order) => {
|
||||
let order = *order;
|
||||
self.cursor += 1;
|
||||
let list = self.parse_list(1, order).await;
|
||||
Some(ParsedMarkdownElement::List(list))
|
||||
let list = self.parse_list(order).await;
|
||||
Some(list)
|
||||
}
|
||||
Tag::BlockQuote => {
|
||||
self.cursor += 1;
|
||||
let block_quote = self.parse_block_quote().await;
|
||||
Some(ParsedMarkdownElement::BlockQuote(block_quote))
|
||||
Some(vec![ParsedMarkdownElement::BlockQuote(block_quote)])
|
||||
}
|
||||
Tag::CodeBlock(kind) => {
|
||||
let language = match kind {
|
||||
@@ -156,7 +159,7 @@ impl<'a> MarkdownParser<'a> {
|
||||
self.cursor += 1;
|
||||
|
||||
let code_block = self.parse_code_block(language).await;
|
||||
Some(ParsedMarkdownElement::CodeBlock(code_block))
|
||||
Some(vec![ParsedMarkdownElement::CodeBlock(code_block)])
|
||||
}
|
||||
_ => {
|
||||
self.cursor += 1;
|
||||
@@ -166,7 +169,7 @@ impl<'a> MarkdownParser<'a> {
|
||||
Event::Rule => {
|
||||
let source_range = source_range.clone();
|
||||
self.cursor += 1;
|
||||
Some(ParsedMarkdownElement::HorizontalRule(source_range))
|
||||
Some(vec![ParsedMarkdownElement::HorizontalRule(source_range)])
|
||||
}
|
||||
_ => {
|
||||
self.cursor += 1;
|
||||
@@ -175,9 +178,16 @@ impl<'a> MarkdownParser<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_text(&mut self, should_complete_on_soft_break: bool) -> ParsedMarkdownText {
|
||||
let (_current, source_range) = self.previous().unwrap();
|
||||
let source_range = source_range.clone();
|
||||
fn parse_text(
|
||||
&mut self,
|
||||
should_complete_on_soft_break: bool,
|
||||
source_range: Option<Range<usize>>,
|
||||
) -> ParsedMarkdownText {
|
||||
let source_range = source_range.unwrap_or_else(|| {
|
||||
self.current()
|
||||
.map(|(_, range)| range.clone())
|
||||
.unwrap_or_default()
|
||||
});
|
||||
|
||||
let mut text = String::new();
|
||||
let mut bold_depth = 0;
|
||||
@@ -379,7 +389,7 @@ impl<'a> MarkdownParser<'a> {
|
||||
fn parse_heading(&mut self, level: pulldown_cmark::HeadingLevel) -> ParsedMarkdownHeading {
|
||||
let (_event, source_range) = self.previous().unwrap();
|
||||
let source_range = source_range.clone();
|
||||
let text = self.parse_text(true);
|
||||
let text = self.parse_text(true, None);
|
||||
|
||||
// Advance past the heading end tag
|
||||
self.cursor += 1;
|
||||
@@ -415,7 +425,8 @@ impl<'a> MarkdownParser<'a> {
|
||||
break;
|
||||
}
|
||||
|
||||
let (current, _source_range) = self.current().unwrap();
|
||||
let (current, source_range) = self.current().unwrap();
|
||||
let source_range = source_range.clone();
|
||||
match current {
|
||||
Event::Start(Tag::TableHead)
|
||||
| Event::Start(Tag::TableRow)
|
||||
@@ -424,7 +435,7 @@ impl<'a> MarkdownParser<'a> {
|
||||
}
|
||||
Event::Start(Tag::TableCell) => {
|
||||
self.cursor += 1;
|
||||
let cell_contents = self.parse_text(false);
|
||||
let cell_contents = self.parse_text(false, Some(source_range));
|
||||
current_row.push(cell_contents);
|
||||
}
|
||||
Event::End(TagEnd::TableHead) | Event::End(TagEnd::TableRow) => {
|
||||
@@ -465,35 +476,53 @@ impl<'a> MarkdownParser<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
#[async_recursion]
|
||||
async fn parse_list(&mut self, depth: u16, order: Option<u64>) -> ParsedMarkdownList {
|
||||
let (_event, source_range) = self.previous().unwrap();
|
||||
let source_range = source_range.clone();
|
||||
let mut children = vec![];
|
||||
let mut inside_list_item = false;
|
||||
let mut order = order;
|
||||
let mut task_item = None;
|
||||
async fn parse_list(&mut self, order: Option<u64>) -> Vec<ParsedMarkdownElement> {
|
||||
let (_, list_source_range) = self.previous().unwrap();
|
||||
|
||||
let mut current_list_items: Vec<Box<ParsedMarkdownElement>> = vec![];
|
||||
let mut items = Vec::new();
|
||||
let mut items_stack = vec![Vec::new()];
|
||||
let mut depth = 1;
|
||||
let mut task_item = None;
|
||||
let mut order = order;
|
||||
let mut order_stack = Vec::new();
|
||||
|
||||
let mut insertion_indices = FxHashMap::default();
|
||||
let mut source_ranges = FxHashMap::default();
|
||||
let mut start_item_range = list_source_range.clone();
|
||||
|
||||
while !self.eof() {
|
||||
let (current, _source_range) = self.current().unwrap();
|
||||
let (current, source_range) = self.current().unwrap();
|
||||
match current {
|
||||
Event::Start(Tag::List(order)) => {
|
||||
let order = *order;
|
||||
self.cursor += 1;
|
||||
Event::Start(Tag::List(new_order)) => {
|
||||
if items_stack.last().is_some() && !insertion_indices.contains_key(&depth) {
|
||||
insertion_indices.insert(depth, items.len());
|
||||
}
|
||||
|
||||
let inner_list = self.parse_list(depth + 1, order).await;
|
||||
let block = ParsedMarkdownElement::List(inner_list);
|
||||
current_list_items.push(Box::new(block));
|
||||
// We will use the start of the nested list as the end for the current item's range,
|
||||
// because we don't care about the hierarchy of list items
|
||||
if !source_ranges.contains_key(&depth) {
|
||||
source_ranges.insert(depth, start_item_range.start..source_range.start);
|
||||
}
|
||||
|
||||
order_stack.push(order);
|
||||
order = *new_order;
|
||||
self.cursor += 1;
|
||||
depth += 1;
|
||||
}
|
||||
Event::End(TagEnd::List(_)) => {
|
||||
order = order_stack.pop().flatten();
|
||||
self.cursor += 1;
|
||||
break;
|
||||
depth -= 1;
|
||||
|
||||
if depth == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Event::Start(Tag::Item) => {
|
||||
start_item_range = source_range.clone();
|
||||
|
||||
self.cursor += 1;
|
||||
inside_list_item = true;
|
||||
items_stack.push(Vec::new());
|
||||
|
||||
// Check for task list marker (`- [ ]` or `- [x]`)
|
||||
if let Some(event) = self.current_event() {
|
||||
@@ -508,17 +537,21 @@ impl<'a> MarkdownParser<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(event) = self.current_event() {
|
||||
if let Some((event, range)) = self.current() {
|
||||
// This is a plain list item.
|
||||
// For example `- some text` or `1. [Docs](./docs.md)`
|
||||
if MarkdownParser::is_text_like(event) {
|
||||
let text = self.parse_text(false);
|
||||
let text = self.parse_text(false, Some(range.clone()));
|
||||
let block = ParsedMarkdownElement::Paragraph(text);
|
||||
current_list_items.push(Box::new(block));
|
||||
if let Some(content) = items_stack.last_mut() {
|
||||
content.push(block);
|
||||
}
|
||||
} else {
|
||||
let block = self.parse_block().await;
|
||||
if let Some(block) = block {
|
||||
current_list_items.push(Box::new(block));
|
||||
if let Some(content) = items_stack.last_mut() {
|
||||
content.extend(block);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -543,34 +576,55 @@ impl<'a> MarkdownParser<'a> {
|
||||
order = Some(current + 1);
|
||||
}
|
||||
|
||||
let contents = std::mem::replace(&mut current_list_items, vec![]);
|
||||
if let Some(content) = items_stack.pop() {
|
||||
let source_range = source_ranges
|
||||
.remove(&depth)
|
||||
.unwrap_or(start_item_range.clone());
|
||||
|
||||
children.push(ParsedMarkdownListItem {
|
||||
contents,
|
||||
depth,
|
||||
item_type,
|
||||
});
|
||||
// We need to remove the last character of the source range, because it includes the newline character
|
||||
let source_range = source_range.start..source_range.end - 1;
|
||||
let item = ParsedMarkdownElement::ListItem(ParsedMarkdownListItem {
|
||||
source_range,
|
||||
content,
|
||||
depth,
|
||||
item_type,
|
||||
});
|
||||
|
||||
if let Some(index) = insertion_indices.get(&depth) {
|
||||
items.insert(*index, item);
|
||||
insertion_indices.remove(&depth);
|
||||
} else {
|
||||
items.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
inside_list_item = false;
|
||||
task_item = None;
|
||||
}
|
||||
_ => {
|
||||
if !inside_list_item {
|
||||
if depth == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
// This can only happen if a list item starts with more then one paragraph,
|
||||
// or the list item contains blocks that should be rendered after the nested list items
|
||||
let block = self.parse_block().await;
|
||||
if let Some(block) = block {
|
||||
current_list_items.push(Box::new(block));
|
||||
if let Some(items_stack) = items_stack.last_mut() {
|
||||
// If we did not insert any nested items yet (in this case insertion index is set), we can append the block to the current list item
|
||||
if !insertion_indices.contains_key(&depth) {
|
||||
items_stack.extend(block);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise we need to insert the block after all the nested items
|
||||
// that have been parsed so far
|
||||
items.extend(block);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ParsedMarkdownList {
|
||||
source_range,
|
||||
children,
|
||||
}
|
||||
items
|
||||
}
|
||||
|
||||
#[async_recursion]
|
||||
@@ -579,13 +633,13 @@ impl<'a> MarkdownParser<'a> {
|
||||
let source_range = source_range.clone();
|
||||
let mut nested_depth = 1;
|
||||
|
||||
let mut children: Vec<Box<ParsedMarkdownElement>> = vec![];
|
||||
let mut children: Vec<ParsedMarkdownElement> = vec![];
|
||||
|
||||
while !self.eof() {
|
||||
let block = self.parse_block().await;
|
||||
|
||||
if let Some(block) = block {
|
||||
children.push(Box::new(block));
|
||||
children.extend(block);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
@@ -674,7 +728,6 @@ mod tests {
|
||||
use language::{tree_sitter_rust, HighlightId, Language, LanguageConfig, LanguageMatcher};
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use ParsedMarkdownElement::*;
|
||||
use ParsedMarkdownListItemType::*;
|
||||
|
||||
async fn parse(input: &str) -> ParsedMarkdown {
|
||||
@@ -688,9 +741,9 @@ mod tests {
|
||||
assert_eq!(
|
||||
parsed.children,
|
||||
vec![
|
||||
h1(text("Heading one", 0..14), 0..14),
|
||||
h2(text("Heading two", 14..29), 14..29),
|
||||
h3(text("Heading three", 29..46), 29..46),
|
||||
h1(text("Heading one", 2..13), 0..14),
|
||||
h2(text("Heading two", 17..28), 14..29),
|
||||
h3(text("Heading three", 33..46), 29..46),
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -711,7 +764,7 @@ mod tests {
|
||||
|
||||
assert_eq!(
|
||||
parsed.children,
|
||||
vec![h1(text("Zed", 0..6), 0..6), p("The editor", 6..16),]
|
||||
vec![h1(text("Zed", 2..5), 0..6), p("The editor", 6..16),]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -881,14 +934,11 @@ Some other content
|
||||
|
||||
assert_eq!(
|
||||
parsed.children,
|
||||
vec![list(
|
||||
vec![
|
||||
list_item(1, Unordered, vec![p("Item 1", 0..9)]),
|
||||
list_item(1, Unordered, vec![p("Item 2", 9..18)]),
|
||||
list_item(1, Unordered, vec![p("Item 3", 18..27)]),
|
||||
],
|
||||
0..27
|
||||
),]
|
||||
vec![
|
||||
list_item(0..8, 1, Unordered, vec![p("Item 1", 2..8)]),
|
||||
list_item(9..17, 1, Unordered, vec![p("Item 2", 11..17)]),
|
||||
list_item(18..26, 1, Unordered, vec![p("Item 3", 20..26)]),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -904,13 +954,10 @@ Some other content
|
||||
|
||||
assert_eq!(
|
||||
parsed.children,
|
||||
vec![list(
|
||||
vec![
|
||||
list_item(1, Task(false, 2..5), vec![p("TODO", 2..5)]),
|
||||
list_item(1, Task(true, 13..16), vec![p("Checked", 13..16)]),
|
||||
],
|
||||
0..25
|
||||
),]
|
||||
vec![
|
||||
list_item(0..10, 1, Task(false, 2..5), vec![p("TODO", 6..10)]),
|
||||
list_item(11..24, 1, Task(true, 13..16), vec![p("Checked", 17..24)]),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -927,13 +974,10 @@ Some other content
|
||||
|
||||
assert_eq!(
|
||||
parsed.children,
|
||||
vec![list(
|
||||
vec![
|
||||
list_item(1, Task(false, 2..5), vec![p("Task 1", 2..5)]),
|
||||
list_item(1, Task(true, 16..19), vec![p("Task 2", 16..19)]),
|
||||
],
|
||||
0..27
|
||||
),]
|
||||
vec![
|
||||
list_item(0..13, 1, Task(false, 2..5), vec![p("Task 1", 6..12)]),
|
||||
list_item(14..26, 1, Task(true, 16..19), vec![p("Task 2", 20..26)]),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -965,84 +1009,21 @@ Some other content
|
||||
assert_eq!(
|
||||
parsed.children,
|
||||
vec![
|
||||
list(
|
||||
vec![
|
||||
list_item(1, Unordered, vec![p("Item 1", 0..9)]),
|
||||
list_item(1, Unordered, vec![p("Item 2", 9..18)]),
|
||||
list_item(1, Unordered, vec![p("Item 3", 18..28)]),
|
||||
],
|
||||
0..28
|
||||
),
|
||||
list(
|
||||
vec![
|
||||
list_item(1, Ordered(1), vec![p("Hello", 28..37)]),
|
||||
list_item(
|
||||
1,
|
||||
Ordered(2),
|
||||
vec![
|
||||
p("Two", 37..56),
|
||||
list(
|
||||
vec![list_item(2, Ordered(1), vec![p("Three", 47..56)]),],
|
||||
47..56
|
||||
),
|
||||
]
|
||||
),
|
||||
list_item(1, Ordered(3), vec![p("Four", 56..64)]),
|
||||
list_item(1, Ordered(4), vec![p("Five", 64..73)]),
|
||||
],
|
||||
28..73
|
||||
),
|
||||
list(
|
||||
vec![
|
||||
list_item(
|
||||
1,
|
||||
Unordered,
|
||||
vec![
|
||||
p("First", 73..155),
|
||||
list(
|
||||
vec![
|
||||
list_item(
|
||||
2,
|
||||
Ordered(1),
|
||||
vec![
|
||||
p("Hello", 83..141),
|
||||
list(
|
||||
vec![list_item(
|
||||
3,
|
||||
Ordered(1),
|
||||
vec![
|
||||
p("Goodbyte", 97..141),
|
||||
list(
|
||||
vec![
|
||||
list_item(
|
||||
4,
|
||||
Unordered,
|
||||
vec![p("Inner", 117..125)]
|
||||
),
|
||||
list_item(
|
||||
4,
|
||||
Unordered,
|
||||
vec![p("Inner", 133..141)]
|
||||
),
|
||||
],
|
||||
117..141
|
||||
)
|
||||
]
|
||||
),],
|
||||
97..141
|
||||
)
|
||||
]
|
||||
),
|
||||
list_item(2, Ordered(2), vec![p("Goodbyte", 143..155)]),
|
||||
],
|
||||
83..155
|
||||
)
|
||||
]
|
||||
),
|
||||
list_item(1, Unordered, vec![p("Last", 155..162)]),
|
||||
],
|
||||
73..162
|
||||
),
|
||||
list_item(0..8, 1, Unordered, vec![p("Item 1", 2..8)]),
|
||||
list_item(9..17, 1, Unordered, vec![p("Item 2", 11..17)]),
|
||||
list_item(18..27, 1, Unordered, vec![p("Item 3", 20..26)]),
|
||||
list_item(28..36, 1, Ordered(1), vec![p("Hello", 31..36)]),
|
||||
list_item(37..46, 1, Ordered(2), vec![p("Two", 40..43),]),
|
||||
list_item(47..55, 2, Ordered(1), vec![p("Three", 50..55)]),
|
||||
list_item(56..63, 1, Ordered(3), vec![p("Four", 59..63)]),
|
||||
list_item(64..72, 1, Ordered(4), vec![p("Five", 67..71)]),
|
||||
list_item(73..82, 1, Unordered, vec![p("First", 75..80)]),
|
||||
list_item(83..96, 2, Ordered(1), vec![p("Hello", 86..91)]),
|
||||
list_item(97..116, 3, Ordered(1), vec![p("Goodbyte", 100..108)]),
|
||||
list_item(117..124, 4, Unordered, vec![p("Inner", 119..124)]),
|
||||
list_item(133..140, 4, Unordered, vec![p("Inner", 135..140)]),
|
||||
list_item(143..154, 2, Ordered(2), vec![p("Goodbyte", 146..154)]),
|
||||
list_item(155..161, 1, Unordered, vec![p("Last", 157..161)]),
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -1053,23 +1034,49 @@ Some other content
|
||||
"\
|
||||
* This is a list item with two paragraphs.
|
||||
|
||||
This is the second paragraph in the list item.",
|
||||
This is the second paragraph in the list item.
|
||||
",
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
parsed.children,
|
||||
vec![list(
|
||||
vec![list_item(
|
||||
1,
|
||||
Unordered,
|
||||
vec![
|
||||
p("This is a list item with two paragraphs.", 4..45),
|
||||
p("This is the second paragraph in the list item.", 50..96)
|
||||
],
|
||||
),],
|
||||
vec![list_item(
|
||||
0..96,
|
||||
),]
|
||||
1,
|
||||
Unordered,
|
||||
vec![
|
||||
p("This is a list item with two paragraphs.", 4..44),
|
||||
p("This is the second paragraph in the list item.", 50..97)
|
||||
],
|
||||
),],
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_nested_list_with_paragraph_inside() {
|
||||
let parsed = parse(
|
||||
"\
|
||||
1. a
|
||||
1. b
|
||||
1. c
|
||||
|
||||
text
|
||||
|
||||
1. d
|
||||
",
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
parsed.children,
|
||||
vec![
|
||||
list_item(0..7, 1, Ordered(1), vec![p("a", 3..4)],),
|
||||
list_item(8..20, 2, Ordered(1), vec![p("b", 12..13),],),
|
||||
list_item(21..27, 3, Ordered(1), vec![p("c", 25..26),],),
|
||||
p("text", 32..37),
|
||||
list_item(41..46, 2, Ordered(1), vec![p("d", 45..46),],),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1086,14 +1093,11 @@ Some other content
|
||||
|
||||
assert_eq!(
|
||||
parsed.children,
|
||||
vec![list(
|
||||
vec![
|
||||
list_item(1, Unordered, vec![p("code", 0..9)],),
|
||||
list_item(1, Unordered, vec![p("bold", 9..20)]),
|
||||
list_item(1, Unordered, vec![p("link", 20..50)],)
|
||||
],
|
||||
0..50,
|
||||
),]
|
||||
vec![
|
||||
list_item(0..8, 1, Unordered, vec![p("code", 2..8)]),
|
||||
list_item(9..19, 1, Unordered, vec![p("bold", 11..19)]),
|
||||
list_item(20..49, 1, Unordered, vec![p("link", 22..49)],)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1127,7 +1131,7 @@ Some other content
|
||||
parsed.children,
|
||||
vec![block_quote(
|
||||
vec![
|
||||
h1(text("Heading", 2..12), 2..12),
|
||||
h1(text("Heading", 4..11), 2..12),
|
||||
p("More text", 14..26),
|
||||
p("More text", 30..40)
|
||||
],
|
||||
@@ -1157,7 +1161,7 @@ More text
|
||||
block_quote(
|
||||
vec![
|
||||
p("A", 2..4),
|
||||
block_quote(vec![h1(text("B", 10..14), 10..14)], 8..14),
|
||||
block_quote(vec![h1(text("B", 12..13), 10..14)], 8..14),
|
||||
p("C", 18..20)
|
||||
],
|
||||
0..20
|
||||
@@ -1279,7 +1283,7 @@ fn main() {
|
||||
) -> ParsedMarkdownElement {
|
||||
ParsedMarkdownElement::BlockQuote(ParsedMarkdownBlockQuote {
|
||||
source_range,
|
||||
children: children.into_iter().map(Box::new).collect(),
|
||||
children,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1297,26 +1301,18 @@ fn main() {
|
||||
})
|
||||
}
|
||||
|
||||
fn list(
|
||||
children: Vec<ParsedMarkdownListItem>,
|
||||
source_range: Range<usize>,
|
||||
) -> ParsedMarkdownElement {
|
||||
List(ParsedMarkdownList {
|
||||
source_range,
|
||||
children,
|
||||
})
|
||||
}
|
||||
|
||||
fn list_item(
|
||||
source_range: Range<usize>,
|
||||
depth: u16,
|
||||
item_type: ParsedMarkdownListItemType,
|
||||
contents: Vec<ParsedMarkdownElement>,
|
||||
) -> ParsedMarkdownListItem {
|
||||
ParsedMarkdownListItem {
|
||||
content: Vec<ParsedMarkdownElement>,
|
||||
) -> ParsedMarkdownElement {
|
||||
ParsedMarkdownElement::ListItem(ParsedMarkdownListItem {
|
||||
source_range,
|
||||
item_type,
|
||||
depth,
|
||||
contents: contents.into_iter().map(Box::new).collect(),
|
||||
}
|
||||
content,
|
||||
})
|
||||
}
|
||||
|
||||
fn table(
|
||||
|
||||
@@ -15,6 +15,7 @@ use ui::prelude::*;
|
||||
use workspace::item::{Item, ItemHandle, TabContentParams};
|
||||
use workspace::{Pane, Workspace};
|
||||
|
||||
use crate::markdown_elements::ParsedMarkdownElement;
|
||||
use crate::OpenPreviewToTheSide;
|
||||
use crate::{
|
||||
markdown_elements::ParsedMarkdown,
|
||||
@@ -180,9 +181,14 @@ impl MarkdownPreviewView {
|
||||
let block = contents.children.get(ix).unwrap();
|
||||
let rendered_block = render_markdown_block(block, &mut render_cx);
|
||||
|
||||
let should_apply_padding = Self::should_apply_padding_between(
|
||||
block,
|
||||
contents.children.get(ix + 1),
|
||||
);
|
||||
|
||||
div()
|
||||
.id(ix)
|
||||
.pb_3()
|
||||
.when(should_apply_padding, |this| this.pb_3())
|
||||
.group("markdown-block")
|
||||
.on_click(cx.listener(move |this, event: &ClickEvent, cx| {
|
||||
if event.down.click_count == 2 {
|
||||
@@ -404,7 +410,7 @@ impl MarkdownPreviewView {
|
||||
let Range { start, end } = block.source_range();
|
||||
|
||||
// Check if the cursor is between the last block and the current block
|
||||
if last_end > cursor && cursor < start {
|
||||
if last_end <= cursor && cursor < start {
|
||||
block_index = Some(i.saturating_sub(1));
|
||||
break;
|
||||
}
|
||||
@@ -423,6 +429,13 @@ impl MarkdownPreviewView {
|
||||
|
||||
block_index.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn should_apply_padding_between(
|
||||
current_block: &ParsedMarkdownElement,
|
||||
next_block: Option<&ParsedMarkdownElement>,
|
||||
) -> bool {
|
||||
!(current_block.is_list_item() && next_block.map(|b| b.is_list_item()).unwrap_or(false))
|
||||
}
|
||||
}
|
||||
|
||||
impl FocusableView for MarkdownPreviewView {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use crate::markdown_elements::{
|
||||
HeadingLevel, Link, ParsedMarkdown, ParsedMarkdownBlockQuote, ParsedMarkdownCodeBlock,
|
||||
ParsedMarkdownElement, ParsedMarkdownHeading, ParsedMarkdownList, ParsedMarkdownListItemType,
|
||||
ParsedMarkdownTable, ParsedMarkdownTableAlignment, ParsedMarkdownTableRow, ParsedMarkdownText,
|
||||
ParsedMarkdownElement, ParsedMarkdownHeading, ParsedMarkdownListItem,
|
||||
ParsedMarkdownListItemType, ParsedMarkdownTable, ParsedMarkdownTableAlignment,
|
||||
ParsedMarkdownTableRow, ParsedMarkdownText,
|
||||
};
|
||||
use gpui::{
|
||||
div, px, rems, AbsoluteLength, AnyElement, DefiniteLength, Div, Element, ElementId,
|
||||
@@ -110,7 +111,7 @@ pub fn render_markdown_block(block: &ParsedMarkdownElement, cx: &mut RenderConte
|
||||
match block {
|
||||
Paragraph(text) => render_markdown_paragraph(text, cx),
|
||||
Heading(heading) => render_markdown_heading(heading, cx),
|
||||
List(list) => render_markdown_list(list, cx),
|
||||
ListItem(list_item) => render_markdown_list_item(list_item, cx),
|
||||
Table(table) => render_markdown_table(table, cx),
|
||||
BlockQuote(block_quote) => render_markdown_block_quote(block_quote, cx),
|
||||
CodeBlock(code_block) => render_markdown_code_block(code_block, cx),
|
||||
@@ -146,79 +147,77 @@ fn render_markdown_heading(parsed: &ParsedMarkdownHeading, cx: &mut RenderContex
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn render_markdown_list(parsed: &ParsedMarkdownList, cx: &mut RenderContext) -> AnyElement {
|
||||
fn render_markdown_list_item(
|
||||
parsed: &ParsedMarkdownListItem,
|
||||
cx: &mut RenderContext,
|
||||
) -> AnyElement {
|
||||
use ParsedMarkdownListItemType::*;
|
||||
|
||||
let mut items = vec![];
|
||||
for item in &parsed.children {
|
||||
let padding = rems((item.depth - 1) as f32 * 0.25);
|
||||
let padding = rems((parsed.depth - 1) as f32);
|
||||
|
||||
let bullet = match &item.item_type {
|
||||
Ordered(order) => format!("{}.", order).into_any_element(),
|
||||
Unordered => "•".into_any_element(),
|
||||
Task(checked, range) => div()
|
||||
.id(cx.next_id(range))
|
||||
.mt(px(3.))
|
||||
.child(
|
||||
Checkbox::new(
|
||||
"checkbox",
|
||||
if *checked {
|
||||
Selection::Selected
|
||||
} else {
|
||||
Selection::Unselected
|
||||
},
|
||||
)
|
||||
.when_some(
|
||||
cx.checkbox_clicked_callback.clone(),
|
||||
|this, callback| {
|
||||
this.on_click({
|
||||
let range = range.clone();
|
||||
move |selection, cx| {
|
||||
let checked = match selection {
|
||||
Selection::Selected => true,
|
||||
Selection::Unselected => false,
|
||||
_ => return,
|
||||
};
|
||||
|
||||
if cx.modifiers().secondary() {
|
||||
callback(checked, range.clone(), cx);
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
),
|
||||
let bullet = match &parsed.item_type {
|
||||
Ordered(order) => format!("{}.", order).into_any_element(),
|
||||
Unordered => "•".into_any_element(),
|
||||
Task(checked, range) => div()
|
||||
.id(cx.next_id(range))
|
||||
.mt(px(3.))
|
||||
.child(
|
||||
Checkbox::new(
|
||||
"checkbox",
|
||||
if *checked {
|
||||
Selection::Selected
|
||||
} else {
|
||||
Selection::Unselected
|
||||
},
|
||||
)
|
||||
.hover(|s| s.cursor_pointer())
|
||||
.tooltip(|cx| {
|
||||
let secondary_modifier = Keystroke {
|
||||
key: "".to_string(),
|
||||
modifiers: Modifiers::secondary_key(),
|
||||
ime_key: None,
|
||||
};
|
||||
Tooltip::text(
|
||||
format!("{}-click to toggle the checkbox", secondary_modifier),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.into_any_element(),
|
||||
};
|
||||
let bullet = div().mr_2().child(bullet);
|
||||
.when_some(
|
||||
cx.checkbox_clicked_callback.clone(),
|
||||
|this, callback| {
|
||||
this.on_click({
|
||||
let range = range.clone();
|
||||
move |selection, cx| {
|
||||
let checked = match selection {
|
||||
Selection::Selected => true,
|
||||
Selection::Unselected => false,
|
||||
_ => return,
|
||||
};
|
||||
|
||||
let contents: Vec<AnyElement> = item
|
||||
.contents
|
||||
.iter()
|
||||
.map(|c| render_markdown_block(c.as_ref(), cx))
|
||||
.collect();
|
||||
if cx.modifiers().secondary() {
|
||||
callback(checked, range.clone(), cx);
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
),
|
||||
)
|
||||
.hover(|s| s.cursor_pointer())
|
||||
.tooltip(|cx| {
|
||||
let secondary_modifier = Keystroke {
|
||||
key: "".to_string(),
|
||||
modifiers: Modifiers::secondary_key(),
|
||||
ime_key: None,
|
||||
};
|
||||
Tooltip::text(
|
||||
format!("{}-click to toggle the checkbox", secondary_modifier),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.into_any_element(),
|
||||
};
|
||||
let bullet = div().mr_2().child(bullet);
|
||||
|
||||
let item = h_flex()
|
||||
.pl(DefiniteLength::Absolute(AbsoluteLength::Rems(padding)))
|
||||
.items_start()
|
||||
.children(vec![bullet, div().children(contents).pr_4().w_full()]);
|
||||
let contents: Vec<AnyElement> = parsed
|
||||
.content
|
||||
.iter()
|
||||
.map(|c| render_markdown_block(c, cx))
|
||||
.collect();
|
||||
|
||||
items.push(item);
|
||||
}
|
||||
let item = h_flex()
|
||||
.pl(DefiniteLength::Absolute(AbsoluteLength::Rems(padding)))
|
||||
.items_start()
|
||||
.children(vec![bullet, div().children(contents).pr_4().w_full()]);
|
||||
|
||||
cx.with_common_p(div()).children(items).into_any()
|
||||
cx.with_common_p(item).into_any()
|
||||
}
|
||||
|
||||
fn render_markdown_table(parsed: &ParsedMarkdownTable, cx: &mut RenderContext) -> AnyElement {
|
||||
|
||||
@@ -274,7 +274,7 @@ impl PickerDelegate for OutlineViewDelegate {
|
||||
let text_style = TextStyle {
|
||||
color: cx.theme().colors().text,
|
||||
font_family: settings.buffer_font.family.clone(),
|
||||
font_features: settings.buffer_font.features,
|
||||
font_features: settings.buffer_font.features.clone(),
|
||||
font_size: settings.buffer_font_size(cx).into(),
|
||||
font_weight: FontWeight::NORMAL,
|
||||
font_style: FontStyle::Normal,
|
||||
|
||||
@@ -79,11 +79,18 @@ pub trait PickerDelegate: Sized + 'static {
|
||||
false
|
||||
}
|
||||
|
||||
fn confirm_update_query(&mut self, _cx: &mut ViewContext<Picker<Self>>) -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
fn confirm(&mut self, secondary: bool, cx: &mut ViewContext<Picker<Self>>);
|
||||
/// Instead of interacting with currently selected entry, treats editor input literally,
|
||||
/// performing some kind of action on it.
|
||||
fn confirm_input(&mut self, _secondary: bool, _: &mut ViewContext<Picker<Self>>) {}
|
||||
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>);
|
||||
fn should_dismiss(&self) -> bool {
|
||||
true
|
||||
}
|
||||
fn selected_as_query(&self) -> Option<String> {
|
||||
None
|
||||
}
|
||||
@@ -267,8 +274,10 @@ impl<D: PickerDelegate> Picker<D> {
|
||||
}
|
||||
|
||||
pub fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
|
||||
self.delegate.dismissed(cx);
|
||||
cx.emit(DismissEvent);
|
||||
if self.delegate.should_dismiss() {
|
||||
self.delegate.dismissed(cx);
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
|
||||
@@ -280,7 +289,7 @@ impl<D: PickerDelegate> Picker<D> {
|
||||
self.confirm_on_update = Some(false)
|
||||
} else {
|
||||
self.pending_update_matches.take();
|
||||
self.delegate.confirm(false, cx);
|
||||
self.do_confirm(false, cx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -292,7 +301,7 @@ impl<D: PickerDelegate> Picker<D> {
|
||||
{
|
||||
self.confirm_on_update = Some(true)
|
||||
} else {
|
||||
self.delegate.confirm(true, cx);
|
||||
self.do_confirm(true, cx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -311,7 +320,16 @@ impl<D: PickerDelegate> Picker<D> {
|
||||
cx.stop_propagation();
|
||||
cx.prevent_default();
|
||||
self.delegate.set_selected_index(ix, cx);
|
||||
self.delegate.confirm(secondary, cx);
|
||||
self.do_confirm(secondary, cx)
|
||||
}
|
||||
|
||||
fn do_confirm(&mut self, secondary: bool, cx: &mut ViewContext<Self>) {
|
||||
if let Some(update_query) = self.delegate.confirm_update_query(cx) {
|
||||
self.set_query(update_query, cx);
|
||||
self.delegate.set_selected_index(0, cx);
|
||||
} else {
|
||||
self.delegate.confirm(secondary, cx)
|
||||
}
|
||||
}
|
||||
|
||||
fn on_input_editor_event(
|
||||
@@ -385,7 +403,7 @@ impl<D: PickerDelegate> Picker<D> {
|
||||
self.scroll_to_item_index(index);
|
||||
self.pending_update_matches = None;
|
||||
if let Some(secondary) = self.confirm_on_update.take() {
|
||||
self.delegate.confirm(secondary, cx);
|
||||
self.do_confirm(secondary, cx);
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ use futures::{
|
||||
stream::FuturesUnordered,
|
||||
AsyncWriteExt, Future, FutureExt, StreamExt, TryFutureExt,
|
||||
};
|
||||
use fuzzy::CharBag;
|
||||
use git::{blame::Blame, repository::GitRepository};
|
||||
use globset::{Glob, GlobSet, GlobSetBuilder};
|
||||
use gpui::{
|
||||
@@ -370,6 +371,22 @@ pub struct ProjectPath {
|
||||
pub path: Arc<Path>,
|
||||
}
|
||||
|
||||
impl ProjectPath {
|
||||
pub fn from_proto(p: proto::ProjectPath) -> Self {
|
||||
Self {
|
||||
worktree_id: WorktreeId::from_proto(p.worktree_id),
|
||||
path: Arc::from(PathBuf::from(p.path)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_proto(&self) -> proto::ProjectPath {
|
||||
proto::ProjectPath {
|
||||
worktree_id: self.worktree_id.to_proto(),
|
||||
path: self.path.to_string_lossy().to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct InlayHint {
|
||||
pub position: language::Anchor,
|
||||
@@ -2189,33 +2206,37 @@ impl Project {
|
||||
let path = file.path.clone();
|
||||
worktree.update(cx, |worktree, cx| match worktree {
|
||||
Worktree::Local(worktree) => worktree.save_buffer(buffer, path, false, cx),
|
||||
Worktree::Remote(worktree) => worktree.save_buffer(buffer, cx),
|
||||
Worktree::Remote(worktree) => worktree.save_buffer(buffer, None, cx),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn save_buffer_as(
|
||||
&mut self,
|
||||
buffer: Model<Buffer>,
|
||||
abs_path: PathBuf,
|
||||
path: ProjectPath,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let worktree_task = self.find_or_create_local_worktree(&abs_path, true, cx);
|
||||
let old_file = File::from_dyn(buffer.read(cx).file())
|
||||
.filter(|f| f.is_local())
|
||||
.cloned();
|
||||
let Some(worktree) = self.worktree_for_id(path.worktree_id, cx) else {
|
||||
return Task::ready(Err(anyhow!("worktree does not exist")));
|
||||
};
|
||||
|
||||
cx.spawn(move |this, mut cx| async move {
|
||||
if let Some(old_file) = &old_file {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.unregister_buffer_from_language_servers(&buffer, old_file, cx);
|
||||
})?;
|
||||
}
|
||||
let (worktree, path) = worktree_task.await?;
|
||||
worktree
|
||||
.update(&mut cx, |worktree, cx| match worktree {
|
||||
Worktree::Local(worktree) => {
|
||||
worktree.save_buffer(buffer.clone(), path.into(), true, cx)
|
||||
worktree.save_buffer(buffer.clone(), path.path, true, cx)
|
||||
}
|
||||
Worktree::Remote(worktree) => {
|
||||
worktree.save_buffer(buffer.clone(), Some(path.to_proto()), cx)
|
||||
}
|
||||
Worktree::Remote(_) => panic!("cannot remote buffers as new files"),
|
||||
})?
|
||||
.await?;
|
||||
|
||||
@@ -2699,7 +2720,6 @@ impl Project {
|
||||
|
||||
for (_, _, server) in self.language_servers_for_worktree(worktree_id) {
|
||||
let text = include_text(server.as_ref()).then(|| buffer.read(cx).text());
|
||||
|
||||
server
|
||||
.notify::<lsp::notification::DidSaveTextDocument>(
|
||||
lsp::DidSaveTextDocumentParams {
|
||||
@@ -2710,46 +2730,8 @@ impl Project {
|
||||
.log_err();
|
||||
}
|
||||
|
||||
let language_server_ids = self.language_server_ids_for_buffer(buffer.read(cx), cx);
|
||||
for language_server_id in language_server_ids {
|
||||
if let Some(LanguageServerState::Running {
|
||||
adapter,
|
||||
simulate_disk_based_diagnostics_completion,
|
||||
..
|
||||
}) = self.language_servers.get_mut(&language_server_id)
|
||||
{
|
||||
// After saving a buffer using a language server that doesn't provide
|
||||
// a disk-based progress token, kick off a timer that will reset every
|
||||
// time the buffer is saved. If the timer eventually fires, simulate
|
||||
// disk-based diagnostics being finished so that other pieces of UI
|
||||
// (e.g., project diagnostics view, diagnostic status bar) can update.
|
||||
// We don't emit an event right away because the language server might take
|
||||
// some time to publish diagnostics.
|
||||
if adapter.disk_based_diagnostics_progress_token.is_none() {
|
||||
const DISK_BASED_DIAGNOSTICS_DEBOUNCE: Duration =
|
||||
Duration::from_secs(1);
|
||||
|
||||
let task = cx.spawn(move |this, mut cx| async move {
|
||||
cx.background_executor().timer(DISK_BASED_DIAGNOSTICS_DEBOUNCE).await;
|
||||
if let Some(this) = this.upgrade() {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.disk_based_diagnostics_finished(
|
||||
language_server_id,
|
||||
cx,
|
||||
);
|
||||
this.enqueue_buffer_ordered_message(
|
||||
BufferOrderedMessage::LanguageServerUpdate {
|
||||
language_server_id,
|
||||
message:proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated(Default::default())
|
||||
},
|
||||
)
|
||||
.ok();
|
||||
}).ok();
|
||||
}
|
||||
});
|
||||
*simulate_disk_based_diagnostics_completion = Some(task);
|
||||
}
|
||||
}
|
||||
for language_server_id in self.language_server_ids_for_buffer(buffer.read(cx), cx) {
|
||||
self.simulate_disk_based_diagnostics_events_if_needed(language_server_id, cx);
|
||||
}
|
||||
}
|
||||
BufferEvent::FileHandleChanged => {
|
||||
@@ -2783,6 +2765,57 @@ impl Project {
|
||||
None
|
||||
}
|
||||
|
||||
// After saving a buffer using a language server that doesn't provide a disk-based progress token,
|
||||
// kick off a timer that will reset every time the buffer is saved. If the timer eventually fires,
|
||||
// simulate disk-based diagnostics being finished so that other pieces of UI (e.g., project
|
||||
// diagnostics view, diagnostic status bar) can update. We don't emit an event right away because
|
||||
// the language server might take some time to publish diagnostics.
|
||||
fn simulate_disk_based_diagnostics_events_if_needed(
|
||||
&mut self,
|
||||
language_server_id: LanguageServerId,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
const DISK_BASED_DIAGNOSTICS_DEBOUNCE: Duration = Duration::from_secs(1);
|
||||
|
||||
let Some(LanguageServerState::Running {
|
||||
simulate_disk_based_diagnostics_completion,
|
||||
adapter,
|
||||
..
|
||||
}) = self.language_servers.get_mut(&language_server_id)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
if adapter.disk_based_diagnostics_progress_token.is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
let prev_task = simulate_disk_based_diagnostics_completion.replace(cx.spawn(
|
||||
move |this, mut cx| async move {
|
||||
cx.background_executor()
|
||||
.timer(DISK_BASED_DIAGNOSTICS_DEBOUNCE)
|
||||
.await;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.disk_based_diagnostics_finished(language_server_id, cx);
|
||||
|
||||
if let Some(LanguageServerState::Running {
|
||||
simulate_disk_based_diagnostics_completion,
|
||||
..
|
||||
}) = this.language_servers.get_mut(&language_server_id)
|
||||
{
|
||||
*simulate_disk_based_diagnostics_completion = None;
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
},
|
||||
));
|
||||
|
||||
if prev_task.is_none() {
|
||||
self.disk_based_diagnostics_started(language_server_id, cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn request_buffer_diff_recalculation(
|
||||
&mut self,
|
||||
buffer: &Model<Buffer>,
|
||||
@@ -4041,13 +4074,7 @@ impl Project {
|
||||
match progress {
|
||||
lsp::WorkDoneProgress::Begin(report) => {
|
||||
if is_disk_based_diagnostics_progress {
|
||||
language_server_status.has_pending_diagnostic_updates = true;
|
||||
self.disk_based_diagnostics_started(language_server_id, cx);
|
||||
self.enqueue_buffer_ordered_message(BufferOrderedMessage::LanguageServerUpdate {
|
||||
language_server_id,
|
||||
message: proto::update_language_server::Variant::DiskBasedDiagnosticsUpdating(Default::default())
|
||||
})
|
||||
.ok();
|
||||
} else {
|
||||
self.on_lsp_work_start(
|
||||
language_server_id,
|
||||
@@ -4092,18 +4119,7 @@ impl Project {
|
||||
language_server_status.progress_tokens.remove(&token);
|
||||
|
||||
if is_disk_based_diagnostics_progress {
|
||||
language_server_status.has_pending_diagnostic_updates = false;
|
||||
self.disk_based_diagnostics_finished(language_server_id, cx);
|
||||
self.enqueue_buffer_ordered_message(
|
||||
BufferOrderedMessage::LanguageServerUpdate {
|
||||
language_server_id,
|
||||
message:
|
||||
proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated(
|
||||
Default::default(),
|
||||
),
|
||||
},
|
||||
)
|
||||
.ok();
|
||||
} else {
|
||||
self.on_lsp_work_end(language_server_id, token.clone(), cx);
|
||||
}
|
||||
@@ -7708,13 +7724,7 @@ impl Project {
|
||||
|
||||
pub fn diagnostic_summary(&self, include_ignored: bool, cx: &AppContext) -> DiagnosticSummary {
|
||||
let mut summary = DiagnosticSummary::default();
|
||||
for (_, _, path_summary) in
|
||||
self.diagnostic_summaries(include_ignored, cx)
|
||||
.filter(|(path, _, _)| {
|
||||
let worktree = self.entry_for_path(path, cx).map(|entry| entry.is_ignored);
|
||||
include_ignored || worktree == Some(false)
|
||||
})
|
||||
{
|
||||
for (_, _, path_summary) in self.diagnostic_summaries(include_ignored, cx) {
|
||||
summary.error_count += path_summary.error_count;
|
||||
summary.warning_count += path_summary.warning_count;
|
||||
}
|
||||
@@ -7726,20 +7736,23 @@ impl Project {
|
||||
include_ignored: bool,
|
||||
cx: &'a AppContext,
|
||||
) -> impl Iterator<Item = (ProjectPath, LanguageServerId, DiagnosticSummary)> + 'a {
|
||||
self.visible_worktrees(cx)
|
||||
.flat_map(move |worktree| {
|
||||
let worktree = worktree.read(cx);
|
||||
let worktree_id = worktree.id();
|
||||
worktree
|
||||
.diagnostic_summaries()
|
||||
.map(move |(path, server_id, summary)| {
|
||||
(ProjectPath { worktree_id, path }, server_id, summary)
|
||||
})
|
||||
})
|
||||
.filter(move |(path, _, _)| {
|
||||
let worktree = self.entry_for_path(path, cx).map(|entry| entry.is_ignored);
|
||||
include_ignored || worktree == Some(false)
|
||||
})
|
||||
self.visible_worktrees(cx).flat_map(move |worktree| {
|
||||
let worktree = worktree.read(cx);
|
||||
let worktree_id = worktree.id();
|
||||
worktree
|
||||
.diagnostic_summaries()
|
||||
.filter_map(move |(path, server_id, summary)| {
|
||||
if include_ignored
|
||||
|| worktree
|
||||
.entry_for_path(path.as_ref())
|
||||
.map_or(false, |entry| !entry.is_ignored)
|
||||
{
|
||||
Some((ProjectPath { worktree_id, path }, server_id, summary))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn disk_based_diagnostics_started(
|
||||
@@ -7747,7 +7760,22 @@ impl Project {
|
||||
language_server_id: LanguageServerId,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
if let Some(language_server_status) =
|
||||
self.language_server_statuses.get_mut(&language_server_id)
|
||||
{
|
||||
language_server_status.has_pending_diagnostic_updates = true;
|
||||
}
|
||||
|
||||
cx.emit(Event::DiskBasedDiagnosticsStarted { language_server_id });
|
||||
if self.is_local() {
|
||||
self.enqueue_buffer_ordered_message(BufferOrderedMessage::LanguageServerUpdate {
|
||||
language_server_id,
|
||||
message: proto::update_language_server::Variant::DiskBasedDiagnosticsUpdating(
|
||||
Default::default(),
|
||||
),
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn disk_based_diagnostics_finished(
|
||||
@@ -7755,7 +7783,23 @@ impl Project {
|
||||
language_server_id: LanguageServerId,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
if let Some(language_server_status) =
|
||||
self.language_server_statuses.get_mut(&language_server_id)
|
||||
{
|
||||
language_server_status.has_pending_diagnostic_updates = false;
|
||||
}
|
||||
|
||||
cx.emit(Event::DiskBasedDiagnosticsFinished { language_server_id });
|
||||
|
||||
if self.is_local() {
|
||||
self.enqueue_buffer_ordered_message(BufferOrderedMessage::LanguageServerUpdate {
|
||||
language_server_id,
|
||||
message: proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated(
|
||||
Default::default(),
|
||||
),
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn active_entry(&self) -> Option<ProjectEntryId> {
|
||||
@@ -8653,8 +8697,17 @@ impl Project {
|
||||
.await?;
|
||||
let buffer_id = buffer.update(&mut cx, |buffer, _| buffer.remote_id())?;
|
||||
|
||||
this.update(&mut cx, |this, cx| this.save_buffer(buffer.clone(), cx))?
|
||||
if let Some(new_path) = envelope.payload.new_path {
|
||||
let new_path = ProjectPath::from_proto(new_path);
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.save_buffer_as(buffer.clone(), new_path, cx)
|
||||
})?
|
||||
.await?;
|
||||
} else {
|
||||
this.update(&mut cx, |this, cx| this.save_buffer(buffer.clone(), cx))?
|
||||
.await?;
|
||||
}
|
||||
|
||||
buffer.update(&mut cx, |buffer, _| proto::BufferSaved {
|
||||
project_id,
|
||||
buffer_id: buffer_id.into(),
|
||||
@@ -10391,6 +10444,7 @@ pub struct PathMatchCandidateSet {
|
||||
pub snapshot: Snapshot,
|
||||
pub include_ignored: bool,
|
||||
pub include_root_name: bool,
|
||||
pub directories_only: bool,
|
||||
}
|
||||
|
||||
impl<'a> fuzzy::PathMatchCandidateSet<'a> for PathMatchCandidateSet {
|
||||
@@ -10420,7 +10474,11 @@ impl<'a> fuzzy::PathMatchCandidateSet<'a> for PathMatchCandidateSet {
|
||||
|
||||
fn candidates(&'a self, start: usize) -> Self::Candidates {
|
||||
PathMatchCandidateSetIter {
|
||||
traversal: self.snapshot.files(self.include_ignored, start),
|
||||
traversal: if self.directories_only {
|
||||
self.snapshot.directories(self.include_ignored, start)
|
||||
} else {
|
||||
self.snapshot.files(self.include_ignored, start)
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10433,15 +10491,16 @@ impl<'a> Iterator for PathMatchCandidateSetIter<'a> {
|
||||
type Item = fuzzy::PathMatchCandidate<'a>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.traversal.next().map(|entry| {
|
||||
if let EntryKind::File(char_bag) = entry.kind {
|
||||
fuzzy::PathMatchCandidate {
|
||||
path: &entry.path,
|
||||
char_bag,
|
||||
}
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
self.traversal.next().map(|entry| match entry.kind {
|
||||
EntryKind::Dir => fuzzy::PathMatchCandidate {
|
||||
path: &entry.path,
|
||||
char_bag: CharBag::from_iter(entry.path.to_string_lossy().to_lowercase().chars()),
|
||||
},
|
||||
EntryKind::File(char_bag) => fuzzy::PathMatchCandidate {
|
||||
path: &entry.path,
|
||||
char_bag,
|
||||
},
|
||||
EntryKind::UnloadedDir | EntryKind::PendingDir => unreachable!(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2942,7 +2942,12 @@ async fn test_save_as(cx: &mut gpui::TestAppContext) {
|
||||
});
|
||||
project
|
||||
.update(cx, |project, cx| {
|
||||
project.save_buffer_as(buffer.clone(), "/dir/file1.rs".into(), cx)
|
||||
let worktree_id = project.worktrees().next().unwrap().read(cx).id();
|
||||
let path = ProjectPath {
|
||||
worktree_id,
|
||||
path: Arc::from(Path::new("file1.rs")),
|
||||
};
|
||||
project.save_buffer_as(buffer.clone(), path, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -887,7 +887,7 @@ impl ProjectPanel {
|
||||
|
||||
let answer = (!action.skip_prompt).then(|| {
|
||||
cx.prompt(
|
||||
PromptLevel::Info,
|
||||
PromptLevel::Destructive,
|
||||
&format!("Delete {file_name:?}?"),
|
||||
None,
|
||||
&["Delete", "Cancel"],
|
||||
|
||||
@@ -310,7 +310,6 @@ impl PickerDelegate for RecentProjectsDelegate {
|
||||
workspace.open_workspace_for_paths(false, paths, cx)
|
||||
}
|
||||
}
|
||||
//TODO support opening remote projects in the same window
|
||||
SerializedWorkspaceLocation::Remote(remote_project) => {
|
||||
let store = ::remote_projects::Store::global(cx).read(cx);
|
||||
let Some(project_id) = store
|
||||
@@ -338,12 +337,38 @@ impl PickerDelegate for RecentProjectsDelegate {
|
||||
})
|
||||
};
|
||||
if let Some(app_state) = AppState::global(cx).upgrade() {
|
||||
let task =
|
||||
workspace::join_remote_project(project_id, app_state, cx);
|
||||
cx.spawn(|_, _| async move {
|
||||
task.await?;
|
||||
Ok(())
|
||||
})
|
||||
let handle = if replace_current_window {
|
||||
cx.window_handle().downcast::<Workspace>()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(handle) = handle {
|
||||
cx.spawn(move |workspace, mut cx| async move {
|
||||
let continue_replacing = workspace
|
||||
.update(&mut cx, |workspace, cx| {
|
||||
workspace.
|
||||
prepare_to_close(true, cx)
|
||||
})?
|
||||
.await?;
|
||||
if continue_replacing {
|
||||
workspace
|
||||
.update(&mut cx, |_workspace, cx| {
|
||||
workspace::join_remote_project(project_id, app_state, Some(handle), cx)
|
||||
})?
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
else {
|
||||
let task =
|
||||
workspace::join_remote_project(project_id, app_state, None, cx);
|
||||
cx.spawn(|_, _| async move {
|
||||
task.await?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
} else {
|
||||
Task::ready(Err(anyhow::anyhow!("App state not found")))
|
||||
}
|
||||
|
||||
@@ -216,7 +216,7 @@ impl RemoteProjects {
|
||||
|
||||
fn delete_dev_server(&mut self, id: DevServerId, cx: &mut ViewContext<Self>) {
|
||||
let answer = cx.prompt(
|
||||
gpui::PromptLevel::Info,
|
||||
gpui::PromptLevel::Destructive,
|
||||
"Are you sure?",
|
||||
Some("This will delete the dev server and all of its remote projects."),
|
||||
&["Delete", "Cancel"],
|
||||
@@ -386,7 +386,7 @@ impl RemoteProjects {
|
||||
.on_click(cx.listener(move |_, _, cx| {
|
||||
if let Some(project_id) = project_id {
|
||||
if let Some(app_state) = AppState::global(cx).upgrade() {
|
||||
workspace::join_remote_project(project_id, app_state, cx)
|
||||
workspace::join_remote_project(project_id, app_state, None, cx)
|
||||
.detach_and_prompt_err("Could not join project", cx, |_, _| None)
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -769,6 +769,12 @@ message SaveBuffer {
|
||||
uint64 project_id = 1;
|
||||
uint64 buffer_id = 2;
|
||||
repeated VectorClockEntry version = 3;
|
||||
optional ProjectPath new_path = 4;
|
||||
}
|
||||
|
||||
message ProjectPath {
|
||||
uint64 worktree_id = 1;
|
||||
string path = 2;
|
||||
}
|
||||
|
||||
message BufferSaved {
|
||||
|
||||
@@ -114,7 +114,7 @@ impl BufferSearchBar {
|
||||
color
|
||||
},
|
||||
font_family: settings.buffer_font.family.clone(),
|
||||
font_features: settings.buffer_font.features,
|
||||
font_features: settings.buffer_font.features.clone(),
|
||||
font_size: rems(0.875).into(),
|
||||
font_weight: FontWeight::NORMAL,
|
||||
font_style: FontStyle::Normal,
|
||||
@@ -1106,7 +1106,7 @@ mod tests {
|
||||
let store = settings::SettingsStore::test(cx);
|
||||
cx.set_global(store);
|
||||
editor::init(cx);
|
||||
|
||||
workspace::init_settings(cx);
|
||||
language::init(cx);
|
||||
Project::init_settings(cx);
|
||||
theme::init(theme::LoadThemes::JustBase, cx);
|
||||
|
||||
@@ -19,14 +19,14 @@ use gpui::{
|
||||
WeakModel, WeakView, WhiteSpace, WindowContext,
|
||||
};
|
||||
use menu::Confirm;
|
||||
use project::{search::SearchQuery, search_history::SearchHistoryCursor, Project};
|
||||
use project::{search::SearchQuery, search_history::SearchHistoryCursor, Project, ProjectPath};
|
||||
use settings::Settings;
|
||||
use smol::stream::StreamExt;
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
mem,
|
||||
ops::{Not, Range},
|
||||
path::{Path, PathBuf},
|
||||
path::Path,
|
||||
};
|
||||
use theme::ThemeSettings;
|
||||
use ui::{
|
||||
@@ -439,7 +439,7 @@ impl Item for ProjectSearchView {
|
||||
fn save_as(
|
||||
&mut self,
|
||||
_: Model<Project>,
|
||||
_: PathBuf,
|
||||
_: ProjectPath,
|
||||
_: &mut ViewContext<Self>,
|
||||
) -> Task<anyhow::Result<()>> {
|
||||
unreachable!("save_as should not have been called")
|
||||
@@ -1307,7 +1307,7 @@ impl ProjectSearchBar {
|
||||
cx.theme().colors().text
|
||||
},
|
||||
font_family: settings.buffer_font.family.clone(),
|
||||
font_features: settings.buffer_font.features,
|
||||
font_features: settings.buffer_font.features.clone(),
|
||||
font_size: rems(0.875).into(),
|
||||
font_weight: FontWeight::NORMAL,
|
||||
font_style: FontStyle::Normal,
|
||||
|
||||
@@ -9,8 +9,8 @@ use fs::Fs;
|
||||
use futures::stream::StreamExt;
|
||||
use futures_batch::ChunksTimeoutStreamExt;
|
||||
use gpui::{
|
||||
AppContext, AsyncAppContext, Context, EntityId, EventEmitter, Global, Model, ModelContext,
|
||||
Subscription, Task, WeakModel,
|
||||
AppContext, AsyncAppContext, BorrowAppContext, Context, Entity, EntityId, EventEmitter, Global,
|
||||
Model, ModelContext, Subscription, Task, WeakModel,
|
||||
};
|
||||
use heed::types::{SerdeBincode, Str};
|
||||
use language::LanguageRegistry;
|
||||
@@ -68,6 +68,18 @@ impl SemanticIndex {
|
||||
project: Model<Project>,
|
||||
cx: &mut AppContext,
|
||||
) -> Model<ProjectIndex> {
|
||||
let project_weak = project.downgrade();
|
||||
project.update(cx, move |_, cx| {
|
||||
cx.on_release(move |_, cx| {
|
||||
if cx.has_global::<SemanticIndex>() {
|
||||
cx.update_global::<SemanticIndex, _>(|this, _| {
|
||||
this.project_indices.remove(&project_weak);
|
||||
})
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
});
|
||||
|
||||
self.project_indices
|
||||
.entry(project.downgrade())
|
||||
.or_insert_with(|| {
|
||||
@@ -86,7 +98,7 @@ impl SemanticIndex {
|
||||
|
||||
pub struct ProjectIndex {
|
||||
db_connection: heed::Env,
|
||||
project: Model<Project>,
|
||||
project: WeakModel<Project>,
|
||||
worktree_indices: HashMap<EntityId, WorktreeIndexHandle>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
fs: Arc<dyn Fs>,
|
||||
@@ -116,7 +128,7 @@ impl ProjectIndex {
|
||||
let fs = project.read(cx).fs().clone();
|
||||
let mut this = ProjectIndex {
|
||||
db_connection,
|
||||
project: project.clone(),
|
||||
project: project.downgrade(),
|
||||
worktree_indices: HashMap::default(),
|
||||
language_registry,
|
||||
fs,
|
||||
@@ -143,8 +155,11 @@ impl ProjectIndex {
|
||||
}
|
||||
|
||||
fn update_worktree_indices(&mut self, cx: &mut ModelContext<Self>) {
|
||||
let worktrees = self
|
||||
.project
|
||||
let Some(project) = self.project.upgrade() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let worktrees = project
|
||||
.read(cx)
|
||||
.visible_worktrees(cx)
|
||||
.filter_map(|worktree| {
|
||||
|
||||
@@ -35,8 +35,5 @@ strum = { version = "0.25.0", features = ["derive"] }
|
||||
theme.workspace = true
|
||||
ui = { workspace = true, features = ["stories"] }
|
||||
|
||||
[target.'cfg(target_os = "windows")'.build-dependencies]
|
||||
winresource = "0.1"
|
||||
|
||||
[dev-dependencies]
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -9,16 +9,5 @@ fn main() {
|
||||
{
|
||||
println!("cargo:rustc-link-arg=/stack:{}", 8 * 1024 * 1024);
|
||||
}
|
||||
|
||||
let manifest = std::path::Path::new("../zed/resources/windows/manifest.xml");
|
||||
println!("cargo:rerun-if-changed={}", manifest.display());
|
||||
|
||||
let mut res = winresource::WindowsResource::new();
|
||||
res.set_manifest_file(manifest.to_str().unwrap());
|
||||
|
||||
if let Err(e) = res.compile() {
|
||||
eprintln!("{}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,6 +66,8 @@ pub fn init(cx: &mut AppContext) {
|
||||
cx,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
toggle_modal(workspace, cx);
|
||||
};
|
||||
});
|
||||
},
|
||||
@@ -76,17 +78,19 @@ pub fn init(cx: &mut AppContext) {
|
||||
fn spawn_task_or_modal(workspace: &mut Workspace, action: &Spawn, cx: &mut ViewContext<Workspace>) {
|
||||
match &action.task_name {
|
||||
Some(name) => spawn_task_with_name(name.clone(), cx),
|
||||
None => {
|
||||
let inventory = workspace.project().read(cx).task_inventory().clone();
|
||||
let workspace_handle = workspace.weak_handle();
|
||||
let task_context = task_context(workspace, cx);
|
||||
workspace.toggle_modal(cx, |cx| {
|
||||
TasksModal::new(inventory, task_context, workspace_handle, cx)
|
||||
})
|
||||
}
|
||||
None => toggle_modal(workspace, cx),
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle_modal(workspace: &mut Workspace, cx: &mut ViewContext<'_, Workspace>) {
|
||||
let inventory = workspace.project().read(cx).task_inventory().clone();
|
||||
let workspace_handle = workspace.weak_handle();
|
||||
let task_context = task_context(workspace, cx);
|
||||
workspace.toggle_modal(cx, |cx| {
|
||||
TasksModal::new(inventory, task_context, workspace_handle, cx)
|
||||
})
|
||||
}
|
||||
|
||||
fn spawn_task_with_name(name: String, cx: &mut ViewContext<Workspace>) {
|
||||
cx.spawn(|workspace, mut cx| async move {
|
||||
let did_spawn = workspace
|
||||
|
||||
@@ -62,6 +62,7 @@ pub(crate) struct TasksModalDelegate {
|
||||
inventory: Model<Inventory>,
|
||||
candidates: Option<Vec<(TaskSourceKind, ResolvedTask)>>,
|
||||
last_used_candidate_index: Option<usize>,
|
||||
divider_index: Option<usize>,
|
||||
matches: Vec<StringMatch>,
|
||||
selected_index: usize,
|
||||
workspace: WeakView<Workspace>,
|
||||
@@ -82,6 +83,7 @@ impl TasksModalDelegate {
|
||||
candidates: None,
|
||||
matches: Vec::new(),
|
||||
last_used_candidate_index: None,
|
||||
divider_index: None,
|
||||
selected_index: 0,
|
||||
prompt: String::default(),
|
||||
task_context,
|
||||
@@ -255,7 +257,17 @@ impl PickerDelegate for TasksModalDelegate {
|
||||
.update(&mut cx, |picker, _| {
|
||||
let delegate = &mut picker.delegate;
|
||||
delegate.matches = matches;
|
||||
if let Some(index) = delegate.last_used_candidate_index {
|
||||
delegate.matches.sort_by_key(|m| m.candidate_id > index);
|
||||
}
|
||||
|
||||
delegate.prompt = query;
|
||||
delegate.divider_index = delegate.last_used_candidate_index.and_then(|index| {
|
||||
let index = delegate
|
||||
.matches
|
||||
.partition_point(|matching_task| matching_task.candidate_id <= index);
|
||||
Some(index).and_then(|index| (index != 0).then(|| index - 1))
|
||||
});
|
||||
|
||||
if delegate.matches.is_empty() {
|
||||
delegate.selected_index = 0;
|
||||
@@ -352,7 +364,7 @@ impl PickerDelegate for TasksModalDelegate {
|
||||
})
|
||||
.map(|item| {
|
||||
let item = if matches!(source_kind, TaskSourceKind::UserInput)
|
||||
|| Some(ix) <= self.last_used_candidate_index
|
||||
|| Some(ix) <= self.divider_index
|
||||
{
|
||||
let task_index = hit.candidate_id;
|
||||
let delete_button = div().child(
|
||||
@@ -412,7 +424,7 @@ impl PickerDelegate for TasksModalDelegate {
|
||||
}
|
||||
|
||||
fn separators_after_indices(&self) -> Vec<usize> {
|
||||
if let Some(i) = self.last_used_candidate_index {
|
||||
if let Some(i) = self.divider_index {
|
||||
vec![i]
|
||||
} else {
|
||||
Vec::new()
|
||||
|
||||
@@ -578,7 +578,8 @@ impl Element for TerminalElement {
|
||||
|
||||
let font_features = terminal_settings
|
||||
.font_features
|
||||
.unwrap_or(settings.buffer_font.features);
|
||||
.clone()
|
||||
.unwrap_or(settings.buffer_font.features.clone());
|
||||
|
||||
let line_height = terminal_settings.line_height.value();
|
||||
let font_size = terminal_settings.font_size;
|
||||
|
||||
@@ -325,13 +325,13 @@ impl settings::Settings for ThemeSettings {
|
||||
ui_font_size: defaults.ui_font_size.unwrap().into(),
|
||||
ui_font: Font {
|
||||
family: defaults.ui_font_family.clone().unwrap().into(),
|
||||
features: defaults.ui_font_features.unwrap(),
|
||||
features: defaults.ui_font_features.clone().unwrap(),
|
||||
weight: Default::default(),
|
||||
style: Default::default(),
|
||||
},
|
||||
buffer_font: Font {
|
||||
family: defaults.buffer_font_family.clone().unwrap().into(),
|
||||
features: defaults.buffer_font_features.unwrap(),
|
||||
features: defaults.buffer_font_features.clone().unwrap(),
|
||||
weight: FontWeight::default(),
|
||||
style: FontStyle::default(),
|
||||
},
|
||||
@@ -349,14 +349,14 @@ impl settings::Settings for ThemeSettings {
|
||||
if let Some(value) = value.buffer_font_family.clone() {
|
||||
this.buffer_font.family = value.into();
|
||||
}
|
||||
if let Some(value) = value.buffer_font_features {
|
||||
if let Some(value) = value.buffer_font_features.clone() {
|
||||
this.buffer_font.features = value;
|
||||
}
|
||||
|
||||
if let Some(value) = value.ui_font_family.clone() {
|
||||
this.ui_font.family = value.into();
|
||||
}
|
||||
if let Some(value) = value.ui_font_features {
|
||||
if let Some(value) = value.ui_font_features.clone() {
|
||||
this.ui_font.features = value;
|
||||
}
|
||||
|
||||
|
||||
@@ -50,38 +50,49 @@ impl LabelCommon for HighlightedLabel {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn highlight_ranges(
|
||||
text: &str,
|
||||
indices: &Vec<usize>,
|
||||
style: HighlightStyle,
|
||||
) -> Vec<(Range<usize>, HighlightStyle)> {
|
||||
let mut highlight_indices = indices.iter().copied().peekable();
|
||||
let mut highlights: Vec<(Range<usize>, HighlightStyle)> = Vec::new();
|
||||
|
||||
while let Some(start_ix) = highlight_indices.next() {
|
||||
let mut end_ix = start_ix;
|
||||
|
||||
loop {
|
||||
end_ix = end_ix + text[end_ix..].chars().next().unwrap().len_utf8();
|
||||
if let Some(&next_ix) = highlight_indices.peek() {
|
||||
if next_ix == end_ix {
|
||||
end_ix = next_ix;
|
||||
highlight_indices.next();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
highlights.push((start_ix..end_ix, style));
|
||||
}
|
||||
|
||||
highlights
|
||||
}
|
||||
|
||||
impl RenderOnce for HighlightedLabel {
|
||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||
let highlight_color = cx.theme().colors().text_accent;
|
||||
|
||||
let mut highlight_indices = self.highlight_indices.iter().copied().peekable();
|
||||
let mut highlights: Vec<(Range<usize>, HighlightStyle)> = Vec::new();
|
||||
let highlights = highlight_ranges(
|
||||
&self.label,
|
||||
&self.highlight_indices,
|
||||
HighlightStyle {
|
||||
color: Some(highlight_color),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
while let Some(start_ix) = highlight_indices.next() {
|
||||
let mut end_ix = start_ix;
|
||||
|
||||
loop {
|
||||
end_ix = end_ix + self.label[end_ix..].chars().next().unwrap().len_utf8();
|
||||
if let Some(&next_ix) = highlight_indices.peek() {
|
||||
if next_ix == end_ix {
|
||||
end_ix = next_ix;
|
||||
highlight_indices.next();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
highlights.push((
|
||||
start_ix..end_ix,
|
||||
HighlightStyle {
|
||||
color: Some(highlight_color),
|
||||
..Default::default()
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
let mut text_style = cx.text_style().clone();
|
||||
let mut text_style = cx.text_style();
|
||||
text_style.color = self.base.color.color(cx);
|
||||
|
||||
self.base
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
use std::cmp::Ordering;
|
||||
|
||||
use crate::TabBarPlacement;
|
||||
use crate::{prelude::*, BASE_REM_SIZE_IN_PX};
|
||||
|
||||
use gpui::{AnyElement, IntoElement, Stateful};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use crate::{prelude::*, BASE_REM_SIZE_IN_PX};
|
||||
|
||||
/// The position of a [`Tab`] within a list of tabs.
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||
pub enum TabPosition {
|
||||
@@ -30,6 +31,7 @@ pub enum TabCloseSide {
|
||||
pub struct Tab {
|
||||
div: Stateful<Div>,
|
||||
selected: bool,
|
||||
tab_bar_placement: TabBarPlacement,
|
||||
position: TabPosition,
|
||||
close_side: TabCloseSide,
|
||||
start_slot: Option<AnyElement>,
|
||||
@@ -45,6 +47,7 @@ impl Tab {
|
||||
.id(id.clone())
|
||||
.debug_selector(|| format!("TAB-{}", id)),
|
||||
selected: false,
|
||||
tab_bar_placement: TabBarPlacement::Top,
|
||||
position: TabPosition::First,
|
||||
close_side: TabCloseSide::End,
|
||||
start_slot: None,
|
||||
@@ -57,6 +60,11 @@ impl Tab {
|
||||
|
||||
const CONTENT_HEIGHT_IN_REMS: f32 = 28. / BASE_REM_SIZE_IN_PX;
|
||||
|
||||
pub fn tab_bar_placement(mut self, tab_bar_placement: TabBarPlacement) -> Self {
|
||||
self.tab_bar_placement = tab_bar_placement;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn position(mut self, position: TabPosition) -> Self {
|
||||
self.position = position;
|
||||
self
|
||||
@@ -117,6 +125,9 @@ impl RenderOnce for Tab {
|
||||
),
|
||||
};
|
||||
|
||||
let placement_top = self.tab_bar_placement == TabBarPlacement::Top;
|
||||
let placement_bottom = self.tab_bar_placement == TabBarPlacement::Bottom;
|
||||
|
||||
self.div
|
||||
.h(rems(Self::CONTAINER_HEIGHT_IN_REMS))
|
||||
.bg(tab_bg)
|
||||
@@ -124,21 +135,46 @@ impl RenderOnce for Tab {
|
||||
.map(|this| match self.position {
|
||||
TabPosition::First => {
|
||||
if self.selected {
|
||||
this.pl_px().border_r().pb_px()
|
||||
this.pl_px()
|
||||
.border_r()
|
||||
.when(placement_top, Styled::pb_px)
|
||||
.when(placement_bottom, Styled::pt_px)
|
||||
} else {
|
||||
this.pl_px().pr_px().border_b()
|
||||
this.pl_px()
|
||||
.pr_px()
|
||||
.when(placement_top, Styled::border_b)
|
||||
.when(placement_bottom, Styled::border_t)
|
||||
}
|
||||
}
|
||||
TabPosition::Last => {
|
||||
if self.selected {
|
||||
this.border_l().border_r().pb_px()
|
||||
this.border_l()
|
||||
.border_r()
|
||||
.when(placement_top, Styled::pb_px)
|
||||
.when(placement_bottom, Styled::pt_px)
|
||||
} else {
|
||||
this.pr_px().pl_px().border_b().border_r()
|
||||
this.pr_px()
|
||||
.pl_px()
|
||||
.border_r()
|
||||
.when(placement_top, Styled::border_b)
|
||||
.when(placement_bottom, Styled::border_t)
|
||||
}
|
||||
}
|
||||
TabPosition::Middle(Ordering::Equal) => this.border_l().border_r().pb_px(),
|
||||
TabPosition::Middle(Ordering::Less) => this.border_l().pr_px().border_b(),
|
||||
TabPosition::Middle(Ordering::Greater) => this.border_r().pl_px().border_b(),
|
||||
TabPosition::Middle(Ordering::Equal) => this
|
||||
.border_l()
|
||||
.border_r()
|
||||
.when(placement_top, Styled::pb_px)
|
||||
.when(placement_bottom, Styled::pt_px),
|
||||
TabPosition::Middle(Ordering::Less) => this
|
||||
.border_l()
|
||||
.pr_px()
|
||||
.when(placement_top, Styled::border_b)
|
||||
.when(placement_bottom, Styled::border_t),
|
||||
TabPosition::Middle(Ordering::Greater) => this
|
||||
.border_r()
|
||||
.pl_px()
|
||||
.when(placement_top, Styled::border_b)
|
||||
.when(placement_bottom, Styled::border_t),
|
||||
})
|
||||
.cursor_pointer()
|
||||
.child(
|
||||
|
||||
@@ -3,9 +3,19 @@ use smallvec::SmallVec;
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
/// Placement of the tab bar in relation to the main content area.
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
pub enum TabBarPlacement {
|
||||
/// On top.
|
||||
Top,
|
||||
/// At the bottom.
|
||||
Bottom,
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct TabBar {
|
||||
id: ElementId,
|
||||
placement: TabBarPlacement,
|
||||
start_children: SmallVec<[AnyElement; 2]>,
|
||||
children: SmallVec<[AnyElement; 2]>,
|
||||
end_children: SmallVec<[AnyElement; 2]>,
|
||||
@@ -16,6 +26,7 @@ impl TabBar {
|
||||
pub fn new(id: impl Into<ElementId>) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
placement: TabBarPlacement::Top,
|
||||
start_children: SmallVec::new(),
|
||||
children: SmallVec::new(),
|
||||
end_children: SmallVec::new(),
|
||||
@@ -23,6 +34,11 @@ impl TabBar {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn placement(mut self, placement: TabBarPlacement) -> Self {
|
||||
self.placement = placement;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn track_scroll(mut self, scroll_handle: ScrollHandle) -> Self {
|
||||
self.scroll_handle = Some(scroll_handle);
|
||||
self
|
||||
@@ -90,6 +106,9 @@ impl ParentElement for TabBar {
|
||||
|
||||
impl RenderOnce for TabBar {
|
||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||
let placement_top = self.placement == TabBarPlacement::Top;
|
||||
let placement_bottom = self.placement == TabBarPlacement::Bottom;
|
||||
|
||||
div()
|
||||
.id(self.id)
|
||||
.group("tab_bar")
|
||||
@@ -104,7 +123,8 @@ impl RenderOnce for TabBar {
|
||||
.flex_none()
|
||||
.gap_1()
|
||||
.px_1()
|
||||
.border_b()
|
||||
.when(placement_top, Styled::border_b)
|
||||
.when(placement_bottom, Styled::border_t)
|
||||
.border_r()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.children(self.start_children),
|
||||
@@ -122,7 +142,8 @@ impl RenderOnce for TabBar {
|
||||
.top_0()
|
||||
.left_0()
|
||||
.size_full()
|
||||
.border_b()
|
||||
.when(placement_top, Styled::border_b)
|
||||
.when(placement_bottom, Styled::border_t)
|
||||
.border_color(cx.theme().colors().border),
|
||||
)
|
||||
.child(
|
||||
@@ -142,7 +163,8 @@ impl RenderOnce for TabBar {
|
||||
.flex_none()
|
||||
.gap_1()
|
||||
.px_1()
|
||||
.border_b()
|
||||
.when(placement_top, Styled::border_b)
|
||||
.when(placement_bottom, Styled::border_t)
|
||||
.border_l()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.children(self.end_children),
|
||||
|
||||
@@ -123,7 +123,7 @@ impl Render for TextField {
|
||||
|
||||
let text_style = TextStyle {
|
||||
font_family: settings.buffer_font.family.clone(),
|
||||
font_features: settings.buffer_font.features,
|
||||
font_features: settings.buffer_font.features.clone(),
|
||||
font_size: rems(0.875).into(),
|
||||
font_weight: FontWeight::NORMAL,
|
||||
font_style: FontStyle::Normal,
|
||||
|
||||
@@ -55,7 +55,9 @@ fn blurred(editor: View<Editor>, cx: &mut WindowContext) {
|
||||
}
|
||||
}
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.set_cursor_shape(language::CursorShape::Hollow, cx);
|
||||
if editor.use_modal_editing() {
|
||||
editor.set_cursor_shape(language::CursorShape::Hollow, cx);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -31,48 +31,42 @@ pub fn change_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &m
|
||||
editor.set_clip_at_line_ends(false, cx);
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.move_with(|map, selection| {
|
||||
motion_succeeded |= if let Motion::NextWordStart { ignore_punctuation } = motion
|
||||
{
|
||||
expand_changed_word_selection(
|
||||
map,
|
||||
selection,
|
||||
times,
|
||||
ignore_punctuation,
|
||||
&text_layout_details,
|
||||
false,
|
||||
)
|
||||
} else if let Motion::NextSubwordStart { ignore_punctuation } = motion {
|
||||
expand_changed_word_selection(
|
||||
map,
|
||||
selection,
|
||||
times,
|
||||
ignore_punctuation,
|
||||
&text_layout_details,
|
||||
true,
|
||||
)
|
||||
} else {
|
||||
let result = motion.expand_selection(
|
||||
map,
|
||||
selection,
|
||||
times,
|
||||
false,
|
||||
&text_layout_details,
|
||||
);
|
||||
if let Motion::CurrentLine = motion {
|
||||
let mut start_offset = selection.start.to_offset(map, Bias::Left);
|
||||
let scope = map
|
||||
.buffer_snapshot
|
||||
.language_scope_at(selection.start.to_point(&map));
|
||||
for (ch, offset) in map.buffer_chars_at(start_offset) {
|
||||
if ch == '\n' || char_kind(&scope, ch) != CharKind::Whitespace {
|
||||
break;
|
||||
}
|
||||
start_offset = offset + ch.len_utf8();
|
||||
}
|
||||
selection.start = start_offset.to_display_point(map);
|
||||
motion_succeeded |= match motion {
|
||||
Motion::NextWordStart { ignore_punctuation }
|
||||
| Motion::NextSubwordStart { ignore_punctuation } => {
|
||||
expand_changed_word_selection(
|
||||
map,
|
||||
selection,
|
||||
times,
|
||||
ignore_punctuation,
|
||||
&text_layout_details,
|
||||
motion == Motion::NextSubwordStart { ignore_punctuation },
|
||||
)
|
||||
}
|
||||
result
|
||||
};
|
||||
_ => {
|
||||
let result = motion.expand_selection(
|
||||
map,
|
||||
selection,
|
||||
times,
|
||||
false,
|
||||
&text_layout_details,
|
||||
);
|
||||
if let Motion::CurrentLine = motion {
|
||||
let mut start_offset = selection.start.to_offset(map, Bias::Left);
|
||||
let scope = map
|
||||
.buffer_snapshot
|
||||
.language_scope_at(selection.start.to_point(&map));
|
||||
for (ch, offset) in map.buffer_chars_at(start_offset) {
|
||||
if ch == '\n' || char_kind(&scope, ch) != CharKind::Whitespace {
|
||||
break;
|
||||
}
|
||||
start_offset = offset + ch.len_utf8();
|
||||
}
|
||||
selection.start = start_offset.to_display_point(map);
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
copy_selections_content(vim, editor, motion.linewise(), cx);
|
||||
@@ -116,8 +110,8 @@ pub fn change_object(vim: &mut Vim, object: Object, around: bool, cx: &mut Windo
|
||||
// Special case: "cw" and "cW" are treated like "ce" and "cE" if the cursor is
|
||||
// on a non-blank. This is because "cw" is interpreted as change-word, and a
|
||||
// word does not include the following white space. {Vi: "cw" when on a blank
|
||||
// followed by other blanks changes only the first blank; this is probably a
|
||||
// bug, because "dw" deletes all the blanks}
|
||||
// followed by other blanks changes only the first blank; this is probably a
|
||||
// bug, because "dw" deletes all the blanks}
|
||||
fn expand_changed_word_selection(
|
||||
map: &DisplaySnapshot,
|
||||
selection: &mut Selection<DisplayPoint>,
|
||||
@@ -126,7 +120,7 @@ fn expand_changed_word_selection(
|
||||
text_layout_details: &TextLayoutDetails,
|
||||
use_subword: bool,
|
||||
) -> bool {
|
||||
if times.is_none() || times.unwrap() == 1 {
|
||||
let is_in_word = || {
|
||||
let scope = map
|
||||
.buffer_snapshot
|
||||
.language_scope_at(selection.start.to_point(map));
|
||||
@@ -135,25 +129,28 @@ fn expand_changed_word_selection(
|
||||
.next()
|
||||
.map(|(c, _)| char_kind(&scope, c) != CharKind::Whitespace)
|
||||
.unwrap_or_default();
|
||||
|
||||
if in_word {
|
||||
if use_subword {
|
||||
selection.end =
|
||||
motion::next_subword_end(map, selection.end, ignore_punctuation, 1, false);
|
||||
} else {
|
||||
selection.end =
|
||||
motion::next_word_end(map, selection.end, ignore_punctuation, 1, false);
|
||||
return in_word;
|
||||
};
|
||||
if (times.is_none() || times.unwrap() == 1) && is_in_word() {
|
||||
let next_char = map
|
||||
.buffer_chars_at(
|
||||
motion::next_char(map, selection.end, false).to_offset(map, Bias::Left),
|
||||
)
|
||||
.next();
|
||||
match next_char {
|
||||
Some((' ', _)) => selection.end = motion::next_char(map, selection.end, false),
|
||||
_ => {
|
||||
if use_subword {
|
||||
selection.end =
|
||||
motion::next_subword_end(map, selection.end, ignore_punctuation, 1, false);
|
||||
} else {
|
||||
selection.end =
|
||||
motion::next_word_end(map, selection.end, ignore_punctuation, 1, false);
|
||||
}
|
||||
selection.end = motion::next_char(map, selection.end, false);
|
||||
}
|
||||
selection.end = motion::next_char(map, selection.end, false);
|
||||
true
|
||||
} else {
|
||||
let motion = if use_subword {
|
||||
Motion::NextSubwordStart { ignore_punctuation }
|
||||
} else {
|
||||
Motion::NextWordStart { ignore_punctuation }
|
||||
};
|
||||
motion.expand_selection(map, selection, None, false, &text_layout_details)
|
||||
}
|
||||
true
|
||||
} else {
|
||||
let motion = if use_subword {
|
||||
Motion::NextSubwordStart { ignore_punctuation }
|
||||
@@ -209,6 +206,7 @@ mod test {
|
||||
cx.assert("Teˇst").await;
|
||||
cx.assert("Tˇest test").await;
|
||||
cx.assert("Testˇ test").await;
|
||||
cx.assert("Tesˇt test").await;
|
||||
cx.assert(indoc! {"
|
||||
Test teˇst
|
||||
test"})
|
||||
|
||||
@@ -117,13 +117,16 @@ fn find_number(
|
||||
) -> Option<(Range<Point>, String, u32)> {
|
||||
let mut offset = start.to_offset(snapshot);
|
||||
|
||||
// go backwards to the start of any number the selection is within
|
||||
for ch in snapshot.reversed_chars_at(offset) {
|
||||
if ch.is_ascii_digit() || ch == '-' || ch == 'b' || ch == 'x' {
|
||||
offset -= ch.len_utf8();
|
||||
continue;
|
||||
let ch0 = snapshot.chars_at(offset).next();
|
||||
if ch0.as_ref().is_some_and(char::is_ascii_digit) || matches!(ch0, Some('-' | 'b' | 'x')) {
|
||||
// go backwards to the start of any number the selection is within
|
||||
for ch in snapshot.reversed_chars_at(offset) {
|
||||
if ch.is_ascii_digit() || ch == '-' || ch == 'b' || ch == 'x' {
|
||||
offset -= ch.len_utf8();
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
let mut begin = None;
|
||||
@@ -217,6 +220,48 @@ mod test {
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_increment_with_dot(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.set_shared_state(indoc! {"
|
||||
1ˇ.2
|
||||
"})
|
||||
.await;
|
||||
|
||||
cx.simulate_shared_keystrokes(["ctrl-a"]).await;
|
||||
cx.assert_shared_state(indoc! {"
|
||||
1.ˇ3
|
||||
"})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["ctrl-x"]).await;
|
||||
cx.assert_shared_state(indoc! {"
|
||||
1.ˇ2
|
||||
"})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_increment_with_two_dots(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.set_shared_state(indoc! {"
|
||||
111.ˇ.2
|
||||
"})
|
||||
.await;
|
||||
|
||||
cx.simulate_shared_keystrokes(["ctrl-a"]).await;
|
||||
cx.assert_shared_state(indoc! {"
|
||||
111..ˇ3
|
||||
"})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["ctrl-x"]).await;
|
||||
cx.assert_shared_state(indoc! {"
|
||||
111..ˇ2
|
||||
"})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_increment_radix(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
@@ -159,11 +159,21 @@ fn search_submit(workspace: &mut Workspace, _: &SearchSubmit, cx: &mut ViewConte
|
||||
search_bar.select_match(direction, count, cx);
|
||||
search_bar.focus_editor(&Default::default(), cx);
|
||||
|
||||
let prior_selections = state.prior_selections.drain(..).collect();
|
||||
let mut prior_selections: Vec<_> = state.prior_selections.drain(..).collect();
|
||||
let prior_mode = state.prior_mode;
|
||||
let prior_operator = state.prior_operator.take();
|
||||
let new_selections = vim.editor_selections(cx);
|
||||
|
||||
// If the active editor has changed during a search, don't panic.
|
||||
if prior_selections.iter().any(|s| {
|
||||
vim.update_active_editor(cx, |_vim, editor, cx| {
|
||||
!s.start.is_valid(&editor.snapshot(cx).buffer_snapshot)
|
||||
})
|
||||
.unwrap_or(true)
|
||||
}) {
|
||||
prior_selections.clear();
|
||||
}
|
||||
|
||||
if prior_mode != vim.state().mode {
|
||||
vim.switch_mode(prior_mode, true, cx);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,10 @@
|
||||
{"Key":"c"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"Testˇtest","mode":"Insert"}}
|
||||
{"Put":{"state":"Tesˇt test"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"Tesˇ test","mode":"Insert"}}
|
||||
{"Put":{"state":"Test teˇst\ntest"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"w"}
|
||||
|
||||
5
crates/vim/test_data/test_increment_with_dot.json
Normal file
5
crates/vim/test_data/test_increment_with_dot.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{"Put":{"state":"1ˇ.2\n"}}
|
||||
{"Key":"ctrl-a"}
|
||||
{"Get":{"state":"1.ˇ3\n","mode":"Normal"}}
|
||||
{"Key":"ctrl-x"}
|
||||
{"Get":{"state":"1.ˇ2\n","mode":"Normal"}}
|
||||
5
crates/vim/test_data/test_increment_with_two_dots.json
Normal file
5
crates/vim/test_data/test_increment_with_two_dots.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{"Put":{"state":"111.ˇ.2\n"}}
|
||||
{"Key":"ctrl-a"}
|
||||
{"Get":{"state":"111..ˇ3\n","mode":"Normal"}}
|
||||
{"Key":"ctrl-x"}
|
||||
{"Get":{"state":"111..ˇ2\n","mode":"Normal"}}
|
||||
@@ -2,7 +2,7 @@ use crate::{
|
||||
pane::{self, Pane},
|
||||
persistence::model::ItemId,
|
||||
searchable::SearchableItemHandle,
|
||||
workspace_settings::{AutosaveSetting, WorkspaceSettings},
|
||||
workspace_settings::{AutosaveSetting, TabBarPlacement, WorkspaceSettings},
|
||||
DelayedDebouncedEditAction, FollowableItemBuilders, ItemNavHistory, ToolbarItemLocation,
|
||||
ViewId, Workspace, WorkspaceId,
|
||||
};
|
||||
@@ -26,7 +26,6 @@ use std::{
|
||||
any::{Any, TypeId},
|
||||
cell::RefCell,
|
||||
ops::Range,
|
||||
path::PathBuf,
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
@@ -37,7 +36,7 @@ use ui::Element as _;
|
||||
pub const LEADER_UPDATE_THROTTLE: Duration = Duration::from_millis(200);
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct ItemSettings {
|
||||
pub struct TabsSettings {
|
||||
pub git_status: bool,
|
||||
pub close_position: ClosePosition,
|
||||
}
|
||||
@@ -66,10 +65,10 @@ impl ClosePosition {
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct ItemSettingsContent {
|
||||
pub struct TabsSettingsContent {
|
||||
/// Whether to show the Git file status on a tab item.
|
||||
///
|
||||
/// Default: true
|
||||
/// Default: false
|
||||
git_status: Option<bool>,
|
||||
/// Position of the close button in a tab.
|
||||
///
|
||||
@@ -90,10 +89,10 @@ pub struct PreviewTabsSettingsContent {
|
||||
enable_preview_from_file_finder: Option<bool>,
|
||||
}
|
||||
|
||||
impl Settings for ItemSettings {
|
||||
impl Settings for TabsSettings {
|
||||
const KEY: Option<&'static str> = Some("tabs");
|
||||
|
||||
type FileContent = ItemSettingsContent;
|
||||
type FileContent = TabsSettingsContent;
|
||||
|
||||
fn load(sources: SettingsSources<Self::FileContent>, _: &mut AppContext) -> Result<Self> {
|
||||
sources.json_merge()
|
||||
@@ -196,7 +195,7 @@ pub trait Item: FocusableView + EventEmitter<Self::Event> {
|
||||
fn save_as(
|
||||
&mut self,
|
||||
_project: Model<Project>,
|
||||
_abs_path: PathBuf,
|
||||
_path: ProjectPath,
|
||||
_cx: &mut ViewContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
unimplemented!("save_as() must be implemented if can_save() returns true")
|
||||
@@ -226,6 +225,10 @@ pub trait Item: FocusableView + EventEmitter<Self::Event> {
|
||||
None
|
||||
}
|
||||
|
||||
fn tab_bar_placement(&self) -> TabBarPlacement {
|
||||
TabBarPlacement::Top
|
||||
}
|
||||
|
||||
fn breadcrumb_location(&self) -> ToolbarItemLocation {
|
||||
ToolbarItemLocation::Hidden
|
||||
}
|
||||
@@ -309,7 +312,7 @@ pub trait ItemHandle: 'static + Send {
|
||||
fn save_as(
|
||||
&self,
|
||||
project: Model<Project>,
|
||||
abs_path: PathBuf,
|
||||
path: ProjectPath,
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<Result<()>>;
|
||||
fn reload(&self, project: Model<Project>, cx: &mut WindowContext) -> Task<Result<()>>;
|
||||
@@ -321,6 +324,7 @@ pub trait ItemHandle: 'static + Send {
|
||||
callback: Box<dyn FnOnce(&mut AppContext) + Send>,
|
||||
) -> gpui::Subscription;
|
||||
fn to_searchable_item_handle(&self, cx: &AppContext) -> Option<Box<dyn SearchableItemHandle>>;
|
||||
fn tab_bar_placement(&self, cx: &AppContext) -> TabBarPlacement;
|
||||
fn breadcrumb_location(&self, cx: &AppContext) -> ToolbarItemLocation;
|
||||
fn breadcrumbs(&self, theme: &Theme, cx: &AppContext) -> Option<Vec<BreadcrumbText>>;
|
||||
fn serialized_item_kind(&self) -> Option<&'static str>;
|
||||
@@ -647,10 +651,10 @@ impl<T: Item> ItemHandle for View<T> {
|
||||
fn save_as(
|
||||
&self,
|
||||
project: Model<Project>,
|
||||
abs_path: PathBuf,
|
||||
path: ProjectPath,
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<anyhow::Result<()>> {
|
||||
self.update(cx, |item, cx| item.save_as(project, abs_path, cx))
|
||||
self.update(cx, |item, cx| item.save_as(project, path, cx))
|
||||
}
|
||||
|
||||
fn reload(&self, project: Model<Project>, cx: &mut WindowContext) -> Task<Result<()>> {
|
||||
@@ -679,6 +683,10 @@ impl<T: Item> ItemHandle for View<T> {
|
||||
self.read(cx).as_searchable(self)
|
||||
}
|
||||
|
||||
fn tab_bar_placement(&self, cx: &AppContext) -> TabBarPlacement {
|
||||
self.read(cx).tab_bar_placement()
|
||||
}
|
||||
|
||||
fn breadcrumb_location(&self, cx: &AppContext) -> ToolbarItemLocation {
|
||||
self.read(cx).breadcrumb_location()
|
||||
}
|
||||
@@ -1126,7 +1134,7 @@ pub mod test {
|
||||
fn save_as(
|
||||
&mut self,
|
||||
_: Model<Project>,
|
||||
_: std::path::PathBuf,
|
||||
_: ProjectPath,
|
||||
_: &mut ViewContext<Self>,
|
||||
) -> Task<anyhow::Result<()>> {
|
||||
self.save_as_count += 1;
|
||||
|
||||
@@ -263,7 +263,7 @@ impl Render for LanguageServerPrompt {
|
||||
PromptLevel::Warning => {
|
||||
Some(DiagnosticSeverity::WARNING)
|
||||
}
|
||||
PromptLevel::Critical => {
|
||||
PromptLevel::Critical | PromptLevel::Destructive => {
|
||||
Some(DiagnosticSeverity::ERROR)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use crate::{
|
||||
item::{
|
||||
ClosePosition, Item, ItemHandle, ItemSettings, PreviewTabsSettings, TabContentParams,
|
||||
ClosePosition, Item, ItemHandle, PreviewTabsSettings, TabContentParams, TabsSettings,
|
||||
WeakItemHandle,
|
||||
},
|
||||
toolbar::Toolbar,
|
||||
workspace_settings::{AutosaveSetting, TabBarSettings, WorkspaceSettings},
|
||||
workspace_settings::{AutosaveSetting, TabBarPlacement, TabBarSettings, WorkspaceSettings},
|
||||
NewCenterTerminal, NewFile, NewSearch, OpenInTerminal, OpenTerminal, OpenVisible,
|
||||
SplitDirection, ToggleZoom, Workspace,
|
||||
};
|
||||
@@ -26,7 +26,7 @@ use std::{
|
||||
any::Any,
|
||||
cmp, fmt, mem,
|
||||
ops::ControlFlow,
|
||||
path::{Path, PathBuf},
|
||||
path::PathBuf,
|
||||
rc::Rc,
|
||||
sync::{
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
@@ -1322,14 +1322,10 @@ impl Pane {
|
||||
pane.update(cx, |_, cx| item.save(should_format, project, cx))?
|
||||
.await?;
|
||||
} else if can_save_as {
|
||||
let start_abs_path = project
|
||||
.update(cx, |project, cx| {
|
||||
let worktree = project.visible_worktrees(cx).next()?;
|
||||
Some(worktree.read(cx).as_local()?.abs_path().to_path_buf())
|
||||
})?
|
||||
.unwrap_or_else(|| Path::new("").into());
|
||||
|
||||
let abs_path = cx.update(|cx| cx.prompt_for_new_path(&start_abs_path))?;
|
||||
let abs_path = pane.update(cx, |pane, cx| {
|
||||
pane.workspace
|
||||
.update(cx, |workspace, cx| workspace.prompt_for_new_path(cx))
|
||||
})??;
|
||||
if let Some(abs_path) = abs_path.await.ok().flatten() {
|
||||
pane.update(cx, |_, cx| item.save_as(project, abs_path, cx))?
|
||||
.await?;
|
||||
@@ -1427,6 +1423,7 @@ impl Pane {
|
||||
ix: usize,
|
||||
item: &Box<dyn ItemHandle>,
|
||||
detail: usize,
|
||||
tab_bar_placement: ui::TabBarPlacement,
|
||||
cx: &mut ViewContext<'_, Pane>,
|
||||
) -> impl IntoElement {
|
||||
let is_active = ix == self.active_item_index;
|
||||
@@ -1443,7 +1440,7 @@ impl Pane {
|
||||
},
|
||||
cx,
|
||||
);
|
||||
let close_side = &ItemSettings::get_global(cx).close_position;
|
||||
let close_side = &TabsSettings::get_global(cx).close_position;
|
||||
let indicator = render_item_indicator(item.boxed_clone(), cx);
|
||||
let item_id = item.item_id();
|
||||
let is_first_item = ix == 0;
|
||||
@@ -1451,6 +1448,7 @@ impl Pane {
|
||||
let position_relative_to_active_item = ix.cmp(&self.active_item_index);
|
||||
|
||||
let tab = Tab::new(ix)
|
||||
.tab_bar_placement(tab_bar_placement)
|
||||
.position(if is_first_item {
|
||||
TabPosition::First
|
||||
} else if is_last_item {
|
||||
@@ -1663,8 +1661,20 @@ impl Pane {
|
||||
})
|
||||
}
|
||||
|
||||
fn render_tab_bar(&mut self, cx: &mut ViewContext<'_, Pane>) -> impl IntoElement {
|
||||
fn need_tab_bar_at(&self, placement: TabBarPlacement, cx: &mut ViewContext<'_, Pane>) -> bool {
|
||||
let Some(item) = self.active_item() else {
|
||||
return false;
|
||||
};
|
||||
item.tab_bar_placement(cx) == placement
|
||||
}
|
||||
|
||||
fn render_tab_bar(
|
||||
&mut self,
|
||||
placement: ui::TabBarPlacement,
|
||||
cx: &mut ViewContext<'_, Pane>,
|
||||
) -> impl IntoElement {
|
||||
TabBar::new("tab_bar")
|
||||
.placement(placement)
|
||||
.track_scroll(self.tab_bar_scroll_handle.clone())
|
||||
.when(
|
||||
self.display_nav_history_buttons.unwrap_or_default(),
|
||||
@@ -1708,7 +1718,7 @@ impl Pane {
|
||||
.iter()
|
||||
.enumerate()
|
||||
.zip(tab_details(&self.items, cx))
|
||||
.map(|((ix, item), detail)| self.render_tab(ix, item, detail, cx)),
|
||||
.map(|((ix, item), detail)| self.render_tab(ix, item, detail, placement, cx)),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
@@ -2046,8 +2056,8 @@ impl Render for Pane {
|
||||
}
|
||||
}),
|
||||
)
|
||||
.when(self.active_item().is_some(), |pane| {
|
||||
pane.child(self.render_tab_bar(cx))
|
||||
.when(self.need_tab_bar_at(TabBarPlacement::Top, cx), |pane| {
|
||||
pane.child(self.render_tab_bar(ui::TabBarPlacement::Top, cx))
|
||||
})
|
||||
.child({
|
||||
let has_worktrees = self.project.read(cx).worktrees().next().is_some();
|
||||
@@ -2117,6 +2127,9 @@ impl Render for Pane {
|
||||
}),
|
||||
)
|
||||
})
|
||||
.when(self.need_tab_bar_at(TabBarPlacement::Bottom, cx), |pane| {
|
||||
pane.child(self.render_tab_bar(ui::TabBarPlacement::Bottom, cx))
|
||||
})
|
||||
.on_mouse_down(
|
||||
MouseButton::Navigate(NavigationDirection::Back),
|
||||
cx.listener(|pane, _, cx| {
|
||||
|
||||
@@ -33,8 +33,8 @@ use gpui::{
|
||||
Size, Subscription, Task, View, WeakView, WindowHandle, WindowOptions,
|
||||
};
|
||||
use item::{
|
||||
FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, PreviewTabsSettings,
|
||||
ProjectItem,
|
||||
FollowableItem, FollowableItemHandle, Item, ItemHandle, PreviewTabsSettings, ProjectItem,
|
||||
TabsSettings,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use language::{LanguageRegistry, Rope};
|
||||
@@ -85,7 +85,7 @@ use ui::{
|
||||
use util::{maybe, ResultExt};
|
||||
use uuid::Uuid;
|
||||
pub use workspace_settings::{
|
||||
AutosaveSetting, RestoreOnStartupBehaviour, TabBarSettings, WorkspaceSettings,
|
||||
AutosaveSetting, RestoreOnStartupBehaviour, TabBarPlacement, TabBarSettings, WorkspaceSettings,
|
||||
};
|
||||
|
||||
use crate::notifications::NotificationId;
|
||||
@@ -265,7 +265,8 @@ impl Column for WorkspaceId {
|
||||
}
|
||||
pub fn init_settings(cx: &mut AppContext) {
|
||||
WorkspaceSettings::register(cx);
|
||||
ItemSettings::register(cx);
|
||||
TabsSettings::register(cx);
|
||||
TabsSettings::register(cx);
|
||||
PreviewTabsSettings::register(cx);
|
||||
TabBarSettings::register(cx);
|
||||
}
|
||||
@@ -544,6 +545,10 @@ pub enum OpenVisible {
|
||||
OnlyDirectories,
|
||||
}
|
||||
|
||||
type PromptForNewPath = Box<
|
||||
dyn Fn(&mut Workspace, &mut ViewContext<Workspace>) -> oneshot::Receiver<Option<ProjectPath>>,
|
||||
>;
|
||||
|
||||
/// Collects everything project-related for a certain window opened.
|
||||
/// In some way, is a counterpart of a window, as the [`WindowHandle`] could be downcast into `Workspace`.
|
||||
///
|
||||
@@ -585,6 +590,7 @@ pub struct Workspace {
|
||||
bounds: Bounds<Pixels>,
|
||||
centered_layout: bool,
|
||||
bounds_save_task_queued: Option<Task<()>>,
|
||||
on_prompt_for_new_path: Option<PromptForNewPath>,
|
||||
}
|
||||
|
||||
impl EventEmitter<Event> for Workspace {}
|
||||
@@ -875,6 +881,7 @@ impl Workspace {
|
||||
bounds: Default::default(),
|
||||
centered_layout: false,
|
||||
bounds_save_task_queued: None,
|
||||
on_prompt_for_new_path: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1223,6 +1230,59 @@ impl Workspace {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn set_prompt_for_new_path(&mut self, prompt: PromptForNewPath) {
|
||||
self.on_prompt_for_new_path = Some(prompt)
|
||||
}
|
||||
|
||||
pub fn prompt_for_new_path(
|
||||
&mut self,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> oneshot::Receiver<Option<ProjectPath>> {
|
||||
if let Some(prompt) = self.on_prompt_for_new_path.take() {
|
||||
let rx = prompt(self, cx);
|
||||
self.on_prompt_for_new_path = Some(prompt);
|
||||
rx
|
||||
} else {
|
||||
let start_abs_path = self
|
||||
.project
|
||||
.update(cx, |project, cx| {
|
||||
let worktree = project.visible_worktrees(cx).next()?;
|
||||
Some(worktree.read(cx).as_local()?.abs_path().to_path_buf())
|
||||
})
|
||||
.unwrap_or_else(|| Path::new("").into());
|
||||
|
||||
let (tx, rx) = oneshot::channel();
|
||||
let abs_path = cx.prompt_for_new_path(&start_abs_path);
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let abs_path = abs_path.await?;
|
||||
let project_path = abs_path.and_then(|abs_path| {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.project.update(cx, |project, cx| {
|
||||
project.find_or_create_local_worktree(abs_path, true, cx)
|
||||
})
|
||||
})
|
||||
.ok()
|
||||
});
|
||||
|
||||
if let Some(project_path) = project_path {
|
||||
let (worktree, path) = project_path.await?;
|
||||
let worktree_id = worktree.read_with(&cx, |worktree, _| worktree.id())?;
|
||||
tx.send(Some(ProjectPath {
|
||||
worktree_id,
|
||||
path: path.into(),
|
||||
}))
|
||||
.ok();
|
||||
} else {
|
||||
tx.send(None).ok();
|
||||
}
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
|
||||
rx
|
||||
}
|
||||
}
|
||||
|
||||
pub fn titlebar_item(&self) -> Option<AnyView> {
|
||||
self.titlebar_item.clone()
|
||||
}
|
||||
@@ -4785,6 +4845,7 @@ pub fn join_hosted_project(
|
||||
pub fn join_remote_project(
|
||||
project_id: ProjectId,
|
||||
app_state: Arc<AppState>,
|
||||
window_to_replace: Option<WindowHandle<Workspace>>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<WindowHandle<Workspace>>> {
|
||||
let windows = cx.windows();
|
||||
@@ -4816,16 +4877,25 @@ pub fn join_remote_project(
|
||||
)
|
||||
.await?;
|
||||
|
||||
let window_bounds_override = window_bounds_env_override();
|
||||
cx.update(|cx| {
|
||||
let mut options = (app_state.build_window_options)(None, cx);
|
||||
options.bounds = window_bounds_override;
|
||||
cx.open_window(options, |cx| {
|
||||
cx.new_view(|cx| {
|
||||
if let Some(window_to_replace) = window_to_replace {
|
||||
cx.update_window(window_to_replace.into(), |_, cx| {
|
||||
cx.replace_root_view(|cx| {
|
||||
Workspace::new(Default::default(), project, app_state.clone(), cx)
|
||||
});
|
||||
})?;
|
||||
window_to_replace
|
||||
} else {
|
||||
let window_bounds_override = window_bounds_env_override();
|
||||
cx.update(|cx| {
|
||||
let mut options = (app_state.build_window_options)(None, cx);
|
||||
options.bounds = window_bounds_override;
|
||||
cx.open_window(options, |cx| {
|
||||
cx.new_view(|cx| {
|
||||
Workspace::new(Default::default(), project, app_state.clone(), cx)
|
||||
})
|
||||
})
|
||||
})
|
||||
})?
|
||||
})?
|
||||
}
|
||||
};
|
||||
|
||||
workspace.update(&mut cx, |_, cx| {
|
||||
|
||||
@@ -58,13 +58,30 @@ pub struct WorkspaceSettingsContent {
|
||||
pub drop_target_size: Option<f32>,
|
||||
}
|
||||
|
||||
/// The tab bar placement in a pane.
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum TabBarPlacement {
|
||||
/// Don't show tab bar.
|
||||
No,
|
||||
/// Place tab bar on top of the pane.
|
||||
Top,
|
||||
/// Place tab bar at the bottom of the pane.
|
||||
Bottom,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct TabBarSettings {
|
||||
pub placement: TabBarPlacement,
|
||||
pub show_nav_history_buttons: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct TabBarSettingsContent {
|
||||
/// Where to place tab bar in the editor.
|
||||
///
|
||||
/// Default: top
|
||||
pub placement: Option<TabBarPlacement>,
|
||||
/// Whether or not to show the navigation history buttons in the tab bar.
|
||||
///
|
||||
/// Default: true
|
||||
|
||||
@@ -1625,6 +1625,7 @@ impl RemoteWorktree {
|
||||
pub fn save_buffer(
|
||||
&self,
|
||||
buffer_handle: Model<Buffer>,
|
||||
new_path: Option<proto::ProjectPath>,
|
||||
cx: &mut ModelContext<Worktree>,
|
||||
) -> Task<Result<()>> {
|
||||
let buffer = buffer_handle.read(cx);
|
||||
@@ -1637,6 +1638,7 @@ impl RemoteWorktree {
|
||||
.request(proto::SaveBuffer {
|
||||
project_id,
|
||||
buffer_id,
|
||||
new_path,
|
||||
version: serialize_version(&version),
|
||||
})
|
||||
.await?;
|
||||
@@ -1911,6 +1913,7 @@ impl Snapshot {
|
||||
|
||||
fn traverse_from_offset(
|
||||
&self,
|
||||
include_files: bool,
|
||||
include_dirs: bool,
|
||||
include_ignored: bool,
|
||||
start_offset: usize,
|
||||
@@ -1919,6 +1922,7 @@ impl Snapshot {
|
||||
cursor.seek(
|
||||
&TraversalTarget::Count {
|
||||
count: start_offset,
|
||||
include_files,
|
||||
include_dirs,
|
||||
include_ignored,
|
||||
},
|
||||
@@ -1927,6 +1931,7 @@ impl Snapshot {
|
||||
);
|
||||
Traversal {
|
||||
cursor,
|
||||
include_files,
|
||||
include_dirs,
|
||||
include_ignored,
|
||||
}
|
||||
@@ -1934,6 +1939,7 @@ impl Snapshot {
|
||||
|
||||
fn traverse_from_path(
|
||||
&self,
|
||||
include_files: bool,
|
||||
include_dirs: bool,
|
||||
include_ignored: bool,
|
||||
path: &Path,
|
||||
@@ -1942,17 +1948,22 @@ impl Snapshot {
|
||||
cursor.seek(&TraversalTarget::Path(path), Bias::Left, &());
|
||||
Traversal {
|
||||
cursor,
|
||||
include_files,
|
||||
include_dirs,
|
||||
include_ignored,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn files(&self, include_ignored: bool, start: usize) -> Traversal {
|
||||
self.traverse_from_offset(false, include_ignored, start)
|
||||
self.traverse_from_offset(true, false, include_ignored, start)
|
||||
}
|
||||
|
||||
pub fn directories(&self, include_ignored: bool, start: usize) -> Traversal {
|
||||
self.traverse_from_offset(false, true, include_ignored, start)
|
||||
}
|
||||
|
||||
pub fn entries(&self, include_ignored: bool) -> Traversal {
|
||||
self.traverse_from_offset(true, include_ignored, 0)
|
||||
self.traverse_from_offset(true, true, include_ignored, 0)
|
||||
}
|
||||
|
||||
pub fn repositories(&self) -> impl Iterator<Item = (&Arc<Path>, &RepositoryEntry)> {
|
||||
@@ -2084,6 +2095,7 @@ impl Snapshot {
|
||||
cursor.seek(&TraversalTarget::Path(parent_path), Bias::Right, &());
|
||||
let traversal = Traversal {
|
||||
cursor,
|
||||
include_files: true,
|
||||
include_dirs: true,
|
||||
include_ignored: true,
|
||||
};
|
||||
@@ -2103,6 +2115,7 @@ impl Snapshot {
|
||||
cursor.seek(&TraversalTarget::Path(parent_path), Bias::Left, &());
|
||||
let mut traversal = Traversal {
|
||||
cursor,
|
||||
include_files: true,
|
||||
include_dirs,
|
||||
include_ignored,
|
||||
};
|
||||
@@ -2141,7 +2154,7 @@ impl Snapshot {
|
||||
|
||||
pub fn entry_for_path(&self, path: impl AsRef<Path>) -> Option<&Entry> {
|
||||
let path = path.as_ref();
|
||||
self.traverse_from_path(true, true, path)
|
||||
self.traverse_from_path(true, true, true, path)
|
||||
.entry()
|
||||
.and_then(|entry| {
|
||||
if entry.path.as_ref() == path {
|
||||
@@ -4532,12 +4545,15 @@ struct TraversalProgress<'a> {
|
||||
}
|
||||
|
||||
impl<'a> TraversalProgress<'a> {
|
||||
fn count(&self, include_dirs: bool, include_ignored: bool) -> usize {
|
||||
match (include_ignored, include_dirs) {
|
||||
(true, true) => self.count,
|
||||
(true, false) => self.file_count,
|
||||
(false, true) => self.non_ignored_count,
|
||||
(false, false) => self.non_ignored_file_count,
|
||||
fn count(&self, include_files: bool, include_dirs: bool, include_ignored: bool) -> usize {
|
||||
match (include_files, include_dirs, include_ignored) {
|
||||
(true, true, true) => self.count,
|
||||
(true, true, false) => self.non_ignored_count,
|
||||
(true, false, true) => self.file_count,
|
||||
(true, false, false) => self.non_ignored_file_count,
|
||||
(false, true, true) => self.count - self.file_count,
|
||||
(false, true, false) => self.non_ignored_count - self.non_ignored_file_count,
|
||||
(false, false, _) => 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4600,6 +4616,7 @@ impl<'a> sum_tree::Dimension<'a, EntrySummary> for GitStatuses {
|
||||
pub struct Traversal<'a> {
|
||||
cursor: sum_tree::Cursor<'a, Entry, TraversalProgress<'a>>,
|
||||
include_ignored: bool,
|
||||
include_files: bool,
|
||||
include_dirs: bool,
|
||||
}
|
||||
|
||||
@@ -4609,6 +4626,7 @@ impl<'a> Traversal<'a> {
|
||||
&TraversalTarget::Count {
|
||||
count: self.end_offset() + 1,
|
||||
include_dirs: self.include_dirs,
|
||||
include_files: self.include_files,
|
||||
include_ignored: self.include_ignored,
|
||||
},
|
||||
Bias::Left,
|
||||
@@ -4624,7 +4642,8 @@ impl<'a> Traversal<'a> {
|
||||
&(),
|
||||
);
|
||||
if let Some(entry) = self.cursor.item() {
|
||||
if (self.include_dirs || !entry.is_dir())
|
||||
if (self.include_files || !entry.is_file())
|
||||
&& (self.include_dirs || !entry.is_dir())
|
||||
&& (self.include_ignored || !entry.is_ignored)
|
||||
{
|
||||
return true;
|
||||
@@ -4641,13 +4660,13 @@ impl<'a> Traversal<'a> {
|
||||
pub fn start_offset(&self) -> usize {
|
||||
self.cursor
|
||||
.start()
|
||||
.count(self.include_dirs, self.include_ignored)
|
||||
.count(self.include_files, self.include_dirs, self.include_ignored)
|
||||
}
|
||||
|
||||
pub fn end_offset(&self) -> usize {
|
||||
self.cursor
|
||||
.end(&())
|
||||
.count(self.include_dirs, self.include_ignored)
|
||||
.count(self.include_files, self.include_dirs, self.include_ignored)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4670,6 +4689,7 @@ enum TraversalTarget<'a> {
|
||||
PathSuccessor(&'a Path),
|
||||
Count {
|
||||
count: usize,
|
||||
include_files: bool,
|
||||
include_ignored: bool,
|
||||
include_dirs: bool,
|
||||
},
|
||||
@@ -4688,11 +4708,12 @@ impl<'a, 'b> SeekTarget<'a, EntrySummary, TraversalProgress<'a>> for TraversalTa
|
||||
}
|
||||
TraversalTarget::Count {
|
||||
count,
|
||||
include_files,
|
||||
include_dirs,
|
||||
include_ignored,
|
||||
} => Ord::cmp(
|
||||
count,
|
||||
&cursor_location.count(*include_dirs, *include_ignored),
|
||||
&cursor_location.count(*include_files, *include_dirs, *include_ignored),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,14 +52,11 @@ fn main() {
|
||||
println!("cargo:rustc-link-arg=/stack:{}", 8 * 1024 * 1024);
|
||||
}
|
||||
|
||||
let manifest = std::path::Path::new("resources/windows/manifest.xml");
|
||||
let icon = std::path::Path::new("resources/windows/app-icon.ico");
|
||||
println!("cargo:rerun-if-changed={}", manifest.display());
|
||||
println!("cargo:rerun-if-changed={}", icon.display());
|
||||
|
||||
let mut res = winresource::WindowsResource::new();
|
||||
res.set_icon(icon.to_str().unwrap());
|
||||
res.set_manifest_file(manifest.to_str().unwrap());
|
||||
|
||||
if let Err(e) = res.compile() {
|
||||
eprintln!("{}", e);
|
||||
|
||||
@@ -145,6 +145,13 @@ fn init_headless(dev_server_token: DevServerToken) {
|
||||
);
|
||||
handle_settings_file_changes(user_settings_file_rx, cx);
|
||||
|
||||
let (installation_id, _) = cx
|
||||
.background_executor()
|
||||
.block(installation_id())
|
||||
.ok()
|
||||
.unzip();
|
||||
upload_panics_and_crashes(client.http_client(), installation_id, cx);
|
||||
|
||||
headless::init(
|
||||
client.clone(),
|
||||
headless::AppState {
|
||||
@@ -323,7 +330,7 @@ fn init_ui(args: Args) {
|
||||
.detach();
|
||||
|
||||
let telemetry = client.telemetry();
|
||||
telemetry.start(installation_id, session_id, cx);
|
||||
telemetry.start(installation_id.clone(), session_id, cx);
|
||||
telemetry.report_setting_event("theme", cx.theme().name.to_string());
|
||||
telemetry.report_setting_event("keymap", BaseKeymap::get_global(cx).to_string());
|
||||
telemetry.report_app_event(
|
||||
@@ -378,8 +385,7 @@ fn init_ui(args: Args) {
|
||||
cx.set_menus(app_menus());
|
||||
initialize_workspace(app_state.clone(), cx);
|
||||
|
||||
// todo(linux): unblock this
|
||||
upload_panics_and_crashes(client.http_client(), cx);
|
||||
upload_panics_and_crashes(client.http_client(), installation_id, cx);
|
||||
|
||||
cx.activate(true);
|
||||
|
||||
@@ -824,7 +830,11 @@ fn init_panic_hook(app: &App, installation_id: Option<String>, session_id: Strin
|
||||
}));
|
||||
}
|
||||
|
||||
fn upload_panics_and_crashes(http: Arc<HttpClientWithUrl>, cx: &mut AppContext) {
|
||||
fn upload_panics_and_crashes(
|
||||
http: Arc<HttpClientWithUrl>,
|
||||
installation_id: Option<String>,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
let telemetry_settings = *client::TelemetrySettings::get_global(cx);
|
||||
cx.background_executor()
|
||||
.spawn(async move {
|
||||
@@ -832,7 +842,7 @@ fn upload_panics_and_crashes(http: Arc<HttpClientWithUrl>, cx: &mut AppContext)
|
||||
.await
|
||||
.log_err()
|
||||
.flatten();
|
||||
upload_previous_crashes(http, most_recent_panic, telemetry_settings)
|
||||
upload_previous_crashes(http, most_recent_panic, installation_id, telemetry_settings)
|
||||
.await
|
||||
.log_err()
|
||||
})
|
||||
@@ -915,6 +925,7 @@ static LAST_CRASH_UPLOADED: &'static str = "LAST_CRASH_UPLOADED";
|
||||
async fn upload_previous_crashes(
|
||||
http: Arc<HttpClientWithUrl>,
|
||||
most_recent_panic: Option<(i64, String)>,
|
||||
installation_id: Option<String>,
|
||||
telemetry_settings: client::TelemetrySettings,
|
||||
) -> Result<()> {
|
||||
if !telemetry_settings.diagnostics {
|
||||
@@ -964,6 +975,9 @@ async fn upload_previous_crashes(
|
||||
.header("x-zed-panicked-on", format!("{}", panicked_on))
|
||||
.header("x-zed-panic", payload)
|
||||
}
|
||||
if let Some(installation_id) = installation_id.as_ref() {
|
||||
request = request.header("x-zed-installation-id", installation_id);
|
||||
}
|
||||
|
||||
let request = request.body(body.into())?;
|
||||
|
||||
|
||||
@@ -304,6 +304,104 @@ List of `string` values
|
||||
|
||||
`boolean` values
|
||||
|
||||
## Editor Tab Bar
|
||||
|
||||
- Description: Settings related to the editor's tab bar.
|
||||
- Settings: `tab_bar`
|
||||
- Default:
|
||||
|
||||
```json
|
||||
"tab_bar": {
|
||||
"placement": "top",
|
||||
"show_nav_history_buttons": true
|
||||
}
|
||||
```
|
||||
|
||||
### Placement
|
||||
|
||||
- Description: Where to place the editor tab bar.
|
||||
- Setting: `placement`
|
||||
- Default: `top`
|
||||
|
||||
**Options**
|
||||
|
||||
1. Place the tab bar on top of the editor:
|
||||
|
||||
```json
|
||||
{
|
||||
"placement": "top"
|
||||
}
|
||||
```
|
||||
|
||||
2. Place the tab bar at the bottom of the editor:
|
||||
|
||||
```json
|
||||
{
|
||||
"placement": "bottom"
|
||||
}
|
||||
```
|
||||
|
||||
3. Hide the tab bar:
|
||||
|
||||
```json
|
||||
{
|
||||
"placement": "no"
|
||||
}
|
||||
```
|
||||
|
||||
### Navigation History Buttons
|
||||
|
||||
- Description: Whether to show the navigation history buttons.
|
||||
- Setting: `show_nav_history_buttons`
|
||||
- Default: `true`
|
||||
|
||||
**Options**
|
||||
|
||||
`boolean` values
|
||||
|
||||
## Editor Tabs
|
||||
|
||||
- Description: Configuration for the editor tabs.
|
||||
- Setting: `tabs`
|
||||
- Default:
|
||||
|
||||
```json
|
||||
"tabs": {
|
||||
"close_position": "right",
|
||||
"git_status": false
|
||||
},
|
||||
```
|
||||
|
||||
### Close Position
|
||||
|
||||
- Description: Where to display close button within a tab.
|
||||
- Setting: `close_position`
|
||||
- Default: `right`
|
||||
|
||||
**Options**
|
||||
|
||||
1. Display the close button on the right:
|
||||
|
||||
```json
|
||||
{
|
||||
"close_position": "right"
|
||||
}
|
||||
```
|
||||
|
||||
2. Display the close button on the left:
|
||||
|
||||
```json
|
||||
{
|
||||
"close_position": "left"
|
||||
}
|
||||
```
|
||||
|
||||
### Git Status
|
||||
|
||||
- Description: Whether or not to show Git file status in tab.
|
||||
- Setting: `git_status`
|
||||
- Default: `false`
|
||||
|
||||
## Editor Toolbar
|
||||
|
||||
- Description: Whether or not to show various elements in the editor toolbar.
|
||||
|
||||
Reference in New Issue
Block a user