Compare commits

..

14 Commits

Author SHA1 Message Date
Conrad Irwin
0a6f97a19c Simplify implementation 2025-01-07 09:17:25 -07:00
0x2CA
4737458b2f add g shift-j test 2025-01-07 13:27:20 +08:00
0x2CA
eabefea234 fix editor command 2025-01-07 13:07:36 +08:00
0x2CA
b6280f01dc fix 2024-12-30 14:07:08 +08:00
0x2CA
031774cc61 add remove_indent 2024-12-30 12:32:22 +08:00
0x2CA
0e95ea8888 fix clippy 2024-12-30 10:34:37 +08:00
0x2CA
b045b33743 separator 2024-12-30 10:24:19 +08:00
张小白
95911aaa14 windows: Fix crashing when minimizing a window on Windows 11 (#22414)
Closes #22366

This PR fixes the issue of crashing when minimizing a window on Windows
11. And this PR supersedes #22366.

The main change in this PR is to stop rendering the window when it is
minimized. Additionally, I’ve made some modifications to the code in
#21756 to improve clarity (I think...).

cc @mgsloan 

Release Notes:

- N/A
2024-12-25 09:52:19 +00:00
Piotr Osiewicz
1a9f0a647a chore: Remove explicit usages of once_cell in favor of std (#22407)
Closes #ISSUE

Release Notes:

- N/A
2024-12-25 00:33:26 +00:00
Agus Zubiaga
45c714110e Sticky multibuffer headers (#22391)
https://github.com/user-attachments/assets/92cc5ff7-d8be-4e4b-ac6e-68eb310fffce

Release Notes:

- Multibuffer headers will now stick to the top of the viewport as you
scroll
- Added support for expanding diagnostic excerpts

---------

Co-authored-by: Michael <michael@zed.dev>
2024-12-24 17:55:25 +00:00
Marshall Bowers
4c84600630 assistant2: Derive the Context icon at render time instead of storing (#22397)
This PR is a follow up to #22385 that makes it so we treat the icon as
derived state.

Release Notes:

- N/A
2024-12-24 16:13:35 +00:00
Danilo Leal
564936e1fe assistant2: Add stray visual adjusments (#22386)
This PR adds just some tiny visual clean ups to the assistant2 panel.
Nothing major, honestly.

<img width="800" alt="Screenshot 2024-12-24 at 12 19 46 AM"
src="https://github.com/user-attachments/assets/da22aa7f-8a42-4ff0-9e4c-5e8b60b28802"
/>

Release Notes:

- N/A
2024-12-24 04:26:13 +00:00
Danilo Leal
3d4e0780c4 assistant2: Add icons to the context pill (#22385)
This PR adds an icon field to the `ContextKind` enum, which means that
icons will now display on context pills, both on the message editor and
on the active thread.

<img width="800" alt="Screenshot 2024-12-24 at 12 23 17 AM"
src="https://github.com/user-attachments/assets/f00e540b-30fe-49ac-b3df-7c7a5dc86d65"
/>

Release Notes:

- N/A
2024-12-24 04:26:09 +00:00
Danilo Leal
44a46e3713 assistant2: Don't render the context space if there's none (#22383)
Note the extra bottom space on the before version. That was because,
previously, the container that holds the context pills in an active
thread was being rendered even if there was no attached context.

| Before | After |
|--------|--------|
| <img width="1577" alt="Screenshot 2024-12-23 at 8 42 00 PM"
src="https://github.com/user-attachments/assets/b74bdc67-0a08-4d59-b1ec-43a00a59a373"
/> | <img width="1577" alt="Screenshot 2024-12-23 at 8 39 00 PM"
src="https://github.com/user-attachments/assets/1cbc340d-19df-4cce-8c0b-b671703a0ff5"
/> |

Release Notes:

- N/A
2024-12-24 04:26:04 +00:00
28 changed files with 492 additions and 340 deletions

3
Cargo.lock generated
View File

@@ -2484,7 +2484,6 @@ dependencies = [
"exec",
"fork",
"ipc-channel",
"once_cell",
"parking_lot",
"paths",
"plist",
@@ -2511,7 +2510,6 @@ dependencies = [
"gpui",
"http_client",
"log",
"once_cell",
"parking_lot",
"paths",
"postage",
@@ -10266,7 +10264,6 @@ name = "release_channel"
version = "0.1.0"
dependencies = [
"gpui",
"once_cell",
]
[[package]]

View File

@@ -408,7 +408,6 @@ nanoid = "0.4"
nbformat = { version = "0.9.0" }
nix = "0.29"
num-format = "0.4.4"
once_cell = "1.19.0"
ordered-float = "2.1.1"
palette = { version = "0.7.5", default-features = false, features = ["std"] }
parking_lot = "0.12.1"

View File

@@ -197,6 +197,7 @@
"d": ["vim::PushOperator", "Delete"],
"shift-d": "vim::DeleteToEndOfLine",
"shift-j": "vim::JoinLines",
"g shift-j": "vim::JoinLinesNoWhitespace",
"y": ["vim::PushOperator", "Yank"],
"shift-y": "vim::YankLine",
"i": "vim::InsertBefore",
@@ -278,6 +279,7 @@
"g shift-i": "vim::VisualInsertFirstNonWhiteSpace",
"g shift-a": "vim::VisualInsertEndOfLine",
"shift-j": "vim::JoinLines",
"g shift-j": "vim::JoinLinesNoWhitespace",
"r": ["vim::PushOperator", "Replace"],
"ctrl-c": ["vim::SwitchMode", "Normal"],
"escape": ["vim::SwitchMode", "Normal"],
@@ -389,9 +391,6 @@
"bindings": {
"w": "vim::Word",
"shift-w": ["vim::Word", { "ignorePunctuation": true }],
// Subword TextObject
// "w": "vim::Subword",
// "shift-w": ["vim::Subword", { "ignorePunctuation": true }],
"t": "vim::Tag",
"s": "vim::Sentence",
"p": "vim::Paragraph",

View File

@@ -3,9 +3,9 @@ use std::sync::Arc;
use assistant_tool::ToolWorkingSet;
use collections::HashMap;
use gpui::{
list, AbsoluteLength, AnyElement, AppContext, CornersRefinement, DefiniteLength,
EdgesRefinement, Empty, Length, ListAlignment, ListState, Model, StyleRefinement, Subscription,
TextStyleRefinement, View, WeakView,
list, AbsoluteLength, AnyElement, AppContext, DefiniteLength, EdgesRefinement, Empty, Length,
ListAlignment, ListState, Model, StyleRefinement, Subscription, TextStyleRefinement, View,
WeakView,
};
use language::LanguageRegistry;
use language_model::Role;
@@ -108,7 +108,7 @@ impl ActiveThread {
selection_background_color: cx.theme().players().local().selection,
code_block: StyleRefinement {
margin: EdgesRefinement {
top: Some(Length::Definite(rems(0.5).into())),
top: Some(Length::Definite(rems(1.0).into())),
left: Some(Length::Definite(rems(0.).into())),
right: Some(Length::Definite(rems(0.).into())),
bottom: Some(Length::Definite(rems(1.).into())),
@@ -120,19 +120,13 @@ impl ActiveThread {
bottom: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
},
background: Some(colors.editor_foreground.opacity(0.01).into()),
border_color: Some(colors.border_variant.opacity(0.25)),
border_color: Some(colors.border_variant.opacity(0.3)),
border_widths: EdgesRefinement {
top: Some(AbsoluteLength::Pixels(Pixels(1.0))),
left: Some(AbsoluteLength::Pixels(Pixels(1.))),
right: Some(AbsoluteLength::Pixels(Pixels(1.))),
bottom: Some(AbsoluteLength::Pixels(Pixels(1.))),
},
corner_radii: CornersRefinement {
top_left: Some(AbsoluteLength::Pixels(Pixels(2.))),
top_right: Some(AbsoluteLength::Pixels(Pixels(2.))),
bottom_right: Some(AbsoluteLength::Pixels(Pixels(2.))),
bottom_left: Some(AbsoluteLength::Pixels(Pixels(2.))),
},
text: Some(TextStyleRefinement {
font_family: Some(theme_settings.buffer_font.family.clone()),
font_size: Some(buffer_font_size.into()),
@@ -272,15 +266,20 @@ impl ActiveThread {
),
),
)
.child(v_flex().p_2p5().text_ui(cx).child(markdown.clone()))
.child(div().p_2p5().text_ui(cx).child(markdown.clone()))
.when_some(context, |parent, context| {
parent.child(
h_flex().flex_wrap().gap_1().p_1p5().children(
context
.iter()
.map(|context| ContextPill::new(context.clone())),
),
)
if !context.is_empty() {
parent.child(
h_flex()
.flex_wrap()
.gap_1()
.px_1p5()
.pb_1p5()
.children(context.iter().map(|c| ContextPill::new(c.clone()))),
)
} else {
parent
}
}),
)
.into_any()

View File

@@ -21,7 +21,7 @@ pub struct Context {
pub text: SharedString,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ContextKind {
File,
Directory,

View File

@@ -220,67 +220,73 @@ impl Render for MessageEditor {
.p_2()
.bg(bg_color)
.child(self.context_strip.clone())
.child({
let settings = ThemeSettings::get_global(cx);
let text_style = TextStyle {
color: cx.theme().colors().editor_foreground,
font_family: settings.ui_font.family.clone(),
font_features: settings.ui_font.features.clone(),
font_size: font_size.into(),
font_weight: settings.ui_font.weight,
line_height: line_height.into(),
..Default::default()
};
.child(
v_flex()
.gap_4()
.child({
let settings = ThemeSettings::get_global(cx);
let text_style = TextStyle {
color: cx.theme().colors().text,
font_family: settings.ui_font.family.clone(),
font_features: settings.ui_font.features.clone(),
font_size: font_size.into(),
font_weight: settings.ui_font.weight,
line_height: line_height.into(),
..Default::default()
};
EditorElement::new(
&self.editor,
EditorStyle {
background: bg_color,
local_player: cx.theme().players().local(),
text: text_style,
..Default::default()
},
)
})
.child(
PopoverMenu::new("inline-context-picker")
.menu(move |_cx| Some(inline_context_picker.clone()))
.attach(gpui::Corner::TopLeft)
.anchor(gpui::Corner::BottomLeft)
.offset(gpui::Point {
x: px(0.0),
y: px(-16.0),
EditorElement::new(
&self.editor,
EditorStyle {
background: bg_color,
local_player: cx.theme().players().local(),
text: text_style,
..Default::default()
},
)
})
.with_handle(self.inline_context_picker_menu_handle.clone()),
)
.child(
h_flex()
.justify_between()
.child(SwitchWithLabel::new(
"use-tools",
Label::new("Tools"),
self.use_tools.into(),
cx.listener(|this, selection, _cx| {
this.use_tools = match selection {
ToggleState::Selected => true,
ToggleState::Unselected | ToggleState::Indeterminate => false,
};
}),
))
.child(
h_flex().gap_1().child(self.model_selector.clone()).child(
ButtonLike::new("chat")
.style(ButtonStyle::Filled)
.layer(ElevationIndex::ModalSurface)
.child(Label::new("Submit"))
.children(
KeyBinding::for_action_in(&Chat, &focus_handle, cx)
.map(|binding| binding.into_any_element()),
)
.on_click(move |_event, cx| {
focus_handle.dispatch_action(&Chat, cx);
PopoverMenu::new("inline-context-picker")
.menu(move |_cx| Some(inline_context_picker.clone()))
.attach(gpui::Corner::TopLeft)
.anchor(gpui::Corner::BottomLeft)
.offset(gpui::Point {
x: px(0.0),
y: px(-16.0),
})
.with_handle(self.inline_context_picker_menu_handle.clone()),
)
.child(
h_flex()
.justify_between()
.child(SwitchWithLabel::new(
"use-tools",
Label::new("Tools"),
self.use_tools.into(),
cx.listener(|this, selection, _cx| {
this.use_tools = match selection {
ToggleState::Selected => true,
ToggleState::Unselected | ToggleState::Indeterminate => {
false
}
};
}),
),
))
.child(
h_flex().gap_1().child(self.model_selector.clone()).child(
ButtonLike::new("chat")
.style(ButtonStyle::Filled)
.layer(ElevationIndex::ModalSurface)
.child(Label::new("Submit"))
.children(
KeyBinding::for_action_in(&Chat, &focus_handle, cx)
.map(|binding| binding.into_any_element()),
)
.on_click(move |_event, cx| {
focus_handle.dispatch_action(&Chat, cx);
}),
),
),
),
)
}

View File

@@ -3,7 +3,7 @@ use std::rc::Rc;
use gpui::ClickEvent;
use ui::{prelude::*, IconButtonShape};
use crate::context::Context;
use crate::context::{Context, ContextKind};
#[derive(IntoElement)]
pub struct ContextPill {
@@ -27,15 +27,28 @@ impl ContextPill {
impl RenderOnce for ContextPill {
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
let padding_right = if self.on_remove.is_some() {
px(2.)
} else {
px(4.)
};
let icon = match self.context.kind {
ContextKind::File => IconName::File,
ContextKind::Directory => IconName::Folder,
ContextKind::FetchedUrl => IconName::Globe,
ContextKind::Thread => IconName::MessageCircle,
};
h_flex()
.gap_1()
.pl_1p5()
.pr_0p5()
.pl_1()
.pr(padding_right)
.pb(px(1.))
.border_1()
.border_color(cx.theme().colors().border.opacity(0.5))
.bg(cx.theme().colors().element_background)
.rounded_md()
.child(Icon::new(icon).size(IconSize::XSmall).color(Color::Muted))
.child(Label::new(self.context.name.clone()).size(LabelSize::Small))
.when_some(self.on_remove, |parent, on_remove| {
parent.child(

View File

@@ -25,7 +25,6 @@ anyhow.workspace = true
clap.workspace = true
collections.workspace = true
ipc-channel = "0.19"
once_cell.workspace = true
parking_lot.workspace = true
paths.workspace = true
release_channel.workspace = true

View File

@@ -277,6 +277,7 @@ mod linux {
os::unix::net::{SocketAddr, UnixDatagram},
path::{Path, PathBuf},
process::{self, ExitStatus},
sync::LazyLock,
thread,
time::Duration,
};
@@ -284,12 +285,11 @@ mod linux {
use anyhow::anyhow;
use cli::FORCE_CLI_MODE_ENV_VAR_NAME;
use fork::Fork;
use once_cell::sync::Lazy;
use crate::{Detect, InstalledApp};
static RELEASE_CHANNEL: Lazy<String> =
Lazy::new(|| include_str!("../../zed/RELEASE_CHANNEL").trim().to_string());
static RELEASE_CHANNEL: LazyLock<String> =
LazyLock::new(|| include_str!("../../zed/RELEASE_CHANNEL").trim().to_string());
struct App(PathBuf);

View File

@@ -27,7 +27,6 @@ futures.workspace = true
gpui.workspace = true
http_client.workspace = true
log.workspace = true
once_cell.workspace = true
paths.workspace = true
parking_lot.workspace = true
postage.workspace = true

View File

@@ -8,7 +8,6 @@ use futures::channel::mpsc;
use futures::{Future, StreamExt};
use gpui::{AppContext, BackgroundExecutor, Task};
use http_client::{self, AsyncBody, HttpClient, HttpClientWithUrl, Method, Request};
use once_cell::sync::Lazy;
use parking_lot::Mutex;
use release_channel::ReleaseChannel;
use settings::{Settings, SettingsStore};
@@ -16,7 +15,12 @@ use sha2::{Digest, Sha256};
use std::fs::File;
use std::io::Write;
use std::time::Instant;
use std::{env, mem, path::PathBuf, sync::Arc, time::Duration};
use std::{
env, mem,
path::PathBuf,
sync::{Arc, LazyLock},
time::Duration,
};
use telemetry_events::{
AppEvent, AssistantEvent, CallEvent, EditEvent, Event, EventRequestBody, EventWrapper,
InlineCompletionEvent,
@@ -84,7 +88,7 @@ const FLUSH_INTERVAL: Duration = Duration::from_secs(1);
#[cfg(not(debug_assertions))]
const FLUSH_INTERVAL: Duration = Duration::from_secs(60 * 5);
static ZED_CLIENT_CHECKSUM_SEED: Lazy<Option<Vec<u8>>> = Lazy::new(|| {
static ZED_CLIENT_CHECKSUM_SEED: LazyLock<Option<Vec<u8>>> = LazyLock::new(|| {
option_env!("ZED_CLIENT_CHECKSUM_SEED")
.map(|s| s.as_bytes().into())
.or_else(|| {

View File

@@ -166,7 +166,7 @@ impl ProjectDiagnosticsEditor {
let excerpts = cx.new_model(|cx| MultiBuffer::new(project_handle.read(cx).capability()));
let editor = cx.new_view(|cx| {
let mut editor =
Editor::for_multibuffer(excerpts.clone(), Some(project_handle.clone()), false, cx);
Editor::for_multibuffer(excerpts.clone(), Some(project_handle.clone()), true, cx);
editor.set_vertical_scroll_margin(5, cx);
editor
});

View File

@@ -167,10 +167,10 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
editor_blocks(&editor, cx),
[
(DisplayRow(0), FILE_HEADER.into()),
(DisplayRow(2), DIAGNOSTIC_HEADER.into()),
(DisplayRow(15), EXCERPT_HEADER.into()),
(DisplayRow(16), DIAGNOSTIC_HEADER.into()),
(DisplayRow(25), EXCERPT_HEADER.into()),
(DisplayRow(3), DIAGNOSTIC_HEADER.into()),
(DisplayRow(16), EXCERPT_HEADER.into()),
(DisplayRow(18), DIAGNOSTIC_HEADER.into()),
(DisplayRow(27), EXCERPT_HEADER.into()),
]
);
assert_eq!(
@@ -184,6 +184,7 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
// diagnostic group 1
"\n", // primary message
"\n", // padding
"\n", // expand
" let x = vec![];\n",
" let y = vec![];\n",
"\n", // supporting diagnostic
@@ -195,6 +196,7 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
" c(y);\n",
"\n", // supporting diagnostic
" d(x);\n",
"\n", // expand
"\n", // context ellipsis
// diagnostic group 2
"\n", // primary message
@@ -206,11 +208,13 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
" a(x);\n",
"\n", // supporting diagnostic
" b(y);\n",
"\n", // expand
"\n", // context ellipsis
" c(y);\n",
" d(x);\n",
"\n", // supporting diagnostic
"}"
"}",
"\n", // expand
)
);
@@ -218,7 +222,7 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
editor.update(cx, |editor, cx| {
assert_eq!(
editor.selections.display_ranges(cx),
[DisplayPoint::new(DisplayRow(12), 6)..DisplayPoint::new(DisplayRow(12), 6)]
[DisplayPoint::new(DisplayRow(13), 6)..DisplayPoint::new(DisplayRow(13), 6)]
);
});
@@ -253,12 +257,12 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
editor_blocks(&editor, cx),
[
(DisplayRow(0), FILE_HEADER.into()),
(DisplayRow(2), DIAGNOSTIC_HEADER.into()),
(DisplayRow(7), FILE_HEADER.into()),
(DisplayRow(9), DIAGNOSTIC_HEADER.into()),
(DisplayRow(22), EXCERPT_HEADER.into()),
(DisplayRow(23), DIAGNOSTIC_HEADER.into()),
(DisplayRow(32), EXCERPT_HEADER.into()),
(DisplayRow(3), DIAGNOSTIC_HEADER.into()),
(DisplayRow(8), FILE_HEADER.into()),
(DisplayRow(12), DIAGNOSTIC_HEADER.into()),
(DisplayRow(25), EXCERPT_HEADER.into()),
(DisplayRow(27), DIAGNOSTIC_HEADER.into()),
(DisplayRow(36), EXCERPT_HEADER.into()),
]
);
@@ -273,6 +277,7 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
// diagnostic group 1
"\n", // primary message
"\n", // padding
"\n", // expand
"const a: i32 = 'a';\n",
"\n", // supporting diagnostic
"const b: i32 = c;\n",
@@ -284,6 +289,8 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
// diagnostic group 1
"\n", // primary message
"\n", // padding
"\n", // expand
"\n", // expand
" let x = vec![];\n",
" let y = vec![];\n",
"\n", // supporting diagnostic
@@ -299,6 +306,7 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
// diagnostic group 2
"\n", // primary message
"\n", // filename
"\n", // expand
"fn main() {\n",
" let x = vec![];\n",
"\n", // supporting diagnostic
@@ -306,11 +314,13 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
" a(x);\n",
"\n", // supporting diagnostic
" b(y);\n",
"\n", // expand
"\n", // context ellipsis
" c(y);\n",
" d(x);\n",
"\n", // supporting diagnostic
"}"
"}",
"\n", // expand
)
);
@@ -318,7 +328,7 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
editor.update(cx, |editor, cx| {
assert_eq!(
editor.selections.display_ranges(cx),
[DisplayPoint::new(DisplayRow(19), 6)..DisplayPoint::new(DisplayRow(19), 6)]
[DisplayPoint::new(DisplayRow(22), 6)..DisplayPoint::new(DisplayRow(22), 6)]
);
});
@@ -366,14 +376,14 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
editor_blocks(&editor, cx),
[
(DisplayRow(0), FILE_HEADER.into()),
(DisplayRow(2), DIAGNOSTIC_HEADER.into()),
(DisplayRow(7), EXCERPT_HEADER.into()),
(DisplayRow(8), DIAGNOSTIC_HEADER.into()),
(DisplayRow(13), FILE_HEADER.into()),
(DisplayRow(15), DIAGNOSTIC_HEADER.into()),
(DisplayRow(28), EXCERPT_HEADER.into()),
(DisplayRow(29), DIAGNOSTIC_HEADER.into()),
(DisplayRow(38), EXCERPT_HEADER.into()),
(DisplayRow(3), DIAGNOSTIC_HEADER.into()),
(DisplayRow(8), EXCERPT_HEADER.into()),
(DisplayRow(10), DIAGNOSTIC_HEADER.into()),
(DisplayRow(15), FILE_HEADER.into()),
(DisplayRow(19), DIAGNOSTIC_HEADER.into()),
(DisplayRow(32), EXCERPT_HEADER.into()),
(DisplayRow(34), DIAGNOSTIC_HEADER.into()),
(DisplayRow(43), EXCERPT_HEADER.into()),
]
);
@@ -388,6 +398,7 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
// diagnostic group 1
"\n", // primary message
"\n", // padding
"\n", // expand
"const a: i32 = 'a';\n",
"\n", // supporting diagnostic
"const b: i32 = c;\n",
@@ -395,6 +406,7 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
// diagnostic group 2
"\n", // primary message
"\n", // padding
"\n", // expand
"const a: i32 = 'a';\n",
"const b: i32 = c;\n",
"\n", // supporting diagnostic
@@ -406,6 +418,8 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
// diagnostic group 1
"\n", // primary message
"\n", // padding
"\n", // expand
"\n", // expand
" let x = vec![];\n",
" let y = vec![];\n",
"\n", // supporting diagnostic
@@ -421,6 +435,7 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
// diagnostic group 2
"\n", // primary message
"\n", // filename
"\n", // expand
"fn main() {\n",
" let x = vec![];\n",
"\n", // supporting diagnostic
@@ -428,11 +443,13 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
" a(x);\n",
"\n", // supporting diagnostic
" b(y);\n",
"\n", // expand
"\n", // context ellipsis
" c(y);\n",
" d(x);\n",
"\n", // supporting diagnostic
"}"
"}",
"\n", // expand
)
);
}
@@ -513,7 +530,7 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
editor_blocks(&editor, cx),
[
(DisplayRow(0), FILE_HEADER.into()),
(DisplayRow(2), DIAGNOSTIC_HEADER.into()),
(DisplayRow(3), DIAGNOSTIC_HEADER.into()),
]
);
assert_eq!(
@@ -524,8 +541,9 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
// diagnostic group 1
"\n", // primary message
"\n", // padding
"\n", // expand
"a();\n", //
"b();",
"b();", "\n", // expand
)
);
@@ -561,9 +579,9 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
editor_blocks(&editor, cx),
[
(DisplayRow(0), FILE_HEADER.into()),
(DisplayRow(2), DIAGNOSTIC_HEADER.into()),
(DisplayRow(6), EXCERPT_HEADER.into()),
(DisplayRow(7), DIAGNOSTIC_HEADER.into()),
(DisplayRow(3), DIAGNOSTIC_HEADER.into()),
(DisplayRow(7), EXCERPT_HEADER.into()),
(DisplayRow(9), DIAGNOSTIC_HEADER.into()),
]
);
assert_eq!(
@@ -574,8 +592,10 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
// diagnostic group 1
"\n", // primary message
"\n", // padding
"\n", // expand
"a();\n", // location
"b();\n", //
"\n", // expand
"\n", // collapsed context
// diagnostic group 2
"\n", // primary message
@@ -583,6 +603,7 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
"a();\n", // context
"b();\n", //
"c();", // context
"\n", // expand
)
);
@@ -629,9 +650,9 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
editor_blocks(&editor, cx),
[
(DisplayRow(0), FILE_HEADER.into()),
(DisplayRow(2), DIAGNOSTIC_HEADER.into()),
(DisplayRow(7), EXCERPT_HEADER.into()),
(DisplayRow(8), DIAGNOSTIC_HEADER.into()),
(DisplayRow(3), DIAGNOSTIC_HEADER.into()),
(DisplayRow(8), EXCERPT_HEADER.into()),
(DisplayRow(10), DIAGNOSTIC_HEADER.into()),
]
);
assert_eq!(
@@ -642,9 +663,11 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
// diagnostic group 1
"\n", // primary message
"\n", // padding
"\n", // expand
"a();\n", // location
"b();\n", //
"c();\n", // context
"\n", // expand
"\n", // collapsed context
// diagnostic group 2
"\n", // primary message
@@ -652,6 +675,7 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
"b();\n", // context
"c();\n", //
"d();", // context
"\n", // expand
)
);
@@ -687,9 +711,9 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
editor_blocks(&editor, cx),
[
(DisplayRow(0), FILE_HEADER.into()),
(DisplayRow(2), DIAGNOSTIC_HEADER.into()),
(DisplayRow(7), EXCERPT_HEADER.into()),
(DisplayRow(8), DIAGNOSTIC_HEADER.into()),
(DisplayRow(3), DIAGNOSTIC_HEADER.into()),
(DisplayRow(8), EXCERPT_HEADER.into()),
(DisplayRow(10), DIAGNOSTIC_HEADER.into()),
]
);
assert_eq!(
@@ -700,9 +724,11 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
// diagnostic group 1
"\n", // primary message
"\n", // padding
"\n", // expand
"b();\n", // location
"c();\n", //
"d();\n", // context
"\n", // expand
"\n", // collapsed context
// diagnostic group 2
"\n", // primary message
@@ -710,6 +736,7 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
"c();\n", // context
"d();\n", //
"e();", // context
"\n", // expand
)
);
}

View File

@@ -204,7 +204,7 @@ impl_actions!(
ToggleCodeActions,
ToggleComments,
UnfoldAt,
FoldAtLevel
FoldAtLevel,
]
);

View File

@@ -32,6 +32,7 @@ use crate::{
pub use block_map::{
Block, BlockBufferRows, BlockChunks as DisplayChunks, BlockContext, BlockId, BlockMap,
BlockPlacement, BlockPoint, BlockProperties, BlockStyle, CustomBlockId, RenderBlock,
StickyHeaderExcerpt,
};
use block_map::{BlockRow, BlockSnapshot};
use collections::{HashMap, HashSet};
@@ -1105,6 +1106,10 @@ impl DisplaySnapshot {
.map(|(row, block)| (DisplayRow(row), block))
}
pub fn sticky_header_excerpt(&self, row: DisplayRow) -> Option<StickyHeaderExcerpt<'_>> {
self.block_snapshot.sticky_header_excerpt(row.0)
}
pub fn block_for_id(&self, id: BlockId) -> Option<Block> {
self.block_snapshot.block_for_id(id)
}

View File

@@ -1411,6 +1411,66 @@ impl BlockSnapshot {
})
}
pub fn sticky_header_excerpt(&self, top_row: u32) -> Option<StickyHeaderExcerpt<'_>> {
let mut cursor = self.transforms.cursor::<BlockRow>(&());
cursor.seek(&BlockRow(top_row), Bias::Left, &());
while let Some(transform) = cursor.item() {
let start = cursor.start().0;
let end = cursor.end(&()).0;
match &transform.block {
Some(Block::ExcerptBoundary {
prev_excerpt,
next_excerpt,
starts_new_buffer,
show_excerpt_controls,
..
}) => {
let matches_start = if *show_excerpt_controls && prev_excerpt.is_some() {
start < top_row
} else {
start <= top_row
};
if matches_start && top_row <= end {
return next_excerpt.as_ref().map(|excerpt| StickyHeaderExcerpt {
next_buffer_row: None,
next_excerpt_controls_present: *show_excerpt_controls,
excerpt,
});
}
let next_buffer_row = if *starts_new_buffer { Some(end) } else { None };
return prev_excerpt.as_ref().map(|excerpt| StickyHeaderExcerpt {
excerpt,
next_buffer_row,
next_excerpt_controls_present: *show_excerpt_controls,
});
}
Some(Block::FoldedBuffer {
prev_excerpt: Some(excerpt),
..
}) if top_row <= start => {
return Some(StickyHeaderExcerpt {
next_buffer_row: Some(end),
next_excerpt_controls_present: false,
excerpt,
});
}
Some(Block::FoldedBuffer { .. }) | Some(Block::Custom(_)) | None => {}
}
// This is needed to iterate past None / FoldedBuffer / Custom blocks. For FoldedBuffer,
// if scrolled slightly past the header of a folded block, the next block is needed for
// the sticky header.
cursor.next(&());
}
None
}
pub fn block_for_id(&self, block_id: BlockId) -> Option<Block> {
let buffer = self.wrap_snapshot.buffer_snapshot();
let wrap_point = match block_id {
@@ -1694,6 +1754,12 @@ impl<'a> BlockChunks<'a> {
}
}
pub struct StickyHeaderExcerpt<'a> {
pub excerpt: &'a ExcerptInfo,
pub next_excerpt_controls_present: bool,
pub next_buffer_row: Option<u32>,
}
impl<'a> Iterator for BlockChunks<'a> {
type Item = Chunk<'a>;

View File

@@ -5842,7 +5842,7 @@ impl Editor {
});
}
pub fn join_lines(&mut self, _: &JoinLines, cx: &mut ViewContext<Self>) {
pub fn join_lines_impl(&mut self, insert_whitespace: bool, cx: &mut ViewContext<Self>) {
if self.read_only(cx) {
return;
}
@@ -5884,11 +5884,12 @@ impl Editor {
let indent = snapshot.indent_size_for_line(next_line_row);
let start_of_next_line = Point::new(next_line_row.0, indent.len);
let replace = if snapshot.line_len(next_line_row) > indent.len {
" "
} else {
""
};
let replace =
if snapshot.line_len(next_line_row) > indent.len && insert_whitespace {
" "
} else {
""
};
this.buffer.update(cx, |buffer, cx| {
buffer.edit([(end_of_line..start_of_next_line, replace)], None, cx)
@@ -5902,6 +5903,10 @@ impl Editor {
});
}
pub fn join_lines(&mut self, _: &JoinLines, cx: &mut ViewContext<Self>) {
self.join_lines_impl(true, cx);
}
pub fn sort_lines_case_sensitive(
&mut self,
_: &SortLinesCaseSensitive,

View File

@@ -22,7 +22,7 @@ use crate::{
EditorSnapshot, EditorStyle, ExpandExcerpts, FocusedBlock, GutterDimensions, HalfPageDown,
HalfPageUp, HandleInput, HoveredCursor, HoveredHunk, InlineCompletion, JumpData, LineDown,
LineUp, OpenExcerpts, PageDown, PageUp, Point, RowExt, RowRangeExt, SelectPhase, Selection,
SoftWrap, ToPoint, ToggleFold, CURSORS_VISIBLE_FOR, FILE_HEADER_HEIGHT,
SoftWrap, StickyHeaderExcerpt, ToPoint, ToggleFold, CURSORS_VISIBLE_FOR, FILE_HEADER_HEIGHT,
GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED, MAX_LINE_LEN, MULTI_BUFFER_EXCERPT_HEADER_HEIGHT,
};
use client::ParticipantIndex;
@@ -30,14 +30,14 @@ use collections::{BTreeMap, HashMap, HashSet};
use file_icons::FileIcons;
use git::{blame::BlameEntry, diff::DiffHunkStatus, Oid};
use gpui::{
anchored, deferred, div, fill, outline, point, px, quad, relative, size, svg,
transparent_black, Action, AnyElement, AvailableSpace, Axis, Bounds, ClickEvent, ClipboardItem,
ContentMask, Corner, Corners, CursorStyle, DispatchPhase, Edges, Element, ElementInputHandler,
Entity, FontId, GlobalElementId, Hitbox, Hsla, InteractiveElement, IntoElement, Length,
ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad,
ParentElement, Pixels, ScrollDelta, ScrollWheelEvent, ShapedLine, SharedString, Size,
StatefulInteractiveElement, Style, Styled, Subscription, TextRun, TextStyleRefinement, View,
ViewContext, WeakView, WindowContext,
anchored, deferred, div, fill, linear_color_stop, linear_gradient, outline, point, px, quad,
relative, size, svg, transparent_black, Action, AnyElement, AvailableSpace, Axis, Bounds,
ClickEvent, ClipboardItem, ContentMask, Corner, Corners, CursorStyle, DispatchPhase, Edges,
Element, ElementInputHandler, Entity, FontId, GlobalElementId, Hitbox, Hsla,
InteractiveElement, IntoElement, Length, ModifiersChangedEvent, MouseButton, MouseDownEvent,
MouseMoveEvent, MouseUpEvent, PaintQuad, ParentElement, Pixels, ScrollDelta, ScrollWheelEvent,
ShapedLine, SharedString, Size, StatefulInteractiveElement, Style, Styled, Subscription,
TextRun, TextStyleRefinement, View, ViewContext, WeakView, WindowContext,
};
use itertools::Itertools;
use language::{
@@ -2210,9 +2210,9 @@ impl EditorElement {
resized_blocks: &mut HashMap<CustomBlockId, u32>,
selections: &[Selection<Point>],
is_row_soft_wrapped: impl Copy + Fn(usize) -> bool,
sticky_header_excerpt_id: Option<ExcerptId>,
cx: &mut WindowContext,
) -> (AnyElement, Size<Pixels>) {
let header_padding = px(6.0);
let mut element = match block {
Block::Custom(block) => {
let block_start = block.start().to_point(&snapshot.buffer_snapshot);
@@ -2305,14 +2305,7 @@ impl EditorElement {
let jump_data = jump_data(snapshot, block_row_start, *height, first_excerpt, cx);
result
.child(self.render_buffer_header(
first_excerpt,
header_padding,
true,
selected,
jump_data,
cx,
))
.child(self.render_buffer_header(first_excerpt, true, selected, jump_data, cx))
.into_any_element()
}
Block::ExcerptBoundary {
@@ -2347,14 +2340,19 @@ impl EditorElement {
if let Some(next_excerpt) = next_excerpt {
let jump_data = jump_data(snapshot, block_row_start, *height, next_excerpt, cx);
if *starts_new_buffer {
result = result.child(self.render_buffer_header(
next_excerpt,
header_padding,
false,
false,
jump_data,
cx,
));
if sticky_header_excerpt_id != Some(next_excerpt.id) {
result = result.child(self.render_buffer_header(
next_excerpt,
false,
false,
jump_data,
cx,
));
} else {
result =
result.child(div().h(FILE_HEADER_HEIGHT as f32 * cx.line_height()));
}
if *show_excerpt_controls {
result = result.child(
h_flex()
@@ -2507,7 +2505,6 @@ impl EditorElement {
fn render_buffer_header(
&self,
for_excerpt: &ExcerptInfo,
header_padding: Pixels,
is_folded: bool,
is_selected: bool,
jump_data: JumpData,
@@ -2531,8 +2528,8 @@ impl EditorElement {
let focus_handle = self.editor.focus_handle(cx);
div()
.px(header_padding)
.pt(header_padding)
.px_2()
.pt_2()
.w_full()
.h(FILE_HEADER_HEIGHT as f32 * cx.line_height())
.child(
@@ -2686,6 +2683,7 @@ impl EditorElement {
line_layouts: &[LineWithInvisibles],
selections: &[Selection<Point>],
is_row_soft_wrapped: impl Copy + Fn(usize) -> bool,
sticky_header_excerpt_id: Option<ExcerptId>,
cx: &mut WindowContext,
) -> Result<Vec<BlockLayout>, HashMap<CustomBlockId, u32>> {
let (fixed_blocks, non_fixed_blocks) = snapshot
@@ -2724,6 +2722,7 @@ impl EditorElement {
&mut resized_blocks,
selections,
is_row_soft_wrapped,
sticky_header_excerpt_id,
cx,
);
fixed_block_max_width = fixed_block_max_width.max(element_size.width + em_width);
@@ -2735,6 +2734,7 @@ impl EditorElement {
style: BlockStyle::Fixed,
});
}
for (row, block) in non_fixed_blocks {
let style = block.style();
let width = match style {
@@ -2770,6 +2770,7 @@ impl EditorElement {
&mut resized_blocks,
selections,
is_row_soft_wrapped,
sticky_header_excerpt_id,
cx,
);
@@ -2817,6 +2818,7 @@ impl EditorElement {
&mut resized_blocks,
selections,
is_row_soft_wrapped,
sticky_header_excerpt_id,
cx,
);
@@ -2883,6 +2885,71 @@ impl EditorElement {
}
}
fn layout_sticky_buffer_header(
&self,
StickyHeaderExcerpt {
excerpt,
next_excerpt_controls_present,
next_buffer_row,
}: StickyHeaderExcerpt<'_>,
scroll_position: f32,
line_height: Pixels,
snapshot: &EditorSnapshot,
hitbox: &Hitbox,
cx: &mut WindowContext,
) -> AnyElement {
let jump_data = jump_data(snapshot, DisplayRow(0), FILE_HEADER_HEIGHT, excerpt, cx);
let editor_bg_color = cx.theme().colors().editor_background;
let mut header = v_flex()
.relative()
.child(
div()
.w(hitbox.bounds.size.width)
.h(FILE_HEADER_HEIGHT as f32 * line_height)
.bg(linear_gradient(
0.,
linear_color_stop(editor_bg_color.opacity(0.), 0.),
linear_color_stop(editor_bg_color, 0.6),
))
.absolute()
.top_0(),
)
.child(
self.render_buffer_header(excerpt, false, false, jump_data, cx)
.into_any_element(),
)
.into_any_element();
let mut origin = hitbox.origin;
if let Some(next_buffer_row) = next_buffer_row {
// Push up the sticky header when the excerpt is getting close to the top of the viewport
let mut max_row = next_buffer_row - FILE_HEADER_HEIGHT * 2;
if next_excerpt_controls_present {
max_row -= MULTI_BUFFER_EXCERPT_HEADER_HEIGHT;
}
let offset = scroll_position - max_row as f32;
if offset > 0.0 {
origin.y -= Pixels(offset) * line_height;
}
}
let size = size(
AvailableSpace::Definite(hitbox.size.width),
AvailableSpace::MinContent,
);
header.prepaint_as_root(origin, size, cx);
header
}
#[allow(clippy::too_many_arguments)]
fn layout_context_menu(
&self,
@@ -4945,11 +5012,14 @@ fn jump_data(
let excerpt_start_row = language::ToPoint::to_point(&jump_anchor, buffer).row;
jump_position.row - excerpt_start_row
};
let line_offset_from_top = block_row_start.0 + height + offset_from_excerpt_start
- snapshot
.scroll_anchor
.scroll_position(&snapshot.display_snapshot)
.y as u32;
let line_offset_from_top = block_row_start.0
+ height
+ offset_from_excerpt_start.saturating_sub(
snapshot
.scroll_anchor
.scroll_position(&snapshot.display_snapshot)
.y as u32,
);
JumpData {
excerpt_id: for_excerpt.id,
anchor: jump_anchor,
@@ -6096,6 +6166,14 @@ impl Element for EditorElement {
let scroll_range_bounds = scrollbar_range_data.scroll_range;
let mut scroll_width = scroll_range_bounds.size.width;
let sticky_header_excerpt = if snapshot.buffer_snapshot.show_headers() {
snapshot.sticky_header_excerpt(start_row)
} else {
None
};
let sticky_header_excerpt_id =
sticky_header_excerpt.as_ref().map(|top| top.excerpt.id);
let blocks = cx.with_element_namespace("blocks", |cx| {
self.render_blocks(
start_row..end_row,
@@ -6111,6 +6189,7 @@ impl Element for EditorElement {
&line_layouts,
&local_selections,
is_row_soft_wrapped,
sticky_header_excerpt_id,
cx,
)
});
@@ -6124,6 +6203,19 @@ impl Element for EditorElement {
}
};
let sticky_buffer_header = sticky_header_excerpt.map(|sticky_header_excerpt| {
cx.with_element_namespace("blocks", |cx| {
self.layout_sticky_buffer_header(
sticky_header_excerpt,
scroll_position.y,
line_height,
&snapshot,
&hitbox,
cx,
)
})
});
let start_buffer_row =
MultiBufferRow(start_anchor.to_point(&snapshot.buffer_snapshot).row);
let end_buffer_row =
@@ -6251,6 +6343,7 @@ impl Element for EditorElement {
);
let mut block_start_rows = HashSet::default();
cx.with_element_namespace("blocks", |cx| {
self.layout_blocks(
&mut blocks,
@@ -6542,6 +6635,7 @@ impl Element for EditorElement {
crease_trailers,
tab_invisible,
space_invisible,
sticky_buffer_header,
}
})
})
@@ -6623,6 +6717,12 @@ impl Element for EditorElement {
});
}
cx.with_element_namespace("blocks", |cx| {
if let Some(mut sticky_header) = layout.sticky_buffer_header.take() {
sticky_header.paint(cx)
}
});
self.paint_scrollbars(layout, cx);
self.paint_inline_completion_popover(layout, cx);
self.paint_mouse_context_menu(layout, cx);
@@ -6730,6 +6830,7 @@ pub struct EditorLayout {
mouse_context_menu: Option<AnyElement>,
tab_invisible: ShapedLine,
space_invisible: ShapedLine,
sticky_buffer_header: Option<AnyElement>,
}
impl EditorLayout {

View File

@@ -146,19 +146,18 @@ fn handle_size_msg(
// Don't resize the renderer when the window is minimized, but record that it was minimized so
// that on restore the swap chain can be recreated via `update_drawable_size_even_if_unchanged`.
if wparam.0 == SIZE_MINIMIZED as usize {
lock.is_minimized = Some(true);
lock.restore_from_minimized = lock.callbacks.request_frame.take();
return Some(0);
}
let may_have_been_minimized = lock.is_minimized.unwrap_or(true);
lock.is_minimized = Some(false);
let width = lparam.loword().max(1) as i32;
let height = lparam.hiword().max(1) as i32;
let new_size = size(DevicePixels(width), DevicePixels(height));
let scale_factor = lock.scale_factor;
if may_have_been_minimized {
if lock.restore_from_minimized.is_some() {
lock.renderer
.update_drawable_size_even_if_unchanged(new_size);
lock.callbacks.request_frame = lock.restore_from_minimized.take();
} else {
lock.renderer.update_drawable_size(new_size);
}

View File

@@ -38,7 +38,7 @@ pub struct WindowsWindowState {
pub fullscreen_restore_bounds: Bounds<Pixels>,
pub border_offset: WindowBorderOffset,
pub scale_factor: f32,
pub is_minimized: Option<bool>,
pub restore_from_minimized: Option<Box<dyn FnMut(RequestFrameOptions)>>,
pub callbacks: Callbacks,
pub input_handler: Option<PlatformInputHandler>,
@@ -94,7 +94,7 @@ impl WindowsWindowState {
size: logical_size,
};
let border_offset = WindowBorderOffset::default();
let is_minimized = None;
let restore_from_minimized = None;
let renderer = windows_renderer::init(gpu_context, hwnd, transparent)?;
let callbacks = Callbacks::default();
let input_handler = None;
@@ -112,7 +112,7 @@ impl WindowsWindowState {
fullscreen_restore_bounds,
border_offset,
scale_factor,
is_minimized,
restore_from_minimized,
callbacks,
input_handler,
system_key_handled,

View File

@@ -614,11 +614,11 @@ impl Element for MarkdownElement {
};
builder.push_div(
div()
.mb_1()
.h_flex()
.mb_2()
.line_height(rems(1.3))
.items_start()
.gap_1()
.line_height(rems(1.3))
.child(bullet),
range,
markdown_end,

View File

@@ -10,4 +10,3 @@ workspace = true
[dependencies]
gpui.workspace = true
once_cell.workspace = true

View File

@@ -2,24 +2,23 @@
#![deny(missing_docs)]
use std::{env, str::FromStr};
use std::{env, str::FromStr, sync::LazyLock};
use gpui::{AppContext, Global, SemanticVersion};
use once_cell::sync::Lazy;
/// stable | dev | nightly | preview
pub static RELEASE_CHANNEL_NAME: Lazy<String> = if cfg!(debug_assertions) {
Lazy::new(|| {
pub static RELEASE_CHANNEL_NAME: LazyLock<String> = LazyLock::new(|| {
if cfg!(debug_assertions) {
env::var("ZED_RELEASE_CHANNEL")
.unwrap_or_else(|_| include_str!("../../zed/RELEASE_CHANNEL").trim().to_string())
})
} else {
Lazy::new(|| include_str!("../../zed/RELEASE_CHANNEL").trim().to_string())
};
} else {
include_str!("../../zed/RELEASE_CHANNEL").trim().to_string()
}
});
#[doc(hidden)]
pub static RELEASE_CHANNEL: Lazy<ReleaseChannel> =
Lazy::new(|| match ReleaseChannel::from_str(&RELEASE_CHANNEL_NAME) {
pub static RELEASE_CHANNEL: LazyLock<ReleaseChannel> =
LazyLock::new(|| match ReleaseChannel::from_str(&RELEASE_CHANNEL_NAME) {
Ok(channel) => channel,
_ => panic!("invalid release channel {}", *RELEASE_CHANNEL_NAME),
});

View File

@@ -44,6 +44,8 @@ actions!(
InsertLineAbove,
InsertLineBelow,
InsertAtPrevious,
JoinLines,
JoinLinesNoWhitespace,
DeleteLeft,
DeleteRight,
ChangeToEndOfLine,
@@ -53,7 +55,6 @@ actions!(
ChangeCase,
ConvertToUpperCase,
ConvertToLowerCase,
JoinLines,
ToggleComments,
Undo,
Redo,
@@ -108,25 +109,11 @@ pub(crate) fn register(editor: &mut Editor, cx: &mut ViewContext<Vim>) {
);
});
Vim::action(editor, cx, |vim, _: &JoinLines, cx| {
vim.record_current_action(cx);
let mut times = Vim::take_count(cx).unwrap_or(1);
if vim.mode.is_visual() {
times = 1;
} else if times > 1 {
// 2J joins two lines together (same as J or 1J)
times -= 1;
}
vim.join_lines_impl(true, cx);
});
vim.update_editor(cx, |_, editor, cx| {
editor.transact(cx, |editor, cx| {
for _ in 0..times {
editor.join_lines(&Default::default(), cx)
}
})
});
if vim.mode.is_visual() {
vim.switch_mode(Mode::Normal, true, cx)
}
Vim::action(editor, cx, |vim, _: &JoinLinesNoWhitespace, cx| {
vim.join_lines_impl(false, cx);
});
Vim::action(editor, cx, |vim, _: &Undo, cx| {
@@ -401,6 +388,28 @@ impl Vim {
});
}
fn join_lines_impl(&mut self, insert_whitespace: bool, cx: &mut ViewContext<Self>) {
self.record_current_action(cx);
let mut times = Vim::take_count(cx).unwrap_or(1);
if self.mode.is_visual() {
times = 1;
} else if times > 1 {
// 2J joins two lines together (same as J or 1J)
times -= 1;
}
self.update_editor(cx, |_, editor, cx| {
editor.transact(cx, |editor, cx| {
for _ in 0..times {
editor.join_lines_impl(insert_whitespace, cx)
}
})
});
if self.mode.is_visual() {
self.switch_mode(Mode::Normal, true, cx)
}
}
fn yank_line(&mut self, _: &YankLine, cx: &mut ViewContext<Self>) {
let count = Vim::take_count(cx);
self.yank_motion(motion::Motion::CurrentLine, count, cx)

View File

@@ -21,7 +21,6 @@ use serde::Deserialize;
#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
pub enum Object {
Word { ignore_punctuation: bool },
Subword { ignore_punctuation: bool },
Sentence,
Paragraph,
Quotes,
@@ -48,18 +47,12 @@ struct Word {
}
#[derive(Clone, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
struct Subword {
#[serde(default)]
ignore_punctuation: bool,
}
#[derive(Clone, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
struct IndentObj {
#[serde(default)]
include_below: bool,
}
impl_actions!(vim, [Word, Subword, IndentObj]);
impl_actions!(vim, [Word, IndentObj]);
actions!(
vim,
@@ -90,13 +83,6 @@ pub fn register(editor: &mut Editor, cx: &mut ViewContext<Vim>) {
vim.object(Object::Word { ignore_punctuation }, cx)
},
);
Vim::action(
editor,
cx,
|vim, &Subword { ignore_punctuation }: &Subword, cx| {
vim.object(Object::Subword { ignore_punctuation }, cx)
},
);
Vim::action(editor, cx, |vim, _: &Tag, cx| vim.object(Object::Tag, cx));
Vim::action(editor, cx, |vim, _: &Sentence, cx| {
vim.object(Object::Sentence, cx)
@@ -168,7 +154,6 @@ impl Object {
pub fn is_multiline(self) -> bool {
match self {
Object::Word { .. }
| Object::Subword { .. }
| Object::Quotes
| Object::BackQuotes
| Object::VerticalBars
@@ -191,7 +176,6 @@ impl Object {
pub fn always_expands_both_ways(self) -> bool {
match self {
Object::Word { .. }
| Object::Subword { .. }
| Object::Sentence
| Object::Paragraph
| Object::Argument
@@ -214,7 +198,6 @@ impl Object {
pub fn target_visual_mode(self, current_mode: Mode, around: bool) -> Mode {
match self {
Object::Word { .. }
| Object::Subword { .. }
| Object::Sentence
| Object::Quotes
| Object::BackQuotes
@@ -260,13 +243,6 @@ impl Object {
in_word(map, relative_to, ignore_punctuation)
}
}
Object::Subword { ignore_punctuation } => {
if around {
around_subword(map, relative_to, ignore_punctuation)
} else {
in_subword(map, relative_to, ignore_punctuation)
}
}
Object::Sentence => sentence(map, relative_to, around),
Object::Paragraph => paragraph(map, relative_to, around),
Object::Quotes => {
@@ -374,63 +350,6 @@ fn in_word(
Some(start..end)
}
fn in_subword(
map: &DisplaySnapshot,
relative_to: DisplayPoint,
ignore_punctuation: bool,
) -> Option<Range<DisplayPoint>> {
let offset = relative_to.to_offset(map, Bias::Left);
// Use motion::right so that we consider the character under the cursor when looking for the start
let classifier = map
.buffer_snapshot
.char_classifier_at(relative_to.to_point(map))
.ignore_punctuation(ignore_punctuation);
let in_subword = map
.buffer_chars_at(offset)
.next()
.map(|(c, _)| {
if classifier.is_word('-') {
!classifier.is_whitespace(c) && c != '_' && c != '-'
} else {
!classifier.is_whitespace(c) && c != '_'
}
})
.unwrap_or(false);
let start = if in_subword {
movement::find_preceding_boundary_display_point(
map,
right(map, relative_to, 1),
movement::FindRange::SingleLine,
|left, right| {
let is_word_start = classifier.kind(left) != classifier.kind(right);
let is_subword_start = classifier.is_word('-') && left == '-' && right != '-'
|| left == '_' && right != '_'
|| left.is_lowercase() && right.is_uppercase();
is_word_start || is_subword_start
},
)
} else {
movement::find_boundary(map, relative_to, FindRange::SingleLine, |left, right| {
let is_word_start = classifier.kind(left) != classifier.kind(right);
let is_subword_start = classifier.is_word('-') && left == '-' && right != '-'
|| left == '_' && right != '_'
|| left.is_lowercase() && right.is_uppercase();
is_word_start || is_subword_start
})
};
let end = movement::find_boundary(map, relative_to, FindRange::SingleLine, |left, right| {
let is_word_end = classifier.kind(left) != classifier.kind(right);
let is_subword_end = classifier.is_word('-') && left != '-' && right == '-'
|| left != '_' && right == '_'
|| left.is_lowercase() && right.is_uppercase();
is_word_end || is_subword_end
});
Some(start..end)
}
pub fn surrounding_html_tag(
map: &DisplaySnapshot,
head: DisplayPoint,
@@ -542,40 +461,6 @@ fn around_word(
}
}
fn around_subword(
map: &DisplaySnapshot,
relative_to: DisplayPoint,
ignore_punctuation: bool,
) -> Option<Range<DisplayPoint>> {
// Use motion::right so that we consider the character under the cursor when looking for the start
let classifier = map
.buffer_snapshot
.char_classifier_at(relative_to.to_point(map))
.ignore_punctuation(ignore_punctuation);
let start = movement::find_preceding_boundary_display_point(
map,
right(map, relative_to, 1),
movement::FindRange::SingleLine,
|left, right| {
let is_word_start = classifier.kind(left) != classifier.kind(right);
let is_subword_start = classifier.is_word('-') && left != '-' && right == '-'
|| left != '_' && right == '_'
|| left.is_lowercase() && right.is_uppercase();
is_word_start || is_subword_start
},
);
let end = movement::find_boundary(map, relative_to, FindRange::SingleLine, |left, right| {
let is_word_end = classifier.kind(left) != classifier.kind(right);
let is_subword_end = classifier.is_word('-') && left != '-' && right == '-'
|| left != '_' && right == '_'
|| left.is_lowercase() && right.is_uppercase();
is_word_end || is_subword_end
});
Some(start..end)
}
fn around_containing_word(
map: &DisplaySnapshot,
relative_to: DisplayPoint,

View File

@@ -367,6 +367,46 @@ async fn test_join_lines(cx: &mut gpui::TestAppContext) {
two three fourˇ five
six
"});
cx.set_shared_state(indoc! {"
ˇone
two
three
four
five
six
"})
.await;
cx.simulate_shared_keystrokes("g shift-j").await;
cx.shared_state().await.assert_eq(indoc! {"
oneˇtwo
three
four
five
six
"});
cx.simulate_shared_keystrokes("3 g shift-j").await;
cx.shared_state().await.assert_eq(indoc! {"
onetwothreeˇfour
five
six
"});
cx.set_shared_state(indoc! {"
ˇone
two
three
four
five
six
"})
.await;
cx.simulate_shared_keystrokes("j v 3 j g shift-j").await;
cx.shared_state().await.assert_eq(indoc! {"
one
twothreefourˇfive
six
"});
}
#[cfg(target_os = "macos")]

View File

@@ -11,3 +11,19 @@
{"Key":"j"}
{"Key":"shift-j"}
{"Get":{"state":"one\ntwo three fourˇ five\nsix\n","mode":"Normal"}}
{"Put":{"state":"ˇone\ntwo\nthree\nfour\nfive\nsix\n"}}
{"Key":"g"}
{"Key":"shift-j"}
{"Get":{"state":"oneˇtwo\nthree\nfour\nfive\nsix\n","mode":"Normal"}}
{"Key":"3"}
{"Key":"g"}
{"Key":"shift-j"}
{"Get":{"state":"onetwothreeˇfour\nfive\nsix\n","mode":"Normal"}}
{"Put":{"state":"ˇone\ntwo\nthree\nfour\nfive\nsix\n"}}
{"Key":"j"}
{"Key":"v"}
{"Key":"3"}
{"Key":"j"}
{"Key":"g"}
{"Key":"shift-j"}
{"Get":{"state":"one\ntwothreefourˇfive\nsix\n","mode":"Normal"}}

View File

@@ -392,20 +392,6 @@ Subword motion, which allows you to navigate and select individual words in came
]
```
Similarly subword text objects are available, and can be bound with:
```json
[
{
"context": "vim_operator == a || vim_operator == i || vim_operator == cs",
"bindings": {
"w": "vim::Subword",
"shift-w": ["vim::Subword", { "ignorePunctuation": true }],
}
}
]
```
Vim mode comes with shortcuts to surround the selection in normal mode (`ys`), but it doesn't have a shortcut to add surrounds in visual mode. By default, `shift-s` substitutes the selection (erases the text and enters insert mode). To use `shift-s` to add surrounds in visual mode, you can add the following object to your keymap.
```json