Merge branch 'main' into drag-and-drop

This commit is contained in:
K Simmons 2022-08-22 17:18:29 -07:00
commit 13e9336049
65 changed files with 2371 additions and 797 deletions

View file

@ -28,7 +28,6 @@ jobs:
run: |
rustup set profile minimal
rustup update stable
rustup component add clippy
rustup target add wasm32-wasi
- name: Install Node
@ -40,14 +39,6 @@ jobs:
uses: actions/checkout@v2
with:
clean: false
- name: Run clippy
run: >
cargo clippy --workspace --
-Dwarnings
-Aclippy::reversed_empty_ranges
-Aclippy::missing_safety_doc
-Aclippy::let_unit_value
- name: Run tests
run: cargo test --workspace --no-fail-fast

60
Cargo.lock generated
View file

@ -75,7 +75,7 @@ version = "0.17.0-dev"
source = "git+https://github.com/zed-industries/alacritty?rev=4e1f0c6177975a040b37f942dfb0e723e46a9971#4e1f0c6177975a040b37f942dfb0e723e46a9971"
dependencies = [
"alacritty_config_derive",
"base64 0.13.0",
"base64",
"bitflags",
"dirs 4.0.0",
"libc",
@ -430,7 +430,7 @@ checksum = "c2cc6e8e8c993cb61a005fab8c1e5093a29199b7253b05a6883999312935c1ff"
dependencies = [
"async-trait",
"axum-core",
"base64 0.13.0",
"base64",
"bitflags",
"bytes",
"futures-util",
@ -507,12 +507,6 @@ dependencies = [
"rustc-demangle",
]
[[package]]
name = "base64"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff"
[[package]]
name = "base64"
version = "0.13.0"
@ -984,7 +978,7 @@ dependencies = [
"async-tungstenite",
"axum",
"axum-extra",
"base64 0.13.0",
"base64",
"clap 3.2.8",
"client",
"collections",
@ -2295,7 +2289,7 @@ version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cff78e5788be1e0ab65b04d306b2ed5092c815ec97ec70f4ebd5aee158aa55d"
dependencies = [
"base64 0.13.0",
"base64",
"bitflags",
"bytes",
"headers-core",
@ -2395,15 +2389,6 @@ dependencies = [
"itoa",
]
[[package]]
name = "http-auth-basic"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df69b6a68474b935f436fb9c84139f32de4f7759810090d1a3a5e592553f7ee0"
dependencies = [
"base64 0.12.3",
]
[[package]]
name = "http-body"
version = "0.4.5"
@ -3596,7 +3581,7 @@ version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd56cbd21fea48d0c440b41cd69c589faacade08c992d9a54e471b79d0fd13eb"
dependencies = [
"base64 0.13.0",
"base64",
"once_cell",
"regex",
]
@ -3698,7 +3683,7 @@ version = "1.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd39bc6cdc9355ad1dc5eeedefee696bb35c34caf21768741e81826c0bbd7225"
dependencies = [
"base64 0.13.0",
"base64",
"indexmap",
"line-wrap",
"serde",
@ -4259,7 +4244,7 @@ version = "0.11.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b75aa69a3f06bbcc66ede33af2af253c6f7a86b1ca0033f60c580a27074fbf92"
dependencies = [
"base64 0.13.0",
"base64",
"bytes",
"encoding_rs",
"futures-core",
@ -4355,7 +4340,7 @@ dependencies = [
"anyhow",
"async-lock",
"async-tungstenite",
"base64 0.13.0",
"base64",
"clock",
"collections",
"ctor",
@ -4474,7 +4459,7 @@ version = "0.19.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35edb675feee39aec9c99fa5ff985081995a06d594114ae14cbe797ad7b7a6d7"
dependencies = [
"base64 0.13.0",
"base64",
"log",
"ring",
"sct 0.6.1",
@ -4499,7 +4484,7 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7522c9de787ff061458fe9a829dc790a3f5b22dc571694fc5883f448b94d9a9"
dependencies = [
"base64 0.13.0",
"base64",
]
[[package]]
@ -5111,7 +5096,7 @@ checksum = "6b69bf218860335ddda60d6ce85ee39f6cf6e5630e300e19757d1de15886a093"
dependencies = [
"ahash",
"atoi",
"base64 0.13.0",
"base64",
"bitflags",
"byteorder",
"bytes",
@ -5702,7 +5687,7 @@ checksum = "ff08f4649d10a70ffa3522ca559031285d8e421d727ac85c60825761818f5d0a"
dependencies = [
"async-stream",
"async-trait",
"base64 0.13.0",
"base64",
"bytes",
"futures-core",
"futures-util",
@ -5891,6 +5876,15 @@ dependencies = [
"tree-sitter",
]
[[package]]
name = "tree-sitter-elixir"
version = "0.19.0"
source = "git+https://github.com/elixir-lang/tree-sitter-elixir?rev=05e3631c6a0701c1fa518b0fee7be95a2ceef5e2#05e3631c6a0701c1fa518b0fee7be95a2ceef5e2"
dependencies = [
"cc",
"tree-sitter",
]
[[package]]
name = "tree-sitter-go"
version = "0.19.1"
@ -5991,7 +5985,7 @@ version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ad3713a14ae247f22a728a0456a545df14acf3867f905adff84be99e23b3ad1"
dependencies = [
"base64 0.13.0",
"base64",
"byteorder",
"bytes",
"http",
@ -6010,7 +6004,7 @@ version = "0.17.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d96a2dea40e7570482f28eb57afbe42d97551905da6a9400acc5c328d24004f5"
dependencies = [
"base64 0.13.0",
"base64",
"byteorder",
"bytes",
"http",
@ -6150,7 +6144,7 @@ version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef8352f317d8f9a918ba5154797fb2a93e2730244041cf7d5be35148266adfa5"
dependencies = [
"base64 0.13.0",
"base64",
"data-url",
"flate2",
"fontdb",
@ -6501,7 +6495,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "743a9f142d93318262d7e1fe329394ff2e8f86a1df45ae5e4f0eedba215ca5ce"
dependencies = [
"anyhow",
"base64 0.13.0",
"base64",
"bincode",
"directories-next",
"file-per-thread-logger",
@ -7005,7 +6999,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.50.0"
version = "0.52.0"
dependencies = [
"activity_indicator",
"anyhow",
@ -7037,7 +7031,6 @@ dependencies = [
"fuzzy",
"go_to_line",
"gpui",
"http-auth-basic",
"ignore",
"image",
"indexmap",
@ -7081,6 +7074,7 @@ dependencies = [
"tree-sitter",
"tree-sitter-c",
"tree-sitter-cpp",
"tree-sitter-elixir",
"tree-sitter-go",
"tree-sitter-json 0.20.0",
"tree-sitter-markdown",

View file

@ -2,7 +2,7 @@
[![CI](https://github.com/zed-industries/zed/actions/workflows/ci.yml/badge.svg)](https://github.com/zed-industries/zed/actions/workflows/ci.yml)
Welcome to Zed, a lightning-fast, collaborative code editor that makes your dreams come true.
Welcome to Zed, a lightning-fast, collaborative code editor that makes your dreams come true.
## Development tips
@ -42,6 +42,24 @@ script/zed_with_local_servers --release
If you trigger `cmd-alt-i`, Zed will copy a JSON representation of the current window contents to the clipboard. You can paste this in a tool like [DJSON](https://chrome.google.com/webstore/detail/djson-json-viewer-formatt/chaeijjekipecdajnijdldjjipaegdjc?hl=en) to navigate the state of on-screen elements in a structured way.
### Experimental Features
A feature flag can be added to Zed by:
* Adding a setting to the crates/settings/src/settings.rs FeatureFlags struct. Use a boolean for a simple on/off, or use a struct to experiment with different configuration options.
* If the feature needs keybindings, add a file to the `assets/keymaps/experiments/` folder, then update the `FeatureFlags::keymap_files()` method to check for your feature's flag and add it's keybindings's path to the method's list.
The Settings global should be initialized with the user's feature flags by the time the feature's `init(cx)` equivalent is called.
To promote an experimental feature to a full feature:
* Take the features settings (if any) and add them under a new variable in the Settings struct. Don't forget to add a `merge()` call in `set_user_settings()`!
* Take the feature's keybindings and add them to the default.json (or equivalent) file
* Remove the file from the `FeatureFlags::keymap_files()` method
* Remove the conditional in the feature's `init(cx)` equivalent.
That's it 😸
### Wasm Plugins
Zed has a Wasm-based plugin runtime which it currently uses to embed plugins. To compile Zed, you'll need to have the `wasm32-wasi` toolchain installed on your system. To install this toolchain, run:

View file

@ -28,6 +28,7 @@
"cmd-h": "zed::Hide",
"alt-cmd-h": "zed::HideOthers",
"cmd-m": "zed::Minimize",
"ctrl-cmd-f": "zed::ToggleFullScreen",
"cmd-n": "workspace::NewFile",
"cmd-shift-n": "workspace::NewWindow",
"cmd-o": "workspace::Open",
@ -307,7 +308,7 @@
"cmd-p": "file_finder::Toggle",
"cmd-shift-p": "command_palette::Toggle",
"cmd-shift-m": "diagnostics::Deploy",
"cmd-shift-e": "project_panel::Toggle",
"cmd-shift-e": "project_panel::ToggleFocus",
"cmd-alt-s": "workspace::SaveAll"
}
},
@ -392,7 +393,7 @@
{
"context": "Workspace",
"bindings": {
"cmd-shift-c": "contacts_panel::Toggle",
"cmd-shift-c": "contacts_panel::ToggleFocus",
"cmd-shift-b": "workspace::ToggleRightSidebar"
}
},
@ -419,15 +420,10 @@
"enter": "terminal::Enter",
"ctrl-c": "terminal::CtrlC",
// Useful terminal actions:
"ctrl-cmd-space": "terminal::ShowCharacterPalette",
"cmd-c": "terminal::Copy",
"cmd-v": "terminal::Paste",
"cmd-k": "terminal::Clear"
}
},
{
"context": "ModalTerminal",
"bindings": {
"shift-escape": "terminal::DeployModal"
}
}
]

View file

@ -0,0 +1,15 @@
[
{
"context": "Workspace",
"bindings": {
"shift-escape": "terminal::DeployModal"
}
},
{
"context": "ModalTerminal",
"bindings": {
"ctrl-cmd-space": "terminal::ShowCharacterPalette",
"shift-escape": "terminal::DeployModal"
}
}
]

View file

@ -102,17 +102,37 @@
//
//
"working_directory": "current_project_directory",
//Any key-value pairs added to this list will be added to the terminal's
//enviroment. Use `:` to seperate multiple values.
// Set the cursor blinking behavior in the terminal.
// May take 4 values:
// 1. Never blink the cursor, ignoring the terminal mode
// "blinking": "off",
// 2. Default the cursor blink to off, but allow the terminal to
// set blinking
// "blinking": "terminal_controlled",
// 3. Always blink the cursor, ignoring the terminal mode
// "blinking": "on",
"blinking": "terminal_controlled",
// Set whether Alternate Scroll mode (code: ?1007) is active by default.
// Alternate Scroll mode converts mouse scroll events into up / down key
// presses when in the alternate screen (e.g. when running applications
// like vim or less). The terminal can still set and unset this mode.
// May take 2 values:
// 1. Default alternate scroll mode to on
// "alternate_scroll": "on",
// 2. Default alternate scroll mode to off
// "alternate_scroll": "off",
"alternate_scroll": "off",
// Any key-value pairs added to this list will be added to the terminal's
// enviroment. Use `:` to seperate multiple values.
"env": {
//"KEY": "value1:value2"
// "KEY": "value1:value2"
}
//Set the terminal's font size. If this option is not included,
//the terminal will default to matching the buffer's font size.
//"font_size": "15"
//Set the terminal's font family. If this option is not included,
//the terminal will default to matching the buffer's font family.
//"font_family": "Zed Mono"
// Set the terminal's font size. If this option is not included,
// the terminal will default to matching the buffer's font size.
// "font_size": "15"
// Set the terminal's font family. If this option is not included,
// the terminal will default to matching the buffer's font family.
// "font_family": "Zed Mono"
},
// Different settings for specific languages.
"languages": {
@ -125,6 +145,9 @@
"C++": {
"tab_size": 2
},
"Elixir": {
"tab_size": 2
},
"Go": {
"tab_size": 4,
"hard_tabs": true
@ -145,15 +168,15 @@
"tab_size": 2
}
},
//LSP Specific settings.
// LSP Specific settings.
"lsp": {
//Specify the LSP name as a key here.
//As of 8/10/22, supported LSPs are:
//pyright
//gopls
//rust-analyzer
//typescript-language-server
//vscode-json-languageserver
// Specify the LSP name as a key here.
// As of 8/10/22, supported LSPs are:
// pyright
// gopls
// rust-analyzer
// typescript-language-server
// vscode-json-languageserver
// "rust_analyzer": {
// //These initialization options are merged into Zed's defaults
// "initialization_options": {

View file

@ -18,9 +18,3 @@ CREATE TABLE IF NOT EXISTS "signups" (
"email_address" VARCHAR,
"about" TEXT
);
INSERT INTO users (github_login, admin)
VALUES
('nathansobo', true),
('maxbrunsfeld', true),
('as-cii', true);

View file

@ -1,8 +1,7 @@
use clap::Parser;
use collab::{Error, Result};
use db::{Db, PostgresDb, UserId};
use rand::prelude::*;
use serde::Deserialize;
use serde::{de::DeserializeOwned, Deserialize};
use std::fmt::Write;
use time::{Duration, OffsetDateTime};
@ -10,62 +9,52 @@ use time::{Duration, OffsetDateTime};
#[path = "../db.rs"]
mod db;
#[derive(Parser)]
struct Args {
/// Seed users from GitHub.
#[clap(short, long)]
github_users: bool,
}
#[derive(Debug, Deserialize)]
struct GitHubUser {
id: usize,
login: String,
email: Option<String>,
}
#[tokio::main]
async fn main() {
let args = Args::parse();
let mut rng = StdRng::from_entropy();
let database_url = std::env::var("DATABASE_URL").expect("missing DATABASE_URL env var");
let db = PostgresDb::new(&database_url, 5)
.await
.expect("failed to connect to postgres database");
let github_token = std::env::var("GITHUB_TOKEN").expect("missing GITHUB_TOKEN env var");
let client = reqwest::Client::new();
let mut zed_users = vec![
("nathansobo".to_string(), Some("nathan@zed.dev")),
("maxbrunsfeld".to_string(), Some("max@zed.dev")),
("as-cii".to_string(), Some("antonio@zed.dev")),
("iamnbutler".to_string(), Some("nate@zed.dev")),
("gibusu".to_string(), Some("greg@zed.dev")),
("Kethku".to_string(), Some("keith@zed.dev")),
];
let current_user =
fetch_github::<GitHubUser>(&client, &github_token, "https://api.github.com/user").await;
let staff_users = fetch_github::<Vec<GitHubUser>>(
&client,
&github_token,
"https://api.github.com/orgs/zed-industries/teams/staff/members",
)
.await;
if args.github_users {
let github_token = std::env::var("GITHUB_TOKEN").expect("missing GITHUB_TOKEN env var");
let client = reqwest::Client::new();
let mut zed_users = Vec::new();
zed_users.push((current_user, true));
zed_users.extend(staff_users.into_iter().map(|user| (user, true)));
let user_count = db
.get_all_users(0, 200)
.await
.expect("failed to load users from db")
.len();
if user_count < 100 {
let mut last_user_id = None;
for page in 0..20 {
println!("Downloading users from GitHub, page {}", page);
for _ in 0..10 {
let mut uri = "https://api.github.com/users?per_page=100".to_string();
if let Some(last_user_id) = last_user_id {
write!(&mut uri, "&since={}", last_user_id).unwrap();
}
let response = client
.get(uri)
.bearer_auth(&github_token)
.header("user-agent", "zed")
.send()
.await
.expect("failed to fetch github users");
let users = response
.json::<Vec<GitHubUser>>()
.await
.expect("failed to deserialize github user");
zed_users.extend(users.iter().map(|user| (user.login.clone(), None)));
let users = fetch_github::<Vec<GitHubUser>>(&client, &github_token, &uri).await;
if let Some(last_user) = users.last() {
last_user_id = Some(last_user.id);
zed_users.extend(users.into_iter().map(|user| (user, false)));
} else {
break;
}
@ -73,16 +62,16 @@ async fn main() {
}
let mut zed_user_ids = Vec::<UserId>::new();
for (zed_user, email) in zed_users {
for (github_user, admin) in zed_users {
if let Some(user) = db
.get_user_by_github_login(&zed_user)
.get_user_by_github_login(&github_user.login)
.await
.expect("failed to fetch user")
{
zed_user_ids.push(user.id);
} else {
zed_user_ids.push(
db.create_user(&zed_user, email, true)
db.create_user(&github_user.login, github_user.email.as_deref(), admin)
.await
.expect("failed to insert user"),
);
@ -140,3 +129,21 @@ async fn main() {
.expect("failed to insert channel membership");
}
}
async fn fetch_github<T: DeserializeOwned>(
client: &reqwest::Client,
access_token: &str,
url: &str,
) -> T {
let response = client
.get(url)
.bearer_auth(&access_token)
.header("user-agent", "zed")
.send()
.await
.expect(&format!("failed to fetch '{}'", url));
response
.json()
.await
.expect(&format!("failed to deserialize github user from '{}'", url))
}

View file

@ -709,7 +709,7 @@ impl Db for PostgresDb {
user_durations.user_id = project_durations.user_id AND
user_durations.user_id = users.id AND
project_durations.project_id = project_collaborators.project_id
ORDER BY total_duration DESC, user_id ASC
ORDER BY total_duration DESC, user_id ASC, project_id ASC
";
let mut rows = sqlx::query_as::<_, (UserId, String, ProjectId, i64, i64)>(query)

View file

@ -1390,6 +1390,52 @@ async fn test_leaving_worktree_while_opening_buffer(
.await;
}
#[gpui::test(iterations = 10)]
async fn test_canceling_buffer_opening(
deterministic: Arc<Deterministic>,
cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext,
) {
deterministic.forbid_parking();
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
server
.make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
.await;
client_a
.fs
.insert_tree(
"/dir",
json!({
"a.txt": "abc",
}),
)
.await;
let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
let buffer_a = project_a
.update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
.await
.unwrap();
// Open a buffer as client B but cancel after a random amount of time.
let buffer_b = project_b.update(cx_b, |p, cx| p.open_buffer_by_id(buffer_a.id() as u64, cx));
deterministic.simulate_random_delay().await;
drop(buffer_b);
// Try opening the same buffer again as client B, and ensure we can
// still do it despite the cancellation above.
let buffer_b = project_b
.update(cx_b, |p, cx| p.open_buffer_by_id(buffer_a.id() as u64, cx))
.await
.unwrap();
buffer_b.read_with(cx_b, |buf, _| assert_eq!(buf.text(), "abc"));
}
#[gpui::test(iterations = 10)]
async fn test_leaving_project(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
cx_a.foreground().forbid_parking();

View file

@ -187,6 +187,7 @@ impl Server {
.add_request_handler(Server::forward_project_request::<proto::RenameProjectEntry>)
.add_request_handler(Server::forward_project_request::<proto::CopyProjectEntry>)
.add_request_handler(Server::forward_project_request::<proto::DeleteProjectEntry>)
.add_message_handler(Server::create_buffer_for_peer)
.add_request_handler(Server::update_buffer)
.add_message_handler(Server::update_buffer_file)
.add_message_handler(Server::buffer_reloaded)
@ -1186,6 +1187,18 @@ impl Server {
Ok(())
}
async fn create_buffer_for_peer(
self: Arc<Server>,
request: TypedEnvelope<proto::CreateBufferForPeer>,
) -> Result<()> {
self.peer.forward_send(
request.sender_id,
ConnectionId(request.payload.peer_id),
request.payload,
)?;
Ok(())
}
async fn update_buffer(
self: Arc<Server>,
request: TypedEnvelope<proto::UpdateBuffer>,

View file

@ -43,7 +43,9 @@ impl View for ContactFinder {
}
fn on_focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
cx.focus(&self.picker);
if cx.is_self_focused() {
cx.focus(&self.picker);
}
}
}

View file

@ -26,7 +26,7 @@ use std::{ops::DerefMut, sync::Arc};
use theme::IconButton;
use workspace::{sidebar::SidebarItem, JoinProject, ToggleProjectOnline, Workspace};
actions!(contacts_panel, [Toggle]);
actions!(contacts_panel, [ToggleFocus]);
impl_actions!(
contacts_panel,

View file

@ -42,7 +42,7 @@ use language::{
DiagnosticSeverity, IndentKind, IndentSize, Language, OffsetRangeExt, OffsetUtf16, Point,
Selection, SelectionGoal, TransactionId,
};
use link_go_to_definition::LinkGoToDefinitionState;
use link_go_to_definition::{hide_link_definition, LinkGoToDefinitionState};
pub use multi_buffer::{
Anchor, AnchorRangeExt, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, ToOffset,
ToPoint,
@ -1789,15 +1789,15 @@ impl Editor {
cx.notify();
}
pub fn are_selections_empty(&self) -> bool {
let pending_empty = match self.selections.pending_anchor() {
Some(Selection { start, end, .. }) => start == end,
None => true,
pub fn has_pending_nonempty_selection(&self) -> bool {
let pending_nonempty_selection = match self.selections.pending_anchor() {
Some(Selection { start, end, .. }) => start != end,
None => false,
};
pending_empty && self.columnar_selection_tail.is_none()
pending_nonempty_selection || self.columnar_selection_tail.is_some()
}
pub fn is_selecting(&self) -> bool {
pub fn has_pending_selection(&self) -> bool {
self.selections.pending_anchor().is_some() || self.columnar_selection_tail.is_some()
}
@ -6010,6 +6010,7 @@ impl View for Editor {
if let Some(editor) = handle.upgrade(cx) {
editor.update(cx, |editor, cx| {
hide_hover(editor, cx);
hide_link_definition(editor, cx);
})
}
});

View file

@ -110,13 +110,17 @@ impl EditorElement {
self.update_view(cx, |view, cx| view.snapshot(cx))
}
#[allow(clippy::too_many_arguments)]
fn mouse_down(
&self,
position: Vector2F,
alt: bool,
shift: bool,
mut click_count: usize,
MouseButtonEvent {
position,
ctrl,
alt,
shift,
cmd,
mut click_count,
..
}: MouseButtonEvent,
layout: &mut LayoutState,
paint: &mut PaintState,
cx: &mut EventContext,
@ -135,7 +139,7 @@ impl EditorElement {
position,
goal_column: target_position.column(),
}));
} else if shift {
} else if shift && !ctrl && !alt && !cmd {
cx.dispatch_action(Select(SelectPhase::Extend {
position,
click_count,
@ -179,14 +183,14 @@ impl EditorElement {
cx: &mut EventContext,
) -> bool {
let view = self.view(cx.app.as_ref());
let end_selection = view.is_selecting();
let selections_empty = view.are_selections_empty();
let end_selection = view.has_pending_selection();
let pending_nonempty_selections = view.has_pending_nonempty_selection();
if end_selection {
cx.dispatch_action(Select(SelectPhase::End));
}
if selections_empty && cmd && paint.text_bounds.contains_point(position) {
if !pending_nonempty_selections && cmd && paint.text_bounds.contains_point(position) {
let (point, target_point) =
paint.point_for_position(&self.snapshot(cx), layout, position);
@ -206,14 +210,38 @@ impl EditorElement {
fn mouse_dragged(
&self,
position: Vector2F,
MouseMovedEvent {
cmd,
shift,
position,
..
}: MouseMovedEvent,
layout: &mut LayoutState,
paint: &mut PaintState,
cx: &mut EventContext,
) -> bool {
let view = self.view(cx.app.as_ref());
// This will be handled more correctly once https://github.com/zed-industries/zed/issues/1218 is completed
// Don't trigger hover popover if mouse is hovering over context menu
let point = if paint.text_bounds.contains_point(position) {
let (point, target_point) =
paint.point_for_position(&self.snapshot(cx), layout, position);
if point == target_point {
Some(point)
} else {
None
}
} else {
None
};
if view.is_selecting() {
cx.dispatch_action(UpdateGoToDefinitionLink {
point,
cmd_held: cmd,
shift_held: shift,
});
let view = self.view(cx.app);
if view.has_pending_selection() {
let rect = paint.text_bounds;
let mut scroll_delta = Vector2F::zero();
@ -250,8 +278,11 @@ impl EditorElement {
scroll_position: (snapshot.scroll_position() + scroll_delta)
.clamp(Vector2F::zero(), layout.scroll_max),
}));
cx.dispatch_action(HoverAt { point });
true
} else {
cx.dispatch_action(HoverAt { point });
false
}
}
@ -1549,14 +1580,12 @@ impl Element for EditorElement {
}
match event {
&Event::MouseDown(MouseButtonEvent {
button: MouseButton::Left,
position,
alt,
shift,
click_count,
..
}) => self.mouse_down(position, alt, shift, click_count, layout, paint, cx),
&Event::MouseDown(
event @ MouseButtonEvent {
button: MouseButton::Left,
..
},
) => self.mouse_down(event, layout, paint, cx),
&Event::MouseDown(MouseButtonEvent {
button: MouseButton::Right,
@ -1572,16 +1601,18 @@ impl Element for EditorElement {
..
}) => self.mouse_up(position, cmd, shift, layout, paint, cx),
Event::MouseMoved(MouseMovedEvent {
pressed_button: Some(MouseButton::Left),
position,
..
}) => self.mouse_dragged(*position, layout, paint, cx),
Event::MouseMoved(
event @ MouseMovedEvent {
pressed_button: Some(MouseButton::Left),
..
},
) => self.mouse_dragged(*event, layout, paint, cx),
Event::ScrollWheel(ScrollWheelEvent {
position,
delta,
precise,
..
}) => self.scroll(*position, *delta, *precise, layout, paint, cx),
&Event::ModifiersChanged(event) => self.modifiers_changed(event, cx),
@ -1753,6 +1784,7 @@ pub enum CursorShape {
Bar,
Block,
Underscore,
Hollow,
}
impl Default for CursorShape {
@ -1800,7 +1832,7 @@ impl Cursor {
pub fn paint(&self, origin: Vector2F, cx: &mut PaintContext) {
let bounds = match self.shape {
CursorShape::Bar => RectF::new(self.origin + origin, vec2f(2.0, self.line_height)),
CursorShape::Block => RectF::new(
CursorShape::Block | CursorShape::Hollow => RectF::new(
self.origin + origin,
vec2f(self.block_width, self.line_height),
),
@ -1810,17 +1842,31 @@ impl Cursor {
),
};
cx.scene.push_quad(Quad {
bounds,
background: Some(self.color),
border: Border::new(0., Color::black()),
corner_radius: 0.,
});
//Draw background or border quad
if matches!(self.shape, CursorShape::Hollow) {
cx.scene.push_quad(Quad {
bounds,
background: None,
border: Border::all(1., self.color),
corner_radius: 0.,
});
} else {
cx.scene.push_quad(Quad {
bounds,
background: Some(self.color),
border: Default::default(),
corner_radius: 0.,
});
}
if let Some(block_text) = &self.block_text {
block_text.paint(self.origin + origin, bounds, self.line_height, cx);
}
}
pub fn shape(&self) -> CursorShape {
self.shape
}
}
#[derive(Debug)]

View file

@ -1,5 +1,6 @@
use crate::{
Anchor, Autoscroll, Editor, Event, ExcerptId, MultiBuffer, NavigationData, ToPoint as _,
link_go_to_definition::hide_link_definition, Anchor, Autoscroll, Editor, Event, ExcerptId,
MultiBuffer, NavigationData, ToPoint as _,
};
use anyhow::{anyhow, Result};
use futures::FutureExt;
@ -376,6 +377,11 @@ impl Item for Editor {
self.push_to_nav_history(selection.head(), None, cx);
}
fn workspace_deactivated(&mut self, cx: &mut ViewContext<Self>) {
hide_link_definition(self, cx);
self.link_go_to_definition_state.last_mouse_location = None;
}
fn is_dirty(&self, cx: &AppContext) -> bool {
self.buffer().read(cx).read(cx).is_dirty()
}
@ -397,7 +403,7 @@ impl Item for Editor {
let buffers = buffer.read(cx).all_buffers();
let mut timeout = cx.background().timer(FORMAT_TIMEOUT).fuse();
let format = project.update(cx, |project, cx| project.format(buffers, true, cx));
cx.spawn(|this, mut cx| async move {
cx.spawn(|_, mut cx| async move {
let transaction = futures::select_biased! {
_ = timeout => {
log::warn!("timed out waiting for formatting");
@ -406,9 +412,6 @@ impl Item for Editor {
transaction = format.log_err().fuse() => transaction,
};
this.update(&mut cx, |editor, cx| {
editor.request_autoscroll(Autoscroll::Fit, cx)
});
buffer
.update(&mut cx, |buffer, cx| {
if let Some(transaction) = transaction {

View file

@ -52,7 +52,7 @@ pub fn init(cx: &mut MutableAppContext) {
cx.add_action(go_to_fetched_type_definition);
}
#[derive(Default)]
#[derive(Debug, Default)]
pub struct LinkGoToDefinitionState {
pub last_mouse_location: Option<Anchor>,
pub symbol_range: Option<Range<Anchor>>,
@ -70,6 +70,8 @@ pub fn update_go_to_definition_link(
}: &UpdateGoToDefinitionLink,
cx: &mut ViewContext<Editor>,
) {
let pending_nonempty_selection = editor.has_pending_nonempty_selection();
// Store new mouse point as an anchor
let snapshot = editor.snapshot(cx);
let point = point.map(|point| {
@ -89,6 +91,12 @@ pub fn update_go_to_definition_link(
}
editor.link_go_to_definition_state.last_mouse_location = point.clone();
if pending_nonempty_selection {
hide_link_definition(editor, cx);
return;
}
if cmd_held {
if let Some(point) = point {
let kind = if shift_held {
@ -113,12 +121,14 @@ pub fn cmd_shift_changed(
}: &CmdShiftChanged,
cx: &mut ViewContext<Editor>,
) {
let pending_selection = editor.has_pending_selection();
if let Some(point) = editor
.link_go_to_definition_state
.last_mouse_location
.clone()
{
if cmd_down {
if cmd_down && !pending_selection {
let snapshot = editor.snapshot(cx);
let kind = if shift_down {
LinkDefinitionKind::Type
@ -127,10 +137,11 @@ pub fn cmd_shift_changed(
};
show_link_definition(kind, editor, point, snapshot, cx);
} else {
hide_link_definition(editor, cx)
return;
}
}
hide_link_definition(editor, cx)
}
#[derive(Debug, Clone, Copy, PartialEq)]
@ -243,28 +254,32 @@ pub fn show_link_definition(
this.link_go_to_definition_state.definitions = definitions.clone();
let buffer_snapshot = buffer.read(cx).snapshot();
// Only show highlight if there exists a definition to jump to that doesn't contain
// the current location.
if definitions.iter().any(|definition| {
let target = &definition.target;
if target.buffer == buffer {
let range = &target.range;
// Expand range by one character as lsp definition ranges include positions adjacent
// but not contained by the symbol range
let start = buffer_snapshot.clip_offset(
range.start.to_offset(&buffer_snapshot).saturating_sub(1),
Bias::Left,
);
let end = buffer_snapshot.clip_offset(
range.end.to_offset(&buffer_snapshot) + 1,
Bias::Right,
);
let offset = buffer_position.to_offset(&buffer_snapshot);
!(start <= offset && end >= offset)
} else {
true
}
}) {
let any_definition_does_not_contain_current_location =
definitions.iter().any(|definition| {
let target = &definition.target;
if target.buffer == buffer {
let range = &target.range;
// Expand range by one character as lsp definition ranges include positions adjacent
// but not contained by the symbol range
let start = buffer_snapshot.clip_offset(
range.start.to_offset(&buffer_snapshot).saturating_sub(1),
Bias::Left,
);
let end = buffer_snapshot.clip_offset(
range.end.to_offset(&buffer_snapshot) + 1,
Bias::Right,
);
let offset = buffer_position.to_offset(&buffer_snapshot);
!(start <= offset && end >= offset)
} else {
true
}
});
if any_definition_does_not_contain_current_location {
// If no symbol range returned from language server, use the surrounding word.
let highlight_range = symbol_range.unwrap_or_else(|| {
let snapshot = &snapshot.buffer_snapshot;
@ -280,7 +295,7 @@ pub fn show_link_definition(
vec![highlight_range],
style,
cx,
)
);
} else {
hide_link_definition(this, cx);
}
@ -706,7 +721,34 @@ mod tests {
fn do_work() { «test»(); }
"});
// Moving within symbol range doesn't re-request
// Deactivating the window dismisses the highlight
cx.update_workspace(|workspace, cx| {
workspace.on_window_activation_changed(false, cx);
});
cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
fn test() { do_work(); }
fn do_work() { test(); }
"});
// Moving the mouse restores the highlights.
cx.update_editor(|editor, cx| {
update_go_to_definition_link(
editor,
&UpdateGoToDefinitionLink {
point: Some(hover_point),
cmd_held: true,
shift_held: false,
},
cx,
);
});
cx.foreground().run_until_parked();
cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
fn test() { do_work(); }
fn do_work() { «test»(); }
"});
// Moving again within the same symbol range doesn't re-request
let hover_point = cx.display_point(indoc! {"
fn test() { do_work(); }
fn do_work() { tesˇt(); }
@ -779,5 +821,59 @@ mod tests {
fn test() { do_work(); }
fn «do_workˇ»() { test(); }
"});
// 1. We have a pending selection, mouse point is over a symbol that we have a response for, hitting cmd and nothing happens
// 2. Selection is completed, hovering
let hover_point = cx.display_point(indoc! {"
fn test() { do_wˇork(); }
fn do_work() { test(); }
"});
let target_range = cx.lsp_range(indoc! {"
fn test() { do_work(); }
fn «do_work»() { test(); }
"});
let mut requests = cx.handle_request::<GotoDefinition, _, _>(move |url, _, _| async move {
Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
lsp::LocationLink {
origin_selection_range: None,
target_uri: url,
target_range,
target_selection_range: target_range,
},
])))
});
// create a pending selection
let selection_range = cx.ranges(indoc! {"
fn «test() { do_w»ork(); }
fn do_work() { test(); }
"})[0]
.clone();
cx.update_editor(|editor, cx| {
let snapshot = editor.buffer().read(cx).snapshot(cx);
let anchor_range = snapshot.anchor_before(selection_range.start)
..snapshot.anchor_after(selection_range.end);
editor.change_selections(Some(crate::Autoscroll::Fit), cx, |s| {
s.set_pending_anchor_range(anchor_range, crate::SelectMode::Character)
});
});
cx.update_editor(|editor, cx| {
update_go_to_definition_link(
editor,
&UpdateGoToDefinitionLink {
point: Some(hover_point),
cmd_held: true,
shift_held: false,
},
cx,
);
});
cx.foreground().run_until_parked();
assert!(requests.try_next().is_err());
cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
fn test() { do_work(); }
fn do_work() { test(); }
"});
cx.foreground().run_until_parked();
}
}

View file

@ -183,7 +183,7 @@ impl<'a> EditorTestContext<'a> {
}
}
fn ranges(&self, marked_text: &str) -> Vec<Range<usize>> {
pub fn ranges(&self, marked_text: &str) -> Vec<Range<usize>> {
let (unmarked_text, ranges) = marked_text_ranges(marked_text, false);
assert_eq!(self.buffer_text(), unmarked_text);
ranges

View file

@ -344,7 +344,14 @@ impl WindowInputHandler {
where
F: FnOnce(&dyn AnyView, &AppContext) -> T,
{
let app = self.app.borrow();
// Input-related application hooks are sometimes called by the OS during
// a call to a window-manipulation API, like prompting the user for file
// paths. In that case, the AppContext will already be borrowed, so any
// InputHandler methods need to fail gracefully.
//
// See https://github.com/zed-industries/feedback/issues/444
let app = self.app.try_borrow().ok()?;
let view_id = app.focused_view_id(self.window_id)?;
let view = app.cx.views.get(&(self.window_id, view_id))?;
let result = f(view.as_ref(), &app);
@ -355,7 +362,7 @@ impl WindowInputHandler {
where
F: FnOnce(usize, usize, &mut dyn AnyView, &mut MutableAppContext) -> T,
{
let mut app = self.app.borrow_mut();
let mut app = self.app.try_borrow_mut().ok()?;
app.update(|app| {
let view_id = app.focused_view_id(self.window_id)?;
let mut view = app.cx.views.remove(&(self.window_id, view_id))?;
@ -1318,6 +1325,11 @@ impl MutableAppContext {
window.zoom();
}
pub fn toggle_window_full_screen(&self, window_id: usize) {
let (_, window) = &self.presenters_and_platform_windows[&window_id];
window.toggle_full_screen();
}
fn prompt(
&self,
window_id: usize,
@ -3682,6 +3694,10 @@ impl<'a, T: View> ViewContext<'a, T> {
self.app.zoom_window(self.window_id)
}
pub fn toggle_full_screen(&self) {
self.app.toggle_window_full_screen(self.window_id)
}
pub fn prompt(
&self,
level: PromptLevel,

View file

@ -293,6 +293,7 @@ impl Element for Flex {
position,
delta,
precise,
..
}) = event
{
if *remaining_space < 0. && bounds.contains_point(position) {

View file

@ -316,6 +316,7 @@ impl Element for List {
position,
delta,
precise,
..
}) = event
{
if bounds.contains_point(*position)

View file

@ -315,6 +315,7 @@ impl Element for UniformList {
position,
delta,
precise,
..
}) = event
{
if bounds.contains_point(*position)

View file

@ -381,6 +381,17 @@ impl Deterministic {
state.forbid_parking = true;
state.rng = StdRng::seed_from_u64(state.seed);
}
pub async fn simulate_random_delay(&self) {
use rand::prelude::*;
use smol::future::yield_now;
if self.state.lock().rng.gen_bool(0.2) {
let yields = self.state.lock().rng.gen_range(1..=10);
for _ in 0..yields {
yield_now().await;
}
}
}
}
impl Drop for Timer {
@ -662,17 +673,9 @@ impl Background {
#[cfg(any(test, feature = "test-support"))]
pub async fn simulate_random_delay(&self) {
use rand::prelude::*;
use smol::future::yield_now;
match self {
Self::Deterministic { executor, .. } => {
if executor.state.lock().rng.gen_bool(0.2) {
let yields = executor.state.lock().rng.gen_range(1..=10);
for _ in 0..yields {
yield_now().await;
}
}
executor.simulate_random_delay().await;
}
_ => {
panic!("this method can only be called on a deterministic executor")

View file

@ -123,6 +123,7 @@ pub trait Window: WindowContext {
fn show_character_palette(&self);
fn minimize(&self);
fn zoom(&self);
fn toggle_full_screen(&self);
}
pub trait WindowContext {

View file

@ -24,6 +24,10 @@ pub struct ScrollWheelEvent {
pub position: Vector2F,
pub delta: Vector2F,
pub precise: bool,
pub ctrl: bool,
pub alt: bool,
pub shift: bool,
pub cmd: bool,
}
#[derive(Hash, PartialEq, Eq, Copy, Clone, Debug)]
@ -64,7 +68,7 @@ impl Default for MouseButton {
}
}
#[derive(Clone, Debug, Default)]
#[derive(Clone, Copy, Debug, Default)]
pub struct MouseButtonEvent {
pub button: MouseButton,
pub position: Vector2F,

View file

@ -148,6 +148,8 @@ impl Event {
})
}
NSEventType::NSScrollWheel => window_height.map(|window_height| {
let modifiers = native_event.modifierFlags();
Self::ScrollWheel(ScrollWheelEvent {
position: vec2f(
native_event.locationInWindow().x as f32,
@ -158,6 +160,10 @@ impl Event {
native_event.scrollingDeltaY() as f32,
),
precise: native_event.hasPreciseScrollingDeltas() == YES,
ctrl: modifiers.contains(NSEventModifierFlags::NSControlKeyMask),
alt: modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask),
shift: modifiers.contains(NSEventModifierFlags::NSShiftKeyMask),
cmd: modifiers.contains(NSEventModifierFlags::NSCommandKeyMask),
})
}),
NSEventType::NSLeftMouseDragged

View file

@ -278,6 +278,18 @@ unsafe fn build_classes() {
pub struct Window(Rc<RefCell<WindowState>>);
///Used to track what the IME does when we send it a keystroke.
///This is only used to handle the case where the IME mysteriously
///swallows certain keys.
///
///Basically a direct copy of the approach that WezTerm uses in:
///github.com/wez/wezterm : d5755f3e : window/src/os/macos/window.rs
enum ImeState {
Continue,
Acted,
None,
}
struct WindowState {
id: usize,
native_window: id,
@ -299,6 +311,10 @@ struct WindowState {
layer: id,
traffic_light_position: Option<Vector2F>,
previous_modifiers_changed_event: Option<Event>,
//State tracking what the IME did after the last request
ime_state: ImeState,
//Retains the last IME Text
ime_text: Option<String>,
}
struct InsertText {
@ -395,6 +411,8 @@ impl Window {
layer,
traffic_light_position: options.traffic_light_position,
previous_modifiers_changed_event: None,
ime_state: ImeState::None,
ime_text: None,
})));
(*native_window).set_ivar(
@ -458,9 +476,15 @@ impl Window {
impl Drop for Window {
fn drop(&mut self) {
unsafe {
self.0.as_ref().borrow().native_window.close();
}
let this = self.0.borrow();
let window = this.native_window;
this.executor
.spawn(async move {
unsafe {
window.close();
}
})
.detach();
}
}
@ -601,6 +625,18 @@ impl platform::Window for Window {
})
.detach();
}
fn toggle_full_screen(&self) {
let this = self.0.borrow();
let window = this.native_window;
this.executor
.spawn(async move {
unsafe {
window.toggleFullScreen_(nil);
}
})
.detach();
}
}
impl platform::WindowContext for Window {
@ -746,6 +782,7 @@ extern "C" fn handle_key_event(this: &Object, native_event: id, key_equivalent:
let mut window_state_borrow = window_state.as_ref().borrow_mut();
let event = unsafe { Event::from_native(native_event, Some(window_state_borrow.size().y())) };
if let Some(event) = event {
if key_equivalent {
window_state_borrow.performed_key_equivalent = true;
@ -766,12 +803,12 @@ extern "C" fn handle_key_event(this: &Object, native_event: id, key_equivalent:
} else {
window_state_borrow.last_fresh_keydown = Some(keydown);
}
function_is_held = event.keystroke.function;
Some((event, None))
}
_ => return NO,
};
drop(window_state_borrow);
if !function_is_held {
@ -783,7 +820,9 @@ extern "C" fn handle_key_event(this: &Object, native_event: id, key_equivalent:
let mut handled = false;
let mut window_state_borrow = window_state.borrow_mut();
let ime_text = window_state_borrow.ime_text.clone();
if let Some((event, insert_text)) = window_state_borrow.pending_key_down.take() {
let is_held = event.is_held;
if let Some(mut callback) = window_state_borrow.event_callback.take() {
drop(window_state_borrow);
@ -802,6 +841,18 @@ extern "C" fn handle_key_event(this: &Object, native_event: id, key_equivalent:
input_handler
.replace_text_in_range(insert.replacement_range, &insert.text)
});
} else if !is_composing && is_held {
if let Some(last_insert_text) = ime_text {
//MacOS IME is a bit funky, and even when you've told it there's nothing to
//inter it will still swallow certain keys (e.g. 'f', 'j') and not others
//(e.g. 'n'). This is a problem for certain kinds of views, like the terminal
with_input_handler(this, |input_handler| {
if input_handler.selected_text_range().is_none() {
handled = true;
input_handler.replace_text_in_range(None, &last_insert_text)
}
});
}
}
}
@ -1114,7 +1165,6 @@ extern "C" fn first_rect_for_character_range(
let window = get_window_state(this).borrow().native_window;
NSView::frame(window)
};
with_input_handler(this, |input_handler| {
input_handler.rect_for_range(range.to_range()?)
})
@ -1152,27 +1202,25 @@ extern "C" fn insert_text(this: &Object, _: Sel, text: id, replacement_range: NS
.unwrap();
let replacement_range = replacement_range.to_range();
window_state.borrow_mut().ime_text = Some(text.to_string());
window_state.borrow_mut().ime_state = ImeState::Acted;
let is_composing =
with_input_handler(this, |input_handler| input_handler.marked_text_range())
.flatten()
.is_some();
match pending_key_down {
None | Some(_) if is_composing || text.chars().count() > 1 => {
with_input_handler(this, |input_handler| {
input_handler.replace_text_in_range(replacement_range, text)
});
}
Some(mut pending_key_down) => {
pending_key_down.1 = Some(InsertText {
replacement_range,
text: text.to_string(),
});
window_state.borrow_mut().pending_key_down = Some(pending_key_down);
}
_ => unreachable!(),
if is_composing || text.chars().count() > 1 || pending_key_down.is_none() {
with_input_handler(this, |input_handler| {
input_handler.replace_text_in_range(replacement_range, text)
});
} else {
let mut pending_key_down = pending_key_down.unwrap();
pending_key_down.1 = Some(InsertText {
replacement_range,
text: text.to_string(),
});
window_state.borrow_mut().pending_key_down = Some(pending_key_down);
}
}
}
@ -1185,7 +1233,8 @@ extern "C" fn set_marked_text(
replacement_range: NSRange,
) {
unsafe {
get_window_state(this).borrow_mut().pending_key_down.take();
let window_state = get_window_state(this);
window_state.borrow_mut().pending_key_down.take();
let is_attributed_string: BOOL =
msg_send![text, isKindOfClass: [class!(NSAttributedString)]];
@ -1200,6 +1249,9 @@ extern "C" fn set_marked_text(
.to_str()
.unwrap();
window_state.borrow_mut().ime_state = ImeState::Acted;
window_state.borrow_mut().ime_text = Some(text.to_string());
with_input_handler(this, |input_handler| {
input_handler.replace_and_mark_text_in_range(replacement_range, text, selected_range);
});
@ -1207,6 +1259,13 @@ extern "C" fn set_marked_text(
}
extern "C" fn unmark_text(this: &Object, _: Sel) {
unsafe {
let state = get_window_state(this);
let mut borrow = state.borrow_mut();
borrow.ime_state = ImeState::Acted;
borrow.ime_text.take();
}
with_input_handler(this, |input_handler| input_handler.unmark_text());
}
@ -1233,7 +1292,14 @@ extern "C" fn attributed_substring_for_proposed_range(
.unwrap_or(nil)
}
extern "C" fn do_command_by_selector(_: &Object, _: Sel, _: Sel) {}
extern "C" fn do_command_by_selector(this: &Object, _: Sel, _: Sel) {
unsafe {
let state = get_window_state(this);
let mut borrow = state.borrow_mut();
borrow.ime_state = ImeState::Continue;
borrow.ime_text.take();
}
}
async fn synthetic_drag(
window_state: Weak<RefCell<WindowState>>,

View file

@ -294,6 +294,8 @@ impl super::Window for Window {
fn minimize(&self) {}
fn zoom(&self) {}
fn toggle_full_screen(&self) {}
}
pub fn platform() -> Platform {

View file

@ -321,6 +321,7 @@ impl Presenter {
self.last_mouse_moved_event = Some(event.clone());
}
_ => {}
}

View file

@ -1,6 +1,7 @@
use std::{any::TypeId, mem::Discriminant, rc::Rc};
use collections::HashMap;
use pathfinder_geometry::rect::RectF;
use crate::{EventContext, MouseButton};
@ -111,6 +112,14 @@ impl MouseRegion {
self.handlers = self.handlers.on_hover(handler);
self
}
pub fn on_move(
mut self,
handler: impl Fn(MoveRegionEvent, &mut EventContext) + 'static,
) -> Self {
self.handlers = self.handlers.on_move(handler);
self
}
}
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]

View file

@ -358,7 +358,7 @@ impl Buffer {
pub fn from_proto(
replica_id: ReplicaId,
message: proto::BufferState,
message: proto::Buffer,
file: Option<Arc<dyn File>>,
cx: &mut ModelContext<Self>,
) -> Result<Self> {
@ -406,7 +406,7 @@ impl Buffer {
Ok(this)
}
pub fn to_proto(&self) -> proto::BufferState {
pub fn to_proto(&self) -> proto::Buffer {
let mut operations = self
.text
.history()
@ -414,7 +414,7 @@ impl Buffer {
.chain(self.deferred_ops.iter().map(proto::serialize_operation))
.collect::<Vec<_>>();
operations.sort_unstable_by_key(proto::lamport_timestamp_for_operation);
proto::BufferState {
proto::Buffer {
id: self.remote_id(),
file: self.file.as_ref().map(|f| f.to_proto()),
base_text: self.base_text().to_string(),

View file

@ -9,7 +9,7 @@ use rpc::proto;
use std::{ops::Range, sync::Arc};
use text::*;
pub use proto::{Buffer, BufferState, LineEnding, SelectionSet};
pub use proto::{Buffer, LineEnding, SelectionSet};
pub fn deserialize_line_ending(message: proto::LineEnding) -> text::LineEnding {
match message {
@ -39,7 +39,6 @@ pub fn serialize_operation(operation: &Operation) -> proto::Operation {
local_timestamp: undo.id.value,
lamport_timestamp: lamport_timestamp.value,
version: serialize_version(&undo.version),
transaction_version: serialize_version(&undo.transaction_version),
counts: undo
.counts
.iter()
@ -199,7 +198,6 @@ pub fn deserialize_operation(message: proto::Operation) -> Result<Operation> {
)
})
.collect(),
transaction_version: deserialize_version(undo.transaction_version),
},
}),
proto::operation::Variant::UpdateSelections(message) => {

View file

@ -522,11 +522,10 @@ async fn location_links_from_proto(
for link in proto_links {
let origin = match link.origin {
Some(origin) => {
let buffer = origin
.buffer
.ok_or_else(|| anyhow!("missing origin buffer"))?;
let buffer = project
.update(&mut cx, |this, cx| this.deserialize_buffer(buffer, cx))
.update(&mut cx, |this, cx| {
this.wait_for_buffer(origin.buffer_id, cx)
})
.await?;
let start = origin
.start
@ -548,9 +547,10 @@ async fn location_links_from_proto(
};
let target = link.target.ok_or_else(|| anyhow!("missing target"))?;
let buffer = target.buffer.ok_or_else(|| anyhow!("missing buffer"))?;
let buffer = project
.update(&mut cx, |this, cx| this.deserialize_buffer(buffer, cx))
.update(&mut cx, |this, cx| {
this.wait_for_buffer(target.buffer_id, cx)
})
.await?;
let start = target
.start
@ -664,19 +664,19 @@ fn location_links_to_proto(
.into_iter()
.map(|definition| {
let origin = definition.origin.map(|origin| {
let buffer = project.serialize_buffer_for_peer(&origin.buffer, peer_id, cx);
let buffer_id = project.create_buffer_for_peer(&origin.buffer, peer_id, cx);
proto::Location {
start: Some(serialize_anchor(&origin.range.start)),
end: Some(serialize_anchor(&origin.range.end)),
buffer: Some(buffer),
buffer_id,
}
});
let buffer = project.serialize_buffer_for_peer(&definition.target.buffer, peer_id, cx);
let buffer_id = project.create_buffer_for_peer(&definition.target.buffer, peer_id, cx);
let target = proto::Location {
start: Some(serialize_anchor(&definition.target.range.start)),
end: Some(serialize_anchor(&definition.target.range.end)),
buffer: Some(buffer),
buffer_id,
};
proto::LocationLink {
@ -792,11 +792,11 @@ impl LspCommand for GetReferences {
let locations = response
.into_iter()
.map(|definition| {
let buffer = project.serialize_buffer_for_peer(&definition.buffer, peer_id, cx);
let buffer_id = project.create_buffer_for_peer(&definition.buffer, peer_id, cx);
proto::Location {
start: Some(serialize_anchor(&definition.range.start)),
end: Some(serialize_anchor(&definition.range.end)),
buffer: Some(buffer),
buffer_id,
}
})
.collect();
@ -812,9 +812,10 @@ impl LspCommand for GetReferences {
) -> Result<Vec<Location>> {
let mut locations = Vec::new();
for location in message.locations {
let buffer = location.buffer.ok_or_else(|| anyhow!("missing buffer"))?;
let target_buffer = project
.update(&mut cx, |this, cx| this.deserialize_buffer(buffer, cx))
.update(&mut cx, |this, cx| {
this.wait_for_buffer(location.buffer_id, cx)
})
.await?;
let start = location
.start

View file

@ -112,7 +112,7 @@ pub struct Project {
collaborators: HashMap<PeerId, Collaborator>,
client_subscriptions: Vec<client::Subscription>,
_subscriptions: Vec<gpui::Subscription>,
opened_buffer: (Rc<RefCell<watch::Sender<()>>>, watch::Receiver<()>),
opened_buffer: (watch::Sender<()>, watch::Receiver<()>),
shared_buffers: HashMap<PeerId, HashSet<u64>>,
#[allow(clippy::type_complexity)]
loading_buffers: HashMap<
@ -202,6 +202,7 @@ pub enum Event {
pub enum LanguageServerState {
Starting(Task<Option<Arc<LanguageServer>>>),
Running {
language: Arc<Language>,
adapter: Arc<CachedLspAdapter>,
server: Arc<LanguageServer>,
},
@ -375,6 +376,7 @@ impl Project {
client.add_model_message_handler(Self::handle_update_project);
client.add_model_message_handler(Self::handle_unregister_project);
client.add_model_message_handler(Self::handle_project_unshared);
client.add_model_message_handler(Self::handle_create_buffer_for_peer);
client.add_model_message_handler(Self::handle_update_buffer_file);
client.add_model_message_handler(Self::handle_update_buffer);
client.add_model_message_handler(Self::handle_update_diagnostic_summary);
@ -454,7 +456,6 @@ impl Project {
let handle = cx.weak_handle();
project_store.update(cx, |store, cx| store.add_project(handle, cx));
let (opened_buffer_tx, opened_buffer_rx) = watch::channel();
Self {
worktrees: Default::default(),
collaborators: Default::default(),
@ -472,7 +473,7 @@ impl Project {
_maintain_remote_id,
_maintain_online_status,
},
opened_buffer: (Rc::new(RefCell::new(opened_buffer_tx)), opened_buffer_rx),
opened_buffer: watch::channel(),
client_subscriptions: Vec::new(),
_subscriptions: vec![cx.observe_global::<Settings, _>(Self::on_settings_changed)],
_maintain_buffer_languages: Self::maintain_buffer_languages(&languages, cx),
@ -540,7 +541,6 @@ impl Project {
worktrees.push(worktree);
}
let (opened_buffer_tx, opened_buffer_rx) = watch::channel();
let this = cx.add_model(|cx: &mut ModelContext<Self>| {
let handle = cx.weak_handle();
project_store.update(cx, |store, cx| store.add_project(handle, cx));
@ -548,7 +548,7 @@ impl Project {
let mut this = Self {
worktrees: Vec::new(),
loading_buffers: Default::default(),
opened_buffer: (Rc::new(RefCell::new(opened_buffer_tx)), opened_buffer_rx),
opened_buffer: watch::channel(),
shared_buffers: Default::default(),
loading_local_worktrees: Default::default(),
active_entry: None,
@ -1624,9 +1624,10 @@ impl Project {
path: path_string,
})
.await?;
let buffer = response.buffer.ok_or_else(|| anyhow!("missing buffer"))?;
this.update(&mut cx, |this, cx| this.deserialize_buffer(buffer, cx))
.await
this.update(&mut cx, |this, cx| {
this.wait_for_buffer(response.buffer_id, cx)
})
.await
})
}
@ -1684,11 +1685,8 @@ impl Project {
.client
.request(proto::OpenBufferById { project_id, id });
cx.spawn(|this, mut cx| async move {
let buffer = request
.await?
.buffer
.ok_or_else(|| anyhow!("invalid buffer"))?;
this.update(&mut cx, |this, cx| this.deserialize_buffer(buffer, cx))
let buffer_id = request.await?.buffer_id;
this.update(&mut cx, |this, cx| this.wait_for_buffer(buffer_id, cx))
.await
})
} else {
@ -1800,6 +1798,7 @@ impl Project {
})
.detach();
*self.opened_buffer.0.borrow_mut() = ();
Ok(())
}
@ -1971,7 +1970,7 @@ impl Project {
uri: lsp::Url::from_file_path(abs_path).unwrap(),
};
for (_, server) in self.language_servers_for_worktree(worktree_id) {
for (_, _, server) in self.language_servers_for_worktree(worktree_id) {
server
.notify::<lsp::notification::DidSaveTextDocument>(
lsp::DidSaveTextDocumentParams {
@ -2006,15 +2005,18 @@ impl Project {
fn language_servers_for_worktree(
&self,
worktree_id: WorktreeId,
) -> impl Iterator<Item = (&Arc<CachedLspAdapter>, &Arc<LanguageServer>)> {
) -> impl Iterator<Item = (&Arc<CachedLspAdapter>, &Arc<Language>, &Arc<LanguageServer>)> {
self.language_server_ids
.iter()
.filter_map(move |((language_server_worktree_id, _), id)| {
if *language_server_worktree_id == worktree_id {
if let Some(LanguageServerState::Running { adapter, server }) =
self.language_servers.get(id)
if let Some(LanguageServerState::Running {
adapter,
language,
server,
}) = self.language_servers.get(id)
{
return Some((adapter, server));
return Some((adapter, language, server));
}
}
None
@ -2284,6 +2286,7 @@ impl Project {
server_id,
LanguageServerState::Running {
adapter: adapter.clone(),
language,
server: language_server.clone(),
},
);
@ -3316,10 +3319,14 @@ impl Project {
.worktree_for_id(worktree_id, cx)
.and_then(|worktree| worktree.read(cx).as_local())
{
if let Some(LanguageServerState::Running { adapter, server }) =
self.language_servers.get(server_id)
if let Some(LanguageServerState::Running {
adapter,
language,
server,
}) = self.language_servers.get(server_id)
{
let adapter = adapter.clone();
let language = language.clone();
let worktree_abs_path = worktree.abs_path().clone();
requests.push(
server
@ -3333,6 +3340,7 @@ impl Project {
.map(move |response| {
(
adapter,
language,
worktree_id,
worktree_abs_path,
response.unwrap_or_default(),
@ -3352,7 +3360,14 @@ impl Project {
};
let symbols = this.read_with(&cx, |this, cx| {
let mut symbols = Vec::new();
for (adapter, source_worktree_id, worktree_abs_path, response) in responses {
for (
adapter,
adapter_language,
source_worktree_id,
worktree_abs_path,
response,
) in responses
{
symbols.extend(response.into_iter().flatten().filter_map(|lsp_symbol| {
let abs_path = lsp_symbol.location.uri.to_file_path().ok()?;
let mut worktree_id = source_worktree_id;
@ -3371,16 +3386,15 @@ impl Project {
path: path.into(),
};
let signature = this.symbol_signature(&project_path);
let language = this.languages.select_language(&project_path.path);
let language = this
.languages
.select_language(&project_path.path)
.unwrap_or(adapter_language.clone());
let language_server_name = adapter.name.clone();
Some(async move {
let label = if let Some(language) = language {
language
.label_for_symbol(&lsp_symbol.name, lsp_symbol.kind)
.await
} else {
None
};
let label = language
.label_for_symbol(&lsp_symbol.name, lsp_symbol.kind)
.await;
Symbol {
language_server_name,
@ -3476,9 +3490,10 @@ impl Project {
});
cx.spawn(|this, mut cx| async move {
let response = request.await?;
let buffer = response.buffer.ok_or_else(|| anyhow!("invalid buffer"))?;
this.update(&mut cx, |this, cx| this.deserialize_buffer(buffer, cx))
.await
this.update(&mut cx, |this, cx| {
this.wait_for_buffer(response.buffer_id, cx)
})
.await
})
} else {
Task::ready(Err(anyhow!("project does not have a remote id")))
@ -4294,9 +4309,10 @@ impl Project {
let response = request.await?;
let mut result = HashMap::default();
for location in response.locations {
let buffer = location.buffer.ok_or_else(|| anyhow!("missing buffer"))?;
let target_buffer = this
.update(&mut cx, |this, cx| this.deserialize_buffer(buffer, cx))
.update(&mut cx, |this, cx| {
this.wait_for_buffer(location.buffer_id, cx)
})
.await?;
let start = location
.start
@ -5107,6 +5123,36 @@ impl Project {
})
}
async fn handle_create_buffer_for_peer(
this: ModelHandle<Self>,
envelope: TypedEnvelope<proto::CreateBufferForPeer>,
_: Arc<Client>,
mut cx: AsyncAppContext,
) -> Result<()> {
this.update(&mut cx, |this, cx| {
let mut buffer = envelope
.payload
.buffer
.ok_or_else(|| anyhow!("invalid buffer"))?;
let mut buffer_file = None;
if let Some(file) = buffer.file.take() {
let worktree_id = WorktreeId::from_proto(file.worktree_id);
let worktree = this
.worktree_for_id(worktree_id, cx)
.ok_or_else(|| anyhow!("no worktree found for id {}", file.worktree_id))?;
buffer_file = Some(Arc::new(File::from_proto(file, worktree.clone(), cx)?)
as Arc<dyn language::File>);
}
let buffer = cx.add_model(|cx| {
Buffer::from_proto(this.replica_id(), buffer, buffer_file, cx).unwrap()
});
this.register_buffer(&buffer, cx)?;
Ok(())
})
}
async fn handle_update_buffer_file(
this: ModelHandle<Self>,
envelope: TypedEnvelope<proto::UpdateBufferFile>,
@ -5448,9 +5494,9 @@ impl Project {
for range in ranges {
let start = serialize_anchor(&range.start);
let end = serialize_anchor(&range.end);
let buffer = this.serialize_buffer_for_peer(&buffer, peer_id, cx);
let buffer_id = this.create_buffer_for_peer(&buffer, peer_id, cx);
locations.push(proto::Location {
buffer: Some(buffer),
buffer_id,
start: Some(start),
end: Some(end),
});
@ -5487,9 +5533,9 @@ impl Project {
.await?;
Ok(proto::OpenBufferForSymbolResponse {
buffer: Some(this.update(&mut cx, |this, cx| {
this.serialize_buffer_for_peer(&buffer, peer_id, cx)
})),
buffer_id: this.update(&mut cx, |this, cx| {
this.create_buffer_for_peer(&buffer, peer_id, cx)
}),
})
}
@ -5515,7 +5561,7 @@ impl Project {
.await?;
this.update(&mut cx, |this, cx| {
Ok(proto::OpenBufferResponse {
buffer: Some(this.serialize_buffer_for_peer(&buffer, peer_id, cx)),
buffer_id: this.create_buffer_for_peer(&buffer, peer_id, cx),
})
})
}
@ -5541,7 +5587,7 @@ impl Project {
let buffer = open_buffer.await?;
this.update(&mut cx, |this, cx| {
Ok(proto::OpenBufferResponse {
buffer: Some(this.serialize_buffer_for_peer(&buffer, peer_id, cx)),
buffer_id: this.create_buffer_for_peer(&buffer, peer_id, cx),
})
})
}
@ -5553,13 +5599,13 @@ impl Project {
cx: &AppContext,
) -> proto::ProjectTransaction {
let mut serialized_transaction = proto::ProjectTransaction {
buffers: Default::default(),
buffer_ids: Default::default(),
transactions: Default::default(),
};
for (buffer, transaction) in project_transaction.0 {
serialized_transaction
.buffers
.push(self.serialize_buffer_for_peer(&buffer, peer_id, cx));
.buffer_ids
.push(self.create_buffer_for_peer(&buffer, peer_id, cx));
serialized_transaction
.transactions
.push(language::proto::serialize_transaction(&transaction));
@ -5575,9 +5621,10 @@ impl Project {
) -> Task<Result<ProjectTransaction>> {
cx.spawn(|this, mut cx| async move {
let mut project_transaction = ProjectTransaction::default();
for (buffer, transaction) in message.buffers.into_iter().zip(message.transactions) {
for (buffer_id, transaction) in message.buffer_ids.into_iter().zip(message.transactions)
{
let buffer = this
.update(&mut cx, |this, cx| this.deserialize_buffer(buffer, cx))
.update(&mut cx, |this, cx| this.wait_for_buffer(buffer_id, cx))
.await?;
let transaction = language::proto::deserialize_transaction(transaction)?;
project_transaction.0.insert(buffer, transaction);
@ -5601,81 +5648,51 @@ impl Project {
})
}
fn serialize_buffer_for_peer(
fn create_buffer_for_peer(
&mut self,
buffer: &ModelHandle<Buffer>,
peer_id: PeerId,
cx: &AppContext,
) -> proto::Buffer {
) -> u64 {
let buffer_id = buffer.read(cx).remote_id();
let shared_buffers = self.shared_buffers.entry(peer_id).or_default();
if shared_buffers.insert(buffer_id) {
proto::Buffer {
variant: Some(proto::buffer::Variant::State(buffer.read(cx).to_proto())),
}
} else {
proto::Buffer {
variant: Some(proto::buffer::Variant::Id(buffer_id)),
if let Some(project_id) = self.remote_id() {
let shared_buffers = self.shared_buffers.entry(peer_id).or_default();
if shared_buffers.insert(buffer_id) {
self.client
.send(proto::CreateBufferForPeer {
project_id,
peer_id: peer_id.0,
buffer: Some(buffer.read(cx).to_proto()),
})
.log_err();
}
}
buffer_id
}
fn deserialize_buffer(
&mut self,
buffer: proto::Buffer,
fn wait_for_buffer(
&self,
id: u64,
cx: &mut ModelContext<Self>,
) -> Task<Result<ModelHandle<Buffer>>> {
let replica_id = self.replica_id();
let opened_buffer_tx = self.opened_buffer.0.clone();
let mut opened_buffer_rx = self.opened_buffer.1.clone();
cx.spawn(|this, mut cx| async move {
match buffer.variant.ok_or_else(|| anyhow!("missing buffer"))? {
proto::buffer::Variant::Id(id) => {
let buffer = loop {
let buffer = this.read_with(&cx, |this, cx| {
this.opened_buffers
.get(&id)
.and_then(|buffer| buffer.upgrade(cx))
});
if let Some(buffer) = buffer {
break buffer;
}
opened_buffer_rx
.next()
.await
.ok_or_else(|| anyhow!("project dropped while waiting for buffer"))?;
};
Ok(buffer)
cx.spawn(|this, cx| async move {
let buffer = loop {
let buffer = this.read_with(&cx, |this, cx| {
this.opened_buffers
.get(&id)
.and_then(|buffer| buffer.upgrade(cx))
});
if let Some(buffer) = buffer {
break buffer;
}
proto::buffer::Variant::State(mut buffer) => {
let mut buffer_worktree = None;
let mut buffer_file = None;
if let Some(file) = buffer.file.take() {
this.read_with(&cx, |this, cx| {
let worktree_id = WorktreeId::from_proto(file.worktree_id);
let worktree =
this.worktree_for_id(worktree_id, cx).ok_or_else(|| {
anyhow!("no worktree found for id {}", file.worktree_id)
})?;
buffer_file =
Some(Arc::new(File::from_proto(file, worktree.clone(), cx)?)
as Arc<dyn language::File>);
buffer_worktree = Some(worktree);
Ok::<_, anyhow::Error>(())
})?;
}
let buffer = cx.add_model(|cx| {
Buffer::from_proto(replica_id, buffer, buffer_file, cx).unwrap()
});
this.update(&mut cx, |this, cx| this.register_buffer(&buffer, cx))?;
*opened_buffer_tx.borrow_mut().borrow_mut() = ();
Ok(buffer)
}
}
opened_buffer_rx
.next()
.await
.ok_or_else(|| anyhow!("project dropped while waiting for buffer"))?;
};
Ok(buffer)
})
}
@ -5939,8 +5956,9 @@ impl Project {
let key = (worktree_id, name);
if let Some(server_id) = self.language_server_ids.get(&key) {
if let Some(LanguageServerState::Running { adapter, server }) =
self.language_servers.get(server_id)
if let Some(LanguageServerState::Running {
adapter, server, ..
}) = self.language_servers.get(server_id)
{
return Some((adapter, server));
}

View file

@ -110,7 +110,7 @@ actions!(
Paste,
Delete,
Rename,
Toggle
ToggleFocus
]
);
impl_internal_actions!(project_panel, [Open, ToggleExpanded, DeployContextMenu]);
@ -160,7 +160,7 @@ impl ProjectPanel {
{
this.expand_entry(worktree_id, *entry_id, cx);
this.update_visible_entries(Some((worktree_id, *entry_id)), cx);
this.autoscroll();
this.autoscroll(cx);
cx.notify();
}
}
@ -184,6 +184,13 @@ impl ProjectPanel {
)
});
cx.subscribe(&filename_editor, |this, _, event, cx| match event {
editor::Event::BufferEdited | editor::Event::SelectionsChanged { .. } => {
this.autoscroll(cx);
}
_ => {}
})
.detach();
cx.observe_focus(&filename_editor, |this, _, is_focused, cx| {
if !is_focused
&& this
@ -390,7 +397,7 @@ impl ProjectPanel {
worktree_id: *worktree_id,
entry_id: worktree_entries[entry_ix].id,
});
self.autoscroll();
self.autoscroll(cx);
cx.notify();
} else {
self.select_first(cx);
@ -558,6 +565,7 @@ impl ProjectPanel {
.update(cx, |editor, cx| editor.clear(cx));
cx.focus(&self.filename_editor);
self.update_visible_entries(Some((worktree_id, NEW_ENTRY_ID)), cx);
self.autoscroll(cx);
cx.notify();
}
}
@ -587,6 +595,7 @@ impl ProjectPanel {
});
cx.focus(&self.filename_editor);
self.update_visible_entries(None, cx);
self.autoscroll(cx);
cx.notify();
}
}
@ -635,7 +644,7 @@ impl ProjectPanel {
worktree_id: *worktree_id,
entry_id: entry.id,
});
self.autoscroll();
self.autoscroll(cx);
cx.notify();
}
}
@ -657,15 +666,16 @@ impl ProjectPanel {
worktree_id,
entry_id: root_entry.id,
});
self.autoscroll();
self.autoscroll(cx);
cx.notify();
}
}
}
fn autoscroll(&mut self) {
fn autoscroll(&mut self, cx: &mut ViewContext<Self>) {
if let Some((_, _, index)) = self.selection.and_then(|s| self.index_for_selection(s)) {
self.list.scroll_to(ScrollTarget::Show(index));
cx.notify();
}
}

View file

@ -55,58 +55,59 @@ message Envelope {
OpenBufferById open_buffer_by_id = 44;
OpenBufferByPath open_buffer_by_path = 45;
OpenBufferResponse open_buffer_response = 46;
UpdateBuffer update_buffer = 47;
UpdateBufferFile update_buffer_file = 48;
SaveBuffer save_buffer = 49;
BufferSaved buffer_saved = 50;
BufferReloaded buffer_reloaded = 51;
ReloadBuffers reload_buffers = 52;
ReloadBuffersResponse reload_buffers_response = 53;
FormatBuffers format_buffers = 54;
FormatBuffersResponse format_buffers_response = 55;
GetCompletions get_completions = 56;
GetCompletionsResponse get_completions_response = 57;
ApplyCompletionAdditionalEdits apply_completion_additional_edits = 58;
ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 59;
GetCodeActions get_code_actions = 60;
GetCodeActionsResponse get_code_actions_response = 61;
GetHover get_hover = 62;
GetHoverResponse get_hover_response = 63;
ApplyCodeAction apply_code_action = 64;
ApplyCodeActionResponse apply_code_action_response = 65;
PrepareRename prepare_rename = 66;
PrepareRenameResponse prepare_rename_response = 67;
PerformRename perform_rename = 68;
PerformRenameResponse perform_rename_response = 69;
SearchProject search_project = 70;
SearchProjectResponse search_project_response = 71;
CreateBufferForPeer create_buffer_for_peer = 47;
UpdateBuffer update_buffer = 48;
UpdateBufferFile update_buffer_file = 49;
SaveBuffer save_buffer = 50;
BufferSaved buffer_saved = 51;
BufferReloaded buffer_reloaded = 52;
ReloadBuffers reload_buffers = 53;
ReloadBuffersResponse reload_buffers_response = 54;
FormatBuffers format_buffers = 55;
FormatBuffersResponse format_buffers_response = 56;
GetCompletions get_completions = 57;
GetCompletionsResponse get_completions_response = 58;
ApplyCompletionAdditionalEdits apply_completion_additional_edits = 59;
ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 60;
GetCodeActions get_code_actions = 61;
GetCodeActionsResponse get_code_actions_response = 62;
GetHover get_hover = 63;
GetHoverResponse get_hover_response = 64;
ApplyCodeAction apply_code_action = 65;
ApplyCodeActionResponse apply_code_action_response = 66;
PrepareRename prepare_rename = 67;
PrepareRenameResponse prepare_rename_response = 68;
PerformRename perform_rename = 69;
PerformRenameResponse perform_rename_response = 70;
SearchProject search_project = 71;
SearchProjectResponse search_project_response = 72;
GetChannels get_channels = 72;
GetChannelsResponse get_channels_response = 73;
JoinChannel join_channel = 74;
JoinChannelResponse join_channel_response = 75;
LeaveChannel leave_channel = 76;
SendChannelMessage send_channel_message = 77;
SendChannelMessageResponse send_channel_message_response = 78;
ChannelMessageSent channel_message_sent = 79;
GetChannelMessages get_channel_messages = 80;
GetChannelMessagesResponse get_channel_messages_response = 81;
GetChannels get_channels = 73;
GetChannelsResponse get_channels_response = 74;
JoinChannel join_channel = 75;
JoinChannelResponse join_channel_response = 76;
LeaveChannel leave_channel = 77;
SendChannelMessage send_channel_message = 78;
SendChannelMessageResponse send_channel_message_response = 79;
ChannelMessageSent channel_message_sent = 80;
GetChannelMessages get_channel_messages = 81;
GetChannelMessagesResponse get_channel_messages_response = 82;
UpdateContacts update_contacts = 82;
UpdateInviteInfo update_invite_info = 83;
ShowContacts show_contacts = 84;
UpdateContacts update_contacts = 83;
UpdateInviteInfo update_invite_info = 84;
ShowContacts show_contacts = 85;
GetUsers get_users = 85;
FuzzySearchUsers fuzzy_search_users = 86;
UsersResponse users_response = 87;
RequestContact request_contact = 88;
RespondToContactRequest respond_to_contact_request = 89;
RemoveContact remove_contact = 90;
GetUsers get_users = 86;
FuzzySearchUsers fuzzy_search_users = 87;
UsersResponse users_response = 88;
RequestContact request_contact = 89;
RespondToContactRequest respond_to_contact_request = 90;
RemoveContact remove_contact = 91;
Follow follow = 91;
FollowResponse follow_response = 92;
UpdateFollowers update_followers = 93;
Unfollow unfollow = 94;
Follow follow = 92;
FollowResponse follow_response = 93;
UpdateFollowers update_followers = 94;
Unfollow unfollow = 95;
}
}
@ -299,7 +300,7 @@ message GetDocumentHighlightsResponse {
}
message Location {
Buffer buffer = 1;
uint64 buffer_id = 1;
Anchor start = 2;
Anchor end = 3;
}
@ -348,7 +349,7 @@ message OpenBufferForSymbol {
}
message OpenBufferForSymbolResponse {
Buffer buffer = 1;
uint64 buffer_id = 1;
}
message OpenBufferByPath {
@ -363,12 +364,13 @@ message OpenBufferById {
}
message OpenBufferResponse {
Buffer buffer = 1;
uint64 buffer_id = 1;
}
message CloseBuffer {
message CreateBufferForPeer {
uint64 project_id = 1;
uint64 buffer_id = 2;
uint32 peer_id = 2;
Buffer buffer = 3;
}
message UpdateBuffer {
@ -539,7 +541,7 @@ message CodeAction {
}
message ProjectTransaction {
repeated Buffer buffers = 1;
repeated uint64 buffer_ids = 1;
repeated Transaction transactions = 2;
}
@ -807,13 +809,6 @@ message Entry {
}
message Buffer {
oneof variant {
uint64 id = 1;
BufferState state = 2;
}
}
message BufferState {
uint64 id = 1;
optional File file = 2;
string base_text = 3;
@ -901,8 +896,7 @@ message Operation {
uint32 local_timestamp = 2;
uint32 lamport_timestamp = 3;
repeated VectorClockEntry version = 4;
repeated VectorClockEntry transaction_version = 6;
repeated UndoCount counts = 7;
repeated UndoCount counts = 5;
}
message UpdateSelections {

View file

@ -86,6 +86,7 @@ messages!(
(RemoveContact, Foreground),
(ChannelMessageSent, Foreground),
(CopyProjectEntry, Foreground),
(CreateBufferForPeer, Foreground),
(CreateProjectEntry, Foreground),
(DeleteProjectEntry, Foreground),
(Error, Foreground),
@ -222,6 +223,7 @@ entity_messages!(
BufferReloaded,
BufferSaved,
CopyProjectEntry,
CreateBufferForPeer,
CreateProjectEntry,
DeleteProjectEntry,
Follow,

View file

@ -6,4 +6,4 @@ pub use conn::Connection;
pub use peer::*;
mod macros;
pub const PROTOCOL_VERSION: u32 = 29;
pub const PROTOCOL_VERSION: u32 = 30;

View file

@ -450,7 +450,6 @@ impl ProjectSearchView {
search.update(cx, |search, cx| {
if let Some(query) = query {
search.set_query(&query, cx);
search.search(cx);
}
search.focus_query_editor(cx)
});

View file

@ -1,4 +1,4 @@
use crate::parse_json_with_comments;
use crate::{parse_json_with_comments, Settings};
use anyhow::{Context, Result};
use assets::Assets;
use collections::BTreeMap;
@ -10,6 +10,7 @@ use schemars::{
};
use serde::Deserialize;
use serde_json::{value::RawValue, Value};
use util::ResultExt;
#[derive(Deserialize, Default, Clone, JsonSchema)]
#[serde(transparent)]
@ -41,7 +42,9 @@ struct ActionWithData(Box<str>, Box<RawValue>);
impl KeymapFileContent {
pub fn load_defaults(cx: &mut MutableAppContext) {
for path in ["keymaps/default.json", "keymaps/vim.json"] {
let mut paths = vec!["keymaps/default.json", "keymaps/vim.json"];
paths.extend(cx.global::<Settings>().experiments.keymap_files());
for path in paths {
Self::load(path, cx).unwrap();
}
}
@ -56,26 +59,27 @@ impl KeymapFileContent {
for KeymapBlock { context, bindings } in self.0 {
let bindings = bindings
.into_iter()
.map(|(keystroke, action)| {
.filter_map(|(keystroke, action)| {
let action = action.0.get();
// This is a workaround for a limitation in serde: serde-rs/json#497
// We want to deserialize the action data as a `RawValue` so that we can
// deserialize the action itself dynamically directly from the JSON
// string. But `RawValue` currently does not work inside of an untagged enum.
let action = if action.starts_with('[') {
let ActionWithData(name, data) = serde_json::from_str(action)?;
if action.starts_with('[') {
let ActionWithData(name, data) = serde_json::from_str(action).log_err()?;
cx.deserialize_action(&name, Some(data.get()))
} else {
let name = serde_json::from_str(action)?;
let name = serde_json::from_str(action).log_err()?;
cx.deserialize_action(name, None)
}
.with_context(|| {
format!(
"invalid binding value for keystroke {keystroke}, context {context:?}"
)
})?;
Binding::load(&keystroke, action, context.as_deref())
})
.log_err()
.map(|action| Binding::load(&keystroke, action, context.as_deref()))
})
.collect::<Result<Vec<_>>>()?;

View file

@ -20,6 +20,7 @@ pub use keymap_file::{keymap_file_json_schema, KeymapFileContent};
#[derive(Clone)]
pub struct Settings {
pub experiments: FeatureFlags,
pub projects_online_by_default: bool,
pub buffer_font_family: FamilyId,
pub default_buffer_font_size: f32,
@ -38,6 +39,25 @@ pub struct Settings {
pub theme: Arc<Theme>,
}
#[derive(Copy, Clone, Debug, Default, Deserialize, JsonSchema)]
pub struct FeatureFlags {
modal_terminal: Option<bool>,
}
impl FeatureFlags {
pub fn keymap_files(&self) -> Vec<&'static str> {
let mut res = vec![];
if self.modal_terminal() {
res.push("keymaps/experiments/modal_terminal.json")
}
res
}
pub fn modal_terminal(&self) -> bool {
self.modal_terminal.unwrap_or_default()
}
}
#[derive(Clone, Debug, Default, Deserialize, JsonSchema)]
pub struct EditorSettings {
pub tab_size: Option<NonZeroU32>,
@ -83,6 +103,22 @@ pub struct TerminalSettings {
pub font_size: Option<f32>,
pub font_family: Option<String>,
pub env: Option<HashMap<String, String>>,
pub blinking: Option<TerminalBlink>,
pub alternate_scroll: Option<AlternateScroll>,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum TerminalBlink {
Off,
TerminalControlled,
On,
}
impl Default for TerminalBlink {
fn default() -> Self {
TerminalBlink::TerminalControlled
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, JsonSchema)]
@ -99,6 +135,19 @@ impl Default for Shell {
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum AlternateScroll {
On,
Off,
}
impl Default for AlternateScroll {
fn default() -> Self {
AlternateScroll::On
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum WorkingDirectory {
@ -110,6 +159,7 @@ pub enum WorkingDirectory {
#[derive(Clone, Debug, Default, Deserialize, JsonSchema)]
pub struct SettingsFileContent {
pub experiments: Option<FeatureFlags>,
#[serde(default)]
pub projects_online_by_default: Option<bool>,
#[serde(default)]
@ -160,6 +210,7 @@ impl Settings {
.unwrap();
Self {
experiments: FeatureFlags::default(),
buffer_font_family: font_cache
.load_family(&[defaults.buffer_font_family.as_ref().unwrap()])
.unwrap(),
@ -218,6 +269,7 @@ impl Settings {
);
merge(&mut self.vim_mode, data.vim_mode);
merge(&mut self.autosave, data.autosave);
merge(&mut self.experiments, data.experiments);
// Ensure terminal font is loaded, so we can request it in terminal_element layout
if let Some(terminal_font) = &data.terminal.font_family {
@ -279,6 +331,7 @@ impl Settings {
#[cfg(any(test, feature = "test-support"))]
pub fn test(cx: &gpui::AppContext) -> Settings {
Settings {
experiments: FeatureFlags::default(),
buffer_font_family: cx.font_cache().load_family(&["Monaco"]).unwrap(),
buffer_font_size: 14.,
default_buffer_font_size: 14.,

View file

@ -4,6 +4,20 @@ This crate is split into two conceptual halves:
- The terminal.rs file and the src/mappings/ folder, these contain the code for interacting with Alacritty and maintaining the pty event loop. Some behavior in this file is constrained by terminal protocols and standards. The Zed init function is also placed here.
- Everything else. These other files integrate the `Terminal` struct created in terminal.rs into the rest of GPUI. The main entry point for GPUI is the terminal_view.rs file and the modal.rs file.
Terminals are created externally, and so can fail in unexpected ways However, GPUI currently does not have an API for models than can fail to instantiate. `TerminalBuilder` solves this by using Rust's type system to split `Terminal` instantiation into a 2 step process: first attempt to create the file handles with `TerminalBuilder::new()`, check the result, then call `TerminalBuilder::subscribe(cx)` from within a model context.
The TerminalView struct abstracts over failed and successful terminals, and provides a standardized way of instantiating an always-successful view of a terminal.
ttys are created externally, and so can fail in unexpected ways. However, GPUI currently does not have an API for models than can fail to instantiate. `TerminalBuilder` solves this by using Rust's type system to split tty instantiation into a 2 step process: first attempt to create the file handles with `TerminalBuilder::new()`, check the result, then call `TerminalBuilder::subscribe(cx)` from within a model context.
The TerminalView struct abstracts over failed and successful terminals, passing focus through to the associated view and allowing clients to build a terminal without worrying about errors.
#Input
There are currently many distinct paths for getting keystrokes to the terminal:
1. Terminal specific characters and bindings. Things like ctrl-a mapping to ASCII control character 1, ANSI escape codes associated with the function keys, etc. These are caught with a raw key-down handler in the element and are processed immediately. This is done with the `try_keystroke()` method on Terminal
2. GPU Action handlers. GPUI clobbers a few vital keys by adding bindings to them in the global context. These keys are synthesized and then dispatched through the same `try_keystroke()` API as the above mappings
3. IME text. When the special character mappings fail, we pass the keystroke back to GPUI to hand it to the IME system. This comes back to us in the `View::replace_text_in_range()` method, and we then send that to the terminal directly, bypassing `try_keystroke()`.
4. Pasted text has a seperate pathway.
Generally, there's a distinction between 'keystrokes that need to be mapped' and 'strings which need to be written'. I've attempted to unify these under the '.try_keystroke()' API and the `.input()` API (which try_keystroke uses) so we have consistent input handling across the terminal

View file

@ -1,23 +1,25 @@
use alacritty_terminal::{
ansi::{Color as AnsiColor, Color::Named, NamedColor},
grid::{Dimensions, Scroll},
index::{Column as GridCol, Line as GridLine, Point, Side},
ansi::{Color as AnsiColor, Color::Named, CursorShape as AlacCursorShape, NamedColor},
grid::Dimensions,
index::Point,
selection::SelectionRange,
term::cell::{Cell, Flags},
term::{
cell::{Cell, Flags},
TermMode,
},
};
use editor::{Cursor, CursorShape, HighlightedRange, HighlightedRangeLine};
use gpui::{
color::Color,
elements::*,
fonts::{Properties, Style::Italic, TextStyle, Underline, Weight},
geometry::{
rect::RectF,
vector::{vec2f, Vector2F},
},
json::json,
serde_json::json,
text_layout::{Line, RunStyle},
Event, FontCache, KeyDownEvent, MouseButton, MouseRegion, PaintContext, Quad, ScrollWheelEvent,
TextLayoutCache, WeakModelHandle, WeakViewHandle,
Element, Event, EventContext, FontCache, KeyDownEvent, ModelContext, MouseButton, MouseRegion,
PaintContext, Quad, TextLayoutCache, WeakModelHandle, WeakViewHandle,
};
use itertools::Itertools;
use ordered_float::OrderedFloat;
@ -25,12 +27,11 @@ use settings::Settings;
use theme::TerminalStyle;
use util::ResultExt;
use std::fmt::Debug;
use std::{
cmp::min,
mem,
ops::{Deref, Range},
};
use std::{fmt::Debug, ops::Sub};
use crate::{
connected_view::{ConnectedView, DeployContextMenu},
@ -38,11 +39,6 @@ use crate::{
Terminal, TerminalSize,
};
///Scrolling is unbearably sluggish by default. Alacritty supports a configurable
///Scroll multiplier that is set to 3 by default. This will be removed when I
///Implement scroll bars.
pub const ALACRITTY_SCROLL_MULTIPLIER: f32 = 3.;
///The information generated during layout that is nescessary for painting
pub struct LayoutState {
cells: Vec<LayoutCell>,
@ -52,7 +48,7 @@ pub struct LayoutState {
background_color: Color,
selection_color: Color,
size: TerminalSize,
display_offset: usize,
mode: TermMode,
}
#[derive(Debug)]
@ -200,6 +196,8 @@ pub struct TerminalEl {
terminal: WeakModelHandle<Terminal>,
view: WeakViewHandle<ConnectedView>,
modal: bool,
focused: bool,
cursor_visible: bool,
}
impl TerminalEl {
@ -207,11 +205,15 @@ impl TerminalEl {
view: WeakViewHandle<ConnectedView>,
terminal: WeakModelHandle<Terminal>,
modal: bool,
focused: bool,
cursor_visible: bool,
) -> TerminalEl {
TerminalEl {
view,
terminal,
modal,
focused,
cursor_visible,
}
}
@ -407,75 +409,158 @@ impl TerminalEl {
}
}
fn generic_button_handler<E>(
connection: WeakModelHandle<Terminal>,
origin: Vector2F,
f: impl Fn(&mut Terminal, Vector2F, E, &mut ModelContext<Terminal>),
) -> impl Fn(E, &mut EventContext) {
move |event, cx| {
cx.focus_parent_view();
if let Some(conn_handle) = connection.upgrade(cx.app) {
conn_handle.update(cx.app, |terminal, cx| {
f(terminal, origin, event, cx);
cx.notify();
})
}
}
}
fn attach_mouse_handlers(
&self,
origin: Vector2F,
view_id: usize,
visible_bounds: RectF,
cur_size: TerminalSize,
display_offset: usize,
mode: TermMode,
cx: &mut PaintContext,
) {
let mouse_down_connection = self.terminal;
let click_connection = self.terminal;
let drag_connection = self.terminal;
cx.scene.push_mouse_region(
MouseRegion::new(view_id, None, visible_bounds)
.on_down(MouseButton::Left, move |e, cx| {
if let Some(conn_handle) = mouse_down_connection.upgrade(cx.app) {
let connection = self.terminal;
let mut region = MouseRegion::new(view_id, None, visible_bounds);
// Terminal Emulator controlled behavior:
region = region
// Start selections
.on_down(
MouseButton::Left,
TerminalEl::generic_button_handler(
connection,
origin,
move |terminal, origin, e, _cx| {
terminal.mouse_down(&e, origin);
},
),
)
// Update drag selections
.on_drag(MouseButton::Left, move |event, cx| {
if cx.is_parent_view_focused() {
if let Some(conn_handle) = connection.upgrade(cx.app) {
conn_handle.update(cx.app, |terminal, cx| {
let (point, side) = TerminalEl::mouse_to_cell_data(
e.position,
origin,
cur_size,
display_offset,
);
terminal.mouse_down(point, side);
terminal.mouse_drag(event, origin);
cx.notify();
})
}
})
.on_click(MouseButton::Left, move |e, cx| {
cx.focus_parent_view();
if let Some(conn_handle) = click_connection.upgrade(cx.app) {
conn_handle.update(cx.app, |terminal, cx| {
let (point, side) = TerminalEl::mouse_to_cell_data(
e.position,
origin,
cur_size,
display_offset,
);
terminal.click(point, side, e.click_count);
cx.notify();
});
}
})
.on_click(MouseButton::Right, move |e, cx| {
}
})
// Copy on up behavior
.on_up(
MouseButton::Left,
TerminalEl::generic_button_handler(
connection,
origin,
move |terminal, origin, e, _cx| {
terminal.mouse_up(&e, origin);
},
),
)
// Handle click based selections
.on_click(
MouseButton::Left,
TerminalEl::generic_button_handler(
connection,
origin,
move |terminal, origin, e, _cx| {
terminal.left_click(&e, origin);
},
),
)
// Context menu
.on_click(MouseButton::Right, move |e, cx| {
let mouse_mode = if let Some(conn_handle) = connection.upgrade(cx.app) {
conn_handle.update(cx.app, |terminal, _cx| terminal.mouse_mode(e.shift))
} else {
// If we can't get the model handle, probably can't deploy the context menu
true
};
if !mouse_mode {
cx.dispatch_action(DeployContextMenu {
position: e.position,
});
})
.on_drag(MouseButton::Left, move |e, cx| {
if let Some(conn_handle) = drag_connection.upgrade(cx.app) {
conn_handle.update(cx.app, |terminal, cx| {
let (point, side) = TerminalEl::mouse_to_cell_data(
e.position,
origin,
cur_size,
display_offset,
);
}
});
terminal.drag(point, side);
cx.notify()
});
// Mouse mode handlers:
// All mouse modes need the extra click handlers
if mode.intersects(TermMode::MOUSE_MODE) {
region = region
.on_down(
MouseButton::Right,
TerminalEl::generic_button_handler(
connection,
origin,
move |terminal, origin, e, _cx| {
terminal.mouse_down(&e, origin);
},
),
)
.on_down(
MouseButton::Middle,
TerminalEl::generic_button_handler(
connection,
origin,
move |terminal, origin, e, _cx| {
terminal.mouse_down(&e, origin);
},
),
)
.on_up(
MouseButton::Right,
TerminalEl::generic_button_handler(
connection,
origin,
move |terminal, origin, e, _cx| {
terminal.mouse_up(&e, origin);
},
),
)
.on_up(
MouseButton::Middle,
TerminalEl::generic_button_handler(
connection,
origin,
move |terminal, origin, e, _cx| {
terminal.mouse_up(&e, origin);
},
),
)
}
//Mouse move manages both dragging and motion events
if mode.intersects(TermMode::MOUSE_DRAG | TermMode::MOUSE_MOTION) {
region = region
//TODO: This does not fire on right-mouse-down-move events.
.on_move(move |event, cx| {
if cx.is_parent_view_focused() {
if let Some(conn_handle) = connection.upgrade(cx.app) {
conn_handle.update(cx.app, |terminal, cx| {
terminal.mouse_move(&event, origin);
cx.notify();
})
}
}
}),
);
})
}
cx.scene.push_mouse_region(region);
}
///Configures a text style from the current settings.
@ -509,47 +594,6 @@ impl TerminalEl {
underline: Default::default(),
}
}
pub fn mouse_to_cell_data(
pos: Vector2F,
origin: Vector2F,
cur_size: TerminalSize,
display_offset: usize,
) -> (Point, alacritty_terminal::index::Direction) {
let pos = pos.sub(origin);
let point = {
let col = pos.x() / cur_size.cell_width; //TODO: underflow...
let col = min(GridCol(col as usize), cur_size.last_column());
let line = pos.y() / cur_size.line_height;
let line = min(line as i32, cur_size.bottommost_line().0);
Point::new(GridLine(line - display_offset as i32), col)
};
//Copied (with modifications) from alacritty/src/input.rs > Processor::cell_side()
let side = {
let x = pos.0.x() as usize;
let cell_x =
x.saturating_sub(cur_size.cell_width as usize) % cur_size.cell_width as usize;
let half_cell_width = (cur_size.cell_width / 2.0) as usize;
let additional_padding =
(cur_size.width() - cur_size.cell_width * 2.) % cur_size.cell_width;
let end_of_grid = cur_size.width() - cur_size.cell_width - additional_padding;
//Width: Pixels or columns?
if cell_x > half_cell_width
// Edge case when mouse leaves the window.
|| x as f32 >= end_of_grid
{
Side::Right
} else {
Side::Left
}
};
(point, side)
}
}
impl Element for TerminalEl {
@ -580,7 +624,7 @@ impl Element for TerminalEl {
terminal_theme.colors.background
};
let (cells, selection, cursor, display_offset, cursor_text) = self
let (cells, selection, cursor, display_offset, cursor_text, mode) = self
.terminal
.upgrade(cx)
.unwrap()
@ -603,13 +647,13 @@ impl Element for TerminalEl {
cell: ic.cell.clone(),
}),
);
(
cells,
content.selection,
content.cursor,
content.display_offset,
cursor_text,
content.mode,
)
})
});
@ -624,11 +668,21 @@ impl Element for TerminalEl {
selection,
);
//Layout cursor
let cursor = {
//Layout cursor. Rectangle is used for IME, so we should lay it out even
//if we don't end up showing it.
let cursor = if let AlacCursorShape::Hidden = cursor.shape {
None
} else {
let cursor_point = DisplayCursor::from(cursor.point, display_offset);
let cursor_text = {
let str_trxt = cursor_text.to_string();
let color = if self.focused {
terminal_theme.colors.background
} else {
terminal_theme.colors.foreground
};
cx.text_layout_cache.layout_str(
&str_trxt,
text_style.font_size,
@ -636,7 +690,7 @@ impl Element for TerminalEl {
str_trxt.len(),
RunStyle {
font_id: text_style.font_id,
color: terminal_theme.colors.background,
color,
underline: Default::default(),
},
)],
@ -645,12 +699,22 @@ impl Element for TerminalEl {
TerminalEl::shape_cursor(cursor_point, dimensions, &cursor_text).map(
move |(cursor_position, block_width)| {
let shape = match cursor.shape {
AlacCursorShape::Block if !self.focused => CursorShape::Hollow,
AlacCursorShape::Block => CursorShape::Block,
AlacCursorShape::Underline => CursorShape::Underscore,
AlacCursorShape::Beam => CursorShape::Bar,
AlacCursorShape::HollowBlock => CursorShape::Hollow,
//This case is handled in the if wrapping the whole cursor layout
AlacCursorShape::Hidden => unreachable!(),
};
Cursor::new(
cursor_position,
block_width,
dimensions.line_height,
terminal_theme.colors.cursor,
CursorShape::Block,
shape,
Some(cursor_text),
)
},
@ -668,7 +732,7 @@ impl Element for TerminalEl {
size: dimensions,
rects,
highlights,
display_offset,
mode,
},
)
}
@ -687,14 +751,7 @@ impl Element for TerminalEl {
let origin = bounds.origin() + vec2f(layout.size.cell_width, 0.);
//Elements are ephemeral, only at paint time do we know what could be clicked by a mouse
self.attach_mouse_handlers(
origin,
self.view.id(),
visible_bounds,
layout.size,
layout.display_offset,
cx,
);
self.attach_mouse_handlers(origin, self.view.id(), visible_bounds, layout.mode, cx);
cx.paint_layer(clip_bounds, |cx| {
//Start with a background color
@ -745,10 +802,12 @@ impl Element for TerminalEl {
});
//Draw cursor
if let Some(cursor) = &layout.cursor {
cx.paint_layer(clip_bounds, |cx| {
cursor.paint(origin, cx);
})
if self.cursor_visible {
if let Some(cursor) = &layout.cursor {
cx.paint_layer(clip_bounds, |cx| {
cursor.paint(origin, cx);
})
}
}
});
}
@ -756,28 +815,22 @@ impl Element for TerminalEl {
fn dispatch_event(
&mut self,
event: &gpui::Event,
_bounds: gpui::geometry::rect::RectF,
bounds: gpui::geometry::rect::RectF,
visible_bounds: gpui::geometry::rect::RectF,
layout: &mut Self::LayoutState,
_paint: &mut Self::PaintState,
cx: &mut gpui::EventContext,
) -> bool {
match event {
Event::ScrollWheel(ScrollWheelEvent {
delta, position, ..
}) => visible_bounds
.contains_point(*position)
Event::ScrollWheel(e) => visible_bounds
.contains_point(e.position)
.then(|| {
let vertical_scroll =
(delta.y() / layout.size.line_height) * ALACRITTY_SCROLL_MULTIPLIER;
let origin = bounds.origin() + vec2f(layout.size.cell_width, 0.);
if let Some(terminal) = self.terminal.upgrade(cx.app) {
terminal.update(cx.app, |term, _| {
term.scroll(Scroll::Delta(vertical_scroll.round() as i32))
});
terminal.update(cx.app, |term, _| term.scroll(e, origin));
cx.notify();
}
cx.notify();
})
.is_some(),
Event::KeyDown(KeyDownEvent { keystroke, .. }) => {
@ -785,9 +838,11 @@ impl Element for TerminalEl {
return false;
}
//TODO Talk to keith about how to catch events emitted from an element.
if let Some(view) = self.view.upgrade(cx.app) {
view.update(cx.app, |view, cx| view.clear_bel(cx))
view.update(cx.app, |view, cx| {
view.clear_bel(cx);
view.pause_cursor_blinking(cx);
})
}
self.terminal
@ -838,36 +893,3 @@ impl Element for TerminalEl {
Some(layout.cursor.as_ref()?.bounding_rect(origin))
}
}
mod test {
#[test]
fn test_mouse_to_selection() {
let term_width = 100.;
let term_height = 200.;
let cell_width = 10.;
let line_height = 20.;
let mouse_pos_x = 100.; //Window relative
let mouse_pos_y = 100.; //Window relative
let origin_x = 10.;
let origin_y = 20.;
let cur_size = crate::connected_el::TerminalSize::new(
line_height,
cell_width,
gpui::geometry::vector::vec2f(term_width, term_height),
);
let mouse_pos = gpui::geometry::vector::vec2f(mouse_pos_x, mouse_pos_y);
let origin = gpui::geometry::vector::vec2f(origin_x, origin_y); //Position of terminal window, 1 'cell' in
let (point, _) =
crate::connected_el::TerminalEl::mouse_to_cell_data(mouse_pos, origin, cur_size, 0);
assert_eq!(
point,
alacritty_terminal::index::Point::new(
alacritty_terminal::index::Line(((mouse_pos_y - origin_y) / line_height) as i32),
alacritty_terminal::index::Column(((mouse_pos_x - origin_x) / cell_width) as usize),
)
);
}
}

View file

@ -1,3 +1,5 @@
use std::time::Duration;
use alacritty_terminal::term::TermMode;
use context_menu::{ContextMenu, ContextMenuItem};
use gpui::{
@ -9,10 +11,14 @@ use gpui::{
AnyViewHandle, AppContext, Element, ElementBox, ModelHandle, MutableAppContext, View,
ViewContext, ViewHandle,
};
use settings::{Settings, TerminalBlink};
use smol::Timer;
use workspace::pane;
use crate::{connected_el::TerminalEl, Event, Terminal};
const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500);
///Event to transmit the scroll from the element to the view
#[derive(Clone, Debug, PartialEq)]
pub struct ScrollTerminal(pub i32);
@ -24,7 +30,17 @@ pub struct DeployContextMenu {
actions!(
terminal,
[Up, Down, CtrlC, Escape, Enter, Clear, Copy, Paste,]
[
Up,
Down,
CtrlC,
Escape,
Enter,
Clear,
Copy,
Paste,
ShowCharacterPalette,
]
);
impl_internal_actions!(project_panel, [DeployContextMenu]);
@ -40,6 +56,7 @@ pub fn init(cx: &mut MutableAppContext) {
cx.add_action(ConnectedView::copy);
cx.add_action(ConnectedView::paste);
cx.add_action(ConnectedView::clear);
cx.add_action(ConnectedView::show_character_palette);
}
///A terminal view, maintains the PTY's file handles and communicates with the terminal
@ -51,6 +68,10 @@ pub struct ConnectedView {
// Only for styling purposes. Doesn't effect behavior
modal: bool,
context_menu: ViewHandle<ContextMenu>,
blink_state: bool,
blinking_on: bool,
blinking_paused: bool,
blink_epoch: usize,
}
impl ConnectedView {
@ -72,7 +93,7 @@ impl ConnectedView {
this.has_bell = true;
cx.emit(Event::Wakeup);
}
Event::BlinkChanged => this.blinking_on = !this.blinking_on,
_ => cx.emit(*event),
})
.detach();
@ -83,6 +104,10 @@ impl ConnectedView {
has_bell: false,
modal,
context_menu: cx.add_view(ContextMenu::new),
blink_state: true,
blinking_on: false,
blinking_paused: false,
blink_epoch: 0,
}
}
@ -115,11 +140,109 @@ impl ConnectedView {
cx.notify();
}
fn show_character_palette(&mut self, _: &ShowCharacterPalette, cx: &mut ViewContext<Self>) {
if !self
.terminal
.read(cx)
.last_mode
.contains(TermMode::ALT_SCREEN)
{
cx.show_character_palette();
} else {
self.terminal.update(cx, |term, _| {
term.try_keystroke(&Keystroke::parse("ctrl-cmd-space").unwrap())
});
}
}
fn clear(&mut self, _: &Clear, cx: &mut ViewContext<Self>) {
self.terminal.update(cx, |term, _| term.clear());
cx.notify();
}
pub fn should_show_cursor(
&self,
focused: bool,
cx: &mut gpui::RenderContext<'_, Self>,
) -> bool {
//Don't blink the cursor when not focused, blinking is disabled, or paused
if !focused
|| !self.blinking_on
|| self.blinking_paused
|| self
.terminal
.read(cx)
.last_mode
.contains(TermMode::ALT_SCREEN)
{
return true;
}
let setting = {
let settings = cx.global::<Settings>();
settings
.terminal_overrides
.blinking
.clone()
.unwrap_or(TerminalBlink::TerminalControlled)
};
match setting {
//If the user requested to never blink, don't blink it.
TerminalBlink::Off => true,
//If the terminal is controlling it, check terminal mode
TerminalBlink::TerminalControlled | TerminalBlink::On => self.blink_state,
}
}
fn blink_cursors(&mut self, epoch: usize, cx: &mut ViewContext<Self>) {
if epoch == self.blink_epoch && !self.blinking_paused {
self.blink_state = !self.blink_state;
cx.notify();
let epoch = self.next_blink_epoch();
cx.spawn(|this, mut cx| {
let this = this.downgrade();
async move {
Timer::after(CURSOR_BLINK_INTERVAL).await;
if let Some(this) = this.upgrade(&cx) {
this.update(&mut cx, |this, cx| this.blink_cursors(epoch, cx));
}
}
})
.detach();
}
}
pub fn pause_cursor_blinking(&mut self, cx: &mut ViewContext<Self>) {
self.blink_state = true;
cx.notify();
let epoch = self.next_blink_epoch();
cx.spawn(|this, mut cx| {
let this = this.downgrade();
async move {
Timer::after(CURSOR_BLINK_INTERVAL).await;
if let Some(this) = this.upgrade(&cx) {
this.update(&mut cx, |this, cx| this.resume_cursor_blinking(epoch, cx))
}
}
})
.detach();
}
fn next_blink_epoch(&mut self) -> usize {
self.blink_epoch += 1;
self.blink_epoch
}
fn resume_cursor_blinking(&mut self, epoch: usize, cx: &mut ViewContext<Self>) {
if epoch == self.blink_epoch {
self.blinking_paused = false;
self.blink_cursors(epoch, cx);
}
}
///Attempt to paste the clipboard into the terminal
fn copy(&mut self, _: &Copy, cx: &mut ViewContext<Self>) {
self.terminal.update(cx, |term, _| term.copy())
@ -128,48 +251,49 @@ impl ConnectedView {
///Attempt to paste the clipboard into the terminal
fn paste(&mut self, _: &Paste, cx: &mut ViewContext<Self>) {
if let Some(item) = cx.read_from_clipboard() {
self.terminal.read(cx).paste(item.text());
self.terminal
.update(cx, |terminal, _cx| terminal.paste(item.text()));
}
}
///Synthesize the keyboard event corresponding to 'up'
fn up(&mut self, _: &Up, cx: &mut ViewContext<Self>) {
self.clear_bel(cx);
self.terminal
.read(cx)
.try_keystroke(&Keystroke::parse("up").unwrap());
self.terminal.update(cx, |term, _| {
term.try_keystroke(&Keystroke::parse("up").unwrap())
});
}
///Synthesize the keyboard event corresponding to 'down'
fn down(&mut self, _: &Down, cx: &mut ViewContext<Self>) {
self.clear_bel(cx);
self.terminal
.read(cx)
.try_keystroke(&Keystroke::parse("down").unwrap());
self.terminal.update(cx, |term, _| {
term.try_keystroke(&Keystroke::parse("down").unwrap())
});
}
///Synthesize the keyboard event corresponding to 'ctrl-c'
fn ctrl_c(&mut self, _: &CtrlC, cx: &mut ViewContext<Self>) {
self.clear_bel(cx);
self.terminal
.read(cx)
.try_keystroke(&Keystroke::parse("ctrl-c").unwrap());
self.terminal.update(cx, |term, _| {
term.try_keystroke(&Keystroke::parse("ctrl-c").unwrap())
});
}
///Synthesize the keyboard event corresponding to 'escape'
fn escape(&mut self, _: &Escape, cx: &mut ViewContext<Self>) {
self.clear_bel(cx);
self.terminal
.read(cx)
.try_keystroke(&Keystroke::parse("escape").unwrap());
self.terminal.update(cx, |term, _| {
term.try_keystroke(&Keystroke::parse("escape").unwrap())
});
}
///Synthesize the keyboard event corresponding to 'enter'
fn enter(&mut self, _: &Enter, cx: &mut ViewContext<Self>) {
self.clear_bel(cx);
self.terminal
.read(cx)
.try_keystroke(&Keystroke::parse("enter").unwrap());
self.terminal.update(cx, |term, _| {
term.try_keystroke(&Keystroke::parse("enter").unwrap())
});
}
}
@ -181,20 +305,41 @@ impl View for ConnectedView {
fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox {
let terminal_handle = self.terminal.clone().downgrade();
let self_id = cx.view_id();
let focused = cx
.focused_view_id(cx.window_id())
.filter(|view_id| *view_id == self_id)
.is_some();
Stack::new()
.with_child(
TerminalEl::new(cx.handle(), terminal_handle, self.modal)
.contained()
.boxed(),
TerminalEl::new(
cx.handle(),
terminal_handle,
self.modal,
focused,
self.should_show_cursor(focused, cx),
)
.contained()
.boxed(),
)
.with_child(ChildView::new(&self.context_menu).boxed())
.boxed()
}
fn on_focus_in(&mut self, _: AnyViewHandle, _cx: &mut ViewContext<Self>) {
fn on_focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
self.has_new_content = false;
self.terminal.read(cx).focus_in();
self.blink_cursors(self.blink_epoch, cx);
cx.notify();
}
fn on_focus_out(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
self.terminal.read(cx).focus_out();
cx.notify();
}
//IME stuff
fn selected_text_range(&self, cx: &AppContext) -> Option<std::ops::Range<usize>> {
if self
.terminal
@ -214,15 +359,91 @@ impl View for ConnectedView {
text: &str,
cx: &mut ViewContext<Self>,
) {
self.terminal
.update(cx, |terminal, _| terminal.write_to_pty(text.into()));
self.terminal.update(cx, |terminal, _| {
terminal.input(text.into());
});
}
fn keymap_context(&self, _: &gpui::AppContext) -> gpui::keymap::Context {
fn keymap_context(&self, cx: &gpui::AppContext) -> gpui::keymap::Context {
let mut context = Self::default_keymap_context();
if self.modal {
context.set.insert("ModalTerminal".into());
}
let mode = self.terminal.read(cx).last_mode;
context.map.insert(
"screen".to_string(),
(if mode.contains(TermMode::ALT_SCREEN) {
"alt"
} else {
"normal"
})
.to_string(),
);
if mode.contains(TermMode::APP_CURSOR) {
context.set.insert("DECCKM".to_string());
}
if mode.contains(TermMode::APP_KEYPAD) {
context.set.insert("DECPAM".to_string());
}
//Note the ! here
if !mode.contains(TermMode::APP_KEYPAD) {
context.set.insert("DECPNM".to_string());
}
if mode.contains(TermMode::SHOW_CURSOR) {
context.set.insert("DECTCEM".to_string());
}
if mode.contains(TermMode::LINE_WRAP) {
context.set.insert("DECAWM".to_string());
}
if mode.contains(TermMode::ORIGIN) {
context.set.insert("DECOM".to_string());
}
if mode.contains(TermMode::INSERT) {
context.set.insert("IRM".to_string());
}
//LNM is apparently the name for this. https://vt100.net/docs/vt510-rm/LNM.html
if mode.contains(TermMode::LINE_FEED_NEW_LINE) {
context.set.insert("LNM".to_string());
}
if mode.contains(TermMode::FOCUS_IN_OUT) {
context.set.insert("report_focus".to_string());
}
if mode.contains(TermMode::ALTERNATE_SCROLL) {
context.set.insert("alternate_scroll".to_string());
}
if mode.contains(TermMode::BRACKETED_PASTE) {
context.set.insert("bracketed_paste".to_string());
}
if mode.intersects(TermMode::MOUSE_MODE) {
context.set.insert("any_mouse_reporting".to_string());
}
{
let mouse_reporting = if mode.contains(TermMode::MOUSE_REPORT_CLICK) {
"click"
} else if mode.contains(TermMode::MOUSE_DRAG) {
"drag"
} else if mode.contains(TermMode::MOUSE_MOTION) {
"motion"
} else {
"off"
};
context
.map
.insert("mouse_reporting".to_string(), mouse_reporting.to_string());
}
{
let format = if mode.contains(TermMode::SGR_MOUSE) {
"sgr"
} else if mode.contains(TermMode::UTF8_MOUSE) {
"utf8"
} else {
"normal"
};
context
.map
.insert("mouse_format".to_string(), format.to_string());
}
context
}
}

View file

@ -1,3 +1,4 @@
/// The mappings defined in this file where created from reading the alacritty source
use alacritty_terminal::term::TermMode;
use gpui::keymap::Keystroke;
@ -53,7 +54,6 @@ pub fn to_esc_str(keystroke: &Keystroke, mode: &TermMode) -> Option<String> {
// Manual Bindings including modifiers
let manual_esc_str = match (keystroke.key.as_ref(), &modifiers) {
//Basic special keys
("space", Modifiers::None) => Some(" ".to_string()),
("tab", Modifiers::None) => Some("\x09".to_string()),
("escape", Modifiers::None) => Some("\x1b".to_string()),
("enter", Modifiers::None) => Some("\x0d".to_string()),

View file

@ -1,2 +1,3 @@
pub mod colors;
pub mod keys;
pub mod mouse;

View file

@ -0,0 +1,329 @@
use std::cmp::{max, min};
use std::iter::repeat;
use alacritty_terminal::grid::Dimensions;
/// Most of the code, and specifically the constants, in this are copied from Alacritty,
/// with modifications for our circumstances
use alacritty_terminal::index::{Column as GridCol, Line as GridLine, Point, Side};
use alacritty_terminal::term::TermMode;
use gpui::{geometry::vector::Vector2F, MouseButtonEvent, MouseMovedEvent, ScrollWheelEvent};
use crate::TerminalSize;
struct Modifiers {
ctrl: bool,
shift: bool,
alt: bool,
}
impl Modifiers {
fn from_moved(e: &MouseMovedEvent) -> Self {
Modifiers {
ctrl: e.ctrl,
shift: e.shift,
alt: e.alt,
}
}
fn from_button(e: &MouseButtonEvent) -> Self {
Modifiers {
ctrl: e.ctrl,
shift: e.shift,
alt: e.alt,
}
}
//TODO: Determine if I should add modifiers into the ScrollWheelEvent type
fn from_scroll() -> Self {
Modifiers {
ctrl: false,
shift: false,
alt: false,
}
}
}
enum MouseFormat {
SGR,
Normal(bool),
}
impl MouseFormat {
fn from_mode(mode: TermMode) -> Self {
if mode.contains(TermMode::SGR_MOUSE) {
MouseFormat::SGR
} else if mode.contains(TermMode::UTF8_MOUSE) {
MouseFormat::Normal(true)
} else {
MouseFormat::Normal(false)
}
}
}
#[derive(Debug)]
enum MouseButton {
LeftButton = 0,
MiddleButton = 1,
RightButton = 2,
LeftMove = 32,
MiddleMove = 33,
RightMove = 34,
NoneMove = 35,
ScrollUp = 64,
ScrollDown = 65,
Other = 99,
}
impl MouseButton {
fn from_move(e: &MouseMovedEvent) -> Self {
match e.pressed_button {
Some(b) => match b {
gpui::MouseButton::Left => MouseButton::LeftMove,
gpui::MouseButton::Middle => MouseButton::MiddleMove,
gpui::MouseButton::Right => MouseButton::RightMove,
gpui::MouseButton::Navigate(_) => MouseButton::Other,
},
None => MouseButton::NoneMove,
}
}
fn from_button(e: &MouseButtonEvent) -> Self {
match e.button {
gpui::MouseButton::Left => MouseButton::LeftButton,
gpui::MouseButton::Right => MouseButton::MiddleButton,
gpui::MouseButton::Middle => MouseButton::RightButton,
gpui::MouseButton::Navigate(_) => MouseButton::Other,
}
}
fn from_scroll(e: &ScrollWheelEvent) -> Self {
if e.delta.y() > 0. {
MouseButton::ScrollUp
} else {
MouseButton::ScrollDown
}
}
fn is_other(&self) -> bool {
match self {
MouseButton::Other => true,
_ => false,
}
}
}
pub fn scroll_report(
point: Point,
scroll_lines: i32,
e: &ScrollWheelEvent,
mode: TermMode,
) -> Option<impl Iterator<Item = Vec<u8>>> {
if mode.intersects(TermMode::MOUSE_MODE) {
mouse_report(
point,
MouseButton::from_scroll(e),
true,
Modifiers::from_scroll(),
MouseFormat::from_mode(mode),
)
.map(|report| repeat(report).take(max(scroll_lines, 1) as usize))
} else {
None
}
}
pub fn alt_scroll(scroll_lines: i32) -> Vec<u8> {
let cmd = if scroll_lines > 0 { b'A' } else { b'B' };
let mut content = Vec::with_capacity(scroll_lines.abs() as usize * 3);
for _ in 0..scroll_lines.abs() {
content.push(0x1b);
content.push(b'O');
content.push(cmd);
}
content
}
pub fn mouse_button_report(
point: Point,
e: &MouseButtonEvent,
pressed: bool,
mode: TermMode,
) -> Option<Vec<u8>> {
let button = MouseButton::from_button(e);
if !button.is_other() && mode.intersects(TermMode::MOUSE_MODE) {
mouse_report(
point,
button,
pressed,
Modifiers::from_button(e),
MouseFormat::from_mode(mode),
)
} else {
None
}
}
pub fn mouse_moved_report(point: Point, e: &MouseMovedEvent, mode: TermMode) -> Option<Vec<u8>> {
let button = MouseButton::from_move(e);
if !button.is_other() && mode.intersects(TermMode::MOUSE_MOTION | TermMode::MOUSE_DRAG) {
//Only drags are reported in drag mode, so block NoneMove.
if mode.contains(TermMode::MOUSE_DRAG) && matches!(button, MouseButton::NoneMove) {
None
} else {
mouse_report(
point,
button,
true,
Modifiers::from_moved(e),
MouseFormat::from_mode(mode),
)
}
} else {
None
}
}
pub fn mouse_side(pos: Vector2F, cur_size: TerminalSize) -> alacritty_terminal::index::Direction {
let x = pos.0.x() as usize;
let cell_x = x.saturating_sub(cur_size.cell_width as usize) % cur_size.cell_width as usize;
let half_cell_width = (cur_size.cell_width / 2.0) as usize;
let additional_padding = (cur_size.width() - cur_size.cell_width * 2.) % cur_size.cell_width;
let end_of_grid = cur_size.width() - cur_size.cell_width - additional_padding;
//Width: Pixels or columns?
if cell_x > half_cell_width
// Edge case when mouse leaves the window.
|| x as f32 >= end_of_grid
{
Side::Right
} else {
Side::Left
}
}
pub fn mouse_point(pos: Vector2F, cur_size: TerminalSize, display_offset: usize) -> Point {
let col = pos.x() / cur_size.cell_width;
let col = min(GridCol(col as usize), cur_size.last_column());
let line = pos.y() / cur_size.line_height;
let line = min(line as i32, cur_size.bottommost_line().0);
Point::new(GridLine(line - display_offset as i32), col)
}
///Generate the bytes to send to the terminal, from the cell location, a mouse event, and the terminal mode
fn mouse_report(
point: Point,
button: MouseButton,
pressed: bool,
modifiers: Modifiers,
format: MouseFormat,
) -> Option<Vec<u8>> {
if point.line < 0 {
return None;
}
let mut mods = 0;
if modifiers.shift {
mods += 4;
}
if modifiers.alt {
mods += 8;
}
if modifiers.ctrl {
mods += 16;
}
match format {
MouseFormat::SGR => {
Some(sgr_mouse_report(point, button as u8 + mods, pressed).into_bytes())
}
MouseFormat::Normal(utf8) => {
if pressed {
normal_mouse_report(point, button as u8 + mods, utf8)
} else {
normal_mouse_report(point, 3 + mods, utf8)
}
}
}
}
fn normal_mouse_report(point: Point, button: u8, utf8: bool) -> Option<Vec<u8>> {
let Point { line, column } = point;
let max_point = if utf8 { 2015 } else { 223 };
if line >= max_point || column >= max_point {
return None;
}
let mut msg = vec![b'\x1b', b'[', b'M', 32 + button];
let mouse_pos_encode = |pos: usize| -> Vec<u8> {
let pos = 32 + 1 + pos;
let first = 0xC0 + pos / 64;
let second = 0x80 + (pos & 63);
vec![first as u8, second as u8]
};
if utf8 && column >= 95 {
msg.append(&mut mouse_pos_encode(column.0));
} else {
msg.push(32 + 1 + column.0 as u8);
}
if utf8 && line >= 95 {
msg.append(&mut mouse_pos_encode(line.0 as usize));
} else {
msg.push(32 + 1 + line.0 as u8);
}
Some(msg)
}
fn sgr_mouse_report(point: Point, button: u8, pressed: bool) -> String {
let c = if pressed { 'M' } else { 'm' };
let msg = format!(
"\x1b[<{};{};{}{}",
button,
point.column + 1,
point.line + 1,
c
);
msg
}
#[cfg(test)]
mod test {
use crate::mappings::mouse::mouse_point;
#[test]
fn test_mouse_to_selection() {
let term_width = 100.;
let term_height = 200.;
let cell_width = 10.;
let line_height = 20.;
let mouse_pos_x = 100.; //Window relative
let mouse_pos_y = 100.; //Window relative
let origin_x = 10.;
let origin_y = 20.;
let cur_size = crate::TerminalSize::new(
line_height,
cell_width,
gpui::geometry::vector::vec2f(term_width, term_height),
);
let mouse_pos = gpui::geometry::vector::vec2f(mouse_pos_x, mouse_pos_y);
let origin = gpui::geometry::vector::vec2f(origin_x, origin_y); //Position of terminal window, 1 'cell' in
let mouse_pos = mouse_pos - origin;
let point = mouse_point(mouse_pos, cur_size, 0);
assert_eq!(
point,
alacritty_terminal::index::Point::new(
alacritty_terminal::index::Line(((mouse_pos_y - origin_y) / line_height) as i32),
alacritty_terminal::index::Column(((mouse_pos_x - origin_x) / cell_width) as usize),
)
);
}
}

View file

@ -24,15 +24,20 @@ use futures::{
FutureExt,
};
use mappings::mouse::{
alt_scroll, mouse_button_report, mouse_moved_report, mouse_point, mouse_side, scroll_report,
};
use modal::deploy_modal;
use settings::{Settings, Shell};
use std::{collections::HashMap, fmt::Display, path::PathBuf, sync::Arc, time::Duration};
use settings::{AlternateScroll, Settings, Shell, TerminalBlink};
use std::{collections::HashMap, fmt::Display, ops::Sub, path::PathBuf, sync::Arc, time::Duration};
use thiserror::Error;
use gpui::{
geometry::vector::{vec2f, Vector2F},
keymap::Keystroke,
ClipboardItem, Entity, ModelContext, MutableAppContext,
scene::{ClickRegionEvent, DownRegionEvent, DragRegionEvent, UpRegionEvent},
ClipboardItem, Entity, ModelContext, MouseButton, MouseMovedEvent, MutableAppContext,
ScrollWheelEvent,
};
use crate::mappings::{
@ -42,18 +47,24 @@ use crate::mappings::{
///Initialize and register all of our action handlers
pub fn init(cx: &mut MutableAppContext) {
cx.add_action(deploy_modal);
let settings = cx.global::<Settings>();
if settings.experiments.modal_terminal() {
cx.add_action(deploy_modal);
}
terminal_view::init(cx);
connected_view::init(cx);
}
///Scrolling is unbearably sluggish by default. Alacritty supports a configurable
///Scroll multiplier that is set to 3 by default. This will be removed when I
///Implement scroll bars.
pub const ALACRITTY_SCROLL_MULTIPLIER: f32 = 3.;
const DEBUG_TERMINAL_WIDTH: f32 = 500.;
const DEBUG_TERMINAL_HEIGHT: f32 = 30.; //This needs to be wide enough that the CI & a local dev's prompt can fill the whole space.
const DEBUG_TERMINAL_HEIGHT: f32 = 30.;
const DEBUG_CELL_WIDTH: f32 = 5.;
const DEBUG_LINE_HEIGHT: f32 = 5.;
// const MAX_FRAME_RATE: f32 = 60.;
// const BACK_BUFFER_SIZE: usize = 5000;
///Upward flowing events, for changing the title and such
#[derive(Clone, Copy, Debug)]
@ -62,6 +73,7 @@ pub enum Event {
CloseTerminal,
Bell,
Wakeup,
BlinkChanged,
}
#[derive(Clone, Debug)]
@ -254,6 +266,8 @@ impl TerminalBuilder {
shell: Option<Shell>,
env: Option<HashMap<String, String>>,
initial_size: TerminalSize,
blink_settings: Option<TerminalBlink>,
alternate_scroll: &AlternateScroll,
) -> Result<TerminalBuilder> {
let pty_config = {
let alac_shell = shell.clone().and_then(|shell| match shell {
@ -287,9 +301,24 @@ impl TerminalBuilder {
setup_env(&config);
//Spawn a task so the Alacritty EventLoop can communicate with us in a view context
//TODO: Remove with a bounded sender which can be dispatched on &self
let (events_tx, events_rx) = unbounded();
//Set up the terminal...
let term = Term::new(&config, &initial_size, ZedListener(events_tx.clone()));
let mut term = Term::new(&config, &initial_size, ZedListener(events_tx.clone()));
//Start off blinking if we need to
if let Some(TerminalBlink::On) = blink_settings {
term.set_mode(alacritty_terminal::ansi::Mode::BlinkingCursor)
}
//Start alternate_scroll if we need to
if let AlternateScroll::On = alternate_scroll {
term.set_mode(alacritty_terminal::ansi::Mode::AlternateScroll)
} else {
//Alacritty turns it on by default, so we need to turn it off.
term.unset_mode(alacritty_terminal::ansi::Mode::AlternateScroll)
}
let term = Arc::new(FairMutex::new(term));
//Setup the pty...
@ -321,7 +350,7 @@ impl TerminalBuilder {
//And connect them together
let event_loop = EventLoop::new(
term.clone(),
ZedListener(events_tx),
ZedListener(events_tx.clone()),
pty,
pty_config.hold,
false,
@ -339,7 +368,8 @@ impl TerminalBuilder {
default_title: shell_txt,
last_mode: TermMode::NONE,
cur_size: initial_size,
// utilization: 0.,
last_mouse: None,
last_offset: 0,
};
Ok(TerminalBuilder {
@ -397,27 +427,6 @@ impl TerminalBuilder {
})
.detach();
// //Render loop
// cx.spawn_weak(|this, mut cx| async move {
// loop {
// let utilization = match this.upgrade(&cx) {
// Some(this) => this.update(&mut cx, |this, cx| {
// cx.notify();
// this.utilization()
// }),
// None => break,
// };
// let utilization = (1. - utilization).clamp(0.1, 1.);
// let delay = cx.background().timer(Duration::from_secs_f32(
// 1.0 / (Terminal::default_fps() * utilization),
// ));
// delay.await;
// }
// })
// .detach();
self.terminal
}
}
@ -430,19 +439,11 @@ pub struct Terminal {
title: String,
cur_size: TerminalSize,
last_mode: TermMode,
//Percentage, between 0 and 1
// utilization: f32,
last_offset: usize,
last_mouse: Option<(Point, Direction)>,
}
impl Terminal {
// fn default_fps() -> f32 {
// MAX_FRAME_RATE
// }
// fn utilization(&self) -> f32 {
// self.utilization
// }
fn process_event(&mut self, event: &AlacTermEvent, cx: &mut ModelContext<Self>) {
match event {
AlacTermEvent::Title(title) => {
@ -456,17 +457,17 @@ impl Terminal {
AlacTermEvent::ClipboardStore(_, data) => {
cx.write_to_clipboard(ClipboardItem::new(data.to_string()))
}
AlacTermEvent::ClipboardLoad(_, format) => self.notify_pty(format(
AlacTermEvent::ClipboardLoad(_, format) => self.write_to_pty(format(
&cx.read_from_clipboard()
.map(|ci| ci.text().to_string())
.unwrap_or_else(|| "".to_string()),
)),
AlacTermEvent::PtyWrite(out) => self.notify_pty(out.clone()),
AlacTermEvent::PtyWrite(out) => self.write_to_pty(out.clone()),
AlacTermEvent::TextAreaSizeRequest(format) => {
self.notify_pty(format(self.cur_size.into()))
self.write_to_pty(format(self.cur_size.into()))
}
AlacTermEvent::CursorBlinkingChange => {
//TODO whatever state we need to set to get the cursor blinking
cx.emit(Event::BlinkChanged);
}
AlacTermEvent::Bell => {
cx.emit(Event::Bell);
@ -485,12 +486,6 @@ impl Terminal {
}
}
// fn process_events(&mut self, events: Vec<AlacTermEvent>, cx: &mut ModelContext<Self>) {
// for event in events.into_iter() {
// self.process_event(&event, cx);
// }
// }
///Takes events from Alacritty and translates them to behavior on this view
fn process_terminal_event(
&mut self,
@ -498,7 +493,6 @@ impl Terminal {
term: &mut Term<ZedListener>,
cx: &mut ModelContext<Self>,
) {
// TODO: Handle is_self_focused in subscription on terminal view
match event {
InternalEvent::TermEvent(term_event) => {
if let AlacTermEvent::ColorRequest(index, format) = term_event {
@ -506,7 +500,7 @@ impl Terminal {
let term_style = &cx.global::<Settings>().theme.terminal;
to_alac_rgb(get_color_at_index(index, &term_style.colors))
});
self.notify_pty(format(color))
self.write_to_pty(format(color))
}
}
InternalEvent::Resize(new_size) => {
@ -517,7 +511,7 @@ impl Terminal {
term.resize(*new_size);
}
InternalEvent::Clear => {
self.notify_pty("\x0c".to_string());
self.write_to_pty("\x0c".to_string());
term.clear_screen(ClearMode::Saved);
}
InternalEvent::Scroll(scroll) => term.scroll_display(*scroll),
@ -537,12 +531,14 @@ impl Terminal {
}
}
pub fn notify_pty(&self, txt: String) {
self.pty_tx.notify(txt.into_bytes());
pub fn input(&mut self, input: String) {
self.events.push(InternalEvent::Scroll(Scroll::Bottom));
self.events.push(InternalEvent::SetSelection(None));
self.write_to_pty(input);
}
///Write the Input payload to the tty.
pub fn write_to_pty(&mut self, input: String) {
fn write_to_pty(&self, input: String) {
self.pty_tx.notify(input.into_bytes());
}
@ -555,10 +551,10 @@ impl Terminal {
self.events.push(InternalEvent::Clear)
}
pub fn try_keystroke(&self, keystroke: &Keystroke) -> bool {
pub fn try_keystroke(&mut self, keystroke: &Keystroke) -> bool {
let esc = to_esc_str(keystroke, &self.last_mode);
if let Some(esc) = esc {
self.notify_pty(esc);
self.input(esc);
true
} else {
false
@ -566,14 +562,13 @@ impl Terminal {
}
///Paste text into the terminal
pub fn paste(&self, text: &str) {
if self.last_mode.contains(TermMode::BRACKETED_PASTE) {
self.notify_pty("\x1b[200~".to_string());
self.notify_pty(text.replace('\x1b', ""));
self.notify_pty("\x1b[201~".to_string());
pub fn paste(&mut self, text: &str) {
let paste_text = if self.last_mode.contains(TermMode::BRACKETED_PASTE) {
format!("{}{}{}", "\x1b[200~", text.replace('\x1b', ""), "\x1b[201~")
} else {
self.notify_pty(text.replace("\r\n", "\r").replace('\n', "\r"));
}
text.replace("\r\n", "\r").replace('\n', "\r")
};
self.input(paste_text)
}
pub fn copy(&mut self) {
@ -591,56 +586,165 @@ impl Terminal {
self.process_terminal_event(&e, &mut term, cx)
}
// self.utilization = Self::estimate_utilization(term.take_last_processed_bytes());
self.last_mode = *term.mode();
let content = term.renderable_content();
self.last_offset = content.display_offset;
let cursor_text = term.grid()[content.cursor.point].c;
f(content, cursor_text)
}
// fn estimate_utilization(last_processed: usize) -> f32 {
// let buffer_utilization = (last_processed as f32 / (READ_BUFFER_SIZE as f32)).clamp(0., 1.);
pub fn focus_in(&self) {
if self.last_mode.contains(TermMode::FOCUS_IN_OUT) {
self.write_to_pty("\x1b[I".to_string());
}
}
// //Scale result to bias low, then high
// buffer_utilization * buffer_utilization
// }
pub fn focus_out(&self) {
if self.last_mode.contains(TermMode::FOCUS_IN_OUT) {
self.write_to_pty("\x1b[O".to_string());
}
}
pub fn mouse_changed(&mut self, point: Point, side: Direction) -> bool {
match self.last_mouse {
Some((old_point, old_side)) => {
if old_point == point && old_side == side {
false
} else {
self.last_mouse = Some((point, side));
true
}
}
None => {
self.last_mouse = Some((point, side));
true
}
}
}
pub fn mouse_mode(&self, shift: bool) -> bool {
self.last_mode.intersects(TermMode::MOUSE_MODE) && !shift
}
pub fn mouse_move(&mut self, e: &MouseMovedEvent, origin: Vector2F) {
let position = e.position.sub(origin);
let point = mouse_point(position, self.cur_size, self.last_offset);
let side = mouse_side(position, self.cur_size);
if self.mouse_changed(point, side) && self.mouse_mode(e.shift) {
if let Some(bytes) = mouse_moved_report(point, e, self.last_mode) {
self.pty_tx.notify(bytes);
}
}
}
pub fn mouse_drag(&mut self, e: DragRegionEvent, origin: Vector2F) {
let position = e.position.sub(origin);
if !self.mouse_mode(e.shift) {
let point = mouse_point(position, self.cur_size, self.last_offset);
let side = mouse_side(position, self.cur_size);
self.events
.push(InternalEvent::UpdateSelection((point, side)));
}
}
pub fn mouse_down(&mut self, e: &DownRegionEvent, origin: Vector2F) {
let position = e.position.sub(origin);
let point = mouse_point(position, self.cur_size, self.last_offset);
let side = mouse_side(position, self.cur_size);
if self.mouse_mode(e.shift) {
if let Some(bytes) = mouse_button_report(point, e, true, self.last_mode) {
self.pty_tx.notify(bytes);
}
} else if e.button == MouseButton::Left {
self.events
.push(InternalEvent::SetSelection(Some(Selection::new(
SelectionType::Simple,
point,
side,
))));
}
}
pub fn left_click(&mut self, e: &ClickRegionEvent, origin: Vector2F) {
let position = e.position.sub(origin);
if !self.mouse_mode(e.shift) {
let point = mouse_point(position, self.cur_size, self.last_offset);
let side = mouse_side(position, self.cur_size);
let selection_type = match e.click_count {
0 => return, //This is a release
1 => Some(SelectionType::Simple),
2 => Some(SelectionType::Semantic),
3 => Some(SelectionType::Lines),
_ => None,
};
let selection =
selection_type.map(|selection_type| Selection::new(selection_type, point, side));
self.events.push(InternalEvent::SetSelection(selection));
}
}
pub fn mouse_up(&mut self, e: &UpRegionEvent, origin: Vector2F) {
let position = e.position.sub(origin);
if self.mouse_mode(e.shift) {
let point = mouse_point(position, self.cur_size, self.last_offset);
if let Some(bytes) = mouse_button_report(point, e, false, self.last_mode) {
self.pty_tx.notify(bytes);
}
} else if e.button == MouseButton::Left {
// Seems pretty standard to automatically copy on mouse_up for terminals,
// so let's do that here
self.copy();
}
}
///Scroll the terminal
pub fn scroll(&mut self, scroll: Scroll) {
self.events.push(InternalEvent::Scroll(scroll));
}
pub fn scroll(&mut self, scroll: &ScrollWheelEvent, origin: Vector2F) {
if self.mouse_mode(scroll.shift) {
//TODO: Currently this only sends the current scroll reports as they come in. Alacritty
//Sends the *entire* scroll delta on *every* scroll event, only resetting it when
//The scroll enters 'TouchPhase::Started'. Do I need to replicate this?
//This would be consistent with a scroll model based on 'distance from origin'...
let scroll_lines = (scroll.delta.y() / self.cur_size.line_height) as i32;
let point = mouse_point(scroll.position.sub(origin), self.cur_size, self.last_offset);
pub fn click(&mut self, point: Point, side: Direction, clicks: usize) {
let selection_type = match clicks {
0 => return, //This is a release
1 => Some(SelectionType::Simple),
2 => Some(SelectionType::Semantic),
3 => Some(SelectionType::Lines),
_ => None,
};
if let Some(scrolls) = scroll_report(point, scroll_lines as i32, scroll, self.last_mode)
{
for scroll in scrolls {
self.pty_tx.notify(scroll);
}
};
} else if self
.last_mode
.contains(TermMode::ALT_SCREEN | TermMode::ALTERNATE_SCROLL)
&& !scroll.shift
{
//TODO: See above TODO, also applies here.
let scroll_lines = ((scroll.delta.y() * ALACRITTY_SCROLL_MULTIPLIER)
/ self.cur_size.line_height) as i32;
let selection =
selection_type.map(|selection_type| Selection::new(selection_type, point, side));
self.events.push(InternalEvent::SetSelection(selection));
}
pub fn drag(&mut self, point: Point, side: Direction) {
self.events
.push(InternalEvent::UpdateSelection((point, side)));
}
///TODO: Check if the mouse_down-then-click assumption holds, so this code works as expected
pub fn mouse_down(&mut self, point: Point, side: Direction) {
self.events
.push(InternalEvent::SetSelection(Some(Selection::new(
SelectionType::Simple,
point,
side,
))));
self.pty_tx.notify(alt_scroll(scroll_lines))
} else {
let scroll_lines = ((scroll.delta.y() * ALACRITTY_SCROLL_MULTIPLIER)
/ self.cur_size.line_height) as i32;
if scroll_lines != 0 {
let scroll = Scroll::Delta(scroll_lines);
self.events.push(InternalEvent::Scroll(scroll));
}
}
}
}

View file

@ -10,7 +10,7 @@ use workspace::{Item, Workspace};
use crate::TerminalSize;
use project::{LocalWorktree, Project, ProjectPath};
use settings::{Settings, WorkingDirectory};
use settings::{AlternateScroll, Settings, WorkingDirectory};
use smallvec::SmallVec;
use std::path::{Path, PathBuf};
@ -94,8 +94,27 @@ impl TerminalView {
let shell = settings.terminal_overrides.shell.clone();
let envs = settings.terminal_overrides.env.clone(); //Should be short and cheap.
let content = match TerminalBuilder::new(working_directory.clone(), shell, envs, size_info)
{
//TODO: move this pattern to settings
let scroll = settings
.terminal_overrides
.alternate_scroll
.as_ref()
.unwrap_or(
settings
.terminal_defaults
.alternate_scroll
.as_ref()
.unwrap_or_else(|| &AlternateScroll::On),
);
let content = match TerminalBuilder::new(
working_directory.clone(),
shell,
envs,
size_info,
settings.terminal_overrides.blinking.clone(),
scroll,
) {
Ok(terminal) => {
let terminal = cx.add_model(|cx| terminal.subscribe(cx));
let view = cx.add_view(|cx| ConnectedView::from_terminal(terminal, modal, cx));

View file

@ -512,7 +512,6 @@ pub struct EditOperation {
pub struct UndoOperation {
pub id: clock::Local,
pub counts: HashMap<clock::Local, u32>,
pub transaction_version: clock::Global,
pub version: clock::Global,
}
@ -1109,14 +1108,8 @@ impl Buffer {
let mut fragment = fragment.clone();
let fragment_was_visible = fragment.visible;
if fragment.was_visible(&undo.transaction_version, &self.undo_map)
|| undo
.counts
.contains_key(&fragment.insertion_timestamp.local())
{
fragment.visible = fragment.is_visible(&self.undo_map);
fragment.max_undos.observe(undo.id);
}
fragment.visible = fragment.is_visible(&self.undo_map);
fragment.max_undos.observe(undo.id);
let old_start = old_fragments.start().1;
let new_start = new_fragments.summary().text.visible;
@ -1297,7 +1290,6 @@ impl Buffer {
id: self.local_clock.tick(),
version: self.version(),
counts,
transaction_version: transaction.start,
};
self.apply_undo(&undo)?;
let operation = Operation::Undo {

View file

@ -262,6 +262,7 @@ pub struct AppState {
pub trait Item: View {
fn deactivated(&mut self, _: &mut ViewContext<Self>) {}
fn workspace_deactivated(&mut self, _: &mut ViewContext<Self>) {}
fn navigate(&mut self, _: Box<dyn Any>, _: &mut ViewContext<Self>) -> bool {
false
}
@ -434,6 +435,7 @@ pub trait ItemHandle: 'static + fmt::Debug {
cx: &mut ViewContext<Workspace>,
);
fn deactivated(&self, cx: &mut MutableAppContext);
fn workspace_deactivated(&self, cx: &mut MutableAppContext);
fn navigate(&self, data: Box<dyn Any>, cx: &mut MutableAppContext) -> bool;
fn id(&self) -> usize;
fn to_any(&self) -> AnyViewHandle;
@ -630,6 +632,10 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
self.update(cx, |this, cx| this.deactivated(cx));
}
fn workspace_deactivated(&self, cx: &mut MutableAppContext) {
self.update(cx, |this, cx| this.workspace_deactivated(cx));
}
fn navigate(&self, data: Box<dyn Any>, cx: &mut MutableAppContext) -> bool {
self.update(cx, |this, cx| this.navigate(data, cx))
}
@ -1868,33 +1874,41 @@ impl Workspace {
};
ConstrainedBox::new(
Container::new(
Stack::new()
.with_child(
Label::new(worktree_root_names, theme.workspace.titlebar.title.clone())
.aligned()
.left()
.boxed(),
)
.with_child(
Align::new(
Flex::row()
.with_children(self.render_collaborators(theme, cx))
.with_children(self.render_current_user(
self.user_store.read(cx).current_user().as_ref(),
replica_id,
theme,
cx,
))
.with_children(self.render_connection_status(cx))
MouseEventHandler::new::<Self, _, _>(0, cx, |_, cx| {
Container::new(
Stack::new()
.with_child(
Label::new(worktree_root_names, theme.workspace.titlebar.title.clone())
.aligned()
.left()
.boxed(),
)
.right()
.with_child(
Align::new(
Flex::row()
.with_children(self.render_collaborators(theme, cx))
.with_children(self.render_current_user(
self.user_store.read(cx).current_user().as_ref(),
replica_id,
theme,
cx,
))
.with_children(self.render_connection_status(cx))
.boxed(),
)
.right()
.boxed(),
)
.boxed(),
)
.boxed(),
)
.with_style(container_theme)
)
.with_style(container_theme)
.boxed()
})
.on_click(MouseButton::Left, |event, cx| {
if event.click_count == 2 {
cx.zoom_window(cx.window_id());
}
})
.boxed(),
)
.with_height(theme.workspace.titlebar.height)
@ -2387,18 +2401,21 @@ impl Workspace {
None
}
fn on_window_activation_changed(&mut self, active: bool, cx: &mut ViewContext<Self>) {
if !active
&& matches!(
cx.global::<Settings>().autosave,
Autosave::OnWindowChange | Autosave::OnFocusChange
)
{
pub fn on_window_activation_changed(&mut self, active: bool, cx: &mut ViewContext<Self>) {
if !active {
for pane in &self.panes {
pane.update(cx, |pane, cx| {
for item in pane.items() {
Pane::autosave_item(item.as_ref(), self.project.clone(), cx)
.detach_and_log_err(cx);
if let Some(item) = pane.active_item() {
item.workspace_deactivated(cx);
}
if matches!(
cx.global::<Settings>().autosave,
Autosave::OnWindowChange | Autosave::OnFocusChange
) {
for item in pane.items() {
Pane::autosave_item(item.as_ref(), self.project.clone(), cx)
.detach_and_log_err(cx);
}
}
});
}

View file

@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathansobo@gmail.com>"]
description = "The fast, collaborative code editor."
edition = "2021"
name = "zed"
version = "0.50.0"
version = "0.52.0"
[lib]
name = "zed"
@ -64,7 +64,6 @@ dirs = "3.0"
easy-parallel = "3.1.0"
env_logger = "0.9"
futures = "0.3"
http-auth-basic = "0.1.3"
ignore = "0.4"
image = "0.23"
indexmap = "1.6.2"
@ -92,6 +91,7 @@ toml = "0.5"
tree-sitter = "0.20"
tree-sitter-c = "0.20.1"
tree-sitter-cpp = "0.20.0"
tree-sitter-elixir = { git = "https://github.com/elixir-lang/tree-sitter-elixir", rev = "05e3631c6a0701c1fa518b0fee7be95a2ceef5e2" }
tree-sitter-go = { git = "https://github.com/tree-sitter/tree-sitter-go", rev = "aeb2f33b366fd78d5789ff104956ce23508b85db" }
tree-sitter-json = { git = "https://github.com/tree-sitter/tree-sitter-json", rev = "137e1ce6a02698fc246cdb9c6b886ed1de9a1ed8" }
tree-sitter-rust = "0.20.1"

View file

@ -5,6 +5,7 @@ use rust_embed::RustEmbed;
use std::{borrow::Cow, str, sync::Arc};
mod c;
mod elixir;
mod go;
mod installation;
mod json;
@ -45,6 +46,11 @@ pub async fn init(languages: Arc<LanguageRegistry>, _executor: Arc<Background>)
tree_sitter_cpp::language(),
Some(CachedLspAdapter::new(c::CLspAdapter).await),
),
(
"elixir",
tree_sitter_elixir::language(),
Some(CachedLspAdapter::new(elixir::ElixirLspAdapter).await),
),
(
"go",
tree_sitter_go::language(),

View file

@ -0,0 +1,195 @@
use super::installation::{latest_github_release, GitHubLspBinaryVersion};
use anyhow::{anyhow, Context, Result};
use async_trait::async_trait;
use client::http::HttpClient;
use futures::StreamExt;
pub use language::*;
use lsp::{CompletionItemKind, SymbolKind};
use smol::fs::{self, File};
use std::{any::Any, path::PathBuf, sync::Arc};
use util::ResultExt;
pub struct ElixirLspAdapter;
#[async_trait]
impl LspAdapter for ElixirLspAdapter {
async fn name(&self) -> LanguageServerName {
LanguageServerName("elixir-ls".into())
}
async fn fetch_latest_server_version(
&self,
http: Arc<dyn HttpClient>,
) -> Result<Box<dyn 'static + Send + Any>> {
let release = latest_github_release("elixir-lsp/elixir-ls", http).await?;
let asset_name = "elixir-ls.zip";
let asset = release
.assets
.iter()
.find(|asset| asset.name == asset_name)
.ok_or_else(|| anyhow!("no asset found matching {:?}", asset_name))?;
let version = GitHubLspBinaryVersion {
name: release.name,
url: asset.browser_download_url.clone(),
};
Ok(Box::new(version) as Box<_>)
}
async fn fetch_server_binary(
&self,
version: Box<dyn 'static + Send + Any>,
http: Arc<dyn HttpClient>,
container_dir: PathBuf,
) -> Result<PathBuf> {
let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
let zip_path = container_dir.join(format!("elixir-ls_{}.zip", version.name));
let version_dir = container_dir.join(format!("elixir-ls_{}", version.name));
let binary_path = version_dir.join("language_server.sh");
if fs::metadata(&binary_path).await.is_err() {
let mut response = http
.get(&version.url, Default::default(), true)
.await
.context("error downloading release")?;
let mut file = File::create(&zip_path)
.await
.with_context(|| format!("failed to create file {}", zip_path.display()))?;
if !response.status().is_success() {
Err(anyhow!(
"download failed with status {}",
response.status().to_string()
))?;
}
futures::io::copy(response.body_mut(), &mut file).await?;
fs::create_dir_all(&version_dir)
.await
.with_context(|| format!("failed to create directory {}", version_dir.display()))?;
let unzip_status = smol::process::Command::new("unzip")
.arg(&zip_path)
.arg("-d")
.arg(&version_dir)
.output()
.await?
.status;
if !unzip_status.success() {
Err(anyhow!("failed to unzip clangd archive"))?;
}
if let Some(mut entries) = fs::read_dir(&container_dir).await.log_err() {
while let Some(entry) = entries.next().await {
if let Some(entry) = entry.log_err() {
let entry_path = entry.path();
if entry_path.as_path() != version_dir {
if let Ok(metadata) = fs::metadata(&entry_path).await {
if metadata.is_file() {
fs::remove_file(&entry_path).await.log_err();
} else {
fs::remove_dir_all(&entry_path).await.log_err();
}
}
}
}
}
}
}
Ok(binary_path)
}
async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<PathBuf> {
(|| async move {
let mut last = None;
let mut entries = fs::read_dir(&container_dir).await?;
while let Some(entry) = entries.next().await {
last = Some(entry?.path());
}
last.ok_or_else(|| anyhow!("no cached binary"))
})()
.await
.log_err()
}
async fn label_for_completion(
&self,
completion: &lsp::CompletionItem,
language: &Language,
) -> Option<CodeLabel> {
match completion.kind.zip(completion.detail.as_ref()) {
Some((_, detail)) if detail.starts_with("(function)") => {
let text = detail.strip_prefix("(function) ")?;
let filter_range = 0..text.find('(').unwrap_or(text.len());
let source = Rope::from(format!("def {text}").as_str());
let runs = language.highlight_text(&source, 4..4 + text.len());
return Some(CodeLabel {
text: text.to_string(),
runs,
filter_range,
});
}
Some((_, detail)) if detail.starts_with("(macro)") => {
let text = detail.strip_prefix("(macro) ")?;
let filter_range = 0..text.find('(').unwrap_or(text.len());
let source = Rope::from(format!("defmacro {text}").as_str());
let runs = language.highlight_text(&source, 9..9 + text.len());
return Some(CodeLabel {
text: text.to_string(),
runs,
filter_range,
});
}
Some((
CompletionItemKind::CLASS
| CompletionItemKind::MODULE
| CompletionItemKind::INTERFACE
| CompletionItemKind::STRUCT,
_,
)) => {
let filter_range = 0..completion
.label
.find(" (")
.unwrap_or(completion.label.len());
let text = &completion.label[filter_range.clone()];
let source = Rope::from(format!("defmodule {text}").as_str());
let runs = language.highlight_text(&source, 10..10 + text.len());
return Some(CodeLabel {
text: completion.label.clone(),
runs,
filter_range,
});
}
_ => {}
}
None
}
async fn label_for_symbol(
&self,
name: &str,
kind: SymbolKind,
language: &Language,
) -> Option<CodeLabel> {
let (text, filter_range, display_range) = match kind {
SymbolKind::METHOD | SymbolKind::FUNCTION => {
let text = format!("def {}", name);
let filter_range = 4..4 + name.len();
let display_range = 0..filter_range.end;
(text, filter_range, display_range)
}
SymbolKind::CLASS | SymbolKind::MODULE | SymbolKind::INTERFACE | SymbolKind::STRUCT => {
let text = format!("defmodule {}", name);
let filter_range = 10..10 + name.len();
let display_range = 0..filter_range.end;
(text, filter_range, display_range)
}
_ => return None,
};
Some(CodeLabel {
runs: language.highlight_text(&text.as_str().into(), display_range.clone()),
text: text[display_range].to_string(),
filter_range,
})
}
}

View file

@ -0,0 +1,5 @@
("(" @open ")" @close)
("[" @open "]" @close)
("{" @open "}" @close)
("\"" @open "\"" @close)
("do" @open "end" @close)

View file

@ -0,0 +1,10 @@
name = "Elixir"
path_suffixes = ["ex", "exs"]
line_comment = "# "
autoclose_before = ";:.,=}])>"
brackets = [
{ start = "{", end = "}", close = true, newline = true },
{ start = "[", end = "]", close = true, newline = true },
{ start = "(", end = ")", close = true, newline = true },
{ start = "\"", end = "\"", close = true, newline = false }
]

View file

@ -0,0 +1,155 @@
["when" "and" "or" "not" "in" "not in" "fn" "do" "end" "catch" "rescue" "after" "else"] @keyword
(unary_operator
operator: "@" @comment.doc
operand: (call
target: (identifier) @comment.doc.__attribute__
(arguments
[
(string) @comment.doc
(charlist) @comment.doc
(sigil
quoted_start: _ @comment.doc
quoted_end: _ @comment.doc) @comment.doc
(boolean) @comment.doc
]))
(#match? @comment.doc.__attribute__ "^(moduledoc|typedoc|doc)$"))
(unary_operator
operator: "&"
operand: (integer) @operator)
(operator_identifier) @operator
(unary_operator
operator: _ @operator)
(binary_operator
operator: _ @operator)
(dot
operator: _ @operator)
(stab_clause
operator: _ @operator)
[
(boolean)
(nil)
] @constant
[
(integer)
(float)
] @number
(alias) @type
(call
target: (dot
left: (atom) @type))
(char) @constant
(interpolation "#{" @punctuation.special "}" @punctuation.special) @embedded
(escape_sequence) @string.escape
[
(atom)
(quoted_atom)
(keyword)
(quoted_keyword)
] @string.special.symbol
[
(string)
(charlist)
] @string
(sigil
(sigil_name) @__name__
quoted_start: _ @string
quoted_end: _ @string
(#match? @__name__ "^[sS]$")) @string
(sigil
(sigil_name) @__name__
quoted_start: _ @string.regex
quoted_end: _ @string.regex
(#match? @__name__ "^[rR]$")) @string.regex
(sigil
(sigil_name) @__name__
quoted_start: _ @string.special
quoted_end: _ @string.special) @string.special
(call
target: [
(identifier) @function
(dot
right: (identifier) @function)
])
(call
target: (identifier) @keyword
(arguments
[
(identifier) @function
(binary_operator
left: (identifier) @function
operator: "when")
])
(#match? @keyword "^(def|defdelegate|defguard|defguardp|defmacro|defmacrop|defn|defnp|defp)$"))
(call
target: (identifier) @keyword
(arguments
(binary_operator
operator: "|>"
right: (identifier)))
(#match? @keyword "^(def|defdelegate|defguard|defguardp|defmacro|defmacrop|defn|defnp|defp)$"))
(binary_operator
operator: "|>"
right: (identifier) @function)
(call
target: (identifier) @keyword
(#match? @keyword "^(def|defdelegate|defexception|defguard|defguardp|defimpl|defmacro|defmacrop|defmodule|defn|defnp|defoverridable|defp|defprotocol|defstruct)$"))
(call
target: (identifier) @keyword
(#match? @keyword "^(alias|case|cond|else|for|if|import|quote|raise|receive|require|reraise|super|throw|try|unless|unquote|unquote_splicing|use|with)$"))
(
(identifier) @constant.builtin
(#match? @constant.builtin "^(__MODULE__|__DIR__|__ENV__|__CALLER__|__STACKTRACE__)$")
)
(
(identifier) @comment.unused
(#match? @comment.unused "^_")
)
(comment) @comment
[
"%"
] @punctuation
[
","
";"
] @punctuation.delimiter
[
"("
")"
"["
"]"
"{"
"}"
"<<"
">>"
] @punctuation.bracket

View file

@ -0,0 +1,8 @@
[
(call)
] @indent
(_ "[" "]" @end) @indent
(_ "{" "}" @end) @indent
(_ "(" ")" @end) @indent
(_ "do" "end" @end) @indent

View file

@ -0,0 +1,16 @@
(call
target: (identifier) @context
(arguments (alias) @name)
(#match? @context "^(defmodule|defprotocol)$")) @item
(call
target: (identifier) @context
(arguments
[
(identifier) @name
(call target: (identifier) @name)
(binary_operator
left: (call target: (identifier) @name)
operator: "when")
])
(#match? @context "^(def|defp|defdelegate|defguard|defguardp|defmacro|defmacrop|defn|defnp)$")) @item

View file

@ -95,6 +95,12 @@ fn main() {
.spawn(languages::init(languages.clone(), cx.background().clone()));
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http.clone(), cx));
let (settings_file, keymap_file) = cx.background().block(config_files).unwrap();
//Setup settings global before binding actions
watch_settings_file(default_settings, settings_file, themes.clone(), cx);
watch_keymap_file(keymap_file, cx);
context_menu::init(cx);
project::Project::init(&client);
client::Channel::init(&client);
@ -114,10 +120,6 @@ fn main() {
terminal::init(cx);
let db = cx.background().block(db);
let (settings_file, keymap_file) = cx.background().block(config_files).unwrap();
watch_settings_file(default_settings, settings_file, themes.clone(), cx);
watch_keymap_file(keymap_file, cx);
cx.spawn(|cx| watch_themes(fs.clone(), themes.clone(), cx))
.detach();

View file

@ -242,11 +242,11 @@ pub fn menus() -> Vec<Menu<'static>> {
MenuItem::Separator,
MenuItem::Action {
name: "Project Panel",
action: Box::new(project_panel::Toggle),
action: Box::new(project_panel::ToggleFocus),
},
MenuItem::Action {
name: "Contacts Panel",
action: Box::new(contacts_panel::Toggle),
action: Box::new(contacts_panel::ToggleFocus),
},
MenuItem::Action {
name: "Command Palette",

View file

@ -51,6 +51,7 @@ actions!(
ShowAll,
Minimize,
Zoom,
ToggleFullScreen,
Quit,
DebugElements,
OpenSettings,
@ -88,6 +89,11 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
cx.zoom_window();
},
);
cx.add_action(
|_: &mut Workspace, _: &ToggleFullScreen, cx: &mut ViewContext<Workspace>| {
cx.toggle_full_screen();
},
);
cx.add_global_action(quit);
cx.add_global_action(move |action: &OpenBrowser, cx| cx.platform().open_url(&action.url));
cx.add_global_action(move |_: &IncreaseBufferFontSize, cx| {
@ -195,12 +201,16 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
},
);
cx.add_action(
|workspace: &mut Workspace, _: &project_panel::Toggle, cx: &mut ViewContext<Workspace>| {
|workspace: &mut Workspace,
_: &project_panel::ToggleFocus,
cx: &mut ViewContext<Workspace>| {
workspace.toggle_sidebar_item_focus(Side::Left, 0, cx);
},
);
cx.add_action(
|workspace: &mut Workspace, _: &contacts_panel::Toggle, cx: &mut ViewContext<Workspace>| {
|workspace: &mut Workspace,
_: &contacts_panel::ToggleFocus,
cx: &mut ViewContext<Workspace>| {
workspace.toggle_sidebar_item_focus(Side::Right, 0, cx);
},
);

13
script/bootstrap Executable file
View file

@ -0,0 +1,13 @@
#!/usr/bin/env bash
echo "installing foreman..."
which foreman > /dev/null || brew install foreman
echo "creating database..."
script/sqlx database create
echo "migrating database..."
script/sqlx migrate run
echo "seeding database..."
script/seed-db

View file

@ -4,6 +4,6 @@ set -e
cd crates/collab
# Export contents of .env.toml
eval "$(cargo run --bin dotenv)"
eval "$(cargo run --quiet --bin dotenv)"
cargo run --package=collab --features seed-support --bin seed -- $@
cargo run --quiet --package=collab --features seed-support --bin seed -- $@

View file

@ -8,7 +8,7 @@ set -e
cd crates/collab
# Export contents of .env.toml
eval "$(cargo run --bin dotenv)"
eval "$(cargo run --quiet --bin dotenv)"
# Run sqlx command
sqlx $@