mirror of
https://github.com/zed-industries/zed.git
synced 2025-01-23 18:32:17 +00:00
Merge branch 'main' into theme-improvements
This commit is contained in:
commit
bcc554a3db
76 changed files with 4053 additions and 1557 deletions
4
Cargo.lock
generated
4
Cargo.lock
generated
|
@ -1611,6 +1611,7 @@ dependencies = [
|
|||
"anyhow",
|
||||
"clock",
|
||||
"collections",
|
||||
"context_menu",
|
||||
"ctor",
|
||||
"env_logger",
|
||||
"futures",
|
||||
|
@ -5364,6 +5365,7 @@ dependencies = [
|
|||
"ordered-float",
|
||||
"project",
|
||||
"settings",
|
||||
"shellexpand",
|
||||
"smallvec",
|
||||
"theme",
|
||||
"util",
|
||||
|
@ -6990,7 +6992,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.46.0"
|
||||
version = "0.48.1"
|
||||
dependencies = [
|
||||
"activity_indicator",
|
||||
"anyhow",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# syntax = docker/dockerfile:1.2
|
||||
|
||||
FROM rust:1.58-bullseye as builder
|
||||
FROM rust:1.62-bullseye as builder
|
||||
WORKDIR app
|
||||
COPY . .
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# syntax = docker/dockerfile:1.2
|
||||
|
||||
FROM rust:1.58-bullseye as builder
|
||||
FROM rust:1.62-bullseye as builder
|
||||
WORKDIR app
|
||||
RUN --mount=type=cache,target=/usr/local/cargo/registry \
|
||||
--mount=type=cache,target=./target \
|
||||
|
|
|
@ -407,20 +407,22 @@
|
|||
{
|
||||
"context": "Terminal",
|
||||
"bindings": {
|
||||
"ctrl-c": "terminal::Sigint",
|
||||
"escape": "terminal::Escape",
|
||||
"shift-escape": "terminal::DeployModal",
|
||||
"ctrl-d": "terminal::Quit",
|
||||
"backspace": "terminal::Del",
|
||||
"enter": "terminal::Return",
|
||||
"left": "terminal::Left",
|
||||
"right": "terminal::Right",
|
||||
// Overrides for global bindings, remove at your own risk:
|
||||
"up": "terminal::Up",
|
||||
"down": "terminal::Down",
|
||||
"tab": "terminal::Tab",
|
||||
"cmd-v": "terminal::Paste",
|
||||
"escape": "terminal::Escape",
|
||||
"enter": "terminal::Enter",
|
||||
"ctrl-c": "terminal::CtrlC",
|
||||
// Useful terminal actions:
|
||||
"cmd-c": "terminal::Copy",
|
||||
"ctrl-l": "terminal::Clear"
|
||||
"cmd-v": "terminal::Paste",
|
||||
"cmd-k": "terminal::Clear"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ModalTerminal",
|
||||
"bindings": {
|
||||
"shift-escape": "terminal::DeployModal"
|
||||
}
|
||||
}
|
||||
]
|
|
@ -1,29 +1,25 @@
|
|||
{
|
||||
// The name of the Zed theme to use for the UI
|
||||
"theme": "cave-dark",
|
||||
|
||||
// The name of a font to use for rendering text in the editor
|
||||
"buffer_font_family": "Zed Mono",
|
||||
|
||||
// The default font size for text in the editor
|
||||
"buffer_font_size": 15,
|
||||
|
||||
// Whether to enable vim modes and key bindings
|
||||
"vim_mode": false,
|
||||
|
||||
// Whether to show the informational hover box when moving the mouse
|
||||
// over symbols in the editor.
|
||||
"hover_popover_enabled": true,
|
||||
|
||||
// Whether to pop the completions menu while typing in an editor without
|
||||
// explicitly requesting it.
|
||||
"show_completions_on_input": true,
|
||||
// Whether new projects should start out 'online'. Online projects
|
||||
// appear in the contacts panel under your name, so that your contacts
|
||||
// can see which projects you are working on. Regardless of this
|
||||
// setting, projects keep their last online status when you reopen them.
|
||||
"projects_online_by_default": true,
|
||||
|
||||
// Whether to use language servers to provide code intelligence.
|
||||
"enable_language_server": true,
|
||||
|
||||
// When to automatically save edited buffers. This setting can
|
||||
// take four values.
|
||||
//
|
||||
|
@ -36,7 +32,6 @@
|
|||
// 4. Save when idle for a certain amount of time:
|
||||
// "autosave": { "after_delay": {"milliseconds": 500} },
|
||||
"autosave": "off",
|
||||
|
||||
// How to auto-format modified buffers when saving them. This
|
||||
// setting can take three values:
|
||||
//
|
||||
|
@ -47,12 +42,11 @@
|
|||
// 3. Format code using an external command:
|
||||
// "format_on_save": {
|
||||
// "external": {
|
||||
// "command": "sed",
|
||||
// "arguments": ["-e", "s/ *$//"]
|
||||
// "command": "prettier",
|
||||
// "arguments": ["--stdin-filepath", "{buffer_path}"]
|
||||
// }
|
||||
// },
|
||||
// }
|
||||
"format_on_save": "language_server",
|
||||
|
||||
// How to soft-wrap long lines of text. This setting can take
|
||||
// three values:
|
||||
//
|
||||
|
@ -60,21 +54,65 @@
|
|||
// "soft_wrap": "none",
|
||||
// 2. Soft wrap lines that overflow the editor:
|
||||
// "soft_wrap": "editor_width",
|
||||
// 2. Soft wrap lines at the preferred line length
|
||||
// 3. Soft wrap lines at the preferred line length
|
||||
// "soft_wrap": "preferred_line_length",
|
||||
"soft_wrap": "none",
|
||||
|
||||
// The column at which to soft-wrap lines, for buffers where soft-wrap
|
||||
// is enabled.
|
||||
"preferred_line_length": 80,
|
||||
|
||||
// Whether to indent lines using tab characters, as opposed to multiple
|
||||
// spaces.
|
||||
"hard_tabs": false,
|
||||
|
||||
// How many columns a tab should occupy.
|
||||
"tab_size": 4,
|
||||
|
||||
// Settings specific to the terminal
|
||||
"terminal": {
|
||||
// What shell to use when opening a terminal. May take 3 values:
|
||||
// 1. Use the system's default terminal configuration (e.g. $TERM).
|
||||
// "shell": "system"
|
||||
// 2. A program:
|
||||
// "shell": {
|
||||
// "program": "sh"
|
||||
// }
|
||||
// 3. A program with arguments:
|
||||
// "shell": {
|
||||
// "with_arguments": {
|
||||
// "program": "/bin/bash",
|
||||
// "arguments": ["--login"]
|
||||
// }
|
||||
// }
|
||||
"shell": "system",
|
||||
// What working directory to use when launching the terminal.
|
||||
// May take 4 values:
|
||||
// 1. Use the current file's project directory.
|
||||
// "working_directory": "current_project_directory"
|
||||
// 2. Use the first project in this workspace's directory
|
||||
// "working_directory": "first_project_directory"
|
||||
// 3. Always use this platform's home directory (if we can find it)
|
||||
// "working_directory": "always_home"
|
||||
// 4. Always use a specific directory. This value will be shell expanded.
|
||||
// If this path is not a valid directory the terminal will default to
|
||||
// this platform's home directory (if we can find it)
|
||||
// "working_directory": {
|
||||
// "always": {
|
||||
// "directory": "~/zed/projects/"
|
||||
// }
|
||||
// }
|
||||
//
|
||||
//
|
||||
"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, not multiple list items
|
||||
"env": [
|
||||
//["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"
|
||||
},
|
||||
// Different settings for specific languages.
|
||||
"languages": {
|
||||
"Plain Text": {
|
||||
|
|
|
@ -6,3 +6,6 @@
|
|||
// To see all of Zed's default settings without changing your
|
||||
// custom settings, run the `open default settings` command
|
||||
// from the command palette or from `Zed` application menu.
|
||||
{
|
||||
"buffer_font_size": 15
|
||||
}
|
|
@ -3,7 +3,7 @@ use editor::Editor;
|
|||
use futures::StreamExt;
|
||||
use gpui::{
|
||||
actions, elements::*, platform::CursorStyle, Action, AppContext, Entity, ModelHandle,
|
||||
MutableAppContext, RenderContext, View, ViewContext, ViewHandle,
|
||||
MouseButton, MutableAppContext, RenderContext, View, ViewContext, ViewHandle,
|
||||
};
|
||||
use language::{LanguageRegistry, LanguageServerBinaryStatus};
|
||||
use project::{LanguageServerProgress, Project};
|
||||
|
@ -317,7 +317,9 @@ impl View for ActivityIndicator {
|
|||
if let Some(action) = action {
|
||||
element = element
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(move |_, _, cx| cx.dispatch_any_action(action.boxed_clone()));
|
||||
.on_click(MouseButton::Left, move |_, cx| {
|
||||
cx.dispatch_any_action(action.boxed_clone())
|
||||
});
|
||||
}
|
||||
|
||||
element.boxed()
|
||||
|
|
|
@ -2,7 +2,7 @@ use crate::ViewReleaseNotes;
|
|||
use gpui::{
|
||||
elements::{Flex, MouseEventHandler, Padding, ParentElement, Svg, Text},
|
||||
platform::{AppVersion, CursorStyle},
|
||||
Element, Entity, View, ViewContext,
|
||||
Element, Entity, MouseButton, View, ViewContext,
|
||||
};
|
||||
use menu::Cancel;
|
||||
use settings::Settings;
|
||||
|
@ -62,7 +62,7 @@ impl View for UpdateNotification {
|
|||
.boxed()
|
||||
})
|
||||
.with_padding(Padding::uniform(5.))
|
||||
.on_click(move |_, _, cx| cx.dispatch_action(Cancel))
|
||||
.on_click(MouseButton::Left, move |_, cx| cx.dispatch_action(Cancel))
|
||||
.aligned()
|
||||
.constrained()
|
||||
.with_height(cx.font_cache().line_height(theme.message.text.font_size))
|
||||
|
@ -84,7 +84,9 @@ impl View for UpdateNotification {
|
|||
.boxed()
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(|_, _, cx| cx.dispatch_action(ViewReleaseNotes))
|
||||
.on_click(MouseButton::Left, |_, cx| {
|
||||
cx.dispatch_action(ViewReleaseNotes)
|
||||
})
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,8 +8,8 @@ use gpui::{
|
|||
elements::*,
|
||||
platform::CursorStyle,
|
||||
views::{ItemType, Select, SelectStyle},
|
||||
AppContext, Entity, ModelHandle, MutableAppContext, RenderContext, Subscription, Task, View,
|
||||
ViewContext, ViewHandle,
|
||||
AppContext, Entity, ModelHandle, MouseButton, MutableAppContext, RenderContext, Subscription,
|
||||
Task, View, ViewContext, ViewHandle,
|
||||
};
|
||||
use menu::Confirm;
|
||||
use postage::prelude::Stream;
|
||||
|
@ -320,7 +320,7 @@ impl ChatPanel {
|
|||
.boxed()
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(move |_, _, cx| {
|
||||
.on_click(MouseButton::Left, move |_, cx| {
|
||||
let rpc = rpc.clone();
|
||||
let this = this.clone();
|
||||
cx.spawn(|mut cx| async move {
|
||||
|
|
|
@ -17,7 +17,7 @@ use axum::{
|
|||
use axum_extra::response::ErasedJson;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use std::sync::Arc;
|
||||
use std::{sync::Arc, time::Duration};
|
||||
use time::OffsetDateTime;
|
||||
use tower::ServiceBuilder;
|
||||
use tracing::instrument;
|
||||
|
@ -43,6 +43,7 @@ pub fn routes(rpc_server: &Arc<rpc::Server>, state: Arc<AppState>) -> Router<Bod
|
|||
"/user_activity/timeline/:user_id",
|
||||
get(get_user_activity_timeline),
|
||||
)
|
||||
.route("/user_activity/counts", get(get_active_user_counts))
|
||||
.route("/project_metadata", get(get_project_metadata))
|
||||
.layer(
|
||||
ServiceBuilder::new()
|
||||
|
@ -298,6 +299,46 @@ async fn get_user_activity_timeline(
|
|||
Ok(ErasedJson::pretty(summary))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ActiveUserCountParams {
|
||||
#[serde(flatten)]
|
||||
period: TimePeriodParams,
|
||||
durations_in_minutes: String,
|
||||
#[serde(default)]
|
||||
only_collaborative: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ActiveUserSet {
|
||||
active_time_in_minutes: u64,
|
||||
user_count: usize,
|
||||
}
|
||||
|
||||
async fn get_active_user_counts(
|
||||
Query(params): Query<ActiveUserCountParams>,
|
||||
Extension(app): Extension<Arc<AppState>>,
|
||||
) -> Result<ErasedJson> {
|
||||
let durations_in_minutes = params.durations_in_minutes.split(',');
|
||||
let mut user_sets = Vec::new();
|
||||
for duration in durations_in_minutes {
|
||||
let duration = duration
|
||||
.parse()
|
||||
.map_err(|_| anyhow!("invalid duration: {duration}"))?;
|
||||
user_sets.push(ActiveUserSet {
|
||||
active_time_in_minutes: duration,
|
||||
user_count: app
|
||||
.db
|
||||
.get_active_user_count(
|
||||
params.period.start..params.period.end,
|
||||
Duration::from_secs(duration * 60),
|
||||
params.only_collaborative,
|
||||
)
|
||||
.await?,
|
||||
})
|
||||
}
|
||||
Ok(ErasedJson::pretty(user_sets))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct GetProjectMetadataParams {
|
||||
project_id: u64,
|
||||
|
|
|
@ -69,6 +69,15 @@ pub trait Db: Send + Sync {
|
|||
active_projects: &[(UserId, ProjectId)],
|
||||
) -> Result<()>;
|
||||
|
||||
/// Get the number of users who have been active in the given
|
||||
/// time period for at least the given time duration.
|
||||
async fn get_active_user_count(
|
||||
&self,
|
||||
time_period: Range<OffsetDateTime>,
|
||||
min_duration: Duration,
|
||||
only_collaborative: bool,
|
||||
) -> Result<usize>;
|
||||
|
||||
/// Get the users that have been most active during the given time period,
|
||||
/// along with the amount of time they have been active in each project.
|
||||
async fn get_top_users_activity_summary(
|
||||
|
@ -593,6 +602,81 @@ impl Db for PostgresDb {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_active_user_count(
|
||||
&self,
|
||||
time_period: Range<OffsetDateTime>,
|
||||
min_duration: Duration,
|
||||
only_collaborative: bool,
|
||||
) -> Result<usize> {
|
||||
let mut with_clause = String::new();
|
||||
with_clause.push_str("WITH\n");
|
||||
with_clause.push_str(
|
||||
"
|
||||
project_durations AS (
|
||||
SELECT user_id, project_id, SUM(duration_millis) AS project_duration
|
||||
FROM project_activity_periods
|
||||
WHERE $1 < ended_at AND ended_at <= $2
|
||||
GROUP BY user_id, project_id
|
||||
),
|
||||
",
|
||||
);
|
||||
with_clause.push_str(
|
||||
"
|
||||
project_collaborators as (
|
||||
SELECT project_id, COUNT(DISTINCT user_id) as max_collaborators
|
||||
FROM project_durations
|
||||
GROUP BY project_id
|
||||
),
|
||||
",
|
||||
);
|
||||
|
||||
if only_collaborative {
|
||||
with_clause.push_str(
|
||||
"
|
||||
user_durations AS (
|
||||
SELECT user_id, SUM(project_duration) as total_duration
|
||||
FROM project_durations, project_collaborators
|
||||
WHERE
|
||||
project_durations.project_id = project_collaborators.project_id AND
|
||||
max_collaborators > 1
|
||||
GROUP BY user_id
|
||||
ORDER BY total_duration DESC
|
||||
LIMIT $3
|
||||
)
|
||||
",
|
||||
);
|
||||
} else {
|
||||
with_clause.push_str(
|
||||
"
|
||||
user_durations AS (
|
||||
SELECT user_id, SUM(project_duration) as total_duration
|
||||
FROM project_durations
|
||||
GROUP BY user_id
|
||||
ORDER BY total_duration DESC
|
||||
LIMIT $3
|
||||
)
|
||||
",
|
||||
);
|
||||
}
|
||||
|
||||
let query = format!(
|
||||
"
|
||||
{with_clause}
|
||||
SELECT count(user_durations.user_id)
|
||||
FROM user_durations
|
||||
WHERE user_durations.total_duration >= $3
|
||||
"
|
||||
);
|
||||
|
||||
let count: i64 = sqlx::query_scalar(&query)
|
||||
.bind(time_period.start)
|
||||
.bind(time_period.end)
|
||||
.bind(min_duration.as_millis() as i64)
|
||||
.fetch_one(&self.pool)
|
||||
.await?;
|
||||
Ok(count as usize)
|
||||
}
|
||||
|
||||
async fn get_top_users_activity_summary(
|
||||
&self,
|
||||
time_period: Range<OffsetDateTime>,
|
||||
|
@ -612,16 +696,22 @@ impl Db for PostgresDb {
|
|||
GROUP BY user_id
|
||||
ORDER BY total_duration DESC
|
||||
LIMIT $3
|
||||
),
|
||||
project_collaborators as (
|
||||
SELECT project_id, COUNT(DISTINCT user_id) as max_collaborators
|
||||
FROM project_durations
|
||||
GROUP BY project_id
|
||||
)
|
||||
SELECT user_durations.user_id, users.github_login, project_id, project_duration
|
||||
FROM user_durations, project_durations, users
|
||||
SELECT user_durations.user_id, users.github_login, project_durations.project_id, project_duration, max_collaborators
|
||||
FROM user_durations, project_durations, project_collaborators, users
|
||||
WHERE
|
||||
user_durations.user_id = project_durations.user_id AND
|
||||
user_durations.user_id = users.id
|
||||
user_durations.user_id = users.id AND
|
||||
project_durations.project_id = project_collaborators.project_id
|
||||
ORDER BY total_duration DESC, user_id ASC
|
||||
";
|
||||
|
||||
let mut rows = sqlx::query_as::<_, (UserId, String, ProjectId, i64)>(query)
|
||||
let mut rows = sqlx::query_as::<_, (UserId, String, ProjectId, i64, i64)>(query)
|
||||
.bind(time_period.start)
|
||||
.bind(time_period.end)
|
||||
.bind(max_user_count as i32)
|
||||
|
@ -629,18 +719,23 @@ impl Db for PostgresDb {
|
|||
|
||||
let mut result = Vec::<UserActivitySummary>::new();
|
||||
while let Some(row) = rows.next().await {
|
||||
let (user_id, github_login, project_id, duration_millis) = row?;
|
||||
let (user_id, github_login, project_id, duration_millis, project_collaborators) = row?;
|
||||
let project_id = project_id;
|
||||
let duration = Duration::from_millis(duration_millis as u64);
|
||||
let project_activity = ProjectActivitySummary {
|
||||
id: project_id,
|
||||
duration,
|
||||
max_collaborators: project_collaborators as usize,
|
||||
};
|
||||
if let Some(last_summary) = result.last_mut() {
|
||||
if last_summary.id == user_id {
|
||||
last_summary.project_activity.push((project_id, duration));
|
||||
last_summary.project_activity.push(project_activity);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
result.push(UserActivitySummary {
|
||||
id: user_id,
|
||||
project_activity: vec![(project_id, duration)],
|
||||
project_activity: vec![project_activity],
|
||||
github_login,
|
||||
});
|
||||
}
|
||||
|
@ -1272,7 +1367,14 @@ pub struct Project {
|
|||
pub struct UserActivitySummary {
|
||||
pub id: UserId,
|
||||
pub github_login: String,
|
||||
pub project_activity: Vec<(ProjectId, Duration)>,
|
||||
pub project_activity: Vec<ProjectActivitySummary>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize)]
|
||||
pub struct ProjectActivitySummary {
|
||||
id: ProjectId,
|
||||
duration: Duration,
|
||||
max_collaborators: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize)]
|
||||
|
@ -1544,7 +1646,7 @@ pub mod tests {
|
|||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_project_activity() {
|
||||
async fn test_user_activity() {
|
||||
let test_db = TestDb::postgres().await;
|
||||
let db = test_db.db();
|
||||
|
||||
|
@ -1625,22 +1727,100 @@ pub mod tests {
|
|||
id: user_1,
|
||||
github_login: "user_1".to_string(),
|
||||
project_activity: vec![
|
||||
(project_1, Duration::from_secs(25)),
|
||||
(project_2, Duration::from_secs(30)),
|
||||
ProjectActivitySummary {
|
||||
id: project_1,
|
||||
duration: Duration::from_secs(25),
|
||||
max_collaborators: 2
|
||||
},
|
||||
ProjectActivitySummary {
|
||||
id: project_2,
|
||||
duration: Duration::from_secs(30),
|
||||
max_collaborators: 2
|
||||
}
|
||||
]
|
||||
},
|
||||
UserActivitySummary {
|
||||
id: user_2,
|
||||
github_login: "user_2".to_string(),
|
||||
project_activity: vec![(project_2, Duration::from_secs(50))]
|
||||
project_activity: vec![ProjectActivitySummary {
|
||||
id: project_2,
|
||||
duration: Duration::from_secs(50),
|
||||
max_collaborators: 2
|
||||
}]
|
||||
},
|
||||
UserActivitySummary {
|
||||
id: user_3,
|
||||
github_login: "user_3".to_string(),
|
||||
project_activity: vec![(project_1, Duration::from_secs(15))]
|
||||
project_activity: vec![ProjectActivitySummary {
|
||||
id: project_1,
|
||||
duration: Duration::from_secs(15),
|
||||
max_collaborators: 2
|
||||
}]
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
db.get_active_user_count(t0..t6, Duration::from_secs(56), false)
|
||||
.await
|
||||
.unwrap(),
|
||||
0
|
||||
);
|
||||
assert_eq!(
|
||||
db.get_active_user_count(t0..t6, Duration::from_secs(56), true)
|
||||
.await
|
||||
.unwrap(),
|
||||
0
|
||||
);
|
||||
assert_eq!(
|
||||
db.get_active_user_count(t0..t6, Duration::from_secs(54), false)
|
||||
.await
|
||||
.unwrap(),
|
||||
1
|
||||
);
|
||||
assert_eq!(
|
||||
db.get_active_user_count(t0..t6, Duration::from_secs(54), true)
|
||||
.await
|
||||
.unwrap(),
|
||||
1
|
||||
);
|
||||
assert_eq!(
|
||||
db.get_active_user_count(t0..t6, Duration::from_secs(30), false)
|
||||
.await
|
||||
.unwrap(),
|
||||
2
|
||||
);
|
||||
assert_eq!(
|
||||
db.get_active_user_count(t0..t6, Duration::from_secs(30), true)
|
||||
.await
|
||||
.unwrap(),
|
||||
2
|
||||
);
|
||||
assert_eq!(
|
||||
db.get_active_user_count(t0..t6, Duration::from_secs(10), false)
|
||||
.await
|
||||
.unwrap(),
|
||||
3
|
||||
);
|
||||
assert_eq!(
|
||||
db.get_active_user_count(t0..t6, Duration::from_secs(10), true)
|
||||
.await
|
||||
.unwrap(),
|
||||
3
|
||||
);
|
||||
assert_eq!(
|
||||
db.get_active_user_count(t0..t1, Duration::from_secs(5), false)
|
||||
.await
|
||||
.unwrap(),
|
||||
1
|
||||
);
|
||||
assert_eq!(
|
||||
db.get_active_user_count(t0..t1, Duration::from_secs(5), true)
|
||||
.await
|
||||
.unwrap(),
|
||||
0
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
db.get_user_activity_timeline(t3..t6, user_1).await.unwrap(),
|
||||
&[
|
||||
|
@ -2477,6 +2657,15 @@ pub mod tests {
|
|||
unimplemented!()
|
||||
}
|
||||
|
||||
async fn get_active_user_count(
|
||||
&self,
|
||||
_time_period: Range<OffsetDateTime>,
|
||||
_min_duration: Duration,
|
||||
_only_collaborative: bool,
|
||||
) -> Result<usize> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
async fn get_top_users_activity_summary(
|
||||
&self,
|
||||
_time_period: Range<OffsetDateTime>,
|
||||
|
|
|
@ -13,8 +13,9 @@ use gpui::{
|
|||
geometry::{rect::RectF, vector::vec2f},
|
||||
impl_actions, impl_internal_actions,
|
||||
platform::CursorStyle,
|
||||
AppContext, ClipboardItem, Element, ElementBox, Entity, ModelHandle, MutableAppContext,
|
||||
RenderContext, Subscription, View, ViewContext, ViewHandle, WeakModelHandle, WeakViewHandle,
|
||||
AppContext, ClipboardItem, Element, ElementBox, Entity, ModelHandle, MouseButton,
|
||||
MutableAppContext, RenderContext, Subscription, View, ViewContext, ViewHandle, WeakModelHandle,
|
||||
WeakViewHandle,
|
||||
};
|
||||
use join_project_notification::JoinProjectNotification;
|
||||
use menu::{Confirm, SelectNext, SelectPrev};
|
||||
|
@ -310,7 +311,9 @@ impl ContactsPanel {
|
|||
.boxed()
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(move |_, _, cx| cx.dispatch_action(ToggleExpanded(section)))
|
||||
.on_click(MouseButton::Left, move |_, cx| {
|
||||
cx.dispatch_action(ToggleExpanded(section))
|
||||
})
|
||||
.boxed()
|
||||
}
|
||||
|
||||
|
@ -445,7 +448,7 @@ impl ContactsPanel {
|
|||
Some(
|
||||
button
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(move |_, _, cx| {
|
||||
.on_click(MouseButton::Left, move |_, cx| {
|
||||
cx.dispatch_action(ToggleProjectOnline {
|
||||
project: Some(open_project.clone()),
|
||||
})
|
||||
|
@ -499,7 +502,7 @@ impl ContactsPanel {
|
|||
} else {
|
||||
CursorStyle::Arrow
|
||||
})
|
||||
.on_click(move |_, _, cx| {
|
||||
.on_click(MouseButton::Left, move |_, cx| {
|
||||
if !is_host {
|
||||
cx.dispatch_global_action(JoinProject {
|
||||
contact: contact.clone(),
|
||||
|
@ -563,7 +566,7 @@ impl ContactsPanel {
|
|||
} else {
|
||||
button
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(move |_, _, cx| {
|
||||
.on_click(MouseButton::Left, move |_, cx| {
|
||||
let project = project_handle.upgrade(cx.deref_mut());
|
||||
cx.dispatch_action(ToggleProjectOnline { project })
|
||||
})
|
||||
|
@ -646,7 +649,7 @@ impl ContactsPanel {
|
|||
.boxed()
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(move |_, _, cx| {
|
||||
.on_click(MouseButton::Left, move |_, cx| {
|
||||
cx.dispatch_action(RespondToContactRequest {
|
||||
user_id,
|
||||
accept: false,
|
||||
|
@ -668,7 +671,7 @@ impl ContactsPanel {
|
|||
.boxed()
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(move |_, _, cx| {
|
||||
.on_click(MouseButton::Left, move |_, cx| {
|
||||
cx.dispatch_action(RespondToContactRequest {
|
||||
user_id,
|
||||
accept: true,
|
||||
|
@ -691,7 +694,9 @@ impl ContactsPanel {
|
|||
})
|
||||
.with_padding(Padding::uniform(2.))
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(move |_, _, cx| cx.dispatch_action(RemoveContact(user_id)))
|
||||
.on_click(MouseButton::Left, move |_, cx| {
|
||||
cx.dispatch_action(RemoveContact(user_id))
|
||||
})
|
||||
.flex_float()
|
||||
.boxed(),
|
||||
);
|
||||
|
@ -1078,7 +1083,9 @@ impl View for ContactsPanel {
|
|||
.boxed()
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(|_, _, cx| cx.dispatch_action(contact_finder::Toggle))
|
||||
.on_click(MouseButton::Left, |_, cx| {
|
||||
cx.dispatch_action(contact_finder::Toggle)
|
||||
})
|
||||
.boxed(),
|
||||
)
|
||||
.constrained()
|
||||
|
@ -1126,7 +1133,7 @@ impl View for ContactsPanel {
|
|||
},
|
||||
)
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(move |_, _, cx| {
|
||||
.on_click(MouseButton::Left, move |_, cx| {
|
||||
cx.write_to_clipboard(ClipboardItem::new(
|
||||
info.url.to_string(),
|
||||
));
|
||||
|
|
|
@ -3,7 +3,7 @@ use client::User;
|
|||
use gpui::{
|
||||
elements::{Flex, Image, Label, MouseEventHandler, Padding, ParentElement, Text},
|
||||
platform::CursorStyle,
|
||||
Action, Element, ElementBox, RenderContext, View,
|
||||
Action, Element, ElementBox, MouseButton, RenderContext, View,
|
||||
};
|
||||
use settings::Settings;
|
||||
use std::sync::Arc;
|
||||
|
@ -61,7 +61,9 @@ pub fn render_user_notification<V: View, A: Action + Clone>(
|
|||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.with_padding(Padding::uniform(5.))
|
||||
.on_click(move |_, _, cx| cx.dispatch_any_action(dismiss_action.boxed_clone()))
|
||||
.on_click(MouseButton::Left, move |_, cx| {
|
||||
cx.dispatch_any_action(dismiss_action.boxed_clone())
|
||||
})
|
||||
.aligned()
|
||||
.constrained()
|
||||
.with_height(
|
||||
|
@ -96,7 +98,9 @@ pub fn render_user_notification<V: View, A: Action + Clone>(
|
|||
.boxed()
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(move |_, _, cx| cx.dispatch_any_action(action.boxed_clone()))
|
||||
.on_click(MouseButton::Left, move |_, cx| {
|
||||
cx.dispatch_any_action(action.boxed_clone())
|
||||
})
|
||||
.boxed()
|
||||
},
|
||||
))
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use gpui::{
|
||||
elements::*, geometry::vector::Vector2F, impl_internal_actions, keymap, platform::CursorStyle,
|
||||
Action, AppContext, Axis, Entity, MutableAppContext, RenderContext, SizeConstraint,
|
||||
Subscription, View, ViewContext,
|
||||
Action, AppContext, Axis, Entity, MouseButton, MutableAppContext, RenderContext,
|
||||
SizeConstraint, Subscription, View, ViewContext,
|
||||
};
|
||||
use menu::*;
|
||||
use settings::Settings;
|
||||
|
@ -124,6 +124,10 @@ impl ContextMenu {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn visible(&self) -> bool {
|
||||
self.visible
|
||||
}
|
||||
|
||||
fn action_dispatched(&mut self, action_id: TypeId, cx: &mut ViewContext<Self>) {
|
||||
if let Some(ix) = self
|
||||
.items
|
||||
|
@ -333,7 +337,7 @@ impl ContextMenu {
|
|||
.boxed()
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(move |_, _, cx| {
|
||||
.on_click(MouseButton::Left, move |_, cx| {
|
||||
cx.dispatch_action(Clicked);
|
||||
cx.dispatch_any_action(action.boxed_clone());
|
||||
})
|
||||
|
@ -351,7 +355,7 @@ impl ContextMenu {
|
|||
.with_style(style.container)
|
||||
.boxed()
|
||||
})
|
||||
.on_mouse_down_out(|_, cx| cx.dispatch_action(Cancel))
|
||||
.on_right_mouse_down_out(|_, cx| cx.dispatch_action(Cancel))
|
||||
.on_mouse_down_out(MouseButton::Left, |_, cx| cx.dispatch_action(Cancel))
|
||||
.on_mouse_down_out(MouseButton::Right, |_, cx| cx.dispatch_action(Cancel))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -501,7 +501,12 @@ impl ProjectDiagnosticsEditor {
|
|||
}
|
||||
|
||||
impl workspace::Item for ProjectDiagnosticsEditor {
|
||||
fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox {
|
||||
fn tab_content(
|
||||
&self,
|
||||
_detail: Option<usize>,
|
||||
style: &theme::Tab,
|
||||
cx: &AppContext,
|
||||
) -> ElementBox {
|
||||
render_summary(
|
||||
&self.summary,
|
||||
&style.label.text,
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
use collections::HashSet;
|
||||
use editor::{Editor, GoToNextDiagnostic};
|
||||
use gpui::{
|
||||
elements::*, platform::CursorStyle, serde_json, Entity, ModelHandle, MutableAppContext,
|
||||
RenderContext, Subscription, View, ViewContext, ViewHandle, WeakViewHandle,
|
||||
elements::*, platform::CursorStyle, serde_json, Entity, ModelHandle, MouseButton,
|
||||
MutableAppContext, RenderContext, Subscription, View, ViewContext, ViewHandle, WeakViewHandle,
|
||||
};
|
||||
use language::Diagnostic;
|
||||
use project::Project;
|
||||
|
@ -161,7 +161,7 @@ impl View for DiagnosticIndicator {
|
|||
.boxed()
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(|_, _, cx| cx.dispatch_action(crate::Deploy))
|
||||
.on_click(MouseButton::Left, |_, cx| cx.dispatch_action(crate::Deploy))
|
||||
.with_tooltip::<Summary, _>(
|
||||
0,
|
||||
"Project Diagnostics".to_string(),
|
||||
|
@ -201,7 +201,9 @@ impl View for DiagnosticIndicator {
|
|||
.boxed()
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(|_, _, cx| cx.dispatch_action(GoToNextDiagnostic))
|
||||
.on_click(MouseButton::Left, |_, cx| {
|
||||
cx.dispatch_action(GoToNextDiagnostic)
|
||||
})
|
||||
.boxed(),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@ test-support = [
|
|||
text = { path = "../text" }
|
||||
clock = { path = "../clock" }
|
||||
collections = { path = "../collections" }
|
||||
context_menu = { path = "../context_menu" }
|
||||
fuzzy = { path = "../fuzzy" }
|
||||
gpui = { path = "../gpui" }
|
||||
language = { path = "../language" }
|
||||
|
|
|
@ -4,6 +4,7 @@ mod highlight_matching_bracket;
|
|||
mod hover_popover;
|
||||
pub mod items;
|
||||
mod link_go_to_definition;
|
||||
mod mouse_context_menu;
|
||||
pub mod movement;
|
||||
mod multi_buffer;
|
||||
pub mod selections_collection;
|
||||
|
@ -29,11 +30,12 @@ use gpui::{
|
|||
impl_actions, impl_internal_actions,
|
||||
platform::CursorStyle,
|
||||
text_layout, AppContext, AsyncAppContext, ClipboardItem, Element, ElementBox, Entity,
|
||||
ModelHandle, MutableAppContext, RenderContext, Subscription, Task, View, ViewContext,
|
||||
ViewHandle, WeakViewHandle,
|
||||
ModelHandle, MouseButton, MutableAppContext, RenderContext, Subscription, Task, View,
|
||||
ViewContext, ViewHandle, WeakViewHandle,
|
||||
};
|
||||
use highlight_matching_bracket::refresh_matching_bracket_highlights;
|
||||
use hover_popover::{hide_hover, HoverState};
|
||||
pub use items::MAX_TAB_TITLE_LEN;
|
||||
pub use language::{char_kind, CharKind};
|
||||
use language::{
|
||||
BracketPair, Buffer, CodeAction, CodeLabel, Completion, Diagnostic, DiagnosticSeverity,
|
||||
|
@ -319,6 +321,7 @@ pub fn init(cx: &mut MutableAppContext) {
|
|||
|
||||
hover_popover::init(cx);
|
||||
link_go_to_definition::init(cx);
|
||||
mouse_context_menu::init(cx);
|
||||
|
||||
workspace::register_project_item::<Editor>(cx);
|
||||
workspace::register_followable_item::<Editor>(cx);
|
||||
|
@ -425,6 +428,7 @@ pub struct Editor {
|
|||
background_highlights: BTreeMap<TypeId, (fn(&Theme) -> Color, Vec<Range<Anchor>>)>,
|
||||
nav_history: Option<ItemNavHistory>,
|
||||
context_menu: Option<ContextMenu>,
|
||||
mouse_context_menu: ViewHandle<context_menu::ContextMenu>,
|
||||
completion_tasks: Vec<(CompletionId, Task<Option<()>>)>,
|
||||
next_completion_id: CompletionId,
|
||||
available_code_actions: Option<(ModelHandle<Buffer>, Arc<[CodeAction]>)>,
|
||||
|
@ -703,7 +707,7 @@ impl CompletionsMenu {
|
|||
},
|
||||
)
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_mouse_down(move |_, cx| {
|
||||
.on_mouse_down(MouseButton::Left, move |_, cx| {
|
||||
cx.dispatch_action(ConfirmCompletion {
|
||||
item_ix: Some(item_ix),
|
||||
});
|
||||
|
@ -836,7 +840,7 @@ impl CodeActionsMenu {
|
|||
.boxed()
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_mouse_down(move |_, cx| {
|
||||
.on_mouse_down(MouseButton::Left, move |_, cx| {
|
||||
cx.dispatch_action(ConfirmCodeAction {
|
||||
item_ix: Some(item_ix),
|
||||
});
|
||||
|
@ -1010,11 +1014,11 @@ impl Editor {
|
|||
background_highlights: Default::default(),
|
||||
nav_history: None,
|
||||
context_menu: None,
|
||||
mouse_context_menu: cx.add_view(|cx| context_menu::ContextMenu::new(cx)),
|
||||
completion_tasks: Default::default(),
|
||||
next_completion_id: 0,
|
||||
available_code_actions: Default::default(),
|
||||
code_actions_task: Default::default(),
|
||||
|
||||
document_highlights_task: Default::default(),
|
||||
pending_rename: Default::default(),
|
||||
searchable: true,
|
||||
|
@ -1070,7 +1074,7 @@ impl Editor {
|
|||
&self.buffer
|
||||
}
|
||||
|
||||
pub fn title(&self, cx: &AppContext) -> String {
|
||||
pub fn title<'a>(&self, cx: &'a AppContext) -> Cow<'a, str> {
|
||||
self.buffer().read(cx).title(cx)
|
||||
}
|
||||
|
||||
|
@ -1596,7 +1600,7 @@ impl Editor {
|
|||
s.delete(newest_selection.id)
|
||||
}
|
||||
|
||||
s.set_pending_range(start..end, mode);
|
||||
s.set_pending_anchor_range(start..end, mode);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1937,6 +1941,10 @@ impl Editor {
|
|||
}
|
||||
|
||||
fn trigger_completion_on_input(&mut self, text: &str, cx: &mut ViewContext<Self>) {
|
||||
if !cx.global::<Settings>().show_completions_on_input {
|
||||
return;
|
||||
}
|
||||
|
||||
let selection = self.selections.newest_anchor();
|
||||
if self
|
||||
.buffer
|
||||
|
@ -2666,7 +2674,7 @@ impl Editor {
|
|||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.with_padding(Padding::uniform(3.))
|
||||
.on_mouse_down(|_, cx| {
|
||||
.on_mouse_down(MouseButton::Left, |_, cx| {
|
||||
cx.dispatch_action(ToggleCodeActions {
|
||||
deployed_from_indicator: true,
|
||||
});
|
||||
|
@ -2901,9 +2909,17 @@ impl Editor {
|
|||
if selections.iter().all(|s| s.is_empty()) {
|
||||
self.transact(cx, |this, cx| {
|
||||
this.buffer.update(cx, |buffer, cx| {
|
||||
let mut prev_cursor_row = 0;
|
||||
let mut row_delta = 0;
|
||||
for selection in &mut selections {
|
||||
let language_name =
|
||||
buffer.language_at(selection.start, cx).map(|l| l.name());
|
||||
let mut cursor = selection.start;
|
||||
if cursor.row != prev_cursor_row {
|
||||
row_delta = 0;
|
||||
prev_cursor_row = cursor.row;
|
||||
}
|
||||
cursor.column += row_delta;
|
||||
|
||||
let language_name = buffer.language_at(cursor, cx).map(|l| l.name());
|
||||
let settings = cx.global::<Settings>();
|
||||
let tab_size = if settings.hard_tabs(language_name.as_deref()) {
|
||||
IndentSize::tab()
|
||||
|
@ -2911,21 +2927,18 @@ impl Editor {
|
|||
let tab_size = settings.tab_size(language_name.as_deref()).get();
|
||||
let char_column = buffer
|
||||
.read(cx)
|
||||
.text_for_range(Point::new(selection.start.row, 0)..selection.start)
|
||||
.text_for_range(Point::new(cursor.row, 0)..cursor)
|
||||
.flat_map(str::chars)
|
||||
.count();
|
||||
let chars_to_next_tab_stop = tab_size - (char_column as u32 % tab_size);
|
||||
IndentSize::spaces(chars_to_next_tab_stop)
|
||||
};
|
||||
buffer.edit(
|
||||
[(
|
||||
selection.start..selection.start,
|
||||
tab_size.chars().collect::<String>(),
|
||||
)],
|
||||
cx,
|
||||
);
|
||||
selection.start.column += tab_size.len;
|
||||
selection.end = selection.start;
|
||||
buffer.edit([(cursor..cursor, tab_size.chars().collect::<String>())], cx);
|
||||
cursor.column += tab_size.len;
|
||||
selection.start = cursor;
|
||||
selection.end = cursor;
|
||||
|
||||
row_delta += tab_size.len;
|
||||
}
|
||||
});
|
||||
this.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
|
@ -5780,7 +5793,12 @@ impl View for Editor {
|
|||
});
|
||||
}
|
||||
|
||||
EditorElement::new(self.handle.clone(), style.clone(), self.cursor_shape).boxed()
|
||||
Stack::new()
|
||||
.with_child(
|
||||
EditorElement::new(self.handle.clone(), style.clone(), self.cursor_shape).boxed(),
|
||||
)
|
||||
.with_child(ChildView::new(&self.mouse_context_menu).boxed())
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn ui_name() -> &'static str {
|
||||
|
@ -6225,7 +6243,8 @@ pub fn styled_runs_for_code_label<'a>(
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::test::{
|
||||
assert_text_with_selections, build_editor, select_ranges, EditorTestContext,
|
||||
assert_text_with_selections, build_editor, select_ranges, EditorLspTestContext,
|
||||
EditorTestContext,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
@ -6236,7 +6255,6 @@ mod tests {
|
|||
};
|
||||
use indoc::indoc;
|
||||
use language::{FakeLspAdapter, LanguageConfig};
|
||||
use lsp::FakeLanguageServer;
|
||||
use project::FakeFs;
|
||||
use settings::EditorSettings;
|
||||
use std::{cell::RefCell, rc::Rc, time::Instant};
|
||||
|
@ -6244,7 +6262,9 @@ mod tests {
|
|||
use unindent::Unindent;
|
||||
use util::{
|
||||
assert_set_eq,
|
||||
test::{marked_text_by, marked_text_ranges, marked_text_ranges_by, sample_text},
|
||||
test::{
|
||||
marked_text_by, marked_text_ranges, marked_text_ranges_by, sample_text, TextRangeMarker,
|
||||
},
|
||||
};
|
||||
use workspace::{FollowableItem, ItemHandle, NavigationEntry, Pane};
|
||||
|
||||
|
@ -7551,6 +7571,27 @@ mod tests {
|
|||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_tab(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
cx.update(|cx| {
|
||||
cx.update_global::<Settings, _, _>(|settings, _| {
|
||||
settings.editor_overrides.tab_size = Some(NonZeroU32::new(3).unwrap());
|
||||
});
|
||||
});
|
||||
cx.set_state(indoc! {"
|
||||
|ab|c
|
||||
|🏀|🏀|efg
|
||||
d|
|
||||
"});
|
||||
cx.update_editor(|e, cx| e.tab(&Tab, cx));
|
||||
cx.assert_editor_state(indoc! {"
|
||||
|ab |c
|
||||
|🏀 |🏀 |efg
|
||||
d |
|
||||
"});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_indent_outdent(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
|
@ -9524,199 +9565,182 @@ mod tests {
|
|||
|
||||
#[gpui::test]
|
||||
async fn test_completion(cx: &mut gpui::TestAppContext) {
|
||||
let mut language = Language::new(
|
||||
LanguageConfig {
|
||||
name: "Rust".into(),
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
let mut cx = EditorLspTestContext::new_rust(
|
||||
lsp::ServerCapabilities {
|
||||
completion_provider: Some(lsp::CompletionOptions {
|
||||
trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::language()),
|
||||
);
|
||||
let mut fake_servers = language
|
||||
.set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
|
||||
capabilities: lsp::ServerCapabilities {
|
||||
completion_provider: Some(lsp::CompletionOptions {
|
||||
trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
}))
|
||||
.await;
|
||||
|
||||
let text = "
|
||||
one
|
||||
two
|
||||
three
|
||||
"
|
||||
.unindent();
|
||||
|
||||
let fs = FakeFs::new(cx.background().clone());
|
||||
fs.insert_file("/file.rs", text).await;
|
||||
|
||||
let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
|
||||
project.update(cx, |project, _| project.languages().add(Arc::new(language)));
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| project.open_local_buffer("/file.rs", cx))
|
||||
.await
|
||||
.unwrap();
|
||||
let mut fake_server = fake_servers.next().await.unwrap();
|
||||
|
||||
let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
let (_, editor) = cx.add_window(|cx| build_editor(buffer, cx));
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.project = Some(project);
|
||||
editor.change_selections(None, cx, |s| {
|
||||
s.select_ranges([Point::new(0, 3)..Point::new(0, 3)])
|
||||
});
|
||||
editor.handle_input(&Input(".".to_string()), cx);
|
||||
});
|
||||
|
||||
handle_completion_request(
|
||||
&mut fake_server,
|
||||
"/file.rs",
|
||||
Point::new(0, 4),
|
||||
vec![
|
||||
(Point::new(0, 4)..Point::new(0, 4), "first_completion"),
|
||||
(Point::new(0, 4)..Point::new(0, 4), "second_completion"),
|
||||
],
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
editor
|
||||
.condition(&cx, |editor, _| editor.context_menu_visible())
|
||||
.await;
|
||||
|
||||
let apply_additional_edits = editor.update(cx, |editor, cx| {
|
||||
cx.set_state(indoc! {"
|
||||
one|
|
||||
two
|
||||
three"});
|
||||
cx.simulate_keystroke(".");
|
||||
handle_completion_request(
|
||||
&mut cx,
|
||||
indoc! {"
|
||||
one.|<>
|
||||
two
|
||||
three"},
|
||||
vec!["first_completion", "second_completion"],
|
||||
)
|
||||
.await;
|
||||
cx.condition(|editor, _| editor.context_menu_visible())
|
||||
.await;
|
||||
let apply_additional_edits = cx.update_editor(|editor, cx| {
|
||||
editor.move_down(&MoveDown, cx);
|
||||
let apply_additional_edits = editor
|
||||
editor
|
||||
.confirm_completion(&ConfirmCompletion::default(), cx)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
editor.text(cx),
|
||||
"
|
||||
one.second_completion
|
||||
two
|
||||
three
|
||||
"
|
||||
.unindent()
|
||||
);
|
||||
apply_additional_edits
|
||||
.unwrap()
|
||||
});
|
||||
cx.assert_editor_state(indoc! {"
|
||||
one.second_completion|
|
||||
two
|
||||
three"});
|
||||
|
||||
handle_resolve_completion_request(
|
||||
&mut fake_server,
|
||||
Some((Point::new(2, 5)..Point::new(2, 5), "\nadditional edit")),
|
||||
)
|
||||
.await;
|
||||
apply_additional_edits.await.unwrap();
|
||||
assert_eq!(
|
||||
editor.read_with(cx, |editor, cx| editor.text(cx)),
|
||||
"
|
||||
one.second_completion
|
||||
two
|
||||
three
|
||||
additional edit
|
||||
"
|
||||
.unindent()
|
||||
);
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.change_selections(None, cx, |s| {
|
||||
s.select_ranges([
|
||||
Point::new(1, 3)..Point::new(1, 3),
|
||||
Point::new(2, 5)..Point::new(2, 5),
|
||||
])
|
||||
});
|
||||
|
||||
editor.handle_input(&Input(" ".to_string()), cx);
|
||||
assert!(editor.context_menu.is_none());
|
||||
editor.handle_input(&Input("s".to_string()), cx);
|
||||
assert!(editor.context_menu.is_none());
|
||||
});
|
||||
|
||||
handle_completion_request(
|
||||
&mut fake_server,
|
||||
"/file.rs",
|
||||
Point::new(2, 7),
|
||||
vec![
|
||||
(Point::new(2, 6)..Point::new(2, 7), "fourth_completion"),
|
||||
(Point::new(2, 6)..Point::new(2, 7), "fifth_completion"),
|
||||
(Point::new(2, 6)..Point::new(2, 7), "sixth_completion"),
|
||||
],
|
||||
)
|
||||
.await;
|
||||
editor
|
||||
.condition(&cx, |editor, _| editor.context_menu_visible())
|
||||
.await;
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.handle_input(&Input("i".to_string()), cx);
|
||||
});
|
||||
|
||||
handle_completion_request(
|
||||
&mut fake_server,
|
||||
"/file.rs",
|
||||
Point::new(2, 8),
|
||||
vec![
|
||||
(Point::new(2, 6)..Point::new(2, 8), "fourth_completion"),
|
||||
(Point::new(2, 6)..Point::new(2, 8), "fifth_completion"),
|
||||
(Point::new(2, 6)..Point::new(2, 8), "sixth_completion"),
|
||||
],
|
||||
)
|
||||
.await;
|
||||
editor
|
||||
.condition(&cx, |editor, _| editor.context_menu_visible())
|
||||
.await;
|
||||
|
||||
let apply_additional_edits = editor.update(cx, |editor, cx| {
|
||||
let apply_additional_edits = editor
|
||||
.confirm_completion(&ConfirmCompletion::default(), cx)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
editor.text(cx),
|
||||
"
|
||||
&mut cx,
|
||||
Some((
|
||||
indoc! {"
|
||||
one.second_completion
|
||||
two sixth_completion
|
||||
three sixth_completion
|
||||
additional edit
|
||||
"
|
||||
.unindent()
|
||||
);
|
||||
apply_additional_edits
|
||||
two
|
||||
three<>"},
|
||||
"\nadditional edit",
|
||||
)),
|
||||
)
|
||||
.await;
|
||||
apply_additional_edits.await.unwrap();
|
||||
cx.assert_editor_state(indoc! {"
|
||||
one.second_completion|
|
||||
two
|
||||
three
|
||||
additional edit"});
|
||||
|
||||
cx.set_state(indoc! {"
|
||||
one.second_completion
|
||||
two|
|
||||
three|
|
||||
additional edit"});
|
||||
cx.simulate_keystroke(" ");
|
||||
assert!(cx.editor(|e, _| e.context_menu.is_none()));
|
||||
cx.simulate_keystroke("s");
|
||||
assert!(cx.editor(|e, _| e.context_menu.is_none()));
|
||||
|
||||
cx.assert_editor_state(indoc! {"
|
||||
one.second_completion
|
||||
two s|
|
||||
three s|
|
||||
additional edit"});
|
||||
handle_completion_request(
|
||||
&mut cx,
|
||||
indoc! {"
|
||||
one.second_completion
|
||||
two s
|
||||
three <s|>
|
||||
additional edit"},
|
||||
vec!["fourth_completion", "fifth_completion", "sixth_completion"],
|
||||
)
|
||||
.await;
|
||||
cx.condition(|editor, _| editor.context_menu_visible())
|
||||
.await;
|
||||
|
||||
cx.simulate_keystroke("i");
|
||||
|
||||
handle_completion_request(
|
||||
&mut cx,
|
||||
indoc! {"
|
||||
one.second_completion
|
||||
two si
|
||||
three <si|>
|
||||
additional edit"},
|
||||
vec!["fourth_completion", "fifth_completion", "sixth_completion"],
|
||||
)
|
||||
.await;
|
||||
cx.condition(|editor, _| editor.context_menu_visible())
|
||||
.await;
|
||||
|
||||
let apply_additional_edits = cx.update_editor(|editor, cx| {
|
||||
editor
|
||||
.confirm_completion(&ConfirmCompletion::default(), cx)
|
||||
.unwrap()
|
||||
});
|
||||
handle_resolve_completion_request(&mut fake_server, None).await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
one.second_completion
|
||||
two sixth_completion|
|
||||
three sixth_completion|
|
||||
additional edit"});
|
||||
|
||||
handle_resolve_completion_request(&mut cx, None).await;
|
||||
apply_additional_edits.await.unwrap();
|
||||
|
||||
async fn handle_completion_request(
|
||||
fake: &mut FakeLanguageServer,
|
||||
path: &'static str,
|
||||
position: Point,
|
||||
completions: Vec<(Range<Point>, &'static str)>,
|
||||
cx.update(|cx| {
|
||||
cx.update_global::<Settings, _, _>(|settings, _| {
|
||||
settings.show_completions_on_input = false;
|
||||
})
|
||||
});
|
||||
cx.set_state("editor|");
|
||||
cx.simulate_keystroke(".");
|
||||
assert!(cx.editor(|e, _| e.context_menu.is_none()));
|
||||
cx.simulate_keystrokes(["c", "l", "o"]);
|
||||
cx.assert_editor_state("editor.clo|");
|
||||
assert!(cx.editor(|e, _| e.context_menu.is_none()));
|
||||
cx.update_editor(|editor, cx| {
|
||||
editor.show_completions(&ShowCompletions, cx);
|
||||
});
|
||||
handle_completion_request(&mut cx, "editor.<clo|>", vec!["close", "clobber"]).await;
|
||||
cx.condition(|editor, _| editor.context_menu_visible())
|
||||
.await;
|
||||
let apply_additional_edits = cx.update_editor(|editor, cx| {
|
||||
editor
|
||||
.confirm_completion(&ConfirmCompletion::default(), cx)
|
||||
.unwrap()
|
||||
});
|
||||
cx.assert_editor_state("editor.close|");
|
||||
handle_resolve_completion_request(&mut cx, None).await;
|
||||
apply_additional_edits.await.unwrap();
|
||||
|
||||
// Handle completion request passing a marked string specifying where the completion
|
||||
// should be triggered from using '|' character, what range should be replaced, and what completions
|
||||
// should be returned using '<' and '>' to delimit the range
|
||||
async fn handle_completion_request<'a>(
|
||||
cx: &mut EditorLspTestContext<'a>,
|
||||
marked_string: &str,
|
||||
completions: Vec<&'static str>,
|
||||
) {
|
||||
fake.handle_request::<lsp::request::Completion, _, _>(move |params, _| {
|
||||
let complete_from_marker: TextRangeMarker = '|'.into();
|
||||
let replace_range_marker: TextRangeMarker = ('<', '>').into();
|
||||
let (_, mut marked_ranges) = marked_text_ranges_by(
|
||||
marked_string,
|
||||
vec![complete_from_marker.clone(), replace_range_marker.clone()],
|
||||
);
|
||||
|
||||
let complete_from_position =
|
||||
cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
|
||||
let replace_range =
|
||||
cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
|
||||
|
||||
cx.handle_request::<lsp::request::Completion, _, _>(move |url, params, _| {
|
||||
let completions = completions.clone();
|
||||
async move {
|
||||
assert_eq!(
|
||||
params.text_document_position.text_document.uri,
|
||||
lsp::Url::from_file_path(path).unwrap()
|
||||
);
|
||||
assert_eq!(params.text_document_position.text_document.uri, url.clone());
|
||||
assert_eq!(
|
||||
params.text_document_position.position,
|
||||
lsp::Position::new(position.row, position.column)
|
||||
complete_from_position
|
||||
);
|
||||
Ok(Some(lsp::CompletionResponse::Array(
|
||||
completions
|
||||
.iter()
|
||||
.map(|(range, new_text)| lsp::CompletionItem {
|
||||
label: new_text.to_string(),
|
||||
.map(|completion_text| lsp::CompletionItem {
|
||||
label: completion_text.to_string(),
|
||||
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
|
||||
range: lsp::Range::new(
|
||||
lsp::Position::new(range.start.row, range.start.column),
|
||||
lsp::Position::new(range.start.row, range.start.column),
|
||||
),
|
||||
new_text: new_text.to_string(),
|
||||
range: replace_range.clone(),
|
||||
new_text: completion_text.to_string(),
|
||||
})),
|
||||
..Default::default()
|
||||
})
|
||||
|
@ -9728,23 +9752,26 @@ mod tests {
|
|||
.await;
|
||||
}
|
||||
|
||||
async fn handle_resolve_completion_request(
|
||||
fake: &mut FakeLanguageServer,
|
||||
edit: Option<(Range<Point>, &'static str)>,
|
||||
async fn handle_resolve_completion_request<'a>(
|
||||
cx: &mut EditorLspTestContext<'a>,
|
||||
edit: Option<(&'static str, &'static str)>,
|
||||
) {
|
||||
fake.handle_request::<lsp::request::ResolveCompletionItem, _, _>(move |_, _| {
|
||||
let edit = edit.map(|(marked_string, new_text)| {
|
||||
let replace_range_marker: TextRangeMarker = ('<', '>').into();
|
||||
let (_, mut marked_ranges) =
|
||||
marked_text_ranges_by(marked_string, vec![replace_range_marker.clone()]);
|
||||
|
||||
let replace_range = cx
|
||||
.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
|
||||
|
||||
vec![lsp::TextEdit::new(replace_range, new_text.to_string())]
|
||||
});
|
||||
|
||||
cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
|
||||
let edit = edit.clone();
|
||||
async move {
|
||||
Ok(lsp::CompletionItem {
|
||||
additional_text_edits: edit.map(|(range, new_text)| {
|
||||
vec![lsp::TextEdit::new(
|
||||
lsp::Range::new(
|
||||
lsp::Position::new(range.start.row, range.start.column),
|
||||
lsp::Position::new(range.end.row, range.end.column),
|
||||
),
|
||||
new_text.to_string(),
|
||||
)]
|
||||
}),
|
||||
additional_text_edits: edit,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ use crate::{
|
|||
display_map::{BlockStyle, DisplaySnapshot, TransformBlock},
|
||||
hover_popover::HoverAt,
|
||||
link_go_to_definition::{CmdChanged, GoToFetchedDefinition, UpdateGoToDefinitionLink},
|
||||
mouse_context_menu::DeployMouseContextMenu,
|
||||
EditorStyle,
|
||||
};
|
||||
use clock::ReplicaId;
|
||||
|
@ -24,7 +25,7 @@ use gpui::{
|
|||
platform::CursorStyle,
|
||||
text_layout::{self, Line, RunStyle, TextLayoutCache},
|
||||
AppContext, Axis, Border, CursorRegion, Element, ElementBox, Event, EventContext, KeyDownEvent,
|
||||
LayoutContext, ModifiersChangedEvent, MouseButton, MouseEvent, MouseMovedEvent,
|
||||
LayoutContext, ModifiersChangedEvent, MouseButton, MouseButtonEvent, MouseMovedEvent,
|
||||
MutableAppContext, PaintContext, Quad, Scene, ScrollWheelEvent, SizeConstraint, ViewContext,
|
||||
WeakViewHandle,
|
||||
};
|
||||
|
@ -152,6 +153,24 @@ impl EditorElement {
|
|||
true
|
||||
}
|
||||
|
||||
fn mouse_right_down(
|
||||
&self,
|
||||
position: Vector2F,
|
||||
layout: &mut LayoutState,
|
||||
paint: &mut PaintState,
|
||||
cx: &mut EventContext,
|
||||
) -> bool {
|
||||
if !paint.text_bounds.contains_point(position) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let snapshot = self.snapshot(cx.app);
|
||||
let (point, _) = paint.point_for_position(&snapshot, layout, position);
|
||||
|
||||
cx.dispatch_action(DeployMouseContextMenu { position, point });
|
||||
true
|
||||
}
|
||||
|
||||
fn mouse_up(&self, _position: Vector2F, cx: &mut EventContext) -> bool {
|
||||
if self.view(cx.app.as_ref()).is_selecting() {
|
||||
cx.dispatch_action(Select(SelectPhase::End));
|
||||
|
@ -949,7 +968,9 @@ impl EditorElement {
|
|||
.boxed()
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(move |_, _, cx| cx.dispatch_action(jump_action.clone()))
|
||||
.on_click(MouseButton::Left, move |_, cx| {
|
||||
cx.dispatch_action(jump_action.clone())
|
||||
})
|
||||
.with_tooltip::<JumpIcon, _>(
|
||||
*key,
|
||||
"Jump to Buffer".to_string(),
|
||||
|
@ -1464,7 +1485,7 @@ impl Element for EditorElement {
|
|||
}
|
||||
|
||||
match event {
|
||||
Event::MouseDown(MouseEvent {
|
||||
Event::MouseDown(MouseButtonEvent {
|
||||
button: MouseButton::Left,
|
||||
position,
|
||||
cmd,
|
||||
|
@ -1482,7 +1503,12 @@ impl Element for EditorElement {
|
|||
paint,
|
||||
cx,
|
||||
),
|
||||
Event::MouseUp(MouseEvent {
|
||||
Event::MouseDown(MouseButtonEvent {
|
||||
button: MouseButton::Right,
|
||||
position,
|
||||
..
|
||||
}) => self.mouse_right_down(*position, layout, paint, cx),
|
||||
Event::MouseUp(MouseButtonEvent {
|
||||
button: MouseButton::Left,
|
||||
position,
|
||||
..
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
use crate::{Anchor, Autoscroll, Editor, Event, ExcerptId, NavigationData, ToPoint as _};
|
||||
use crate::{
|
||||
Anchor, Autoscroll, Editor, Event, ExcerptId, MultiBuffer, NavigationData, ToPoint as _,
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
use futures::FutureExt;
|
||||
use gpui::{
|
||||
|
@ -10,12 +12,18 @@ use project::{File, Project, ProjectEntryId, ProjectPath};
|
|||
use rpc::proto::{self, update_view};
|
||||
use settings::Settings;
|
||||
use smallvec::SmallVec;
|
||||
use std::{fmt::Write, path::PathBuf, time::Duration};
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
fmt::Write,
|
||||
path::{Path, PathBuf},
|
||||
time::Duration,
|
||||
};
|
||||
use text::{Point, Selection};
|
||||
use util::TryFutureExt;
|
||||
use workspace::{FollowableItem, Item, ItemHandle, ItemNavHistory, ProjectItem, StatusItemView};
|
||||
|
||||
pub const FORMAT_TIMEOUT: Duration = Duration::from_secs(2);
|
||||
pub const MAX_TAB_TITLE_LEN: usize = 24;
|
||||
|
||||
impl FollowableItem for Editor {
|
||||
fn from_state_proto(
|
||||
|
@ -292,9 +300,44 @@ impl Item for Editor {
|
|||
}
|
||||
}
|
||||
|
||||
fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox {
|
||||
let title = self.title(cx);
|
||||
Label::new(title, style.label.clone()).boxed()
|
||||
fn tab_description<'a>(&'a self, detail: usize, cx: &'a AppContext) -> Option<Cow<'a, str>> {
|
||||
match path_for_buffer(&self.buffer, detail, true, cx)? {
|
||||
Cow::Borrowed(path) => Some(path.to_string_lossy()),
|
||||
Cow::Owned(path) => Some(path.to_string_lossy().to_string().into()),
|
||||
}
|
||||
}
|
||||
|
||||
fn tab_content(
|
||||
&self,
|
||||
detail: Option<usize>,
|
||||
style: &theme::Tab,
|
||||
cx: &AppContext,
|
||||
) -> ElementBox {
|
||||
Flex::row()
|
||||
.with_child(
|
||||
Label::new(self.title(cx).into(), style.label.clone())
|
||||
.aligned()
|
||||
.boxed(),
|
||||
)
|
||||
.with_children(detail.and_then(|detail| {
|
||||
let path = path_for_buffer(&self.buffer, detail, false, cx)?;
|
||||
let description = path.to_string_lossy();
|
||||
Some(
|
||||
Label::new(
|
||||
if description.len() > MAX_TAB_TITLE_LEN {
|
||||
description[..MAX_TAB_TITLE_LEN].to_string() + "…"
|
||||
} else {
|
||||
description.into()
|
||||
},
|
||||
style.description.text.clone(),
|
||||
)
|
||||
.contained()
|
||||
.with_style(style.description.container)
|
||||
.aligned()
|
||||
.boxed(),
|
||||
)
|
||||
}))
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn project_path(&self, cx: &AppContext) -> Option<ProjectPath> {
|
||||
|
@ -534,3 +577,42 @@ impl StatusItemView for CursorPosition {
|
|||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
fn path_for_buffer<'a>(
|
||||
buffer: &ModelHandle<MultiBuffer>,
|
||||
mut height: usize,
|
||||
include_filename: bool,
|
||||
cx: &'a AppContext,
|
||||
) -> Option<Cow<'a, Path>> {
|
||||
let file = buffer.read(cx).as_singleton()?.read(cx).file()?;
|
||||
// Ensure we always render at least the filename.
|
||||
height += 1;
|
||||
|
||||
let mut prefix = file.path().as_ref();
|
||||
while height > 0 {
|
||||
if let Some(parent) = prefix.parent() {
|
||||
prefix = parent;
|
||||
height -= 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Here we could have just always used `full_path`, but that is very
|
||||
// allocation-heavy and so we try to use a `Cow<Path>` if we haven't
|
||||
// traversed all the way up to the worktree's root.
|
||||
if height > 0 {
|
||||
let full_path = file.full_path(cx);
|
||||
if include_filename {
|
||||
Some(full_path.into())
|
||||
} else {
|
||||
Some(full_path.parent().unwrap().to_path_buf().into())
|
||||
}
|
||||
} else {
|
||||
let mut path = file.path().strip_prefix(prefix).unwrap();
|
||||
if !include_filename {
|
||||
path = path.parent().unwrap();
|
||||
}
|
||||
Some(path.into())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -342,17 +342,16 @@ mod tests {
|
|||
test();"});
|
||||
|
||||
let mut requests =
|
||||
cx.lsp
|
||||
.handle_request::<lsp::request::GotoDefinition, _, _>(move |_, _| async move {
|
||||
Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
|
||||
lsp::LocationLink {
|
||||
origin_selection_range: Some(symbol_range),
|
||||
target_uri: lsp::Url::from_file_path("/root/dir/file.rs").unwrap(),
|
||||
target_range,
|
||||
target_selection_range: target_range,
|
||||
},
|
||||
])))
|
||||
});
|
||||
cx.handle_request::<lsp::request::GotoDefinition, _, _>(move |url, _, _| async move {
|
||||
Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
|
||||
lsp::LocationLink {
|
||||
origin_selection_range: Some(symbol_range),
|
||||
target_uri: url.clone(),
|
||||
target_range,
|
||||
target_selection_range: target_range,
|
||||
},
|
||||
])))
|
||||
});
|
||||
cx.update_editor(|editor, cx| {
|
||||
update_go_to_definition_link(
|
||||
editor,
|
||||
|
@ -387,18 +386,17 @@ mod tests {
|
|||
// Response without source range still highlights word
|
||||
cx.update_editor(|editor, _| editor.link_go_to_definition_state.last_mouse_location = None);
|
||||
let mut requests =
|
||||
cx.lsp
|
||||
.handle_request::<lsp::request::GotoDefinition, _, _>(move |_, _| async move {
|
||||
Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
|
||||
lsp::LocationLink {
|
||||
// No origin range
|
||||
origin_selection_range: None,
|
||||
target_uri: lsp::Url::from_file_path("/root/dir/file.rs").unwrap(),
|
||||
target_range,
|
||||
target_selection_range: target_range,
|
||||
},
|
||||
])))
|
||||
});
|
||||
cx.handle_request::<lsp::request::GotoDefinition, _, _>(move |url, _, _| async move {
|
||||
Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
|
||||
lsp::LocationLink {
|
||||
// No origin range
|
||||
origin_selection_range: None,
|
||||
target_uri: url.clone(),
|
||||
target_range,
|
||||
target_selection_range: target_range,
|
||||
},
|
||||
])))
|
||||
});
|
||||
cx.update_editor(|editor, cx| {
|
||||
update_go_to_definition_link(
|
||||
editor,
|
||||
|
@ -495,17 +493,16 @@ mod tests {
|
|||
test();"});
|
||||
|
||||
let mut requests =
|
||||
cx.lsp
|
||||
.handle_request::<lsp::request::GotoDefinition, _, _>(move |_, _| async move {
|
||||
Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
|
||||
lsp::LocationLink {
|
||||
origin_selection_range: Some(symbol_range),
|
||||
target_uri: lsp::Url::from_file_path("/root/dir/file.rs").unwrap(),
|
||||
target_range,
|
||||
target_selection_range: target_range,
|
||||
},
|
||||
])))
|
||||
});
|
||||
cx.handle_request::<lsp::request::GotoDefinition, _, _>(move |url, _, _| async move {
|
||||
Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
|
||||
lsp::LocationLink {
|
||||
origin_selection_range: Some(symbol_range),
|
||||
target_uri: url,
|
||||
target_range,
|
||||
target_selection_range: target_range,
|
||||
},
|
||||
])))
|
||||
});
|
||||
cx.update_editor(|editor, cx| {
|
||||
cmd_changed(editor, &CmdChanged { cmd_down: true }, cx);
|
||||
});
|
||||
|
@ -584,17 +581,16 @@ mod tests {
|
|||
test();"});
|
||||
|
||||
let mut requests =
|
||||
cx.lsp
|
||||
.handle_request::<lsp::request::GotoDefinition, _, _>(move |_, _| async move {
|
||||
Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
|
||||
lsp::LocationLink {
|
||||
origin_selection_range: None,
|
||||
target_uri: lsp::Url::from_file_path("/root/dir/file.rs").unwrap(),
|
||||
target_range,
|
||||
target_selection_range: target_range,
|
||||
},
|
||||
])))
|
||||
});
|
||||
cx.handle_request::<lsp::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,
|
||||
},
|
||||
])))
|
||||
});
|
||||
cx.update_workspace(|workspace, cx| {
|
||||
go_to_fetched_definition(workspace, &GoToFetchedDefinition { point: hover_point }, cx);
|
||||
});
|
||||
|
|
103
crates/editor/src/mouse_context_menu.rs
Normal file
103
crates/editor/src/mouse_context_menu.rs
Normal file
|
@ -0,0 +1,103 @@
|
|||
use context_menu::ContextMenuItem;
|
||||
use gpui::{geometry::vector::Vector2F, impl_internal_actions, MutableAppContext, ViewContext};
|
||||
|
||||
use crate::{
|
||||
DisplayPoint, Editor, EditorMode, FindAllReferences, GoToDefinition, Rename, SelectMode,
|
||||
ToggleCodeActions,
|
||||
};
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct DeployMouseContextMenu {
|
||||
pub position: Vector2F,
|
||||
pub point: DisplayPoint,
|
||||
}
|
||||
|
||||
impl_internal_actions!(editor, [DeployMouseContextMenu]);
|
||||
|
||||
pub fn init(cx: &mut MutableAppContext) {
|
||||
cx.add_action(deploy_context_menu);
|
||||
}
|
||||
|
||||
pub fn deploy_context_menu(
|
||||
editor: &mut Editor,
|
||||
&DeployMouseContextMenu { position, point }: &DeployMouseContextMenu,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) {
|
||||
// Don't show context menu for inline editors
|
||||
if editor.mode() != EditorMode::Full {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't show the context menu if there isn't a project associated with this editor
|
||||
if editor.project.is_none() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Move the cursor to the clicked location so that dispatched actions make sense
|
||||
editor.change_selections(None, cx, |s| {
|
||||
s.clear_disjoint();
|
||||
s.set_pending_display_range(point..point, SelectMode::Character);
|
||||
});
|
||||
|
||||
editor.mouse_context_menu.update(cx, |menu, cx| {
|
||||
menu.show(
|
||||
position,
|
||||
vec![
|
||||
ContextMenuItem::item("Rename Symbol", Rename),
|
||||
ContextMenuItem::item("Go To Definition", GoToDefinition),
|
||||
ContextMenuItem::item("Find All References", FindAllReferences),
|
||||
ContextMenuItem::item(
|
||||
"Code Actions",
|
||||
ToggleCodeActions {
|
||||
deployed_from_indicator: false,
|
||||
},
|
||||
),
|
||||
],
|
||||
cx,
|
||||
);
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use indoc::indoc;
|
||||
|
||||
use crate::test::EditorLspTestContext;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_mouse_context_menu(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = EditorLspTestContext::new_rust(
|
||||
lsp::ServerCapabilities {
|
||||
hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
|
||||
..Default::default()
|
||||
},
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
|
||||
cx.set_state(indoc! {"
|
||||
fn te|st()
|
||||
do_work();"});
|
||||
let point = cx.display_point(indoc! {"
|
||||
fn test()
|
||||
do_w|ork();"});
|
||||
cx.update_editor(|editor, cx| {
|
||||
deploy_context_menu(
|
||||
editor,
|
||||
&DeployMouseContextMenu {
|
||||
position: Default::default(),
|
||||
point,
|
||||
},
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
cx.assert_editor_state(indoc! {"
|
||||
fn test()
|
||||
do_w|ork();"});
|
||||
cx.editor(|editor, app| assert!(editor.mouse_context_menu.read(app).visible()));
|
||||
}
|
||||
}
|
|
@ -14,6 +14,7 @@ use language::{
|
|||
use settings::Settings;
|
||||
use smallvec::SmallVec;
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
cell::{Ref, RefCell},
|
||||
cmp, fmt, io,
|
||||
iter::{self, FromIterator},
|
||||
|
@ -1194,14 +1195,14 @@ impl MultiBuffer {
|
|||
.collect()
|
||||
}
|
||||
|
||||
pub fn title(&self, cx: &AppContext) -> String {
|
||||
if let Some(title) = self.title.clone() {
|
||||
return title;
|
||||
pub fn title<'a>(&'a self, cx: &'a AppContext) -> Cow<'a, str> {
|
||||
if let Some(title) = self.title.as_ref() {
|
||||
return title.into();
|
||||
}
|
||||
|
||||
if let Some(buffer) = self.as_singleton() {
|
||||
if let Some(file) = buffer.read(cx).file() {
|
||||
return file.file_name(cx).to_string_lossy().into();
|
||||
return file.file_name(cx).to_string_lossy();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -384,7 +384,7 @@ impl<'a> MutableSelectionsCollection<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn set_pending_range(&mut self, range: Range<Anchor>, mode: SelectMode) {
|
||||
pub fn set_pending_anchor_range(&mut self, range: Range<Anchor>, mode: SelectMode) {
|
||||
self.collection.pending = Some(PendingSelection {
|
||||
selection: Selection {
|
||||
id: post_inc(&mut self.collection.next_selection_id),
|
||||
|
@ -398,6 +398,42 @@ impl<'a> MutableSelectionsCollection<'a> {
|
|||
self.selections_changed = true;
|
||||
}
|
||||
|
||||
pub fn set_pending_display_range(&mut self, range: Range<DisplayPoint>, mode: SelectMode) {
|
||||
let (start, end, reversed) = {
|
||||
let display_map = self.display_map();
|
||||
let buffer = self.buffer();
|
||||
let mut start = range.start;
|
||||
let mut end = range.end;
|
||||
let reversed = if start > end {
|
||||
mem::swap(&mut start, &mut end);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
let end_bias = if end > start { Bias::Left } else { Bias::Right };
|
||||
(
|
||||
buffer.anchor_before(start.to_point(&display_map)),
|
||||
buffer.anchor_at(end.to_point(&display_map), end_bias),
|
||||
reversed,
|
||||
)
|
||||
};
|
||||
|
||||
let new_pending = PendingSelection {
|
||||
selection: Selection {
|
||||
id: post_inc(&mut self.collection.next_selection_id),
|
||||
start,
|
||||
end,
|
||||
reversed,
|
||||
goal: SelectionGoal::None,
|
||||
},
|
||||
mode,
|
||||
};
|
||||
|
||||
self.collection.pending = Some(new_pending);
|
||||
self.selections_changed = true;
|
||||
}
|
||||
|
||||
pub fn set_pending(&mut self, selection: Selection<Anchor>, mode: SelectMode) {
|
||||
self.collection.pending = Some(PendingSelection { selection, mode });
|
||||
self.selections_changed = true;
|
||||
|
|
|
@ -4,12 +4,14 @@ use std::{
|
|||
sync::Arc,
|
||||
};
|
||||
|
||||
use futures::StreamExt;
|
||||
use anyhow::Result;
|
||||
use futures::{Future, StreamExt};
|
||||
use indoc::indoc;
|
||||
|
||||
use collections::BTreeMap;
|
||||
use gpui::{json, keymap::Keystroke, AppContext, ModelHandle, ViewContext, ViewHandle};
|
||||
use language::{point_to_lsp, FakeLspAdapter, Language, LanguageConfig, Selection};
|
||||
use lsp::request;
|
||||
use project::Project;
|
||||
use settings::Settings;
|
||||
use util::{
|
||||
|
@ -110,6 +112,13 @@ impl<'a> EditorTestContext<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn condition(
|
||||
&self,
|
||||
predicate: impl FnMut(&Editor, &AppContext) -> bool,
|
||||
) -> impl Future<Output = ()> {
|
||||
self.editor.condition(self.cx, predicate)
|
||||
}
|
||||
|
||||
pub fn editor<F, T>(&mut self, read: F) -> T
|
||||
where
|
||||
F: FnOnce(&Editor, &AppContext) -> T,
|
||||
|
@ -424,6 +433,7 @@ pub struct EditorLspTestContext<'a> {
|
|||
pub cx: EditorTestContext<'a>,
|
||||
pub lsp: lsp::FakeLanguageServer,
|
||||
pub workspace: ViewHandle<Workspace>,
|
||||
pub editor_lsp_url: lsp::Url,
|
||||
}
|
||||
|
||||
impl<'a> EditorLspTestContext<'a> {
|
||||
|
@ -497,6 +507,7 @@ impl<'a> EditorLspTestContext<'a> {
|
|||
},
|
||||
lsp,
|
||||
workspace,
|
||||
editor_lsp_url: lsp::Url::from_file_path("/root/dir/file.rs").unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -520,11 +531,15 @@ impl<'a> EditorLspTestContext<'a> {
|
|||
pub fn lsp_range(&mut self, marked_text: &str) -> lsp::Range {
|
||||
let (unmarked, mut ranges) = marked_text_ranges_by(marked_text, vec![('[', ']').into()]);
|
||||
assert_eq!(unmarked, self.cx.buffer_text());
|
||||
let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx));
|
||||
|
||||
let offset_range = ranges.remove(&('[', ']').into()).unwrap()[0].clone();
|
||||
let start_point = offset_range.start.to_point(&snapshot.buffer_snapshot);
|
||||
let end_point = offset_range.end.to_point(&snapshot.buffer_snapshot);
|
||||
self.to_lsp_range(offset_range)
|
||||
}
|
||||
|
||||
pub fn to_lsp_range(&mut self, range: Range<usize>) -> lsp::Range {
|
||||
let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx));
|
||||
let start_point = range.start.to_point(&snapshot.buffer_snapshot);
|
||||
let end_point = range.end.to_point(&snapshot.buffer_snapshot);
|
||||
|
||||
self.editor(|editor, cx| {
|
||||
let buffer = editor.buffer().read(cx);
|
||||
let start = point_to_lsp(
|
||||
|
@ -546,12 +561,45 @@ impl<'a> EditorLspTestContext<'a> {
|
|||
})
|
||||
}
|
||||
|
||||
pub fn to_lsp(&mut self, offset: usize) -> lsp::Position {
|
||||
let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx));
|
||||
let point = offset.to_point(&snapshot.buffer_snapshot);
|
||||
|
||||
self.editor(|editor, cx| {
|
||||
let buffer = editor.buffer().read(cx);
|
||||
point_to_lsp(
|
||||
buffer
|
||||
.point_to_buffer_offset(point, cx)
|
||||
.unwrap()
|
||||
.1
|
||||
.to_point_utf16(&buffer.read(cx)),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn update_workspace<F, T>(&mut self, update: F) -> T
|
||||
where
|
||||
F: FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> T,
|
||||
{
|
||||
self.workspace.update(self.cx.cx, update)
|
||||
}
|
||||
|
||||
pub fn handle_request<T, F, Fut>(
|
||||
&self,
|
||||
mut handler: F,
|
||||
) -> futures::channel::mpsc::UnboundedReceiver<()>
|
||||
where
|
||||
T: 'static + request::Request,
|
||||
T::Params: 'static + Send,
|
||||
F: 'static + Send + FnMut(lsp::Url, T::Params, gpui::AsyncAppContext) -> Fut,
|
||||
Fut: 'static + Send + Future<Output = Result<T::Result>>,
|
||||
{
|
||||
let url = self.editor_lsp_url.clone();
|
||||
self.lsp.handle_request::<T, _, _>(move |params, cx| {
|
||||
let url = url.clone();
|
||||
handler(url, params, cx)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Deref for EditorLspTestContext<'a> {
|
||||
|
|
|
@ -5401,7 +5401,7 @@ impl RefCounts {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{actions, elements::*, impl_actions, MouseButton, MouseEvent};
|
||||
use crate::{actions, elements::*, impl_actions, MouseButton, MouseButtonEvent};
|
||||
use serde::Deserialize;
|
||||
use smol::future::poll_once;
|
||||
use std::{
|
||||
|
@ -5754,7 +5754,7 @@ mod tests {
|
|||
let presenter = cx.presenters_and_platform_windows[&window_id].0.clone();
|
||||
// Ensure window's root element is in a valid lifecycle state.
|
||||
presenter.borrow_mut().dispatch_event(
|
||||
Event::MouseDown(MouseEvent {
|
||||
Event::MouseDown(MouseButtonEvent {
|
||||
position: Default::default(),
|
||||
button: MouseButton::Left,
|
||||
ctrl: false,
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
use crate::{
|
||||
geometry::vector::Vector2F, CursorRegion, DebugContext, Element, ElementBox, Event,
|
||||
EventContext, LayoutContext, MouseButton, MouseEvent, MouseRegion, NavigationDirection,
|
||||
EventContext, LayoutContext, MouseButton, MouseButtonEvent, MouseRegion, NavigationDirection,
|
||||
PaintContext, SizeConstraint,
|
||||
};
|
||||
use pathfinder_geometry::rect::RectF;
|
||||
use serde_json::json;
|
||||
use std::{any::TypeId, rc::Rc};
|
||||
use std::any::TypeId;
|
||||
|
||||
pub struct EventHandler {
|
||||
child: ElementBox,
|
||||
|
@ -82,19 +82,11 @@ impl Element for EventHandler {
|
|||
bounds: visible_bounds,
|
||||
style: Default::default(),
|
||||
});
|
||||
cx.scene.push_mouse_region(MouseRegion {
|
||||
view_id: cx.current_view_id(),
|
||||
discriminant: Some(discriminant),
|
||||
bounds: visible_bounds,
|
||||
hover: Some(Rc::new(|_, _, _| {})),
|
||||
mouse_down: Some(Rc::new(|_, _| {})),
|
||||
click: Some(Rc::new(|_, _, _| {})),
|
||||
right_mouse_down: Some(Rc::new(|_, _| {})),
|
||||
right_click: Some(Rc::new(|_, _, _| {})),
|
||||
drag: Some(Rc::new(|_, _, _| {})),
|
||||
mouse_down_out: Some(Rc::new(|_, _| {})),
|
||||
right_mouse_down_out: Some(Rc::new(|_, _| {})),
|
||||
});
|
||||
cx.scene.push_mouse_region(MouseRegion::handle_all(
|
||||
cx.current_view_id(),
|
||||
Some(discriminant),
|
||||
visible_bounds,
|
||||
));
|
||||
cx.scene.pop_stacking_context();
|
||||
}
|
||||
self.child.paint(bounds.origin(), visible_bounds, cx);
|
||||
|
@ -117,7 +109,7 @@ impl Element for EventHandler {
|
|||
true
|
||||
} else {
|
||||
match event {
|
||||
Event::MouseDown(MouseEvent {
|
||||
Event::MouseDown(MouseButtonEvent {
|
||||
button: MouseButton::Left,
|
||||
position,
|
||||
..
|
||||
|
@ -129,7 +121,7 @@ impl Element for EventHandler {
|
|||
}
|
||||
false
|
||||
}
|
||||
Event::MouseDown(MouseEvent {
|
||||
Event::MouseDown(MouseButtonEvent {
|
||||
button: MouseButton::Right,
|
||||
position,
|
||||
..
|
||||
|
@ -141,7 +133,7 @@ impl Element for EventHandler {
|
|||
}
|
||||
false
|
||||
}
|
||||
Event::MouseDown(MouseEvent {
|
||||
Event::MouseDown(MouseButtonEvent {
|
||||
button: MouseButton::Navigate(direction),
|
||||
position,
|
||||
..
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use std::{any::TypeId, rc::Rc};
|
||||
use std::any::TypeId;
|
||||
|
||||
use super::Padding;
|
||||
use crate::{
|
||||
|
@ -7,25 +7,18 @@ use crate::{
|
|||
vector::{vec2f, Vector2F},
|
||||
},
|
||||
platform::CursorStyle,
|
||||
scene::CursorRegion,
|
||||
DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, MouseRegion, MouseState,
|
||||
PaintContext, RenderContext, SizeConstraint, View,
|
||||
scene::{CursorRegion, HandlerSet},
|
||||
DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, MouseButton,
|
||||
MouseButtonEvent, MouseMovedEvent, MouseRegion, MouseState, PaintContext, RenderContext,
|
||||
SizeConstraint, View,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
pub struct MouseEventHandler {
|
||||
child: ElementBox,
|
||||
tag: TypeId,
|
||||
id: usize,
|
||||
discriminant: (TypeId, usize),
|
||||
cursor_style: Option<CursorStyle>,
|
||||
mouse_down: Option<Rc<dyn Fn(Vector2F, &mut EventContext)>>,
|
||||
click: Option<Rc<dyn Fn(Vector2F, usize, &mut EventContext)>>,
|
||||
right_mouse_down: Option<Rc<dyn Fn(Vector2F, &mut EventContext)>>,
|
||||
right_click: Option<Rc<dyn Fn(Vector2F, usize, &mut EventContext)>>,
|
||||
mouse_down_out: Option<Rc<dyn Fn(Vector2F, &mut EventContext)>>,
|
||||
right_mouse_down_out: Option<Rc<dyn Fn(Vector2F, &mut EventContext)>>,
|
||||
drag: Option<Rc<dyn Fn(Vector2F, Vector2F, &mut EventContext)>>,
|
||||
hover: Option<Rc<dyn Fn(Vector2F, bool, &mut EventContext)>>,
|
||||
handlers: HandlerSet,
|
||||
padding: Padding,
|
||||
}
|
||||
|
||||
|
@ -37,18 +30,10 @@ impl MouseEventHandler {
|
|||
F: FnOnce(MouseState, &mut RenderContext<V>) -> ElementBox,
|
||||
{
|
||||
Self {
|
||||
id,
|
||||
tag: TypeId::of::<Tag>(),
|
||||
child: render_child(cx.mouse_state::<Tag>(id), cx),
|
||||
cursor_style: None,
|
||||
mouse_down: None,
|
||||
click: None,
|
||||
right_mouse_down: None,
|
||||
right_click: None,
|
||||
mouse_down_out: None,
|
||||
right_mouse_down_out: None,
|
||||
drag: None,
|
||||
hover: None,
|
||||
discriminant: (TypeId::of::<Tag>(), id),
|
||||
handlers: Default::default(),
|
||||
padding: Default::default(),
|
||||
}
|
||||
}
|
||||
|
@ -60,65 +45,45 @@ impl MouseEventHandler {
|
|||
|
||||
pub fn on_mouse_down(
|
||||
mut self,
|
||||
handler: impl Fn(Vector2F, &mut EventContext) + 'static,
|
||||
button: MouseButton,
|
||||
handler: impl Fn(MouseButtonEvent, &mut EventContext) + 'static,
|
||||
) -> Self {
|
||||
self.mouse_down = Some(Rc::new(handler));
|
||||
self.handlers = self.handlers.on_down(button, handler);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_click(
|
||||
mut self,
|
||||
handler: impl Fn(Vector2F, usize, &mut EventContext) + 'static,
|
||||
button: MouseButton,
|
||||
handler: impl Fn(MouseButtonEvent, &mut EventContext) + 'static,
|
||||
) -> Self {
|
||||
self.click = Some(Rc::new(handler));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_right_mouse_down(
|
||||
mut self,
|
||||
handler: impl Fn(Vector2F, &mut EventContext) + 'static,
|
||||
) -> Self {
|
||||
self.right_mouse_down = Some(Rc::new(handler));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_right_click(
|
||||
mut self,
|
||||
handler: impl Fn(Vector2F, usize, &mut EventContext) + 'static,
|
||||
) -> Self {
|
||||
self.right_click = Some(Rc::new(handler));
|
||||
self.handlers = self.handlers.on_click(button, handler);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_mouse_down_out(
|
||||
mut self,
|
||||
handler: impl Fn(Vector2F, &mut EventContext) + 'static,
|
||||
button: MouseButton,
|
||||
handler: impl Fn(MouseButtonEvent, &mut EventContext) + 'static,
|
||||
) -> Self {
|
||||
self.mouse_down_out = Some(Rc::new(handler));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_right_mouse_down_out(
|
||||
mut self,
|
||||
handler: impl Fn(Vector2F, &mut EventContext) + 'static,
|
||||
) -> Self {
|
||||
self.right_mouse_down_out = Some(Rc::new(handler));
|
||||
self.handlers = self.handlers.on_down_out(button, handler);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_drag(
|
||||
mut self,
|
||||
handler: impl Fn(Vector2F, Vector2F, &mut EventContext) + 'static,
|
||||
button: MouseButton,
|
||||
handler: impl Fn(Vector2F, MouseMovedEvent, &mut EventContext) + 'static,
|
||||
) -> Self {
|
||||
self.drag = Some(Rc::new(handler));
|
||||
self.handlers = self.handlers.on_drag(button, handler);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_hover(
|
||||
mut self,
|
||||
handler: impl Fn(Vector2F, bool, &mut EventContext) + 'static,
|
||||
handler: impl Fn(bool, MouseMovedEvent, &mut EventContext) + 'static,
|
||||
) -> Self {
|
||||
self.hover = Some(Rc::new(handler));
|
||||
self.handlers = self.handlers.on_hover(handler);
|
||||
self
|
||||
}
|
||||
|
||||
|
@ -163,19 +128,12 @@ impl Element for MouseEventHandler {
|
|||
});
|
||||
}
|
||||
|
||||
cx.scene.push_mouse_region(MouseRegion {
|
||||
view_id: cx.current_view_id(),
|
||||
discriminant: Some((self.tag, self.id)),
|
||||
bounds: hit_bounds,
|
||||
hover: self.hover.clone(),
|
||||
click: self.click.clone(),
|
||||
mouse_down: self.mouse_down.clone(),
|
||||
right_click: self.right_click.clone(),
|
||||
right_mouse_down: self.right_mouse_down.clone(),
|
||||
mouse_down_out: self.mouse_down_out.clone(),
|
||||
right_mouse_down_out: self.right_mouse_down_out.clone(),
|
||||
drag: self.drag.clone(),
|
||||
});
|
||||
cx.scene.push_mouse_region(MouseRegion::from_handlers(
|
||||
cx.current_view_id(),
|
||||
Some(self.discriminant.clone()),
|
||||
hit_bounds,
|
||||
self.handlers.clone(),
|
||||
));
|
||||
|
||||
self.child.paint(bounds.origin(), visible_bounds, cx);
|
||||
}
|
||||
|
|
|
@ -6,8 +6,8 @@ use crate::{
|
|||
fonts::TextStyle,
|
||||
geometry::{rect::RectF, vector::Vector2F},
|
||||
json::json,
|
||||
Action, Axis, ElementStateHandle, LayoutContext, PaintContext, RenderContext, SizeConstraint,
|
||||
Task, View,
|
||||
Action, Axis, ElementStateHandle, LayoutContext, MouseMovedEvent, PaintContext, RenderContext,
|
||||
SizeConstraint, Task, View,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use std::{
|
||||
|
@ -91,7 +91,7 @@ impl Tooltip {
|
|||
};
|
||||
let child =
|
||||
MouseEventHandler::new::<MouseEventHandlerState<Tag>, _, _>(id, cx, |_, _| child)
|
||||
.on_hover(move |position, hover, cx| {
|
||||
.on_hover(move |hover, MouseMovedEvent { position, .. }, cx| {
|
||||
let window_id = cx.window_id();
|
||||
if let Some(view_id) = cx.view_id() {
|
||||
if hover {
|
||||
|
|
|
@ -21,20 +21,26 @@ pub struct ModifiersChangedEvent {
|
|||
pub cmd: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct ScrollWheelEvent {
|
||||
pub position: Vector2F,
|
||||
pub delta: Vector2F,
|
||||
pub precise: bool,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
#[derive(Hash, PartialEq, Eq, Copy, Clone, Debug)]
|
||||
pub enum NavigationDirection {
|
||||
Back,
|
||||
Forward,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
impl Default for NavigationDirection {
|
||||
fn default() -> Self {
|
||||
Self::Back
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Hash, PartialEq, Eq, Copy, Clone, Debug)]
|
||||
pub enum MouseButton {
|
||||
Left,
|
||||
Right,
|
||||
|
@ -42,8 +48,26 @@ pub enum MouseButton {
|
|||
Navigate(NavigationDirection),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct MouseEvent {
|
||||
impl MouseButton {
|
||||
pub fn all() -> Vec<Self> {
|
||||
vec![
|
||||
MouseButton::Left,
|
||||
MouseButton::Right,
|
||||
MouseButton::Middle,
|
||||
MouseButton::Navigate(NavigationDirection::Back),
|
||||
MouseButton::Navigate(NavigationDirection::Forward),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for MouseButton {
|
||||
fn default() -> Self {
|
||||
Self::Left
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct MouseButtonEvent {
|
||||
pub button: MouseButton,
|
||||
pub position: Vector2F,
|
||||
pub ctrl: bool,
|
||||
|
@ -53,7 +77,7 @@ pub struct MouseEvent {
|
|||
pub click_count: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub struct MouseMovedEvent {
|
||||
pub position: Vector2F,
|
||||
pub pressed_button: Option<MouseButton>,
|
||||
|
@ -68,8 +92,8 @@ pub enum Event {
|
|||
KeyDown(KeyDownEvent),
|
||||
KeyUp(KeyUpEvent),
|
||||
ModifiersChanged(ModifiersChangedEvent),
|
||||
MouseDown(MouseEvent),
|
||||
MouseUp(MouseEvent),
|
||||
MouseDown(MouseButtonEvent),
|
||||
MouseUp(MouseButtonEvent),
|
||||
MouseMoved(MouseMovedEvent),
|
||||
ScrollWheel(ScrollWheelEvent),
|
||||
}
|
||||
|
|
|
@ -2,8 +2,8 @@ use crate::{
|
|||
geometry::vector::vec2f,
|
||||
keymap::Keystroke,
|
||||
platform::{Event, NavigationDirection},
|
||||
KeyDownEvent, KeyUpEvent, ModifiersChangedEvent, MouseButton, MouseEvent, MouseMovedEvent,
|
||||
ScrollWheelEvent,
|
||||
KeyDownEvent, KeyUpEvent, ModifiersChangedEvent, MouseButton, MouseButtonEvent,
|
||||
MouseMovedEvent, ScrollWheelEvent,
|
||||
};
|
||||
use cocoa::{
|
||||
appkit::{NSEvent, NSEventModifierFlags, NSEventType},
|
||||
|
@ -126,7 +126,7 @@ impl Event {
|
|||
let modifiers = native_event.modifierFlags();
|
||||
|
||||
window_height.map(|window_height| {
|
||||
Self::MouseDown(MouseEvent {
|
||||
Self::MouseDown(MouseButtonEvent {
|
||||
button,
|
||||
position: vec2f(
|
||||
native_event.locationInWindow().x as f32,
|
||||
|
@ -155,7 +155,7 @@ impl Event {
|
|||
|
||||
window_height.map(|window_height| {
|
||||
let modifiers = native_event.modifierFlags();
|
||||
Self::MouseUp(MouseEvent {
|
||||
Self::MouseUp(MouseButtonEvent {
|
||||
button,
|
||||
position: vec2f(
|
||||
native_event.locationInWindow().x as f32,
|
||||
|
|
|
@ -6,13 +6,13 @@ use crate::{
|
|||
},
|
||||
keymap::Keystroke,
|
||||
platform::{self, Event, WindowBounds, WindowContext},
|
||||
KeyDownEvent, ModifiersChangedEvent, MouseButton, MouseEvent, MouseMovedEvent, Scene,
|
||||
KeyDownEvent, ModifiersChangedEvent, MouseButton, MouseButtonEvent, MouseMovedEvent, Scene,
|
||||
};
|
||||
use block::ConcreteBlock;
|
||||
use cocoa::{
|
||||
appkit::{
|
||||
CGPoint, NSApplication, NSBackingStoreBuffered, NSScreen, NSView, NSViewHeightSizable,
|
||||
NSViewWidthSizable, NSWindow, NSWindowButton, NSWindowStyleMask,
|
||||
CGPoint, NSApplication, NSBackingStoreBuffered, NSModalResponse, NSScreen, NSView,
|
||||
NSViewHeightSizable, NSViewWidthSizable, NSWindow, NSWindowButton, NSWindowStyleMask,
|
||||
},
|
||||
base::{id, nil},
|
||||
foundation::{NSAutoreleasePool, NSInteger, NSSize, NSString},
|
||||
|
@ -228,8 +228,16 @@ impl Window {
|
|||
native_window.setFrame_display_(screen.visibleFrame(), YES);
|
||||
}
|
||||
|
||||
let device =
|
||||
metal::Device::system_default().expect("could not find default metal device");
|
||||
let device = if let Some(device) = metal::Device::system_default() {
|
||||
device
|
||||
} else {
|
||||
let alert: id = msg_send![class!(NSAlert), alloc];
|
||||
let _: () = msg_send![alert, init];
|
||||
let _: () = msg_send![alert, setAlertStyle: 2];
|
||||
let _: () = msg_send![alert, setMessageText: ns_string("Unable to access a compatible graphics device")];
|
||||
let _: NSModalResponse = msg_send![alert, runModal];
|
||||
std::process::exit(1);
|
||||
};
|
||||
|
||||
let layer: id = msg_send![class!(CAMetalLayer), layer];
|
||||
let _: () = msg_send![layer, setDevice: device.as_ptr()];
|
||||
|
@ -635,7 +643,7 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) {
|
|||
))
|
||||
.detach();
|
||||
}
|
||||
Event::MouseUp(MouseEvent {
|
||||
Event::MouseUp(MouseButtonEvent {
|
||||
button: MouseButton::Left,
|
||||
..
|
||||
}) => {
|
||||
|
|
|
@ -6,10 +6,10 @@ use crate::{
|
|||
json::{self, ToJson},
|
||||
keymap::Keystroke,
|
||||
platform::{CursorStyle, Event},
|
||||
scene::CursorRegion,
|
||||
scene::{CursorRegion, MouseRegionEvent},
|
||||
text_layout::TextLayoutCache,
|
||||
Action, AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, AssetCache, ElementBox, Entity,
|
||||
FontSystem, ModelHandle, MouseButton, MouseEvent, MouseMovedEvent, MouseRegion, MouseRegionId,
|
||||
FontSystem, ModelHandle, MouseButtonEvent, MouseMovedEvent, MouseRegion, MouseRegionId,
|
||||
ReadModel, ReadView, RenderContext, RenderParams, Scene, UpgradeModelHandle, UpgradeViewHandle,
|
||||
View, ViewHandle, WeakModelHandle, WeakViewHandle,
|
||||
};
|
||||
|
@ -230,105 +230,57 @@ impl Presenter {
|
|||
let mut mouse_down_out_handlers = Vec::new();
|
||||
let mut mouse_down_region = None;
|
||||
let mut clicked_region = None;
|
||||
let mut right_mouse_down_region = None;
|
||||
let mut right_clicked_region = None;
|
||||
let mut dragged_region = None;
|
||||
|
||||
match event {
|
||||
Event::MouseDown(MouseEvent {
|
||||
position,
|
||||
button: MouseButton::Left,
|
||||
..
|
||||
}) => {
|
||||
match &event {
|
||||
Event::MouseDown(
|
||||
e @ MouseButtonEvent {
|
||||
position, button, ..
|
||||
},
|
||||
) => {
|
||||
let mut hit = false;
|
||||
for (region, _) in self.mouse_regions.iter().rev() {
|
||||
if region.bounds.contains_point(position) {
|
||||
if region.bounds.contains_point(*position) {
|
||||
if !hit {
|
||||
hit = true;
|
||||
invalidated_views.push(region.view_id);
|
||||
mouse_down_region = Some((region.clone(), position));
|
||||
mouse_down_region =
|
||||
Some((region.clone(), MouseRegionEvent::Down(e.clone())));
|
||||
self.clicked_region = Some(region.clone());
|
||||
self.prev_drag_position = Some(position);
|
||||
self.prev_drag_position = Some(*position);
|
||||
}
|
||||
} else if let Some(handler) = region.mouse_down_out.clone() {
|
||||
mouse_down_out_handlers.push((handler, region.view_id, position));
|
||||
} else if let Some(handler) = region
|
||||
.handlers
|
||||
.get(&(MouseRegionEvent::down_out_disc(), Some(*button)))
|
||||
{
|
||||
mouse_down_out_handlers.push((
|
||||
handler,
|
||||
region.view_id,
|
||||
MouseRegionEvent::DownOut(e.clone()),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
Event::MouseUp(MouseEvent {
|
||||
position,
|
||||
click_count,
|
||||
button: MouseButton::Left,
|
||||
..
|
||||
}) => {
|
||||
Event::MouseUp(e @ MouseButtonEvent { position, .. }) => {
|
||||
self.prev_drag_position.take();
|
||||
if let Some(region) = self.clicked_region.take() {
|
||||
invalidated_views.push(region.view_id);
|
||||
if region.bounds.contains_point(position) {
|
||||
clicked_region = Some((region, position, click_count));
|
||||
if region.bounds.contains_point(*position) {
|
||||
clicked_region = Some((region, MouseRegionEvent::Click(e.clone())));
|
||||
}
|
||||
}
|
||||
}
|
||||
Event::MouseDown(MouseEvent {
|
||||
position,
|
||||
button: MouseButton::Right,
|
||||
..
|
||||
}) => {
|
||||
let mut hit = false;
|
||||
for (region, _) in self.mouse_regions.iter().rev() {
|
||||
if region.bounds.contains_point(position) {
|
||||
if !hit {
|
||||
hit = true;
|
||||
invalidated_views.push(region.view_id);
|
||||
right_mouse_down_region = Some((region.clone(), position));
|
||||
self.right_clicked_region = Some(region.clone());
|
||||
}
|
||||
} else if let Some(handler) = region.right_mouse_down_out.clone() {
|
||||
mouse_down_out_handlers.push((handler, region.view_id, position));
|
||||
}
|
||||
}
|
||||
}
|
||||
Event::MouseUp(MouseEvent {
|
||||
position,
|
||||
click_count,
|
||||
button: MouseButton::Right,
|
||||
..
|
||||
}) => {
|
||||
if let Some(region) = self.right_clicked_region.take() {
|
||||
invalidated_views.push(region.view_id);
|
||||
if region.bounds.contains_point(position) {
|
||||
right_clicked_region = Some((region, position, click_count));
|
||||
}
|
||||
}
|
||||
}
|
||||
Event::MouseMoved(MouseMovedEvent {
|
||||
pressed_button,
|
||||
position,
|
||||
shift,
|
||||
ctrl,
|
||||
alt,
|
||||
cmd,
|
||||
..
|
||||
}) => {
|
||||
if let Some(MouseButton::Left) = pressed_button {
|
||||
if let Some((clicked_region, prev_drag_position)) = self
|
||||
.clicked_region
|
||||
.as_ref()
|
||||
.zip(self.prev_drag_position.as_mut())
|
||||
{
|
||||
dragged_region =
|
||||
Some((clicked_region.clone(), *prev_drag_position, position));
|
||||
*prev_drag_position = position;
|
||||
}
|
||||
|
||||
self.last_mouse_moved_event = Some(Event::MouseMoved(MouseMovedEvent {
|
||||
position,
|
||||
pressed_button: Some(MouseButton::Left),
|
||||
shift,
|
||||
ctrl,
|
||||
alt,
|
||||
cmd,
|
||||
}));
|
||||
Event::MouseMoved(e @ MouseMovedEvent { position, .. }) => {
|
||||
if let Some((clicked_region, prev_drag_position)) = self
|
||||
.clicked_region
|
||||
.as_ref()
|
||||
.zip(self.prev_drag_position.as_mut())
|
||||
{
|
||||
dragged_region = Some((
|
||||
clicked_region.clone(),
|
||||
MouseRegionEvent::Drag(*prev_drag_position, e.clone()),
|
||||
));
|
||||
*prev_drag_position = *position;
|
||||
}
|
||||
|
||||
self.last_mouse_moved_event = Some(event.clone());
|
||||
|
@ -339,51 +291,39 @@ impl Presenter {
|
|||
let (mut handled, mut event_cx) =
|
||||
self.handle_hover_events(&event, &mut invalidated_views, cx);
|
||||
|
||||
for (handler, view_id, position) in mouse_down_out_handlers {
|
||||
event_cx.with_current_view(view_id, |event_cx| handler(position, event_cx))
|
||||
for (handler, view_id, region_event) in mouse_down_out_handlers {
|
||||
event_cx.with_current_view(view_id, |event_cx| handler(region_event, event_cx))
|
||||
}
|
||||
|
||||
if let Some((mouse_down_region, position)) = mouse_down_region {
|
||||
if let Some((mouse_down_region, region_event)) = mouse_down_region {
|
||||
handled = true;
|
||||
if let Some(mouse_down_callback) = mouse_down_region.mouse_down {
|
||||
if let Some(mouse_down_callback) =
|
||||
mouse_down_region.handlers.get(®ion_event.handler_key())
|
||||
{
|
||||
event_cx.with_current_view(mouse_down_region.view_id, |event_cx| {
|
||||
mouse_down_callback(position, event_cx);
|
||||
mouse_down_callback(region_event, event_cx);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((clicked_region, position, click_count)) = clicked_region {
|
||||
if let Some((clicked_region, region_event)) = clicked_region {
|
||||
handled = true;
|
||||
if let Some(click_callback) = clicked_region.click {
|
||||
if let Some(click_callback) =
|
||||
clicked_region.handlers.get(®ion_event.handler_key())
|
||||
{
|
||||
event_cx.with_current_view(clicked_region.view_id, |event_cx| {
|
||||
click_callback(position, click_count, event_cx);
|
||||
click_callback(region_event, event_cx);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((right_mouse_down_region, position)) = right_mouse_down_region {
|
||||
if let Some((dragged_region, region_event)) = dragged_region {
|
||||
handled = true;
|
||||
if let Some(right_mouse_down_callback) = right_mouse_down_region.right_mouse_down {
|
||||
event_cx.with_current_view(right_mouse_down_region.view_id, |event_cx| {
|
||||
right_mouse_down_callback(position, event_cx);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((right_clicked_region, position, click_count)) = right_clicked_region {
|
||||
handled = true;
|
||||
if let Some(right_click_callback) = right_clicked_region.right_click {
|
||||
event_cx.with_current_view(right_clicked_region.view_id, |event_cx| {
|
||||
right_click_callback(position, click_count, event_cx);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((dragged_region, prev_position, position)) = dragged_region {
|
||||
handled = true;
|
||||
if let Some(drag_callback) = dragged_region.drag {
|
||||
if let Some(drag_callback) =
|
||||
dragged_region.handlers.get(®ion_event.handler_key())
|
||||
{
|
||||
event_cx.with_current_view(dragged_region.view_id, |event_cx| {
|
||||
drag_callback(prev_position, position, event_cx);
|
||||
drag_callback(region_event, event_cx);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -420,14 +360,17 @@ impl Presenter {
|
|||
invalidated_views: &mut Vec<usize>,
|
||||
cx: &'a mut MutableAppContext,
|
||||
) -> (bool, EventContext<'a>) {
|
||||
let mut unhovered_regions = Vec::new();
|
||||
let mut hovered_regions = Vec::new();
|
||||
let mut hover_regions = Vec::new();
|
||||
// let mut unhovered_regions = Vec::new();
|
||||
// let mut hovered_regions = Vec::new();
|
||||
|
||||
if let Event::MouseMoved(MouseMovedEvent {
|
||||
position,
|
||||
pressed_button,
|
||||
..
|
||||
}) = event
|
||||
if let Event::MouseMoved(
|
||||
e @ MouseMovedEvent {
|
||||
position,
|
||||
pressed_button,
|
||||
..
|
||||
},
|
||||
) = event
|
||||
{
|
||||
if let None = pressed_button {
|
||||
let mut style_to_assign = CursorStyle::Arrow;
|
||||
|
@ -448,7 +391,10 @@ impl Presenter {
|
|||
if let Some(region_id) = region.id() {
|
||||
if !self.hovered_region_ids.contains(®ion_id) {
|
||||
invalidated_views.push(region.view_id);
|
||||
hovered_regions.push((region.clone(), position));
|
||||
hover_regions.push((
|
||||
region.clone(),
|
||||
MouseRegionEvent::Hover(true, e.clone()),
|
||||
));
|
||||
self.hovered_region_ids.insert(region_id);
|
||||
}
|
||||
}
|
||||
|
@ -456,7 +402,10 @@ impl Presenter {
|
|||
if let Some(region_id) = region.id() {
|
||||
if self.hovered_region_ids.contains(®ion_id) {
|
||||
invalidated_views.push(region.view_id);
|
||||
unhovered_regions.push((region.clone(), position));
|
||||
hover_regions.push((
|
||||
region.clone(),
|
||||
MouseRegionEvent::Hover(false, e.clone()),
|
||||
));
|
||||
self.hovered_region_ids.remove(®ion_id);
|
||||
}
|
||||
}
|
||||
|
@ -468,20 +417,11 @@ impl Presenter {
|
|||
let mut event_cx = self.build_event_context(cx);
|
||||
let mut handled = false;
|
||||
|
||||
for (unhovered_region, position) in unhovered_regions {
|
||||
for (hover_region, region_event) in hover_regions {
|
||||
handled = true;
|
||||
if let Some(hover_callback) = unhovered_region.hover {
|
||||
event_cx.with_current_view(unhovered_region.view_id, |event_cx| {
|
||||
hover_callback(*position, false, event_cx);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for (hovered_region, position) in hovered_regions {
|
||||
handled = true;
|
||||
if let Some(hover_callback) = hovered_region.hover {
|
||||
event_cx.with_current_view(hovered_region.view_id, |event_cx| {
|
||||
hover_callback(*position, true, event_cx);
|
||||
if let Some(hover_callback) = hover_region.handlers.get(®ion_event.handler_key()) {
|
||||
event_cx.with_current_view(hover_region.view_id, |event_cx| {
|
||||
hover_callback(region_event, event_cx);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
mod mouse_region;
|
||||
|
||||
use serde::Deserialize;
|
||||
use serde_json::json;
|
||||
use std::{any::TypeId, borrow::Cow, rc::Rc, sync::Arc};
|
||||
use std::{borrow::Cow, sync::Arc};
|
||||
|
||||
use crate::{
|
||||
color::Color,
|
||||
|
@ -8,8 +10,9 @@ use crate::{
|
|||
geometry::{rect::RectF, vector::Vector2F},
|
||||
json::ToJson,
|
||||
platform::CursorStyle,
|
||||
EventContext, ImageData, MouseEvent, MouseMovedEvent, ScrollWheelEvent,
|
||||
ImageData,
|
||||
};
|
||||
pub use mouse_region::*;
|
||||
|
||||
pub struct Scene {
|
||||
scale_factor: f32,
|
||||
|
@ -44,38 +47,6 @@ pub struct CursorRegion {
|
|||
pub style: CursorStyle,
|
||||
}
|
||||
|
||||
pub enum MouseRegionEvent {
|
||||
Moved(MouseMovedEvent),
|
||||
Hover(MouseEvent),
|
||||
Down(MouseEvent),
|
||||
Up(MouseEvent),
|
||||
Click(MouseEvent),
|
||||
DownOut(MouseEvent),
|
||||
ScrollWheel(ScrollWheelEvent),
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct MouseRegion {
|
||||
pub view_id: usize,
|
||||
pub discriminant: Option<(TypeId, usize)>,
|
||||
pub bounds: RectF,
|
||||
|
||||
pub hover: Option<Rc<dyn Fn(Vector2F, bool, &mut EventContext)>>,
|
||||
pub mouse_down: Option<Rc<dyn Fn(Vector2F, &mut EventContext)>>,
|
||||
pub click: Option<Rc<dyn Fn(Vector2F, usize, &mut EventContext)>>,
|
||||
pub right_mouse_down: Option<Rc<dyn Fn(Vector2F, &mut EventContext)>>,
|
||||
pub right_click: Option<Rc<dyn Fn(Vector2F, usize, &mut EventContext)>>,
|
||||
pub drag: Option<Rc<dyn Fn(Vector2F, Vector2F, &mut EventContext)>>,
|
||||
pub mouse_down_out: Option<Rc<dyn Fn(Vector2F, &mut EventContext)>>,
|
||||
pub right_mouse_down_out: Option<Rc<dyn Fn(Vector2F, &mut EventContext)>>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct MouseRegionId {
|
||||
pub view_id: usize,
|
||||
pub discriminant: (TypeId, usize),
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub struct Quad {
|
||||
pub bounds: RectF,
|
||||
|
|
336
crates/gpui/src/scene/mouse_region.rs
Normal file
336
crates/gpui/src/scene/mouse_region.rs
Normal file
|
@ -0,0 +1,336 @@
|
|||
use std::{any::TypeId, mem::Discriminant, rc::Rc};
|
||||
|
||||
use collections::HashMap;
|
||||
use pathfinder_geometry::{rect::RectF, vector::Vector2F};
|
||||
|
||||
use crate::{EventContext, MouseButton, MouseButtonEvent, MouseMovedEvent, ScrollWheelEvent};
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct MouseRegion {
|
||||
pub view_id: usize,
|
||||
pub discriminant: Option<(TypeId, usize)>,
|
||||
pub bounds: RectF,
|
||||
pub handlers: HandlerSet,
|
||||
}
|
||||
|
||||
impl MouseRegion {
|
||||
pub fn new(view_id: usize, discriminant: Option<(TypeId, usize)>, bounds: RectF) -> Self {
|
||||
Self::from_handlers(view_id, discriminant, bounds, Default::default())
|
||||
}
|
||||
|
||||
pub fn from_handlers(
|
||||
view_id: usize,
|
||||
discriminant: Option<(TypeId, usize)>,
|
||||
bounds: RectF,
|
||||
handlers: HandlerSet,
|
||||
) -> Self {
|
||||
Self {
|
||||
view_id,
|
||||
discriminant,
|
||||
bounds,
|
||||
handlers,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_all(
|
||||
view_id: usize,
|
||||
discriminant: Option<(TypeId, usize)>,
|
||||
bounds: RectF,
|
||||
) -> Self {
|
||||
Self {
|
||||
view_id,
|
||||
discriminant,
|
||||
bounds,
|
||||
handlers: HandlerSet::handle_all(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_down(
|
||||
mut self,
|
||||
button: MouseButton,
|
||||
handler: impl Fn(MouseButtonEvent, &mut EventContext) + 'static,
|
||||
) -> Self {
|
||||
self.handlers = self.handlers.on_down(button, handler);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_up(
|
||||
mut self,
|
||||
button: MouseButton,
|
||||
handler: impl Fn(MouseButtonEvent, &mut EventContext) + 'static,
|
||||
) -> Self {
|
||||
self.handlers = self.handlers.on_up(button, handler);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_click(
|
||||
mut self,
|
||||
button: MouseButton,
|
||||
handler: impl Fn(MouseButtonEvent, &mut EventContext) + 'static,
|
||||
) -> Self {
|
||||
self.handlers = self.handlers.on_click(button, handler);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_down_out(
|
||||
mut self,
|
||||
button: MouseButton,
|
||||
handler: impl Fn(MouseButtonEvent, &mut EventContext) + 'static,
|
||||
) -> Self {
|
||||
self.handlers = self.handlers.on_down_out(button, handler);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_drag(
|
||||
mut self,
|
||||
button: MouseButton,
|
||||
handler: impl Fn(Vector2F, MouseMovedEvent, &mut EventContext) + 'static,
|
||||
) -> Self {
|
||||
self.handlers = self.handlers.on_drag(button, handler);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_hover(
|
||||
mut self,
|
||||
handler: impl Fn(bool, MouseMovedEvent, &mut EventContext) + 'static,
|
||||
) -> Self {
|
||||
self.handlers = self.handlers.on_hover(handler);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct MouseRegionId {
|
||||
pub view_id: usize,
|
||||
pub discriminant: (TypeId, usize),
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct HandlerSet {
|
||||
pub set: HashMap<
|
||||
(Discriminant<MouseRegionEvent>, Option<MouseButton>),
|
||||
Rc<dyn Fn(MouseRegionEvent, &mut EventContext)>,
|
||||
>,
|
||||
}
|
||||
|
||||
impl HandlerSet {
|
||||
pub fn handle_all() -> Self {
|
||||
let mut set: HashMap<
|
||||
(Discriminant<MouseRegionEvent>, Option<MouseButton>),
|
||||
Rc<dyn Fn(MouseRegionEvent, &mut EventContext)>,
|
||||
> = Default::default();
|
||||
|
||||
set.insert((MouseRegionEvent::move_disc(), None), Rc::new(|_, _| {}));
|
||||
set.insert((MouseRegionEvent::hover_disc(), None), Rc::new(|_, _| {}));
|
||||
for button in MouseButton::all() {
|
||||
set.insert(
|
||||
(MouseRegionEvent::drag_disc(), Some(button)),
|
||||
Rc::new(|_, _| {}),
|
||||
);
|
||||
set.insert(
|
||||
(MouseRegionEvent::down_disc(), Some(button)),
|
||||
Rc::new(|_, _| {}),
|
||||
);
|
||||
set.insert(
|
||||
(MouseRegionEvent::up_disc(), Some(button)),
|
||||
Rc::new(|_, _| {}),
|
||||
);
|
||||
set.insert(
|
||||
(MouseRegionEvent::click_disc(), Some(button)),
|
||||
Rc::new(|_, _| {}),
|
||||
);
|
||||
set.insert(
|
||||
(MouseRegionEvent::down_out_disc(), Some(button)),
|
||||
Rc::new(|_, _| {}),
|
||||
);
|
||||
}
|
||||
set.insert(
|
||||
(MouseRegionEvent::scroll_wheel_disc(), None),
|
||||
Rc::new(|_, _| {}),
|
||||
);
|
||||
|
||||
HandlerSet { set }
|
||||
}
|
||||
|
||||
pub fn get(
|
||||
&self,
|
||||
key: &(Discriminant<MouseRegionEvent>, Option<MouseButton>),
|
||||
) -> Option<Rc<dyn Fn(MouseRegionEvent, &mut EventContext)>> {
|
||||
self.set.get(key).cloned()
|
||||
}
|
||||
|
||||
pub fn on_down(
|
||||
mut self,
|
||||
button: MouseButton,
|
||||
handler: impl Fn(MouseButtonEvent, &mut EventContext) + 'static,
|
||||
) -> Self {
|
||||
self.set.insert((MouseRegionEvent::down_disc(), Some(button)),
|
||||
Rc::new(move |region_event, cx| {
|
||||
if let MouseRegionEvent::Down(mouse_button_event) = region_event {
|
||||
handler(mouse_button_event, cx);
|
||||
} else {
|
||||
panic!(
|
||||
"Mouse Region Event incorrectly called with mismatched event type. Expected MouseRegionEvent::Down, found {:?}",
|
||||
region_event);
|
||||
}
|
||||
}));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_up(
|
||||
mut self,
|
||||
button: MouseButton,
|
||||
handler: impl Fn(MouseButtonEvent, &mut EventContext) + 'static,
|
||||
) -> Self {
|
||||
self.set.insert((MouseRegionEvent::up_disc(), Some(button)),
|
||||
Rc::new(move |region_event, cx| {
|
||||
if let MouseRegionEvent::Up(mouse_button_event) = region_event {
|
||||
handler(mouse_button_event, cx);
|
||||
} else {
|
||||
panic!(
|
||||
"Mouse Region Event incorrectly called with mismatched event type. Expected MouseRegionEvent::Up, found {:?}",
|
||||
region_event);
|
||||
}
|
||||
}));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_click(
|
||||
mut self,
|
||||
button: MouseButton,
|
||||
handler: impl Fn(MouseButtonEvent, &mut EventContext) + 'static,
|
||||
) -> Self {
|
||||
self.set.insert((MouseRegionEvent::click_disc(), Some(button)),
|
||||
Rc::new(move |region_event, cx| {
|
||||
if let MouseRegionEvent::Click(mouse_button_event) = region_event {
|
||||
handler(mouse_button_event, cx);
|
||||
} else {
|
||||
panic!(
|
||||
"Mouse Region Event incorrectly called with mismatched event type. Expected MouseRegionEvent::Click, found {:?}",
|
||||
region_event);
|
||||
}
|
||||
}));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_down_out(
|
||||
mut self,
|
||||
button: MouseButton,
|
||||
handler: impl Fn(MouseButtonEvent, &mut EventContext) + 'static,
|
||||
) -> Self {
|
||||
self.set.insert((MouseRegionEvent::down_out_disc(), Some(button)),
|
||||
Rc::new(move |region_event, cx| {
|
||||
if let MouseRegionEvent::DownOut(mouse_button_event) = region_event {
|
||||
handler(mouse_button_event, cx);
|
||||
} else {
|
||||
panic!(
|
||||
"Mouse Region Event incorrectly called with mismatched event type. Expected MouseRegionEvent::DownOut, found {:?}",
|
||||
region_event);
|
||||
}
|
||||
}));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_drag(
|
||||
mut self,
|
||||
button: MouseButton,
|
||||
handler: impl Fn(Vector2F, MouseMovedEvent, &mut EventContext) + 'static,
|
||||
) -> Self {
|
||||
self.set.insert((MouseRegionEvent::drag_disc(), Some(button)),
|
||||
Rc::new(move |region_event, cx| {
|
||||
if let MouseRegionEvent::Drag(prev_drag_position, mouse_moved_event) = region_event {
|
||||
handler(prev_drag_position, mouse_moved_event, cx);
|
||||
} else {
|
||||
panic!(
|
||||
"Mouse Region Event incorrectly called with mismatched event type. Expected MouseRegionEvent::Drag, found {:?}",
|
||||
region_event);
|
||||
}
|
||||
}));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_hover(
|
||||
mut self,
|
||||
handler: impl Fn(bool, MouseMovedEvent, &mut EventContext) + 'static,
|
||||
) -> Self {
|
||||
self.set.insert((MouseRegionEvent::hover_disc(), None),
|
||||
Rc::new(move |region_event, cx| {
|
||||
if let MouseRegionEvent::Hover(hover, mouse_moved_event) = region_event {
|
||||
handler(hover, mouse_moved_event, cx);
|
||||
} else {
|
||||
panic!(
|
||||
"Mouse Region Event incorrectly called with mismatched event type. Expected MouseRegionEvent::Hover, found {:?}",
|
||||
region_event);
|
||||
}
|
||||
}));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum MouseRegionEvent {
|
||||
Move(MouseMovedEvent),
|
||||
Drag(Vector2F, MouseMovedEvent),
|
||||
Hover(bool, MouseMovedEvent),
|
||||
Down(MouseButtonEvent),
|
||||
Up(MouseButtonEvent),
|
||||
Click(MouseButtonEvent),
|
||||
DownOut(MouseButtonEvent),
|
||||
ScrollWheel(ScrollWheelEvent),
|
||||
}
|
||||
|
||||
impl MouseRegionEvent {
|
||||
pub fn move_disc() -> Discriminant<MouseRegionEvent> {
|
||||
std::mem::discriminant(&MouseRegionEvent::Move(Default::default()))
|
||||
}
|
||||
pub fn drag_disc() -> Discriminant<MouseRegionEvent> {
|
||||
std::mem::discriminant(&MouseRegionEvent::Drag(
|
||||
Default::default(),
|
||||
Default::default(),
|
||||
))
|
||||
}
|
||||
pub fn hover_disc() -> Discriminant<MouseRegionEvent> {
|
||||
std::mem::discriminant(&MouseRegionEvent::Hover(
|
||||
Default::default(),
|
||||
Default::default(),
|
||||
))
|
||||
}
|
||||
pub fn down_disc() -> Discriminant<MouseRegionEvent> {
|
||||
std::mem::discriminant(&MouseRegionEvent::Down(Default::default()))
|
||||
}
|
||||
pub fn up_disc() -> Discriminant<MouseRegionEvent> {
|
||||
std::mem::discriminant(&MouseRegionEvent::Up(Default::default()))
|
||||
}
|
||||
pub fn click_disc() -> Discriminant<MouseRegionEvent> {
|
||||
std::mem::discriminant(&MouseRegionEvent::Click(Default::default()))
|
||||
}
|
||||
pub fn down_out_disc() -> Discriminant<MouseRegionEvent> {
|
||||
std::mem::discriminant(&MouseRegionEvent::DownOut(Default::default()))
|
||||
}
|
||||
pub fn scroll_wheel_disc() -> Discriminant<MouseRegionEvent> {
|
||||
std::mem::discriminant(&MouseRegionEvent::ScrollWheel(Default::default()))
|
||||
}
|
||||
|
||||
pub fn handler_key(&self) -> (Discriminant<MouseRegionEvent>, Option<MouseButton>) {
|
||||
match self {
|
||||
MouseRegionEvent::Move(_) => (Self::move_disc(), None),
|
||||
MouseRegionEvent::Drag(_, MouseMovedEvent { pressed_button, .. }) => {
|
||||
(Self::drag_disc(), *pressed_button)
|
||||
}
|
||||
MouseRegionEvent::Hover(_, _) => (Self::hover_disc(), None),
|
||||
MouseRegionEvent::Down(MouseButtonEvent { button, .. }) => {
|
||||
(Self::down_disc(), Some(*button))
|
||||
}
|
||||
MouseRegionEvent::Up(MouseButtonEvent { button, .. }) => {
|
||||
(Self::up_disc(), Some(*button))
|
||||
}
|
||||
MouseRegionEvent::Click(MouseButtonEvent { button, .. }) => {
|
||||
(Self::click_disc(), Some(*button))
|
||||
}
|
||||
MouseRegionEvent::DownOut(MouseButtonEvent { button, .. }) => {
|
||||
(Self::down_out_disc(), Some(*button))
|
||||
}
|
||||
MouseRegionEvent::ScrollWheel(_) => (Self::scroll_wheel_disc(), None),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,8 +1,8 @@
|
|||
use serde::Deserialize;
|
||||
|
||||
use crate::{
|
||||
actions, elements::*, impl_actions, AppContext, Entity, MutableAppContext, RenderContext, View,
|
||||
ViewContext, WeakViewHandle,
|
||||
actions, elements::*, impl_actions, AppContext, Entity, MouseButton, MutableAppContext,
|
||||
RenderContext, View, ViewContext, WeakViewHandle,
|
||||
};
|
||||
|
||||
pub struct Select {
|
||||
|
@ -119,7 +119,9 @@ impl View for Select {
|
|||
.with_style(style.header)
|
||||
.boxed()
|
||||
})
|
||||
.on_click(move |_, _, cx| cx.dispatch_action(ToggleSelect))
|
||||
.on_click(MouseButton::Left, move |_, cx| {
|
||||
cx.dispatch_action(ToggleSelect)
|
||||
})
|
||||
.boxed(),
|
||||
);
|
||||
if self.is_open {
|
||||
|
@ -151,7 +153,7 @@ impl View for Select {
|
|||
)
|
||||
},
|
||||
)
|
||||
.on_click(move |_, _, cx| {
|
||||
.on_click(MouseButton::Left, move |_, cx| {
|
||||
cx.dispatch_action(SelectItem(ix))
|
||||
})
|
||||
.boxed()
|
||||
|
|
|
@ -20,7 +20,7 @@ use std::{
|
|||
any::Any,
|
||||
cmp::{self, Ordering},
|
||||
collections::{BTreeMap, HashMap},
|
||||
ffi::OsString,
|
||||
ffi::OsStr,
|
||||
future::Future,
|
||||
iter::{self, Iterator, Peekable},
|
||||
mem,
|
||||
|
@ -185,7 +185,7 @@ pub trait File: Send + Sync {
|
|||
|
||||
/// Returns the last component of this handle's absolute path. If this handle refers to the root
|
||||
/// of its worktree, then this method will return the name of the worktree itself.
|
||||
fn file_name(&self, cx: &AppContext) -> OsString;
|
||||
fn file_name<'a>(&'a self, cx: &'a AppContext) -> &'a OsStr;
|
||||
|
||||
fn is_deleted(&self) -> bool;
|
||||
|
||||
|
|
|
@ -443,7 +443,7 @@ async fn test_outline(cx: &mut gpui::TestAppContext) {
|
|||
|
||||
async fn search<'a>(
|
||||
outline: &'a Outline<Anchor>,
|
||||
query: &str,
|
||||
query: &'a str,
|
||||
cx: &'a gpui::TestAppContext,
|
||||
) -> Vec<(&'a str, Vec<usize>)> {
|
||||
let matches = cx
|
||||
|
|
|
@ -7,8 +7,8 @@ use gpui::{
|
|||
geometry::vector::{vec2f, Vector2F},
|
||||
keymap,
|
||||
platform::CursorStyle,
|
||||
AppContext, Axis, Element, ElementBox, Entity, MouseState, MutableAppContext, RenderContext,
|
||||
Task, View, ViewContext, ViewHandle, WeakViewHandle,
|
||||
AppContext, Axis, Element, ElementBox, Entity, MouseButton, MouseState, MutableAppContext,
|
||||
RenderContext, Task, View, ViewContext, ViewHandle, WeakViewHandle,
|
||||
};
|
||||
use menu::{Cancel, Confirm, SelectFirst, SelectIndex, SelectLast, SelectNext, SelectPrev};
|
||||
use settings::Settings;
|
||||
|
@ -90,7 +90,9 @@ impl<D: PickerDelegate> View for Picker<D> {
|
|||
.read(cx)
|
||||
.render_match(ix, state, ix == selected_ix, cx)
|
||||
})
|
||||
.on_mouse_down(move |_, cx| cx.dispatch_action(SelectIndex(ix)))
|
||||
.on_mouse_down(MouseButton::Left, move |_, cx| {
|
||||
cx.dispatch_action(SelectIndex(ix))
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.boxed()
|
||||
}));
|
||||
|
|
|
@ -15,4 +15,4 @@ pollster = "0.2.5"
|
|||
smol = "1.2.5"
|
||||
|
||||
[build-dependencies]
|
||||
wasmtime = "0.38"
|
||||
wasmtime = { version = "0.38", features = ["all-arch"] }
|
||||
|
|
188
crates/plugin_runtime/OPAQUE.md
Normal file
188
crates/plugin_runtime/OPAQUE.md
Normal file
|
@ -0,0 +1,188 @@
|
|||
# Opaque handles to resources
|
||||
|
||||
Currently, Zed's plugin system only supports moving *data* (e.g. things you can serialize) across the boundary between guest-side plugin and host-side runtime. Resources, things you can't just copy, have been set aside for now. Given how important this is to Zed, I think it's about time we address this.
|
||||
|
||||
Managing resources is very important to Zed, because a lot of what Zed does is exactly that—managing resources. Each open buffer you're editing is a resource, as is the language server you're querying, or the collaboration session you're currently in. Therefore, writing a plugin system with deep integration with Zed requires some mechanism to manage resources.
|
||||
|
||||
The reason resources are problematic is because, unlike data, we can't pass resources across the ABI boundary. Wasm can't take references to host memory (and even if it could, that doesn't mean that it's a good idea). To add support for resources to plugins, we'd need three things:
|
||||
|
||||
1. Some sort of way for the host-side runtime to hang onto **references** to a resource. If the plugin requests to modify a resource, but we don't even know where that resource is, that's kinda bad, isn't it?
|
||||
|
||||
2. Some sort of way for the guest-side runtime to hang onto **handles** to a resource. We can't reference the resource directly from a plugin, but if a resource *has* been registered with the runtime, we can at least take a runtime-provided handle to that resource so that we may request that the runtime modify it in the future.
|
||||
|
||||
3. Some sort of way to **modify the resources** we're holding onto. This requires two things: some way for a plugin to request a modification, and some for the runtime to apply that modification. Here I'm using 'modification' in the most general sense, which includes, e.g. reading or writing to the resource, i.e. calling a method on it.
|
||||
|
||||
Luckily for us, managing resources across boundaries is a problem that languages have had to deal with for eons. File descriptors referencing resources managed by the kernel quintessentially defines of resource management, but this pattern is oft repeated in games, scripting languages, or surprise surprise, when writing plugins.
|
||||
|
||||
To see what managing resources in plugins could look like in Rust, we need look no further than Rhai. Rhai is a scripting language powered by a tree-walk interpreter written in Rust. It's pretty neat, but what we care about is not the language itself, but how it interfaces with Rust types.
|
||||
|
||||
In its [guide](https://rhai.rs/book/rust/custom-types.html), Rhai claims the following:
|
||||
|
||||
> Rhai works seamlessly with any Rust type, as long as it implements `Clone` as this allows the `Engine` to pass by value.
|
||||
|
||||
This doesn't mean that the underlying resources themselves need to be copied:
|
||||
|
||||
> \[Because Rhai works with types implementing `Clone`\] it is extremely easy to use Rhai with data types such as `Rc<...>`, `Arc<...>`, `Rc<RefCell<...>>`, `Arc<Mutex<...>>` etc.
|
||||
|
||||
Given that we have to register a resource with our plugin runtime before we use it, requiring the resource to be behind a shared reference makes sense, so I think the `Clone` bound is reasonable. So how does `Rhai` represent types under the hood?
|
||||
|
||||
> A custom type is stored in Rhai as a Rust trait object (specifically, a `dyn rhai::Variant`), with no restrictions other than being `Clone` (plus `Send + Sync` under the `sync` feature).
|
||||
|
||||
I'd be interested to know how Rhai disambiguates between different types if everything's a trait object under the hood.
|
||||
|
||||
Rhai actually exposes a pretty nice interface for working with native Rust types. We can register a type using `Engine::register_type::<T: Variant + Clone>()`. Internally, this just grabs the string name of the type for future reference.
|
||||
|
||||
> **Note**: Rhai uses strings, but I wonder if you could get away with something more compact using `TypeIds`. Maybe not, given that `TypeId`s are not deterministic across builds, and we'd need matching IDs both host-side and guest side.
|
||||
|
||||
In Rhai, we can alternatively use the method `Engine::register_type_with_name::<T: Variant + Clone>(name: &str)` if we have a different type name host-side (in Rust) and guest-side (in Rhai).
|
||||
|
||||
With respect to Wasm plugins, I think an interface like this is fairly important, because we don't know whether the original plugin was written in Rust. (This may not be true now, because we write all the plugins Zed uses, but once we allow packaging and shipping plugins, it's important to maintain a consistent interface, because even Rust changes over time.)
|
||||
|
||||
Once we've registered a type, we can begin using this type in functions. We can add new function using the standard `Engine::register_fn` function, which has the following signature:
|
||||
|
||||
```rust
|
||||
pub fn register_fn<N, A, F>(&mut self, name: N, func: F) -> &mut Self
|
||||
where
|
||||
N: AsRef<str> + Into<Identifier>,
|
||||
F: RegisterNativeFunction<A, ()>,
|
||||
```
|
||||
|
||||
This is quite complex, but under the hood it's fairly similar to our own `PluginBuilder::host_function` async method. Looking at `RegisterNativeFunction`, it seems as though this trait essentially provides methods that expose the `TypeID`s and type/param names of the arguments and return types of the function.
|
||||
|
||||
So once we register a function, what happens when we call it? Well, let me introduce you to my friend `Engine::call_native_fn`, whose type signature is too complex to list here.
|
||||
|
||||
> **Note**: Finding this function took like 7 levels of indirection from `eval`. It's surprising how much shuffling of data Rhai does under the hood, I bet you could probably make it a lot faster.
|
||||
|
||||
This takes and returns, like everything else in Rhai, an object of type `Dynamic`. We know that we can use native Rust types, so how does Rhai perform the conversion to and from `Dynamic`?
|
||||
|
||||
The secret lies in `Dynamic::try_cast::<T: Any>(self) -> Option<T>`. Like most dynamic scripting languages, Rhai uses a tagged `Union` to represent types. Remember `dyn Variant` from earlier? Rhai's `Union` has a variant, `Variant`, to hold the dynamic native types:
|
||||
|
||||
```rust
|
||||
/// Any type as a trait object.
|
||||
#[allow(clippy::redundant_allocation)]
|
||||
Variant(Box<Box<dyn Variant>>, Tag, AccessMode),
|
||||
```
|
||||
|
||||
Redundant allocations aside, To `try_cast` a `Dynamic` type to `T: Any`thing, we pattern match on `Union`. In the case of variant, we:
|
||||
|
||||
```rust
|
||||
Union::Variant(v, ..) => (*v).as_boxed_any().downcast().ok().map(|x| *x),
|
||||
```
|
||||
|
||||
Now Rhai can do this because it's implemented in Rust. In other words, unlike Wasm, Rhai scripts can, indirectly, hold references to places in host memory. For us to implement something like this for Wasm plugins, we'd have to keep track of a "`ResourcePool`"—alive for the duration of each function call—that we can check rust types into and out of.
|
||||
|
||||
I think I've got a handle on how Rhai works now, so let's stop talking about Rhai and discuss what this opaque object system would look like if we implemented it in Rust.
|
||||
|
||||
# Design Sketch
|
||||
|
||||
First things first, we'd have to generalize the arguments we can pass to and return from functions host-side. Currently, we support anything that's `serde`able. We'd have to create a new trait, say `Value`, that has blanket implementations for both `serde` and `Clone` (or something like this; if a type is both `serde` and `clone`, we'd have to figure out a way to disambiguate).
|
||||
|
||||
We'd also create a `ResourcePool` struct that essentially is a `Vec` of `Box<dyn Any>`. When calling a function, all `Value` arguments that are resources (e.g. `Clone` instead of `serde`) would be typecasted to `dyn Any` and stored in the `ResourcePool`.
|
||||
|
||||
We'd probably also need a `Resource` trait that defines an associated handle for a resource. Something like this:
|
||||
|
||||
```rust
|
||||
pub trait Resource {
|
||||
type Handle: Serialize + DeserializeOwned;
|
||||
fn handle(index: u32) -> Self;
|
||||
fn index(handle: Self) -> u32;
|
||||
}
|
||||
```
|
||||
|
||||
Where a handle is just a dead-simple wrapper around a `u32`:
|
||||
|
||||
```rust
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct CoolHandle(u32);
|
||||
```
|
||||
|
||||
It's important that this handle be accessible *both* host-side and plugin side. I don't know if this means that we have another crate, like `plugin_handles`, that contains a bunch of u32 wrappers, or something else. Because a `Resource::Handle` is just a u32, it's trivially `serde`, and can cross the ABI boundary.
|
||||
|
||||
So when we add each `T: Resource` to the `ResourcePool`, the resource pool typecasts it to `Any`, appends it to the `Vec`, and returns the associated `Resource::Handle`. This handle is what we pass through to Wasm.
|
||||
|
||||
```rust
|
||||
// Implementations and attributes omitted
|
||||
pub struct Rope { ... };
|
||||
pub struct RopeHandle(u32);
|
||||
impl Resource for Arc<RwLock<Rope>> { ... }
|
||||
|
||||
let builder: PluginBuilder = ...;
|
||||
let builder = builder
|
||||
.host_fn_async(
|
||||
"append",
|
||||
|(rope, string): (Arc<RwLock<Rope>>, &str)| async move {
|
||||
rope.write().await.append(Rope::from(string))
|
||||
}
|
||||
)
|
||||
// ...
|
||||
```
|
||||
|
||||
He're we're providing a host function, `append` that can be called from Wasm. To import this function into a plugin, we'd do something like the following:
|
||||
|
||||
```rust
|
||||
use plugin::prelude::*;
|
||||
use plugin_handles::RopeHandle;
|
||||
|
||||
#[import]
|
||||
pub fn append(rope: RopeHandle, string: &str);
|
||||
```
|
||||
|
||||
This allows us to perform an operation on a `Rope`, but how do we get a `RopeHandle` into a plugin? Well, as plugins, we can only aquire resources to handles we're given, so we'd need to expose a fuction that takes a handle.
|
||||
|
||||
To illustrate that point, here's an example. First, we'd define a plugin-side function as follows:
|
||||
|
||||
```rust
|
||||
// same file as above ...
|
||||
|
||||
#[export]
|
||||
pub fn append_newline(rope: RopeHandle){
|
||||
append(rope, "\n");
|
||||
}
|
||||
```
|
||||
|
||||
Host-side, we'd treat this function like any other:
|
||||
|
||||
```rust
|
||||
pub struct NewlineAppenderPlugin {
|
||||
append_newline: WasiFn<Arc<RwLock<Rope>>, ()>,
|
||||
runtime: Arc<Mutex<Plugin>>,
|
||||
}
|
||||
```
|
||||
|
||||
To call this function, we'd do the following:
|
||||
|
||||
```rust
|
||||
let plugin: NewlineAppenderPlugin = ...;
|
||||
let rope = Arc::new(RwLock::new(Rope::from("Hello World")));
|
||||
|
||||
plugin.lock().await.call(
|
||||
&plugin.append_newline,
|
||||
rope.clone(),
|
||||
).await?;
|
||||
|
||||
// `rope` is now "Hello World\n"
|
||||
```
|
||||
|
||||
So here's what calling `append_newline` would do, from the top:
|
||||
|
||||
1. First, we'd create a new `ResourcePool`, and insert the `Arc<RwLock<Rope>>`, creating a `RopeHandle` in the process. (We could also reuse a resource pool across calls, but the idea is that the pool only keeps track of resources for the duration of the call).
|
||||
|
||||
2. Then, we'd call the Wasm plugin function `append_newline`, passing in the `RopeHandle` we created, which easily crosses the ABI boundary.
|
||||
|
||||
3. Next, in Wasm, we call the native imported function `append`. This sends the `RopeHandle` back over the boundary, to Rust.
|
||||
|
||||
4. Looking in the `Plugin`'s `ResourcePool`, we'd convert the handle into an index, grab and downcast the `dyn Any` back into the type we need, and then call the async Rust callback with an `Arc<RwLock<Rope>>`.
|
||||
|
||||
5. The Rust async callback actually acquires a lock and appends the newline.
|
||||
|
||||
6. And from here on out we return up the callstack, through Wasm, to Rust all the way back to where we started. Right before we return, we clear out the `ResourcePool`, so that we're no longer holding onto the underlying resource.
|
||||
|
||||
Throughout this entire chain of calls, the resource remain host-side. By temporarilty checking it into a `ResourcePool`, we're able to keep a reference to the resource that we can use, while avoiding copying the uncopyable resource.
|
||||
|
||||
## Final Notes
|
||||
|
||||
Using this approach, it should be possible to add fairly good support for resources to Wasm. I've only done a little rough prototyping, so we're bound to run into some issues along the way, but I think this should be a good first approximation.
|
||||
|
||||
This next week, I'll try to get a production-ready version of this working, using the `Language` resource required by some Language Server Adapters.
|
||||
|
||||
Hope this guide made sense!
|
|
@ -152,7 +152,7 @@ Plugins in the `plugins` directory are automatically recompiled and serialized t
|
|||
|
||||
- `plugin.wasm` is the plugin compiled to Wasm. As a baseline, this should be about 4MB for debug builds and 2MB for release builds, but it depends on the specific plugin being built.
|
||||
|
||||
- `plugin.wasm.pre` is the plugin compiled to Wasm *and additionally* precompiled to host-platform-agnostic cranelift-specific IR. This should be about 700KB for debug builds and 500KB in release builds. Each plugin takes about 1 or 2 seconds to compile to native code using cranelift, so precompiling plugins drastically reduces the startup time required to begin to run a plugin.
|
||||
- `plugin.wasm.pre` is the plugin compiled to Wasm *and additionally* precompiled to host-platform-specific native code, determined by the `TARGET` cargo exposes at compile-time. This should be about 700KB for debug builds and 500KB in release builds. Each plugin takes about 1 or 2 seconds to compile to native code using cranelift, so precompiling plugins drastically reduces the startup time required to begin to run a plugin.
|
||||
|
||||
For all intents and purposes, it is *highly recommended* that you use precompiled plugins where possible, as they are much more lightweight and take much less time to instantiate.
|
||||
|
||||
|
@ -246,18 +246,17 @@ Once all imports are marked, we can instantiate the plugin. To instantiate the p
|
|||
```rust
|
||||
let plugin = builder
|
||||
.init(
|
||||
true,
|
||||
include_bytes!("../../../plugins/bin/cool_plugin.wasm.pre"),
|
||||
PluginBinary::Precompiled(bytes),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
```
|
||||
|
||||
The `.init` method currently takes two arguments:
|
||||
The `.init` method takes a single argument containing the plugin binary.
|
||||
|
||||
1. First, the 'precompiled' flag, indicating whether the plugin is *normal* (`.wasm`) or precompiled (`.wasm.pre`). When using a precompiled plugin, set this flag to `true`.
|
||||
1. If not precompiled, use `PluginBinary::Wasm(bytes)`. This supports both the WebAssembly Textual format (`.wat`) and the WebAssembly Binary format (`.wasm`).
|
||||
|
||||
2. Second, the raw plugin Wasm itself, as an array of bytes. When not precompiled, this can be either the Wasm binary format (`.wasm`) or the Wasm textual format (`.wat`). When precompiled, this must be the precompiled plugin (`.wasm.pre`).
|
||||
2. If precompiled, use `PluginBinary::Precompiled(bytes)`. This supports precompiled plugins ending in `.wasm.pre`. You need to be extra-careful when using precompiled plugins to ensure that the plugin target matches the target of the binary you are compiling.
|
||||
|
||||
The `.init` method is asynchronous, and must be `.await`ed upon. If the plugin is malformed or doesn't import the right functions, an error will be raised.
|
||||
|
||||
|
|
|
@ -26,7 +26,6 @@ fn main() {
|
|||
"release" => (&["--release"][..], "release"),
|
||||
unknown => panic!("unknown profile `{}`", unknown),
|
||||
};
|
||||
|
||||
// Invoke cargo to build the plugins
|
||||
let build_successful = std::process::Command::new("cargo")
|
||||
.args([
|
||||
|
@ -42,8 +41,13 @@ fn main() {
|
|||
.success();
|
||||
assert!(build_successful);
|
||||
|
||||
// Get the target architecture for pre-cross-compilation of plugins
|
||||
// and create and engine with the appropriate config
|
||||
let target_triple = std::env::var("TARGET").unwrap().to_string();
|
||||
println!("cargo:rerun-if-env-changed=TARGET");
|
||||
let engine = create_default_engine(&target_triple);
|
||||
|
||||
// Find all compiled binaries
|
||||
let engine = create_default_engine();
|
||||
let binaries = std::fs::read_dir(base.join("target/wasm32-wasi").join(profile_target))
|
||||
.expect("Could not find compiled plugins in target");
|
||||
|
||||
|
@ -66,11 +70,17 @@ fn main() {
|
|||
}
|
||||
}
|
||||
|
||||
/// Creates a default engine for compiling Wasm.
|
||||
fn create_default_engine() -> Engine {
|
||||
/// Creates an engine with the default configuration.
|
||||
/// N.B. This must create an engine with the same config as the one
|
||||
/// in `plugin_runtime/src/plugin.rs`.
|
||||
fn create_default_engine(target_triple: &str) -> Engine {
|
||||
let mut config = Config::default();
|
||||
config
|
||||
.target(target_triple)
|
||||
.expect(&format!("Could not set target to `{}`", target_triple));
|
||||
config.async_support(true);
|
||||
Engine::new(&config).expect("Could not create engine")
|
||||
config.consume_fuel(true);
|
||||
Engine::new(&config).expect("Could not create precompilation engine")
|
||||
}
|
||||
|
||||
fn precompile(path: &Path, engine: &Engine) {
|
||||
|
@ -80,7 +90,7 @@ fn precompile(path: &Path, engine: &Engine) {
|
|||
.expect("Could not precompile module");
|
||||
let out_path = path.parent().unwrap().join(&format!(
|
||||
"{}.pre",
|
||||
path.file_name().unwrap().to_string_lossy()
|
||||
path.file_name().unwrap().to_string_lossy(),
|
||||
));
|
||||
let mut out_file = std::fs::File::create(out_path)
|
||||
.expect("Could not create output file for precompiled module");
|
||||
|
|
|
@ -23,7 +23,7 @@ mod tests {
|
|||
}
|
||||
|
||||
async {
|
||||
let mut runtime = PluginBuilder::new_fuel_with_default_ctx(PluginYield::default_fuel())
|
||||
let mut runtime = PluginBuilder::new_default()
|
||||
.unwrap()
|
||||
.host_function("mystery_number", |input: u32| input + 7)
|
||||
.unwrap()
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
use std::future::Future;
|
||||
|
||||
use std::time::Duration;
|
||||
use std::{fs::File, marker::PhantomData, path::Path};
|
||||
|
||||
use anyhow::{anyhow, Error};
|
||||
|
@ -55,34 +54,14 @@ impl<A: Serialize, R: DeserializeOwned> Clone for WasiFn<A, R> {
|
|||
}
|
||||
}
|
||||
|
||||
pub struct PluginYieldEpoch {
|
||||
delta: u64,
|
||||
epoch: std::time::Duration,
|
||||
}
|
||||
|
||||
pub struct PluginYieldFuel {
|
||||
pub struct Metering {
|
||||
initial: u64,
|
||||
refill: u64,
|
||||
}
|
||||
|
||||
pub enum PluginYield {
|
||||
Epoch {
|
||||
yield_epoch: PluginYieldEpoch,
|
||||
initialize_incrementer: Box<dyn FnOnce(Engine) -> () + Send>,
|
||||
},
|
||||
Fuel(PluginYieldFuel),
|
||||
}
|
||||
|
||||
impl PluginYield {
|
||||
pub fn default_epoch() -> PluginYieldEpoch {
|
||||
PluginYieldEpoch {
|
||||
delta: 1,
|
||||
epoch: Duration::from_millis(1),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn default_fuel() -> PluginYieldFuel {
|
||||
PluginYieldFuel {
|
||||
impl Default for Metering {
|
||||
fn default() -> Self {
|
||||
Metering {
|
||||
initial: 1000,
|
||||
refill: 1000,
|
||||
}
|
||||
|
@ -97,110 +76,44 @@ pub struct PluginBuilder {
|
|||
wasi_ctx: WasiCtx,
|
||||
engine: Engine,
|
||||
linker: Linker<WasiCtxAlloc>,
|
||||
yield_when: PluginYield,
|
||||
metering: Metering,
|
||||
}
|
||||
|
||||
/// Creates an engine with the default configuration.
|
||||
/// N.B. This must create an engine with the same config as the one
|
||||
/// in `plugin_runtime/build.rs`.
|
||||
fn create_default_engine() -> Result<Engine, Error> {
|
||||
let mut config = Config::default();
|
||||
config.async_support(true);
|
||||
config.consume_fuel(true);
|
||||
Engine::new(&config)
|
||||
}
|
||||
|
||||
impl PluginBuilder {
|
||||
/// Creates an engine with the proper configuration given the yield mechanism in use
|
||||
fn create_engine(yield_when: &PluginYield) -> Result<(Engine, Linker<WasiCtxAlloc>), Error> {
|
||||
let mut config = Config::default();
|
||||
config.async_support(true);
|
||||
|
||||
match yield_when {
|
||||
PluginYield::Epoch { .. } => {
|
||||
config.epoch_interruption(true);
|
||||
}
|
||||
PluginYield::Fuel(_) => {
|
||||
config.consume_fuel(true);
|
||||
}
|
||||
}
|
||||
|
||||
let engine = Engine::new(&config)?;
|
||||
let linker = Linker::new(&engine);
|
||||
Ok((engine, linker))
|
||||
}
|
||||
|
||||
/// Create a new [`PluginBuilder`] with the given WASI context.
|
||||
/// Using the default context is a safe bet, see [`new_with_default_context`].
|
||||
/// This plugin will yield after each fixed configurable epoch.
|
||||
pub fn new_epoch<C>(
|
||||
wasi_ctx: WasiCtx,
|
||||
yield_epoch: PluginYieldEpoch,
|
||||
spawn_detached_future: C,
|
||||
) -> Result<Self, Error>
|
||||
where
|
||||
C: FnOnce(std::pin::Pin<Box<dyn Future<Output = ()> + Send + 'static>>) -> ()
|
||||
+ Send
|
||||
+ 'static,
|
||||
{
|
||||
// we can't create the future until after initializing
|
||||
// because we need the engine to load the plugin
|
||||
let epoch = yield_epoch.epoch;
|
||||
let initialize_incrementer = Box::new(move |engine: Engine| {
|
||||
spawn_detached_future(Box::pin(async move {
|
||||
loop {
|
||||
smol::Timer::after(epoch).await;
|
||||
engine.increment_epoch();
|
||||
}
|
||||
}))
|
||||
});
|
||||
|
||||
let yield_when = PluginYield::Epoch {
|
||||
yield_epoch,
|
||||
initialize_incrementer,
|
||||
};
|
||||
let (engine, linker) = Self::create_engine(&yield_when)?;
|
||||
|
||||
Ok(PluginBuilder {
|
||||
wasi_ctx,
|
||||
engine,
|
||||
linker,
|
||||
yield_when,
|
||||
})
|
||||
}
|
||||
|
||||
/// Create a new [`PluginBuilder`] with the given WASI context.
|
||||
/// Using the default context is a safe bet, see [`new_with_default_context`].
|
||||
/// This plugin will yield after a configurable amount of fuel is consumed.
|
||||
pub fn new_fuel(wasi_ctx: WasiCtx, yield_fuel: PluginYieldFuel) -> Result<Self, Error> {
|
||||
let yield_when = PluginYield::Fuel(yield_fuel);
|
||||
let (engine, linker) = Self::create_engine(&yield_when)?;
|
||||
pub fn new(wasi_ctx: WasiCtx, metering: Metering) -> Result<Self, Error> {
|
||||
let engine = create_default_engine()?;
|
||||
let linker = Linker::new(&engine);
|
||||
|
||||
Ok(PluginBuilder {
|
||||
wasi_ctx,
|
||||
engine,
|
||||
linker,
|
||||
yield_when,
|
||||
metering,
|
||||
})
|
||||
}
|
||||
|
||||
/// Create a new `WasiCtx` that inherits the
|
||||
/// host processes' access to `stdout` and `stderr`.
|
||||
fn default_ctx() -> WasiCtx {
|
||||
WasiCtxBuilder::new()
|
||||
/// Create a new `PluginBuilder` with the default `WasiCtx` (see [`default_ctx`]).
|
||||
/// This plugin will yield after a configurable amount of fuel is consumed.
|
||||
pub fn new_default() -> Result<Self, Error> {
|
||||
let default_ctx = WasiCtxBuilder::new()
|
||||
.inherit_stdout()
|
||||
.inherit_stderr()
|
||||
.build()
|
||||
}
|
||||
|
||||
/// Create a new `PluginBuilder` with the default `WasiCtx` (see [`default_ctx`]).
|
||||
/// This plugin will yield after each fixed configurable epoch.
|
||||
pub fn new_epoch_with_default_ctx<C>(
|
||||
yield_epoch: PluginYieldEpoch,
|
||||
spawn_detached_future: C,
|
||||
) -> Result<Self, Error>
|
||||
where
|
||||
C: FnOnce(std::pin::Pin<Box<dyn Future<Output = ()> + Send + 'static>>) -> ()
|
||||
+ Send
|
||||
+ 'static,
|
||||
{
|
||||
Self::new_epoch(Self::default_ctx(), yield_epoch, spawn_detached_future)
|
||||
}
|
||||
|
||||
/// Create a new `PluginBuilder` with the default `WasiCtx` (see [`default_ctx`]).
|
||||
/// This plugin will yield after a configurable amount of fuel is consumed.
|
||||
pub fn new_fuel_with_default_ctx(yield_fuel: PluginYieldFuel) -> Result<Self, Error> {
|
||||
Self::new_fuel(Self::default_ctx(), yield_fuel)
|
||||
.build();
|
||||
let metering = Metering::default();
|
||||
Self::new(default_ctx, metering)
|
||||
}
|
||||
|
||||
/// Add an `async` host function. See [`host_function`] for details.
|
||||
|
@ -433,19 +346,8 @@ impl Plugin {
|
|||
};
|
||||
|
||||
// set up automatic yielding based on configuration
|
||||
match plugin.yield_when {
|
||||
PluginYield::Epoch {
|
||||
yield_epoch: PluginYieldEpoch { delta, .. },
|
||||
initialize_incrementer,
|
||||
} => {
|
||||
store.epoch_deadline_async_yield_and_update(delta);
|
||||
initialize_incrementer(engine);
|
||||
}
|
||||
PluginYield::Fuel(PluginYieldFuel { initial, refill }) => {
|
||||
store.add_fuel(initial).unwrap();
|
||||
store.out_of_fuel_async_yield(u64::MAX, refill);
|
||||
}
|
||||
}
|
||||
store.add_fuel(plugin.metering.initial).unwrap();
|
||||
store.out_of_fuel_async_yield(u64::MAX, plugin.metering.refill);
|
||||
|
||||
// load the provided module into the asynchronous runtime
|
||||
linker.module_async(&mut store, "", &module).await?;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use anyhow::{anyhow, Result};
|
||||
use fsevent::EventStream;
|
||||
use futures::{Stream, StreamExt};
|
||||
use futures::{future::BoxFuture, Stream, StreamExt};
|
||||
use language::LineEnding;
|
||||
use smol::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use std::{
|
||||
|
@ -12,11 +12,18 @@ use std::{
|
|||
};
|
||||
use text::Rope;
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
use collections::{btree_map, BTreeMap};
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
use futures::lock::Mutex;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
use std::sync::{Arc, Weak};
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait Fs: Send + Sync {
|
||||
async fn create_dir(&self, path: &Path) -> Result<()>;
|
||||
async fn create_file(&self, path: &Path, options: CreateOptions) -> Result<()>;
|
||||
async fn copy(&self, source: &Path, target: &Path, options: CopyOptions) -> Result<()>;
|
||||
async fn copy_file(&self, source: &Path, target: &Path, options: CopyOptions) -> Result<()>;
|
||||
async fn rename(&self, source: &Path, target: &Path, options: RenameOptions) -> Result<()>;
|
||||
async fn remove_dir(&self, path: &Path, options: RemoveOptions) -> Result<()>;
|
||||
async fn remove_file(&self, path: &Path, options: RemoveOptions) -> Result<()>;
|
||||
|
@ -92,7 +99,7 @@ impl Fs for RealFs {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
async fn copy(&self, source: &Path, target: &Path, options: CopyOptions) -> Result<()> {
|
||||
async fn copy_file(&self, source: &Path, target: &Path, options: CopyOptions) -> Result<()> {
|
||||
if !options.overwrite && smol::fs::metadata(target).await.is_ok() {
|
||||
if options.ignore_if_exists {
|
||||
return Ok(());
|
||||
|
@ -101,23 +108,7 @@ impl Fs for RealFs {
|
|||
}
|
||||
}
|
||||
|
||||
let metadata = smol::fs::metadata(source).await?;
|
||||
let _ = smol::fs::remove_dir_all(target).await;
|
||||
if metadata.is_dir() {
|
||||
self.create_dir(target).await?;
|
||||
let mut children = smol::fs::read_dir(source).await?;
|
||||
while let Some(child) = children.next().await {
|
||||
if let Ok(child) = child {
|
||||
let child_source_path = child.path();
|
||||
let child_target_path = target.join(child.file_name());
|
||||
self.copy(&child_source_path, &child_target_path, options)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
smol::fs::copy(source, target).await?;
|
||||
}
|
||||
|
||||
smol::fs::copy(source, target).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -235,11 +226,13 @@ impl Fs for RealFs {
|
|||
) -> Pin<Box<dyn Send + Stream<Item = Vec<fsevent::Event>>>> {
|
||||
let (tx, rx) = smol::channel::unbounded();
|
||||
let (stream, handle) = EventStream::new(&[path], latency);
|
||||
std::mem::forget(handle);
|
||||
std::thread::spawn(move || {
|
||||
stream.run(move |events| smol::block_on(tx.send(events)).is_ok());
|
||||
});
|
||||
Box::pin(rx)
|
||||
Box::pin(rx.chain(futures::stream::once(async move {
|
||||
drop(handle);
|
||||
vec![]
|
||||
})))
|
||||
}
|
||||
|
||||
fn is_fake(&self) -> bool {
|
||||
|
@ -252,35 +245,115 @@ impl Fs for RealFs {
|
|||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
#[derive(Clone, Debug)]
|
||||
struct FakeFsEntry {
|
||||
metadata: Metadata,
|
||||
content: Option<String>,
|
||||
pub struct FakeFs {
|
||||
// Use an unfair lock to ensure tests are deterministic.
|
||||
state: Mutex<FakeFsState>,
|
||||
executor: Weak<gpui::executor::Background>,
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
struct FakeFsState {
|
||||
entries: std::collections::BTreeMap<PathBuf, FakeFsEntry>,
|
||||
root: Arc<Mutex<FakeFsEntry>>,
|
||||
next_inode: u64,
|
||||
event_txs: Vec<smol::channel::Sender<Vec<fsevent::Event>>>,
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
#[derive(Debug)]
|
||||
enum FakeFsEntry {
|
||||
File {
|
||||
inode: u64,
|
||||
mtime: SystemTime,
|
||||
content: String,
|
||||
},
|
||||
Dir {
|
||||
inode: u64,
|
||||
mtime: SystemTime,
|
||||
entries: BTreeMap<String, Arc<Mutex<FakeFsEntry>>>,
|
||||
},
|
||||
Symlink {
|
||||
target: PathBuf,
|
||||
},
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
impl FakeFsState {
|
||||
fn validate_path(&self, path: &Path) -> Result<()> {
|
||||
if path.is_absolute()
|
||||
&& path
|
||||
.parent()
|
||||
.and_then(|path| self.entries.get(path))
|
||||
.map_or(false, |e| e.metadata.is_dir)
|
||||
{
|
||||
Ok(())
|
||||
} else {
|
||||
Err(anyhow!("invalid path {:?}", path))
|
||||
}
|
||||
async fn read_path<'a>(&'a self, target: &Path) -> Result<Arc<Mutex<FakeFsEntry>>> {
|
||||
Ok(self
|
||||
.try_read_path(target)
|
||||
.await
|
||||
.ok_or_else(|| anyhow!("path does not exist: {}", target.display()))?
|
||||
.0)
|
||||
}
|
||||
|
||||
async fn emit_event<I, T>(&mut self, paths: I)
|
||||
async fn try_read_path<'a>(
|
||||
&'a self,
|
||||
target: &Path,
|
||||
) -> Option<(Arc<Mutex<FakeFsEntry>>, PathBuf)> {
|
||||
let mut path = target.to_path_buf();
|
||||
let mut real_path = PathBuf::new();
|
||||
let mut entry_stack = Vec::new();
|
||||
'outer: loop {
|
||||
let mut path_components = path.components().collect::<collections::VecDeque<_>>();
|
||||
while let Some(component) = path_components.pop_front() {
|
||||
match component {
|
||||
Component::Prefix(_) => panic!("prefix paths aren't supported"),
|
||||
Component::RootDir => {
|
||||
entry_stack.clear();
|
||||
entry_stack.push(self.root.clone());
|
||||
real_path.clear();
|
||||
real_path.push("/");
|
||||
}
|
||||
Component::CurDir => {}
|
||||
Component::ParentDir => {
|
||||
entry_stack.pop()?;
|
||||
real_path.pop();
|
||||
}
|
||||
Component::Normal(name) => {
|
||||
let current_entry = entry_stack.last().cloned()?;
|
||||
let current_entry = current_entry.lock().await;
|
||||
if let FakeFsEntry::Dir { entries, .. } = &*current_entry {
|
||||
let entry = entries.get(name.to_str().unwrap()).cloned()?;
|
||||
let _entry = entry.lock().await;
|
||||
if let FakeFsEntry::Symlink { target, .. } = &*_entry {
|
||||
let mut target = target.clone();
|
||||
target.extend(path_components);
|
||||
path = target;
|
||||
continue 'outer;
|
||||
} else {
|
||||
entry_stack.push(entry.clone());
|
||||
real_path.push(name);
|
||||
}
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
entry_stack.pop().map(|entry| (entry, real_path))
|
||||
}
|
||||
|
||||
async fn write_path<Fn, T>(&self, path: &Path, callback: Fn) -> Result<T>
|
||||
where
|
||||
Fn: FnOnce(btree_map::Entry<String, Arc<Mutex<FakeFsEntry>>>) -> Result<T>,
|
||||
{
|
||||
let path = normalize_path(path);
|
||||
let filename = path
|
||||
.file_name()
|
||||
.ok_or_else(|| anyhow!("cannot overwrite the root"))?;
|
||||
let parent_path = path.parent().unwrap();
|
||||
|
||||
let parent = self.read_path(parent_path).await?;
|
||||
let mut parent = parent.lock().await;
|
||||
let new_entry = parent
|
||||
.dir_entries(parent_path)?
|
||||
.entry(filename.to_str().unwrap().into());
|
||||
callback(new_entry)
|
||||
}
|
||||
|
||||
fn emit_event<I, T>(&mut self, paths: I)
|
||||
where
|
||||
I: IntoIterator<Item = T>,
|
||||
T: Into<PathBuf>,
|
||||
|
@ -301,33 +374,17 @@ impl FakeFsState {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub struct FakeFs {
|
||||
// Use an unfair lock to ensure tests are deterministic.
|
||||
state: futures::lock::Mutex<FakeFsState>,
|
||||
executor: std::sync::Weak<gpui::executor::Background>,
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
impl FakeFs {
|
||||
pub fn new(executor: std::sync::Arc<gpui::executor::Background>) -> std::sync::Arc<Self> {
|
||||
let mut entries = std::collections::BTreeMap::new();
|
||||
entries.insert(
|
||||
Path::new("/").to_path_buf(),
|
||||
FakeFsEntry {
|
||||
metadata: Metadata {
|
||||
pub fn new(executor: Arc<gpui::executor::Background>) -> Arc<Self> {
|
||||
Arc::new(Self {
|
||||
executor: Arc::downgrade(&executor),
|
||||
state: Mutex::new(FakeFsState {
|
||||
root: Arc::new(Mutex::new(FakeFsEntry::Dir {
|
||||
inode: 0,
|
||||
mtime: SystemTime::now(),
|
||||
is_dir: true,
|
||||
is_symlink: false,
|
||||
},
|
||||
content: None,
|
||||
},
|
||||
);
|
||||
std::sync::Arc::new(Self {
|
||||
executor: std::sync::Arc::downgrade(&executor),
|
||||
state: futures::lock::Mutex::new(FakeFsState {
|
||||
entries,
|
||||
entries: Default::default(),
|
||||
})),
|
||||
next_inode: 1,
|
||||
event_txs: Default::default(),
|
||||
}),
|
||||
|
@ -337,23 +394,48 @@ impl FakeFs {
|
|||
pub async fn insert_file(&self, path: impl AsRef<Path>, content: String) {
|
||||
let mut state = self.state.lock().await;
|
||||
let path = path.as_ref();
|
||||
state.validate_path(path).unwrap();
|
||||
|
||||
let inode = state.next_inode;
|
||||
state.next_inode += 1;
|
||||
state.entries.insert(
|
||||
path.to_path_buf(),
|
||||
FakeFsEntry {
|
||||
metadata: Metadata {
|
||||
inode,
|
||||
mtime: SystemTime::now(),
|
||||
is_dir: false,
|
||||
is_symlink: false,
|
||||
},
|
||||
content: Some(content),
|
||||
},
|
||||
);
|
||||
state.emit_event(&[path]).await;
|
||||
let file = Arc::new(Mutex::new(FakeFsEntry::File {
|
||||
inode,
|
||||
mtime: SystemTime::now(),
|
||||
content,
|
||||
}));
|
||||
state
|
||||
.write_path(path, move |entry| {
|
||||
match entry {
|
||||
btree_map::Entry::Vacant(e) => {
|
||||
e.insert(file);
|
||||
}
|
||||
btree_map::Entry::Occupied(mut e) => {
|
||||
*e.get_mut() = file;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
state.emit_event(&[path]);
|
||||
}
|
||||
|
||||
pub async fn insert_symlink(&self, path: impl AsRef<Path>, target: PathBuf) {
|
||||
let mut state = self.state.lock().await;
|
||||
let path = path.as_ref();
|
||||
let file = Arc::new(Mutex::new(FakeFsEntry::Symlink { target }));
|
||||
state
|
||||
.write_path(path.as_ref(), move |e| match e {
|
||||
btree_map::Entry::Vacant(e) => {
|
||||
e.insert(file);
|
||||
Ok(())
|
||||
}
|
||||
btree_map::Entry::Occupied(mut e) => {
|
||||
*e.get_mut() = file;
|
||||
Ok(())
|
||||
}
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
state.emit_event(&[path]);
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
|
@ -392,13 +474,22 @@ impl FakeFs {
|
|||
}
|
||||
|
||||
pub async fn files(&self) -> Vec<PathBuf> {
|
||||
self.state
|
||||
.lock()
|
||||
.await
|
||||
.entries
|
||||
.iter()
|
||||
.filter_map(|(path, entry)| entry.content.as_ref().map(|_| path.clone()))
|
||||
.collect()
|
||||
let mut result = Vec::new();
|
||||
let mut queue = collections::VecDeque::new();
|
||||
queue.push_back((PathBuf::from("/"), self.state.lock().await.root.clone()));
|
||||
while let Some((path, entry)) = queue.pop_front() {
|
||||
let e = entry.lock().await;
|
||||
match &*e {
|
||||
FakeFsEntry::File { .. } => result.push(path),
|
||||
FakeFsEntry::Dir { entries, .. } => {
|
||||
for (name, entry) in entries {
|
||||
queue.push_back((path.join(name), entry.clone()));
|
||||
}
|
||||
}
|
||||
FakeFsEntry::Symlink { .. } => {}
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
async fn simulate_random_delay(&self) {
|
||||
|
@ -410,182 +501,207 @@ impl FakeFs {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
impl FakeFsEntry {
|
||||
fn is_file(&self) -> bool {
|
||||
matches!(self, Self::File { .. })
|
||||
}
|
||||
|
||||
fn file_content(&self, path: &Path) -> Result<&String> {
|
||||
if let Self::File { content, .. } = self {
|
||||
Ok(content)
|
||||
} else {
|
||||
Err(anyhow!("not a file: {}", path.display()))
|
||||
}
|
||||
}
|
||||
|
||||
fn set_file_content(&mut self, path: &Path, new_content: String) -> Result<()> {
|
||||
if let Self::File { content, mtime, .. } = self {
|
||||
*mtime = SystemTime::now();
|
||||
*content = new_content;
|
||||
Ok(())
|
||||
} else {
|
||||
Err(anyhow!("not a file: {}", path.display()))
|
||||
}
|
||||
}
|
||||
|
||||
fn dir_entries(
|
||||
&mut self,
|
||||
path: &Path,
|
||||
) -> Result<&mut BTreeMap<String, Arc<Mutex<FakeFsEntry>>>> {
|
||||
if let Self::Dir { entries, .. } = self {
|
||||
Ok(entries)
|
||||
} else {
|
||||
Err(anyhow!("not a directory: {}", path.display()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
#[async_trait::async_trait]
|
||||
impl Fs for FakeFs {
|
||||
async fn create_dir(&self, path: &Path) -> Result<()> {
|
||||
self.simulate_random_delay().await;
|
||||
let state = &mut *self.state.lock().await;
|
||||
let path = normalize_path(path);
|
||||
let mut ancestor_path = PathBuf::new();
|
||||
let mut created_dir_paths = Vec::new();
|
||||
let mut state = self.state.lock().await;
|
||||
|
||||
let mut created_dirs = Vec::new();
|
||||
let mut cur_path = PathBuf::new();
|
||||
for component in path.components() {
|
||||
ancestor_path.push(component);
|
||||
let entry = state
|
||||
.entries
|
||||
.entry(ancestor_path.clone())
|
||||
.or_insert_with(|| {
|
||||
let inode = state.next_inode;
|
||||
state.next_inode += 1;
|
||||
created_dir_paths.push(ancestor_path.clone());
|
||||
FakeFsEntry {
|
||||
metadata: Metadata {
|
||||
cur_path.push(component);
|
||||
if cur_path == Path::new("/") {
|
||||
continue;
|
||||
}
|
||||
|
||||
let inode = state.next_inode;
|
||||
state.next_inode += 1;
|
||||
state
|
||||
.write_path(&cur_path, |entry| {
|
||||
entry.or_insert_with(|| {
|
||||
created_dirs.push(cur_path.clone());
|
||||
Arc::new(Mutex::new(FakeFsEntry::Dir {
|
||||
inode,
|
||||
mtime: SystemTime::now(),
|
||||
is_dir: true,
|
||||
is_symlink: false,
|
||||
},
|
||||
content: None,
|
||||
}
|
||||
});
|
||||
if !entry.metadata.is_dir {
|
||||
return Err(anyhow!(
|
||||
"cannot create directory because {:?} is a file",
|
||||
ancestor_path
|
||||
));
|
||||
}
|
||||
entries: Default::default(),
|
||||
}))
|
||||
});
|
||||
Ok(())
|
||||
})
|
||||
.await?;
|
||||
}
|
||||
state.emit_event(&created_dir_paths).await;
|
||||
|
||||
state.emit_event(&created_dirs);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn create_file(&self, path: &Path, options: CreateOptions) -> Result<()> {
|
||||
self.simulate_random_delay().await;
|
||||
let mut state = self.state.lock().await;
|
||||
let inode = state.next_inode;
|
||||
state.next_inode += 1;
|
||||
let file = Arc::new(Mutex::new(FakeFsEntry::File {
|
||||
inode,
|
||||
mtime: SystemTime::now(),
|
||||
content: String::new(),
|
||||
}));
|
||||
state
|
||||
.write_path(path, |entry| {
|
||||
match entry {
|
||||
btree_map::Entry::Occupied(mut e) => {
|
||||
if options.overwrite {
|
||||
*e.get_mut() = file;
|
||||
} else if !options.ignore_if_exists {
|
||||
return Err(anyhow!("path already exists: {}", path.display()));
|
||||
}
|
||||
}
|
||||
btree_map::Entry::Vacant(e) => {
|
||||
e.insert(file);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.await?;
|
||||
state.emit_event(&[path]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn rename(&self, old_path: &Path, new_path: &Path, options: RenameOptions) -> Result<()> {
|
||||
let old_path = normalize_path(old_path);
|
||||
let new_path = normalize_path(new_path);
|
||||
let mut state = self.state.lock().await;
|
||||
let moved_entry = state
|
||||
.write_path(&old_path, |e| {
|
||||
if let btree_map::Entry::Occupied(e) = e {
|
||||
Ok(e.remove())
|
||||
} else {
|
||||
Err(anyhow!("path does not exist: {}", &old_path.display()))
|
||||
}
|
||||
})
|
||||
.await?;
|
||||
state
|
||||
.write_path(&new_path, |e| {
|
||||
match e {
|
||||
btree_map::Entry::Occupied(mut e) => {
|
||||
if options.overwrite {
|
||||
*e.get_mut() = moved_entry;
|
||||
} else if !options.ignore_if_exists {
|
||||
return Err(anyhow!("path already exists: {}", new_path.display()));
|
||||
}
|
||||
}
|
||||
btree_map::Entry::Vacant(e) => {
|
||||
e.insert(moved_entry);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.await?;
|
||||
state.emit_event(&[old_path, new_path]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn copy_file(&self, source: &Path, target: &Path, options: CopyOptions) -> Result<()> {
|
||||
let source = normalize_path(source);
|
||||
let target = normalize_path(target);
|
||||
let mut state = self.state.lock().await;
|
||||
let source_entry = state.read_path(&source).await?;
|
||||
let content = source_entry.lock().await.file_content(&source)?.clone();
|
||||
let entry = state
|
||||
.write_path(&target, |e| match e {
|
||||
btree_map::Entry::Occupied(e) => {
|
||||
if options.overwrite {
|
||||
Ok(Some(e.get().clone()))
|
||||
} else if !options.ignore_if_exists {
|
||||
return Err(anyhow!("{target:?} already exists"));
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
btree_map::Entry::Vacant(e) => Ok(Some(
|
||||
e.insert(Arc::new(Mutex::new(FakeFsEntry::File {
|
||||
inode: 0,
|
||||
mtime: SystemTime::now(),
|
||||
content: String::new(),
|
||||
})))
|
||||
.clone(),
|
||||
)),
|
||||
})
|
||||
.await?;
|
||||
if let Some(entry) = entry {
|
||||
entry.lock().await.set_file_content(&target, content)?;
|
||||
}
|
||||
state.emit_event(&[target]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn remove_dir(&self, path: &Path, options: RemoveOptions) -> Result<()> {
|
||||
let path = normalize_path(path);
|
||||
state.validate_path(&path)?;
|
||||
if let Some(entry) = state.entries.get_mut(&path) {
|
||||
if entry.metadata.is_dir || entry.metadata.is_symlink {
|
||||
return Err(anyhow!(
|
||||
"cannot create file because {:?} is a dir or a symlink",
|
||||
path
|
||||
));
|
||||
}
|
||||
let parent_path = path
|
||||
.parent()
|
||||
.ok_or_else(|| anyhow!("cannot remove the root"))?;
|
||||
let base_name = path.file_name().unwrap();
|
||||
|
||||
if options.overwrite {
|
||||
entry.metadata.mtime = SystemTime::now();
|
||||
entry.content = Some(Default::default());
|
||||
} else if !options.ignore_if_exists {
|
||||
return Err(anyhow!(
|
||||
"cannot create file because {:?} already exists",
|
||||
&path
|
||||
));
|
||||
}
|
||||
} else {
|
||||
let inode = state.next_inode;
|
||||
state.next_inode += 1;
|
||||
let entry = FakeFsEntry {
|
||||
metadata: Metadata {
|
||||
inode,
|
||||
mtime: SystemTime::now(),
|
||||
is_dir: false,
|
||||
is_symlink: false,
|
||||
},
|
||||
content: Some(Default::default()),
|
||||
};
|
||||
state.entries.insert(path.to_path_buf(), entry);
|
||||
}
|
||||
state.emit_event(&[path]).await;
|
||||
let state = self.state.lock().await;
|
||||
let parent_entry = state.read_path(parent_path).await?;
|
||||
let mut parent_entry = parent_entry.lock().await;
|
||||
let entry = parent_entry
|
||||
.dir_entries(parent_path)?
|
||||
.entry(base_name.to_str().unwrap().into());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn rename(&self, source: &Path, target: &Path, options: RenameOptions) -> Result<()> {
|
||||
let source = normalize_path(source);
|
||||
let target = normalize_path(target);
|
||||
|
||||
let mut state = self.state.lock().await;
|
||||
state.validate_path(&source)?;
|
||||
state.validate_path(&target)?;
|
||||
|
||||
if !options.overwrite && state.entries.contains_key(&target) {
|
||||
if options.ignore_if_exists {
|
||||
return Ok(());
|
||||
} else {
|
||||
return Err(anyhow!("{target:?} already exists"));
|
||||
}
|
||||
}
|
||||
|
||||
let mut removed = Vec::new();
|
||||
state.entries.retain(|path, entry| {
|
||||
if let Ok(relative_path) = path.strip_prefix(&source) {
|
||||
removed.push((relative_path.to_path_buf(), entry.clone()));
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
|
||||
for (relative_path, entry) in removed {
|
||||
let new_path = normalize_path(&target.join(relative_path));
|
||||
state.entries.insert(new_path, entry);
|
||||
}
|
||||
|
||||
state.emit_event(&[source, target]).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn copy(&self, source: &Path, target: &Path, options: CopyOptions) -> Result<()> {
|
||||
let source = normalize_path(source);
|
||||
let target = normalize_path(target);
|
||||
|
||||
let mut state = self.state.lock().await;
|
||||
state.validate_path(&source)?;
|
||||
state.validate_path(&target)?;
|
||||
|
||||
if !options.overwrite && state.entries.contains_key(&target) {
|
||||
if options.ignore_if_exists {
|
||||
return Ok(());
|
||||
} else {
|
||||
return Err(anyhow!("{target:?} already exists"));
|
||||
}
|
||||
}
|
||||
|
||||
let mut new_entries = Vec::new();
|
||||
for (path, entry) in &state.entries {
|
||||
if let Ok(relative_path) = path.strip_prefix(&source) {
|
||||
new_entries.push((relative_path.to_path_buf(), entry.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
let mut events = Vec::new();
|
||||
for (relative_path, entry) in new_entries {
|
||||
let new_path = normalize_path(&target.join(relative_path));
|
||||
events.push(new_path.clone());
|
||||
state.entries.insert(new_path, entry);
|
||||
}
|
||||
|
||||
state.emit_event(&events).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn remove_dir(&self, dir_path: &Path, options: RemoveOptions) -> Result<()> {
|
||||
let dir_path = normalize_path(dir_path);
|
||||
let mut state = self.state.lock().await;
|
||||
state.validate_path(&dir_path)?;
|
||||
if let Some(entry) = state.entries.get(&dir_path) {
|
||||
if !entry.metadata.is_dir {
|
||||
return Err(anyhow!(
|
||||
"cannot remove {dir_path:?} because it is not a dir"
|
||||
));
|
||||
}
|
||||
|
||||
if !options.recursive {
|
||||
let descendants = state
|
||||
.entries
|
||||
.keys()
|
||||
.filter(|path| path.starts_with(path))
|
||||
.count();
|
||||
if descendants > 1 {
|
||||
return Err(anyhow!("{dir_path:?} is not empty"));
|
||||
match entry {
|
||||
btree_map::Entry::Vacant(_) => {
|
||||
if !options.ignore_if_not_exists {
|
||||
return Err(anyhow!("{path:?} does not exist"));
|
||||
}
|
||||
}
|
||||
|
||||
state.entries.retain(|path, _| !path.starts_with(&dir_path));
|
||||
state.emit_event(&[dir_path]).await;
|
||||
} else if !options.ignore_if_not_exists {
|
||||
return Err(anyhow!("{dir_path:?} does not exist"));
|
||||
btree_map::Entry::Occupied(e) => {
|
||||
{
|
||||
let mut entry = e.get().lock().await;
|
||||
let children = entry.dir_entries(&path)?;
|
||||
if !options.recursive && !children.is_empty() {
|
||||
return Err(anyhow!("{path:?} is not empty"));
|
||||
}
|
||||
}
|
||||
e.remove();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
@ -593,18 +709,28 @@ impl Fs for FakeFs {
|
|||
|
||||
async fn remove_file(&self, path: &Path, options: RemoveOptions) -> Result<()> {
|
||||
let path = normalize_path(path);
|
||||
let parent_path = path
|
||||
.parent()
|
||||
.ok_or_else(|| anyhow!("cannot remove the root"))?;
|
||||
let base_name = path.file_name().unwrap();
|
||||
let mut state = self.state.lock().await;
|
||||
state.validate_path(&path)?;
|
||||
if let Some(entry) = state.entries.get(&path) {
|
||||
if entry.metadata.is_dir {
|
||||
return Err(anyhow!("cannot remove {path:?} because it is not a file"));
|
||||
let parent_entry = state.read_path(parent_path).await?;
|
||||
let mut parent_entry = parent_entry.lock().await;
|
||||
let entry = parent_entry
|
||||
.dir_entries(parent_path)?
|
||||
.entry(base_name.to_str().unwrap().into());
|
||||
match entry {
|
||||
btree_map::Entry::Vacant(_) => {
|
||||
if !options.ignore_if_not_exists {
|
||||
return Err(anyhow!("{path:?} does not exist"));
|
||||
}
|
||||
}
|
||||
btree_map::Entry::Occupied(e) => {
|
||||
e.get().lock().await.file_content(&path)?;
|
||||
e.remove();
|
||||
}
|
||||
|
||||
state.entries.remove(&path);
|
||||
state.emit_event(&[path]).await;
|
||||
} else if !options.ignore_if_not_exists {
|
||||
return Err(anyhow!("{path:?} does not exist"));
|
||||
}
|
||||
state.emit_event(&[path]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -617,86 +743,84 @@ impl Fs for FakeFs {
|
|||
let path = normalize_path(path);
|
||||
self.simulate_random_delay().await;
|
||||
let state = self.state.lock().await;
|
||||
let text = state
|
||||
.entries
|
||||
.get(&path)
|
||||
.and_then(|e| e.content.as_ref())
|
||||
.ok_or_else(|| anyhow!("file {:?} does not exist", path))?;
|
||||
Ok(text.clone())
|
||||
let entry = state.read_path(&path).await?;
|
||||
let entry = entry.lock().await;
|
||||
entry.file_content(&path).cloned()
|
||||
}
|
||||
|
||||
async fn save(&self, path: &Path, text: &Rope, line_ending: LineEnding) -> Result<()> {
|
||||
self.simulate_random_delay().await;
|
||||
let mut state = self.state.lock().await;
|
||||
let path = normalize_path(path);
|
||||
state.validate_path(&path)?;
|
||||
let content = chunks(text, line_ending).collect();
|
||||
if let Some(entry) = state.entries.get_mut(&path) {
|
||||
if entry.metadata.is_dir {
|
||||
Err(anyhow!("cannot overwrite a directory with a file"))
|
||||
} else {
|
||||
entry.content = Some(content);
|
||||
entry.metadata.mtime = SystemTime::now();
|
||||
state.emit_event(&[path]).await;
|
||||
Ok(())
|
||||
}
|
||||
} else {
|
||||
let inode = state.next_inode;
|
||||
state.next_inode += 1;
|
||||
let entry = FakeFsEntry {
|
||||
metadata: Metadata {
|
||||
inode,
|
||||
mtime: SystemTime::now(),
|
||||
is_dir: false,
|
||||
is_symlink: false,
|
||||
},
|
||||
content: Some(content),
|
||||
};
|
||||
state.entries.insert(path.to_path_buf(), entry);
|
||||
state.emit_event(&[path]).await;
|
||||
Ok(())
|
||||
}
|
||||
self.insert_file(path, content).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn canonicalize(&self, path: &Path) -> Result<PathBuf> {
|
||||
let path = normalize_path(path);
|
||||
self.simulate_random_delay().await;
|
||||
Ok(normalize_path(path))
|
||||
let state = self.state.lock().await;
|
||||
if let Some((_, real_path)) = state.try_read_path(&path).await {
|
||||
Ok(real_path)
|
||||
} else {
|
||||
Err(anyhow!("path does not exist: {}", path.display()))
|
||||
}
|
||||
}
|
||||
|
||||
async fn is_file(&self, path: &Path) -> bool {
|
||||
let path = normalize_path(path);
|
||||
self.simulate_random_delay().await;
|
||||
let state = self.state.lock().await;
|
||||
state
|
||||
.entries
|
||||
.get(&path)
|
||||
.map_or(false, |entry| !entry.metadata.is_dir)
|
||||
if let Some((entry, _)) = state.try_read_path(&path).await {
|
||||
entry.lock().await.is_file()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
async fn metadata(&self, path: &Path) -> Result<Option<Metadata>> {
|
||||
self.simulate_random_delay().await;
|
||||
let state = self.state.lock().await;
|
||||
let path = normalize_path(path);
|
||||
Ok(state.entries.get(&path).map(|entry| entry.metadata.clone()))
|
||||
let state = self.state.lock().await;
|
||||
if let Some((entry, real_path)) = state.try_read_path(&path).await {
|
||||
let entry = entry.lock().await;
|
||||
let is_symlink = real_path != path;
|
||||
|
||||
Ok(Some(match &*entry {
|
||||
FakeFsEntry::File { inode, mtime, .. } => Metadata {
|
||||
inode: *inode,
|
||||
mtime: *mtime,
|
||||
is_dir: false,
|
||||
is_symlink,
|
||||
},
|
||||
FakeFsEntry::Dir { inode, mtime, .. } => Metadata {
|
||||
inode: *inode,
|
||||
mtime: *mtime,
|
||||
is_dir: true,
|
||||
is_symlink,
|
||||
},
|
||||
FakeFsEntry::Symlink { .. } => unreachable!(),
|
||||
}))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
async fn read_dir(
|
||||
&self,
|
||||
abs_path: &Path,
|
||||
path: &Path,
|
||||
) -> Result<Pin<Box<dyn Send + Stream<Item = Result<PathBuf>>>>> {
|
||||
use futures::{future, stream};
|
||||
self.simulate_random_delay().await;
|
||||
let path = normalize_path(path);
|
||||
let state = self.state.lock().await;
|
||||
let abs_path = normalize_path(abs_path);
|
||||
Ok(Box::pin(stream::iter(state.entries.clone()).filter_map(
|
||||
move |(child_path, _)| {
|
||||
future::ready(if child_path.parent() == Some(&abs_path) {
|
||||
Some(Ok(child_path))
|
||||
} else {
|
||||
None
|
||||
})
|
||||
},
|
||||
)))
|
||||
let entry = state.read_path(&path).await?;
|
||||
let mut entry = entry.lock().await;
|
||||
let children = entry.dir_entries(&path)?;
|
||||
let paths = children
|
||||
.keys()
|
||||
.map(|file_name| Ok(path.join(file_name)))
|
||||
.collect::<Vec<_>>();
|
||||
Ok(Box::pin(futures::stream::iter(paths)))
|
||||
}
|
||||
|
||||
async fn watch(
|
||||
|
@ -773,3 +897,112 @@ pub fn normalize_path(path: &Path) -> PathBuf {
|
|||
}
|
||||
ret
|
||||
}
|
||||
|
||||
pub fn copy_recursive<'a>(
|
||||
fs: &'a dyn Fs,
|
||||
source: &'a Path,
|
||||
target: &'a Path,
|
||||
options: CopyOptions,
|
||||
) -> BoxFuture<'a, Result<()>> {
|
||||
use futures::future::FutureExt;
|
||||
|
||||
async move {
|
||||
let metadata = fs
|
||||
.metadata(source)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("path does not exist: {}", source.display()))?;
|
||||
if metadata.is_dir {
|
||||
if !options.overwrite && fs.metadata(target).await.is_ok() {
|
||||
if options.ignore_if_exists {
|
||||
return Ok(());
|
||||
} else {
|
||||
return Err(anyhow!("{target:?} already exists"));
|
||||
}
|
||||
}
|
||||
|
||||
let _ = fs
|
||||
.remove_dir(
|
||||
target,
|
||||
RemoveOptions {
|
||||
recursive: true,
|
||||
ignore_if_not_exists: true,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
fs.create_dir(target).await?;
|
||||
let mut children = fs.read_dir(source).await?;
|
||||
while let Some(child_path) = children.next().await {
|
||||
if let Ok(child_path) = child_path {
|
||||
if let Some(file_name) = child_path.file_name() {
|
||||
let child_target_path = target.join(file_name);
|
||||
copy_recursive(fs, &child_path, &child_target_path, options).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
} else {
|
||||
fs.copy_file(source, target, options).await
|
||||
}
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use gpui::TestAppContext;
|
||||
use serde_json::json;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_fake_fs(cx: &mut TestAppContext) {
|
||||
let fs = FakeFs::new(cx.background());
|
||||
|
||||
fs.insert_tree(
|
||||
"/root",
|
||||
json!({
|
||||
"dir1": {
|
||||
"a": "A",
|
||||
"b": "B"
|
||||
},
|
||||
"dir2": {
|
||||
"c": "C",
|
||||
"dir3": {
|
||||
"d": "D"
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
fs.files().await,
|
||||
vec![
|
||||
PathBuf::from("/root/dir1/a"),
|
||||
PathBuf::from("/root/dir1/b"),
|
||||
PathBuf::from("/root/dir2/c"),
|
||||
PathBuf::from("/root/dir2/dir3/d"),
|
||||
]
|
||||
);
|
||||
|
||||
fs.insert_symlink("/root/dir2/link-to-dir3", "./dir3".into())
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
fs.canonicalize("/root/dir2/link-to-dir3".as_ref())
|
||||
.await
|
||||
.unwrap(),
|
||||
PathBuf::from("/root/dir2/dir3"),
|
||||
);
|
||||
assert_eq!(
|
||||
fs.canonicalize("/root/dir2/link-to-dir3/d".as_ref())
|
||||
.await
|
||||
.unwrap(),
|
||||
PathBuf::from("/root/dir2/dir3/d"),
|
||||
);
|
||||
assert_eq!(
|
||||
fs.load("/root/dir2/link-to-dir3/d".as_ref()).await.unwrap(),
|
||||
"D",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use crate::{ProjectEntryId, RemoveOptions};
|
||||
use crate::{copy_recursive, ProjectEntryId, RemoveOptions};
|
||||
|
||||
use super::{
|
||||
fs::{self, Fs},
|
||||
|
@ -47,7 +47,7 @@ use std::{
|
|||
task::Poll,
|
||||
time::{Duration, SystemTime},
|
||||
};
|
||||
use sum_tree::{Bias, Edit, SeekTarget, SumTree, TreeMap};
|
||||
use sum_tree::{Bias, Edit, SeekTarget, SumTree, TreeMap, TreeSet};
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
|
||||
lazy_static! {
|
||||
|
@ -731,8 +731,13 @@ impl LocalWorktree {
|
|||
let fs = self.fs.clone();
|
||||
let abs_new_path = abs_new_path.clone();
|
||||
async move {
|
||||
fs.copy(&abs_old_path, &abs_new_path, Default::default())
|
||||
.await
|
||||
copy_recursive(
|
||||
fs.as_ref(),
|
||||
&abs_old_path,
|
||||
&abs_new_path,
|
||||
Default::default(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -1486,6 +1491,16 @@ impl LocalSnapshot {
|
|||
}
|
||||
}
|
||||
|
||||
fn ancestor_inodes_for_path(&self, path: &Path) -> TreeSet<u64> {
|
||||
let mut inodes = TreeSet::default();
|
||||
for ancestor in path.ancestors().skip(1) {
|
||||
if let Some(entry) = self.entry_for_path(ancestor) {
|
||||
inodes.insert(entry.inode);
|
||||
}
|
||||
}
|
||||
inodes
|
||||
}
|
||||
|
||||
fn ignore_stack_for_abs_path(&self, abs_path: &Path, is_dir: bool) -> Arc<IgnoreStack> {
|
||||
let mut new_ignores = Vec::new();
|
||||
for ancestor in abs_path.ancestors().skip(1) {
|
||||
|
@ -1646,11 +1661,10 @@ impl language::File for File {
|
|||
|
||||
/// Returns the last component of this handle's absolute path. If this handle refers to the root
|
||||
/// of its worktree, then this method will return the name of the worktree itself.
|
||||
fn file_name(&self, cx: &AppContext) -> OsString {
|
||||
fn file_name<'a>(&'a self, cx: &'a AppContext) -> &'a OsStr {
|
||||
self.path
|
||||
.file_name()
|
||||
.map(|name| name.into())
|
||||
.unwrap_or_else(|| OsString::from(&self.worktree.read(cx).root_name))
|
||||
.unwrap_or_else(|| OsStr::new(&self.worktree.read(cx).root_name))
|
||||
}
|
||||
|
||||
fn is_deleted(&self) -> bool {
|
||||
|
@ -2049,14 +2063,16 @@ impl BackgroundScanner {
|
|||
async fn scan_dirs(&mut self) -> Result<()> {
|
||||
let root_char_bag;
|
||||
let root_abs_path;
|
||||
let next_entry_id;
|
||||
let root_inode;
|
||||
let is_dir;
|
||||
let next_entry_id;
|
||||
{
|
||||
let snapshot = self.snapshot.lock();
|
||||
root_char_bag = snapshot.root_char_bag;
|
||||
root_abs_path = snapshot.abs_path.clone();
|
||||
root_inode = snapshot.root_entry().map(|e| e.inode);
|
||||
is_dir = snapshot.root_entry().map_or(false, |e| e.is_dir());
|
||||
next_entry_id = snapshot.next_entry_id.clone();
|
||||
is_dir = snapshot.root_entry().map_or(false, |e| e.is_dir())
|
||||
};
|
||||
|
||||
// Populate ignores above the root.
|
||||
|
@ -2084,12 +2100,18 @@ impl BackgroundScanner {
|
|||
|
||||
if is_dir {
|
||||
let path: Arc<Path> = Arc::from(Path::new(""));
|
||||
let mut ancestor_inodes = TreeSet::default();
|
||||
if let Some(root_inode) = root_inode {
|
||||
ancestor_inodes.insert(root_inode);
|
||||
}
|
||||
|
||||
let (tx, rx) = channel::unbounded();
|
||||
self.executor
|
||||
.block(tx.send(ScanJob {
|
||||
abs_path: root_abs_path.to_path_buf(),
|
||||
path,
|
||||
ignore_stack,
|
||||
ancestor_inodes,
|
||||
scan_queue: tx.clone(),
|
||||
}))
|
||||
.unwrap();
|
||||
|
@ -2191,24 +2213,30 @@ impl BackgroundScanner {
|
|||
root_char_bag,
|
||||
);
|
||||
|
||||
if child_metadata.is_dir {
|
||||
if child_entry.is_dir() {
|
||||
let is_ignored = ignore_stack.is_abs_path_ignored(&child_abs_path, true);
|
||||
child_entry.is_ignored = is_ignored;
|
||||
new_entries.push(child_entry);
|
||||
new_jobs.push(ScanJob {
|
||||
abs_path: child_abs_path,
|
||||
path: child_path,
|
||||
ignore_stack: if is_ignored {
|
||||
IgnoreStack::all()
|
||||
} else {
|
||||
ignore_stack.clone()
|
||||
},
|
||||
scan_queue: job.scan_queue.clone(),
|
||||
});
|
||||
|
||||
if !job.ancestor_inodes.contains(&child_entry.inode) {
|
||||
let mut ancestor_inodes = job.ancestor_inodes.clone();
|
||||
ancestor_inodes.insert(child_entry.inode);
|
||||
new_jobs.push(ScanJob {
|
||||
abs_path: child_abs_path,
|
||||
path: child_path,
|
||||
ignore_stack: if is_ignored {
|
||||
IgnoreStack::all()
|
||||
} else {
|
||||
ignore_stack.clone()
|
||||
},
|
||||
ancestor_inodes,
|
||||
scan_queue: job.scan_queue.clone(),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
child_entry.is_ignored = ignore_stack.is_abs_path_ignored(&child_abs_path, false);
|
||||
new_entries.push(child_entry);
|
||||
};
|
||||
}
|
||||
|
||||
new_entries.push(child_entry);
|
||||
}
|
||||
|
||||
self.snapshot
|
||||
|
@ -2287,12 +2315,16 @@ impl BackgroundScanner {
|
|||
);
|
||||
fs_entry.is_ignored = ignore_stack.is_all();
|
||||
snapshot.insert_entry(fs_entry, self.fs.as_ref());
|
||||
if metadata.is_dir {
|
||||
|
||||
let mut ancestor_inodes = snapshot.ancestor_inodes_for_path(&path);
|
||||
if metadata.is_dir && !ancestor_inodes.contains(&metadata.inode) {
|
||||
ancestor_inodes.insert(metadata.inode);
|
||||
self.executor
|
||||
.block(scan_queue_tx.send(ScanJob {
|
||||
abs_path,
|
||||
path,
|
||||
ignore_stack,
|
||||
ancestor_inodes,
|
||||
scan_queue: scan_queue_tx.clone(),
|
||||
}))
|
||||
.unwrap();
|
||||
|
@ -2454,6 +2486,7 @@ struct ScanJob {
|
|||
path: Arc<Path>,
|
||||
ignore_stack: Arc<IgnoreStack>,
|
||||
scan_queue: Sender<ScanJob>,
|
||||
ancestor_inodes: TreeSet<u64>,
|
||||
}
|
||||
|
||||
struct UpdateIgnoreStatusJob {
|
||||
|
@ -2740,7 +2773,7 @@ mod tests {
|
|||
use anyhow::Result;
|
||||
use client::test::FakeHttpClient;
|
||||
use fs::RealFs;
|
||||
use gpui::TestAppContext;
|
||||
use gpui::{executor::Deterministic, TestAppContext};
|
||||
use rand::prelude::*;
|
||||
use serde_json::json;
|
||||
use std::{
|
||||
|
@ -2808,6 +2841,87 @@ mod tests {
|
|||
})
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_circular_symlinks(executor: Arc<Deterministic>, cx: &mut TestAppContext) {
|
||||
let fs = FakeFs::new(cx.background());
|
||||
fs.insert_tree(
|
||||
"/root",
|
||||
json!({
|
||||
"lib": {
|
||||
"a": {
|
||||
"a.txt": ""
|
||||
},
|
||||
"b": {
|
||||
"b.txt": ""
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
fs.insert_symlink("/root/lib/a/lib", "..".into()).await;
|
||||
fs.insert_symlink("/root/lib/b/lib", "..".into()).await;
|
||||
|
||||
let http_client = FakeHttpClient::with_404_response();
|
||||
let client = Client::new(http_client);
|
||||
let tree = Worktree::local(
|
||||
client,
|
||||
Arc::from(Path::new("/root")),
|
||||
true,
|
||||
fs.clone(),
|
||||
Default::default(),
|
||||
&mut cx.to_async(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
|
||||
.await;
|
||||
|
||||
tree.read_with(cx, |tree, _| {
|
||||
assert_eq!(
|
||||
tree.entries(false)
|
||||
.map(|entry| entry.path.as_ref())
|
||||
.collect::<Vec<_>>(),
|
||||
vec![
|
||||
Path::new(""),
|
||||
Path::new("lib"),
|
||||
Path::new("lib/a"),
|
||||
Path::new("lib/a/a.txt"),
|
||||
Path::new("lib/a/lib"),
|
||||
Path::new("lib/b"),
|
||||
Path::new("lib/b/b.txt"),
|
||||
Path::new("lib/b/lib"),
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
fs.rename(
|
||||
Path::new("/root/lib/a/lib"),
|
||||
Path::new("/root/lib/a/lib-2"),
|
||||
Default::default(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
executor.run_until_parked();
|
||||
tree.read_with(cx, |tree, _| {
|
||||
assert_eq!(
|
||||
tree.entries(false)
|
||||
.map(|entry| entry.path.as_ref())
|
||||
.collect::<Vec<_>>(),
|
||||
vec![
|
||||
Path::new(""),
|
||||
Path::new("lib"),
|
||||
Path::new("lib/a"),
|
||||
Path::new("lib/a/a.txt"),
|
||||
Path::new("lib/a/lib-2"),
|
||||
Path::new("lib/b"),
|
||||
Path::new("lib/b/b.txt"),
|
||||
Path::new("lib/b/lib"),
|
||||
]
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_rescan_with_gitignore(cx: &mut TestAppContext) {
|
||||
let parent_dir = temp_tree(json!({
|
||||
|
|
|
@ -11,8 +11,9 @@ use gpui::{
|
|||
geometry::vector::Vector2F,
|
||||
impl_internal_actions, keymap,
|
||||
platform::CursorStyle,
|
||||
AppContext, ClipboardItem, Element, ElementBox, Entity, ModelHandle, MutableAppContext,
|
||||
PromptLevel, RenderContext, Task, View, ViewContext, ViewHandle,
|
||||
AppContext, ClipboardItem, Element, ElementBox, Entity, ModelHandle, MouseButton,
|
||||
MouseButtonEvent, MutableAppContext, PromptLevel, RenderContext, Task, View, ViewContext,
|
||||
ViewHandle,
|
||||
};
|
||||
use menu::{Confirm, SelectNext, SelectPrev};
|
||||
use project::{Entry, EntryKind, Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId};
|
||||
|
@ -184,6 +185,20 @@ impl ProjectPanel {
|
|||
)
|
||||
});
|
||||
|
||||
cx.observe_focus(&filename_editor, |this, _, is_focused, cx| {
|
||||
if !is_focused {
|
||||
if this
|
||||
.edit_state
|
||||
.as_ref()
|
||||
.map_or(false, |state| state.processing_filename.is_none())
|
||||
{
|
||||
this.edit_state = None;
|
||||
this.update_visible_entries(None, cx);
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
let mut this = Self {
|
||||
project: project.clone(),
|
||||
list: Default::default(),
|
||||
|
@ -1054,19 +1069,25 @@ impl ProjectPanel {
|
|||
.with_padding_left(padding)
|
||||
.boxed()
|
||||
})
|
||||
.on_click(move |_, click_count, cx| {
|
||||
if kind == EntryKind::Dir {
|
||||
cx.dispatch_action(ToggleExpanded(entry_id))
|
||||
} else {
|
||||
cx.dispatch_action(Open {
|
||||
entry_id,
|
||||
change_focus: click_count > 1,
|
||||
})
|
||||
}
|
||||
})
|
||||
.on_right_mouse_down(move |position, cx| {
|
||||
cx.dispatch_action(DeployContextMenu { entry_id, position })
|
||||
})
|
||||
.on_click(
|
||||
MouseButton::Left,
|
||||
move |MouseButtonEvent { click_count, .. }, cx| {
|
||||
if kind == EntryKind::Dir {
|
||||
cx.dispatch_action(ToggleExpanded(entry_id))
|
||||
} else {
|
||||
cx.dispatch_action(Open {
|
||||
entry_id,
|
||||
change_focus: click_count > 1,
|
||||
})
|
||||
}
|
||||
},
|
||||
)
|
||||
.on_mouse_down(
|
||||
MouseButton::Right,
|
||||
move |MouseButtonEvent { position, .. }, cx| {
|
||||
cx.dispatch_action(DeployContextMenu { entry_id, position })
|
||||
},
|
||||
)
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.boxed()
|
||||
}
|
||||
|
@ -1113,13 +1134,16 @@ impl View for ProjectPanel {
|
|||
.expanded()
|
||||
.boxed()
|
||||
})
|
||||
.on_right_mouse_down(move |position, cx| {
|
||||
// When deploying the context menu anywhere below the last project entry,
|
||||
// act as if the user clicked the root of the last worktree.
|
||||
if let Some(entry_id) = last_worktree_root_id {
|
||||
cx.dispatch_action(DeployContextMenu { entry_id, position })
|
||||
}
|
||||
})
|
||||
.on_mouse_down(
|
||||
MouseButton::Right,
|
||||
move |MouseButtonEvent { position, .. }, cx| {
|
||||
// When deploying the context menu anywhere below the last project entry,
|
||||
// act as if the user clicked the root of the last worktree.
|
||||
if let Some(entry_id) = last_worktree_root_id {
|
||||
cx.dispatch_action(DeployContextMenu { entry_id, position })
|
||||
}
|
||||
},
|
||||
)
|
||||
.boxed(),
|
||||
)
|
||||
.with_child(ChildView::new(&self.context_menu).boxed())
|
||||
|
@ -1544,6 +1568,41 @@ mod tests {
|
|||
" .dockerignore",
|
||||
]
|
||||
);
|
||||
|
||||
panel.update(cx, |panel, cx| panel.rename(&Default::default(), cx));
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..10, cx),
|
||||
&[
|
||||
"v root1",
|
||||
" > .git",
|
||||
" > a",
|
||||
" v b",
|
||||
" > [EDITOR: '3'] <== selected",
|
||||
" > 4",
|
||||
" > new-dir",
|
||||
" a-different-filename",
|
||||
" > C",
|
||||
" .dockerignore",
|
||||
]
|
||||
);
|
||||
|
||||
// Dismiss the rename editor when it loses focus.
|
||||
workspace.update(cx, |_, cx| cx.focus_self());
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..10, cx),
|
||||
&[
|
||||
"v root1",
|
||||
" > .git",
|
||||
" > a",
|
||||
" v b",
|
||||
" > 3 <== selected",
|
||||
" > 4",
|
||||
" > new-dir",
|
||||
" a-different-filename",
|
||||
" > C",
|
||||
" .dockerignore",
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
fn toggle_expand_dir(
|
||||
|
|
|
@ -7,8 +7,8 @@ use collections::HashMap;
|
|||
use editor::{Anchor, Autoscroll, Editor};
|
||||
use gpui::{
|
||||
actions, elements::*, impl_actions, platform::CursorStyle, Action, AppContext, Entity,
|
||||
MutableAppContext, RenderContext, Subscription, Task, View, ViewContext, ViewHandle,
|
||||
WeakViewHandle,
|
||||
MouseButton, MutableAppContext, RenderContext, Subscription, Task, View, ViewContext,
|
||||
ViewHandle, WeakViewHandle,
|
||||
};
|
||||
use language::OffsetRangeExt;
|
||||
use project::search::SearchQuery;
|
||||
|
@ -285,7 +285,9 @@ impl BufferSearchBar {
|
|||
.with_style(style.container)
|
||||
.boxed()
|
||||
})
|
||||
.on_click(move |_, _, cx| cx.dispatch_any_action(option.to_toggle_action()))
|
||||
.on_click(MouseButton::Left, move |_, cx| {
|
||||
cx.dispatch_any_action(option.to_toggle_action())
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.with_tooltip::<Self, _>(
|
||||
option as usize,
|
||||
|
@ -330,9 +332,9 @@ impl BufferSearchBar {
|
|||
.with_style(style.container)
|
||||
.boxed()
|
||||
})
|
||||
.on_click({
|
||||
.on_click(MouseButton::Left, {
|
||||
let action = action.boxed_clone();
|
||||
move |_, _, cx| cx.dispatch_any_action(action.boxed_clone())
|
||||
move |_, cx| cx.dispatch_any_action(action.boxed_clone())
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.with_tooltip::<NavButton, _>(
|
||||
|
|
|
@ -4,11 +4,11 @@ use crate::{
|
|||
ToggleWholeWord,
|
||||
};
|
||||
use collections::HashMap;
|
||||
use editor::{Anchor, Autoscroll, Editor, MultiBuffer, SelectAll};
|
||||
use editor::{Anchor, Autoscroll, Editor, MultiBuffer, SelectAll, MAX_TAB_TITLE_LEN};
|
||||
use gpui::{
|
||||
actions, elements::*, platform::CursorStyle, Action, AppContext, ElementBox, Entity,
|
||||
ModelContext, ModelHandle, MutableAppContext, RenderContext, Subscription, Task, View,
|
||||
ViewContext, ViewHandle, WeakModelHandle, WeakViewHandle,
|
||||
ModelContext, ModelHandle, MouseButton, MutableAppContext, RenderContext, Subscription, Task,
|
||||
View, ViewContext, ViewHandle, WeakModelHandle, WeakViewHandle,
|
||||
};
|
||||
use menu::Confirm;
|
||||
use project::{search::SearchQuery, Project};
|
||||
|
@ -26,8 +26,6 @@ use workspace::{
|
|||
|
||||
actions!(project_search, [Deploy, SearchInNew, ToggleFocus]);
|
||||
|
||||
const MAX_TAB_TITLE_LEN: usize = 24;
|
||||
|
||||
#[derive(Default)]
|
||||
struct ActiveSearches(HashMap<WeakModelHandle<Project>, WeakViewHandle<ProjectSearchView>>);
|
||||
|
||||
|
@ -220,7 +218,12 @@ impl Item for ProjectSearchView {
|
|||
.update(cx, |editor, cx| editor.deactivated(cx));
|
||||
}
|
||||
|
||||
fn tab_content(&self, tab_theme: &theme::Tab, cx: &gpui::AppContext) -> ElementBox {
|
||||
fn tab_content(
|
||||
&self,
|
||||
_detail: Option<usize>,
|
||||
tab_theme: &theme::Tab,
|
||||
cx: &gpui::AppContext,
|
||||
) -> ElementBox {
|
||||
let settings = cx.global::<Settings>();
|
||||
let search_theme = &settings.theme.search;
|
||||
Flex::row()
|
||||
|
@ -732,9 +735,9 @@ impl ProjectSearchBar {
|
|||
.with_style(style.container)
|
||||
.boxed()
|
||||
})
|
||||
.on_click({
|
||||
.on_click(MouseButton::Left, {
|
||||
let action = action.boxed_clone();
|
||||
move |_, _, cx| cx.dispatch_any_action(action.boxed_clone())
|
||||
move |_, cx| cx.dispatch_any_action(action.boxed_clone())
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.with_tooltip::<NavButton, _>(
|
||||
|
@ -767,7 +770,9 @@ impl ProjectSearchBar {
|
|||
.with_style(style.container)
|
||||
.boxed()
|
||||
})
|
||||
.on_click(move |_, _, cx| cx.dispatch_any_action(option.to_toggle_action()))
|
||||
.on_click(MouseButton::Left, move |_, cx| {
|
||||
cx.dispatch_any_action(option.to_toggle_action())
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.with_tooltip::<Self, _>(
|
||||
option as usize,
|
||||
|
|
|
@ -22,13 +22,16 @@ pub use keymap_file::{keymap_file_json_schema, KeymapFileContent};
|
|||
pub struct Settings {
|
||||
pub projects_online_by_default: bool,
|
||||
pub buffer_font_family: FamilyId,
|
||||
pub buffer_font_size: f32,
|
||||
pub default_buffer_font_size: f32,
|
||||
pub buffer_font_size: f32,
|
||||
pub hover_popover_enabled: bool,
|
||||
pub show_completions_on_input: bool,
|
||||
pub vim_mode: bool,
|
||||
pub autosave: Autosave,
|
||||
pub editor_defaults: EditorSettings,
|
||||
pub editor_overrides: EditorSettings,
|
||||
pub terminal_defaults: TerminalSettings,
|
||||
pub terminal_overrides: TerminalSettings,
|
||||
pub language_defaults: HashMap<Arc<str>, EditorSettings>,
|
||||
pub language_overrides: HashMap<Arc<str>, EditorSettings>,
|
||||
pub theme: Arc<Theme>,
|
||||
|
@ -72,6 +75,38 @@ pub enum Autosave {
|
|||
OnWindowChange,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, JsonSchema)]
|
||||
pub struct TerminalSettings {
|
||||
pub shell: Option<Shell>,
|
||||
pub working_directory: Option<WorkingDirectory>,
|
||||
pub font_size: Option<f32>,
|
||||
pub font_family: Option<String>,
|
||||
pub env: Option<Vec<(String, String)>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Shell {
|
||||
System,
|
||||
Program(String),
|
||||
WithArguments { program: String, args: Vec<String> },
|
||||
}
|
||||
|
||||
impl Default for Shell {
|
||||
fn default() -> Self {
|
||||
Shell::System
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum WorkingDirectory {
|
||||
CurrentProjectDirectory,
|
||||
FirstProjectDirectory,
|
||||
AlwaysHome,
|
||||
Always { directory: String },
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, JsonSchema)]
|
||||
pub struct SettingsFileContent {
|
||||
#[serde(default)]
|
||||
|
@ -83,12 +118,16 @@ pub struct SettingsFileContent {
|
|||
#[serde(default)]
|
||||
pub hover_popover_enabled: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub show_completions_on_input: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub vim_mode: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub autosave: Option<Autosave>,
|
||||
#[serde(flatten)]
|
||||
pub editor: EditorSettings,
|
||||
#[serde(default)]
|
||||
pub terminal: TerminalSettings,
|
||||
#[serde(default)]
|
||||
#[serde(alias = "language_overrides")]
|
||||
pub languages: HashMap<Arc<str>, EditorSettings>,
|
||||
#[serde(default)]
|
||||
|
@ -118,6 +157,7 @@ impl Settings {
|
|||
buffer_font_size: defaults.buffer_font_size.unwrap(),
|
||||
default_buffer_font_size: defaults.buffer_font_size.unwrap(),
|
||||
hover_popover_enabled: defaults.hover_popover_enabled.unwrap(),
|
||||
show_completions_on_input: defaults.show_completions_on_input.unwrap(),
|
||||
projects_online_by_default: defaults.projects_online_by_default.unwrap(),
|
||||
vim_mode: defaults.vim_mode.unwrap(),
|
||||
autosave: defaults.autosave.unwrap(),
|
||||
|
@ -129,8 +169,10 @@ impl Settings {
|
|||
format_on_save: required(defaults.editor.format_on_save),
|
||||
enable_language_server: required(defaults.editor.enable_language_server),
|
||||
},
|
||||
language_defaults: defaults.languages,
|
||||
editor_overrides: Default::default(),
|
||||
terminal_defaults: Default::default(),
|
||||
terminal_overrides: Default::default(),
|
||||
language_defaults: defaults.languages,
|
||||
language_overrides: Default::default(),
|
||||
theme: themes.get(&defaults.theme.unwrap()).unwrap(),
|
||||
}
|
||||
|
@ -160,10 +202,21 @@ impl Settings {
|
|||
merge(&mut self.buffer_font_size, data.buffer_font_size);
|
||||
merge(&mut self.default_buffer_font_size, data.buffer_font_size);
|
||||
merge(&mut self.hover_popover_enabled, data.hover_popover_enabled);
|
||||
merge(
|
||||
&mut self.show_completions_on_input,
|
||||
data.show_completions_on_input,
|
||||
);
|
||||
merge(&mut self.vim_mode, data.vim_mode);
|
||||
merge(&mut self.autosave, data.autosave);
|
||||
|
||||
// Ensure terminal font is loaded, so we can request it in terminal_element layout
|
||||
if let Some(terminal_font) = &data.terminal.font_family {
|
||||
font_cache.load_family(&[terminal_font]).log_err();
|
||||
}
|
||||
|
||||
self.editor_overrides = data.editor;
|
||||
self.terminal_defaults.font_size = data.terminal.font_size;
|
||||
self.terminal_overrides = data.terminal;
|
||||
self.language_overrides = data.languages;
|
||||
}
|
||||
|
||||
|
@ -219,6 +272,7 @@ impl Settings {
|
|||
buffer_font_size: 14.,
|
||||
default_buffer_font_size: 14.,
|
||||
hover_popover_enabled: true,
|
||||
show_completions_on_input: true,
|
||||
vim_mode: false,
|
||||
autosave: Autosave::Off,
|
||||
editor_defaults: EditorSettings {
|
||||
|
@ -230,6 +284,8 @@ impl Settings {
|
|||
enable_language_server: Some(true),
|
||||
},
|
||||
editor_overrides: Default::default(),
|
||||
terminal_defaults: Default::default(),
|
||||
terminal_overrides: Default::default(),
|
||||
language_defaults: Default::default(),
|
||||
language_overrides: Default::default(),
|
||||
projects_online_by_default: true,
|
||||
|
@ -248,7 +304,7 @@ impl Settings {
|
|||
|
||||
pub fn settings_file_json_schema(
|
||||
theme_names: Vec<String>,
|
||||
language_names: Vec<String>,
|
||||
language_names: &[String],
|
||||
) -> serde_json::Value {
|
||||
let settings = SchemaSettings::draft07().with(|settings| {
|
||||
settings.option_add_null_type = false;
|
||||
|
@ -275,8 +331,13 @@ pub fn settings_file_json_schema(
|
|||
instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Object))),
|
||||
object: Some(Box::new(ObjectValidation {
|
||||
properties: language_names
|
||||
.into_iter()
|
||||
.map(|name| (name, Schema::new_ref("#/definitions/EditorSettings".into())))
|
||||
.iter()
|
||||
.map(|name| {
|
||||
(
|
||||
name.clone(),
|
||||
Schema::new_ref("#/definitions/EditorSettings".into()),
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
..Default::default()
|
||||
})),
|
||||
|
|
|
@ -5,7 +5,7 @@ use arrayvec::ArrayVec;
|
|||
pub use cursor::{Cursor, FilterCursor, Iter};
|
||||
use std::marker::PhantomData;
|
||||
use std::{cmp::Ordering, fmt, iter::FromIterator, sync::Arc};
|
||||
pub use tree_map::TreeMap;
|
||||
pub use tree_map::{TreeMap, TreeSet};
|
||||
|
||||
#[cfg(test)]
|
||||
const TREE_BASE: usize = 2;
|
||||
|
|
|
@ -20,6 +20,11 @@ pub struct MapKey<K>(K);
|
|||
#[derive(Clone, Debug, Default)]
|
||||
pub struct MapKeyRef<'a, K>(Option<&'a K>);
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct TreeSet<K>(TreeMap<K, ()>)
|
||||
where
|
||||
K: Clone + Debug + Default + Ord;
|
||||
|
||||
impl<K: Clone + Debug + Default + Ord, V: Clone + Debug> TreeMap<K, V> {
|
||||
pub fn from_ordered_entries(entries: impl IntoIterator<Item = (K, V)>) -> Self {
|
||||
let tree = SumTree::from_iter(
|
||||
|
@ -136,6 +141,32 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
impl<K> Default for TreeSet<K>
|
||||
where
|
||||
K: Clone + Debug + Default + Ord,
|
||||
{
|
||||
fn default() -> Self {
|
||||
Self(Default::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl<K> TreeSet<K>
|
||||
where
|
||||
K: Clone + Debug + Default + Ord,
|
||||
{
|
||||
pub fn insert(&mut self, key: K) {
|
||||
self.0.insert(key, ());
|
||||
}
|
||||
|
||||
pub fn contains(&self, key: &K) -> bool {
|
||||
self.0.get(key).is_some()
|
||||
}
|
||||
|
||||
pub fn iter<'a>(&'a self) -> impl 'a + Iterator<Item = &'a K> {
|
||||
self.0.iter().map(|(k, _)| k)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
|
@ -22,11 +22,10 @@ futures = "0.3"
|
|||
ordered-float = "2.1.1"
|
||||
itertools = "0.10"
|
||||
dirs = "4.0.0"
|
||||
shellexpand = "2.1.0"
|
||||
|
||||
[dev-dependencies]
|
||||
gpui = { path = "../gpui", features = ["test-support"] }
|
||||
client = { path = "../client", features = ["test-support"]}
|
||||
project = { path = "../project", features = ["test-support"]}
|
||||
workspace = { path = "../workspace", features = ["test-support"] }
|
||||
|
||||
|
||||
|
|
|
@ -1,25 +1,29 @@
|
|||
mod keymappings;
|
||||
|
||||
use alacritty_terminal::{
|
||||
ansi::{ClearMode, Handler},
|
||||
config::{Config, PtyConfig},
|
||||
config::{Config, Program, PtyConfig},
|
||||
event::{Event as AlacTermEvent, Notify},
|
||||
event_loop::{EventLoop, Msg, Notifier},
|
||||
grid::Scroll,
|
||||
sync::FairMutex,
|
||||
term::SizeInfo,
|
||||
term::{SizeInfo, TermMode},
|
||||
tty::{self, setup_env},
|
||||
Term,
|
||||
};
|
||||
use futures::{channel::mpsc::unbounded, StreamExt};
|
||||
use settings::Settings;
|
||||
use settings::{Settings, Shell};
|
||||
use std::{collections::HashMap, path::PathBuf, sync::Arc};
|
||||
|
||||
use gpui::{ClipboardItem, CursorStyle, Entity, ModelContext};
|
||||
use gpui::{keymap::Keystroke, ClipboardItem, CursorStyle, Entity, ModelContext};
|
||||
|
||||
use crate::{
|
||||
color_translation::{get_color_at_index, to_alac_rgb},
|
||||
ZedListener,
|
||||
};
|
||||
|
||||
use self::keymappings::to_esc_str;
|
||||
|
||||
const DEFAULT_TITLE: &str = "Terminal";
|
||||
|
||||
///Upward flowing events, for changing the title and such
|
||||
|
@ -42,16 +46,32 @@ pub struct TerminalConnection {
|
|||
impl TerminalConnection {
|
||||
pub fn new(
|
||||
working_directory: Option<PathBuf>,
|
||||
shell: Option<Shell>,
|
||||
env_vars: Option<Vec<(String, String)>>,
|
||||
initial_size: SizeInfo,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> TerminalConnection {
|
||||
let pty_config = PtyConfig {
|
||||
shell: None, //Use the users default shell
|
||||
working_directory: working_directory.clone(),
|
||||
hold: false,
|
||||
let pty_config = {
|
||||
let shell = shell.and_then(|shell| match shell {
|
||||
Shell::System => None,
|
||||
Shell::Program(program) => Some(Program::Just(program)),
|
||||
Shell::WithArguments { program, args } => Some(Program::WithArgs { program, args }),
|
||||
});
|
||||
|
||||
PtyConfig {
|
||||
shell,
|
||||
working_directory: working_directory.clone(),
|
||||
hold: false,
|
||||
}
|
||||
};
|
||||
|
||||
let mut env: HashMap<String, String> = HashMap::new();
|
||||
if let Some(envs) = env_vars {
|
||||
for (var, val) in envs {
|
||||
env.insert(var, val);
|
||||
}
|
||||
}
|
||||
|
||||
//TODO: Properly set the current locale,
|
||||
env.insert("LC_ALL".to_string(), "en_US.UTF-8".to_string());
|
||||
|
||||
|
@ -71,7 +91,20 @@ impl TerminalConnection {
|
|||
let term = Arc::new(FairMutex::new(term));
|
||||
|
||||
//Setup the pty...
|
||||
let pty = tty::new(&pty_config, &initial_size, None).expect("Could not create tty");
|
||||
let pty = {
|
||||
if let Some(pty) = tty::new(&pty_config, &initial_size, None).ok() {
|
||||
pty
|
||||
} else {
|
||||
let pty_config = PtyConfig {
|
||||
shell: None,
|
||||
working_directory: working_directory.clone(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
tty::new(&pty_config, &initial_size, None)
|
||||
.expect("Failed with default shell too :(")
|
||||
}
|
||||
};
|
||||
|
||||
//And connect them together
|
||||
let event_loop = EventLoop::new(
|
||||
|
@ -182,6 +215,30 @@ impl TerminalConnection {
|
|||
self.write_to_pty("\x0c".into());
|
||||
self.term.lock().clear_screen(ClearMode::Saved);
|
||||
}
|
||||
|
||||
pub fn try_keystroke(&mut self, keystroke: &Keystroke) -> bool {
|
||||
let guard = self.term.lock();
|
||||
let mode = guard.mode();
|
||||
let esc = to_esc_str(keystroke, mode);
|
||||
drop(guard);
|
||||
if esc.is_some() {
|
||||
self.write_to_pty(esc.unwrap());
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
///Paste text into the terminal
|
||||
pub fn paste(&mut self, text: &str) {
|
||||
if self.term.lock().mode().contains(TermMode::BRACKETED_PASTE) {
|
||||
self.write_to_pty("\x1b[200~".to_string());
|
||||
self.write_to_pty(text.replace('\x1b', "").to_string());
|
||||
self.write_to_pty("\x1b[201~".to_string());
|
||||
} else {
|
||||
self.write_to_pty(text.replace("\r\n", "\r").replace('\n', "\r"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TerminalConnection {
|
||||
|
|
444
crates/terminal/src/connection/keymappings.rs
Normal file
444
crates/terminal/src/connection/keymappings.rs
Normal file
|
@ -0,0 +1,444 @@
|
|||
use alacritty_terminal::term::TermMode;
|
||||
use gpui::keymap::Keystroke;
|
||||
|
||||
/*
|
||||
Connection events still to do:
|
||||
- Reporting mouse events correctly.
|
||||
- Reporting scrolls
|
||||
- Correctly bracketing a paste
|
||||
- Storing changed colors
|
||||
- Focus change sequence
|
||||
*/
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Modifiers {
|
||||
None,
|
||||
Alt,
|
||||
Ctrl,
|
||||
Shift,
|
||||
CtrlShift,
|
||||
Other,
|
||||
}
|
||||
|
||||
impl Modifiers {
|
||||
fn new(ks: &Keystroke) -> Self {
|
||||
match (ks.alt, ks.ctrl, ks.shift, ks.cmd) {
|
||||
(false, false, false, false) => Modifiers::None,
|
||||
(true, false, false, false) => Modifiers::Alt,
|
||||
(false, true, false, false) => Modifiers::Ctrl,
|
||||
(false, false, true, false) => Modifiers::Shift,
|
||||
(false, true, true, false) => Modifiers::CtrlShift,
|
||||
_ => Modifiers::Other,
|
||||
}
|
||||
}
|
||||
|
||||
fn any(&self) -> bool {
|
||||
match &self {
|
||||
Modifiers::None => false,
|
||||
Modifiers::Alt => true,
|
||||
Modifiers::Ctrl => true,
|
||||
Modifiers::Shift => true,
|
||||
Modifiers::CtrlShift => true,
|
||||
Modifiers::Other => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_esc_str(keystroke: &Keystroke, mode: &TermMode) -> Option<String> {
|
||||
let modifiers = Modifiers::new(&keystroke);
|
||||
|
||||
// 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()),
|
||||
("backspace", Modifiers::None) => Some("\x7f".to_string()),
|
||||
//Interesting escape codes
|
||||
("tab", Modifiers::Shift) => Some("\x1b[Z".to_string()),
|
||||
("backspace", Modifiers::Alt) => Some("\x1b\x7f".to_string()),
|
||||
("backspace", Modifiers::Shift) => Some("\x7f".to_string()),
|
||||
("home", Modifiers::Shift) if mode.contains(TermMode::ALT_SCREEN) => {
|
||||
Some("\x1b[1;2H".to_string())
|
||||
}
|
||||
("end", Modifiers::Shift) if mode.contains(TermMode::ALT_SCREEN) => {
|
||||
Some("\x1b[1;2F".to_string())
|
||||
}
|
||||
("pageup", Modifiers::Shift) if mode.contains(TermMode::ALT_SCREEN) => {
|
||||
Some("\x1b[5;2~".to_string())
|
||||
}
|
||||
("pagedown", Modifiers::Shift) if mode.contains(TermMode::ALT_SCREEN) => {
|
||||
Some("\x1b[6;2~".to_string())
|
||||
}
|
||||
("home", Modifiers::None) if mode.contains(TermMode::APP_CURSOR) => {
|
||||
Some("\x1bOH".to_string())
|
||||
}
|
||||
("home", Modifiers::None) if !mode.contains(TermMode::APP_CURSOR) => {
|
||||
Some("\x1b[H".to_string())
|
||||
}
|
||||
("end", Modifiers::None) if mode.contains(TermMode::APP_CURSOR) => {
|
||||
Some("\x1bOF".to_string())
|
||||
}
|
||||
("end", Modifiers::None) if !mode.contains(TermMode::APP_CURSOR) => {
|
||||
Some("\x1b[F".to_string())
|
||||
}
|
||||
("up", Modifiers::None) if mode.contains(TermMode::APP_CURSOR) => {
|
||||
Some("\x1bOA".to_string())
|
||||
}
|
||||
("up", Modifiers::None) if !mode.contains(TermMode::APP_CURSOR) => {
|
||||
Some("\x1b[A".to_string())
|
||||
}
|
||||
("down", Modifiers::None) if mode.contains(TermMode::APP_CURSOR) => {
|
||||
Some("\x1bOB".to_string())
|
||||
}
|
||||
("down", Modifiers::None) if !mode.contains(TermMode::APP_CURSOR) => {
|
||||
Some("\x1b[B".to_string())
|
||||
}
|
||||
("right", Modifiers::None) if mode.contains(TermMode::APP_CURSOR) => {
|
||||
Some("\x1bOC".to_string())
|
||||
}
|
||||
("right", Modifiers::None) if !mode.contains(TermMode::APP_CURSOR) => {
|
||||
Some("\x1b[C".to_string())
|
||||
}
|
||||
("left", Modifiers::None) if mode.contains(TermMode::APP_CURSOR) => {
|
||||
Some("\x1bOD".to_string())
|
||||
}
|
||||
("left", Modifiers::None) if !mode.contains(TermMode::APP_CURSOR) => {
|
||||
Some("\x1b[D".to_string())
|
||||
}
|
||||
("back", Modifiers::None) => Some("\x7f".to_string()),
|
||||
("insert", Modifiers::None) => Some("\x1b[2~".to_string()),
|
||||
("delete", Modifiers::None) => Some("\x1b[3~".to_string()),
|
||||
("pageup", Modifiers::None) => Some("\x1b[5~".to_string()),
|
||||
("pagedown", Modifiers::None) => Some("\x1b[6~".to_string()),
|
||||
("f1", Modifiers::None) => Some("\x1bOP".to_string()),
|
||||
("f2", Modifiers::None) => Some("\x1bOQ".to_string()),
|
||||
("f3", Modifiers::None) => Some("\x1bOR".to_string()),
|
||||
("f4", Modifiers::None) => Some("\x1bOS".to_string()),
|
||||
("f5", Modifiers::None) => Some("\x1b[15~".to_string()),
|
||||
("f6", Modifiers::None) => Some("\x1b[17~".to_string()),
|
||||
("f7", Modifiers::None) => Some("\x1b[18~".to_string()),
|
||||
("f8", Modifiers::None) => Some("\x1b[19~".to_string()),
|
||||
("f9", Modifiers::None) => Some("\x1b[20~".to_string()),
|
||||
("f10", Modifiers::None) => Some("\x1b[21~".to_string()),
|
||||
("f11", Modifiers::None) => Some("\x1b[23~".to_string()),
|
||||
("f12", Modifiers::None) => Some("\x1b[24~".to_string()),
|
||||
("f13", Modifiers::None) => Some("\x1b[25~".to_string()),
|
||||
("f14", Modifiers::None) => Some("\x1b[26~".to_string()),
|
||||
("f15", Modifiers::None) => Some("\x1b[28~".to_string()),
|
||||
("f16", Modifiers::None) => Some("\x1b[29~".to_string()),
|
||||
("f17", Modifiers::None) => Some("\x1b[31~".to_string()),
|
||||
("f18", Modifiers::None) => Some("\x1b[32~".to_string()),
|
||||
("f19", Modifiers::None) => Some("\x1b[33~".to_string()),
|
||||
("f20", Modifiers::None) => Some("\x1b[34~".to_string()),
|
||||
// NumpadEnter, Action::Esc("\n".into());
|
||||
//Mappings for caret notation keys
|
||||
("a", Modifiers::Ctrl) => Some("\x01".to_string()), //1
|
||||
("A", Modifiers::CtrlShift) => Some("\x01".to_string()), //1
|
||||
("b", Modifiers::Ctrl) => Some("\x02".to_string()), //2
|
||||
("B", Modifiers::CtrlShift) => Some("\x02".to_string()), //2
|
||||
("c", Modifiers::Ctrl) => Some("\x03".to_string()), //3
|
||||
("C", Modifiers::CtrlShift) => Some("\x03".to_string()), //3
|
||||
("d", Modifiers::Ctrl) => Some("\x04".to_string()), //4
|
||||
("D", Modifiers::CtrlShift) => Some("\x04".to_string()), //4
|
||||
("e", Modifiers::Ctrl) => Some("\x05".to_string()), //5
|
||||
("E", Modifiers::CtrlShift) => Some("\x05".to_string()), //5
|
||||
("f", Modifiers::Ctrl) => Some("\x06".to_string()), //6
|
||||
("F", Modifiers::CtrlShift) => Some("\x06".to_string()), //6
|
||||
("g", Modifiers::Ctrl) => Some("\x07".to_string()), //7
|
||||
("G", Modifiers::CtrlShift) => Some("\x07".to_string()), //7
|
||||
("h", Modifiers::Ctrl) => Some("\x08".to_string()), //8
|
||||
("H", Modifiers::CtrlShift) => Some("\x08".to_string()), //8
|
||||
("i", Modifiers::Ctrl) => Some("\x09".to_string()), //9
|
||||
("I", Modifiers::CtrlShift) => Some("\x09".to_string()), //9
|
||||
("j", Modifiers::Ctrl) => Some("\x0a".to_string()), //10
|
||||
("J", Modifiers::CtrlShift) => Some("\x0a".to_string()), //10
|
||||
("k", Modifiers::Ctrl) => Some("\x0b".to_string()), //11
|
||||
("K", Modifiers::CtrlShift) => Some("\x0b".to_string()), //11
|
||||
("l", Modifiers::Ctrl) => Some("\x0c".to_string()), //12
|
||||
("L", Modifiers::CtrlShift) => Some("\x0c".to_string()), //12
|
||||
("m", Modifiers::Ctrl) => Some("\x0d".to_string()), //13
|
||||
("M", Modifiers::CtrlShift) => Some("\x0d".to_string()), //13
|
||||
("n", Modifiers::Ctrl) => Some("\x0e".to_string()), //14
|
||||
("N", Modifiers::CtrlShift) => Some("\x0e".to_string()), //14
|
||||
("o", Modifiers::Ctrl) => Some("\x0f".to_string()), //15
|
||||
("O", Modifiers::CtrlShift) => Some("\x0f".to_string()), //15
|
||||
("p", Modifiers::Ctrl) => Some("\x10".to_string()), //16
|
||||
("P", Modifiers::CtrlShift) => Some("\x10".to_string()), //16
|
||||
("q", Modifiers::Ctrl) => Some("\x11".to_string()), //17
|
||||
("Q", Modifiers::CtrlShift) => Some("\x11".to_string()), //17
|
||||
("r", Modifiers::Ctrl) => Some("\x12".to_string()), //18
|
||||
("R", Modifiers::CtrlShift) => Some("\x12".to_string()), //18
|
||||
("s", Modifiers::Ctrl) => Some("\x13".to_string()), //19
|
||||
("S", Modifiers::CtrlShift) => Some("\x13".to_string()), //19
|
||||
("t", Modifiers::Ctrl) => Some("\x14".to_string()), //20
|
||||
("T", Modifiers::CtrlShift) => Some("\x14".to_string()), //20
|
||||
("u", Modifiers::Ctrl) => Some("\x15".to_string()), //21
|
||||
("U", Modifiers::CtrlShift) => Some("\x15".to_string()), //21
|
||||
("v", Modifiers::Ctrl) => Some("\x16".to_string()), //22
|
||||
("V", Modifiers::CtrlShift) => Some("\x16".to_string()), //22
|
||||
("w", Modifiers::Ctrl) => Some("\x17".to_string()), //23
|
||||
("W", Modifiers::CtrlShift) => Some("\x17".to_string()), //23
|
||||
("x", Modifiers::Ctrl) => Some("\x18".to_string()), //24
|
||||
("X", Modifiers::CtrlShift) => Some("\x18".to_string()), //24
|
||||
("y", Modifiers::Ctrl) => Some("\x19".to_string()), //25
|
||||
("Y", Modifiers::CtrlShift) => Some("\x19".to_string()), //25
|
||||
("z", Modifiers::Ctrl) => Some("\x1a".to_string()), //26
|
||||
("Z", Modifiers::CtrlShift) => Some("\x1a".to_string()), //26
|
||||
("@", Modifiers::Ctrl) => Some("\x00".to_string()), //0
|
||||
("[", Modifiers::Ctrl) => Some("\x1b".to_string()), //27
|
||||
("\\", Modifiers::Ctrl) => Some("\x1c".to_string()), //28
|
||||
("]", Modifiers::Ctrl) => Some("\x1d".to_string()), //29
|
||||
("^", Modifiers::Ctrl) => Some("\x1e".to_string()), //30
|
||||
("_", Modifiers::Ctrl) => Some("\x1f".to_string()), //31
|
||||
("?", Modifiers::Ctrl) => Some("\x7f".to_string()), //127
|
||||
_ => None,
|
||||
};
|
||||
if manual_esc_str.is_some() {
|
||||
return manual_esc_str;
|
||||
}
|
||||
|
||||
// Automated bindings applying modifiers
|
||||
if modifiers.any() {
|
||||
let modifier_code = modifier_code(&keystroke);
|
||||
let modified_esc_str = match keystroke.key.as_ref() {
|
||||
"up" => Some(format!("\x1b[1;{}A", modifier_code)),
|
||||
"down" => Some(format!("\x1b[1;{}B", modifier_code)),
|
||||
"right" => Some(format!("\x1b[1;{}C", modifier_code)),
|
||||
"left" => Some(format!("\x1b[1;{}D", modifier_code)),
|
||||
"f1" => Some(format!("\x1b[1;{}P", modifier_code)),
|
||||
"f2" => Some(format!("\x1b[1;{}Q", modifier_code)),
|
||||
"f3" => Some(format!("\x1b[1;{}R", modifier_code)),
|
||||
"f4" => Some(format!("\x1b[1;{}S", modifier_code)),
|
||||
"F5" => Some(format!("\x1b[15;{}~", modifier_code)),
|
||||
"f6" => Some(format!("\x1b[17;{}~", modifier_code)),
|
||||
"f7" => Some(format!("\x1b[18;{}~", modifier_code)),
|
||||
"f8" => Some(format!("\x1b[19;{}~", modifier_code)),
|
||||
"f9" => Some(format!("\x1b[20;{}~", modifier_code)),
|
||||
"f10" => Some(format!("\x1b[21;{}~", modifier_code)),
|
||||
"f11" => Some(format!("\x1b[23;{}~", modifier_code)),
|
||||
"f12" => Some(format!("\x1b[24;{}~", modifier_code)),
|
||||
"f13" => Some(format!("\x1b[25;{}~", modifier_code)),
|
||||
"f14" => Some(format!("\x1b[26;{}~", modifier_code)),
|
||||
"f15" => Some(format!("\x1b[28;{}~", modifier_code)),
|
||||
"f16" => Some(format!("\x1b[29;{}~", modifier_code)),
|
||||
"f17" => Some(format!("\x1b[31;{}~", modifier_code)),
|
||||
"f18" => Some(format!("\x1b[32;{}~", modifier_code)),
|
||||
"f19" => Some(format!("\x1b[33;{}~", modifier_code)),
|
||||
"f20" => Some(format!("\x1b[34;{}~", modifier_code)),
|
||||
_ if modifier_code == 2 => None,
|
||||
"insert" => Some(format!("\x1b[2;{}~", modifier_code)),
|
||||
"pageup" => Some(format!("\x1b[5;{}~", modifier_code)),
|
||||
"pagedown" => Some(format!("\x1b[6;{}~", modifier_code)),
|
||||
"end" => Some(format!("\x1b[1;{}F", modifier_code)),
|
||||
"home" => Some(format!("\x1b[1;{}H", modifier_code)),
|
||||
_ => None,
|
||||
};
|
||||
if modified_esc_str.is_some() {
|
||||
return modified_esc_str;
|
||||
}
|
||||
}
|
||||
|
||||
//Fallback to sending the keystroke input directly
|
||||
//Skin colors in utf8 are implemented as a seperate, invisible character
|
||||
//that modifies the associated emoji. Some languages may have similarly
|
||||
//implemented modifiers, e.g. certain diacritics that can be typed as a single character.
|
||||
//This means that we need to assume some user input can result in multi-byte,
|
||||
//multi-char strings. This is somewhat difficult, as GPUI normalizes all
|
||||
//keys into a string representation. Hence, the check here to filter out GPUI
|
||||
//keys that weren't captured above.
|
||||
if !matches_gpui_key_str(&keystroke.key) {
|
||||
return Some(keystroke.key.clone());
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
///Checks if the given string matches a GPUI key string.
|
||||
///Table made from reading the source at gpui/src/platform/mac/event.rs
|
||||
fn matches_gpui_key_str(str: &str) -> bool {
|
||||
match str {
|
||||
"backspace" => true,
|
||||
"up" => true,
|
||||
"down" => true,
|
||||
"left" => true,
|
||||
"right" => true,
|
||||
"pageup" => true,
|
||||
"pagedown" => true,
|
||||
"home" => true,
|
||||
"end" => true,
|
||||
"delete" => true,
|
||||
"enter" => true,
|
||||
"escape" => true,
|
||||
"tab" => true,
|
||||
"f1" => true,
|
||||
"f2" => true,
|
||||
"f3" => true,
|
||||
"f4" => true,
|
||||
"f5" => true,
|
||||
"f6" => true,
|
||||
"f7" => true,
|
||||
"f8" => true,
|
||||
"f9" => true,
|
||||
"f10" => true,
|
||||
"f11" => true,
|
||||
"f12" => true,
|
||||
"space" => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Code Modifiers
|
||||
/// ---------+---------------------------
|
||||
/// 2 | Shift
|
||||
/// 3 | Alt
|
||||
/// 4 | Shift + Alt
|
||||
/// 5 | Control
|
||||
/// 6 | Shift + Control
|
||||
/// 7 | Alt + Control
|
||||
/// 8 | Shift + Alt + Control
|
||||
/// ---------+---------------------------
|
||||
/// from: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-PC-Style-Function-Keys
|
||||
fn modifier_code(keystroke: &Keystroke) -> u32 {
|
||||
let mut modifier_code = 0;
|
||||
if keystroke.shift {
|
||||
modifier_code |= 1;
|
||||
}
|
||||
if keystroke.alt {
|
||||
modifier_code |= 1 << 1;
|
||||
}
|
||||
if keystroke.ctrl {
|
||||
modifier_code |= 1 << 2;
|
||||
}
|
||||
modifier_code + 1
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_scroll_keys() {
|
||||
//These keys should be handled by the scrolling element directly
|
||||
//Need to signify this by returning 'None'
|
||||
let shift_pageup = Keystroke::parse("shift-pageup").unwrap();
|
||||
let shift_pagedown = Keystroke::parse("shift-pagedown").unwrap();
|
||||
let shift_home = Keystroke::parse("shift-home").unwrap();
|
||||
let shift_end = Keystroke::parse("shift-end").unwrap();
|
||||
|
||||
let none = TermMode::NONE;
|
||||
assert_eq!(to_esc_str(&shift_pageup, &none), None);
|
||||
assert_eq!(to_esc_str(&shift_pagedown, &none), None);
|
||||
assert_eq!(to_esc_str(&shift_home, &none), None);
|
||||
assert_eq!(to_esc_str(&shift_end, &none), None);
|
||||
|
||||
let alt_screen = TermMode::ALT_SCREEN;
|
||||
assert_eq!(
|
||||
to_esc_str(&shift_pageup, &alt_screen),
|
||||
Some("\x1b[5;2~".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
to_esc_str(&shift_pagedown, &alt_screen),
|
||||
Some("\x1b[6;2~".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
to_esc_str(&shift_home, &alt_screen),
|
||||
Some("\x1b[1;2H".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
to_esc_str(&shift_end, &alt_screen),
|
||||
Some("\x1b[1;2F".to_string())
|
||||
);
|
||||
|
||||
let pageup = Keystroke::parse("pageup").unwrap();
|
||||
let pagedown = Keystroke::parse("pagedown").unwrap();
|
||||
let any = TermMode::ANY;
|
||||
|
||||
assert_eq!(to_esc_str(&pageup, &any), Some("\x1b[5~".to_string()));
|
||||
assert_eq!(to_esc_str(&pagedown, &any), Some("\x1b[6~".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multi_char_fallthrough() {
|
||||
let ks = Keystroke {
|
||||
ctrl: false,
|
||||
alt: false,
|
||||
shift: false,
|
||||
cmd: false,
|
||||
|
||||
key: "🖖🏻".to_string(), //2 char string
|
||||
};
|
||||
|
||||
assert_eq!(to_esc_str(&ks, &TermMode::NONE), Some("🖖🏻".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_application_mode() {
|
||||
let app_cursor = TermMode::APP_CURSOR;
|
||||
let none = TermMode::NONE;
|
||||
|
||||
let up = Keystroke::parse("up").unwrap();
|
||||
let down = Keystroke::parse("down").unwrap();
|
||||
let left = Keystroke::parse("left").unwrap();
|
||||
let right = Keystroke::parse("right").unwrap();
|
||||
|
||||
assert_eq!(to_esc_str(&up, &none), Some("\x1b[A".to_string()));
|
||||
assert_eq!(to_esc_str(&down, &none), Some("\x1b[B".to_string()));
|
||||
assert_eq!(to_esc_str(&right, &none), Some("\x1b[C".to_string()));
|
||||
assert_eq!(to_esc_str(&left, &none), Some("\x1b[D".to_string()));
|
||||
|
||||
assert_eq!(to_esc_str(&up, &app_cursor), Some("\x1bOA".to_string()));
|
||||
assert_eq!(to_esc_str(&down, &app_cursor), Some("\x1bOB".to_string()));
|
||||
assert_eq!(to_esc_str(&right, &app_cursor), Some("\x1bOC".to_string()));
|
||||
assert_eq!(to_esc_str(&left, &app_cursor), Some("\x1bOD".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ctrl_codes() {
|
||||
let letters_lower = 'a'..='z';
|
||||
let letters_upper = 'A'..='Z';
|
||||
let mode = TermMode::ANY;
|
||||
|
||||
for (lower, upper) in letters_lower.zip(letters_upper) {
|
||||
assert_eq!(
|
||||
to_esc_str(
|
||||
&Keystroke::parse(&format!("ctrl-{}", lower)).unwrap(),
|
||||
&mode
|
||||
),
|
||||
to_esc_str(
|
||||
&Keystroke::parse(&format!("ctrl-shift-{}", upper)).unwrap(),
|
||||
&mode
|
||||
),
|
||||
"On letter: {}/{}",
|
||||
lower,
|
||||
upper
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_modifier_code_calc() {
|
||||
// Code Modifiers
|
||||
// ---------+---------------------------
|
||||
// 2 | Shift
|
||||
// 3 | Alt
|
||||
// 4 | Shift + Alt
|
||||
// 5 | Control
|
||||
// 6 | Shift + Control
|
||||
// 7 | Alt + Control
|
||||
// 8 | Shift + Alt + Control
|
||||
// ---------+---------------------------
|
||||
// from: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-PC-Style-Function-Keys
|
||||
assert_eq!(2, modifier_code(&Keystroke::parse("shift-A").unwrap()));
|
||||
assert_eq!(3, modifier_code(&Keystroke::parse("alt-A").unwrap()));
|
||||
assert_eq!(4, modifier_code(&Keystroke::parse("shift-alt-A").unwrap()));
|
||||
assert_eq!(5, modifier_code(&Keystroke::parse("ctrl-A").unwrap()));
|
||||
assert_eq!(6, modifier_code(&Keystroke::parse("shift-ctrl-A").unwrap()));
|
||||
assert_eq!(7, modifier_code(&Keystroke::parse("alt-ctrl-A").unwrap()));
|
||||
assert_eq!(
|
||||
8,
|
||||
modifier_code(&Keystroke::parse("shift-ctrl-alt-A").unwrap())
|
||||
);
|
||||
}
|
||||
}
|
|
@ -16,8 +16,11 @@ pub fn deploy_modal(workspace: &mut Workspace, _: &DeployModal, cx: &mut ViewCon
|
|||
if let Some(StoredConnection(stored_connection)) = possible_connection {
|
||||
// Create a view from the stored connection
|
||||
workspace.toggle_modal(cx, |_, cx| {
|
||||
cx.add_view(|cx| Terminal::from_connection(stored_connection, true, cx))
|
||||
cx.add_view(|cx| Terminal::from_connection(stored_connection.clone(), true, cx))
|
||||
});
|
||||
cx.set_global::<Option<StoredConnection>>(Some(StoredConnection(
|
||||
stored_connection.clone(),
|
||||
)));
|
||||
} else {
|
||||
// No connection was stored, create a new terminal
|
||||
if let Some(closed_terminal_handle) = workspace.toggle_modal(cx, |workspace, cx| {
|
||||
|
|
|
@ -5,7 +5,6 @@ pub mod terminal_element;
|
|||
|
||||
use alacritty_terminal::{
|
||||
event::{Event as AlacTermEvent, EventListener},
|
||||
grid::Scroll,
|
||||
term::SizeInfo,
|
||||
};
|
||||
|
||||
|
@ -14,29 +13,19 @@ use dirs::home_dir;
|
|||
use editor::Input;
|
||||
use futures::channel::mpsc::UnboundedSender;
|
||||
use gpui::{
|
||||
actions, elements::*, impl_internal_actions, AppContext, ClipboardItem, Entity, ModelHandle,
|
||||
actions, elements::*, keymap::Keystroke, AppContext, ClipboardItem, Entity, ModelHandle,
|
||||
MutableAppContext, View, ViewContext,
|
||||
};
|
||||
use modal::deploy_modal;
|
||||
|
||||
use project::{Project, ProjectPath};
|
||||
use settings::Settings;
|
||||
use project::{LocalWorktree, Project, ProjectPath};
|
||||
use settings::{Settings, WorkingDirectory};
|
||||
use smallvec::SmallVec;
|
||||
use std::path::PathBuf;
|
||||
use std::path::{Path, PathBuf};
|
||||
use workspace::{Item, Workspace};
|
||||
|
||||
use crate::terminal_element::TerminalEl;
|
||||
|
||||
//ASCII Control characters on a keyboard
|
||||
const ETX_CHAR: char = 3_u8 as char; //'End of text', the control code for 'ctrl-c'
|
||||
const TAB_CHAR: char = 9_u8 as char;
|
||||
const CARRIAGE_RETURN_CHAR: char = 13_u8 as char;
|
||||
const ESC_CHAR: char = 27_u8 as char; // == \x1b
|
||||
const DEL_CHAR: char = 127_u8 as char;
|
||||
const LEFT_SEQ: &str = "\x1b[D";
|
||||
const RIGHT_SEQ: &str = "\x1b[C";
|
||||
const UP_SEQ: &str = "\x1b[A";
|
||||
const DOWN_SEQ: &str = "\x1b[B";
|
||||
const DEBUG_TERMINAL_WIDTH: f32 = 1000.; //This needs to be wide enough that the prompt can fill the whole space.
|
||||
const DEBUG_TERMINAL_HEIGHT: f32 = 200.;
|
||||
const DEBUG_CELL_WIDTH: f32 = 5.;
|
||||
|
@ -52,44 +41,34 @@ pub struct ScrollTerminal(pub i32);
|
|||
actions!(
|
||||
terminal,
|
||||
[
|
||||
Sigint,
|
||||
Escape,
|
||||
Del,
|
||||
Return,
|
||||
Left,
|
||||
Right,
|
||||
Deploy,
|
||||
Up,
|
||||
Down,
|
||||
Tab,
|
||||
CtrlC,
|
||||
Escape,
|
||||
Enter,
|
||||
Clear,
|
||||
Copy,
|
||||
Paste,
|
||||
Deploy,
|
||||
Quit,
|
||||
DeployModal,
|
||||
DeployModal
|
||||
]
|
||||
);
|
||||
impl_internal_actions!(terminal, [ScrollTerminal]);
|
||||
|
||||
///Initialize and register all of our action handlers
|
||||
pub fn init(cx: &mut MutableAppContext) {
|
||||
cx.add_action(Terminal::deploy);
|
||||
cx.add_action(Terminal::send_sigint);
|
||||
cx.add_action(Terminal::escape);
|
||||
cx.add_action(Terminal::quit);
|
||||
cx.add_action(Terminal::del);
|
||||
cx.add_action(Terminal::carriage_return);
|
||||
cx.add_action(Terminal::left);
|
||||
cx.add_action(Terminal::right);
|
||||
//Global binding overrrides
|
||||
cx.add_action(Terminal::ctrl_c);
|
||||
cx.add_action(Terminal::up);
|
||||
cx.add_action(Terminal::down);
|
||||
cx.add_action(Terminal::tab);
|
||||
cx.add_action(Terminal::escape);
|
||||
cx.add_action(Terminal::enter);
|
||||
//Useful terminal actions
|
||||
cx.add_action(Terminal::deploy);
|
||||
cx.add_action(deploy_modal);
|
||||
cx.add_action(Terminal::copy);
|
||||
cx.add_action(Terminal::paste);
|
||||
cx.add_action(Terminal::scroll_terminal);
|
||||
cx.add_action(Terminal::input);
|
||||
cx.add_action(Terminal::clear);
|
||||
cx.add_action(deploy_modal);
|
||||
}
|
||||
|
||||
///A translation struct for Alacritty to communicate with us from their event loop
|
||||
|
@ -131,8 +110,15 @@ impl Terminal {
|
|||
false,
|
||||
);
|
||||
|
||||
let connection =
|
||||
cx.add_model(|cx| TerminalConnection::new(working_directory, size_info, cx));
|
||||
let (shell, envs) = {
|
||||
let settings = cx.global::<Settings>();
|
||||
let shell = settings.terminal_overrides.shell.clone();
|
||||
let envs = settings.terminal_overrides.env.clone(); //Should be short and cheap.
|
||||
(shell, envs)
|
||||
};
|
||||
|
||||
let connection = cx
|
||||
.add_model(|cx| TerminalConnection::new(working_directory, shell, envs, size_info, cx));
|
||||
|
||||
Terminal::from_connection(connection, modal, cx)
|
||||
}
|
||||
|
@ -168,15 +154,6 @@ impl Terminal {
|
|||
}
|
||||
}
|
||||
|
||||
///Scroll the terminal. This locks the terminal
|
||||
fn scroll_terminal(&mut self, scroll: &ScrollTerminal, cx: &mut ViewContext<Self>) {
|
||||
self.connection
|
||||
.read(cx)
|
||||
.term
|
||||
.lock()
|
||||
.scroll_display(Scroll::Delta(scroll.0));
|
||||
}
|
||||
|
||||
fn input(&mut self, Input(text): &Input, cx: &mut ViewContext<Self>) {
|
||||
self.connection.update(cx, |connection, _| {
|
||||
//TODO: This is probably not encoding UTF8 correctly (see alacritty/src/input.rs:L825-837)
|
||||
|
@ -200,11 +177,6 @@ impl Terminal {
|
|||
workspace.add_item(Box::new(cx.add_view(|cx| Terminal::new(wd, false, cx))), cx);
|
||||
}
|
||||
|
||||
///Tell Zed to close us
|
||||
fn quit(&mut self, _: &Quit, cx: &mut ViewContext<Self>) {
|
||||
cx.emit(Event::CloseTerminal);
|
||||
}
|
||||
|
||||
///Attempt to paste the clipboard into the terminal
|
||||
fn copy(&mut self, _: &Copy, cx: &mut ViewContext<Self>) {
|
||||
let term = self.connection.read(cx).term.lock();
|
||||
|
@ -219,71 +191,43 @@ impl Terminal {
|
|||
fn paste(&mut self, _: &Paste, cx: &mut ViewContext<Self>) {
|
||||
if let Some(item) = cx.read_from_clipboard() {
|
||||
self.connection.update(cx, |connection, _| {
|
||||
connection.write_to_pty(item.text().to_owned());
|
||||
connection.paste(item.text());
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
///Send the `up` key
|
||||
///Synthesize the keyboard event corresponding to 'up'
|
||||
fn up(&mut self, _: &Up, cx: &mut ViewContext<Self>) {
|
||||
self.connection.update(cx, |connection, _| {
|
||||
connection.write_to_pty(UP_SEQ.to_string());
|
||||
connection.try_keystroke(&Keystroke::parse("up").unwrap());
|
||||
});
|
||||
}
|
||||
|
||||
///Send the `down` key
|
||||
///Synthesize the keyboard event corresponding to 'down'
|
||||
fn down(&mut self, _: &Down, cx: &mut ViewContext<Self>) {
|
||||
self.connection.update(cx, |connection, _| {
|
||||
connection.write_to_pty(DOWN_SEQ.to_string());
|
||||
connection.try_keystroke(&Keystroke::parse("down").unwrap());
|
||||
});
|
||||
}
|
||||
|
||||
///Send the `tab` key
|
||||
fn tab(&mut self, _: &Tab, cx: &mut ViewContext<Self>) {
|
||||
///Synthesize the keyboard event corresponding to 'ctrl-c'
|
||||
fn ctrl_c(&mut self, _: &CtrlC, cx: &mut ViewContext<Self>) {
|
||||
self.connection.update(cx, |connection, _| {
|
||||
connection.write_to_pty(TAB_CHAR.to_string());
|
||||
connection.try_keystroke(&Keystroke::parse("ctrl-c").unwrap());
|
||||
});
|
||||
}
|
||||
|
||||
///Send `SIGINT` (`ctrl-c`)
|
||||
fn send_sigint(&mut self, _: &Sigint, cx: &mut ViewContext<Self>) {
|
||||
self.connection.update(cx, |connection, _| {
|
||||
connection.write_to_pty(ETX_CHAR.to_string());
|
||||
});
|
||||
}
|
||||
|
||||
///Send the `escape` key
|
||||
///Synthesize the keyboard event corresponding to 'escape'
|
||||
fn escape(&mut self, _: &Escape, cx: &mut ViewContext<Self>) {
|
||||
self.connection.update(cx, |connection, _| {
|
||||
connection.write_to_pty(ESC_CHAR.to_string());
|
||||
connection.try_keystroke(&Keystroke::parse("escape").unwrap());
|
||||
});
|
||||
}
|
||||
|
||||
///Send the `delete` key. TODO: Difference between this and backspace?
|
||||
fn del(&mut self, _: &Del, cx: &mut ViewContext<Self>) {
|
||||
///Synthesize the keyboard event corresponding to 'enter'
|
||||
fn enter(&mut self, _: &Enter, cx: &mut ViewContext<Self>) {
|
||||
self.connection.update(cx, |connection, _| {
|
||||
connection.write_to_pty(DEL_CHAR.to_string());
|
||||
});
|
||||
}
|
||||
|
||||
///Send a carriage return. TODO: May need to check the terminal mode.
|
||||
fn carriage_return(&mut self, _: &Return, cx: &mut ViewContext<Self>) {
|
||||
self.connection.update(cx, |connection, _| {
|
||||
connection.write_to_pty(CARRIAGE_RETURN_CHAR.to_string());
|
||||
});
|
||||
}
|
||||
|
||||
//Send the `left` key
|
||||
fn left(&mut self, _: &Left, cx: &mut ViewContext<Self>) {
|
||||
self.connection.update(cx, |connection, _| {
|
||||
connection.write_to_pty(LEFT_SEQ.to_string());
|
||||
});
|
||||
}
|
||||
|
||||
//Send the `right` key
|
||||
fn right(&mut self, _: &Right, cx: &mut ViewContext<Self>) {
|
||||
self.connection.update(cx, |connection, _| {
|
||||
connection.write_to_pty(RIGHT_SEQ.to_string());
|
||||
connection.try_keystroke(&Keystroke::parse("enter").unwrap());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -324,7 +268,12 @@ impl View for Terminal {
|
|||
}
|
||||
|
||||
impl Item for Terminal {
|
||||
fn tab_content(&self, tab_theme: &theme::Tab, cx: &gpui::AppContext) -> ElementBox {
|
||||
fn tab_content(
|
||||
&self,
|
||||
_detail: Option<usize>,
|
||||
tab_theme: &theme::Tab,
|
||||
cx: &gpui::AppContext,
|
||||
) -> ElementBox {
|
||||
let settings = cx.global::<Settings>();
|
||||
let search_theme = &settings.theme.search; //TODO properly integrate themes
|
||||
|
||||
|
@ -429,12 +378,41 @@ impl Item for Terminal {
|
|||
}
|
||||
}
|
||||
|
||||
///Get's the working directory for the given workspace, respecting the user's settings.
|
||||
fn get_wd_for_workspace(workspace: &Workspace, cx: &AppContext) -> Option<PathBuf> {
|
||||
let wd_setting = cx
|
||||
.global::<Settings>()
|
||||
.terminal_overrides
|
||||
.working_directory
|
||||
.clone()
|
||||
.unwrap_or(WorkingDirectory::CurrentProjectDirectory);
|
||||
let res = match wd_setting {
|
||||
WorkingDirectory::CurrentProjectDirectory => current_project_directory(workspace, cx),
|
||||
WorkingDirectory::FirstProjectDirectory => first_project_directory(workspace, cx),
|
||||
WorkingDirectory::AlwaysHome => None,
|
||||
WorkingDirectory::Always { directory } => shellexpand::full(&directory)
|
||||
.ok()
|
||||
.map(|dir| Path::new(&dir.to_string()).to_path_buf())
|
||||
.filter(|dir| dir.is_dir()),
|
||||
};
|
||||
res.or_else(|| home_dir())
|
||||
}
|
||||
|
||||
///Get's the first project's home directory, or the home directory
|
||||
fn first_project_directory(workspace: &Workspace, cx: &AppContext) -> Option<PathBuf> {
|
||||
workspace
|
||||
.worktrees(cx)
|
||||
.next()
|
||||
.and_then(|worktree_handle| worktree_handle.read(cx).as_local())
|
||||
.and_then(get_path_from_wt)
|
||||
}
|
||||
|
||||
///Gets the intuitively correct working directory from the given workspace
|
||||
///If there is an active entry for this project, returns that entry's worktree root.
|
||||
///If there's no active entry but there is a worktree, returns that worktrees root.
|
||||
///If either of these roots are files, or if there are any other query failures,
|
||||
/// returns the user's home directory
|
||||
fn get_wd_for_workspace(workspace: &Workspace, cx: &AppContext) -> Option<PathBuf> {
|
||||
fn current_project_directory(workspace: &Workspace, cx: &AppContext) -> Option<PathBuf> {
|
||||
let project = workspace.project().read(cx);
|
||||
|
||||
project
|
||||
|
@ -442,96 +420,36 @@ fn get_wd_for_workspace(workspace: &Workspace, cx: &AppContext) -> Option<PathBu
|
|||
.and_then(|entry_id| project.worktree_for_entry(entry_id, cx))
|
||||
.or_else(|| workspace.worktrees(cx).next())
|
||||
.and_then(|worktree_handle| worktree_handle.read(cx).as_local())
|
||||
.and_then(|wt| {
|
||||
wt.root_entry()
|
||||
.filter(|re| re.is_dir())
|
||||
.map(|_| wt.abs_path().to_path_buf())
|
||||
})
|
||||
.or_else(|| home_dir())
|
||||
.and_then(get_path_from_wt)
|
||||
}
|
||||
|
||||
fn get_path_from_wt(wt: &LocalWorktree) -> Option<PathBuf> {
|
||||
wt.root_entry()
|
||||
.filter(|re| re.is_dir())
|
||||
.map(|_| wt.abs_path().to_path_buf())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
use alacritty_terminal::{
|
||||
grid::GridIterator,
|
||||
index::{Column, Line, Point, Side},
|
||||
selection::{Selection, SelectionType},
|
||||
term::cell::Cell,
|
||||
};
|
||||
use gpui::TestAppContext;
|
||||
use itertools::Itertools;
|
||||
use crate::tests::terminal_test_context::TerminalTestContext;
|
||||
|
||||
use std::{path::Path, time::Duration};
|
||||
use super::*;
|
||||
use gpui::TestAppContext;
|
||||
|
||||
use std::path::Path;
|
||||
use workspace::AppState;
|
||||
|
||||
mod terminal_test_context;
|
||||
|
||||
///Basic integration test, can we get the terminal to show up, execute a command,
|
||||
//and produce noticable output?
|
||||
#[gpui::test]
|
||||
#[gpui::test(retries = 5)]
|
||||
async fn test_terminal(cx: &mut TestAppContext) {
|
||||
let terminal = cx.add_view(Default::default(), |cx| Terminal::new(None, false, cx));
|
||||
let mut cx = TerminalTestContext::new(cx);
|
||||
|
||||
terminal.update(cx, |terminal, cx| {
|
||||
terminal.connection.update(cx, |connection, _| {
|
||||
connection.write_to_pty("expr 3 + 4".to_string());
|
||||
});
|
||||
terminal.carriage_return(&Return, cx);
|
||||
});
|
||||
|
||||
cx.set_condition_duration(Some(Duration::from_secs(2)));
|
||||
terminal
|
||||
.condition(cx, |terminal, cx| {
|
||||
let term = terminal.connection.read(cx).term.clone();
|
||||
let content = grid_as_str(term.lock().renderable_content().display_iter);
|
||||
content.contains("7")
|
||||
})
|
||||
cx.execute_and_wait("expr 3 + 4", |content, _cx| content.contains("7"))
|
||||
.await;
|
||||
cx.set_condition_duration(None);
|
||||
}
|
||||
|
||||
/// Integration test for selections, clipboard, and terminal execution
|
||||
#[gpui::test]
|
||||
async fn test_copy(cx: &mut TestAppContext) {
|
||||
let mut result_line: i32 = 0;
|
||||
let terminal = cx.add_view(Default::default(), |cx| Terminal::new(None, false, cx));
|
||||
cx.set_condition_duration(Some(Duration::from_secs(2)));
|
||||
|
||||
terminal.update(cx, |terminal, cx| {
|
||||
terminal.connection.update(cx, |connection, _| {
|
||||
connection.write_to_pty("expr 3 + 4".to_string());
|
||||
});
|
||||
terminal.carriage_return(&Return, cx);
|
||||
});
|
||||
|
||||
terminal
|
||||
.condition(cx, |terminal, cx| {
|
||||
let term = terminal.connection.read(cx).term.clone();
|
||||
let content = grid_as_str(term.lock().renderable_content().display_iter);
|
||||
|
||||
if content.contains("7") {
|
||||
let idx = content.chars().position(|c| c == '7').unwrap();
|
||||
result_line = content.chars().take(idx).filter(|c| *c == '\n').count() as i32;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
terminal.update(cx, |terminal, cx| {
|
||||
let mut term = terminal.connection.read(cx).term.lock();
|
||||
term.selection = Some(Selection::new(
|
||||
SelectionType::Semantic,
|
||||
Point::new(Line(2), Column(0)),
|
||||
Side::Right,
|
||||
));
|
||||
drop(term);
|
||||
terminal.copy(&Copy, cx)
|
||||
});
|
||||
|
||||
cx.assert_clipboard_content(Some(&"7"));
|
||||
cx.set_condition_duration(None);
|
||||
}
|
||||
|
||||
///Working directory calculation tests
|
||||
|
@ -553,8 +471,10 @@ mod tests {
|
|||
assert!(active_entry.is_none());
|
||||
assert!(workspace.worktrees(cx).next().is_none());
|
||||
|
||||
let res = get_wd_for_workspace(workspace, cx);
|
||||
assert_eq!(res, home_dir())
|
||||
let res = current_project_directory(workspace, cx);
|
||||
assert_eq!(res, None);
|
||||
let res = first_project_directory(workspace, cx);
|
||||
assert_eq!(res, None);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -591,8 +511,10 @@ mod tests {
|
|||
assert!(active_entry.is_none());
|
||||
assert!(workspace.worktrees(cx).next().is_some());
|
||||
|
||||
let res = get_wd_for_workspace(workspace, cx);
|
||||
assert_eq!(res, home_dir())
|
||||
let res = current_project_directory(workspace, cx);
|
||||
assert_eq!(res, None);
|
||||
let res = first_project_directory(workspace, cx);
|
||||
assert_eq!(res, None);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -627,7 +549,9 @@ mod tests {
|
|||
assert!(active_entry.is_none());
|
||||
assert!(workspace.worktrees(cx).next().is_some());
|
||||
|
||||
let res = get_wd_for_workspace(workspace, cx);
|
||||
let res = current_project_directory(workspace, cx);
|
||||
assert_eq!(res, Some((Path::new("/root/")).to_path_buf()));
|
||||
let res = first_project_directory(workspace, cx);
|
||||
assert_eq!(res, Some((Path::new("/root/")).to_path_buf()));
|
||||
});
|
||||
}
|
||||
|
@ -639,17 +563,32 @@ mod tests {
|
|||
let params = cx.update(AppState::test);
|
||||
let project = Project::test(params.fs.clone(), [], cx).await;
|
||||
let (_, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx));
|
||||
let (wt, _) = project
|
||||
let (wt1, _) = project
|
||||
.update(cx, |project, cx| {
|
||||
project.find_or_create_local_worktree("/root.txt", true, cx)
|
||||
project.find_or_create_local_worktree("/root1/", true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let (wt2, _) = project
|
||||
.update(cx, |project, cx| {
|
||||
project.find_or_create_local_worktree("/root2.txt", true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
//Setup root
|
||||
let entry = cx
|
||||
let _ = cx
|
||||
.update(|cx| {
|
||||
wt.update(cx, |wt, cx| {
|
||||
wt1.update(cx, |wt, cx| {
|
||||
wt.as_local().unwrap().create_entry(Path::new(""), true, cx)
|
||||
})
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let entry2 = cx
|
||||
.update(|cx| {
|
||||
wt2.update(cx, |wt, cx| {
|
||||
wt.as_local()
|
||||
.unwrap()
|
||||
.create_entry(Path::new(""), false, cx)
|
||||
|
@ -660,8 +599,8 @@ mod tests {
|
|||
|
||||
cx.update(|cx| {
|
||||
let p = ProjectPath {
|
||||
worktree_id: wt.read(cx).id(),
|
||||
path: entry.path,
|
||||
worktree_id: wt2.read(cx).id(),
|
||||
path: entry2.path,
|
||||
};
|
||||
project.update(cx, |project, cx| project.set_active_path(Some(p), cx));
|
||||
});
|
||||
|
@ -673,8 +612,10 @@ mod tests {
|
|||
|
||||
assert!(active_entry.is_some());
|
||||
|
||||
let res = get_wd_for_workspace(workspace, cx);
|
||||
assert_eq!(res, home_dir());
|
||||
let res = current_project_directory(workspace, cx);
|
||||
assert_eq!(res, None);
|
||||
let res = first_project_directory(workspace, cx);
|
||||
assert_eq!(res, Some((Path::new("/root1/")).to_path_buf()));
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -685,17 +626,32 @@ mod tests {
|
|||
let params = cx.update(AppState::test);
|
||||
let project = Project::test(params.fs.clone(), [], cx).await;
|
||||
let (_, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx));
|
||||
let (wt, _) = project
|
||||
let (wt1, _) = project
|
||||
.update(cx, |project, cx| {
|
||||
project.find_or_create_local_worktree("/root/", true, cx)
|
||||
project.find_or_create_local_worktree("/root1/", true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let (wt2, _) = project
|
||||
.update(cx, |project, cx| {
|
||||
project.find_or_create_local_worktree("/root2/", true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
//Setup root
|
||||
let entry = cx
|
||||
let _ = cx
|
||||
.update(|cx| {
|
||||
wt.update(cx, |wt, cx| {
|
||||
wt1.update(cx, |wt, cx| {
|
||||
wt.as_local().unwrap().create_entry(Path::new(""), true, cx)
|
||||
})
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let entry2 = cx
|
||||
.update(|cx| {
|
||||
wt2.update(cx, |wt, cx| {
|
||||
wt.as_local().unwrap().create_entry(Path::new(""), true, cx)
|
||||
})
|
||||
})
|
||||
|
@ -704,8 +660,8 @@ mod tests {
|
|||
|
||||
cx.update(|cx| {
|
||||
let p = ProjectPath {
|
||||
worktree_id: wt.read(cx).id(),
|
||||
path: entry.path,
|
||||
worktree_id: wt2.read(cx).id(),
|
||||
path: entry2.path,
|
||||
};
|
||||
project.update(cx, |project, cx| project.set_active_path(Some(p), cx));
|
||||
});
|
||||
|
@ -717,17 +673,10 @@ mod tests {
|
|||
|
||||
assert!(active_entry.is_some());
|
||||
|
||||
let res = get_wd_for_workspace(workspace, cx);
|
||||
assert_eq!(res, Some((Path::new("/root/")).to_path_buf()));
|
||||
let res = current_project_directory(workspace, cx);
|
||||
assert_eq!(res, Some((Path::new("/root2/")).to_path_buf()));
|
||||
let res = first_project_directory(workspace, cx);
|
||||
assert_eq!(res, Some((Path::new("/root1/")).to_path_buf()));
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn grid_as_str(grid_iterator: GridIterator<Cell>) -> String {
|
||||
let lines = grid_iterator.group_by(|i| i.point.line.0);
|
||||
lines
|
||||
.into_iter()
|
||||
.map(|(_, line)| line.map(|i| i.c).collect::<String>())
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use alacritty_terminal::{
|
||||
grid::{Dimensions, GridIterator, Indexed},
|
||||
grid::{Dimensions, GridIterator, Indexed, Scroll},
|
||||
index::{Column as GridCol, Line as GridLine, Point, Side},
|
||||
selection::{Selection, SelectionRange, SelectionType},
|
||||
sync::FairMutex,
|
||||
|
@ -9,7 +9,7 @@ use alacritty_terminal::{
|
|||
},
|
||||
Term,
|
||||
};
|
||||
use editor::{Cursor, CursorShape, HighlightedRange, HighlightedRangeLine, Input};
|
||||
use editor::{Cursor, CursorShape, HighlightedRange, HighlightedRangeLine};
|
||||
use gpui::{
|
||||
color::Color,
|
||||
elements::*,
|
||||
|
@ -20,20 +20,19 @@ use gpui::{
|
|||
},
|
||||
json::json,
|
||||
text_layout::{Line, RunStyle},
|
||||
Event, FontCache, KeyDownEvent, MouseRegion, PaintContext, Quad, ScrollWheelEvent,
|
||||
SizeConstraint, TextLayoutCache, WeakModelHandle,
|
||||
Event, FontCache, KeyDownEvent, MouseButton, MouseButtonEvent, MouseMovedEvent, MouseRegion,
|
||||
PaintContext, Quad, ScrollWheelEvent, SizeConstraint, TextLayoutCache, WeakModelHandle,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use ordered_float::OrderedFloat;
|
||||
use settings::Settings;
|
||||
use theme::TerminalStyle;
|
||||
use util::ResultExt;
|
||||
|
||||
use std::{cmp::min, ops::Range, rc::Rc, sync::Arc};
|
||||
use std::{cmp::min, ops::Range, sync::Arc};
|
||||
use std::{fmt::Debug, ops::Sub};
|
||||
|
||||
use crate::{
|
||||
color_translation::convert_color, connection::TerminalConnection, ScrollTerminal, ZedListener,
|
||||
};
|
||||
use crate::{color_translation::convert_color, connection::TerminalConnection, ZedListener};
|
||||
|
||||
///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
|
||||
|
@ -258,10 +257,11 @@ impl Element for TerminalEl {
|
|||
for layout_line in &layout.layout_lines {
|
||||
for layout_cell in &layout_line.cells {
|
||||
let position = vec2f(
|
||||
origin.x() + layout_cell.point.column as f32 * layout.em_width.0,
|
||||
(origin.x() + layout_cell.point.column as f32 * layout.em_width.0)
|
||||
.floor(),
|
||||
origin.y() + layout_cell.point.line as f32 * layout.line_height.0,
|
||||
);
|
||||
let size = vec2f(layout.em_width.0, layout.line_height.0);
|
||||
let size = vec2f(layout.em_width.0.ceil(), layout.line_height.0);
|
||||
|
||||
cx.scene.push_quad(Quad {
|
||||
bounds: RectF::new(position, size),
|
||||
|
@ -320,7 +320,7 @@ impl Element for TerminalEl {
|
|||
|
||||
//Don't actually know the start_x for a line, until here:
|
||||
let cell_origin = vec2f(
|
||||
origin.x() + point.column as f32 * layout.em_width.0,
|
||||
(origin.x() + point.column as f32 * layout.em_width.0).floor(),
|
||||
origin.y() + point.line as f32 * layout.line_height.0,
|
||||
);
|
||||
|
||||
|
@ -359,25 +359,6 @@ impl Element for TerminalEl {
|
|||
_paint: &mut Self::PaintState,
|
||||
cx: &mut gpui::EventContext,
|
||||
) -> bool {
|
||||
//The problem:
|
||||
//Depending on the terminal mode, we either send an escape sequence
|
||||
//OR update our own data structures.
|
||||
//e.g. scrolling. If we do smooth scrolling, then we need to check if
|
||||
//we own scrolling and then if so, do our scrolling thing.
|
||||
//Ok, so the terminal connection should have APIs for querying it semantically
|
||||
//something like `should_handle_scroll()`. This means we need a handle to the connection.
|
||||
//Actually, this is the only time that this app needs to talk to the outer world.
|
||||
//TODO for scrolling rework: need a way of intercepting Home/End/PageUp etc.
|
||||
//Sometimes going to scroll our own internal buffer, sometimes going to send ESC
|
||||
//
|
||||
//Same goes for key events
|
||||
//Actually, we don't use the terminal at all in dispatch_event code, the view
|
||||
//Handles it all. Check how the editor implements scrolling, is it view-level
|
||||
//or element level?
|
||||
|
||||
//Question: Can we continue dispatching to the view, so it can talk to the connection
|
||||
//Or should we instead add a connection into here?
|
||||
|
||||
match event {
|
||||
Event::ScrollWheel(ScrollWheelEvent {
|
||||
delta, position, ..
|
||||
|
@ -386,17 +367,30 @@ impl Element for TerminalEl {
|
|||
.then(|| {
|
||||
let vertical_scroll =
|
||||
(delta.y() / layout.line_height.0) * ALACRITTY_SCROLL_MULTIPLIER;
|
||||
cx.dispatch_action(ScrollTerminal(vertical_scroll.round() as i32));
|
||||
})
|
||||
.is_some(),
|
||||
Event::KeyDown(KeyDownEvent {
|
||||
input: Some(input), ..
|
||||
}) => cx
|
||||
.is_parent_view_focused()
|
||||
.then(|| {
|
||||
cx.dispatch_action(Input(input.to_string()));
|
||||
|
||||
if let Some(connection) = self.connection.upgrade(cx.app) {
|
||||
connection.update(cx.app, |connection, _| {
|
||||
connection
|
||||
.term
|
||||
.lock()
|
||||
.scroll_display(Scroll::Delta(vertical_scroll.round() as i32));
|
||||
})
|
||||
}
|
||||
})
|
||||
.is_some(),
|
||||
Event::KeyDown(KeyDownEvent { keystroke, .. }) => {
|
||||
if !cx.is_parent_view_focused() {
|
||||
return false;
|
||||
}
|
||||
|
||||
self.connection
|
||||
.upgrade(cx.app)
|
||||
.map(|connection| {
|
||||
connection
|
||||
.update(cx.app, |connection, _| connection.try_keystroke(keystroke))
|
||||
})
|
||||
.unwrap_or(false)
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
@ -428,14 +422,33 @@ pub fn mouse_to_cell_data(
|
|||
|
||||
///Configures a text style from the current settings.
|
||||
fn make_text_style(font_cache: &FontCache, settings: &Settings) -> TextStyle {
|
||||
// Pull the font family from settings properly overriding
|
||||
let family_id = settings
|
||||
.terminal_overrides
|
||||
.font_family
|
||||
.as_ref()
|
||||
.and_then(|family_name| font_cache.load_family(&[family_name]).log_err())
|
||||
.or_else(|| {
|
||||
settings
|
||||
.terminal_defaults
|
||||
.font_family
|
||||
.as_ref()
|
||||
.and_then(|family_name| font_cache.load_family(&[family_name]).log_err())
|
||||
})
|
||||
.unwrap_or(settings.buffer_font_family);
|
||||
|
||||
TextStyle {
|
||||
color: settings.theme.editor.text_color,
|
||||
font_family_id: settings.buffer_font_family,
|
||||
font_family_name: font_cache.family_name(settings.buffer_font_family).unwrap(),
|
||||
font_family_id: family_id,
|
||||
font_family_name: font_cache.family_name(family_id).unwrap(),
|
||||
font_id: font_cache
|
||||
.select_font(settings.buffer_font_family, &Default::default())
|
||||
.select_font(family_id, &Default::default())
|
||||
.unwrap(),
|
||||
font_size: settings.buffer_font_size,
|
||||
font_size: settings
|
||||
.terminal_overrides
|
||||
.font_size
|
||||
.or(settings.terminal_defaults.font_size)
|
||||
.unwrap_or(settings.buffer_font_size),
|
||||
font_properties: Default::default(),
|
||||
underline: Default::default(),
|
||||
}
|
||||
|
@ -581,63 +594,76 @@ fn attach_mouse_handlers(
|
|||
let drag_mutex = terminal_mutex.clone();
|
||||
let mouse_down_mutex = terminal_mutex.clone();
|
||||
|
||||
cx.scene.push_mouse_region(MouseRegion {
|
||||
view_id,
|
||||
mouse_down: Some(Rc::new(move |pos, _| {
|
||||
let mut term = mouse_down_mutex.lock();
|
||||
let (point, side) = mouse_to_cell_data(
|
||||
pos,
|
||||
origin,
|
||||
cur_size,
|
||||
term.renderable_content().display_offset,
|
||||
);
|
||||
term.selection = Some(Selection::new(SelectionType::Simple, point, side))
|
||||
})),
|
||||
click: Some(Rc::new(move |pos, click_count, cx| {
|
||||
let mut term = click_mutex.lock();
|
||||
cx.scene.push_mouse_region(
|
||||
MouseRegion::new(view_id, None, visible_bounds)
|
||||
.on_down(
|
||||
MouseButton::Left,
|
||||
move |MouseButtonEvent { position, .. }, _| {
|
||||
let mut term = mouse_down_mutex.lock();
|
||||
|
||||
let (point, side) = mouse_to_cell_data(
|
||||
pos,
|
||||
origin,
|
||||
cur_size,
|
||||
term.renderable_content().display_offset,
|
||||
);
|
||||
let (point, side) = mouse_to_cell_data(
|
||||
position,
|
||||
origin,
|
||||
cur_size,
|
||||
term.renderable_content().display_offset,
|
||||
);
|
||||
term.selection = Some(Selection::new(SelectionType::Simple, point, side))
|
||||
},
|
||||
)
|
||||
.on_click(
|
||||
MouseButton::Left,
|
||||
move |MouseButtonEvent {
|
||||
position,
|
||||
click_count,
|
||||
..
|
||||
},
|
||||
cx| {
|
||||
let mut term = click_mutex.lock();
|
||||
|
||||
let selection_type = match click_count {
|
||||
0 => return, //This is a release
|
||||
1 => Some(SelectionType::Simple),
|
||||
2 => Some(SelectionType::Semantic),
|
||||
3 => Some(SelectionType::Lines),
|
||||
_ => None,
|
||||
};
|
||||
let (point, side) = mouse_to_cell_data(
|
||||
position,
|
||||
origin,
|
||||
cur_size,
|
||||
term.renderable_content().display_offset,
|
||||
);
|
||||
|
||||
let selection =
|
||||
selection_type.map(|selection_type| Selection::new(selection_type, point, side));
|
||||
let selection_type = match click_count {
|
||||
0 => return, //This is a release
|
||||
1 => Some(SelectionType::Simple),
|
||||
2 => Some(SelectionType::Semantic),
|
||||
3 => Some(SelectionType::Lines),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
term.selection = selection;
|
||||
cx.focus_parent_view();
|
||||
cx.notify();
|
||||
})),
|
||||
bounds: visible_bounds,
|
||||
drag: Some(Rc::new(move |_delta, pos, cx| {
|
||||
let mut term = drag_mutex.lock();
|
||||
let selection = selection_type
|
||||
.map(|selection_type| Selection::new(selection_type, point, side));
|
||||
|
||||
let (point, side) = mouse_to_cell_data(
|
||||
pos,
|
||||
origin,
|
||||
cur_size,
|
||||
term.renderable_content().display_offset,
|
||||
);
|
||||
term.selection = selection;
|
||||
cx.focus_parent_view();
|
||||
cx.notify();
|
||||
},
|
||||
)
|
||||
.on_drag(
|
||||
MouseButton::Left,
|
||||
move |_, MouseMovedEvent { position, .. }, cx| {
|
||||
let mut term = drag_mutex.lock();
|
||||
|
||||
if let Some(mut selection) = term.selection.take() {
|
||||
selection.update(point, side);
|
||||
term.selection = Some(selection);
|
||||
}
|
||||
let (point, side) = mouse_to_cell_data(
|
||||
position,
|
||||
origin,
|
||||
cur_size,
|
||||
term.renderable_content().display_offset,
|
||||
);
|
||||
|
||||
cx.notify();
|
||||
})),
|
||||
..Default::default()
|
||||
});
|
||||
if let Some(mut selection) = term.selection.take() {
|
||||
selection.update(point, side);
|
||||
term.selection = Some(selection);
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
///Copied (with modifications) from alacritty/src/input.rs > Processor::cell_side()
|
||||
|
|
76
crates/terminal/src/tests/terminal_test_context.rs
Normal file
76
crates/terminal/src/tests/terminal_test_context.rs
Normal file
|
@ -0,0 +1,76 @@
|
|||
use std::time::Duration;
|
||||
|
||||
use alacritty_terminal::term::SizeInfo;
|
||||
use gpui::{AppContext, ModelHandle, ReadModelWith, TestAppContext};
|
||||
use itertools::Itertools;
|
||||
|
||||
use crate::{
|
||||
connection::TerminalConnection, DEBUG_CELL_WIDTH, DEBUG_LINE_HEIGHT, DEBUG_TERMINAL_HEIGHT,
|
||||
DEBUG_TERMINAL_WIDTH,
|
||||
};
|
||||
|
||||
pub struct TerminalTestContext<'a> {
|
||||
pub cx: &'a mut TestAppContext,
|
||||
pub connection: ModelHandle<TerminalConnection>,
|
||||
}
|
||||
|
||||
impl<'a> TerminalTestContext<'a> {
|
||||
pub fn new(cx: &'a mut TestAppContext) -> Self {
|
||||
cx.set_condition_duration(Some(Duration::from_secs(5)));
|
||||
|
||||
let size_info = SizeInfo::new(
|
||||
DEBUG_TERMINAL_WIDTH,
|
||||
DEBUG_TERMINAL_HEIGHT,
|
||||
DEBUG_CELL_WIDTH,
|
||||
DEBUG_LINE_HEIGHT,
|
||||
0.,
|
||||
0.,
|
||||
false,
|
||||
);
|
||||
|
||||
let connection =
|
||||
cx.add_model(|cx| TerminalConnection::new(None, None, None, size_info, cx));
|
||||
|
||||
TerminalTestContext { cx, connection }
|
||||
}
|
||||
|
||||
pub async fn execute_and_wait<F>(&mut self, command: &str, f: F) -> String
|
||||
where
|
||||
F: Fn(String, &AppContext) -> bool,
|
||||
{
|
||||
let command = command.to_string();
|
||||
self.connection.update(self.cx, |connection, _| {
|
||||
connection.write_to_pty(command);
|
||||
connection.write_to_pty("\r".to_string());
|
||||
});
|
||||
|
||||
self.connection
|
||||
.condition(self.cx, |conn, cx| {
|
||||
let content = Self::grid_as_str(conn);
|
||||
f(content, cx)
|
||||
})
|
||||
.await;
|
||||
|
||||
self.cx
|
||||
.read_model_with(&self.connection, &mut |conn, _: &AppContext| {
|
||||
Self::grid_as_str(conn)
|
||||
})
|
||||
}
|
||||
|
||||
fn grid_as_str(connection: &TerminalConnection) -> String {
|
||||
let term = connection.term.lock();
|
||||
let grid_iterator = term.renderable_content().display_iter;
|
||||
let lines = grid_iterator.group_by(|i| i.point.line.0);
|
||||
lines
|
||||
.into_iter()
|
||||
.map(|(_, line)| line.map(|i| i.c).collect::<String>())
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n")
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Drop for TerminalTestContext<'a> {
|
||||
fn drop(&mut self) {
|
||||
self.cx.set_condition_duration(None);
|
||||
}
|
||||
}
|
|
@ -93,6 +93,7 @@ pub struct Tab {
|
|||
pub container: ContainerStyle,
|
||||
#[serde(flatten)]
|
||||
pub label: LabelStyle,
|
||||
pub description: ContainedText,
|
||||
pub spacing: f32,
|
||||
pub icon_width: f32,
|
||||
pub icon_close: Color,
|
||||
|
|
|
@ -24,7 +24,7 @@ pub fn marked_text(marked_text: &str) -> (String, Vec<usize>) {
|
|||
(unmarked_text, markers.remove(&'|').unwrap_or_default())
|
||||
}
|
||||
|
||||
#[derive(Eq, PartialEq, Hash)]
|
||||
#[derive(Clone, Eq, PartialEq, Hash)]
|
||||
pub enum TextRangeMarker {
|
||||
Empty(char),
|
||||
Range(char, char),
|
||||
|
|
|
@ -13,8 +13,9 @@ use gpui::{
|
|||
},
|
||||
impl_actions, impl_internal_actions,
|
||||
platform::{CursorStyle, NavigationDirection},
|
||||
AppContext, AsyncAppContext, Entity, ModelHandle, MutableAppContext, PromptLevel, Quad,
|
||||
RenderContext, Task, View, ViewContext, ViewHandle, WeakViewHandle,
|
||||
AppContext, AsyncAppContext, Entity, ModelHandle, MouseButton, MouseButtonEvent,
|
||||
MutableAppContext, PromptLevel, Quad, RenderContext, Task, View, ViewContext, ViewHandle,
|
||||
WeakViewHandle,
|
||||
};
|
||||
use project::{Project, ProjectEntryId, ProjectPath};
|
||||
use serde::Deserialize;
|
||||
|
@ -71,10 +72,10 @@ const MAX_NAVIGATION_HISTORY_LEN: usize = 1024;
|
|||
|
||||
pub fn init(cx: &mut MutableAppContext) {
|
||||
cx.add_action(|pane: &mut Pane, action: &ActivateItem, cx| {
|
||||
pane.activate_item(action.0, true, true, cx);
|
||||
pane.activate_item(action.0, true, true, false, cx);
|
||||
});
|
||||
cx.add_action(|pane: &mut Pane, _: &ActivateLastItem, cx| {
|
||||
pane.activate_item(pane.items.len() - 1, true, true, cx);
|
||||
pane.activate_item(pane.items.len() - 1, true, true, false, cx);
|
||||
});
|
||||
cx.add_action(|pane: &mut Pane, _: &ActivatePrevItem, cx| {
|
||||
pane.activate_prev_item(cx);
|
||||
|
@ -288,7 +289,7 @@ impl Pane {
|
|||
{
|
||||
let prev_active_item_index = pane.active_item_index;
|
||||
pane.nav_history.borrow_mut().set_mode(mode);
|
||||
pane.activate_item(index, true, true, cx);
|
||||
pane.activate_item(index, true, true, false, cx);
|
||||
pane.nav_history
|
||||
.borrow_mut()
|
||||
.set_mode(NavigationMode::Normal);
|
||||
|
@ -380,7 +381,7 @@ impl Pane {
|
|||
&& item.project_entry_ids(cx).as_slice() == &[project_entry_id]
|
||||
{
|
||||
let item = item.boxed_clone();
|
||||
pane.activate_item(ix, true, focus_item, cx);
|
||||
pane.activate_item(ix, true, focus_item, true, cx);
|
||||
return Some(item);
|
||||
}
|
||||
}
|
||||
|
@ -404,9 +405,11 @@ impl Pane {
|
|||
cx: &mut ViewContext<Workspace>,
|
||||
) {
|
||||
// Prevent adding the same item to the pane more than once.
|
||||
// If there is already an active item, reorder the desired item to be after it
|
||||
// and activate it.
|
||||
if let Some(item_ix) = pane.read(cx).items.iter().position(|i| i.id() == item.id()) {
|
||||
pane.update(cx, |pane, cx| {
|
||||
pane.activate_item(item_ix, activate_pane, focus_item, cx)
|
||||
pane.activate_item(item_ix, activate_pane, focus_item, true, cx)
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
@ -426,7 +429,7 @@ impl Pane {
|
|||
};
|
||||
|
||||
pane.items.insert(item_ix, item);
|
||||
pane.activate_item(item_ix, activate_pane, focus_item, cx);
|
||||
pane.activate_item(item_ix, activate_pane, focus_item, false, cx);
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
|
@ -465,13 +468,31 @@ impl Pane {
|
|||
|
||||
pub fn activate_item(
|
||||
&mut self,
|
||||
index: usize,
|
||||
mut index: usize,
|
||||
activate_pane: bool,
|
||||
focus_item: bool,
|
||||
move_after_current_active: bool,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
use NavigationMode::{GoingBack, GoingForward};
|
||||
if index < self.items.len() {
|
||||
if move_after_current_active {
|
||||
// If there is already an active item, reorder the desired item to be after it
|
||||
// and activate it.
|
||||
if self.active_item_index != index && self.active_item_index < self.items.len() {
|
||||
let pane_to_activate = self.items.remove(index);
|
||||
if self.active_item_index < index {
|
||||
index = self.active_item_index + 1;
|
||||
} else if self.active_item_index < self.items.len() + 1 {
|
||||
index = self.active_item_index;
|
||||
// Index is less than active_item_index. Reordering will decrement the
|
||||
// active_item_index, so adjust it accordingly
|
||||
self.active_item_index = index - 1;
|
||||
}
|
||||
self.items.insert(index, pane_to_activate);
|
||||
}
|
||||
}
|
||||
|
||||
let prev_active_item_ix = mem::replace(&mut self.active_item_index, index);
|
||||
if prev_active_item_ix != self.active_item_index
|
||||
|| matches!(self.nav_history.borrow().mode, GoingBack | GoingForward)
|
||||
|
@ -502,7 +523,7 @@ impl Pane {
|
|||
} else if self.items.len() > 0 {
|
||||
index = self.items.len() - 1;
|
||||
}
|
||||
self.activate_item(index, true, true, cx);
|
||||
self.activate_item(index, true, true, false, cx);
|
||||
}
|
||||
|
||||
pub fn activate_next_item(&mut self, cx: &mut ViewContext<Self>) {
|
||||
|
@ -512,7 +533,7 @@ impl Pane {
|
|||
} else {
|
||||
index = 0;
|
||||
}
|
||||
self.activate_item(index, true, true, cx);
|
||||
self.activate_item(index, true, true, false, cx);
|
||||
}
|
||||
|
||||
pub fn close_active_item(
|
||||
|
@ -641,10 +662,13 @@ impl Pane {
|
|||
pane.update(&mut cx, |pane, cx| {
|
||||
if let Some(item_ix) = pane.items.iter().position(|i| i.id() == item.id()) {
|
||||
if item_ix == pane.active_item_index {
|
||||
if item_ix + 1 < pane.items.len() {
|
||||
pane.activate_next_item(cx);
|
||||
} else if item_ix > 0 {
|
||||
// Activate the previous item if possible.
|
||||
// This returns the user to the previously opened tab if they closed
|
||||
// a ne item they just navigated to.
|
||||
if item_ix > 0 {
|
||||
pane.activate_prev_item(cx);
|
||||
} else if item_ix + 1 < pane.items.len() {
|
||||
pane.activate_next_item(cx);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -712,7 +736,7 @@ impl Pane {
|
|||
|
||||
if has_conflict && can_save {
|
||||
let mut answer = pane.update(cx, |pane, cx| {
|
||||
pane.activate_item(item_ix, true, true, cx);
|
||||
pane.activate_item(item_ix, true, true, false, cx);
|
||||
cx.prompt(
|
||||
PromptLevel::Warning,
|
||||
CONFLICT_MESSAGE,
|
||||
|
@ -733,7 +757,7 @@ impl Pane {
|
|||
});
|
||||
let should_save = if should_prompt_for_save && !will_autosave {
|
||||
let mut answer = pane.update(cx, |pane, cx| {
|
||||
pane.activate_item(item_ix, true, true, cx);
|
||||
pane.activate_item(item_ix, true, true, false, cx);
|
||||
cx.prompt(
|
||||
PromptLevel::Warning,
|
||||
DIRTY_MESSAGE,
|
||||
|
@ -840,8 +864,10 @@ impl Pane {
|
|||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mut row = Flex::row().scrollable::<Tabs, _>(1, autoscroll, cx);
|
||||
for (ix, item) in self.items.iter().enumerate() {
|
||||
for (ix, (item, detail)) in self.items.iter().zip(self.tab_details(cx)).enumerate() {
|
||||
let detail = if detail == 0 { None } else { Some(detail) };
|
||||
let is_active = ix == self.active_item_index;
|
||||
|
||||
row.add_child({
|
||||
|
@ -850,7 +876,7 @@ impl Pane {
|
|||
} else {
|
||||
theme.workspace.tab.clone()
|
||||
};
|
||||
let title = item.tab_content(&tab_style, cx);
|
||||
let title = item.tab_content(detail, &tab_style, cx);
|
||||
|
||||
let mut style = if is_active {
|
||||
theme.workspace.active_tab.clone()
|
||||
|
@ -930,9 +956,9 @@ impl Pane {
|
|||
)
|
||||
.with_padding(Padding::uniform(4.))
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click({
|
||||
.on_click(MouseButton::Left, {
|
||||
let pane = pane.clone();
|
||||
move |_, _, cx| {
|
||||
move |_, cx| {
|
||||
cx.dispatch_action(CloseItem {
|
||||
item_id,
|
||||
pane: pane.clone(),
|
||||
|
@ -953,7 +979,7 @@ impl Pane {
|
|||
.with_style(style.container)
|
||||
.boxed()
|
||||
})
|
||||
.on_mouse_down(move |_, cx| {
|
||||
.on_mouse_down(MouseButton::Left, move |_, cx| {
|
||||
cx.dispatch_action(ActivateItem(ix));
|
||||
})
|
||||
.boxed()
|
||||
|
@ -971,6 +997,43 @@ impl Pane {
|
|||
row.boxed()
|
||||
})
|
||||
}
|
||||
|
||||
fn tab_details(&self, cx: &AppContext) -> Vec<usize> {
|
||||
let mut tab_details = (0..self.items.len()).map(|_| 0).collect::<Vec<_>>();
|
||||
|
||||
let mut tab_descriptions = HashMap::default();
|
||||
let mut done = false;
|
||||
while !done {
|
||||
done = true;
|
||||
|
||||
// Store item indices by their tab description.
|
||||
for (ix, (item, detail)) in self.items.iter().zip(&tab_details).enumerate() {
|
||||
if let Some(description) = item.tab_description(*detail, cx) {
|
||||
if *detail == 0
|
||||
|| Some(&description) != item.tab_description(detail - 1, cx).as_ref()
|
||||
{
|
||||
tab_descriptions
|
||||
.entry(description)
|
||||
.or_insert(Vec::new())
|
||||
.push(ix);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If two or more items have the same tab description, increase their level
|
||||
// of detail and try again.
|
||||
for (_, item_ixs) in tab_descriptions.drain() {
|
||||
if item_ixs.len() > 1 {
|
||||
done = false;
|
||||
for ix in item_ixs {
|
||||
tab_details[ix] += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tab_details
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for Pane {
|
||||
|
@ -1017,9 +1080,12 @@ impl View for Pane {
|
|||
},
|
||||
)
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_mouse_down(|position, cx| {
|
||||
cx.dispatch_action(DeploySplitMenu { position });
|
||||
})
|
||||
.on_mouse_down(
|
||||
MouseButton::Left,
|
||||
|MouseButtonEvent { position, .. }, cx| {
|
||||
cx.dispatch_action(DeploySplitMenu { position });
|
||||
},
|
||||
)
|
||||
.boxed(),
|
||||
)
|
||||
.constrained()
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use crate::StatusItemView;
|
||||
use gpui::{
|
||||
elements::*, impl_actions, platform::CursorStyle, AnyViewHandle, AppContext, Entity,
|
||||
RenderContext, Subscription, View, ViewContext, ViewHandle,
|
||||
MouseButton, MouseMovedEvent, RenderContext, Subscription, View, ViewContext, ViewHandle,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use settings::Settings;
|
||||
|
@ -187,19 +187,27 @@ impl Sidebar {
|
|||
..Default::default()
|
||||
})
|
||||
.with_cursor_style(CursorStyle::ResizeLeftRight)
|
||||
.on_mouse_down(|_, _| {}) // This prevents the mouse down event from being propagated elsewhere
|
||||
.on_drag(move |old_position, new_position, cx| {
|
||||
let delta = new_position.x() - old_position.x();
|
||||
let prev_width = *actual_width.borrow();
|
||||
*custom_width.borrow_mut() = 0f32
|
||||
.max(match side {
|
||||
Side::Left => prev_width + delta,
|
||||
Side::Right => prev_width - delta,
|
||||
})
|
||||
.round();
|
||||
.on_mouse_down(MouseButton::Left, |_, _| {}) // This prevents the mouse down event from being propagated elsewhere
|
||||
.on_drag(
|
||||
MouseButton::Left,
|
||||
move |old_position,
|
||||
MouseMovedEvent {
|
||||
position: new_position,
|
||||
..
|
||||
},
|
||||
cx| {
|
||||
let delta = new_position.x() - old_position.x();
|
||||
let prev_width = *actual_width.borrow();
|
||||
*custom_width.borrow_mut() = 0f32
|
||||
.max(match side {
|
||||
Side::Left => prev_width + delta,
|
||||
Side::Right => prev_width - delta,
|
||||
})
|
||||
.round();
|
||||
|
||||
cx.notify();
|
||||
})
|
||||
cx.notify();
|
||||
},
|
||||
)
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
|
@ -314,9 +322,9 @@ impl View for SidebarButtons {
|
|||
.boxed()
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click({
|
||||
.on_click(MouseButton::Left, {
|
||||
let action = action.clone();
|
||||
move |_, _, cx| cx.dispatch_action(action.clone())
|
||||
move |_, cx| cx.dispatch_action(action.clone())
|
||||
})
|
||||
.with_tooltip::<Self, _>(
|
||||
ix,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use crate::{ItemHandle, Pane};
|
||||
use gpui::{
|
||||
elements::*, platform::CursorStyle, Action, AnyViewHandle, AppContext, ElementBox, Entity,
|
||||
MutableAppContext, RenderContext, View, ViewContext, ViewHandle, WeakViewHandle,
|
||||
MouseButton, MutableAppContext, RenderContext, View, ViewContext, ViewHandle, WeakViewHandle,
|
||||
};
|
||||
use settings::Settings;
|
||||
|
||||
|
@ -191,7 +191,9 @@ fn nav_button<A: Action + Clone>(
|
|||
} else {
|
||||
CursorStyle::default()
|
||||
})
|
||||
.on_click(move |_, _, cx| cx.dispatch_action(action.clone()))
|
||||
.on_click(MouseButton::Left, move |_, cx| {
|
||||
cx.dispatch_action(action.clone())
|
||||
})
|
||||
.with_tooltip::<A, _>(
|
||||
0,
|
||||
action_name.to_string(),
|
||||
|
|
|
@ -21,8 +21,8 @@ use gpui::{
|
|||
json::{self, ToJson},
|
||||
platform::{CursorStyle, WindowOptions},
|
||||
AnyModelHandle, AnyViewHandle, AppContext, AsyncAppContext, Border, Entity, ImageData,
|
||||
ModelContext, ModelHandle, MutableAppContext, PathPromptOptions, PromptLevel, RenderContext,
|
||||
Task, View, ViewContext, ViewHandle, WeakViewHandle,
|
||||
ModelContext, ModelHandle, MouseButton, MutableAppContext, PathPromptOptions, PromptLevel,
|
||||
RenderContext, Task, View, ViewContext, ViewHandle, WeakViewHandle,
|
||||
};
|
||||
use language::LanguageRegistry;
|
||||
use log::error;
|
||||
|
@ -256,7 +256,11 @@ pub trait Item: View {
|
|||
fn navigate(&mut self, _: Box<dyn Any>, _: &mut ViewContext<Self>) -> bool {
|
||||
false
|
||||
}
|
||||
fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox;
|
||||
fn tab_description<'a>(&'a self, _: usize, _: &'a AppContext) -> Option<Cow<'a, str>> {
|
||||
None
|
||||
}
|
||||
fn tab_content(&self, detail: Option<usize>, style: &theme::Tab, cx: &AppContext)
|
||||
-> ElementBox;
|
||||
fn project_path(&self, cx: &AppContext) -> Option<ProjectPath>;
|
||||
fn project_entry_ids(&self, cx: &AppContext) -> SmallVec<[ProjectEntryId; 3]>;
|
||||
fn is_singleton(&self, cx: &AppContext) -> bool;
|
||||
|
@ -409,7 +413,9 @@ impl<T: FollowableItem> FollowableItemHandle for ViewHandle<T> {
|
|||
}
|
||||
|
||||
pub trait ItemHandle: 'static + fmt::Debug {
|
||||
fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox;
|
||||
fn tab_description<'a>(&self, detail: usize, cx: &'a AppContext) -> Option<Cow<'a, str>>;
|
||||
fn tab_content(&self, detail: Option<usize>, style: &theme::Tab, cx: &AppContext)
|
||||
-> ElementBox;
|
||||
fn project_path(&self, cx: &AppContext) -> Option<ProjectPath>;
|
||||
fn project_entry_ids(&self, cx: &AppContext) -> SmallVec<[ProjectEntryId; 3]>;
|
||||
fn is_singleton(&self, cx: &AppContext) -> bool;
|
||||
|
@ -463,8 +469,17 @@ impl dyn ItemHandle {
|
|||
}
|
||||
|
||||
impl<T: Item> ItemHandle for ViewHandle<T> {
|
||||
fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox {
|
||||
self.read(cx).tab_content(style, cx)
|
||||
fn tab_description<'a>(&self, detail: usize, cx: &'a AppContext) -> Option<Cow<'a, str>> {
|
||||
self.read(cx).tab_description(detail, cx)
|
||||
}
|
||||
|
||||
fn tab_content(
|
||||
&self,
|
||||
detail: Option<usize>,
|
||||
style: &theme::Tab,
|
||||
cx: &AppContext,
|
||||
) -> ElementBox {
|
||||
self.read(cx).tab_content(detail, style, cx)
|
||||
}
|
||||
|
||||
fn project_path(&self, cx: &AppContext) -> Option<ProjectPath> {
|
||||
|
@ -562,7 +577,7 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
|
|||
if T::should_activate_item_on_event(event) {
|
||||
pane.update(cx, |pane, cx| {
|
||||
if let Some(ix) = pane.index_for_item(&item) {
|
||||
pane.activate_item(ix, true, true, cx);
|
||||
pane.activate_item(ix, true, true, false, cx);
|
||||
pane.activate(cx);
|
||||
}
|
||||
});
|
||||
|
@ -1507,7 +1522,7 @@ impl Workspace {
|
|||
});
|
||||
if let Some((pane, ix)) = result {
|
||||
self.activate_pane(pane.clone(), cx);
|
||||
pane.update(cx, |pane, cx| pane.activate_item(ix, true, true, cx));
|
||||
pane.update(cx, |pane, cx| pane.activate_item(ix, true, true, false, cx));
|
||||
true
|
||||
} else {
|
||||
false
|
||||
|
@ -1965,7 +1980,7 @@ impl Workspace {
|
|||
.with_style(style.container)
|
||||
.boxed()
|
||||
})
|
||||
.on_click(|_, _, cx| cx.dispatch_action(Authenticate))
|
||||
.on_click(MouseButton::Left, |_, cx| cx.dispatch_action(Authenticate))
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.aligned()
|
||||
.boxed(),
|
||||
|
@ -2016,7 +2031,9 @@ impl Workspace {
|
|||
if let Some((peer_id, peer_github_login)) = peer {
|
||||
MouseEventHandler::new::<ToggleFollow, _, _>(replica_id.into(), cx, move |_, _| content)
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(move |_, _, cx| cx.dispatch_action(ToggleFollow(peer_id)))
|
||||
.on_click(MouseButton::Left, move |_, cx| {
|
||||
cx.dispatch_action(ToggleFollow(peer_id))
|
||||
})
|
||||
.with_tooltip::<ToggleFollow, _>(
|
||||
peer_id.0 as usize,
|
||||
if is_followed {
|
||||
|
@ -2686,11 +2703,62 @@ fn open_new(app_state: &Arc<AppState>, cx: &mut MutableAppContext) {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::cell::Cell;
|
||||
|
||||
use super::*;
|
||||
use gpui::{executor::Deterministic, ModelHandle, TestAppContext, ViewContext};
|
||||
use project::{FakeFs, Project, ProjectEntryId};
|
||||
use serde_json::json;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_tab_disambiguation(cx: &mut TestAppContext) {
|
||||
cx.foreground().forbid_parking();
|
||||
Settings::test_async(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.background());
|
||||
let project = Project::test(fs, [], cx).await;
|
||||
let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx));
|
||||
|
||||
// Adding an item with no ambiguity renders the tab without detail.
|
||||
let item1 = cx.add_view(window_id, |_| {
|
||||
let mut item = TestItem::new();
|
||||
item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
|
||||
item
|
||||
});
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.add_item(Box::new(item1.clone()), cx);
|
||||
});
|
||||
item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), None));
|
||||
|
||||
// Adding an item that creates ambiguity increases the level of detail on
|
||||
// both tabs.
|
||||
let item2 = cx.add_view(window_id, |_| {
|
||||
let mut item = TestItem::new();
|
||||
item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
|
||||
item
|
||||
});
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.add_item(Box::new(item2.clone()), cx);
|
||||
});
|
||||
item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
|
||||
item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
|
||||
|
||||
// Adding an item that creates ambiguity increases the level of detail only
|
||||
// on the ambiguous tabs. In this case, the ambiguity can't be resolved so
|
||||
// we stop at the highest detail available.
|
||||
let item3 = cx.add_view(window_id, |_| {
|
||||
let mut item = TestItem::new();
|
||||
item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
|
||||
item
|
||||
});
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.add_item(Box::new(item3.clone()), cx);
|
||||
});
|
||||
item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
|
||||
item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
|
||||
item3.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_tracking_active_path(cx: &mut TestAppContext) {
|
||||
cx.foreground().forbid_parking();
|
||||
|
@ -2880,7 +2948,7 @@ mod tests {
|
|||
|
||||
let close_items = workspace.update(cx, |workspace, cx| {
|
||||
pane.update(cx, |pane, cx| {
|
||||
pane.activate_item(1, true, true, cx);
|
||||
pane.activate_item(1, true, true, false, cx);
|
||||
assert_eq!(pane.active_item().unwrap().id(), item2.id());
|
||||
});
|
||||
|
||||
|
@ -3211,6 +3279,8 @@ mod tests {
|
|||
project_entry_ids: Vec<ProjectEntryId>,
|
||||
project_path: Option<ProjectPath>,
|
||||
nav_history: Option<ItemNavHistory>,
|
||||
tab_descriptions: Option<Vec<&'static str>>,
|
||||
tab_detail: Cell<Option<usize>>,
|
||||
}
|
||||
|
||||
enum TestItemEvent {
|
||||
|
@ -3230,6 +3300,8 @@ mod tests {
|
|||
project_entry_ids: self.project_entry_ids.clone(),
|
||||
project_path: self.project_path.clone(),
|
||||
nav_history: None,
|
||||
tab_descriptions: None,
|
||||
tab_detail: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3247,6 +3319,8 @@ mod tests {
|
|||
project_path: None,
|
||||
is_singleton: true,
|
||||
nav_history: None,
|
||||
tab_descriptions: None,
|
||||
tab_detail: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3277,7 +3351,15 @@ mod tests {
|
|||
}
|
||||
|
||||
impl Item for TestItem {
|
||||
fn tab_content(&self, _: &theme::Tab, _: &AppContext) -> ElementBox {
|
||||
fn tab_description<'a>(&'a self, detail: usize, _: &'a AppContext) -> Option<Cow<'a, str>> {
|
||||
self.tab_descriptions.as_ref().and_then(|descriptions| {
|
||||
let description = *descriptions.get(detail).or(descriptions.last())?;
|
||||
Some(description.into())
|
||||
})
|
||||
}
|
||||
|
||||
fn tab_content(&self, detail: Option<usize>, _: &theme::Tab, _: &AppContext) -> ElementBox {
|
||||
self.tab_detail.set(detail);
|
||||
Empty::new().boxed()
|
||||
}
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathansobo@gmail.com>"]
|
|||
description = "The fast, collaborative code editor."
|
||||
edition = "2021"
|
||||
name = "zed"
|
||||
version = "0.46.0"
|
||||
version = "0.48.1"
|
||||
|
||||
[lib]
|
||||
name = "zed"
|
||||
|
|
|
@ -2,7 +2,7 @@ use crate::OpenBrowser;
|
|||
use gpui::{
|
||||
elements::{MouseEventHandler, Text},
|
||||
platform::CursorStyle,
|
||||
Element, Entity, RenderContext, View,
|
||||
Element, Entity, MouseButton, RenderContext, View,
|
||||
};
|
||||
use settings::Settings;
|
||||
use workspace::StatusItemView;
|
||||
|
@ -32,7 +32,7 @@ impl View for FeedbackLink {
|
|||
.boxed()
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(|_, _, cx| {
|
||||
.on_click(MouseButton::Left, |_, cx| {
|
||||
cx.dispatch_action(OpenBrowser {
|
||||
url: NEW_ISSUE_URL.into(),
|
||||
})
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
use gpui::executor::Background;
|
||||
pub use language::*;
|
||||
use lazy_static::lazy_static;
|
||||
use rust_embed::RustEmbed;
|
||||
use std::{borrow::Cow, str, sync::Arc};
|
||||
use util::ResultExt;
|
||||
|
||||
mod c;
|
||||
mod go;
|
||||
mod installation;
|
||||
mod json;
|
||||
mod language_plugin;
|
||||
mod python;
|
||||
mod rust;
|
||||
|
@ -17,7 +18,22 @@ mod typescript;
|
|||
#[exclude = "*.rs"]
|
||||
struct LanguageDir;
|
||||
|
||||
pub async fn init(languages: Arc<LanguageRegistry>, executor: Arc<Background>) {
|
||||
// TODO - Remove this once the `init` function is synchronous again.
|
||||
lazy_static! {
|
||||
pub static ref LANGUAGE_NAMES: Vec<String> = LanguageDir::iter()
|
||||
.filter_map(|path| {
|
||||
if path.ends_with("config.toml") {
|
||||
let config = LanguageDir::get(&path)?;
|
||||
let config = toml::from_slice::<LanguageConfig>(&config.data).ok()?;
|
||||
Some(config.name.to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
|
||||
pub async fn init(languages: Arc<LanguageRegistry>, _executor: Arc<Background>) {
|
||||
for (name, grammar, lsp_adapter) in [
|
||||
(
|
||||
"c",
|
||||
|
@ -37,10 +53,7 @@ pub async fn init(languages: Arc<LanguageRegistry>, executor: Arc<Background>) {
|
|||
(
|
||||
"json",
|
||||
tree_sitter_json::language(),
|
||||
match language_plugin::new_json(executor).await.log_err() {
|
||||
Some(lang) => Some(CachedLspAdapter::new(lang).await),
|
||||
None => None,
|
||||
},
|
||||
Some(CachedLspAdapter::new(json::JsonLspAdapter).await),
|
||||
),
|
||||
(
|
||||
"markdown",
|
||||
|
|
106
crates/zed/src/languages/json.rs
Normal file
106
crates/zed/src/languages/json.rs
Normal file
|
@ -0,0 +1,106 @@
|
|||
use super::installation::{npm_install_packages, npm_package_latest_version};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use async_trait::async_trait;
|
||||
use client::http::HttpClient;
|
||||
use collections::HashMap;
|
||||
use futures::StreamExt;
|
||||
use language::{LanguageServerName, LspAdapter};
|
||||
use serde_json::json;
|
||||
use smol::fs;
|
||||
use std::{any::Any, path::PathBuf, sync::Arc};
|
||||
use util::ResultExt;
|
||||
|
||||
pub struct JsonLspAdapter;
|
||||
|
||||
impl JsonLspAdapter {
|
||||
const BIN_PATH: &'static str =
|
||||
"node_modules/vscode-json-languageserver/bin/vscode-json-languageserver";
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl LspAdapter for JsonLspAdapter {
|
||||
async fn name(&self) -> LanguageServerName {
|
||||
LanguageServerName("vscode-json-languageserver".into())
|
||||
}
|
||||
|
||||
async fn server_args(&self) -> Vec<String> {
|
||||
vec!["--stdio".into()]
|
||||
}
|
||||
|
||||
async fn fetch_latest_server_version(
|
||||
&self,
|
||||
_: Arc<dyn HttpClient>,
|
||||
) -> Result<Box<dyn 'static + Any + Send>> {
|
||||
Ok(Box::new(npm_package_latest_version("vscode-json-languageserver").await?) as Box<_>)
|
||||
}
|
||||
|
||||
async fn fetch_server_binary(
|
||||
&self,
|
||||
version: Box<dyn 'static + Send + Any>,
|
||||
_: Arc<dyn HttpClient>,
|
||||
container_dir: PathBuf,
|
||||
) -> Result<PathBuf> {
|
||||
let version = version.downcast::<String>().unwrap();
|
||||
let version_dir = container_dir.join(version.as_str());
|
||||
fs::create_dir_all(&version_dir)
|
||||
.await
|
||||
.context("failed to create version directory")?;
|
||||
let binary_path = version_dir.join(Self::BIN_PATH);
|
||||
|
||||
if fs::metadata(&binary_path).await.is_err() {
|
||||
npm_install_packages(
|
||||
[("vscode-json-languageserver", version.as_str())],
|
||||
&version_dir,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(mut entries) = fs::read_dir(&container_dir).await.log_err() {
|
||||
while let Some(entry) = entries.next().await {
|
||||
if let Some(entry) = entry.log_err() {
|
||||
let entry_path = entry.path();
|
||||
if entry_path.as_path() != version_dir {
|
||||
fs::remove_dir_all(&entry_path).await.log_err();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(binary_path)
|
||||
}
|
||||
|
||||
async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<PathBuf> {
|
||||
(|| async move {
|
||||
let mut last_version_dir = None;
|
||||
let mut entries = fs::read_dir(&container_dir).await?;
|
||||
while let Some(entry) = entries.next().await {
|
||||
let entry = entry?;
|
||||
if entry.file_type().await?.is_dir() {
|
||||
last_version_dir = Some(entry.path());
|
||||
}
|
||||
}
|
||||
let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
|
||||
let bin_path = last_version_dir.join(Self::BIN_PATH);
|
||||
if bin_path.exists() {
|
||||
Ok(bin_path)
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"missing executable in directory {:?}",
|
||||
last_version_dir
|
||||
))
|
||||
}
|
||||
})()
|
||||
.await
|
||||
.log_err()
|
||||
}
|
||||
|
||||
async fn initialization_options(&self) -> Option<serde_json::Value> {
|
||||
Some(json!({
|
||||
"provideFormatter": true
|
||||
}))
|
||||
}
|
||||
|
||||
async fn language_ids(&self) -> HashMap<String, String> {
|
||||
[("JSON".into(), "jsonc".into())].into_iter().collect()
|
||||
}
|
||||
}
|
|
@ -5,16 +5,13 @@ use collections::HashMap;
|
|||
use futures::lock::Mutex;
|
||||
use gpui::executor::Background;
|
||||
use language::{LanguageServerName, LspAdapter};
|
||||
use plugin_runtime::{Plugin, PluginBinary, PluginBuilder, PluginYield, WasiFn};
|
||||
use plugin_runtime::{Plugin, PluginBinary, PluginBuilder, WasiFn};
|
||||
use std::{any::Any, path::PathBuf, sync::Arc};
|
||||
use util::ResultExt;
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn new_json(executor: Arc<Background>) -> Result<PluginLspAdapter> {
|
||||
let executor_ref = executor.clone();
|
||||
let plugin =
|
||||
PluginBuilder::new_epoch_with_default_ctx(PluginYield::default_epoch(), move |future| {
|
||||
executor_ref.spawn(future).detach()
|
||||
})?
|
||||
let plugin = PluginBuilder::new_default()?
|
||||
.host_function_async("command", |command: String| async move {
|
||||
let mut args = command.split(' ');
|
||||
let command = args.next().unwrap();
|
||||
|
@ -26,7 +23,7 @@ pub async fn new_json(executor: Arc<Background>) -> Result<PluginLspAdapter> {
|
|||
.map(|output| output.stdout)
|
||||
})?
|
||||
.init(PluginBinary::Precompiled(include_bytes!(
|
||||
"../../../../plugins/bin/json_language.wasm.pre"
|
||||
"../../../../plugins/bin/json_language.wasm.pre",
|
||||
)))
|
||||
.await?;
|
||||
|
||||
|
@ -46,6 +43,7 @@ pub struct PluginLspAdapter {
|
|||
}
|
||||
|
||||
impl PluginLspAdapter {
|
||||
#[allow(unused)]
|
||||
pub async fn new(mut plugin: Plugin, executor: Arc<Background>) -> Result<Self> {
|
||||
Ok(Self {
|
||||
name: plugin.function("name")?,
|
||||
|
|
|
@ -39,15 +39,20 @@ where
|
|||
Self(rx)
|
||||
}
|
||||
|
||||
///Loads the given watched JSON file. In the special case that the file is
|
||||
///empty (ignoring whitespace) or is not a file, this will return T::default()
|
||||
async fn load(fs: Arc<dyn Fs>, path: &Path) -> Option<T> {
|
||||
if fs.is_file(&path).await {
|
||||
fs.load(&path)
|
||||
.await
|
||||
.log_err()
|
||||
.and_then(|data| parse_json_with_comments(&data).log_err())
|
||||
} else {
|
||||
Some(T::default())
|
||||
if !fs.is_file(&path).await {
|
||||
return Some(T::default());
|
||||
}
|
||||
|
||||
fs.load(&path).await.log_err().and_then(|data| {
|
||||
if data.trim().is_empty() {
|
||||
Some(T::default())
|
||||
} else {
|
||||
parse_json_with_comments(&data).log_err()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -79,18 +79,25 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
|
|||
cx.add_global_action(move |_: &IncreaseBufferFontSize, cx| {
|
||||
cx.update_global::<Settings, _, _>(|settings, cx| {
|
||||
settings.buffer_font_size = (settings.buffer_font_size + 1.0).max(MIN_FONT_SIZE);
|
||||
if let Some(terminal_font_size) = settings.terminal_overrides.font_size.as_mut() {
|
||||
*terminal_font_size = (*terminal_font_size + 1.0).max(MIN_FONT_SIZE);
|
||||
}
|
||||
cx.refresh_windows();
|
||||
});
|
||||
});
|
||||
cx.add_global_action(move |_: &DecreaseBufferFontSize, cx| {
|
||||
cx.update_global::<Settings, _, _>(|settings, cx| {
|
||||
settings.buffer_font_size = (settings.buffer_font_size - 1.0).max(MIN_FONT_SIZE);
|
||||
if let Some(terminal_font_size) = settings.terminal_overrides.font_size.as_mut() {
|
||||
*terminal_font_size = (*terminal_font_size - 1.0).max(MIN_FONT_SIZE);
|
||||
}
|
||||
cx.refresh_windows();
|
||||
});
|
||||
});
|
||||
cx.add_global_action(move |_: &ResetBufferFontSize, cx| {
|
||||
cx.update_global::<Settings, _, _>(|settings, cx| {
|
||||
settings.buffer_font_size = settings.default_buffer_font_size;
|
||||
settings.terminal_overrides.font_size = settings.terminal_defaults.font_size;
|
||||
cx.refresh_windows();
|
||||
});
|
||||
});
|
||||
|
@ -102,14 +109,14 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
|
|||
let app_state = app_state.clone();
|
||||
move |_: &mut Workspace, _: &OpenSettings, cx: &mut ViewContext<Workspace>| {
|
||||
open_config_file(&SETTINGS_PATH, app_state.clone(), cx, || {
|
||||
let header = Assets.load("settings/header-comments.json").unwrap();
|
||||
let json = Assets.load("settings/default.json").unwrap();
|
||||
let header = str::from_utf8(header.as_ref()).unwrap();
|
||||
let json = str::from_utf8(json.as_ref()).unwrap();
|
||||
let mut content = Rope::new();
|
||||
content.push(header);
|
||||
content.push(json);
|
||||
content
|
||||
str::from_utf8(
|
||||
Assets
|
||||
.load("settings/initial_user_settings.json")
|
||||
.unwrap()
|
||||
.as_ref(),
|
||||
)
|
||||
.unwrap()
|
||||
.into()
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@ -209,7 +216,7 @@ pub fn initialize_workspace(
|
|||
cx.emit(workspace::Event::PaneAdded(workspace.active_pane().clone()));
|
||||
|
||||
let theme_names = app_state.themes.list().collect();
|
||||
let language_names = app_state.languages.language_names();
|
||||
let language_names = &languages::LANGUAGE_NAMES;
|
||||
|
||||
workspace.project().update(cx, |project, cx| {
|
||||
let action_names = cx.all_action_names().collect::<Vec<_>>();
|
||||
|
|
208
styles/package-lock.json
generated
208
styles/package-lock.json
generated
|
@ -1,20 +1,196 @@
|
|||
{
|
||||
"name": "styles",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "styles",
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@types/chroma-js": "^2.1.3",
|
||||
"@types/node": "^17.0.23",
|
||||
"case-anything": "^2.1.10",
|
||||
"chroma-js": "^2.4.2",
|
||||
"ts-node": "^10.7.0"
|
||||
}
|
||||
"name": "styles",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "styles",
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@types/chroma-js": "^2.1.3",
|
||||
"@types/node": "^17.0.23",
|
||||
"case-anything": "^2.1.10",
|
||||
"chroma-js": "^2.4.2",
|
||||
"ts-node": "^10.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@cspotcode/source-map-consumer": {
|
||||
"version": "0.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz",
|
||||
"integrity": "sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg==",
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/@cspotcode/source-map-support": {
|
||||
"version": "0.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz",
|
||||
"integrity": "sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA==",
|
||||
"dependencies": {
|
||||
"@cspotcode/source-map-consumer": "0.8.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@tsconfig/node10": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.8.tgz",
|
||||
"integrity": "sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg=="
|
||||
},
|
||||
"node_modules/@tsconfig/node12": {
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.9.tgz",
|
||||
"integrity": "sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw=="
|
||||
},
|
||||
"node_modules/@tsconfig/node14": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.1.tgz",
|
||||
"integrity": "sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg=="
|
||||
},
|
||||
"node_modules/@tsconfig/node16": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.2.tgz",
|
||||
"integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA=="
|
||||
},
|
||||
"node_modules/@types/chroma-js": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/chroma-js/-/chroma-js-2.1.3.tgz",
|
||||
"integrity": "sha512-1xGPhoSGY1CPmXLCBcjVZSQinFjL26vlR8ZqprsBWiFyED4JacJJ9zHhh5aaUXqbY9B37mKQ73nlydVAXmr1+g=="
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "17.0.23",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.23.tgz",
|
||||
"integrity": "sha512-UxDxWn7dl97rKVeVS61vErvw086aCYhDLyvRQZ5Rk65rZKepaFdm53GeqXaKBuOhED4e9uWq34IC3TdSdJJ2Gw=="
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.7.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz",
|
||||
"integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==",
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/acorn-walk": {
|
||||
"version": "8.2.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz",
|
||||
"integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/arg": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
|
||||
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA=="
|
||||
},
|
||||
"node_modules/case-anything": {
|
||||
"version": "2.1.10",
|
||||
"resolved": "https://registry.npmjs.org/case-anything/-/case-anything-2.1.10.tgz",
|
||||
"integrity": "sha512-JczJwVrCP0jPKh05McyVsuOg6AYosrB9XWZKbQzXeDAm2ClE/PJE/BcrrQrVyGYH7Jg8V/LDupmyL4kFlVsVFQ==",
|
||||
"engines": {
|
||||
"node": ">=12.13"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/mesqueeb"
|
||||
}
|
||||
},
|
||||
"node_modules/chroma-js": {
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/chroma-js/-/chroma-js-2.4.2.tgz",
|
||||
"integrity": "sha512-U9eDw6+wt7V8z5NncY2jJfZa+hUH8XEj8FQHgFJTrUFnJfXYf4Ml4adI2vXZOjqRDpFWtYVWypDfZwnJ+HIR4A=="
|
||||
},
|
||||
"node_modules/create-require": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
|
||||
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ=="
|
||||
},
|
||||
"node_modules/diff": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
|
||||
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
|
||||
"engines": {
|
||||
"node": ">=0.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/make-error": {
|
||||
"version": "1.3.6",
|
||||
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
|
||||
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw=="
|
||||
},
|
||||
"node_modules/ts-node": {
|
||||
"version": "10.7.0",
|
||||
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.7.0.tgz",
|
||||
"integrity": "sha512-TbIGS4xgJoX2i3do417KSaep1uRAW/Lu+WAL2doDHC0D6ummjirVOXU5/7aiZotbQ5p1Zp9tP7U6cYhA0O7M8A==",
|
||||
"dependencies": {
|
||||
"@cspotcode/source-map-support": "0.7.0",
|
||||
"@tsconfig/node10": "^1.0.7",
|
||||
"@tsconfig/node12": "^1.0.7",
|
||||
"@tsconfig/node14": "^1.0.0",
|
||||
"@tsconfig/node16": "^1.0.2",
|
||||
"acorn": "^8.4.1",
|
||||
"acorn-walk": "^8.1.1",
|
||||
"arg": "^4.1.0",
|
||||
"create-require": "^1.1.0",
|
||||
"diff": "^4.0.1",
|
||||
"make-error": "^1.1.1",
|
||||
"v8-compile-cache-lib": "^3.0.0",
|
||||
"yn": "3.1.1"
|
||||
},
|
||||
"bin": {
|
||||
"ts-node": "dist/bin.js",
|
||||
"ts-node-cwd": "dist/bin-cwd.js",
|
||||
"ts-node-esm": "dist/bin-esm.js",
|
||||
"ts-node-script": "dist/bin-script.js",
|
||||
"ts-node-transpile-only": "dist/bin-transpile.js",
|
||||
"ts-script": "dist/bin-script-deprecated.js"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@swc/core": ">=1.2.50",
|
||||
"@swc/wasm": ">=1.2.50",
|
||||
"@types/node": "*",
|
||||
"typescript": ">=2.7"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@swc/core": {
|
||||
"optional": true
|
||||
},
|
||||
"@swc/wasm": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "4.6.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.3.tgz",
|
||||
"integrity": "sha512-yNIatDa5iaofVozS/uQJEl3JRWLKKGJKh6Yaiv0GLGSuhpFJe7P3SbHZ8/yjAHRQwKRoA6YZqlfjXWmVzoVSMw==",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/v8-compile-cache-lib": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.0.tgz",
|
||||
"integrity": "sha512-mpSYqfsFvASnSn5qMiwrr4VKfumbPyONLCOPmsR3A6pTY/r0+tSaVbgPWSAIuzbk3lCTa+FForeTiO+wBQGkjA=="
|
||||
},
|
||||
"node_modules/yn": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
|
||||
"integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@cspotcode/source-map-consumer": {
|
||||
"version": "0.8.0",
|
||||
|
|
|
@ -1,7 +1,14 @@
|
|||
import Theme from "../themes/common/theme";
|
||||
import { border, modalShadow } from "./components";
|
||||
import { border, modalShadow, player } from "./components";
|
||||
|
||||
export default function terminal(theme: Theme) {
|
||||
/**
|
||||
* Colors are controlled per-cell in the terminal grid.
|
||||
* Cells can be set to any of these more 'theme-capable' colors
|
||||
* or can be set directly with RGB values.
|
||||
* Here are the common interpretations of these names:
|
||||
* https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
|
||||
*/
|
||||
let colors = {
|
||||
black: theme.ramps.neutral(0).hex(),
|
||||
red: theme.ramps.red(0.5).hex(),
|
||||
|
@ -11,7 +18,7 @@ export default function terminal(theme: Theme) {
|
|||
magenta: theme.ramps.magenta(0.5).hex(),
|
||||
cyan: theme.ramps.cyan(0.5).hex(),
|
||||
white: theme.ramps.neutral(7).hex(),
|
||||
brightBlack: theme.ramps.neutral(2).hex(),
|
||||
brightBlack: theme.ramps.neutral(4).hex(),
|
||||
brightRed: theme.ramps.red(0.25).hex(),
|
||||
brightGreen: theme.ramps.green(0.25).hex(),
|
||||
brightYellow: theme.ramps.yellow(0.25).hex(),
|
||||
|
@ -19,10 +26,19 @@ export default function terminal(theme: Theme) {
|
|||
brightMagenta: theme.ramps.magenta(0.25).hex(),
|
||||
brightCyan: theme.ramps.cyan(0.25).hex(),
|
||||
brightWhite: theme.ramps.neutral(7).hex(),
|
||||
/**
|
||||
* Default color for characters
|
||||
*/
|
||||
foreground: theme.ramps.neutral(7).hex(),
|
||||
/**
|
||||
* Default color for the rectangle background of a cell
|
||||
*/
|
||||
background: theme.ramps.neutral(0).hex(),
|
||||
modalBackground: theme.ramps.neutral(1).hex(),
|
||||
cursor: theme.ramps.neutral(7).hex(),
|
||||
/**
|
||||
* Default color for the cursor
|
||||
*/
|
||||
cursor: player(theme, 1).selection.cursor,
|
||||
dimBlack: theme.ramps.neutral(7).hex(),
|
||||
dimRed: theme.ramps.red(0.75).hex(),
|
||||
dimGreen: theme.ramps.green(0.75).hex(),
|
||||
|
|
|
@ -33,6 +33,10 @@ export default function workspace(theme: Theme) {
|
|||
left: 8,
|
||||
right: 8,
|
||||
},
|
||||
description: {
|
||||
margin: { left: 6, top: 1 },
|
||||
...text(theme, "sans", "muted", { size: "2xs" })
|
||||
}
|
||||
};
|
||||
|
||||
const activeTab = {
|
||||
|
|
Loading…
Reference in a new issue