mirror of
https://github.com/zed-industries/zed.git
synced 2024-09-19 18:41:56 +03:00
Merge branch 'main' into project-panel-with-new-mouse-events
This commit is contained in:
commit
20e1044d49
3
.github/workflows/ci.yml
vendored
3
.github/workflows/ci.yml
vendored
@ -44,6 +44,9 @@ jobs:
|
|||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: cargo test --workspace --no-fail-fast
|
run: cargo test --workspace --no-fail-fast
|
||||||
|
|
||||||
|
- name: Build collab binaries
|
||||||
|
run: cargo build --bins --all-features
|
||||||
|
|
||||||
bundle:
|
bundle:
|
||||||
name: Bundle app
|
name: Bundle app
|
||||||
runs-on:
|
runs-on:
|
||||||
|
@ -20,6 +20,7 @@
|
|||||||
"cmd-shift-S": "workspace::SaveAs",
|
"cmd-shift-S": "workspace::SaveAs",
|
||||||
"cmd-=": "zed::IncreaseBufferFontSize",
|
"cmd-=": "zed::IncreaseBufferFontSize",
|
||||||
"cmd--": "zed::DecreaseBufferFontSize",
|
"cmd--": "zed::DecreaseBufferFontSize",
|
||||||
|
"cmd-0": "zed::ResetBufferFontSize",
|
||||||
"cmd-,": "zed::OpenSettings",
|
"cmd-,": "zed::OpenSettings",
|
||||||
"cmd-q": "zed::Quit",
|
"cmd-q": "zed::Quit",
|
||||||
"cmd-n": "workspace::NewFile",
|
"cmd-n": "workspace::NewFile",
|
||||||
|
@ -57,6 +57,10 @@
|
|||||||
"Delete"
|
"Delete"
|
||||||
],
|
],
|
||||||
"shift-D": "vim::DeleteToEndOfLine",
|
"shift-D": "vim::DeleteToEndOfLine",
|
||||||
|
"y": [
|
||||||
|
"vim::PushOperator",
|
||||||
|
"Yank"
|
||||||
|
],
|
||||||
"i": [
|
"i": [
|
||||||
"vim::SwitchMode",
|
"vim::SwitchMode",
|
||||||
"Insert"
|
"Insert"
|
||||||
@ -71,8 +75,24 @@
|
|||||||
"shift-O": "vim::InsertLineAbove",
|
"shift-O": "vim::InsertLineAbove",
|
||||||
"v": [
|
"v": [
|
||||||
"vim::SwitchMode",
|
"vim::SwitchMode",
|
||||||
"Visual"
|
{
|
||||||
]
|
"Visual": {
|
||||||
|
"line": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"shift-V": [
|
||||||
|
"vim::SwitchMode",
|
||||||
|
{
|
||||||
|
"Visual": {
|
||||||
|
"line": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"p": "vim::Paste",
|
||||||
|
"u": "editor::Undo",
|
||||||
|
"ctrl-r": "editor::Redo",
|
||||||
|
"ctrl-o": "pane::GoBack"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -104,12 +124,19 @@
|
|||||||
"d": "vim::CurrentLine"
|
"d": "vim::CurrentLine"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"context": "Editor && vim_operator == y",
|
||||||
|
"bindings": {
|
||||||
|
"y": "vim::CurrentLine"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"context": "Editor && vim_mode == visual",
|
"context": "Editor && vim_mode == visual",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"c": "vim::VisualChange",
|
"c": "vim::VisualChange",
|
||||||
"d": "vim::VisualDelete",
|
"d": "vim::VisualDelete",
|
||||||
"x": "vim::VisualDelete"
|
"x": "vim::VisualDelete",
|
||||||
|
"y": "vim::VisualYank"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
|
use collab::{Error, Result};
|
||||||
use db::{Db, PostgresDb, UserId};
|
use db::{Db, PostgresDb, UserId};
|
||||||
use rand::prelude::*;
|
use rand::prelude::*;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
@ -32,12 +33,12 @@ async fn main() {
|
|||||||
.expect("failed to connect to postgres database");
|
.expect("failed to connect to postgres database");
|
||||||
|
|
||||||
let mut zed_users = vec![
|
let mut zed_users = vec![
|
||||||
"nathansobo".to_string(),
|
("nathansobo".to_string(), Some("nathan@zed.dev")),
|
||||||
"maxbrunsfeld".to_string(),
|
("maxbrunsfeld".to_string(), Some("max@zed.dev")),
|
||||||
"as-cii".to_string(),
|
("as-cii".to_string(), Some("antonio@zed.dev")),
|
||||||
"iamnbutler".to_string(),
|
("iamnbutler".to_string(), Some("nate@zed.dev")),
|
||||||
"gibusu".to_string(),
|
("gibusu".to_string(), Some("greg@zed.dev")),
|
||||||
"Kethku".to_string(),
|
("Kethku".to_string(), Some("keith@zed.dev")),
|
||||||
];
|
];
|
||||||
|
|
||||||
if args.github_users {
|
if args.github_users {
|
||||||
@ -61,7 +62,7 @@ async fn main() {
|
|||||||
.json::<Vec<GitHubUser>>()
|
.json::<Vec<GitHubUser>>()
|
||||||
.await
|
.await
|
||||||
.expect("failed to deserialize github user");
|
.expect("failed to deserialize github user");
|
||||||
zed_users.extend(users.iter().map(|user| user.login.clone()));
|
zed_users.extend(users.iter().map(|user| (user.login.clone(), None)));
|
||||||
|
|
||||||
if let Some(last_user) = users.last() {
|
if let Some(last_user) = users.last() {
|
||||||
last_user_id = Some(last_user.id);
|
last_user_id = Some(last_user.id);
|
||||||
@ -72,7 +73,7 @@ async fn main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut zed_user_ids = Vec::<UserId>::new();
|
let mut zed_user_ids = Vec::<UserId>::new();
|
||||||
for zed_user in zed_users {
|
for (zed_user, email) in zed_users {
|
||||||
if let Some(user) = db
|
if let Some(user) = db
|
||||||
.get_user_by_github_login(&zed_user)
|
.get_user_by_github_login(&zed_user)
|
||||||
.await
|
.await
|
||||||
@ -81,7 +82,7 @@ async fn main() {
|
|||||||
zed_user_ids.push(user.id);
|
zed_user_ids.push(user.id);
|
||||||
} else {
|
} else {
|
||||||
zed_user_ids.push(
|
zed_user_ids.push(
|
||||||
db.create_user(&zed_user, true)
|
db.create_user(&zed_user, email, true)
|
||||||
.await
|
.await
|
||||||
.expect("failed to insert user"),
|
.expect("failed to insert user"),
|
||||||
);
|
);
|
||||||
|
5359
crates/collab/src/integration_tests.rs
Normal file
5359
crates/collab/src/integration_tests.rs
Normal file
File diff suppressed because it is too large
Load Diff
69
crates/collab/src/lib.rs
Normal file
69
crates/collab/src/lib.rs
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
use axum::{http::StatusCode, response::IntoResponse};
|
||||||
|
|
||||||
|
pub type Result<T, E = Error> = std::result::Result<T, E>;
|
||||||
|
|
||||||
|
pub enum Error {
|
||||||
|
Http(StatusCode, String),
|
||||||
|
Internal(anyhow::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<anyhow::Error> for Error {
|
||||||
|
fn from(error: anyhow::Error) -> Self {
|
||||||
|
Self::Internal(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<sqlx::Error> for Error {
|
||||||
|
fn from(error: sqlx::Error) -> Self {
|
||||||
|
Self::Internal(error.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<axum::Error> for Error {
|
||||||
|
fn from(error: axum::Error) -> Self {
|
||||||
|
Self::Internal(error.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<hyper::Error> for Error {
|
||||||
|
fn from(error: hyper::Error) -> Self {
|
||||||
|
Self::Internal(error.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<serde_json::Error> for Error {
|
||||||
|
fn from(error: serde_json::Error) -> Self {
|
||||||
|
Self::Internal(error.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoResponse for Error {
|
||||||
|
fn into_response(self) -> axum::response::Response {
|
||||||
|
match self {
|
||||||
|
Error::Http(code, message) => (code, message).into_response(),
|
||||||
|
Error::Internal(error) => {
|
||||||
|
(StatusCode::INTERNAL_SERVER_ERROR, format!("{}", &error)).into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for Error {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Error::Http(code, message) => (code, message).fmt(f),
|
||||||
|
Error::Internal(error) => error.fmt(f),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for Error {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Error::Http(code, message) => write!(f, "{code}: {message}"),
|
||||||
|
Error::Internal(error) => error.fmt(f),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for Error {}
|
@ -4,7 +4,11 @@ mod db;
|
|||||||
mod env;
|
mod env;
|
||||||
mod rpc;
|
mod rpc;
|
||||||
|
|
||||||
use axum::{body::Body, http::StatusCode, response::IntoResponse, Router};
|
#[cfg(test)]
|
||||||
|
mod integration_tests;
|
||||||
|
|
||||||
|
use axum::{body::Body, Router};
|
||||||
|
use collab::{Error, Result};
|
||||||
use db::{Db, PostgresDb};
|
use db::{Db, PostgresDb};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::{
|
use std::{
|
||||||
@ -73,74 +77,6 @@ async fn main() -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type Result<T, E = Error> = std::result::Result<T, E>;
|
|
||||||
|
|
||||||
pub enum Error {
|
|
||||||
Http(StatusCode, String),
|
|
||||||
Internal(anyhow::Error),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<anyhow::Error> for Error {
|
|
||||||
fn from(error: anyhow::Error) -> Self {
|
|
||||||
Self::Internal(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<sqlx::Error> for Error {
|
|
||||||
fn from(error: sqlx::Error) -> Self {
|
|
||||||
Self::Internal(error.into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<axum::Error> for Error {
|
|
||||||
fn from(error: axum::Error) -> Self {
|
|
||||||
Self::Internal(error.into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<hyper::Error> for Error {
|
|
||||||
fn from(error: hyper::Error) -> Self {
|
|
||||||
Self::Internal(error.into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<serde_json::Error> for Error {
|
|
||||||
fn from(error: serde_json::Error) -> Self {
|
|
||||||
Self::Internal(error.into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IntoResponse for Error {
|
|
||||||
fn into_response(self) -> axum::response::Response {
|
|
||||||
match self {
|
|
||||||
Error::Http(code, message) => (code, message).into_response(),
|
|
||||||
Error::Internal(error) => {
|
|
||||||
(StatusCode::INTERNAL_SERVER_ERROR, format!("{}", &error)).into_response()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Debug for Error {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
match self {
|
|
||||||
Error::Http(code, message) => (code, message).fmt(f),
|
|
||||||
Error::Internal(error) => error.fmt(f),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for Error {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
match self {
|
|
||||||
Error::Http(code, message) => write!(f, "{code}: {message}"),
|
|
||||||
Error::Internal(error) => error.fmt(f),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::error::Error for Error {}
|
|
||||||
|
|
||||||
pub fn init_tracing(config: &Config) -> Option<()> {
|
pub fn init_tracing(config: &Config) -> Option<()> {
|
||||||
use opentelemetry::KeyValue;
|
use opentelemetry::KeyValue;
|
||||||
use opentelemetry_otlp::WithExportConfig;
|
use opentelemetry_otlp::WithExportConfig;
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -1,272 +0,0 @@
|
|||||||
pub enum ContextMenu {
|
|
||||||
Completions(CompletionsMenu),
|
|
||||||
CodeActions(CodeActionsMenu),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ContextMenu {
|
|
||||||
pub fn select_prev(&mut self, cx: &mut ViewContext<Editor>) -> bool {
|
|
||||||
if self.visible() {
|
|
||||||
match self {
|
|
||||||
ContextMenu::Completions(menu) => menu.select_prev(cx),
|
|
||||||
ContextMenu::CodeActions(menu) => menu.select_prev(cx),
|
|
||||||
}
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn select_next(&mut self, cx: &mut ViewContext<Editor>) -> bool {
|
|
||||||
if self.visible() {
|
|
||||||
match self {
|
|
||||||
ContextMenu::Completions(menu) => menu.select_next(cx),
|
|
||||||
ContextMenu::CodeActions(menu) => menu.select_next(cx),
|
|
||||||
}
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn visible(&self) -> bool {
|
|
||||||
match self {
|
|
||||||
ContextMenu::Completions(menu) => menu.visible(),
|
|
||||||
ContextMenu::CodeActions(menu) => menu.visible(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn render(
|
|
||||||
&self,
|
|
||||||
cursor_position: DisplayPoint,
|
|
||||||
style: EditorStyle,
|
|
||||||
cx: &AppContext,
|
|
||||||
) -> (DisplayPoint, ElementBox) {
|
|
||||||
match self {
|
|
||||||
ContextMenu::Completions(menu) => (cursor_position, menu.render(style, cx)),
|
|
||||||
ContextMenu::CodeActions(menu) => menu.render(cursor_position, style),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct CompletionsMenu {
|
|
||||||
id: CompletionId,
|
|
||||||
initial_position: Anchor,
|
|
||||||
buffer: ModelHandle<Buffer>,
|
|
||||||
completions: Arc<[Completion]>,
|
|
||||||
match_candidates: Vec<StringMatchCandidate>,
|
|
||||||
matches: Arc<[StringMatch]>,
|
|
||||||
selected_item: usize,
|
|
||||||
list: UniformListState,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CompletionsMenu {
|
|
||||||
fn select_prev(&mut self, cx: &mut ViewContext<Editor>) {
|
|
||||||
if self.selected_item > 0 {
|
|
||||||
self.selected_item -= 1;
|
|
||||||
self.list.scroll_to(ScrollTarget::Show(self.selected_item));
|
|
||||||
}
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn select_next(&mut self, cx: &mut ViewContext<Editor>) {
|
|
||||||
if self.selected_item + 1 < self.matches.len() {
|
|
||||||
self.selected_item += 1;
|
|
||||||
self.list.scroll_to(ScrollTarget::Show(self.selected_item));
|
|
||||||
}
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visible(&self) -> bool {
|
|
||||||
!self.matches.is_empty()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render(&self, style: EditorStyle, _: &AppContext) -> ElementBox {
|
|
||||||
enum CompletionTag {}
|
|
||||||
|
|
||||||
let completions = self.completions.clone();
|
|
||||||
let matches = self.matches.clone();
|
|
||||||
let selected_item = self.selected_item;
|
|
||||||
let container_style = style.autocomplete.container;
|
|
||||||
UniformList::new(self.list.clone(), matches.len(), move |range, items, cx| {
|
|
||||||
let start_ix = range.start;
|
|
||||||
for (ix, mat) in matches[range].iter().enumerate() {
|
|
||||||
let completion = &completions[mat.candidate_id];
|
|
||||||
let item_ix = start_ix + ix;
|
|
||||||
items.push(
|
|
||||||
MouseEventHandler::new::<CompletionTag, _, _>(
|
|
||||||
mat.candidate_id,
|
|
||||||
cx,
|
|
||||||
|state, _| {
|
|
||||||
let item_style = if item_ix == selected_item {
|
|
||||||
style.autocomplete.selected_item
|
|
||||||
} else if state.hovered {
|
|
||||||
style.autocomplete.hovered_item
|
|
||||||
} else {
|
|
||||||
style.autocomplete.item
|
|
||||||
};
|
|
||||||
|
|
||||||
Text::new(completion.label.text.clone(), style.text.clone())
|
|
||||||
.with_soft_wrap(false)
|
|
||||||
.with_highlights(combine_syntax_and_fuzzy_match_highlights(
|
|
||||||
&completion.label.text,
|
|
||||||
style.text.color.into(),
|
|
||||||
styled_runs_for_code_label(&completion.label, &style.syntax),
|
|
||||||
&mat.positions,
|
|
||||||
))
|
|
||||||
.contained()
|
|
||||||
.with_style(item_style)
|
|
||||||
.boxed()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.with_cursor_style(CursorStyle::PointingHand)
|
|
||||||
.on_mouse_down(move |cx| {
|
|
||||||
cx.dispatch_action(ConfirmCompletion(Some(item_ix)));
|
|
||||||
})
|
|
||||||
.boxed(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.with_width_from_item(
|
|
||||||
self.matches
|
|
||||||
.iter()
|
|
||||||
.enumerate()
|
|
||||||
.max_by_key(|(_, mat)| {
|
|
||||||
self.completions[mat.candidate_id]
|
|
||||||
.label
|
|
||||||
.text
|
|
||||||
.chars()
|
|
||||||
.count()
|
|
||||||
})
|
|
||||||
.map(|(ix, _)| ix),
|
|
||||||
)
|
|
||||||
.contained()
|
|
||||||
.with_style(container_style)
|
|
||||||
.boxed()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn filter(&mut self, query: Option<&str>, executor: Arc<executor::Background>) {
|
|
||||||
let mut matches = if let Some(query) = query {
|
|
||||||
fuzzy::match_strings(
|
|
||||||
&self.match_candidates,
|
|
||||||
query,
|
|
||||||
false,
|
|
||||||
100,
|
|
||||||
&Default::default(),
|
|
||||||
executor,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
} else {
|
|
||||||
self.match_candidates
|
|
||||||
.iter()
|
|
||||||
.enumerate()
|
|
||||||
.map(|(candidate_id, candidate)| StringMatch {
|
|
||||||
candidate_id,
|
|
||||||
score: Default::default(),
|
|
||||||
positions: Default::default(),
|
|
||||||
string: candidate.string.clone(),
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
};
|
|
||||||
matches.sort_unstable_by_key(|mat| {
|
|
||||||
(
|
|
||||||
Reverse(OrderedFloat(mat.score)),
|
|
||||||
self.completions[mat.candidate_id].sort_key(),
|
|
||||||
)
|
|
||||||
});
|
|
||||||
|
|
||||||
for mat in &mut matches {
|
|
||||||
let filter_start = self.completions[mat.candidate_id].label.filter_range.start;
|
|
||||||
for position in &mut mat.positions {
|
|
||||||
*position += filter_start;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.matches = matches.into();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
struct CodeActionsMenu {
|
|
||||||
actions: Arc<[CodeAction]>,
|
|
||||||
buffer: ModelHandle<Buffer>,
|
|
||||||
selected_item: usize,
|
|
||||||
list: UniformListState,
|
|
||||||
deployed_from_indicator: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CodeActionsMenu {
|
|
||||||
fn select_prev(&mut self, cx: &mut ViewContext<Editor>) {
|
|
||||||
if self.selected_item > 0 {
|
|
||||||
self.selected_item -= 1;
|
|
||||||
cx.notify()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn select_next(&mut self, cx: &mut ViewContext<Editor>) {
|
|
||||||
if self.selected_item + 1 < self.actions.len() {
|
|
||||||
self.selected_item += 1;
|
|
||||||
cx.notify()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visible(&self) -> bool {
|
|
||||||
!self.actions.is_empty()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render(
|
|
||||||
&self,
|
|
||||||
mut cursor_position: DisplayPoint,
|
|
||||||
style: EditorStyle,
|
|
||||||
) -> (DisplayPoint, ElementBox) {
|
|
||||||
enum ActionTag {}
|
|
||||||
|
|
||||||
let container_style = style.autocomplete.container;
|
|
||||||
let actions = self.actions.clone();
|
|
||||||
let selected_item = self.selected_item;
|
|
||||||
let element =
|
|
||||||
UniformList::new(self.list.clone(), actions.len(), move |range, items, cx| {
|
|
||||||
let start_ix = range.start;
|
|
||||||
for (ix, action) in actions[range].iter().enumerate() {
|
|
||||||
let item_ix = start_ix + ix;
|
|
||||||
items.push(
|
|
||||||
MouseEventHandler::new::<ActionTag, _, _>(item_ix, cx, |state, _| {
|
|
||||||
let item_style = if item_ix == selected_item {
|
|
||||||
style.autocomplete.selected_item
|
|
||||||
} else if state.hovered {
|
|
||||||
style.autocomplete.hovered_item
|
|
||||||
} else {
|
|
||||||
style.autocomplete.item
|
|
||||||
};
|
|
||||||
|
|
||||||
Text::new(action.lsp_action.title.clone(), style.text.clone())
|
|
||||||
.with_soft_wrap(false)
|
|
||||||
.contained()
|
|
||||||
.with_style(item_style)
|
|
||||||
.boxed()
|
|
||||||
})
|
|
||||||
.with_cursor_style(CursorStyle::PointingHand)
|
|
||||||
.on_mouse_down(move |cx| {
|
|
||||||
cx.dispatch_action(ConfirmCodeAction(Some(item_ix)));
|
|
||||||
})
|
|
||||||
.boxed(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.with_width_from_item(
|
|
||||||
self.actions
|
|
||||||
.iter()
|
|
||||||
.enumerate()
|
|
||||||
.max_by_key(|(_, action)| action.lsp_action.title.chars().count())
|
|
||||||
.map(|(ix, _)| ix),
|
|
||||||
)
|
|
||||||
.contained()
|
|
||||||
.with_style(container_style)
|
|
||||||
.boxed();
|
|
||||||
|
|
||||||
if self.deployed_from_indicator {
|
|
||||||
*cursor_position.column_mut() = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
(cursor_position, element)
|
|
||||||
}
|
|
||||||
}
|
|
@ -279,6 +279,23 @@ impl DisplaySnapshot {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn expand_to_line(&self, range: Range<Point>) -> Range<Point> {
|
||||||
|
let mut new_start = self.prev_line_boundary(range.start).0;
|
||||||
|
let mut new_end = self.next_line_boundary(range.end).0;
|
||||||
|
|
||||||
|
if new_start.row == range.start.row && new_end.row == range.end.row {
|
||||||
|
if new_end.row < self.buffer_snapshot.max_point().row {
|
||||||
|
new_end.row += 1;
|
||||||
|
new_end.column = 0;
|
||||||
|
} else if new_start.row > 0 {
|
||||||
|
new_start.row -= 1;
|
||||||
|
new_start.column = self.buffer_snapshot.line_len(new_start.row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
new_start..new_end
|
||||||
|
}
|
||||||
|
|
||||||
fn point_to_display_point(&self, point: Point, bias: Bias) -> DisplayPoint {
|
fn point_to_display_point(&self, point: Point, bias: Bias) -> DisplayPoint {
|
||||||
let fold_point = self.folds_snapshot.to_fold_point(point, bias);
|
let fold_point = self.folds_snapshot.to_fold_point(point, bias);
|
||||||
let tab_point = self.tabs_snapshot.to_tab_point(fold_point);
|
let tab_point = self.tabs_snapshot.to_tab_point(fold_point);
|
||||||
|
@ -3,10 +3,10 @@ mod element;
|
|||||||
pub mod items;
|
pub mod items;
|
||||||
pub mod movement;
|
pub mod movement;
|
||||||
mod multi_buffer;
|
mod multi_buffer;
|
||||||
mod selections_collection;
|
pub mod selections_collection;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
mod test;
|
pub mod test;
|
||||||
|
|
||||||
use aho_corasick::AhoCorasick;
|
use aho_corasick::AhoCorasick;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
@ -850,9 +850,9 @@ struct ActiveDiagnosticGroup {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
struct ClipboardSelection {
|
pub struct ClipboardSelection {
|
||||||
len: usize,
|
pub len: usize,
|
||||||
is_entire_line: bool,
|
pub is_entire_line: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@ -1038,6 +1038,10 @@ impl Editor {
|
|||||||
self.buffer.read(cx).replica_id()
|
self.buffer.read(cx).replica_id()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn leader_replica_id(&self) -> Option<ReplicaId> {
|
||||||
|
self.leader_replica_id
|
||||||
|
}
|
||||||
|
|
||||||
pub fn buffer(&self) -> &ModelHandle<MultiBuffer> {
|
pub fn buffer(&self) -> &ModelHandle<MultiBuffer> {
|
||||||
&self.buffer
|
&self.buffer
|
||||||
}
|
}
|
||||||
@ -1332,7 +1336,11 @@ impl Editor {
|
|||||||
) {
|
) {
|
||||||
if self.focused && self.leader_replica_id.is_none() {
|
if self.focused && self.leader_replica_id.is_none() {
|
||||||
self.buffer.update(cx, |buffer, cx| {
|
self.buffer.update(cx, |buffer, cx| {
|
||||||
buffer.set_active_selections(&self.selections.disjoint_anchors(), cx)
|
buffer.set_active_selections(
|
||||||
|
&self.selections.disjoint_anchors(),
|
||||||
|
self.selections.line_mode,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1406,12 +1414,14 @@ impl Editor {
|
|||||||
let old_cursor_position = self.selections.newest_anchor().head();
|
let old_cursor_position = self.selections.newest_anchor().head();
|
||||||
self.push_to_selection_history();
|
self.push_to_selection_history();
|
||||||
|
|
||||||
let result = self.selections.change_with(cx, change);
|
let (changed, result) = self.selections.change_with(cx, change);
|
||||||
|
|
||||||
if let Some(autoscroll) = autoscroll {
|
if changed {
|
||||||
self.request_autoscroll(autoscroll, cx);
|
if let Some(autoscroll) = autoscroll {
|
||||||
|
self.request_autoscroll(autoscroll, cx);
|
||||||
|
}
|
||||||
|
self.selections_did_change(true, &old_cursor_position, cx);
|
||||||
}
|
}
|
||||||
self.selections_did_change(true, &old_cursor_position, cx);
|
|
||||||
|
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
@ -1551,12 +1561,10 @@ impl Editor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||||
if add {
|
if !add {
|
||||||
if click_count > 1 {
|
|
||||||
s.delete(newest_selection.id);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
s.clear_disjoint();
|
s.clear_disjoint();
|
||||||
|
} else if click_count > 1 {
|
||||||
|
s.delete(newest_selection.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
s.set_pending_range(start..end, mode);
|
s.set_pending_range(start..end, mode);
|
||||||
@ -1869,13 +1877,16 @@ impl Editor {
|
|||||||
pub fn insert(&mut self, text: &str, cx: &mut ViewContext<Self>) {
|
pub fn insert(&mut self, text: &str, cx: &mut ViewContext<Self>) {
|
||||||
let text: Arc<str> = text.into();
|
let text: Arc<str> = text.into();
|
||||||
self.transact(cx, |this, cx| {
|
self.transact(cx, |this, cx| {
|
||||||
let old_selections = this.selections.all::<usize>(cx);
|
let old_selections = this.selections.all_adjusted(cx);
|
||||||
let selection_anchors = this.buffer.update(cx, |buffer, cx| {
|
let selection_anchors = this.buffer.update(cx, |buffer, cx| {
|
||||||
let anchors = {
|
let anchors = {
|
||||||
let snapshot = buffer.read(cx);
|
let snapshot = buffer.read(cx);
|
||||||
old_selections
|
old_selections
|
||||||
.iter()
|
.iter()
|
||||||
.map(|s| (s.id, s.goal, snapshot.anchor_after(s.end)))
|
.map(|s| {
|
||||||
|
let anchor = snapshot.anchor_after(s.end);
|
||||||
|
s.map(|_| anchor.clone())
|
||||||
|
})
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
};
|
};
|
||||||
buffer.edit_with_autoindent(
|
buffer.edit_with_autoindent(
|
||||||
@ -1887,25 +1898,8 @@ impl Editor {
|
|||||||
anchors
|
anchors
|
||||||
});
|
});
|
||||||
|
|
||||||
let selections = {
|
|
||||||
let snapshot = this.buffer.read(cx).read(cx);
|
|
||||||
selection_anchors
|
|
||||||
.into_iter()
|
|
||||||
.map(|(id, goal, position)| {
|
|
||||||
let position = position.to_offset(&snapshot);
|
|
||||||
Selection {
|
|
||||||
id,
|
|
||||||
start: position,
|
|
||||||
end: position,
|
|
||||||
goal,
|
|
||||||
reversed: false,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
};
|
|
||||||
|
|
||||||
this.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
this.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||||
s.select(selections);
|
s.select_anchors(selection_anchors);
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -2758,28 +2752,31 @@ impl Editor {
|
|||||||
pub fn backspace(&mut self, _: &Backspace, cx: &mut ViewContext<Self>) {
|
pub fn backspace(&mut self, _: &Backspace, cx: &mut ViewContext<Self>) {
|
||||||
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
|
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
|
||||||
let mut selections = self.selections.all::<Point>(cx);
|
let mut selections = self.selections.all::<Point>(cx);
|
||||||
for selection in &mut selections {
|
if !self.selections.line_mode {
|
||||||
if selection.is_empty() {
|
for selection in &mut selections {
|
||||||
let old_head = selection.head();
|
if selection.is_empty() {
|
||||||
let mut new_head =
|
let old_head = selection.head();
|
||||||
movement::left(&display_map, old_head.to_display_point(&display_map))
|
let mut new_head =
|
||||||
.to_point(&display_map);
|
movement::left(&display_map, old_head.to_display_point(&display_map))
|
||||||
if let Some((buffer, line_buffer_range)) = display_map
|
.to_point(&display_map);
|
||||||
.buffer_snapshot
|
if let Some((buffer, line_buffer_range)) = display_map
|
||||||
.buffer_line_for_row(old_head.row)
|
.buffer_snapshot
|
||||||
{
|
.buffer_line_for_row(old_head.row)
|
||||||
let indent_column = buffer.indent_column_for_line(line_buffer_range.start.row);
|
{
|
||||||
let language_name = buffer.language().map(|language| language.name());
|
let indent_column =
|
||||||
let indent = cx.global::<Settings>().tab_size(language_name.as_deref());
|
buffer.indent_column_for_line(line_buffer_range.start.row);
|
||||||
if old_head.column <= indent_column && old_head.column > 0 {
|
let language_name = buffer.language().map(|language| language.name());
|
||||||
new_head = cmp::min(
|
let indent = cx.global::<Settings>().tab_size(language_name.as_deref());
|
||||||
new_head,
|
if old_head.column <= indent_column && old_head.column > 0 {
|
||||||
Point::new(old_head.row, ((old_head.column - 1) / indent) * indent),
|
new_head = cmp::min(
|
||||||
);
|
new_head,
|
||||||
|
Point::new(old_head.row, ((old_head.column - 1) / indent) * indent),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
selection.set_head(new_head, SelectionGoal::None);
|
selection.set_head(new_head, SelectionGoal::None);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2792,8 +2789,9 @@ impl Editor {
|
|||||||
pub fn delete(&mut self, _: &Delete, cx: &mut ViewContext<Self>) {
|
pub fn delete(&mut self, _: &Delete, cx: &mut ViewContext<Self>) {
|
||||||
self.transact(cx, |this, cx| {
|
self.transact(cx, |this, cx| {
|
||||||
this.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
this.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||||
|
let line_mode = s.line_mode;
|
||||||
s.move_with(|map, selection| {
|
s.move_with(|map, selection| {
|
||||||
if selection.is_empty() {
|
if selection.is_empty() && !line_mode {
|
||||||
let cursor = movement::right(map, selection.head());
|
let cursor = movement::right(map, selection.head());
|
||||||
selection.set_head(cursor, SelectionGoal::None);
|
selection.set_head(cursor, SelectionGoal::None);
|
||||||
}
|
}
|
||||||
@ -2816,7 +2814,7 @@ impl Editor {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut selections = self.selections.all::<Point>(cx);
|
let mut selections = self.selections.all_adjusted(cx);
|
||||||
if selections.iter().all(|s| s.is_empty()) {
|
if selections.iter().all(|s| s.is_empty()) {
|
||||||
self.transact(cx, |this, cx| {
|
self.transact(cx, |this, cx| {
|
||||||
this.buffer.update(cx, |buffer, cx| {
|
this.buffer.update(cx, |buffer, cx| {
|
||||||
@ -3302,8 +3300,9 @@ impl Editor {
|
|||||||
self.transact(cx, |this, cx| {
|
self.transact(cx, |this, cx| {
|
||||||
let edits = this.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
let edits = this.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||||
let mut edits: Vec<(Range<usize>, String)> = Default::default();
|
let mut edits: Vec<(Range<usize>, String)> = Default::default();
|
||||||
|
let line_mode = s.line_mode;
|
||||||
s.move_with(|display_map, selection| {
|
s.move_with(|display_map, selection| {
|
||||||
if !selection.is_empty() {
|
if !selection.is_empty() || line_mode {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3356,7 +3355,7 @@ impl Editor {
|
|||||||
{
|
{
|
||||||
let max_point = buffer.max_point();
|
let max_point = buffer.max_point();
|
||||||
for selection in &mut selections {
|
for selection in &mut selections {
|
||||||
let is_entire_line = selection.is_empty();
|
let is_entire_line = selection.is_empty() || self.selections.line_mode;
|
||||||
if is_entire_line {
|
if is_entire_line {
|
||||||
selection.start = Point::new(selection.start.row, 0);
|
selection.start = Point::new(selection.start.row, 0);
|
||||||
selection.end = cmp::min(max_point, Point::new(selection.end.row + 1, 0));
|
selection.end = cmp::min(max_point, Point::new(selection.end.row + 1, 0));
|
||||||
@ -3387,16 +3386,17 @@ impl Editor {
|
|||||||
let selections = self.selections.all::<Point>(cx);
|
let selections = self.selections.all::<Point>(cx);
|
||||||
let buffer = self.buffer.read(cx).read(cx);
|
let buffer = self.buffer.read(cx).read(cx);
|
||||||
let mut text = String::new();
|
let mut text = String::new();
|
||||||
|
|
||||||
let mut clipboard_selections = Vec::with_capacity(selections.len());
|
let mut clipboard_selections = Vec::with_capacity(selections.len());
|
||||||
{
|
{
|
||||||
let max_point = buffer.max_point();
|
let max_point = buffer.max_point();
|
||||||
for selection in selections.iter() {
|
for selection in selections.iter() {
|
||||||
let mut start = selection.start;
|
let mut start = selection.start;
|
||||||
let mut end = selection.end;
|
let mut end = selection.end;
|
||||||
let is_entire_line = selection.is_empty();
|
let is_entire_line = selection.is_empty() || self.selections.line_mode;
|
||||||
if is_entire_line {
|
if is_entire_line {
|
||||||
start = Point::new(start.row, 0);
|
start = Point::new(start.row, 0);
|
||||||
end = cmp::min(max_point, Point::new(start.row + 1, 0));
|
end = cmp::min(max_point, Point::new(end.row + 1, 0));
|
||||||
}
|
}
|
||||||
let mut len = 0;
|
let mut len = 0;
|
||||||
for chunk in buffer.text_for_range(start..end) {
|
for chunk in buffer.text_for_range(start..end) {
|
||||||
@ -3440,6 +3440,7 @@ impl Editor {
|
|||||||
let snapshot = buffer.read(cx);
|
let snapshot = buffer.read(cx);
|
||||||
let mut start_offset = 0;
|
let mut start_offset = 0;
|
||||||
let mut edits = Vec::new();
|
let mut edits = Vec::new();
|
||||||
|
let line_mode = this.selections.line_mode;
|
||||||
for (ix, selection) in old_selections.iter().enumerate() {
|
for (ix, selection) in old_selections.iter().enumerate() {
|
||||||
let to_insert;
|
let to_insert;
|
||||||
let entire_line;
|
let entire_line;
|
||||||
@ -3457,12 +3458,12 @@ impl Editor {
|
|||||||
// clipboard text was written, then the entire line containing the
|
// clipboard text was written, then the entire line containing the
|
||||||
// selection was copied. If this selection is also currently empty,
|
// selection was copied. If this selection is also currently empty,
|
||||||
// then paste the line before the current line of the buffer.
|
// then paste the line before the current line of the buffer.
|
||||||
let range = if selection.is_empty() && entire_line {
|
let range = if selection.is_empty() && !line_mode && entire_line {
|
||||||
let column = selection.start.to_point(&snapshot).column as usize;
|
let column = selection.start.to_point(&snapshot).column as usize;
|
||||||
let line_start = selection.start - column;
|
let line_start = selection.start - column;
|
||||||
line_start..line_start
|
line_start..line_start
|
||||||
} else {
|
} else {
|
||||||
selection.start..selection.end
|
selection.range()
|
||||||
};
|
};
|
||||||
|
|
||||||
edits.push((range, to_insert));
|
edits.push((range, to_insert));
|
||||||
@ -3512,8 +3513,9 @@ impl Editor {
|
|||||||
|
|
||||||
pub fn move_left(&mut self, _: &MoveLeft, cx: &mut ViewContext<Self>) {
|
pub fn move_left(&mut self, _: &MoveLeft, cx: &mut ViewContext<Self>) {
|
||||||
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||||
|
let line_mode = s.line_mode;
|
||||||
s.move_with(|map, selection| {
|
s.move_with(|map, selection| {
|
||||||
let cursor = if selection.is_empty() {
|
let cursor = if selection.is_empty() && !line_mode {
|
||||||
movement::left(map, selection.start)
|
movement::left(map, selection.start)
|
||||||
} else {
|
} else {
|
||||||
selection.start
|
selection.start
|
||||||
@ -3531,8 +3533,9 @@ impl Editor {
|
|||||||
|
|
||||||
pub fn move_right(&mut self, _: &MoveRight, cx: &mut ViewContext<Self>) {
|
pub fn move_right(&mut self, _: &MoveRight, cx: &mut ViewContext<Self>) {
|
||||||
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||||
|
let line_mode = s.line_mode;
|
||||||
s.move_with(|map, selection| {
|
s.move_with(|map, selection| {
|
||||||
let cursor = if selection.is_empty() {
|
let cursor = if selection.is_empty() && !line_mode {
|
||||||
movement::right(map, selection.end)
|
movement::right(map, selection.end)
|
||||||
} else {
|
} else {
|
||||||
selection.end
|
selection.end
|
||||||
@ -3565,8 +3568,9 @@ impl Editor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||||
|
let line_mode = s.line_mode;
|
||||||
s.move_with(|map, selection| {
|
s.move_with(|map, selection| {
|
||||||
if !selection.is_empty() {
|
if !selection.is_empty() && !line_mode {
|
||||||
selection.goal = SelectionGoal::None;
|
selection.goal = SelectionGoal::None;
|
||||||
}
|
}
|
||||||
let (cursor, goal) = movement::up(&map, selection.start, selection.goal, false);
|
let (cursor, goal) = movement::up(&map, selection.start, selection.goal, false);
|
||||||
@ -3596,8 +3600,9 @@ impl Editor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||||
|
let line_mode = s.line_mode;
|
||||||
s.move_with(|map, selection| {
|
s.move_with(|map, selection| {
|
||||||
if !selection.is_empty() {
|
if !selection.is_empty() && !line_mode {
|
||||||
selection.goal = SelectionGoal::None;
|
selection.goal = SelectionGoal::None;
|
||||||
}
|
}
|
||||||
let (cursor, goal) = movement::down(&map, selection.end, selection.goal, false);
|
let (cursor, goal) = movement::down(&map, selection.end, selection.goal, false);
|
||||||
@ -3679,8 +3684,9 @@ impl Editor {
|
|||||||
) {
|
) {
|
||||||
self.transact(cx, |this, cx| {
|
self.transact(cx, |this, cx| {
|
||||||
this.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
this.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||||
|
let line_mode = s.line_mode;
|
||||||
s.move_with(|map, selection| {
|
s.move_with(|map, selection| {
|
||||||
if selection.is_empty() {
|
if selection.is_empty() && !line_mode {
|
||||||
let cursor = movement::previous_word_start(map, selection.head());
|
let cursor = movement::previous_word_start(map, selection.head());
|
||||||
selection.set_head(cursor, SelectionGoal::None);
|
selection.set_head(cursor, SelectionGoal::None);
|
||||||
}
|
}
|
||||||
@ -3697,8 +3703,9 @@ impl Editor {
|
|||||||
) {
|
) {
|
||||||
self.transact(cx, |this, cx| {
|
self.transact(cx, |this, cx| {
|
||||||
this.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
this.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||||
|
let line_mode = s.line_mode;
|
||||||
s.move_with(|map, selection| {
|
s.move_with(|map, selection| {
|
||||||
if selection.is_empty() {
|
if selection.is_empty() && !line_mode {
|
||||||
let cursor = movement::previous_subword_start(map, selection.head());
|
let cursor = movement::previous_subword_start(map, selection.head());
|
||||||
selection.set_head(cursor, SelectionGoal::None);
|
selection.set_head(cursor, SelectionGoal::None);
|
||||||
}
|
}
|
||||||
@ -3751,8 +3758,9 @@ impl Editor {
|
|||||||
pub fn delete_to_next_word_end(&mut self, _: &DeleteToNextWordEnd, cx: &mut ViewContext<Self>) {
|
pub fn delete_to_next_word_end(&mut self, _: &DeleteToNextWordEnd, cx: &mut ViewContext<Self>) {
|
||||||
self.transact(cx, |this, cx| {
|
self.transact(cx, |this, cx| {
|
||||||
this.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
this.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||||
|
let line_mode = s.line_mode;
|
||||||
s.move_with(|map, selection| {
|
s.move_with(|map, selection| {
|
||||||
if selection.is_empty() {
|
if selection.is_empty() && !line_mode {
|
||||||
let cursor = movement::next_word_end(map, selection.head());
|
let cursor = movement::next_word_end(map, selection.head());
|
||||||
selection.set_head(cursor, SelectionGoal::None);
|
selection.set_head(cursor, SelectionGoal::None);
|
||||||
}
|
}
|
||||||
@ -4698,6 +4706,7 @@ impl Editor {
|
|||||||
// Position the selection in the rename editor so that it matches the current selection.
|
// Position the selection in the rename editor so that it matches the current selection.
|
||||||
this.show_local_selections = false;
|
this.show_local_selections = false;
|
||||||
let rename_editor = cx.add_view(|cx| {
|
let rename_editor = cx.add_view(|cx| {
|
||||||
|
println!("Rename editor created.");
|
||||||
let mut editor = Editor::single_line(None, cx);
|
let mut editor = Editor::single_line(None, cx);
|
||||||
if let Some(old_highlight_id) = old_highlight_id {
|
if let Some(old_highlight_id) = old_highlight_id {
|
||||||
editor.override_text_style =
|
editor.override_text_style =
|
||||||
@ -5612,7 +5621,11 @@ impl View for Editor {
|
|||||||
self.buffer.update(cx, |buffer, cx| {
|
self.buffer.update(cx, |buffer, cx| {
|
||||||
buffer.finalize_last_transaction(cx);
|
buffer.finalize_last_transaction(cx);
|
||||||
if self.leader_replica_id.is_none() {
|
if self.leader_replica_id.is_none() {
|
||||||
buffer.set_active_selections(&self.selections.disjoint_anchors(), cx);
|
buffer.set_active_selections(
|
||||||
|
&self.selections.disjoint_anchors(),
|
||||||
|
self.selections.line_mode,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -6033,7 +6046,9 @@ pub fn styled_runs_for_code_label<'a>(
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::test::{assert_text_with_selections, select_ranges};
|
use crate::test::{
|
||||||
|
assert_text_with_selections, build_editor, select_ranges, EditorTestContext,
|
||||||
|
};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
@ -7305,117 +7320,62 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
fn test_indent_outdent(cx: &mut gpui::MutableAppContext) {
|
async fn test_indent_outdent(cx: &mut gpui::TestAppContext) {
|
||||||
cx.set_global(Settings::test(cx));
|
let mut cx = EditorTestContext::new(cx).await;
|
||||||
let buffer = MultiBuffer::build_simple(
|
|
||||||
indoc! {"
|
|
||||||
one two
|
|
||||||
three
|
|
||||||
four"},
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer.clone(), cx));
|
|
||||||
|
|
||||||
view.update(cx, |view, cx| {
|
cx.set_state(indoc! {"
|
||||||
// two selections on the same line
|
[one} [two}
|
||||||
select_ranges(
|
three
|
||||||
view,
|
four"});
|
||||||
indoc! {"
|
cx.update_editor(|e, cx| e.tab(&Tab, cx));
|
||||||
[one] [two]
|
cx.assert_editor_state(indoc! {"
|
||||||
three
|
[one} [two}
|
||||||
four"},
|
three
|
||||||
cx,
|
four"});
|
||||||
);
|
|
||||||
|
|
||||||
// indent from mid-tabstop to full tabstop
|
cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx));
|
||||||
view.tab(&Tab, cx);
|
cx.assert_editor_state(indoc! {"
|
||||||
assert_text_with_selections(
|
[one} [two}
|
||||||
view,
|
three
|
||||||
indoc! {"
|
four"});
|
||||||
[one] [two]
|
|
||||||
three
|
|
||||||
four"},
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
|
|
||||||
// outdent from 1 tabstop to 0 tabstops
|
// select across line ending
|
||||||
view.tab_prev(&TabPrev, cx);
|
cx.set_state(indoc! {"
|
||||||
assert_text_with_selections(
|
one two
|
||||||
view,
|
t[hree
|
||||||
indoc! {"
|
} four"});
|
||||||
[one] [two]
|
cx.update_editor(|e, cx| e.tab(&Tab, cx));
|
||||||
three
|
cx.assert_editor_state(indoc! {"
|
||||||
four"},
|
one two
|
||||||
cx,
|
t[hree
|
||||||
);
|
} four"});
|
||||||
|
|
||||||
// select across line ending
|
cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx));
|
||||||
select_ranges(
|
cx.assert_editor_state(indoc! {"
|
||||||
view,
|
one two
|
||||||
indoc! {"
|
t[hree
|
||||||
one two
|
} four"});
|
||||||
t[hree
|
|
||||||
] four"},
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
|
|
||||||
// indent and outdent affect only the preceding line
|
// Ensure that indenting/outdenting works when the cursor is at column 0.
|
||||||
view.tab(&Tab, cx);
|
cx.set_state(indoc! {"
|
||||||
assert_text_with_selections(
|
one two
|
||||||
view,
|
|three
|
||||||
indoc! {"
|
four"});
|
||||||
one two
|
cx.update_editor(|e, cx| e.tab(&Tab, cx));
|
||||||
t[hree
|
cx.assert_editor_state(indoc! {"
|
||||||
] four"},
|
one two
|
||||||
cx,
|
|three
|
||||||
);
|
four"});
|
||||||
view.tab_prev(&TabPrev, cx);
|
|
||||||
assert_text_with_selections(
|
|
||||||
view,
|
|
||||||
indoc! {"
|
|
||||||
one two
|
|
||||||
t[hree
|
|
||||||
] four"},
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Ensure that indenting/outdenting works when the cursor is at column 0.
|
cx.set_state(indoc! {"
|
||||||
select_ranges(
|
one two
|
||||||
view,
|
| three
|
||||||
indoc! {"
|
four"});
|
||||||
one two
|
cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx));
|
||||||
[]three
|
cx.assert_editor_state(indoc! {"
|
||||||
four"},
|
one two
|
||||||
cx,
|
|three
|
||||||
);
|
four"});
|
||||||
view.tab(&Tab, cx);
|
|
||||||
assert_text_with_selections(
|
|
||||||
view,
|
|
||||||
indoc! {"
|
|
||||||
one two
|
|
||||||
[]three
|
|
||||||
four"},
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
|
|
||||||
select_ranges(
|
|
||||||
view,
|
|
||||||
indoc! {"
|
|
||||||
one two
|
|
||||||
[] three
|
|
||||||
four"},
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
view.tab_prev(&TabPrev, cx);
|
|
||||||
assert_text_with_selections(
|
|
||||||
view,
|
|
||||||
indoc! {"
|
|
||||||
one two
|
|
||||||
[]three
|
|
||||||
four"},
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
@ -7524,73 +7484,71 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
fn test_backspace(cx: &mut gpui::MutableAppContext) {
|
async fn test_backspace(cx: &mut gpui::TestAppContext) {
|
||||||
cx.set_global(Settings::test(cx));
|
let mut cx = EditorTestContext::new(cx).await;
|
||||||
let (_, view) = cx.add_window(Default::default(), |cx| {
|
// Basic backspace
|
||||||
build_editor(MultiBuffer::build_simple("", cx), cx)
|
cx.set_state(indoc! {"
|
||||||
});
|
on|e two three
|
||||||
|
fou[r} five six
|
||||||
|
seven {eight nine
|
||||||
|
]ten"});
|
||||||
|
cx.update_editor(|e, cx| e.backspace(&Backspace, cx));
|
||||||
|
cx.assert_editor_state(indoc! {"
|
||||||
|
o|e two three
|
||||||
|
fou| five six
|
||||||
|
seven |ten"});
|
||||||
|
|
||||||
view.update(cx, |view, cx| {
|
// Test backspace inside and around indents
|
||||||
view.set_text("one two three\nfour five six\nseven eight nine\nten\n", cx);
|
cx.set_state(indoc! {"
|
||||||
view.change_selections(None, cx, |s| {
|
zero
|
||||||
s.select_display_ranges([
|
|one
|
||||||
// an empty selection - the preceding character is deleted
|
|two
|
||||||
DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2),
|
| | | three
|
||||||
// one character selected - it is deleted
|
| | four"});
|
||||||
DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3),
|
cx.update_editor(|e, cx| e.backspace(&Backspace, cx));
|
||||||
// a line suffix selected - it is deleted
|
cx.assert_editor_state(indoc! {"
|
||||||
DisplayPoint::new(2, 6)..DisplayPoint::new(3, 0),
|
zero
|
||||||
])
|
|one
|
||||||
});
|
|two
|
||||||
view.backspace(&Backspace, cx);
|
| three| four"});
|
||||||
assert_eq!(view.text(cx), "oe two three\nfou five six\nseven ten\n");
|
|
||||||
|
|
||||||
view.set_text(" one\n two\n three\n four", cx);
|
// Test backspace with line_mode set to true
|
||||||
view.change_selections(None, cx, |s| {
|
cx.update_editor(|e, _| e.selections.line_mode = true);
|
||||||
s.select_display_ranges([
|
cx.set_state(indoc! {"
|
||||||
// cursors at the the end of leading indent - last indent is deleted
|
The |quick |brown
|
||||||
DisplayPoint::new(0, 4)..DisplayPoint::new(0, 4),
|
fox jumps over
|
||||||
DisplayPoint::new(1, 8)..DisplayPoint::new(1, 8),
|
the lazy dog
|
||||||
// cursors inside leading indent - overlapping indent deletions are coalesced
|
|The qu[ick b}rown"});
|
||||||
DisplayPoint::new(2, 4)..DisplayPoint::new(2, 4),
|
cx.update_editor(|e, cx| e.backspace(&Backspace, cx));
|
||||||
DisplayPoint::new(2, 5)..DisplayPoint::new(2, 5),
|
cx.assert_editor_state(indoc! {"
|
||||||
DisplayPoint::new(2, 6)..DisplayPoint::new(2, 6),
|
|fox jumps over
|
||||||
// cursor at the beginning of a line - preceding newline is deleted
|
the lazy dog|"});
|
||||||
DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0),
|
|
||||||
// selection inside leading indent - only the selected character is deleted
|
|
||||||
DisplayPoint::new(3, 2)..DisplayPoint::new(3, 3),
|
|
||||||
])
|
|
||||||
});
|
|
||||||
view.backspace(&Backspace, cx);
|
|
||||||
assert_eq!(view.text(cx), "one\n two\n three four");
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
fn test_delete(cx: &mut gpui::MutableAppContext) {
|
async fn test_delete(cx: &mut gpui::TestAppContext) {
|
||||||
cx.set_global(Settings::test(cx));
|
let mut cx = EditorTestContext::new(cx).await;
|
||||||
let buffer =
|
|
||||||
MultiBuffer::build_simple("one two three\nfour five six\nseven eight nine\nten\n", cx);
|
|
||||||
let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer.clone(), cx));
|
|
||||||
|
|
||||||
view.update(cx, |view, cx| {
|
cx.set_state(indoc! {"
|
||||||
view.change_selections(None, cx, |s| {
|
on|e two three
|
||||||
s.select_display_ranges([
|
fou[r} five six
|
||||||
// an empty selection - the following character is deleted
|
seven {eight nine
|
||||||
DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2),
|
]ten"});
|
||||||
// one character selected - it is deleted
|
cx.update_editor(|e, cx| e.delete(&Delete, cx));
|
||||||
DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3),
|
cx.assert_editor_state(indoc! {"
|
||||||
// a line suffix selected - it is deleted
|
on| two three
|
||||||
DisplayPoint::new(2, 6)..DisplayPoint::new(3, 0),
|
fou| five six
|
||||||
])
|
seven |ten"});
|
||||||
});
|
|
||||||
view.delete(&Delete, cx);
|
|
||||||
});
|
|
||||||
|
|
||||||
assert_eq!(
|
// Test backspace with line_mode set to true
|
||||||
buffer.read(cx).read(cx).text(),
|
cx.update_editor(|e, _| e.selections.line_mode = true);
|
||||||
"on two three\nfou five six\nseven ten\n"
|
cx.set_state(indoc! {"
|
||||||
);
|
The |quick |brown
|
||||||
|
fox {jum]ps over
|
||||||
|
the lazy dog
|
||||||
|
|The qu[ick b}rown"});
|
||||||
|
cx.update_editor(|e, cx| e.backspace(&Backspace, cx));
|
||||||
|
cx.assert_editor_state("|the lazy dog|");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
@ -7898,131 +7856,79 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
fn test_clipboard(cx: &mut gpui::MutableAppContext) {
|
async fn test_clipboard(cx: &mut gpui::TestAppContext) {
|
||||||
cx.set_global(Settings::test(cx));
|
let mut cx = EditorTestContext::new(cx).await;
|
||||||
let buffer = MultiBuffer::build_simple("one✅ two three four five six ", cx);
|
|
||||||
let view = cx
|
|
||||||
.add_window(Default::default(), |cx| build_editor(buffer.clone(), cx))
|
|
||||||
.1;
|
|
||||||
|
|
||||||
// Cut with three selections. Clipboard text is divided into three slices.
|
cx.set_state("[one✅ }two [three }four [five }six ");
|
||||||
view.update(cx, |view, cx| {
|
cx.update_editor(|e, cx| e.cut(&Cut, cx));
|
||||||
view.change_selections(None, cx, |s| s.select_ranges(vec![0..7, 11..17, 22..27]));
|
cx.assert_editor_state("|two |four |six ");
|
||||||
view.cut(&Cut, cx);
|
|
||||||
assert_eq!(view.display_text(cx), "two four six ");
|
|
||||||
});
|
|
||||||
|
|
||||||
// Paste with three cursors. Each cursor pastes one slice of the clipboard text.
|
// Paste with three cursors. Each cursor pastes one slice of the clipboard text.
|
||||||
view.update(cx, |view, cx| {
|
cx.set_state("two |four |six |");
|
||||||
view.change_selections(None, cx, |s| s.select_ranges(vec![4..4, 9..9, 13..13]));
|
cx.update_editor(|e, cx| e.paste(&Paste, cx));
|
||||||
view.paste(&Paste, cx);
|
cx.assert_editor_state("two one✅ |four three |six five |");
|
||||||
assert_eq!(view.display_text(cx), "two one✅ four three six five ");
|
|
||||||
assert_eq!(
|
|
||||||
view.selections.display_ranges(cx),
|
|
||||||
&[
|
|
||||||
DisplayPoint::new(0, 11)..DisplayPoint::new(0, 11),
|
|
||||||
DisplayPoint::new(0, 22)..DisplayPoint::new(0, 22),
|
|
||||||
DisplayPoint::new(0, 31)..DisplayPoint::new(0, 31)
|
|
||||||
]
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Paste again but with only two cursors. Since the number of cursors doesn't
|
// Paste again but with only two cursors. Since the number of cursors doesn't
|
||||||
// match the number of slices in the clipboard, the entire clipboard text
|
// match the number of slices in the clipboard, the entire clipboard text
|
||||||
// is pasted at each cursor.
|
// is pasted at each cursor.
|
||||||
view.update(cx, |view, cx| {
|
cx.set_state("|two one✅ four three six five |");
|
||||||
view.change_selections(None, cx, |s| s.select_ranges(vec![0..0, 31..31]));
|
cx.update_editor(|e, cx| {
|
||||||
view.handle_input(&Input("( ".into()), cx);
|
e.handle_input(&Input("( ".into()), cx);
|
||||||
view.paste(&Paste, cx);
|
e.paste(&Paste, cx);
|
||||||
view.handle_input(&Input(") ".into()), cx);
|
e.handle_input(&Input(") ".into()), cx);
|
||||||
assert_eq!(
|
|
||||||
view.display_text(cx),
|
|
||||||
"( one✅ \nthree \nfive ) two one✅ four three six five ( one✅ \nthree \nfive ) "
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
view.update(cx, |view, cx| {
|
|
||||||
view.change_selections(None, cx, |s| s.select_ranges(vec![0..0]));
|
|
||||||
view.handle_input(&Input("123\n4567\n89\n".into()), cx);
|
|
||||||
assert_eq!(
|
|
||||||
view.display_text(cx),
|
|
||||||
"123\n4567\n89\n( one✅ \nthree \nfive ) two one✅ four three six five ( one✅ \nthree \nfive ) "
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
cx.assert_editor_state(indoc! {"
|
||||||
|
( one✅
|
||||||
|
three
|
||||||
|
five ) |two one✅ four three six five ( one✅
|
||||||
|
three
|
||||||
|
five ) |"});
|
||||||
|
|
||||||
// Cut with three selections, one of which is full-line.
|
// Cut with three selections, one of which is full-line.
|
||||||
view.update(cx, |view, cx| {
|
cx.set_state(indoc! {"
|
||||||
view.change_selections(None, cx, |s| s.select_display_ranges(
|
1[2}3
|
||||||
[
|
4|567
|
||||||
DisplayPoint::new(0, 1)..DisplayPoint::new(0, 2),
|
[8}9"});
|
||||||
DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1),
|
cx.update_editor(|e, cx| e.cut(&Cut, cx));
|
||||||
DisplayPoint::new(2, 0)..DisplayPoint::new(2, 1),
|
cx.assert_editor_state(indoc! {"
|
||||||
],
|
1|3
|
||||||
));
|
|9"});
|
||||||
view.cut(&Cut, cx);
|
|
||||||
assert_eq!(
|
|
||||||
view.display_text(cx),
|
|
||||||
"13\n9\n( one✅ \nthree \nfive ) two one✅ four three six five ( one✅ \nthree \nfive ) "
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Paste with three selections, noticing how the copied selection that was full-line
|
// Paste with three selections, noticing how the copied selection that was full-line
|
||||||
// gets inserted before the second cursor.
|
// gets inserted before the second cursor.
|
||||||
view.update(cx, |view, cx| {
|
cx.set_state(indoc! {"
|
||||||
view.change_selections(None, cx, |s| s.select_display_ranges(
|
1|3
|
||||||
[
|
9|
|
||||||
DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1),
|
[o}ne"});
|
||||||
DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1),
|
cx.update_editor(|e, cx| e.paste(&Paste, cx));
|
||||||
DisplayPoint::new(2, 2)..DisplayPoint::new(2, 3),
|
cx.assert_editor_state(indoc! {"
|
||||||
],
|
12|3
|
||||||
));
|
4567
|
||||||
view.paste(&Paste, cx);
|
9|
|
||||||
assert_eq!(
|
8|ne"});
|
||||||
view.display_text(cx),
|
|
||||||
"123\n4567\n9\n( 8ne✅ \nthree \nfive ) two one✅ four three six five ( one✅ \nthree \nfive ) "
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
view.selections.display_ranges(cx),
|
|
||||||
&[
|
|
||||||
DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2),
|
|
||||||
DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1),
|
|
||||||
DisplayPoint::new(3, 3)..DisplayPoint::new(3, 3),
|
|
||||||
]
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Copy with a single cursor only, which writes the whole line into the clipboard.
|
// Copy with a single cursor only, which writes the whole line into the clipboard.
|
||||||
view.update(cx, |view, cx| {
|
cx.set_state(indoc! {"
|
||||||
view.change_selections(None, cx, |s| {
|
The quick brown
|
||||||
s.select_display_ranges([DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1)])
|
fox ju|mps over
|
||||||
});
|
the lazy dog"});
|
||||||
view.copy(&Copy, cx);
|
cx.update_editor(|e, cx| e.copy(&Copy, cx));
|
||||||
});
|
cx.assert_clipboard_content(Some("fox jumps over\n"));
|
||||||
|
|
||||||
// Paste with three selections, noticing how the copied full-line selection is inserted
|
// Paste with three selections, noticing how the copied full-line selection is inserted
|
||||||
// before the empty selections but replaces the selection that is non-empty.
|
// before the empty selections but replaces the selection that is non-empty.
|
||||||
view.update(cx, |view, cx| {
|
cx.set_state(indoc! {"
|
||||||
view.change_selections(None, cx, |s| s.select_display_ranges(
|
T|he quick brown
|
||||||
[
|
[fo}x jumps over
|
||||||
DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1),
|
t|he lazy dog"});
|
||||||
DisplayPoint::new(1, 0)..DisplayPoint::new(1, 2),
|
cx.update_editor(|e, cx| e.paste(&Paste, cx));
|
||||||
DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1),
|
cx.assert_editor_state(indoc! {"
|
||||||
],
|
fox jumps over
|
||||||
));
|
T|he quick brown
|
||||||
view.paste(&Paste, cx);
|
fox jumps over
|
||||||
assert_eq!(
|
|x jumps over
|
||||||
view.display_text(cx),
|
fox jumps over
|
||||||
"123\n123\n123\n67\n123\n9\n( 8ne✅ \nthree \nfive ) two one✅ four three six five ( one✅ \nthree \nfive ) "
|
t|he lazy dog"});
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
view.selections.display_ranges(cx),
|
|
||||||
&[
|
|
||||||
DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1),
|
|
||||||
DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0),
|
|
||||||
DisplayPoint::new(5, 1)..DisplayPoint::new(5, 1),
|
|
||||||
]
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
@ -8761,8 +8667,10 @@ mod tests {
|
|||||||
fn assert(editor: &mut Editor, cx: &mut ViewContext<Editor>, marked_text_ranges: &str) {
|
fn assert(editor: &mut Editor, cx: &mut ViewContext<Editor>, marked_text_ranges: &str) {
|
||||||
let range_markers = ('<', '>');
|
let range_markers = ('<', '>');
|
||||||
let (expected_text, mut selection_ranges_lookup) =
|
let (expected_text, mut selection_ranges_lookup) =
|
||||||
marked_text_ranges_by(marked_text_ranges, vec![range_markers.clone()]);
|
marked_text_ranges_by(marked_text_ranges, vec![range_markers.clone().into()]);
|
||||||
let selection_ranges = selection_ranges_lookup.remove(&range_markers).unwrap();
|
let selection_ranges = selection_ranges_lookup
|
||||||
|
.remove(&range_markers.into())
|
||||||
|
.unwrap();
|
||||||
assert_eq!(editor.text(cx), expected_text);
|
assert_eq!(editor.text(cx), expected_text);
|
||||||
assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
|
assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
|
||||||
}
|
}
|
||||||
@ -9811,10 +9719,6 @@ mod tests {
|
|||||||
point..point
|
point..point
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_editor(buffer: ModelHandle<MultiBuffer>, cx: &mut ViewContext<Editor>) -> Editor {
|
|
||||||
Editor::new(EditorMode::Full, buffer, None, None, None, cx)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn assert_selection_ranges(
|
fn assert_selection_ranges(
|
||||||
marked_text: &str,
|
marked_text: &str,
|
||||||
selection_marker_pairs: Vec<(char, char)>,
|
selection_marker_pairs: Vec<(char, char)>,
|
||||||
|
@ -3,7 +3,10 @@ use super::{
|
|||||||
Anchor, DisplayPoint, Editor, EditorMode, EditorSnapshot, Input, Scroll, Select, SelectPhase,
|
Anchor, DisplayPoint, Editor, EditorMode, EditorSnapshot, Input, Scroll, Select, SelectPhase,
|
||||||
SoftWrap, ToPoint, MAX_LINE_LEN,
|
SoftWrap, ToPoint, MAX_LINE_LEN,
|
||||||
};
|
};
|
||||||
use crate::{display_map::TransformBlock, EditorStyle};
|
use crate::{
|
||||||
|
display_map::{DisplaySnapshot, TransformBlock},
|
||||||
|
EditorStyle,
|
||||||
|
};
|
||||||
use clock::ReplicaId;
|
use clock::ReplicaId;
|
||||||
use collections::{BTreeMap, HashMap};
|
use collections::{BTreeMap, HashMap};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
@ -23,7 +26,7 @@ use gpui::{
|
|||||||
WeakViewHandle,
|
WeakViewHandle,
|
||||||
};
|
};
|
||||||
use json::json;
|
use json::json;
|
||||||
use language::{Bias, DiagnosticSeverity};
|
use language::{Bias, DiagnosticSeverity, Selection};
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
use std::{
|
use std::{
|
||||||
@ -33,6 +36,35 @@ use std::{
|
|||||||
ops::Range,
|
ops::Range,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct SelectionLayout {
|
||||||
|
head: DisplayPoint,
|
||||||
|
range: Range<DisplayPoint>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SelectionLayout {
|
||||||
|
fn new<T: ToPoint + ToDisplayPoint + Clone>(
|
||||||
|
selection: Selection<T>,
|
||||||
|
line_mode: bool,
|
||||||
|
map: &DisplaySnapshot,
|
||||||
|
) -> Self {
|
||||||
|
if line_mode {
|
||||||
|
let selection = selection.map(|p| p.to_point(&map.buffer_snapshot));
|
||||||
|
let point_range = map.expand_to_line(selection.range());
|
||||||
|
Self {
|
||||||
|
head: selection.head().to_display_point(map),
|
||||||
|
range: point_range.start.to_display_point(map)
|
||||||
|
..point_range.end.to_display_point(map),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let selection = selection.map(|p| p.to_display_point(map));
|
||||||
|
Self {
|
||||||
|
head: selection.head(),
|
||||||
|
range: selection.range(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct EditorElement {
|
pub struct EditorElement {
|
||||||
view: WeakViewHandle<Editor>,
|
view: WeakViewHandle<Editor>,
|
||||||
style: EditorStyle,
|
style: EditorStyle,
|
||||||
@ -360,7 +392,7 @@ impl EditorElement {
|
|||||||
|
|
||||||
for selection in selections {
|
for selection in selections {
|
||||||
self.paint_highlighted_range(
|
self.paint_highlighted_range(
|
||||||
selection.start..selection.end,
|
selection.range.clone(),
|
||||||
start_row,
|
start_row,
|
||||||
end_row,
|
end_row,
|
||||||
selection_style.selection,
|
selection_style.selection,
|
||||||
@ -375,7 +407,7 @@ impl EditorElement {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if view.show_local_cursors() || *replica_id != local_replica_id {
|
if view.show_local_cursors() || *replica_id != local_replica_id {
|
||||||
let cursor_position = selection.head();
|
let cursor_position = selection.head;
|
||||||
if (start_row..end_row).contains(&cursor_position.row()) {
|
if (start_row..end_row).contains(&cursor_position.row()) {
|
||||||
let cursor_row_layout =
|
let cursor_row_layout =
|
||||||
&layout.line_layouts[(cursor_position.row() - start_row) as usize];
|
&layout.line_layouts[(cursor_position.row() - start_row) as usize];
|
||||||
@ -922,7 +954,7 @@ impl Element for EditorElement {
|
|||||||
.anchor_before(DisplayPoint::new(end_row, 0).to_offset(&snapshot, Bias::Right))
|
.anchor_before(DisplayPoint::new(end_row, 0).to_offset(&snapshot, Bias::Right))
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut selections = Vec::new();
|
let mut selections: Vec<(ReplicaId, Vec<SelectionLayout>)> = Vec::new();
|
||||||
let mut active_rows = BTreeMap::new();
|
let mut active_rows = BTreeMap::new();
|
||||||
let mut highlighted_rows = None;
|
let mut highlighted_rows = None;
|
||||||
let mut highlighted_ranges = Vec::new();
|
let mut highlighted_ranges = Vec::new();
|
||||||
@ -938,7 +970,7 @@ impl Element for EditorElement {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let mut remote_selections = HashMap::default();
|
let mut remote_selections = HashMap::default();
|
||||||
for (replica_id, selection) in display_map
|
for (replica_id, line_mode, selection) in display_map
|
||||||
.buffer_snapshot
|
.buffer_snapshot
|
||||||
.remote_selections_in_range(&(start_anchor.clone()..end_anchor.clone()))
|
.remote_selections_in_range(&(start_anchor.clone()..end_anchor.clone()))
|
||||||
{
|
{
|
||||||
@ -946,17 +978,10 @@ impl Element for EditorElement {
|
|||||||
if Some(replica_id) == view.leader_replica_id {
|
if Some(replica_id) == view.leader_replica_id {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
remote_selections
|
remote_selections
|
||||||
.entry(replica_id)
|
.entry(replica_id)
|
||||||
.or_insert(Vec::new())
|
.or_insert(Vec::new())
|
||||||
.push(crate::Selection {
|
.push(SelectionLayout::new(selection, line_mode, &display_map));
|
||||||
id: selection.id,
|
|
||||||
goal: selection.goal,
|
|
||||||
reversed: selection.reversed,
|
|
||||||
start: selection.start.to_display_point(&display_map),
|
|
||||||
end: selection.end.to_display_point(&display_map),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
selections.extend(remote_selections);
|
selections.extend(remote_selections);
|
||||||
|
|
||||||
@ -985,12 +1010,8 @@ impl Element for EditorElement {
|
|||||||
local_replica_id,
|
local_replica_id,
|
||||||
local_selections
|
local_selections
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|selection| crate::Selection {
|
.map(|selection| {
|
||||||
id: selection.id,
|
SelectionLayout::new(selection, view.selections.line_mode, &display_map)
|
||||||
goal: selection.goal,
|
|
||||||
reversed: selection.reversed,
|
|
||||||
start: selection.start.to_display_point(&display_map),
|
|
||||||
end: selection.end.to_display_point(&display_map),
|
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
));
|
));
|
||||||
@ -1243,7 +1264,7 @@ pub struct LayoutState {
|
|||||||
em_width: f32,
|
em_width: f32,
|
||||||
em_advance: f32,
|
em_advance: f32,
|
||||||
highlighted_ranges: Vec<(Range<DisplayPoint>, Color)>,
|
highlighted_ranges: Vec<(Range<DisplayPoint>, Color)>,
|
||||||
selections: Vec<(ReplicaId, Vec<text::Selection<DisplayPoint>>)>,
|
selections: Vec<(ReplicaId, Vec<SelectionLayout>)>,
|
||||||
context_menu: Option<(DisplayPoint, ElementBox)>,
|
context_menu: Option<(DisplayPoint, ElementBox)>,
|
||||||
code_actions_indicator: Option<(u32, ElementBox)>,
|
code_actions_indicator: Option<(u32, ElementBox)>,
|
||||||
}
|
}
|
||||||
|
@ -103,7 +103,11 @@ impl FollowableItem for Editor {
|
|||||||
} else {
|
} else {
|
||||||
self.buffer.update(cx, |buffer, cx| {
|
self.buffer.update(cx, |buffer, cx| {
|
||||||
if self.focused {
|
if self.focused {
|
||||||
buffer.set_active_selections(&self.selections.disjoint_anchors(), cx);
|
buffer.set_active_selections(
|
||||||
|
&self.selections.disjoint_anchors(),
|
||||||
|
self.selections.line_mode,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -509,6 +509,7 @@ impl MultiBuffer {
|
|||||||
pub fn set_active_selections(
|
pub fn set_active_selections(
|
||||||
&mut self,
|
&mut self,
|
||||||
selections: &[Selection<Anchor>],
|
selections: &[Selection<Anchor>],
|
||||||
|
line_mode: bool,
|
||||||
cx: &mut ModelContext<Self>,
|
cx: &mut ModelContext<Self>,
|
||||||
) {
|
) {
|
||||||
let mut selections_by_buffer: HashMap<usize, Vec<Selection<text::Anchor>>> =
|
let mut selections_by_buffer: HashMap<usize, Vec<Selection<text::Anchor>>> =
|
||||||
@ -573,7 +574,7 @@ impl MultiBuffer {
|
|||||||
}
|
}
|
||||||
Some(selection)
|
Some(selection)
|
||||||
}));
|
}));
|
||||||
buffer.set_active_selections(merged_selections, cx);
|
buffer.set_active_selections(merged_selections, line_mode, cx);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2397,7 +2398,7 @@ impl MultiBufferSnapshot {
|
|||||||
pub fn remote_selections_in_range<'a>(
|
pub fn remote_selections_in_range<'a>(
|
||||||
&'a self,
|
&'a self,
|
||||||
range: &'a Range<Anchor>,
|
range: &'a Range<Anchor>,
|
||||||
) -> impl 'a + Iterator<Item = (ReplicaId, Selection<Anchor>)> {
|
) -> impl 'a + Iterator<Item = (ReplicaId, bool, Selection<Anchor>)> {
|
||||||
let mut cursor = self.excerpts.cursor::<Option<&ExcerptId>>();
|
let mut cursor = self.excerpts.cursor::<Option<&ExcerptId>>();
|
||||||
cursor.seek(&Some(&range.start.excerpt_id), Bias::Left, &());
|
cursor.seek(&Some(&range.start.excerpt_id), Bias::Left, &());
|
||||||
cursor
|
cursor
|
||||||
@ -2414,7 +2415,7 @@ impl MultiBufferSnapshot {
|
|||||||
excerpt
|
excerpt
|
||||||
.buffer
|
.buffer
|
||||||
.remote_selections_in_range(query_range)
|
.remote_selections_in_range(query_range)
|
||||||
.flat_map(move |(replica_id, selections)| {
|
.flat_map(move |(replica_id, line_mode, selections)| {
|
||||||
selections.map(move |selection| {
|
selections.map(move |selection| {
|
||||||
let mut start = Anchor {
|
let mut start = Anchor {
|
||||||
buffer_id: Some(excerpt.buffer_id),
|
buffer_id: Some(excerpt.buffer_id),
|
||||||
@ -2435,6 +2436,7 @@ impl MultiBufferSnapshot {
|
|||||||
|
|
||||||
(
|
(
|
||||||
replica_id,
|
replica_id,
|
||||||
|
line_mode,
|
||||||
Selection {
|
Selection {
|
||||||
id: selection.id,
|
id: selection.id,
|
||||||
start,
|
start,
|
||||||
|
@ -27,6 +27,7 @@ pub struct SelectionsCollection {
|
|||||||
display_map: ModelHandle<DisplayMap>,
|
display_map: ModelHandle<DisplayMap>,
|
||||||
buffer: ModelHandle<MultiBuffer>,
|
buffer: ModelHandle<MultiBuffer>,
|
||||||
pub next_selection_id: usize,
|
pub next_selection_id: usize,
|
||||||
|
pub line_mode: bool,
|
||||||
disjoint: Arc<[Selection<Anchor>]>,
|
disjoint: Arc<[Selection<Anchor>]>,
|
||||||
pending: Option<PendingSelection>,
|
pending: Option<PendingSelection>,
|
||||||
}
|
}
|
||||||
@ -37,6 +38,7 @@ impl SelectionsCollection {
|
|||||||
display_map,
|
display_map,
|
||||||
buffer,
|
buffer,
|
||||||
next_selection_id: 1,
|
next_selection_id: 1,
|
||||||
|
line_mode: false,
|
||||||
disjoint: Arc::from([]),
|
disjoint: Arc::from([]),
|
||||||
pending: Some(PendingSelection {
|
pending: Some(PendingSelection {
|
||||||
selection: Selection {
|
selection: Selection {
|
||||||
@ -126,6 +128,20 @@ impl SelectionsCollection {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Returns all of the selections, adjusted to take into account the selection line_mode
|
||||||
|
pub fn all_adjusted(&self, cx: &mut MutableAppContext) -> Vec<Selection<Point>> {
|
||||||
|
let mut selections = self.all::<Point>(cx);
|
||||||
|
if self.line_mode {
|
||||||
|
let map = self.display_map(cx);
|
||||||
|
for selection in &mut selections {
|
||||||
|
let new_range = map.expand_to_line(selection.range());
|
||||||
|
selection.start = new_range.start;
|
||||||
|
selection.end = new_range.end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
selections
|
||||||
|
}
|
||||||
|
|
||||||
pub fn disjoint_in_range<'a, D>(
|
pub fn disjoint_in_range<'a, D>(
|
||||||
&self,
|
&self,
|
||||||
range: Range<Anchor>,
|
range: Range<Anchor>,
|
||||||
@ -273,9 +289,10 @@ impl SelectionsCollection {
|
|||||||
&mut self,
|
&mut self,
|
||||||
cx: &mut MutableAppContext,
|
cx: &mut MutableAppContext,
|
||||||
change: impl FnOnce(&mut MutableSelectionsCollection) -> R,
|
change: impl FnOnce(&mut MutableSelectionsCollection) -> R,
|
||||||
) -> R {
|
) -> (bool, R) {
|
||||||
let mut mutable_collection = MutableSelectionsCollection {
|
let mut mutable_collection = MutableSelectionsCollection {
|
||||||
collection: self,
|
collection: self,
|
||||||
|
selections_changed: false,
|
||||||
cx,
|
cx,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -284,12 +301,13 @@ impl SelectionsCollection {
|
|||||||
!mutable_collection.disjoint.is_empty() || mutable_collection.pending.is_some(),
|
!mutable_collection.disjoint.is_empty() || mutable_collection.pending.is_some(),
|
||||||
"There must be at least one selection"
|
"There must be at least one selection"
|
||||||
);
|
);
|
||||||
result
|
(mutable_collection.selections_changed, result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct MutableSelectionsCollection<'a> {
|
pub struct MutableSelectionsCollection<'a> {
|
||||||
collection: &'a mut SelectionsCollection,
|
collection: &'a mut SelectionsCollection,
|
||||||
|
selections_changed: bool,
|
||||||
cx: &'a mut MutableAppContext,
|
cx: &'a mut MutableAppContext,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -307,16 +325,26 @@ impl<'a> MutableSelectionsCollection<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete(&mut self, selection_id: usize) {
|
pub fn delete(&mut self, selection_id: usize) {
|
||||||
|
let mut changed = false;
|
||||||
self.collection.disjoint = self
|
self.collection.disjoint = self
|
||||||
.disjoint
|
.disjoint
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|selection| selection.id != selection_id)
|
.filter(|selection| {
|
||||||
|
let found = selection.id == selection_id;
|
||||||
|
changed |= found;
|
||||||
|
!found
|
||||||
|
})
|
||||||
.cloned()
|
.cloned()
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
self.selections_changed |= changed;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn clear_pending(&mut self) {
|
pub fn clear_pending(&mut self) {
|
||||||
self.collection.pending = None;
|
if self.collection.pending.is_some() {
|
||||||
|
self.collection.pending = None;
|
||||||
|
self.selections_changed = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_pending_range(&mut self, range: Range<Anchor>, mode: SelectMode) {
|
pub fn set_pending_range(&mut self, range: Range<Anchor>, mode: SelectMode) {
|
||||||
@ -329,11 +357,13 @@ impl<'a> MutableSelectionsCollection<'a> {
|
|||||||
goal: SelectionGoal::None,
|
goal: SelectionGoal::None,
|
||||||
},
|
},
|
||||||
mode,
|
mode,
|
||||||
})
|
});
|
||||||
|
self.selections_changed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_pending(&mut self, selection: Selection<Anchor>, mode: SelectMode) {
|
pub fn set_pending(&mut self, selection: Selection<Anchor>, mode: SelectMode) {
|
||||||
self.collection.pending = Some(PendingSelection { selection, mode });
|
self.collection.pending = Some(PendingSelection { selection, mode });
|
||||||
|
self.selections_changed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn try_cancel(&mut self) -> bool {
|
pub fn try_cancel(&mut self) -> bool {
|
||||||
@ -341,12 +371,14 @@ impl<'a> MutableSelectionsCollection<'a> {
|
|||||||
if self.disjoint.is_empty() {
|
if self.disjoint.is_empty() {
|
||||||
self.collection.disjoint = Arc::from([pending.selection]);
|
self.collection.disjoint = Arc::from([pending.selection]);
|
||||||
}
|
}
|
||||||
|
self.selections_changed = true;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut oldest = self.oldest_anchor().clone();
|
let mut oldest = self.oldest_anchor().clone();
|
||||||
if self.count() > 1 {
|
if self.count() > 1 {
|
||||||
self.collection.disjoint = Arc::from([oldest]);
|
self.collection.disjoint = Arc::from([oldest]);
|
||||||
|
self.selections_changed = true;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -355,27 +387,13 @@ impl<'a> MutableSelectionsCollection<'a> {
|
|||||||
oldest.start = head.clone();
|
oldest.start = head.clone();
|
||||||
oldest.end = head;
|
oldest.end = head;
|
||||||
self.collection.disjoint = Arc::from([oldest]);
|
self.collection.disjoint = Arc::from([oldest]);
|
||||||
|
self.selections_changed = true;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn reset_biases(&mut self) {
|
|
||||||
let buffer = self.buffer.read(self.cx).snapshot(self.cx);
|
|
||||||
self.collection.disjoint = self
|
|
||||||
.collection
|
|
||||||
.disjoint
|
|
||||||
.into_iter()
|
|
||||||
.cloned()
|
|
||||||
.map(|selection| reset_biases(selection, &buffer))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
if let Some(pending) = self.collection.pending.as_mut() {
|
|
||||||
pending.selection = reset_biases(pending.selection.clone(), &buffer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn insert_range<T>(&mut self, range: Range<T>)
|
pub fn insert_range<T>(&mut self, range: Range<T>)
|
||||||
where
|
where
|
||||||
T: 'a + ToOffset + ToPoint + TextDimension + Ord + Sub<T, Output = T> + std::marker::Copy,
|
T: 'a + ToOffset + ToPoint + TextDimension + Ord + Sub<T, Output = T> + std::marker::Copy,
|
||||||
@ -437,6 +455,7 @@ impl<'a> MutableSelectionsCollection<'a> {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
self.collection.pending = None;
|
self.collection.pending = None;
|
||||||
|
self.selections_changed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn select_anchors(&mut self, selections: Vec<Selection<Anchor>>) {
|
pub fn select_anchors(&mut self, selections: Vec<Selection<Anchor>>) {
|
||||||
@ -535,18 +554,27 @@ impl<'a> MutableSelectionsCollection<'a> {
|
|||||||
&mut self,
|
&mut self,
|
||||||
mut move_selection: impl FnMut(&DisplaySnapshot, &mut Selection<DisplayPoint>),
|
mut move_selection: impl FnMut(&DisplaySnapshot, &mut Selection<DisplayPoint>),
|
||||||
) {
|
) {
|
||||||
|
let mut changed = false;
|
||||||
let display_map = self.display_map();
|
let display_map = self.display_map();
|
||||||
let selections = self
|
let selections = self
|
||||||
.all::<Point>(self.cx)
|
.all::<Point>(self.cx)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|selection| {
|
.map(|selection| {
|
||||||
let mut selection = selection.map(|point| point.to_display_point(&display_map));
|
let mut moved_selection =
|
||||||
move_selection(&display_map, &mut selection);
|
selection.map(|point| point.to_display_point(&display_map));
|
||||||
selection.map(|display_point| display_point.to_point(&display_map))
|
move_selection(&display_map, &mut moved_selection);
|
||||||
|
let moved_selection =
|
||||||
|
moved_selection.map(|display_point| display_point.to_point(&display_map));
|
||||||
|
if selection != moved_selection {
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
moved_selection
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
self.select(selections)
|
if changed {
|
||||||
|
self.select(selections)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn move_heads_with(
|
pub fn move_heads_with(
|
||||||
@ -670,6 +698,7 @@ impl<'a> MutableSelectionsCollection<'a> {
|
|||||||
pending.selection.end = end;
|
pending.selection.end = end;
|
||||||
}
|
}
|
||||||
self.collection.pending = pending;
|
self.collection.pending = pending;
|
||||||
|
self.selections_changed = true;
|
||||||
|
|
||||||
selections_with_lost_position
|
selections_with_lost_position
|
||||||
}
|
}
|
||||||
@ -714,17 +743,3 @@ fn resolve<D: TextDimension + Ord + Sub<D, Output = D>>(
|
|||||||
) -> Selection<D> {
|
) -> Selection<D> {
|
||||||
selection.map(|p| p.summary::<D>(&buffer))
|
selection.map(|p| p.summary::<D>(&buffer))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn reset_biases(
|
|
||||||
mut selection: Selection<Anchor>,
|
|
||||||
buffer: &MultiBufferSnapshot,
|
|
||||||
) -> Selection<Anchor> {
|
|
||||||
let end_bias = if selection.end.to_offset(buffer) > selection.start.to_offset(buffer) {
|
|
||||||
Bias::Left
|
|
||||||
} else {
|
|
||||||
Bias::Right
|
|
||||||
};
|
|
||||||
selection.start = buffer.anchor_after(selection.start);
|
|
||||||
selection.end = buffer.anchor_at(selection.end, end_bias);
|
|
||||||
selection
|
|
||||||
}
|
|
||||||
|
@ -1,9 +1,19 @@
|
|||||||
use gpui::ViewContext;
|
use std::ops::{Deref, DerefMut, Range};
|
||||||
use util::test::{marked_text, marked_text_ranges};
|
|
||||||
|
use indoc::indoc;
|
||||||
|
|
||||||
|
use collections::BTreeMap;
|
||||||
|
use gpui::{keymap::Keystroke, ModelHandle, ViewContext, ViewHandle};
|
||||||
|
use language::Selection;
|
||||||
|
use settings::Settings;
|
||||||
|
use util::{
|
||||||
|
set_eq,
|
||||||
|
test::{marked_text, marked_text_ranges, marked_text_ranges_by, SetEqError},
|
||||||
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
display_map::{DisplayMap, DisplaySnapshot, ToDisplayPoint},
|
display_map::{DisplayMap, DisplaySnapshot, ToDisplayPoint},
|
||||||
DisplayPoint, Editor, MultiBuffer,
|
Autoscroll, DisplayPoint, Editor, EditorMode, MultiBuffer,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@ -56,3 +66,301 @@ pub fn assert_text_with_selections(
|
|||||||
assert_eq!(editor.text(cx), unmarked_text);
|
assert_eq!(editor.text(cx), unmarked_text);
|
||||||
assert_eq!(editor.selections.ranges(cx), text_ranges);
|
assert_eq!(editor.selections.ranges(cx), text_ranges);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn build_editor(
|
||||||
|
buffer: ModelHandle<MultiBuffer>,
|
||||||
|
cx: &mut ViewContext<Editor>,
|
||||||
|
) -> Editor {
|
||||||
|
Editor::new(EditorMode::Full, buffer, None, None, None, cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct EditorTestContext<'a> {
|
||||||
|
pub cx: &'a mut gpui::TestAppContext,
|
||||||
|
pub window_id: usize,
|
||||||
|
pub editor: ViewHandle<Editor>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> EditorTestContext<'a> {
|
||||||
|
pub async fn new(cx: &'a mut gpui::TestAppContext) -> EditorTestContext<'a> {
|
||||||
|
let (window_id, editor) = cx.update(|cx| {
|
||||||
|
cx.set_global(Settings::test(cx));
|
||||||
|
crate::init(cx);
|
||||||
|
|
||||||
|
let (window_id, editor) = cx.add_window(Default::default(), |cx| {
|
||||||
|
build_editor(MultiBuffer::build_simple("", cx), cx)
|
||||||
|
});
|
||||||
|
|
||||||
|
editor.update(cx, |_, cx| cx.focus_self());
|
||||||
|
|
||||||
|
(window_id, editor)
|
||||||
|
});
|
||||||
|
|
||||||
|
Self {
|
||||||
|
cx,
|
||||||
|
window_id,
|
||||||
|
editor,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_editor<F, T>(&mut self, update: F) -> T
|
||||||
|
where
|
||||||
|
F: FnOnce(&mut Editor, &mut ViewContext<Editor>) -> T,
|
||||||
|
{
|
||||||
|
self.editor.update(self.cx, update)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn editor_text(&mut self) -> String {
|
||||||
|
self.editor
|
||||||
|
.update(self.cx, |editor, cx| editor.snapshot(cx).text())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn simulate_keystroke(&mut self, keystroke_text: &str) {
|
||||||
|
let keystroke = Keystroke::parse(keystroke_text).unwrap();
|
||||||
|
let input = if keystroke.modified() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(keystroke.key.clone())
|
||||||
|
};
|
||||||
|
self.cx
|
||||||
|
.dispatch_keystroke(self.window_id, keystroke, input, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn simulate_keystrokes<const COUNT: usize>(&mut self, keystroke_texts: [&str; COUNT]) {
|
||||||
|
for keystroke_text in keystroke_texts.into_iter() {
|
||||||
|
self.simulate_keystroke(keystroke_text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sets the editor state via a marked string.
|
||||||
|
// `|` characters represent empty selections
|
||||||
|
// `[` to `}` represents a non empty selection with the head at `}`
|
||||||
|
// `{` to `]` represents a non empty selection with the head at `{`
|
||||||
|
pub fn set_state(&mut self, text: &str) {
|
||||||
|
self.editor.update(self.cx, |editor, cx| {
|
||||||
|
let (unmarked_text, mut selection_ranges) = marked_text_ranges_by(
|
||||||
|
&text,
|
||||||
|
vec!['|'.into(), ('[', '}').into(), ('{', ']').into()],
|
||||||
|
);
|
||||||
|
editor.set_text(unmarked_text, cx);
|
||||||
|
|
||||||
|
let mut selections: Vec<Range<usize>> =
|
||||||
|
selection_ranges.remove(&'|'.into()).unwrap_or_default();
|
||||||
|
selections.extend(
|
||||||
|
selection_ranges
|
||||||
|
.remove(&('{', ']').into())
|
||||||
|
.unwrap_or_default()
|
||||||
|
.into_iter()
|
||||||
|
.map(|range| range.end..range.start),
|
||||||
|
);
|
||||||
|
selections.extend(
|
||||||
|
selection_ranges
|
||||||
|
.remove(&('[', '}').into())
|
||||||
|
.unwrap_or_default(),
|
||||||
|
);
|
||||||
|
|
||||||
|
editor.change_selections(Some(Autoscroll::Fit), cx, |s| s.select_ranges(selections));
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Asserts the editor state via a marked string.
|
||||||
|
// `|` characters represent empty selections
|
||||||
|
// `[` to `}` represents a non empty selection with the head at `}`
|
||||||
|
// `{` to `]` represents a non empty selection with the head at `{`
|
||||||
|
pub fn assert_editor_state(&mut self, text: &str) {
|
||||||
|
let (unmarked_text, mut selection_ranges) = marked_text_ranges_by(
|
||||||
|
&text,
|
||||||
|
vec!['|'.into(), ('[', '}').into(), ('{', ']').into()],
|
||||||
|
);
|
||||||
|
let editor_text = self.editor_text();
|
||||||
|
assert_eq!(
|
||||||
|
editor_text, unmarked_text,
|
||||||
|
"Unmarked text doesn't match editor text"
|
||||||
|
);
|
||||||
|
|
||||||
|
let expected_empty_selections = selection_ranges.remove(&'|'.into()).unwrap_or_default();
|
||||||
|
let expected_reverse_selections = selection_ranges
|
||||||
|
.remove(&('{', ']').into())
|
||||||
|
.unwrap_or_default();
|
||||||
|
let expected_forward_selections = selection_ranges
|
||||||
|
.remove(&('[', '}').into())
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
self.assert_selections(
|
||||||
|
expected_empty_selections,
|
||||||
|
expected_reverse_selections,
|
||||||
|
expected_forward_selections,
|
||||||
|
Some(text.to_string()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn assert_editor_selections(&mut self, expected_selections: Vec<Selection<usize>>) {
|
||||||
|
let mut empty_selections = Vec::new();
|
||||||
|
let mut reverse_selections = Vec::new();
|
||||||
|
let mut forward_selections = Vec::new();
|
||||||
|
|
||||||
|
for selection in expected_selections {
|
||||||
|
let range = selection.range();
|
||||||
|
if selection.is_empty() {
|
||||||
|
empty_selections.push(range);
|
||||||
|
} else if selection.reversed {
|
||||||
|
reverse_selections.push(range);
|
||||||
|
} else {
|
||||||
|
forward_selections.push(range)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.assert_selections(
|
||||||
|
empty_selections,
|
||||||
|
reverse_selections,
|
||||||
|
forward_selections,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn assert_selections(
|
||||||
|
&mut self,
|
||||||
|
expected_empty_selections: Vec<Range<usize>>,
|
||||||
|
expected_reverse_selections: Vec<Range<usize>>,
|
||||||
|
expected_forward_selections: Vec<Range<usize>>,
|
||||||
|
asserted_text: Option<String>,
|
||||||
|
) {
|
||||||
|
let (empty_selections, reverse_selections, forward_selections) =
|
||||||
|
self.editor.read_with(self.cx, |editor, cx| {
|
||||||
|
let mut empty_selections = Vec::new();
|
||||||
|
let mut reverse_selections = Vec::new();
|
||||||
|
let mut forward_selections = Vec::new();
|
||||||
|
|
||||||
|
for selection in editor.selections.all::<usize>(cx) {
|
||||||
|
let range = selection.range();
|
||||||
|
if selection.is_empty() {
|
||||||
|
empty_selections.push(range);
|
||||||
|
} else if selection.reversed {
|
||||||
|
reverse_selections.push(range);
|
||||||
|
} else {
|
||||||
|
forward_selections.push(range)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(empty_selections, reverse_selections, forward_selections)
|
||||||
|
});
|
||||||
|
|
||||||
|
let asserted_selections = asserted_text.unwrap_or_else(|| {
|
||||||
|
self.insert_markers(
|
||||||
|
&expected_empty_selections,
|
||||||
|
&expected_reverse_selections,
|
||||||
|
&expected_forward_selections,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
let actual_selections =
|
||||||
|
self.insert_markers(&empty_selections, &reverse_selections, &forward_selections);
|
||||||
|
|
||||||
|
let unmarked_text = self.editor_text();
|
||||||
|
let all_eq: Result<(), SetEqError<String>> =
|
||||||
|
set_eq!(expected_empty_selections, empty_selections)
|
||||||
|
.map_err(|err| {
|
||||||
|
err.map(|missing| {
|
||||||
|
let mut error_text = unmarked_text.clone();
|
||||||
|
error_text.insert(missing.start, '|');
|
||||||
|
error_text
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.and_then(|_| {
|
||||||
|
set_eq!(expected_reverse_selections, reverse_selections).map_err(|err| {
|
||||||
|
err.map(|missing| {
|
||||||
|
let mut error_text = unmarked_text.clone();
|
||||||
|
error_text.insert(missing.start, '{');
|
||||||
|
error_text.insert(missing.end, ']');
|
||||||
|
error_text
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.and_then(|_| {
|
||||||
|
set_eq!(expected_forward_selections, forward_selections).map_err(|err| {
|
||||||
|
err.map(|missing| {
|
||||||
|
let mut error_text = unmarked_text.clone();
|
||||||
|
error_text.insert(missing.start, '[');
|
||||||
|
error_text.insert(missing.end, '}');
|
||||||
|
error_text
|
||||||
|
})
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
match all_eq {
|
||||||
|
Err(SetEqError::LeftMissing(location_text)) => {
|
||||||
|
panic!(
|
||||||
|
indoc! {"
|
||||||
|
Editor has extra selection
|
||||||
|
Extra Selection Location:
|
||||||
|
{}
|
||||||
|
Asserted selections:
|
||||||
|
{}
|
||||||
|
Actual selections:
|
||||||
|
{}"},
|
||||||
|
location_text, asserted_selections, actual_selections,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(SetEqError::RightMissing(location_text)) => {
|
||||||
|
panic!(
|
||||||
|
indoc! {"
|
||||||
|
Editor is missing empty selection
|
||||||
|
Missing Selection Location:
|
||||||
|
{}
|
||||||
|
Asserted selections:
|
||||||
|
{}
|
||||||
|
Actual selections:
|
||||||
|
{}"},
|
||||||
|
location_text, asserted_selections, actual_selections,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn insert_markers(
|
||||||
|
&mut self,
|
||||||
|
empty_selections: &Vec<Range<usize>>,
|
||||||
|
reverse_selections: &Vec<Range<usize>>,
|
||||||
|
forward_selections: &Vec<Range<usize>>,
|
||||||
|
) -> String {
|
||||||
|
let mut editor_text_with_selections = self.editor_text();
|
||||||
|
let mut selection_marks = BTreeMap::new();
|
||||||
|
for range in empty_selections {
|
||||||
|
selection_marks.insert(&range.start, '|');
|
||||||
|
}
|
||||||
|
for range in reverse_selections {
|
||||||
|
selection_marks.insert(&range.start, '{');
|
||||||
|
selection_marks.insert(&range.end, ']');
|
||||||
|
}
|
||||||
|
for range in forward_selections {
|
||||||
|
selection_marks.insert(&range.start, '[');
|
||||||
|
selection_marks.insert(&range.end, '}');
|
||||||
|
}
|
||||||
|
for (offset, mark) in selection_marks.into_iter().rev() {
|
||||||
|
editor_text_with_selections.insert(*offset, mark);
|
||||||
|
}
|
||||||
|
|
||||||
|
editor_text_with_selections
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn assert_clipboard_content(&mut self, expected_content: Option<&str>) {
|
||||||
|
self.cx.update(|cx| {
|
||||||
|
let actual_content = cx.read_from_clipboard().map(|item| item.text().to_owned());
|
||||||
|
let expected_content = expected_content.map(|content| content.to_owned());
|
||||||
|
assert_eq!(actual_content, expected_content);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Deref for EditorTestContext<'a> {
|
||||||
|
type Target = gpui::TestAppContext;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
self.cx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> DerefMut for EditorTestContext<'a> {
|
||||||
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
|
&mut self.cx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -544,12 +544,23 @@ impl TestAppContext {
|
|||||||
!prompts.is_empty()
|
!prompts.is_empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
pub fn current_window_title(&self, window_id: usize) -> Option<String> {
|
||||||
|
let mut state = self.cx.borrow_mut();
|
||||||
|
let (_, window) = state
|
||||||
|
.presenters_and_platform_windows
|
||||||
|
.get_mut(&window_id)
|
||||||
|
.unwrap();
|
||||||
|
let test_window = window
|
||||||
|
.as_any_mut()
|
||||||
|
.downcast_mut::<platform::test::Window>()
|
||||||
|
.unwrap();
|
||||||
|
test_window.title.clone()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn leak_detector(&self) -> Arc<Mutex<LeakDetector>> {
|
pub fn leak_detector(&self) -> Arc<Mutex<LeakDetector>> {
|
||||||
self.cx.borrow().leak_detector()
|
self.cx.borrow().leak_detector()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
|
||||||
pub fn assert_dropped(&self, handle: impl WeakHandle) {
|
pub fn assert_dropped(&self, handle: impl WeakHandle) {
|
||||||
self.cx
|
self.cx
|
||||||
.borrow()
|
.borrow()
|
||||||
@ -757,7 +768,7 @@ type SubscriptionCallback = Box<dyn FnMut(&dyn Any, &mut MutableAppContext) -> b
|
|||||||
type GlobalSubscriptionCallback = Box<dyn FnMut(&dyn Any, &mut MutableAppContext)>;
|
type GlobalSubscriptionCallback = Box<dyn FnMut(&dyn Any, &mut MutableAppContext)>;
|
||||||
type ObservationCallback = Box<dyn FnMut(&mut MutableAppContext) -> bool>;
|
type ObservationCallback = Box<dyn FnMut(&mut MutableAppContext) -> bool>;
|
||||||
type FocusObservationCallback = Box<dyn FnMut(&mut MutableAppContext) -> bool>;
|
type FocusObservationCallback = Box<dyn FnMut(&mut MutableAppContext) -> bool>;
|
||||||
type GlobalObservationCallback = Box<dyn FnMut(&dyn Any, &mut MutableAppContext)>;
|
type GlobalObservationCallback = Box<dyn FnMut(&mut MutableAppContext)>;
|
||||||
type ReleaseObservationCallback = Box<dyn FnOnce(&dyn Any, &mut MutableAppContext)>;
|
type ReleaseObservationCallback = Box<dyn FnOnce(&dyn Any, &mut MutableAppContext)>;
|
||||||
type ActionObservationCallback = Box<dyn FnMut(TypeId, &mut MutableAppContext)>;
|
type ActionObservationCallback = Box<dyn FnMut(TypeId, &mut MutableAppContext)>;
|
||||||
type DeserializeActionCallback = fn(json: &str) -> anyhow::Result<Box<dyn Action>>;
|
type DeserializeActionCallback = fn(json: &str) -> anyhow::Result<Box<dyn Action>>;
|
||||||
@ -1272,7 +1283,7 @@ impl MutableAppContext {
|
|||||||
pub fn observe_global<G, F>(&mut self, mut observe: F) -> Subscription
|
pub fn observe_global<G, F>(&mut self, mut observe: F) -> Subscription
|
||||||
where
|
where
|
||||||
G: Any,
|
G: Any,
|
||||||
F: 'static + FnMut(&G, &mut MutableAppContext),
|
F: 'static + FnMut(&mut MutableAppContext),
|
||||||
{
|
{
|
||||||
let type_id = TypeId::of::<G>();
|
let type_id = TypeId::of::<G>();
|
||||||
let id = post_inc(&mut self.next_subscription_id);
|
let id = post_inc(&mut self.next_subscription_id);
|
||||||
@ -1283,11 +1294,8 @@ impl MutableAppContext {
|
|||||||
.or_default()
|
.or_default()
|
||||||
.insert(
|
.insert(
|
||||||
id,
|
id,
|
||||||
Some(
|
Some(Box::new(move |cx: &mut MutableAppContext| observe(cx))
|
||||||
Box::new(move |global: &dyn Any, cx: &mut MutableAppContext| {
|
as GlobalObservationCallback),
|
||||||
observe(global.downcast_ref().unwrap(), cx)
|
|
||||||
}) as GlobalObservationCallback,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Subscription::GlobalObservation {
|
Subscription::GlobalObservation {
|
||||||
@ -2304,27 +2312,24 @@ impl MutableAppContext {
|
|||||||
fn handle_global_notification_effect(&mut self, observed_type_id: TypeId) {
|
fn handle_global_notification_effect(&mut self, observed_type_id: TypeId) {
|
||||||
let callbacks = self.global_observations.lock().remove(&observed_type_id);
|
let callbacks = self.global_observations.lock().remove(&observed_type_id);
|
||||||
if let Some(callbacks) = callbacks {
|
if let Some(callbacks) = callbacks {
|
||||||
if let Some(global) = self.cx.globals.remove(&observed_type_id) {
|
for (id, callback) in callbacks {
|
||||||
for (id, callback) in callbacks {
|
if let Some(mut callback) = callback {
|
||||||
if let Some(mut callback) = callback {
|
callback(self);
|
||||||
callback(global.as_ref(), self);
|
match self
|
||||||
match self
|
.global_observations
|
||||||
.global_observations
|
.lock()
|
||||||
.lock()
|
.entry(observed_type_id)
|
||||||
.entry(observed_type_id)
|
.or_default()
|
||||||
.or_default()
|
.entry(id)
|
||||||
.entry(id)
|
{
|
||||||
{
|
collections::btree_map::Entry::Vacant(entry) => {
|
||||||
collections::btree_map::Entry::Vacant(entry) => {
|
entry.insert(Some(callback));
|
||||||
entry.insert(Some(callback));
|
}
|
||||||
}
|
collections::btree_map::Entry::Occupied(entry) => {
|
||||||
collections::btree_map::Entry::Occupied(entry) => {
|
entry.remove();
|
||||||
entry.remove();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.cx.globals.insert(observed_type_id, global);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -3303,6 +3308,13 @@ impl<'a, T: View> ViewContext<'a, T> {
|
|||||||
self.app.focus(self.window_id, None);
|
self.app.focus(self.window_id, None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_window_title(&mut self, title: &str) {
|
||||||
|
let window_id = self.window_id();
|
||||||
|
if let Some((_, window)) = self.presenters_and_platform_windows.get_mut(&window_id) {
|
||||||
|
window.set_title(title);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn add_model<S, F>(&mut self, build_model: F) -> ModelHandle<S>
|
pub fn add_model<S, F>(&mut self, build_model: F) -> ModelHandle<S>
|
||||||
where
|
where
|
||||||
S: Entity,
|
S: Entity,
|
||||||
@ -5723,7 +5735,7 @@ mod tests {
|
|||||||
let observation_count = Rc::new(RefCell::new(0));
|
let observation_count = Rc::new(RefCell::new(0));
|
||||||
let subscription = cx.observe_global::<Global, _>({
|
let subscription = cx.observe_global::<Global, _>({
|
||||||
let observation_count = observation_count.clone();
|
let observation_count = observation_count.clone();
|
||||||
move |_, _| {
|
move |_| {
|
||||||
*observation_count.borrow_mut() += 1;
|
*observation_count.borrow_mut() += 1;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -5753,7 +5765,7 @@ mod tests {
|
|||||||
let observation_count = Rc::new(RefCell::new(0));
|
let observation_count = Rc::new(RefCell::new(0));
|
||||||
cx.observe_global::<OtherGlobal, _>({
|
cx.observe_global::<OtherGlobal, _>({
|
||||||
let observation_count = observation_count.clone();
|
let observation_count = observation_count.clone();
|
||||||
move |_, _| {
|
move |_| {
|
||||||
*observation_count.borrow_mut() += 1;
|
*observation_count.borrow_mut() += 1;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -6127,7 +6139,7 @@ mod tests {
|
|||||||
*subscription.borrow_mut() = Some(cx.observe_global::<(), _>({
|
*subscription.borrow_mut() = Some(cx.observe_global::<(), _>({
|
||||||
let observation_count = observation_count.clone();
|
let observation_count = observation_count.clone();
|
||||||
let subscription = subscription.clone();
|
let subscription = subscription.clone();
|
||||||
move |_, _| {
|
move |_| {
|
||||||
subscription.borrow_mut().take();
|
subscription.borrow_mut().take();
|
||||||
*observation_count.borrow_mut() += 1;
|
*observation_count.borrow_mut() += 1;
|
||||||
}
|
}
|
||||||
|
@ -96,6 +96,7 @@ pub trait Window: WindowContext {
|
|||||||
fn on_close(&mut self, callback: Box<dyn FnOnce()>);
|
fn on_close(&mut self, callback: Box<dyn FnOnce()>);
|
||||||
fn prompt(&self, level: PromptLevel, msg: &str, answers: &[&str]) -> oneshot::Receiver<usize>;
|
fn prompt(&self, level: PromptLevel, msg: &str, answers: &[&str]) -> oneshot::Receiver<usize>;
|
||||||
fn activate(&self);
|
fn activate(&self);
|
||||||
|
fn set_title(&mut self, title: &str);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait WindowContext {
|
pub trait WindowContext {
|
||||||
|
@ -202,6 +202,11 @@ impl MacForegroundPlatform {
|
|||||||
|
|
||||||
menu_bar_item.setSubmenu_(menu);
|
menu_bar_item.setSubmenu_(menu);
|
||||||
menu_bar.addItem_(menu_bar_item);
|
menu_bar.addItem_(menu_bar_item);
|
||||||
|
|
||||||
|
if menu_name == "Window" {
|
||||||
|
let app: id = msg_send![APP_CLASS, sharedApplication];
|
||||||
|
app.setWindowsMenu_(menu);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
menu_bar
|
menu_bar
|
||||||
|
@ -386,8 +386,15 @@ impl platform::Window for Window {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn activate(&self) {
|
fn activate(&self) {
|
||||||
|
unsafe { msg_send![self.0.borrow().native_window, makeKeyAndOrderFront: nil] }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_title(&mut self, title: &str) {
|
||||||
unsafe {
|
unsafe {
|
||||||
let _: () = msg_send![self.0.borrow().native_window, makeKeyAndOrderFront: nil];
|
let app = NSApplication::sharedApplication(nil);
|
||||||
|
let window = self.0.borrow().native_window;
|
||||||
|
let title = ns_string(title);
|
||||||
|
msg_send![app, changeWindowsItem:window title:title filename:false]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -37,6 +37,7 @@ pub struct Window {
|
|||||||
event_handlers: Vec<Box<dyn FnMut(super::Event)>>,
|
event_handlers: Vec<Box<dyn FnMut(super::Event)>>,
|
||||||
resize_handlers: Vec<Box<dyn FnMut()>>,
|
resize_handlers: Vec<Box<dyn FnMut()>>,
|
||||||
close_handlers: Vec<Box<dyn FnOnce()>>,
|
close_handlers: Vec<Box<dyn FnOnce()>>,
|
||||||
|
pub(crate) title: Option<String>,
|
||||||
pub(crate) pending_prompts: RefCell<VecDeque<oneshot::Sender<usize>>>,
|
pub(crate) pending_prompts: RefCell<VecDeque<oneshot::Sender<usize>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -189,9 +190,14 @@ impl Window {
|
|||||||
close_handlers: Vec::new(),
|
close_handlers: Vec::new(),
|
||||||
scale_factor: 1.0,
|
scale_factor: 1.0,
|
||||||
current_scene: None,
|
current_scene: None,
|
||||||
|
title: None,
|
||||||
pending_prompts: Default::default(),
|
pending_prompts: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn title(&self) -> Option<String> {
|
||||||
|
self.title.clone()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl super::Dispatcher for Dispatcher {
|
impl super::Dispatcher for Dispatcher {
|
||||||
@ -248,6 +254,10 @@ impl super::Window for Window {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn activate(&self) {}
|
fn activate(&self) {}
|
||||||
|
|
||||||
|
fn set_title(&mut self, title: &str) {
|
||||||
|
self.title = Some(title.to_string())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn platform() -> Platform {
|
pub fn platform() -> Platform {
|
||||||
|
@ -83,6 +83,7 @@ pub struct BufferSnapshot {
|
|||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
struct SelectionSet {
|
struct SelectionSet {
|
||||||
|
line_mode: bool,
|
||||||
selections: Arc<[Selection<Anchor>]>,
|
selections: Arc<[Selection<Anchor>]>,
|
||||||
lamport_timestamp: clock::Lamport,
|
lamport_timestamp: clock::Lamport,
|
||||||
}
|
}
|
||||||
@ -129,6 +130,7 @@ pub enum Operation {
|
|||||||
UpdateSelections {
|
UpdateSelections {
|
||||||
selections: Arc<[Selection<Anchor>]>,
|
selections: Arc<[Selection<Anchor>]>,
|
||||||
lamport_timestamp: clock::Lamport,
|
lamport_timestamp: clock::Lamport,
|
||||||
|
line_mode: bool,
|
||||||
},
|
},
|
||||||
UpdateCompletionTriggers {
|
UpdateCompletionTriggers {
|
||||||
triggers: Vec<String>,
|
triggers: Vec<String>,
|
||||||
@ -343,6 +345,7 @@ impl Buffer {
|
|||||||
this.remote_selections.insert(
|
this.remote_selections.insert(
|
||||||
selection_set.replica_id as ReplicaId,
|
selection_set.replica_id as ReplicaId,
|
||||||
SelectionSet {
|
SelectionSet {
|
||||||
|
line_mode: selection_set.line_mode,
|
||||||
selections: proto::deserialize_selections(selection_set.selections),
|
selections: proto::deserialize_selections(selection_set.selections),
|
||||||
lamport_timestamp,
|
lamport_timestamp,
|
||||||
},
|
},
|
||||||
@ -385,6 +388,7 @@ impl Buffer {
|
|||||||
replica_id: *replica_id as u32,
|
replica_id: *replica_id as u32,
|
||||||
selections: proto::serialize_selections(&set.selections),
|
selections: proto::serialize_selections(&set.selections),
|
||||||
lamport_timestamp: set.lamport_timestamp.value,
|
lamport_timestamp: set.lamport_timestamp.value,
|
||||||
|
line_mode: set.line_mode,
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
diagnostics: proto::serialize_diagnostics(self.diagnostics.iter()),
|
diagnostics: proto::serialize_diagnostics(self.diagnostics.iter()),
|
||||||
@ -1030,6 +1034,7 @@ impl Buffer {
|
|||||||
pub fn set_active_selections(
|
pub fn set_active_selections(
|
||||||
&mut self,
|
&mut self,
|
||||||
selections: Arc<[Selection<Anchor>]>,
|
selections: Arc<[Selection<Anchor>]>,
|
||||||
|
line_mode: bool,
|
||||||
cx: &mut ModelContext<Self>,
|
cx: &mut ModelContext<Self>,
|
||||||
) {
|
) {
|
||||||
let lamport_timestamp = self.text.lamport_clock.tick();
|
let lamport_timestamp = self.text.lamport_clock.tick();
|
||||||
@ -1038,11 +1043,13 @@ impl Buffer {
|
|||||||
SelectionSet {
|
SelectionSet {
|
||||||
selections: selections.clone(),
|
selections: selections.clone(),
|
||||||
lamport_timestamp,
|
lamport_timestamp,
|
||||||
|
line_mode,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
self.send_operation(
|
self.send_operation(
|
||||||
Operation::UpdateSelections {
|
Operation::UpdateSelections {
|
||||||
selections,
|
selections,
|
||||||
|
line_mode,
|
||||||
lamport_timestamp,
|
lamport_timestamp,
|
||||||
},
|
},
|
||||||
cx,
|
cx,
|
||||||
@ -1050,7 +1057,7 @@ impl Buffer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn remove_active_selections(&mut self, cx: &mut ModelContext<Self>) {
|
pub fn remove_active_selections(&mut self, cx: &mut ModelContext<Self>) {
|
||||||
self.set_active_selections(Arc::from([]), cx);
|
self.set_active_selections(Arc::from([]), false, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_text<T>(&mut self, text: T, cx: &mut ModelContext<Self>) -> Option<clock::Local>
|
pub fn set_text<T>(&mut self, text: T, cx: &mut ModelContext<Self>) -> Option<clock::Local>
|
||||||
@ -1287,6 +1294,7 @@ impl Buffer {
|
|||||||
Operation::UpdateSelections {
|
Operation::UpdateSelections {
|
||||||
selections,
|
selections,
|
||||||
lamport_timestamp,
|
lamport_timestamp,
|
||||||
|
line_mode,
|
||||||
} => {
|
} => {
|
||||||
if let Some(set) = self.remote_selections.get(&lamport_timestamp.replica_id) {
|
if let Some(set) = self.remote_selections.get(&lamport_timestamp.replica_id) {
|
||||||
if set.lamport_timestamp > lamport_timestamp {
|
if set.lamport_timestamp > lamport_timestamp {
|
||||||
@ -1299,6 +1307,7 @@ impl Buffer {
|
|||||||
SelectionSet {
|
SelectionSet {
|
||||||
selections,
|
selections,
|
||||||
lamport_timestamp,
|
lamport_timestamp,
|
||||||
|
line_mode,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
self.text.lamport_clock.observe(lamport_timestamp);
|
self.text.lamport_clock.observe(lamport_timestamp);
|
||||||
@ -1890,8 +1899,14 @@ impl BufferSnapshot {
|
|||||||
pub fn remote_selections_in_range<'a>(
|
pub fn remote_selections_in_range<'a>(
|
||||||
&'a self,
|
&'a self,
|
||||||
range: Range<Anchor>,
|
range: Range<Anchor>,
|
||||||
) -> impl 'a + Iterator<Item = (ReplicaId, impl 'a + Iterator<Item = &'a Selection<Anchor>>)>
|
) -> impl 'a
|
||||||
{
|
+ Iterator<
|
||||||
|
Item = (
|
||||||
|
ReplicaId,
|
||||||
|
bool,
|
||||||
|
impl 'a + Iterator<Item = &'a Selection<Anchor>>,
|
||||||
|
),
|
||||||
|
> {
|
||||||
self.remote_selections
|
self.remote_selections
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|(replica_id, set)| {
|
.filter(|(replica_id, set)| {
|
||||||
@ -1909,7 +1924,11 @@ impl BufferSnapshot {
|
|||||||
Ok(ix) | Err(ix) => ix,
|
Ok(ix) | Err(ix) => ix,
|
||||||
};
|
};
|
||||||
|
|
||||||
(*replica_id, set.selections[start_ix..end_ix].iter())
|
(
|
||||||
|
*replica_id,
|
||||||
|
set.line_mode,
|
||||||
|
set.selections[start_ix..end_ix].iter(),
|
||||||
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -43,11 +43,13 @@ pub fn serialize_operation(operation: &Operation) -> proto::Operation {
|
|||||||
}),
|
}),
|
||||||
Operation::UpdateSelections {
|
Operation::UpdateSelections {
|
||||||
selections,
|
selections,
|
||||||
|
line_mode,
|
||||||
lamport_timestamp,
|
lamport_timestamp,
|
||||||
} => proto::operation::Variant::UpdateSelections(proto::operation::UpdateSelections {
|
} => proto::operation::Variant::UpdateSelections(proto::operation::UpdateSelections {
|
||||||
replica_id: lamport_timestamp.replica_id as u32,
|
replica_id: lamport_timestamp.replica_id as u32,
|
||||||
lamport_timestamp: lamport_timestamp.value,
|
lamport_timestamp: lamport_timestamp.value,
|
||||||
selections: serialize_selections(selections),
|
selections: serialize_selections(selections),
|
||||||
|
line_mode: *line_mode,
|
||||||
}),
|
}),
|
||||||
Operation::UpdateDiagnostics {
|
Operation::UpdateDiagnostics {
|
||||||
diagnostics,
|
diagnostics,
|
||||||
@ -217,6 +219,7 @@ pub fn deserialize_operation(message: proto::Operation) -> Result<Operation> {
|
|||||||
value: message.lamport_timestamp,
|
value: message.lamport_timestamp,
|
||||||
},
|
},
|
||||||
selections: Arc::from(selections),
|
selections: Arc::from(selections),
|
||||||
|
line_mode: message.line_mode,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
proto::operation::Variant::UpdateDiagnostics(message) => Operation::UpdateDiagnostics {
|
proto::operation::Variant::UpdateDiagnostics(message) => Operation::UpdateDiagnostics {
|
||||||
|
@ -828,7 +828,7 @@ fn test_random_collaboration(cx: &mut MutableAppContext, mut rng: StdRng) {
|
|||||||
selections
|
selections
|
||||||
);
|
);
|
||||||
active_selections.insert(replica_id, selections.clone());
|
active_selections.insert(replica_id, selections.clone());
|
||||||
buffer.set_active_selections(selections, cx);
|
buffer.set_active_selections(selections, false, cx);
|
||||||
});
|
});
|
||||||
mutation_count -= 1;
|
mutation_count -= 1;
|
||||||
}
|
}
|
||||||
@ -984,7 +984,7 @@ fn test_random_collaboration(cx: &mut MutableAppContext, mut rng: StdRng) {
|
|||||||
let buffer = buffer.read(cx).snapshot();
|
let buffer = buffer.read(cx).snapshot();
|
||||||
let actual_remote_selections = buffer
|
let actual_remote_selections = buffer
|
||||||
.remote_selections_in_range(Anchor::MIN..Anchor::MAX)
|
.remote_selections_in_range(Anchor::MIN..Anchor::MAX)
|
||||||
.map(|(replica_id, selections)| (replica_id, selections.collect::<Vec<_>>()))
|
.map(|(replica_id, _, selections)| (replica_id, selections.collect::<Vec<_>>()))
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
let expected_remote_selections = active_selections
|
let expected_remote_selections = active_selections
|
||||||
.iter()
|
.iter()
|
||||||
|
@ -139,6 +139,7 @@ pub struct Collaborator {
|
|||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
pub enum Event {
|
pub enum Event {
|
||||||
ActiveEntryChanged(Option<ProjectEntryId>),
|
ActiveEntryChanged(Option<ProjectEntryId>),
|
||||||
|
WorktreeAdded,
|
||||||
WorktreeRemoved(WorktreeId),
|
WorktreeRemoved(WorktreeId),
|
||||||
DiskBasedDiagnosticsStarted,
|
DiskBasedDiagnosticsStarted,
|
||||||
DiskBasedDiagnosticsUpdated,
|
DiskBasedDiagnosticsUpdated,
|
||||||
@ -3655,11 +3656,19 @@ impl Project {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remove_worktree(&mut self, id: WorktreeId, cx: &mut ModelContext<Self>) {
|
pub fn remove_worktree(&mut self, id_to_remove: WorktreeId, cx: &mut ModelContext<Self>) {
|
||||||
self.worktrees.retain(|worktree| {
|
self.worktrees.retain(|worktree| {
|
||||||
worktree
|
if let Some(worktree) = worktree.upgrade(cx) {
|
||||||
.upgrade(cx)
|
let id = worktree.read(cx).id();
|
||||||
.map_or(false, |w| w.read(cx).id() != id)
|
if id == id_to_remove {
|
||||||
|
cx.emit(Event::WorktreeRemoved(id));
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
});
|
});
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
@ -3690,6 +3699,7 @@ impl Project {
|
|||||||
self.worktrees
|
self.worktrees
|
||||||
.push(WorktreeHandle::Weak(worktree.downgrade()));
|
.push(WorktreeHandle::Weak(worktree.downgrade()));
|
||||||
}
|
}
|
||||||
|
cx.emit(Event::WorktreeAdded);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -787,6 +787,7 @@ message SelectionSet {
|
|||||||
uint32 replica_id = 1;
|
uint32 replica_id = 1;
|
||||||
repeated Selection selections = 2;
|
repeated Selection selections = 2;
|
||||||
uint32 lamport_timestamp = 3;
|
uint32 lamport_timestamp = 3;
|
||||||
|
bool line_mode = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
message Selection {
|
message Selection {
|
||||||
@ -862,6 +863,7 @@ message Operation {
|
|||||||
uint32 replica_id = 1;
|
uint32 replica_id = 1;
|
||||||
uint32 lamport_timestamp = 2;
|
uint32 lamport_timestamp = 2;
|
||||||
repeated Selection selections = 3;
|
repeated Selection selections = 3;
|
||||||
|
bool line_mode = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
message UpdateCompletionTriggers {
|
message UpdateCompletionTriggers {
|
||||||
|
@ -6,4 +6,4 @@ pub use conn::Connection;
|
|||||||
pub use peer::*;
|
pub use peer::*;
|
||||||
mod macros;
|
mod macros;
|
||||||
|
|
||||||
pub const PROTOCOL_VERSION: u32 = 19;
|
pub const PROTOCOL_VERSION: u32 = 20;
|
||||||
|
@ -21,6 +21,7 @@ pub use keymap_file::{keymap_file_json_schema, KeymapFileContent};
|
|||||||
pub struct Settings {
|
pub struct Settings {
|
||||||
pub buffer_font_family: FamilyId,
|
pub buffer_font_family: FamilyId,
|
||||||
pub buffer_font_size: f32,
|
pub buffer_font_size: f32,
|
||||||
|
pub default_buffer_font_size: f32,
|
||||||
pub vim_mode: bool,
|
pub vim_mode: bool,
|
||||||
pub tab_size: u32,
|
pub tab_size: u32,
|
||||||
pub soft_wrap: SoftWrap,
|
pub soft_wrap: SoftWrap,
|
||||||
@ -73,6 +74,7 @@ impl Settings {
|
|||||||
Ok(Self {
|
Ok(Self {
|
||||||
buffer_font_family: font_cache.load_family(&[buffer_font_family])?,
|
buffer_font_family: font_cache.load_family(&[buffer_font_family])?,
|
||||||
buffer_font_size: 15.,
|
buffer_font_size: 15.,
|
||||||
|
default_buffer_font_size: 15.,
|
||||||
vim_mode: false,
|
vim_mode: false,
|
||||||
tab_size: 4,
|
tab_size: 4,
|
||||||
soft_wrap: SoftWrap::None,
|
soft_wrap: SoftWrap::None,
|
||||||
@ -126,6 +128,7 @@ impl Settings {
|
|||||||
Settings {
|
Settings {
|
||||||
buffer_font_family: cx.font_cache().load_family(&["Monaco"]).unwrap(),
|
buffer_font_family: cx.font_cache().load_family(&["Monaco"]).unwrap(),
|
||||||
buffer_font_size: 14.,
|
buffer_font_size: 14.,
|
||||||
|
default_buffer_font_size: 14.,
|
||||||
vim_mode: false,
|
vim_mode: false,
|
||||||
tab_size: 4,
|
tab_size: 4,
|
||||||
soft_wrap: SoftWrap::None,
|
soft_wrap: SoftWrap::None,
|
||||||
@ -162,6 +165,7 @@ impl Settings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
merge(&mut self.buffer_font_size, data.buffer_font_size);
|
merge(&mut self.buffer_font_size, data.buffer_font_size);
|
||||||
|
merge(&mut self.default_buffer_font_size, data.buffer_font_size);
|
||||||
merge(&mut self.vim_mode, data.vim_mode);
|
merge(&mut self.vim_mode, data.vim_mode);
|
||||||
merge(&mut self.format_on_save, data.format_on_save);
|
merge(&mut self.format_on_save, data.format_on_save);
|
||||||
merge(&mut self.soft_wrap, data.editor.soft_wrap);
|
merge(&mut self.soft_wrap, data.editor.soft_wrap);
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
use crate::Anchor;
|
use crate::Anchor;
|
||||||
use crate::{rope::TextDimension, BufferSnapshot};
|
use crate::{rope::TextDimension, BufferSnapshot};
|
||||||
use std::cmp::Ordering;
|
use std::cmp::Ordering;
|
||||||
|
use std::ops::Range;
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||||
pub enum SelectionGoal {
|
pub enum SelectionGoal {
|
||||||
@ -83,6 +84,10 @@ impl<T: Copy + Ord> Selection<T> {
|
|||||||
self.goal = new_goal;
|
self.goal = new_goal;
|
||||||
self.reversed = false;
|
self.reversed = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn range(&self) -> Range<T> {
|
||||||
|
self.start..self.end
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Selection<usize> {
|
impl Selection<usize> {
|
||||||
|
@ -24,31 +24,67 @@ pub fn marked_text(marked_text: &str) -> (String, Vec<usize>) {
|
|||||||
(unmarked_text, markers.remove(&'|').unwrap_or_default())
|
(unmarked_text, markers.remove(&'|').unwrap_or_default())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Eq, PartialEq, Hash)]
|
||||||
|
pub enum TextRangeMarker {
|
||||||
|
Empty(char),
|
||||||
|
Range(char, char),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TextRangeMarker {
|
||||||
|
fn markers(&self) -> Vec<char> {
|
||||||
|
match self {
|
||||||
|
Self::Empty(m) => vec![*m],
|
||||||
|
Self::Range(l, r) => vec![*l, *r],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<char> for TextRangeMarker {
|
||||||
|
fn from(marker: char) -> Self {
|
||||||
|
Self::Empty(marker)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<(char, char)> for TextRangeMarker {
|
||||||
|
fn from((left_marker, right_marker): (char, char)) -> Self {
|
||||||
|
Self::Range(left_marker, right_marker)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn marked_text_ranges_by(
|
pub fn marked_text_ranges_by(
|
||||||
marked_text: &str,
|
marked_text: &str,
|
||||||
delimiters: Vec<(char, char)>,
|
markers: Vec<TextRangeMarker>,
|
||||||
) -> (String, HashMap<(char, char), Vec<Range<usize>>>) {
|
) -> (String, HashMap<TextRangeMarker, Vec<Range<usize>>>) {
|
||||||
let all_markers = delimiters
|
let all_markers = markers.iter().flat_map(|m| m.markers()).collect();
|
||||||
.iter()
|
|
||||||
.flat_map(|(start, end)| [*start, *end])
|
|
||||||
.collect();
|
|
||||||
let (unmarked_text, mut markers) = marked_text_by(marked_text, all_markers);
|
|
||||||
let range_lookup = delimiters
|
|
||||||
.into_iter()
|
|
||||||
.map(|(start_marker, end_marker)| {
|
|
||||||
let starts = markers.remove(&start_marker).unwrap_or_default();
|
|
||||||
let ends = markers.remove(&end_marker).unwrap_or_default();
|
|
||||||
assert_eq!(starts.len(), ends.len(), "marked ranges are unbalanced");
|
|
||||||
|
|
||||||
let ranges = starts
|
let (unmarked_text, mut marker_offsets) = marked_text_by(marked_text, all_markers);
|
||||||
.into_iter()
|
let range_lookup = markers
|
||||||
.zip(ends)
|
.into_iter()
|
||||||
.map(|(start, end)| {
|
.map(|marker| match marker {
|
||||||
assert!(end >= start, "marked ranges must be disjoint");
|
TextRangeMarker::Empty(empty_marker_char) => {
|
||||||
start..end
|
let ranges = marker_offsets
|
||||||
})
|
.remove(&empty_marker_char)
|
||||||
.collect::<Vec<Range<usize>>>();
|
.unwrap_or_default()
|
||||||
((start_marker, end_marker), ranges)
|
.into_iter()
|
||||||
|
.map(|empty_index| empty_index..empty_index)
|
||||||
|
.collect::<Vec<Range<usize>>>();
|
||||||
|
(marker, ranges)
|
||||||
|
}
|
||||||
|
TextRangeMarker::Range(start_marker, end_marker) => {
|
||||||
|
let starts = marker_offsets.remove(&start_marker).unwrap_or_default();
|
||||||
|
let ends = marker_offsets.remove(&end_marker).unwrap_or_default();
|
||||||
|
assert_eq!(starts.len(), ends.len(), "marked ranges are unbalanced");
|
||||||
|
|
||||||
|
let ranges = starts
|
||||||
|
.into_iter()
|
||||||
|
.zip(ends)
|
||||||
|
.map(|(start, end)| {
|
||||||
|
assert!(end >= start, "marked ranges must be disjoint");
|
||||||
|
start..end
|
||||||
|
})
|
||||||
|
.collect::<Vec<Range<usize>>>();
|
||||||
|
(marker, ranges)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
@ -58,14 +94,16 @@ pub fn marked_text_ranges_by(
|
|||||||
// Returns ranges delimited by (), [], and <> ranges. Ranges using the same markers
|
// Returns ranges delimited by (), [], and <> ranges. Ranges using the same markers
|
||||||
// must not be overlapping. May also include | for empty ranges
|
// must not be overlapping. May also include | for empty ranges
|
||||||
pub fn marked_text_ranges(full_marked_text: &str) -> (String, Vec<Range<usize>>) {
|
pub fn marked_text_ranges(full_marked_text: &str) -> (String, Vec<Range<usize>>) {
|
||||||
let (range_marked_text, empty_offsets) = marked_text(full_marked_text);
|
let (unmarked, range_lookup) = marked_text_ranges_by(
|
||||||
let (unmarked, range_lookup) =
|
&full_marked_text,
|
||||||
marked_text_ranges_by(&range_marked_text, vec![('[', ']'), ('(', ')'), ('<', '>')]);
|
vec![
|
||||||
let mut combined_ranges: Vec<_> = range_lookup
|
'|'.into(),
|
||||||
.into_values()
|
('[', ']').into(),
|
||||||
.flatten()
|
('(', ')').into(),
|
||||||
.chain(empty_offsets.into_iter().map(|offset| offset..offset))
|
('<', '>').into(),
|
||||||
.collect();
|
],
|
||||||
|
);
|
||||||
|
let mut combined_ranges: Vec<_> = range_lookup.into_values().flatten().collect();
|
||||||
|
|
||||||
combined_ranges.sort_by_key(|range| range.start);
|
combined_ranges.sort_by_key(|range| range.start);
|
||||||
(unmarked, combined_ranges)
|
(unmarked, combined_ranges)
|
||||||
|
@ -18,22 +18,31 @@ fn editor_created(EditorCreated(editor): &EditorCreated, cx: &mut MutableAppCont
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn editor_focused(EditorFocused(editor): &EditorFocused, cx: &mut MutableAppContext) {
|
fn editor_focused(EditorFocused(editor): &EditorFocused, cx: &mut MutableAppContext) {
|
||||||
Vim::update(cx, |state, cx| {
|
Vim::update(cx, |vim, cx| {
|
||||||
state.active_editor = Some(editor.downgrade());
|
vim.active_editor = Some(editor.downgrade());
|
||||||
|
vim.selection_subscription = Some(cx.subscribe(editor, |editor, event, cx| {
|
||||||
|
if editor.read(cx).leader_replica_id().is_none() {
|
||||||
|
if let editor::Event::SelectionsChanged { local: true } = event {
|
||||||
|
let newest_empty = editor.read(cx).selections.newest::<usize>(cx).is_empty();
|
||||||
|
editor_local_selections_changed(newest_empty, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
if editor.read(cx).mode() != EditorMode::Full {
|
if editor.read(cx).mode() != EditorMode::Full {
|
||||||
state.switch_mode(Mode::Insert, cx);
|
vim.switch_mode(Mode::Insert, cx);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn editor_blurred(EditorBlurred(editor): &EditorBlurred, cx: &mut MutableAppContext) {
|
fn editor_blurred(EditorBlurred(editor): &EditorBlurred, cx: &mut MutableAppContext) {
|
||||||
Vim::update(cx, |state, cx| {
|
Vim::update(cx, |vim, cx| {
|
||||||
if let Some(previous_editor) = state.active_editor.clone() {
|
if let Some(previous_editor) = vim.active_editor.clone() {
|
||||||
if previous_editor == editor.clone() {
|
if previous_editor == editor.clone() {
|
||||||
state.active_editor = None;
|
vim.active_editor = None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
state.sync_editor_options(cx);
|
vim.sync_editor_options(cx);
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -47,3 +56,11 @@ fn editor_released(EditorReleased(editor): &EditorReleased, cx: &mut MutableAppC
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn editor_local_selections_changed(newest_empty: bool, cx: &mut MutableAppContext) {
|
||||||
|
Vim::update(cx, |vim, cx| {
|
||||||
|
if vim.enabled && vim.state.mode == Mode::Normal && !newest_empty {
|
||||||
|
vim.switch_mode(Mode::Visual { line: false }, cx)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@ -111,7 +111,7 @@ fn motion(motion: Motion, cx: &mut MutableAppContext) {
|
|||||||
});
|
});
|
||||||
match Vim::read(cx).state.mode {
|
match Vim::read(cx).state.mode {
|
||||||
Mode::Normal => normal_motion(motion, cx),
|
Mode::Normal => normal_motion(motion, cx),
|
||||||
Mode::Visual => visual_motion(motion, cx),
|
Mode::Visual { .. } => visual_motion(motion, cx),
|
||||||
Mode::Insert => {
|
Mode::Insert => {
|
||||||
// Shouldn't execute a motion in insert mode. Ignoring
|
// Shouldn't execute a motion in insert mode. Ignoring
|
||||||
}
|
}
|
||||||
@ -192,11 +192,13 @@ impl Motion {
|
|||||||
if selection.end.row() < map.max_point().row() {
|
if selection.end.row() < map.max_point().row() {
|
||||||
*selection.end.row_mut() += 1;
|
*selection.end.row_mut() += 1;
|
||||||
*selection.end.column_mut() = 0;
|
*selection.end.column_mut() = 0;
|
||||||
|
selection.end = map.clip_point(selection.end, Bias::Right);
|
||||||
// Don't reset the end here
|
// Don't reset the end here
|
||||||
return;
|
return;
|
||||||
} else if selection.start.row() > 0 {
|
} else if selection.start.row() > 0 {
|
||||||
*selection.start.row_mut() -= 1;
|
*selection.start.row_mut() -= 1;
|
||||||
*selection.start.column_mut() = map.line_len(selection.start.row());
|
*selection.start.column_mut() = map.line_len(selection.start.row());
|
||||||
|
selection.start = map.clip_point(selection.start, Bias::Left);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
mod change;
|
mod change;
|
||||||
mod delete;
|
mod delete;
|
||||||
|
mod yank;
|
||||||
|
|
||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
motion::Motion,
|
motion::Motion,
|
||||||
@ -8,12 +11,12 @@ use crate::{
|
|||||||
};
|
};
|
||||||
use change::init as change_init;
|
use change::init as change_init;
|
||||||
use collections::HashSet;
|
use collections::HashSet;
|
||||||
use editor::{Autoscroll, Bias, DisplayPoint};
|
use editor::{Autoscroll, Bias, ClipboardSelection, DisplayPoint};
|
||||||
use gpui::{actions, MutableAppContext, ViewContext};
|
use gpui::{actions, MutableAppContext, ViewContext};
|
||||||
use language::SelectionGoal;
|
use language::{Point, SelectionGoal};
|
||||||
use workspace::Workspace;
|
use workspace::Workspace;
|
||||||
|
|
||||||
use self::{change::change_over, delete::delete_over};
|
use self::{change::change_over, delete::delete_over, yank::yank_over};
|
||||||
|
|
||||||
actions!(
|
actions!(
|
||||||
vim,
|
vim,
|
||||||
@ -27,6 +30,8 @@ actions!(
|
|||||||
DeleteRight,
|
DeleteRight,
|
||||||
ChangeToEndOfLine,
|
ChangeToEndOfLine,
|
||||||
DeleteToEndOfLine,
|
DeleteToEndOfLine,
|
||||||
|
Paste,
|
||||||
|
Yank,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -56,6 +61,7 @@ pub fn init(cx: &mut MutableAppContext) {
|
|||||||
delete_over(vim, Motion::EndOfLine, cx);
|
delete_over(vim, Motion::EndOfLine, cx);
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
cx.add_action(paste);
|
||||||
|
|
||||||
change_init(cx);
|
change_init(cx);
|
||||||
}
|
}
|
||||||
@ -64,11 +70,12 @@ pub fn normal_motion(motion: Motion, cx: &mut MutableAppContext) {
|
|||||||
Vim::update(cx, |vim, cx| {
|
Vim::update(cx, |vim, cx| {
|
||||||
match vim.state.operator_stack.pop() {
|
match vim.state.operator_stack.pop() {
|
||||||
None => move_cursor(vim, motion, cx),
|
None => move_cursor(vim, motion, cx),
|
||||||
Some(Operator::Change) => change_over(vim, motion, cx),
|
|
||||||
Some(Operator::Delete) => delete_over(vim, motion, cx),
|
|
||||||
Some(Operator::Namespace(_)) => {
|
Some(Operator::Namespace(_)) => {
|
||||||
// Can't do anything for a namespace operator. Ignoring
|
// Can't do anything for a namespace operator. Ignoring
|
||||||
}
|
}
|
||||||
|
Some(Operator::Change) => change_over(vim, motion, cx),
|
||||||
|
Some(Operator::Delete) => delete_over(vim, motion, cx),
|
||||||
|
Some(Operator::Yank) => yank_over(vim, motion, cx),
|
||||||
}
|
}
|
||||||
vim.clear_operator(cx);
|
vim.clear_operator(cx);
|
||||||
});
|
});
|
||||||
@ -187,6 +194,116 @@ fn insert_line_below(_: &mut Workspace, _: &InsertLineBelow, cx: &mut ViewContex
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Supports non empty selections so it can be bound and called from visual mode
|
||||||
|
fn paste(_: &mut Workspace, _: &Paste, cx: &mut ViewContext<Workspace>) {
|
||||||
|
Vim::update(cx, |vim, cx| {
|
||||||
|
vim.update_active_editor(cx, |editor, cx| {
|
||||||
|
editor.transact(cx, |editor, cx| {
|
||||||
|
if let Some(item) = cx.as_mut().read_from_clipboard() {
|
||||||
|
let mut clipboard_text = Cow::Borrowed(item.text());
|
||||||
|
if let Some(mut clipboard_selections) =
|
||||||
|
item.metadata::<Vec<ClipboardSelection>>()
|
||||||
|
{
|
||||||
|
let (display_map, selections) = editor.selections.all_display(cx);
|
||||||
|
let all_selections_were_entire_line =
|
||||||
|
clipboard_selections.iter().all(|s| s.is_entire_line);
|
||||||
|
if clipboard_selections.len() != selections.len() {
|
||||||
|
let mut newline_separated_text = String::new();
|
||||||
|
let mut clipboard_selections =
|
||||||
|
clipboard_selections.drain(..).peekable();
|
||||||
|
let mut ix = 0;
|
||||||
|
while let Some(clipboard_selection) = clipboard_selections.next() {
|
||||||
|
newline_separated_text
|
||||||
|
.push_str(&clipboard_text[ix..ix + clipboard_selection.len]);
|
||||||
|
ix += clipboard_selection.len;
|
||||||
|
if clipboard_selections.peek().is_some() {
|
||||||
|
newline_separated_text.push('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
clipboard_text = Cow::Owned(newline_separated_text);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut new_selections = Vec::new();
|
||||||
|
editor.buffer().update(cx, |buffer, cx| {
|
||||||
|
let snapshot = buffer.snapshot(cx);
|
||||||
|
let mut start_offset = 0;
|
||||||
|
let mut edits = Vec::new();
|
||||||
|
for (ix, selection) in selections.iter().enumerate() {
|
||||||
|
let to_insert;
|
||||||
|
let linewise;
|
||||||
|
if let Some(clipboard_selection) = clipboard_selections.get(ix) {
|
||||||
|
let end_offset = start_offset + clipboard_selection.len;
|
||||||
|
to_insert = &clipboard_text[start_offset..end_offset];
|
||||||
|
linewise = clipboard_selection.is_entire_line;
|
||||||
|
start_offset = end_offset;
|
||||||
|
} else {
|
||||||
|
to_insert = clipboard_text.as_str();
|
||||||
|
linewise = all_selections_were_entire_line;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the clipboard text was copied linewise, and the current selection
|
||||||
|
// is empty, then paste the text after this line and move the selection
|
||||||
|
// to the start of the pasted text
|
||||||
|
let range = if selection.is_empty() && linewise {
|
||||||
|
let (point, _) = display_map
|
||||||
|
.next_line_boundary(selection.start.to_point(&display_map));
|
||||||
|
|
||||||
|
if !to_insert.starts_with('\n') {
|
||||||
|
// Add newline before pasted text so that it shows up
|
||||||
|
edits.push((point..point, "\n"));
|
||||||
|
}
|
||||||
|
// Drop selection at the start of the next line
|
||||||
|
let selection_point = Point::new(point.row + 1, 0);
|
||||||
|
new_selections.push(selection.map(|_| selection_point.clone()));
|
||||||
|
point..point
|
||||||
|
} else {
|
||||||
|
let mut selection = selection.clone();
|
||||||
|
if !selection.reversed {
|
||||||
|
let mut adjusted = selection.end;
|
||||||
|
// Head is at the end of the selection. Adjust the end position to
|
||||||
|
// to include the character under the cursor.
|
||||||
|
*adjusted.column_mut() = adjusted.column() + 1;
|
||||||
|
adjusted = display_map.clip_point(adjusted, Bias::Right);
|
||||||
|
// If the selection is empty, move both the start and end forward one
|
||||||
|
// character
|
||||||
|
if selection.is_empty() {
|
||||||
|
selection.start = adjusted;
|
||||||
|
selection.end = adjusted;
|
||||||
|
} else {
|
||||||
|
selection.end = adjusted;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let range = selection.map(|p| p.to_point(&display_map)).range();
|
||||||
|
new_selections.push(selection.map(|_| range.start.clone()));
|
||||||
|
range
|
||||||
|
};
|
||||||
|
|
||||||
|
if linewise && to_insert.ends_with('\n') {
|
||||||
|
edits.push((
|
||||||
|
range,
|
||||||
|
&to_insert[0..to_insert.len().saturating_sub(1)],
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
edits.push((range, to_insert));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
drop(snapshot);
|
||||||
|
buffer.edit_with_autoindent(edits, cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||||
|
s.select(new_selections)
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
editor.insert(&clipboard_text, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use indoc::indoc;
|
use indoc::indoc;
|
||||||
@ -678,14 +795,8 @@ mod test {
|
|||||||
|
|
|
|
||||||
The quick"},
|
The quick"},
|
||||||
);
|
);
|
||||||
cx.assert(
|
// Indoc disallows trailing whitspace.
|
||||||
indoc! {"
|
cx.assert(" | \nThe quick", " | \nThe quick");
|
||||||
|
|
|
||||||
The quick"},
|
|
||||||
indoc! {"
|
|
||||||
|
|
|
||||||
The quick"},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
@ -1026,4 +1137,48 @@ mod test {
|
|||||||
brown fox"},
|
brown fox"},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_p(cx: &mut gpui::TestAppContext) {
|
||||||
|
let mut cx = VimTestContext::new(cx, true).await;
|
||||||
|
cx.set_state(
|
||||||
|
indoc! {"
|
||||||
|
The quick brown
|
||||||
|
fox ju|mps over
|
||||||
|
the lazy dog"},
|
||||||
|
Mode::Normal,
|
||||||
|
);
|
||||||
|
|
||||||
|
cx.simulate_keystrokes(["d", "d"]);
|
||||||
|
cx.assert_editor_state(indoc! {"
|
||||||
|
The quick brown
|
||||||
|
the la|zy dog"});
|
||||||
|
|
||||||
|
cx.simulate_keystroke("p");
|
||||||
|
cx.assert_editor_state(indoc! {"
|
||||||
|
The quick brown
|
||||||
|
the lazy dog
|
||||||
|
|fox jumps over"});
|
||||||
|
|
||||||
|
cx.set_state(
|
||||||
|
indoc! {"
|
||||||
|
The quick brown
|
||||||
|
fox [jump}s over
|
||||||
|
the lazy dog"},
|
||||||
|
Mode::Normal,
|
||||||
|
);
|
||||||
|
cx.simulate_keystroke("y");
|
||||||
|
cx.set_state(
|
||||||
|
indoc! {"
|
||||||
|
The quick brown
|
||||||
|
fox jump|s over
|
||||||
|
the lazy dog"},
|
||||||
|
Mode::Normal,
|
||||||
|
);
|
||||||
|
cx.simulate_keystroke("p");
|
||||||
|
cx.assert_editor_state(indoc! {"
|
||||||
|
The quick brown
|
||||||
|
fox jumps|jumps over
|
||||||
|
the lazy dog"});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use crate::{motion::Motion, state::Mode, Vim};
|
use crate::{motion::Motion, state::Mode, utils::copy_selections_content, Vim};
|
||||||
use editor::{char_kind, movement, Autoscroll};
|
use editor::{char_kind, movement, Autoscroll};
|
||||||
use gpui::{impl_actions, MutableAppContext, ViewContext};
|
use gpui::{impl_actions, MutableAppContext, ViewContext};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
@ -27,6 +27,7 @@ pub fn change_over(vim: &mut Vim, motion: Motion, cx: &mut MutableAppContext) {
|
|||||||
motion.expand_selection(map, selection, false);
|
motion.expand_selection(map, selection, false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
copy_selections_content(editor, motion.linewise(), cx);
|
||||||
editor.insert(&"", cx);
|
editor.insert(&"", cx);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -65,6 +66,7 @@ fn change_word(
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
copy_selections_content(editor, false, cx);
|
||||||
editor.insert(&"", cx);
|
editor.insert(&"", cx);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use crate::{motion::Motion, Vim};
|
use crate::{motion::Motion, utils::copy_selections_content, Vim};
|
||||||
use collections::HashMap;
|
use collections::HashMap;
|
||||||
use editor::{Autoscroll, Bias};
|
use editor::{Autoscroll, Bias};
|
||||||
use gpui::MutableAppContext;
|
use gpui::MutableAppContext;
|
||||||
@ -15,6 +15,7 @@ pub fn delete_over(vim: &mut Vim, motion: Motion, cx: &mut MutableAppContext) {
|
|||||||
original_columns.insert(selection.id, original_head.column());
|
original_columns.insert(selection.id, original_head.column());
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
copy_selections_content(editor, motion.linewise(), cx);
|
||||||
editor.insert(&"", cx);
|
editor.insert(&"", cx);
|
||||||
|
|
||||||
// Fixup cursor position after the deletion
|
// Fixup cursor position after the deletion
|
||||||
|
26
crates/vim/src/normal/yank.rs
Normal file
26
crates/vim/src/normal/yank.rs
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
use crate::{motion::Motion, utils::copy_selections_content, Vim};
|
||||||
|
use collections::HashMap;
|
||||||
|
use gpui::MutableAppContext;
|
||||||
|
|
||||||
|
pub fn yank_over(vim: &mut Vim, motion: Motion, cx: &mut MutableAppContext) {
|
||||||
|
vim.update_active_editor(cx, |editor, cx| {
|
||||||
|
editor.transact(cx, |editor, cx| {
|
||||||
|
editor.set_clip_at_line_ends(false, cx);
|
||||||
|
let mut original_positions: HashMap<_, _> = Default::default();
|
||||||
|
editor.change_selections(None, cx, |s| {
|
||||||
|
s.move_with(|map, selection| {
|
||||||
|
let original_position = (selection.head(), selection.goal);
|
||||||
|
motion.expand_selection(map, selection, true);
|
||||||
|
original_positions.insert(selection.id, original_position);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
copy_selections_content(editor, motion.linewise(), cx);
|
||||||
|
editor.change_selections(None, cx, |s| {
|
||||||
|
s.move_with(|_, selection| {
|
||||||
|
let (head, goal) = original_positions.remove(&selection.id).unwrap();
|
||||||
|
selection.collapse_to(head, goal);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
@ -6,7 +6,7 @@ use serde::Deserialize;
|
|||||||
pub enum Mode {
|
pub enum Mode {
|
||||||
Normal,
|
Normal,
|
||||||
Insert,
|
Insert,
|
||||||
Visual,
|
Visual { line: bool },
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Mode {
|
impl Default for Mode {
|
||||||
@ -25,6 +25,7 @@ pub enum Operator {
|
|||||||
Namespace(Namespace),
|
Namespace(Namespace),
|
||||||
Change,
|
Change,
|
||||||
Delete,
|
Delete,
|
||||||
|
Yank,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
@ -36,8 +37,7 @@ pub struct VimState {
|
|||||||
impl VimState {
|
impl VimState {
|
||||||
pub fn cursor_shape(&self) -> CursorShape {
|
pub fn cursor_shape(&self) -> CursorShape {
|
||||||
match self.mode {
|
match self.mode {
|
||||||
Mode::Normal => CursorShape::Block,
|
Mode::Normal | Mode::Visual { .. } => CursorShape::Block,
|
||||||
Mode::Visual => CursorShape::Block,
|
|
||||||
Mode::Insert => CursorShape::Bar,
|
Mode::Insert => CursorShape::Bar,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -46,13 +46,24 @@ impl VimState {
|
|||||||
!matches!(self.mode, Mode::Insert)
|
!matches!(self.mode, Mode::Insert)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn clip_at_line_end(&self) -> bool {
|
||||||
|
match self.mode {
|
||||||
|
Mode::Insert | Mode::Visual { .. } => false,
|
||||||
|
_ => true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn empty_selections_only(&self) -> bool {
|
||||||
|
!matches!(self.mode, Mode::Visual { .. })
|
||||||
|
}
|
||||||
|
|
||||||
pub fn keymap_context_layer(&self) -> Context {
|
pub fn keymap_context_layer(&self) -> Context {
|
||||||
let mut context = Context::default();
|
let mut context = Context::default();
|
||||||
context.map.insert(
|
context.map.insert(
|
||||||
"vim_mode".to_string(),
|
"vim_mode".to_string(),
|
||||||
match self.mode {
|
match self.mode {
|
||||||
Mode::Normal => "normal",
|
Mode::Normal => "normal",
|
||||||
Mode::Visual => "visual",
|
Mode::Visual { .. } => "visual",
|
||||||
Mode::Insert => "insert",
|
Mode::Insert => "insert",
|
||||||
}
|
}
|
||||||
.to_string(),
|
.to_string(),
|
||||||
@ -75,6 +86,7 @@ impl Operator {
|
|||||||
Operator::Namespace(Namespace::G) => "g",
|
Operator::Namespace(Namespace::G) => "g",
|
||||||
Operator::Change => "c",
|
Operator::Change => "c",
|
||||||
Operator::Delete => "d",
|
Operator::Delete => "d",
|
||||||
|
Operator::Yank => "y",
|
||||||
}
|
}
|
||||||
.to_owned();
|
.to_owned();
|
||||||
|
|
||||||
|
25
crates/vim/src/utils.rs
Normal file
25
crates/vim/src/utils.rs
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
use editor::{ClipboardSelection, Editor};
|
||||||
|
use gpui::{ClipboardItem, MutableAppContext};
|
||||||
|
|
||||||
|
pub fn copy_selections_content(editor: &mut Editor, linewise: bool, cx: &mut MutableAppContext) {
|
||||||
|
let selections = editor.selections.all_adjusted(cx);
|
||||||
|
let buffer = editor.buffer().read(cx).snapshot(cx);
|
||||||
|
let mut text = String::new();
|
||||||
|
let mut clipboard_selections = Vec::with_capacity(selections.len());
|
||||||
|
{
|
||||||
|
for selection in selections.iter() {
|
||||||
|
let initial_len = text.len();
|
||||||
|
let start = selection.start;
|
||||||
|
let end = selection.end;
|
||||||
|
for chunk in buffer.text_for_range(start..end) {
|
||||||
|
text.push_str(chunk);
|
||||||
|
}
|
||||||
|
clipboard_selections.push(ClipboardSelection {
|
||||||
|
len: text.len() - initial_len,
|
||||||
|
is_entire_line: linewise,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cx.write_to_clipboard(ClipboardItem::new(text).with_metadata(clipboard_selections));
|
||||||
|
}
|
@ -6,11 +6,12 @@ mod insert;
|
|||||||
mod motion;
|
mod motion;
|
||||||
mod normal;
|
mod normal;
|
||||||
mod state;
|
mod state;
|
||||||
|
mod utils;
|
||||||
mod visual;
|
mod visual;
|
||||||
|
|
||||||
use collections::HashMap;
|
use collections::HashMap;
|
||||||
use editor::{CursorShape, Editor};
|
use editor::{Bias, CursorShape, Editor, Input};
|
||||||
use gpui::{impl_actions, MutableAppContext, ViewContext, WeakViewHandle};
|
use gpui::{impl_actions, MutableAppContext, Subscription, ViewContext, WeakViewHandle};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
@ -40,9 +41,19 @@ pub fn init(cx: &mut MutableAppContext) {
|
|||||||
Vim::update(cx, |vim, cx| vim.push_operator(operator, cx))
|
Vim::update(cx, |vim, cx| vim.push_operator(operator, cx))
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
cx.add_action(|_: &mut Editor, _: &Input, cx| {
|
||||||
|
if Vim::read(cx).active_operator().is_some() {
|
||||||
|
// Defer without updating editor
|
||||||
|
MutableAppContext::defer(cx, |cx| Vim::update(cx, |vim, cx| vim.clear_operator(cx)))
|
||||||
|
} else {
|
||||||
|
cx.propagate_action()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
cx.observe_global::<Settings, _>(|settings, cx| {
|
cx.observe_global::<Settings, _>(|cx| {
|
||||||
Vim::update(cx, |state, cx| state.set_enabled(settings.vim_mode, cx))
|
Vim::update(cx, |state, cx| {
|
||||||
|
state.set_enabled(cx.global::<Settings>().vim_mode, cx)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
}
|
}
|
||||||
@ -51,6 +62,7 @@ pub fn init(cx: &mut MutableAppContext) {
|
|||||||
pub struct Vim {
|
pub struct Vim {
|
||||||
editors: HashMap<usize, WeakViewHandle<Editor>>,
|
editors: HashMap<usize, WeakViewHandle<Editor>>,
|
||||||
active_editor: Option<WeakViewHandle<Editor>>,
|
active_editor: Option<WeakViewHandle<Editor>>,
|
||||||
|
selection_subscription: Option<Subscription>,
|
||||||
|
|
||||||
enabled: bool,
|
enabled: bool,
|
||||||
state: VimState,
|
state: VimState,
|
||||||
@ -101,7 +113,7 @@ impl Vim {
|
|||||||
self.sync_editor_options(cx);
|
self.sync_editor_options(cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn active_operator(&mut self) -> Option<Operator> {
|
fn active_operator(&self) -> Option<Operator> {
|
||||||
self.state.operator_stack.last().copied()
|
self.state.operator_stack.last().copied()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -118,23 +130,38 @@ impl Vim {
|
|||||||
|
|
||||||
fn sync_editor_options(&self, cx: &mut MutableAppContext) {
|
fn sync_editor_options(&self, cx: &mut MutableAppContext) {
|
||||||
let state = &self.state;
|
let state = &self.state;
|
||||||
|
|
||||||
let cursor_shape = state.cursor_shape();
|
let cursor_shape = state.cursor_shape();
|
||||||
|
|
||||||
for editor in self.editors.values() {
|
for editor in self.editors.values() {
|
||||||
if let Some(editor) = editor.upgrade(cx) {
|
if let Some(editor) = editor.upgrade(cx) {
|
||||||
editor.update(cx, |editor, cx| {
|
editor.update(cx, |editor, cx| {
|
||||||
if self.enabled {
|
if self.enabled {
|
||||||
editor.set_cursor_shape(cursor_shape, cx);
|
editor.set_cursor_shape(cursor_shape, cx);
|
||||||
editor.set_clip_at_line_ends(cursor_shape == CursorShape::Block, cx);
|
editor.set_clip_at_line_ends(state.clip_at_line_end(), cx);
|
||||||
editor.set_input_enabled(!state.vim_controlled());
|
editor.set_input_enabled(!state.vim_controlled());
|
||||||
|
editor.selections.line_mode =
|
||||||
|
matches!(state.mode, Mode::Visual { line: true });
|
||||||
let context_layer = state.keymap_context_layer();
|
let context_layer = state.keymap_context_layer();
|
||||||
editor.set_keymap_context_layer::<Self>(context_layer);
|
editor.set_keymap_context_layer::<Self>(context_layer);
|
||||||
} else {
|
} else {
|
||||||
editor.set_cursor_shape(CursorShape::Bar, cx);
|
editor.set_cursor_shape(CursorShape::Bar, cx);
|
||||||
editor.set_clip_at_line_ends(false, cx);
|
editor.set_clip_at_line_ends(false, cx);
|
||||||
editor.set_input_enabled(true);
|
editor.set_input_enabled(true);
|
||||||
|
editor.selections.line_mode = false;
|
||||||
editor.remove_keymap_context_layer::<Self>();
|
editor.remove_keymap_context_layer::<Self>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
editor.change_selections(None, cx, |s| {
|
||||||
|
s.move_with(|map, selection| {
|
||||||
|
selection.set_head(
|
||||||
|
map.clip_point(selection.head(), Bias::Left),
|
||||||
|
selection.goal,
|
||||||
|
);
|
||||||
|
if state.empty_selections_only() {
|
||||||
|
selection.collapse_to(selection.head(), selection.goal)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -169,9 +196,9 @@ mod test {
|
|||||||
assert_eq!(cx.mode(), Mode::Normal);
|
assert_eq!(cx.mode(), Mode::Normal);
|
||||||
cx.simulate_keystrokes(["h", "h", "h", "l"]);
|
cx.simulate_keystrokes(["h", "h", "h", "l"]);
|
||||||
assert_eq!(cx.editor_text(), "hjkl".to_owned());
|
assert_eq!(cx.editor_text(), "hjkl".to_owned());
|
||||||
cx.assert_editor_state("hj|kl");
|
cx.assert_editor_state("h|jkl");
|
||||||
cx.simulate_keystrokes(["i", "T", "e", "s", "t"]);
|
cx.simulate_keystrokes(["i", "T", "e", "s", "t"]);
|
||||||
cx.assert_editor_state("hjTest|kl");
|
cx.assert_editor_state("hTest|jkl");
|
||||||
|
|
||||||
// Disabling and enabling resets to normal mode
|
// Disabling and enabling resets to normal mode
|
||||||
assert_eq!(cx.mode(), Mode::Insert);
|
assert_eq!(cx.mode(), Mode::Insert);
|
||||||
|
@ -1,31 +1,21 @@
|
|||||||
use std::ops::{Deref, Range};
|
use std::ops::{Deref, DerefMut};
|
||||||
|
|
||||||
use collections::BTreeMap;
|
use editor::test::EditorTestContext;
|
||||||
use itertools::{Either, Itertools};
|
use gpui::json::json;
|
||||||
|
|
||||||
use editor::{display_map::ToDisplayPoint, Autoscroll};
|
|
||||||
use gpui::{json::json, keymap::Keystroke, ViewHandle};
|
|
||||||
use indoc::indoc;
|
|
||||||
use language::Selection;
|
|
||||||
use project::Project;
|
use project::Project;
|
||||||
use util::{
|
use workspace::{pane, AppState, WorkspaceHandle};
|
||||||
set_eq,
|
|
||||||
test::{marked_text, marked_text_ranges_by, SetEqError},
|
|
||||||
};
|
|
||||||
use workspace::{AppState, WorkspaceHandle};
|
|
||||||
|
|
||||||
use crate::{state::Operator, *};
|
use crate::{state::Operator, *};
|
||||||
|
|
||||||
pub struct VimTestContext<'a> {
|
pub struct VimTestContext<'a> {
|
||||||
cx: &'a mut gpui::TestAppContext,
|
cx: EditorTestContext<'a>,
|
||||||
window_id: usize,
|
|
||||||
editor: ViewHandle<Editor>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> VimTestContext<'a> {
|
impl<'a> VimTestContext<'a> {
|
||||||
pub async fn new(cx: &'a mut gpui::TestAppContext, enabled: bool) -> VimTestContext<'a> {
|
pub async fn new(cx: &'a mut gpui::TestAppContext, enabled: bool) -> VimTestContext<'a> {
|
||||||
cx.update(|cx| {
|
cx.update(|cx| {
|
||||||
editor::init(cx);
|
editor::init(cx);
|
||||||
|
pane::init(cx);
|
||||||
crate::init(cx);
|
crate::init(cx);
|
||||||
|
|
||||||
settings::KeymapFileContent::load("keymaps/vim.json", cx).unwrap();
|
settings::KeymapFileContent::load("keymaps/vim.json", cx).unwrap();
|
||||||
@ -69,9 +59,11 @@ impl<'a> VimTestContext<'a> {
|
|||||||
editor.update(cx, |_, cx| cx.focus_self());
|
editor.update(cx, |_, cx| cx.focus_self());
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
cx,
|
cx: EditorTestContext {
|
||||||
window_id,
|
cx,
|
||||||
editor,
|
window_id,
|
||||||
|
editor,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -100,219 +92,13 @@ impl<'a> VimTestContext<'a> {
|
|||||||
.read(|cx| cx.global::<Vim>().state.operator_stack.last().copied())
|
.read(|cx| cx.global::<Vim>().state.operator_stack.last().copied())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn editor_text(&mut self) -> String {
|
|
||||||
self.editor
|
|
||||||
.update(self.cx, |editor, cx| editor.snapshot(cx).text())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn simulate_keystroke(&mut self, keystroke_text: &str) {
|
|
||||||
let keystroke = Keystroke::parse(keystroke_text).unwrap();
|
|
||||||
let input = if keystroke.modified() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(keystroke.key.clone())
|
|
||||||
};
|
|
||||||
self.cx
|
|
||||||
.dispatch_keystroke(self.window_id, keystroke, input, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn simulate_keystrokes<const COUNT: usize>(&mut self, keystroke_texts: [&str; COUNT]) {
|
|
||||||
for keystroke_text in keystroke_texts.into_iter() {
|
|
||||||
self.simulate_keystroke(keystroke_text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_state(&mut self, text: &str, mode: Mode) {
|
pub fn set_state(&mut self, text: &str, mode: Mode) {
|
||||||
self.cx
|
self.cx.update(|cx| {
|
||||||
.update(|cx| Vim::update(cx, |vim, cx| vim.switch_mode(mode, cx)));
|
Vim::update(cx, |vim, cx| {
|
||||||
self.editor.update(self.cx, |editor, cx| {
|
vim.switch_mode(mode, cx);
|
||||||
let (unmarked_text, markers) = marked_text(&text);
|
})
|
||||||
editor.set_text(unmarked_text, cx);
|
|
||||||
let cursor_offset = markers[0];
|
|
||||||
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
|
||||||
s.replace_cursors_with(|map| vec![cursor_offset.to_display_point(map)])
|
|
||||||
});
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Asserts the editor state via a marked string.
|
|
||||||
// `|` characters represent empty selections
|
|
||||||
// `[` to `}` represents a non empty selection with the head at `}`
|
|
||||||
// `{` to `]` represents a non empty selection with the head at `{`
|
|
||||||
pub fn assert_editor_state(&mut self, text: &str) {
|
|
||||||
let (text_with_ranges, expected_empty_selections) = marked_text(&text);
|
|
||||||
let (unmarked_text, mut selection_ranges) =
|
|
||||||
marked_text_ranges_by(&text_with_ranges, vec![('[', '}'), ('{', ']')]);
|
|
||||||
let editor_text = self.editor_text();
|
|
||||||
assert_eq!(
|
|
||||||
editor_text, unmarked_text,
|
|
||||||
"Unmarked text doesn't match editor text"
|
|
||||||
);
|
|
||||||
|
|
||||||
let expected_reverse_selections = selection_ranges.remove(&('{', ']')).unwrap_or_default();
|
|
||||||
let expected_forward_selections = selection_ranges.remove(&('[', '}')).unwrap_or_default();
|
|
||||||
|
|
||||||
self.assert_selections(
|
|
||||||
expected_empty_selections,
|
|
||||||
expected_reverse_selections,
|
|
||||||
expected_forward_selections,
|
|
||||||
Some(text.to_string()),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn assert_editor_selections(&mut self, expected_selections: Vec<Selection<usize>>) {
|
|
||||||
let (expected_empty_selections, expected_non_empty_selections): (Vec<_>, Vec<_>) =
|
|
||||||
expected_selections.into_iter().partition_map(|selection| {
|
|
||||||
if selection.is_empty() {
|
|
||||||
Either::Left(selection.head())
|
|
||||||
} else {
|
|
||||||
Either::Right(selection)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let (expected_reverse_selections, expected_forward_selections): (Vec<_>, Vec<_>) =
|
|
||||||
expected_non_empty_selections
|
|
||||||
.into_iter()
|
|
||||||
.partition_map(|selection| {
|
|
||||||
let range = selection.start..selection.end;
|
|
||||||
if selection.reversed {
|
|
||||||
Either::Left(range)
|
|
||||||
} else {
|
|
||||||
Either::Right(range)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
self.assert_selections(
|
|
||||||
expected_empty_selections,
|
|
||||||
expected_reverse_selections,
|
|
||||||
expected_forward_selections,
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn assert_selections(
|
|
||||||
&mut self,
|
|
||||||
expected_empty_selections: Vec<usize>,
|
|
||||||
expected_reverse_selections: Vec<Range<usize>>,
|
|
||||||
expected_forward_selections: Vec<Range<usize>>,
|
|
||||||
asserted_text: Option<String>,
|
|
||||||
) {
|
|
||||||
let (empty_selections, reverse_selections, forward_selections) =
|
|
||||||
self.editor.read_with(self.cx, |editor, cx| {
|
|
||||||
let (empty_selections, non_empty_selections): (Vec<_>, Vec<_>) = editor
|
|
||||||
.selections
|
|
||||||
.all::<usize>(cx)
|
|
||||||
.into_iter()
|
|
||||||
.partition_map(|selection| {
|
|
||||||
if selection.is_empty() {
|
|
||||||
Either::Left(selection.head())
|
|
||||||
} else {
|
|
||||||
Either::Right(selection)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let (reverse_selections, forward_selections): (Vec<_>, Vec<_>) =
|
|
||||||
non_empty_selections.into_iter().partition_map(|selection| {
|
|
||||||
let range = selection.start..selection.end;
|
|
||||||
if selection.reversed {
|
|
||||||
Either::Left(range)
|
|
||||||
} else {
|
|
||||||
Either::Right(range)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
(empty_selections, reverse_selections, forward_selections)
|
|
||||||
});
|
|
||||||
|
|
||||||
let asserted_selections = asserted_text.unwrap_or_else(|| {
|
|
||||||
self.insert_markers(
|
|
||||||
&expected_empty_selections,
|
|
||||||
&expected_reverse_selections,
|
|
||||||
&expected_forward_selections,
|
|
||||||
)
|
|
||||||
});
|
});
|
||||||
let actual_selections =
|
self.cx.set_state(text);
|
||||||
self.insert_markers(&empty_selections, &reverse_selections, &forward_selections);
|
|
||||||
|
|
||||||
let unmarked_text = self.editor_text();
|
|
||||||
let all_eq: Result<(), SetEqError<String>> =
|
|
||||||
set_eq!(expected_empty_selections, empty_selections)
|
|
||||||
.map_err(|err| {
|
|
||||||
err.map(|missing| {
|
|
||||||
let mut error_text = unmarked_text.clone();
|
|
||||||
error_text.insert(missing, '|');
|
|
||||||
error_text
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.and_then(|_| {
|
|
||||||
set_eq!(expected_reverse_selections, reverse_selections).map_err(|err| {
|
|
||||||
err.map(|missing| {
|
|
||||||
let mut error_text = unmarked_text.clone();
|
|
||||||
error_text.insert(missing.start, '{');
|
|
||||||
error_text.insert(missing.end, ']');
|
|
||||||
error_text
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.and_then(|_| {
|
|
||||||
set_eq!(expected_forward_selections, forward_selections).map_err(|err| {
|
|
||||||
err.map(|missing| {
|
|
||||||
let mut error_text = unmarked_text.clone();
|
|
||||||
error_text.insert(missing.start, '[');
|
|
||||||
error_text.insert(missing.end, '}');
|
|
||||||
error_text
|
|
||||||
})
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
match all_eq {
|
|
||||||
Err(SetEqError::LeftMissing(location_text)) => {
|
|
||||||
panic!(
|
|
||||||
indoc! {"
|
|
||||||
Editor has extra selection
|
|
||||||
Extra Selection Location: {}
|
|
||||||
Asserted selections: {}
|
|
||||||
Actual selections: {}"},
|
|
||||||
location_text, asserted_selections, actual_selections,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Err(SetEqError::RightMissing(location_text)) => {
|
|
||||||
panic!(
|
|
||||||
indoc! {"
|
|
||||||
Editor is missing empty selection
|
|
||||||
Missing Selection Location: {}
|
|
||||||
Asserted selections: {}
|
|
||||||
Actual selections: {}"},
|
|
||||||
location_text, asserted_selections, actual_selections,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn insert_markers(
|
|
||||||
&mut self,
|
|
||||||
empty_selections: &Vec<usize>,
|
|
||||||
reverse_selections: &Vec<Range<usize>>,
|
|
||||||
forward_selections: &Vec<Range<usize>>,
|
|
||||||
) -> String {
|
|
||||||
let mut editor_text_with_selections = self.editor_text();
|
|
||||||
let mut selection_marks = BTreeMap::new();
|
|
||||||
for offset in empty_selections {
|
|
||||||
selection_marks.insert(offset, '|');
|
|
||||||
}
|
|
||||||
for range in reverse_selections {
|
|
||||||
selection_marks.insert(&range.start, '{');
|
|
||||||
selection_marks.insert(&range.end, ']');
|
|
||||||
}
|
|
||||||
for range in forward_selections {
|
|
||||||
selection_marks.insert(&range.start, '[');
|
|
||||||
selection_marks.insert(&range.end, '}');
|
|
||||||
}
|
|
||||||
for (offset, mark) in selection_marks.into_iter().rev() {
|
|
||||||
editor_text_with_selections.insert(*offset, mark);
|
|
||||||
}
|
|
||||||
|
|
||||||
editor_text_with_selections
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn assert_binding<const COUNT: usize>(
|
pub fn assert_binding<const COUNT: usize>(
|
||||||
@ -324,8 +110,8 @@ impl<'a> VimTestContext<'a> {
|
|||||||
mode_after: Mode,
|
mode_after: Mode,
|
||||||
) {
|
) {
|
||||||
self.set_state(initial_state, initial_mode);
|
self.set_state(initial_state, initial_mode);
|
||||||
self.simulate_keystrokes(keystrokes);
|
self.cx.simulate_keystrokes(keystrokes);
|
||||||
self.assert_editor_state(state_after);
|
self.cx.assert_editor_state(state_after);
|
||||||
assert_eq!(self.mode(), mode_after);
|
assert_eq!(self.mode(), mode_after);
|
||||||
assert_eq!(self.active_operator(), None);
|
assert_eq!(self.active_operator(), None);
|
||||||
}
|
}
|
||||||
@ -337,13 +123,27 @@ impl<'a> VimTestContext<'a> {
|
|||||||
let mode = self.mode();
|
let mode = self.mode();
|
||||||
VimBindingTestContext::new(keystrokes, mode, mode, self)
|
VimBindingTestContext::new(keystrokes, mode, mode, self)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn assert_clipboard_content(&mut self, expected_content: Option<&str>) {
|
||||||
|
self.cx.update(|cx| {
|
||||||
|
let actual_content = cx.read_from_clipboard().map(|item| item.text().to_owned());
|
||||||
|
let expected_content = expected_content.map(|content| content.to_owned());
|
||||||
|
assert_eq!(actual_content, expected_content);
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Deref for VimTestContext<'a> {
|
impl<'a> Deref for VimTestContext<'a> {
|
||||||
type Target = gpui::TestAppContext;
|
type Target = EditorTestContext<'a>;
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
fn deref(&self) -> &Self::Target {
|
||||||
self.cx
|
&self.cx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> DerefMut for VimTestContext<'a> {
|
||||||
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
|
&mut self.cx
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -404,3 +204,9 @@ impl<'a, const COUNT: usize> Deref for VimBindingTestContext<'a, COUNT> {
|
|||||||
&self.cx
|
&self.cx
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<'a, const COUNT: usize> DerefMut for VimBindingTestContext<'a, COUNT> {
|
||||||
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
|
&mut self.cx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,14 +1,17 @@
|
|||||||
use editor::{Autoscroll, Bias};
|
use collections::HashMap;
|
||||||
|
use editor::{display_map::ToDisplayPoint, Autoscroll, Bias};
|
||||||
use gpui::{actions, MutableAppContext, ViewContext};
|
use gpui::{actions, MutableAppContext, ViewContext};
|
||||||
|
use language::SelectionGoal;
|
||||||
use workspace::Workspace;
|
use workspace::Workspace;
|
||||||
|
|
||||||
use crate::{motion::Motion, state::Mode, Vim};
|
use crate::{motion::Motion, state::Mode, utils::copy_selections_content, Vim};
|
||||||
|
|
||||||
actions!(vim, [VisualDelete, VisualChange]);
|
actions!(vim, [VisualDelete, VisualChange, VisualYank]);
|
||||||
|
|
||||||
pub fn init(cx: &mut MutableAppContext) {
|
pub fn init(cx: &mut MutableAppContext) {
|
||||||
cx.add_action(change);
|
cx.add_action(change);
|
||||||
cx.add_action(delete);
|
cx.add_action(delete);
|
||||||
|
cx.add_action(yank);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn visual_motion(motion: Motion, cx: &mut MutableAppContext) {
|
pub fn visual_motion(motion: Motion, cx: &mut MutableAppContext) {
|
||||||
@ -17,7 +20,6 @@ pub fn visual_motion(motion: Motion, cx: &mut MutableAppContext) {
|
|||||||
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||||
s.move_with(|map, selection| {
|
s.move_with(|map, selection| {
|
||||||
let (new_head, goal) = motion.move_point(map, selection.head(), selection.goal);
|
let (new_head, goal) = motion.move_point(map, selection.head(), selection.goal);
|
||||||
let new_head = map.clip_at_line_end(new_head);
|
|
||||||
let was_reversed = selection.reversed;
|
let was_reversed = selection.reversed;
|
||||||
selection.set_head(new_head, goal);
|
selection.set_head(new_head, goal);
|
||||||
|
|
||||||
@ -30,7 +32,7 @@ pub fn visual_motion(motion: Motion, cx: &mut MutableAppContext) {
|
|||||||
// Head was at the end of the selection, and now is at the start. We need to move the end
|
// Head was at the end of the selection, and now is at the start. We need to move the end
|
||||||
// forward by one if possible in order to compensate for this change.
|
// forward by one if possible in order to compensate for this change.
|
||||||
*selection.end.column_mut() = selection.end.column() + 1;
|
*selection.end.column_mut() = selection.end.column() + 1;
|
||||||
selection.end = map.clip_point(selection.end, Bias::Left);
|
selection.end = map.clip_point(selection.end, Bias::Right);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -42,17 +44,47 @@ pub fn change(_: &mut Workspace, _: &VisualChange, cx: &mut ViewContext<Workspac
|
|||||||
Vim::update(cx, |vim, cx| {
|
Vim::update(cx, |vim, cx| {
|
||||||
vim.update_active_editor(cx, |editor, cx| {
|
vim.update_active_editor(cx, |editor, cx| {
|
||||||
editor.set_clip_at_line_ends(false, cx);
|
editor.set_clip_at_line_ends(false, cx);
|
||||||
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
// Compute edits and resulting anchor selections. If in line mode, adjust
|
||||||
|
// the anchor location and additional newline
|
||||||
|
let mut edits = Vec::new();
|
||||||
|
let mut new_selections = Vec::new();
|
||||||
|
let line_mode = editor.selections.line_mode;
|
||||||
|
editor.change_selections(None, cx, |s| {
|
||||||
s.move_with(|map, selection| {
|
s.move_with(|map, selection| {
|
||||||
if !selection.reversed {
|
if !selection.reversed {
|
||||||
// Head was at the end of the selection, and now is at the start. We need to move the end
|
// Head is at the end of the selection. Adjust the end position to
|
||||||
// forward by one if possible in order to compensate for this change.
|
// to include the character under the cursor.
|
||||||
*selection.end.column_mut() = selection.end.column() + 1;
|
*selection.end.column_mut() = selection.end.column() + 1;
|
||||||
selection.end = map.clip_point(selection.end, Bias::Left);
|
selection.end = map.clip_point(selection.end, Bias::Right);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if line_mode {
|
||||||
|
let range = selection.map(|p| p.to_point(map)).range();
|
||||||
|
let expanded_range = map.expand_to_line(range);
|
||||||
|
// If we are at the last line, the anchor needs to be after the newline so that
|
||||||
|
// it is on a line of its own. Otherwise, the anchor may be after the newline
|
||||||
|
let anchor = if expanded_range.end == map.buffer_snapshot.max_point() {
|
||||||
|
map.buffer_snapshot.anchor_after(expanded_range.end)
|
||||||
|
} else {
|
||||||
|
map.buffer_snapshot.anchor_before(expanded_range.start)
|
||||||
|
};
|
||||||
|
|
||||||
|
edits.push((expanded_range, "\n"));
|
||||||
|
new_selections.push(selection.map(|_| anchor.clone()));
|
||||||
|
} else {
|
||||||
|
let range = selection.map(|p| p.to_point(map)).range();
|
||||||
|
let anchor = map.buffer_snapshot.anchor_after(range.end);
|
||||||
|
edits.push((range, ""));
|
||||||
|
new_selections.push(selection.map(|_| anchor.clone()));
|
||||||
|
}
|
||||||
|
selection.goal = SelectionGoal::None;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
editor.insert("", cx);
|
copy_selections_content(editor, editor.selections.line_mode, cx);
|
||||||
|
editor.edit_with_autoindent(edits, cx);
|
||||||
|
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||||
|
s.select_anchors(new_selections);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
vim.switch_mode(Mode::Insert, cx);
|
vim.switch_mode(Mode::Insert, cx);
|
||||||
});
|
});
|
||||||
@ -60,31 +92,70 @@ pub fn change(_: &mut Workspace, _: &VisualChange, cx: &mut ViewContext<Workspac
|
|||||||
|
|
||||||
pub fn delete(_: &mut Workspace, _: &VisualDelete, cx: &mut ViewContext<Workspace>) {
|
pub fn delete(_: &mut Workspace, _: &VisualDelete, cx: &mut ViewContext<Workspace>) {
|
||||||
Vim::update(cx, |vim, cx| {
|
Vim::update(cx, |vim, cx| {
|
||||||
vim.switch_mode(Mode::Normal, cx);
|
|
||||||
vim.update_active_editor(cx, |editor, cx| {
|
vim.update_active_editor(cx, |editor, cx| {
|
||||||
editor.set_clip_at_line_ends(false, cx);
|
editor.set_clip_at_line_ends(false, cx);
|
||||||
|
let mut original_columns: HashMap<_, _> = Default::default();
|
||||||
|
let line_mode = editor.selections.line_mode;
|
||||||
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||||
s.move_with(|map, selection| {
|
s.move_with(|map, selection| {
|
||||||
if !selection.reversed {
|
if line_mode {
|
||||||
// Head was at the end of the selection, and now is at the start. We need to move the end
|
original_columns
|
||||||
// forward by one if possible in order to compensate for this change.
|
.insert(selection.id, selection.head().to_point(&map).column);
|
||||||
|
} else if !selection.reversed {
|
||||||
|
// Head is at the end of the selection. Adjust the end position to
|
||||||
|
// to include the character under the cursor.
|
||||||
*selection.end.column_mut() = selection.end.column() + 1;
|
*selection.end.column_mut() = selection.end.column() + 1;
|
||||||
selection.end = map.clip_point(selection.end, Bias::Left);
|
selection.end = map.clip_point(selection.end, Bias::Right);
|
||||||
}
|
}
|
||||||
|
selection.goal = SelectionGoal::None;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
copy_selections_content(editor, line_mode, cx);
|
||||||
editor.insert("", cx);
|
editor.insert("", cx);
|
||||||
|
|
||||||
// Fixup cursor position after the deletion
|
// Fixup cursor position after the deletion
|
||||||
editor.set_clip_at_line_ends(true, cx);
|
editor.set_clip_at_line_ends(true, cx);
|
||||||
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||||
s.move_with(|map, selection| {
|
s.move_with(|map, selection| {
|
||||||
let mut cursor = selection.head();
|
let mut cursor = selection.head().to_point(map);
|
||||||
cursor = map.clip_point(cursor, Bias::Left);
|
|
||||||
|
if let Some(column) = original_columns.get(&selection.id) {
|
||||||
|
cursor.column = *column
|
||||||
|
}
|
||||||
|
let cursor = map.clip_point(cursor.to_display_point(map), Bias::Left);
|
||||||
selection.collapse_to(cursor, selection.goal)
|
selection.collapse_to(cursor, selection.goal)
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
vim.switch_mode(Mode::Normal, cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn yank(_: &mut Workspace, _: &VisualYank, cx: &mut ViewContext<Workspace>) {
|
||||||
|
Vim::update(cx, |vim, cx| {
|
||||||
|
vim.update_active_editor(cx, |editor, cx| {
|
||||||
|
editor.set_clip_at_line_ends(false, cx);
|
||||||
|
let line_mode = editor.selections.line_mode;
|
||||||
|
if !editor.selections.line_mode {
|
||||||
|
editor.change_selections(None, cx, |s| {
|
||||||
|
s.move_with(|map, selection| {
|
||||||
|
if !selection.reversed {
|
||||||
|
// Head is at the end of the selection. Adjust the end position to
|
||||||
|
// to include the character under the cursor.
|
||||||
|
*selection.end.column_mut() = selection.end.column() + 1;
|
||||||
|
selection.end = map.clip_point(selection.end, Bias::Right);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
copy_selections_content(editor, line_mode, cx);
|
||||||
|
editor.change_selections(None, cx, |s| {
|
||||||
|
s.move_with(|_, selection| {
|
||||||
|
selection.collapse_to(selection.start, SelectionGoal::None)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
vim.switch_mode(Mode::Normal, cx);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -97,7 +168,9 @@ mod test {
|
|||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_enter_visual_mode(cx: &mut gpui::TestAppContext) {
|
async fn test_enter_visual_mode(cx: &mut gpui::TestAppContext) {
|
||||||
let cx = VimTestContext::new(cx, true).await;
|
let cx = VimTestContext::new(cx, true).await;
|
||||||
let mut cx = cx.binding(["v", "w", "j"]).mode_after(Mode::Visual);
|
let mut cx = cx
|
||||||
|
.binding(["v", "w", "j"])
|
||||||
|
.mode_after(Mode::Visual { line: false });
|
||||||
cx.assert(
|
cx.assert(
|
||||||
indoc! {"
|
indoc! {"
|
||||||
The |quick brown
|
The |quick brown
|
||||||
@ -128,7 +201,9 @@ mod test {
|
|||||||
fox jumps [over
|
fox jumps [over
|
||||||
}the lazy dog"},
|
}the lazy dog"},
|
||||||
);
|
);
|
||||||
let mut cx = cx.binding(["v", "b", "k"]).mode_after(Mode::Visual);
|
let mut cx = cx
|
||||||
|
.binding(["v", "b", "k"])
|
||||||
|
.mode_after(Mode::Visual { line: false });
|
||||||
cx.assert(
|
cx.assert(
|
||||||
indoc! {"
|
indoc! {"
|
||||||
The |quick brown
|
The |quick brown
|
||||||
@ -176,6 +251,13 @@ mod test {
|
|||||||
The |ver
|
The |ver
|
||||||
the lazy dog"},
|
the lazy dog"},
|
||||||
);
|
);
|
||||||
|
// Test pasting code copied on delete
|
||||||
|
cx.simulate_keystrokes(["j", "p"]);
|
||||||
|
cx.assert_editor_state(indoc! {"
|
||||||
|
The ver
|
||||||
|
the l|quick brown
|
||||||
|
fox jumps oazy dog"});
|
||||||
|
|
||||||
cx.assert(
|
cx.assert(
|
||||||
indoc! {"
|
indoc! {"
|
||||||
The quick brown
|
The quick brown
|
||||||
@ -226,6 +308,77 @@ mod test {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_visual_line_delete(cx: &mut gpui::TestAppContext) {
|
||||||
|
let cx = VimTestContext::new(cx, true).await;
|
||||||
|
let mut cx = cx.binding(["shift-V", "x"]);
|
||||||
|
cx.assert(
|
||||||
|
indoc! {"
|
||||||
|
The qu|ick brown
|
||||||
|
fox jumps over
|
||||||
|
the lazy dog"},
|
||||||
|
indoc! {"
|
||||||
|
fox ju|mps over
|
||||||
|
the lazy dog"},
|
||||||
|
);
|
||||||
|
// Test pasting code copied on delete
|
||||||
|
cx.simulate_keystroke("p");
|
||||||
|
cx.assert_editor_state(indoc! {"
|
||||||
|
fox jumps over
|
||||||
|
|The quick brown
|
||||||
|
the lazy dog"});
|
||||||
|
|
||||||
|
cx.assert(
|
||||||
|
indoc! {"
|
||||||
|
The quick brown
|
||||||
|
fox ju|mps over
|
||||||
|
the lazy dog"},
|
||||||
|
indoc! {"
|
||||||
|
The quick brown
|
||||||
|
the la|zy dog"},
|
||||||
|
);
|
||||||
|
cx.assert(
|
||||||
|
indoc! {"
|
||||||
|
The quick brown
|
||||||
|
fox jumps over
|
||||||
|
the la|zy dog"},
|
||||||
|
indoc! {"
|
||||||
|
The quick brown
|
||||||
|
fox ju|mps over"},
|
||||||
|
);
|
||||||
|
let mut cx = cx.binding(["shift-V", "j", "x"]);
|
||||||
|
cx.assert(
|
||||||
|
indoc! {"
|
||||||
|
The qu|ick brown
|
||||||
|
fox jumps over
|
||||||
|
the lazy dog"},
|
||||||
|
"the la|zy dog",
|
||||||
|
);
|
||||||
|
// Test pasting code copied on delete
|
||||||
|
cx.simulate_keystroke("p");
|
||||||
|
cx.assert_editor_state(indoc! {"
|
||||||
|
the lazy dog
|
||||||
|
|The quick brown
|
||||||
|
fox jumps over"});
|
||||||
|
|
||||||
|
cx.assert(
|
||||||
|
indoc! {"
|
||||||
|
The quick brown
|
||||||
|
fox ju|mps over
|
||||||
|
the lazy dog"},
|
||||||
|
"The qu|ick brown",
|
||||||
|
);
|
||||||
|
cx.assert(
|
||||||
|
indoc! {"
|
||||||
|
The quick brown
|
||||||
|
fox jumps over
|
||||||
|
the la|zy dog"},
|
||||||
|
indoc! {"
|
||||||
|
The quick brown
|
||||||
|
fox ju|mps over"},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_visual_change(cx: &mut gpui::TestAppContext) {
|
async fn test_visual_change(cx: &mut gpui::TestAppContext) {
|
||||||
let cx = VimTestContext::new(cx, true).await;
|
let cx = VimTestContext::new(cx, true).await;
|
||||||
@ -290,4 +443,168 @@ mod test {
|
|||||||
the lazy dog"},
|
the lazy dog"},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_visual_line_change(cx: &mut gpui::TestAppContext) {
|
||||||
|
let cx = VimTestContext::new(cx, true).await;
|
||||||
|
let mut cx = cx.binding(["shift-V", "c"]).mode_after(Mode::Insert);
|
||||||
|
cx.assert(
|
||||||
|
indoc! {"
|
||||||
|
The qu|ick brown
|
||||||
|
fox jumps over
|
||||||
|
the lazy dog"},
|
||||||
|
indoc! {"
|
||||||
|
|
|
||||||
|
fox jumps over
|
||||||
|
the lazy dog"},
|
||||||
|
);
|
||||||
|
// Test pasting code copied on change
|
||||||
|
cx.simulate_keystrokes(["escape", "j", "p"]);
|
||||||
|
cx.assert_editor_state(indoc! {"
|
||||||
|
|
||||||
|
fox jumps over
|
||||||
|
|The quick brown
|
||||||
|
the lazy dog"});
|
||||||
|
|
||||||
|
cx.assert(
|
||||||
|
indoc! {"
|
||||||
|
The quick brown
|
||||||
|
fox ju|mps over
|
||||||
|
the lazy dog"},
|
||||||
|
indoc! {"
|
||||||
|
The quick brown
|
||||||
|
|
|
||||||
|
the lazy dog"},
|
||||||
|
);
|
||||||
|
cx.assert(
|
||||||
|
indoc! {"
|
||||||
|
The quick brown
|
||||||
|
fox jumps over
|
||||||
|
the la|zy dog"},
|
||||||
|
indoc! {"
|
||||||
|
The quick brown
|
||||||
|
fox jumps over
|
||||||
|
|"},
|
||||||
|
);
|
||||||
|
let mut cx = cx.binding(["shift-V", "j", "c"]).mode_after(Mode::Insert);
|
||||||
|
cx.assert(
|
||||||
|
indoc! {"
|
||||||
|
The qu|ick brown
|
||||||
|
fox jumps over
|
||||||
|
the lazy dog"},
|
||||||
|
indoc! {"
|
||||||
|
|
|
||||||
|
the lazy dog"},
|
||||||
|
);
|
||||||
|
// Test pasting code copied on delete
|
||||||
|
cx.simulate_keystrokes(["escape", "j", "p"]);
|
||||||
|
cx.assert_editor_state(indoc! {"
|
||||||
|
|
||||||
|
the lazy dog
|
||||||
|
|The quick brown
|
||||||
|
fox jumps over"});
|
||||||
|
cx.assert(
|
||||||
|
indoc! {"
|
||||||
|
The quick brown
|
||||||
|
fox ju|mps over
|
||||||
|
the lazy dog"},
|
||||||
|
indoc! {"
|
||||||
|
The quick brown
|
||||||
|
|"},
|
||||||
|
);
|
||||||
|
cx.assert(
|
||||||
|
indoc! {"
|
||||||
|
The quick brown
|
||||||
|
fox jumps over
|
||||||
|
the la|zy dog"},
|
||||||
|
indoc! {"
|
||||||
|
The quick brown
|
||||||
|
fox jumps over
|
||||||
|
|"},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_visual_yank(cx: &mut gpui::TestAppContext) {
|
||||||
|
let cx = VimTestContext::new(cx, true).await;
|
||||||
|
let mut cx = cx.binding(["v", "w", "y"]);
|
||||||
|
cx.assert("The quick |brown", "The quick |brown");
|
||||||
|
cx.assert_clipboard_content(Some("brown"));
|
||||||
|
let mut cx = cx.binding(["v", "w", "j", "y"]);
|
||||||
|
cx.assert(
|
||||||
|
indoc! {"
|
||||||
|
The |quick brown
|
||||||
|
fox jumps over
|
||||||
|
the lazy dog"},
|
||||||
|
indoc! {"
|
||||||
|
The |quick brown
|
||||||
|
fox jumps over
|
||||||
|
the lazy dog"},
|
||||||
|
);
|
||||||
|
cx.assert_clipboard_content(Some(indoc! {"
|
||||||
|
quick brown
|
||||||
|
fox jumps o"}));
|
||||||
|
cx.assert(
|
||||||
|
indoc! {"
|
||||||
|
The quick brown
|
||||||
|
fox jumps over
|
||||||
|
the |lazy dog"},
|
||||||
|
indoc! {"
|
||||||
|
The quick brown
|
||||||
|
fox jumps over
|
||||||
|
the |lazy dog"},
|
||||||
|
);
|
||||||
|
cx.assert_clipboard_content(Some("lazy d"));
|
||||||
|
cx.assert(
|
||||||
|
indoc! {"
|
||||||
|
The quick brown
|
||||||
|
fox jumps |over
|
||||||
|
the lazy dog"},
|
||||||
|
indoc! {"
|
||||||
|
The quick brown
|
||||||
|
fox jumps |over
|
||||||
|
the lazy dog"},
|
||||||
|
);
|
||||||
|
cx.assert_clipboard_content(Some(indoc! {"
|
||||||
|
over
|
||||||
|
t"}));
|
||||||
|
let mut cx = cx.binding(["v", "b", "k", "y"]);
|
||||||
|
cx.assert(
|
||||||
|
indoc! {"
|
||||||
|
The |quick brown
|
||||||
|
fox jumps over
|
||||||
|
the lazy dog"},
|
||||||
|
indoc! {"
|
||||||
|
|The quick brown
|
||||||
|
fox jumps over
|
||||||
|
the lazy dog"},
|
||||||
|
);
|
||||||
|
cx.assert_clipboard_content(Some("The q"));
|
||||||
|
cx.assert(
|
||||||
|
indoc! {"
|
||||||
|
The quick brown
|
||||||
|
fox jumps over
|
||||||
|
the |lazy dog"},
|
||||||
|
indoc! {"
|
||||||
|
The quick brown
|
||||||
|
|fox jumps over
|
||||||
|
the lazy dog"},
|
||||||
|
);
|
||||||
|
cx.assert_clipboard_content(Some(indoc! {"
|
||||||
|
fox jumps over
|
||||||
|
the l"}));
|
||||||
|
cx.assert(
|
||||||
|
indoc! {"
|
||||||
|
The quick brown
|
||||||
|
fox jumps |over
|
||||||
|
the lazy dog"},
|
||||||
|
indoc! {"
|
||||||
|
The |quick brown
|
||||||
|
fox jumps over
|
||||||
|
the lazy dog"},
|
||||||
|
);
|
||||||
|
cx.assert_clipboard_content(Some(indoc! {"
|
||||||
|
quick brown
|
||||||
|
fox jumps o"}));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,7 @@ use gpui::{
|
|||||||
use project::{Project, ProjectEntryId, ProjectPath};
|
use project::{Project, ProjectEntryId, ProjectPath};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
use std::{any::Any, cell::RefCell, cmp, mem, path::Path, rc::Rc};
|
use std::{any::Any, cell::RefCell, mem, path::Path, rc::Rc};
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
|
|
||||||
actions!(
|
actions!(
|
||||||
@ -109,6 +109,7 @@ pub enum Event {
|
|||||||
ActivateItem { local: bool },
|
ActivateItem { local: bool },
|
||||||
Remove,
|
Remove,
|
||||||
Split(SplitDirection),
|
Split(SplitDirection),
|
||||||
|
ChangeItemTitle,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Pane {
|
pub struct Pane {
|
||||||
@ -334,9 +335,20 @@ impl Pane {
|
|||||||
item.set_nav_history(pane.read(cx).nav_history.clone(), cx);
|
item.set_nav_history(pane.read(cx).nav_history.clone(), cx);
|
||||||
item.added_to_pane(workspace, pane.clone(), cx);
|
item.added_to_pane(workspace, pane.clone(), cx);
|
||||||
pane.update(cx, |pane, cx| {
|
pane.update(cx, |pane, cx| {
|
||||||
let item_idx = cmp::min(pane.active_item_index + 1, pane.items.len());
|
// If there is already an active item, then insert the new item
|
||||||
pane.items.insert(item_idx, item);
|
// right after it. Otherwise, adjust the `active_item_index` field
|
||||||
pane.activate_item(item_idx, activate_pane, focus_item, cx);
|
// before activating the new item, so that in the `activate_item`
|
||||||
|
// method, we can detect that the active item is changing.
|
||||||
|
let item_ix;
|
||||||
|
if pane.active_item_index < pane.items.len() {
|
||||||
|
item_ix = pane.active_item_index + 1
|
||||||
|
} else {
|
||||||
|
item_ix = pane.items.len();
|
||||||
|
pane.active_item_index = usize::MAX;
|
||||||
|
};
|
||||||
|
|
||||||
|
pane.items.insert(item_ix, item);
|
||||||
|
pane.activate_item(item_ix, activate_pane, focus_item, cx);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -383,11 +395,12 @@ impl Pane {
|
|||||||
use NavigationMode::{GoingBack, GoingForward};
|
use NavigationMode::{GoingBack, GoingForward};
|
||||||
if index < self.items.len() {
|
if index < self.items.len() {
|
||||||
let prev_active_item_ix = mem::replace(&mut self.active_item_index, index);
|
let prev_active_item_ix = mem::replace(&mut self.active_item_index, index);
|
||||||
if matches!(self.nav_history.borrow().mode, GoingBack | GoingForward)
|
if prev_active_item_ix != self.active_item_index
|
||||||
|| (prev_active_item_ix != self.active_item_index
|
|| matches!(self.nav_history.borrow().mode, GoingBack | GoingForward)
|
||||||
&& prev_active_item_ix < self.items.len())
|
|
||||||
{
|
{
|
||||||
self.items[prev_active_item_ix].deactivated(cx);
|
if let Some(prev_item) = self.items.get(prev_active_item_ix) {
|
||||||
|
prev_item.deactivated(cx);
|
||||||
|
}
|
||||||
cx.emit(Event::ActivateItem {
|
cx.emit(Event::ActivateItem {
|
||||||
local: activate_pane,
|
local: activate_pane,
|
||||||
});
|
});
|
||||||
@ -424,7 +437,7 @@ impl Pane {
|
|||||||
self.activate_item(index, true, true, cx);
|
self.activate_item(index, true, true, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn close_active_item(
|
pub fn close_active_item(
|
||||||
workspace: &mut Workspace,
|
workspace: &mut Workspace,
|
||||||
_: &CloseActiveItem,
|
_: &CloseActiveItem,
|
||||||
cx: &mut ViewContext<Workspace>,
|
cx: &mut ViewContext<Workspace>,
|
||||||
|
@ -37,6 +37,7 @@ use status_bar::StatusBar;
|
|||||||
pub use status_bar::StatusItemView;
|
pub use status_bar::StatusItemView;
|
||||||
use std::{
|
use std::{
|
||||||
any::{Any, TypeId},
|
any::{Any, TypeId},
|
||||||
|
borrow::Cow,
|
||||||
cell::RefCell,
|
cell::RefCell,
|
||||||
fmt,
|
fmt,
|
||||||
future::Future,
|
future::Future,
|
||||||
@ -543,7 +544,10 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if T::should_update_tab_on_event(event) {
|
if T::should_update_tab_on_event(event) {
|
||||||
pane.update(cx, |_, cx| cx.notify());
|
pane.update(cx, |_, cx| {
|
||||||
|
cx.emit(pane::Event::ChangeItemTitle);
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
@ -755,6 +759,9 @@ impl Workspace {
|
|||||||
project::Event::CollaboratorLeft(peer_id) => {
|
project::Event::CollaboratorLeft(peer_id) => {
|
||||||
this.collaborator_left(*peer_id, cx);
|
this.collaborator_left(*peer_id, cx);
|
||||||
}
|
}
|
||||||
|
project::Event::WorktreeRemoved(_) | project::Event::WorktreeAdded => {
|
||||||
|
this.update_window_title(cx);
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
if project.read(cx).is_read_only() {
|
if project.read(cx).is_read_only() {
|
||||||
@ -766,14 +773,8 @@ impl Workspace {
|
|||||||
|
|
||||||
let pane = cx.add_view(|cx| Pane::new(cx));
|
let pane = cx.add_view(|cx| Pane::new(cx));
|
||||||
let pane_id = pane.id();
|
let pane_id = pane.id();
|
||||||
cx.observe(&pane, move |me, _, cx| {
|
cx.subscribe(&pane, move |this, _, event, cx| {
|
||||||
let active_entry = me.active_project_path(cx);
|
this.handle_pane_event(pane_id, event, cx)
|
||||||
me.project
|
|
||||||
.update(cx, |project, cx| project.set_active_path(active_entry, cx));
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
cx.subscribe(&pane, move |me, _, event, cx| {
|
|
||||||
me.handle_pane_event(pane_id, event, cx)
|
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
cx.focus(&pane);
|
cx.focus(&pane);
|
||||||
@ -836,6 +837,11 @@ impl Workspace {
|
|||||||
_observe_current_user,
|
_observe_current_user,
|
||||||
};
|
};
|
||||||
this.project_remote_id_changed(this.project.read(cx).remote_id(), cx);
|
this.project_remote_id_changed(this.project.read(cx).remote_id(), cx);
|
||||||
|
|
||||||
|
cx.defer(|this, cx| {
|
||||||
|
this.update_window_title(cx);
|
||||||
|
});
|
||||||
|
|
||||||
this
|
this
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1258,14 +1264,8 @@ impl Workspace {
|
|||||||
fn add_pane(&mut self, cx: &mut ViewContext<Self>) -> ViewHandle<Pane> {
|
fn add_pane(&mut self, cx: &mut ViewContext<Self>) -> ViewHandle<Pane> {
|
||||||
let pane = cx.add_view(|cx| Pane::new(cx));
|
let pane = cx.add_view(|cx| Pane::new(cx));
|
||||||
let pane_id = pane.id();
|
let pane_id = pane.id();
|
||||||
cx.observe(&pane, move |me, _, cx| {
|
cx.subscribe(&pane, move |this, _, event, cx| {
|
||||||
let active_entry = me.active_project_path(cx);
|
this.handle_pane_event(pane_id, event, cx)
|
||||||
me.project
|
|
||||||
.update(cx, |project, cx| project.set_active_path(active_entry, cx));
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
cx.subscribe(&pane, move |me, _, event, cx| {
|
|
||||||
me.handle_pane_event(pane_id, event, cx)
|
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
self.panes.push(pane.clone());
|
self.panes.push(pane.clone());
|
||||||
@ -1405,6 +1405,7 @@ impl Workspace {
|
|||||||
self.status_bar.update(cx, |status_bar, cx| {
|
self.status_bar.update(cx, |status_bar, cx| {
|
||||||
status_bar.set_active_pane(&self.active_pane, cx);
|
status_bar.set_active_pane(&self.active_pane, cx);
|
||||||
});
|
});
|
||||||
|
self.active_item_path_changed(cx);
|
||||||
cx.focus(&self.active_pane);
|
cx.focus(&self.active_pane);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
@ -1439,6 +1440,14 @@ impl Workspace {
|
|||||||
if *local {
|
if *local {
|
||||||
self.unfollow(&pane, cx);
|
self.unfollow(&pane, cx);
|
||||||
}
|
}
|
||||||
|
if pane == self.active_pane {
|
||||||
|
self.active_item_path_changed(cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pane::Event::ChangeItemTitle => {
|
||||||
|
if pane == self.active_pane {
|
||||||
|
self.active_item_path_changed(cx);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -1471,6 +1480,8 @@ impl Workspace {
|
|||||||
self.unfollow(&pane, cx);
|
self.unfollow(&pane, cx);
|
||||||
self.last_leaders_by_pane.remove(&pane.downgrade());
|
self.last_leaders_by_pane.remove(&pane.downgrade());
|
||||||
cx.notify();
|
cx.notify();
|
||||||
|
} else {
|
||||||
|
self.active_item_path_changed(cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1658,15 +1669,7 @@ impl Workspace {
|
|||||||
|
|
||||||
fn render_titlebar(&self, theme: &Theme, cx: &mut RenderContext<Self>) -> ElementBox {
|
fn render_titlebar(&self, theme: &Theme, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||||
let mut worktree_root_names = String::new();
|
let mut worktree_root_names = String::new();
|
||||||
{
|
self.worktree_root_names(&mut worktree_root_names, cx);
|
||||||
let mut worktrees = self.project.read(cx).visible_worktrees(cx).peekable();
|
|
||||||
while let Some(worktree) = worktrees.next() {
|
|
||||||
worktree_root_names.push_str(worktree.read(cx).root_name());
|
|
||||||
if worktrees.peek().is_some() {
|
|
||||||
worktree_root_names.push_str(", ");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ConstrainedBox::new(
|
ConstrainedBox::new(
|
||||||
Container::new(
|
Container::new(
|
||||||
@ -1702,6 +1705,50 @@ impl Workspace {
|
|||||||
.named("titlebar")
|
.named("titlebar")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn active_item_path_changed(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
|
let active_entry = self.active_project_path(cx);
|
||||||
|
self.project
|
||||||
|
.update(cx, |project, cx| project.set_active_path(active_entry, cx));
|
||||||
|
self.update_window_title(cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_window_title(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
|
let mut title = String::new();
|
||||||
|
if let Some(path) = self.active_item(cx).and_then(|item| item.project_path(cx)) {
|
||||||
|
let filename = path
|
||||||
|
.path
|
||||||
|
.file_name()
|
||||||
|
.map(|s| s.to_string_lossy())
|
||||||
|
.or_else(|| {
|
||||||
|
Some(Cow::Borrowed(
|
||||||
|
self.project()
|
||||||
|
.read(cx)
|
||||||
|
.worktree_for_id(path.worktree_id, cx)?
|
||||||
|
.read(cx)
|
||||||
|
.root_name(),
|
||||||
|
))
|
||||||
|
});
|
||||||
|
if let Some(filename) = filename {
|
||||||
|
title.push_str(filename.as_ref());
|
||||||
|
title.push_str(" — ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.worktree_root_names(&mut title, cx);
|
||||||
|
if title.is_empty() {
|
||||||
|
title = "empty project".to_string();
|
||||||
|
}
|
||||||
|
cx.set_window_title(&title);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn worktree_root_names(&self, string: &mut String, cx: &mut MutableAppContext) {
|
||||||
|
for (i, worktree) in self.project.read(cx).visible_worktrees(cx).enumerate() {
|
||||||
|
if i != 0 {
|
||||||
|
string.push_str(", ");
|
||||||
|
}
|
||||||
|
string.push_str(worktree.read(cx).root_name());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn render_collaborators(&self, theme: &Theme, cx: &mut RenderContext<Self>) -> Vec<ElementBox> {
|
fn render_collaborators(&self, theme: &Theme, cx: &mut RenderContext<Self>) -> Vec<ElementBox> {
|
||||||
let mut collaborators = self
|
let mut collaborators = self
|
||||||
.project
|
.project
|
||||||
@ -2437,6 +2484,110 @@ mod tests {
|
|||||||
use project::{FakeFs, Project, ProjectEntryId};
|
use project::{FakeFs, Project, ProjectEntryId};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_tracking_active_path(cx: &mut TestAppContext) {
|
||||||
|
cx.foreground().forbid_parking();
|
||||||
|
Settings::test_async(cx);
|
||||||
|
let fs = FakeFs::new(cx.background());
|
||||||
|
fs.insert_tree(
|
||||||
|
"/root1",
|
||||||
|
json!({
|
||||||
|
"one.txt": "",
|
||||||
|
"two.txt": "",
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
fs.insert_tree(
|
||||||
|
"/root2",
|
||||||
|
json!({
|
||||||
|
"three.txt": "",
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let project = Project::test(fs, ["root1".as_ref()], cx).await;
|
||||||
|
let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx));
|
||||||
|
let worktree_id = project.read_with(cx, |project, cx| {
|
||||||
|
project.worktrees(cx).next().unwrap().read(cx).id()
|
||||||
|
});
|
||||||
|
|
||||||
|
let item1 = cx.add_view(window_id, |_| {
|
||||||
|
let mut item = TestItem::new();
|
||||||
|
item.project_path = Some((worktree_id, "one.txt").into());
|
||||||
|
item
|
||||||
|
});
|
||||||
|
let item2 = cx.add_view(window_id, |_| {
|
||||||
|
let mut item = TestItem::new();
|
||||||
|
item.project_path = Some((worktree_id, "two.txt").into());
|
||||||
|
item
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add an item to an empty pane
|
||||||
|
workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item1), cx));
|
||||||
|
project.read_with(cx, |project, cx| {
|
||||||
|
assert_eq!(
|
||||||
|
project.active_entry(),
|
||||||
|
project.entry_for_path(&(worktree_id, "one.txt").into(), cx)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
assert_eq!(
|
||||||
|
cx.current_window_title(window_id).as_deref(),
|
||||||
|
Some("one.txt — root1")
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add a second item to a non-empty pane
|
||||||
|
workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item2), cx));
|
||||||
|
assert_eq!(
|
||||||
|
cx.current_window_title(window_id).as_deref(),
|
||||||
|
Some("two.txt — root1")
|
||||||
|
);
|
||||||
|
project.read_with(cx, |project, cx| {
|
||||||
|
assert_eq!(
|
||||||
|
project.active_entry(),
|
||||||
|
project.entry_for_path(&(worktree_id, "two.txt").into(), cx)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close the active item
|
||||||
|
workspace
|
||||||
|
.update(cx, |workspace, cx| {
|
||||||
|
Pane::close_active_item(workspace, &Default::default(), cx).unwrap()
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
cx.current_window_title(window_id).as_deref(),
|
||||||
|
Some("one.txt — root1")
|
||||||
|
);
|
||||||
|
project.read_with(cx, |project, cx| {
|
||||||
|
assert_eq!(
|
||||||
|
project.active_entry(),
|
||||||
|
project.entry_for_path(&(worktree_id, "one.txt").into(), cx)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add a project folder
|
||||||
|
project
|
||||||
|
.update(cx, |project, cx| {
|
||||||
|
project.find_or_create_local_worktree("/root2", true, cx)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
cx.current_window_title(window_id).as_deref(),
|
||||||
|
Some("one.txt — root1, root2")
|
||||||
|
);
|
||||||
|
|
||||||
|
// Remove a project folder
|
||||||
|
project.update(cx, |project, cx| {
|
||||||
|
project.remove_worktree(worktree_id, cx);
|
||||||
|
});
|
||||||
|
assert_eq!(
|
||||||
|
cx.current_window_title(window_id).as_deref(),
|
||||||
|
Some("one.txt — root2")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_close_window(cx: &mut TestAppContext) {
|
async fn test_close_window(cx: &mut TestAppContext) {
|
||||||
cx.foreground().forbid_parking();
|
cx.foreground().forbid_parking();
|
||||||
@ -2476,18 +2627,6 @@ mod tests {
|
|||||||
cx.foreground().run_until_parked();
|
cx.foreground().run_until_parked();
|
||||||
assert!(!cx.has_pending_prompt(window_id));
|
assert!(!cx.has_pending_prompt(window_id));
|
||||||
assert_eq!(task.await.unwrap(), false);
|
assert_eq!(task.await.unwrap(), false);
|
||||||
|
|
||||||
// If there are multiple dirty items representing the same project entry.
|
|
||||||
workspace.update(cx, |w, cx| {
|
|
||||||
w.add_item(Box::new(item2.clone()), cx);
|
|
||||||
w.add_item(Box::new(item3.clone()), cx);
|
|
||||||
});
|
|
||||||
let task = workspace.update(cx, |w, cx| w.prepare_to_close(cx));
|
|
||||||
cx.foreground().run_until_parked();
|
|
||||||
cx.simulate_prompt_answer(window_id, 2 /* cancel */);
|
|
||||||
cx.foreground().run_until_parked();
|
|
||||||
assert!(!cx.has_pending_prompt(window_id));
|
|
||||||
assert_eq!(task.await.unwrap(), false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
@ -2687,6 +2826,7 @@ mod tests {
|
|||||||
is_dirty: bool,
|
is_dirty: bool,
|
||||||
has_conflict: bool,
|
has_conflict: bool,
|
||||||
project_entry_ids: Vec<ProjectEntryId>,
|
project_entry_ids: Vec<ProjectEntryId>,
|
||||||
|
project_path: Option<ProjectPath>,
|
||||||
is_singleton: bool,
|
is_singleton: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2699,6 +2839,7 @@ mod tests {
|
|||||||
is_dirty: false,
|
is_dirty: false,
|
||||||
has_conflict: false,
|
has_conflict: false,
|
||||||
project_entry_ids: Vec::new(),
|
project_entry_ids: Vec::new(),
|
||||||
|
project_path: None,
|
||||||
is_singleton: true,
|
is_singleton: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2724,7 +2865,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn project_path(&self, _: &AppContext) -> Option<ProjectPath> {
|
fn project_path(&self, _: &AppContext) -> Option<ProjectPath> {
|
||||||
None
|
self.project_path.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn project_entry_ids(&self, _: &AppContext) -> SmallVec<[ProjectEntryId; 3]> {
|
fn project_entry_ids(&self, _: &AppContext) -> SmallVec<[ProjectEntryId; 3]> {
|
||||||
@ -2783,5 +2924,9 @@ mod tests {
|
|||||||
self.reload_count += 1;
|
self.reload_count += 1;
|
||||||
Task::ready(Ok(()))
|
Task::ready(Ok(()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn should_update_tab_on_event(_: &Self::Event) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -180,8 +180,8 @@ fn main() {
|
|||||||
|
|
||||||
cx.observe_global::<Settings, _>({
|
cx.observe_global::<Settings, _>({
|
||||||
let languages = languages.clone();
|
let languages = languages.clone();
|
||||||
move |settings, _| {
|
move |cx| {
|
||||||
languages.set_theme(&settings.theme.editor.syntax);
|
languages.set_theme(&cx.global::<Settings>().theme.editor.syntax);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
|
@ -15,6 +15,14 @@ pub fn menus() -> Vec<Menu<'static>> {
|
|||||||
action: Box::new(auto_update::Check),
|
action: Box::new(auto_update::Check),
|
||||||
},
|
},
|
||||||
MenuItem::Separator,
|
MenuItem::Separator,
|
||||||
|
MenuItem::Action {
|
||||||
|
name: "Open Settings",
|
||||||
|
action: Box::new(super::OpenSettings),
|
||||||
|
},
|
||||||
|
MenuItem::Action {
|
||||||
|
name: "Open Key Bindings",
|
||||||
|
action: Box::new(super::OpenKeymap),
|
||||||
|
},
|
||||||
MenuItem::Action {
|
MenuItem::Action {
|
||||||
name: "Install CLI",
|
name: "Install CLI",
|
||||||
action: Box::new(super::InstallCommandLineInterface),
|
action: Box::new(super::InstallCommandLineInterface),
|
||||||
@ -164,6 +172,10 @@ pub fn menus() -> Vec<Menu<'static>> {
|
|||||||
name: "Zoom Out",
|
name: "Zoom Out",
|
||||||
action: Box::new(super::DecreaseBufferFontSize),
|
action: Box::new(super::DecreaseBufferFontSize),
|
||||||
},
|
},
|
||||||
|
MenuItem::Action {
|
||||||
|
name: "Reset Zoom",
|
||||||
|
action: Box::new(super::ResetBufferFontSize),
|
||||||
|
},
|
||||||
MenuItem::Separator,
|
MenuItem::Separator,
|
||||||
MenuItem::Action {
|
MenuItem::Action {
|
||||||
name: "Project Browser",
|
name: "Project Browser",
|
||||||
@ -229,12 +241,31 @@ pub fn menus() -> Vec<Menu<'static>> {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
Menu {
|
||||||
|
name: "Window",
|
||||||
|
items: vec![MenuItem::Separator],
|
||||||
|
},
|
||||||
Menu {
|
Menu {
|
||||||
name: "Help",
|
name: "Help",
|
||||||
items: vec![MenuItem::Action {
|
items: vec![
|
||||||
name: "Command Palette",
|
MenuItem::Action {
|
||||||
action: Box::new(command_palette::Toggle),
|
name: "Command Palette",
|
||||||
}],
|
action: Box::new(command_palette::Toggle),
|
||||||
|
},
|
||||||
|
MenuItem::Separator,
|
||||||
|
MenuItem::Action {
|
||||||
|
name: "Zed.dev",
|
||||||
|
action: Box::new(crate::OpenBrowser {
|
||||||
|
url: "https://zed.dev".into(),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
MenuItem::Action {
|
||||||
|
name: "Zed Twitter",
|
||||||
|
action: Box::new(crate::OpenBrowser {
|
||||||
|
url: "https://twitter.com/zeddotdev".into(),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,7 @@ use editor::Editor;
|
|||||||
use gpui::{
|
use gpui::{
|
||||||
actions,
|
actions,
|
||||||
geometry::vector::vec2f,
|
geometry::vector::vec2f,
|
||||||
|
impl_actions,
|
||||||
platform::{WindowBounds, WindowOptions},
|
platform::{WindowBounds, WindowOptions},
|
||||||
AsyncAppContext, ViewContext,
|
AsyncAppContext, ViewContext,
|
||||||
};
|
};
|
||||||
@ -23,6 +24,7 @@ use project::Project;
|
|||||||
pub use project::{self, fs};
|
pub use project::{self, fs};
|
||||||
use project_panel::ProjectPanel;
|
use project_panel::ProjectPanel;
|
||||||
use search::{BufferSearchBar, ProjectSearchBar};
|
use search::{BufferSearchBar, ProjectSearchBar};
|
||||||
|
use serde::Deserialize;
|
||||||
use serde_json::to_string_pretty;
|
use serde_json::to_string_pretty;
|
||||||
use settings::{keymap_file_json_schema, settings_file_json_schema, Settings};
|
use settings::{keymap_file_json_schema, settings_file_json_schema, Settings};
|
||||||
use std::{
|
use std::{
|
||||||
@ -33,6 +35,13 @@ use util::ResultExt;
|
|||||||
pub use workspace;
|
pub use workspace;
|
||||||
use workspace::{AppState, Workspace};
|
use workspace::{AppState, Workspace};
|
||||||
|
|
||||||
|
#[derive(Deserialize, Clone)]
|
||||||
|
struct OpenBrowser {
|
||||||
|
url: Arc<str>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl_actions!(zed, [OpenBrowser]);
|
||||||
|
|
||||||
actions!(
|
actions!(
|
||||||
zed,
|
zed,
|
||||||
[
|
[
|
||||||
@ -43,6 +52,7 @@ actions!(
|
|||||||
OpenKeymap,
|
OpenKeymap,
|
||||||
IncreaseBufferFontSize,
|
IncreaseBufferFontSize,
|
||||||
DecreaseBufferFontSize,
|
DecreaseBufferFontSize,
|
||||||
|
ResetBufferFontSize,
|
||||||
InstallCommandLineInterface,
|
InstallCommandLineInterface,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
@ -60,6 +70,7 @@ lazy_static! {
|
|||||||
pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
|
pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
|
||||||
cx.add_action(about);
|
cx.add_action(about);
|
||||||
cx.add_global_action(quit);
|
cx.add_global_action(quit);
|
||||||
|
cx.add_global_action(move |action: &OpenBrowser, cx| cx.platform().open_url(&action.url));
|
||||||
cx.add_global_action(move |_: &IncreaseBufferFontSize, cx| {
|
cx.add_global_action(move |_: &IncreaseBufferFontSize, cx| {
|
||||||
cx.update_global::<Settings, _, _>(|settings, cx| {
|
cx.update_global::<Settings, _, _>(|settings, cx| {
|
||||||
settings.buffer_font_size = (settings.buffer_font_size + 1.0).max(MIN_FONT_SIZE);
|
settings.buffer_font_size = (settings.buffer_font_size + 1.0).max(MIN_FONT_SIZE);
|
||||||
@ -72,6 +83,12 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
|
|||||||
cx.refresh_windows();
|
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;
|
||||||
|
cx.refresh_windows();
|
||||||
|
});
|
||||||
|
});
|
||||||
cx.add_global_action(move |_: &InstallCommandLineInterface, cx| {
|
cx.add_global_action(move |_: &InstallCommandLineInterface, cx| {
|
||||||
cx.spawn(|cx| async move { install_cli(&cx).await.context("error creating CLI symlink") })
|
cx.spawn(|cx| async move { install_cli(&cx).await.context("error creating CLI symlink") })
|
||||||
.detach_and_log_err(cx);
|
.detach_and_log_err(cx);
|
||||||
|
Loading…
Reference in New Issue
Block a user