Compare commits
46 Commits
jump-into-
...
add_subwor
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
404a268412 | ||
|
|
7aede4a348 | ||
|
|
8a724acc4d | ||
|
|
7595d36943 | ||
|
|
d25c2ff866 | ||
|
|
7f33f31ebe | ||
|
|
5df409971c | ||
|
|
8ccc7b81c8 | ||
|
|
a0e4464a33 | ||
|
|
204af9cac5 | ||
|
|
a2022d7da3 | ||
|
|
b51a28b75f | ||
|
|
dbb76100e5 | ||
|
|
6b92e0b5da | ||
|
|
2930211af9 | ||
|
|
1449377278 | ||
|
|
831930aad0 | ||
|
|
bc32b4d016 | ||
|
|
fac5118f10 | ||
|
|
7184b15f48 | ||
|
|
e82af55d64 | ||
|
|
dcd21e6f23 | ||
|
|
9efa13116d | ||
|
|
6dbc12f6af | ||
|
|
a8afc63a91 | ||
|
|
7c6feeb3a8 | ||
|
|
fadf9ff4f4 | ||
|
|
9b2bc458e3 | ||
|
|
ca9cee85e1 | ||
|
|
c01403b4b1 | ||
|
|
8ee04bf04a | ||
|
|
4ed0e5160f | ||
|
|
72e56eee7a | ||
|
|
306fc19739 | ||
|
|
4fbb568f42 | ||
|
|
7913b6a5a2 | ||
|
|
d566792ae1 | ||
|
|
62f5ca562e | ||
|
|
4eb8492308 | ||
|
|
d824baeece | ||
|
|
8a858fee7c | ||
|
|
cbd2e81a7e | ||
|
|
b25d8ecb75 | ||
|
|
7c03e11cfc | ||
|
|
f3fc4d6279 | ||
|
|
e4493d60dc |
8
.github/workflows/ci.yml
vendored
8
.github/workflows/ci.yml
vendored
@@ -94,6 +94,10 @@ jobs:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
|
||||
# To support writing comments that they will certainly be revisited.
|
||||
- name: Check for todo! and FIXME comments
|
||||
run: script/check-todos
|
||||
|
||||
- name: Run style checks
|
||||
uses: ./.github/actions/check_style
|
||||
|
||||
@@ -360,6 +364,8 @@ jobs:
|
||||
env:
|
||||
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
|
||||
ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: ${{ secrets.ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON }}
|
||||
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
|
||||
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
@@ -406,6 +412,8 @@ jobs:
|
||||
env:
|
||||
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
|
||||
ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: ${{ secrets.ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON }}
|
||||
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
|
||||
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
|
||||
18
Cargo.lock
generated
18
Cargo.lock
generated
@@ -83,8 +83,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "alacritty_terminal"
|
||||
version = "0.24.1-dev"
|
||||
source = "git+https://github.com/alacritty/alacritty?rev=91d034ff8b53867143c005acfaa14609147c9a2c#91d034ff8b53867143c005acfaa14609147c9a2c"
|
||||
version = "0.24.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52b9241459831fee2f22fcda52ddaf9e449b6627334a0f40f13a1b3344018060"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bitflags 2.6.0",
|
||||
@@ -493,6 +494,7 @@ dependencies = [
|
||||
"project",
|
||||
"proto",
|
||||
"rand 0.8.5",
|
||||
"release_channel",
|
||||
"rope",
|
||||
"schemars",
|
||||
"serde",
|
||||
@@ -3680,6 +3682,7 @@ dependencies = [
|
||||
"ctor",
|
||||
"editor",
|
||||
"env_logger 0.11.5",
|
||||
"feature_flags",
|
||||
"gpui",
|
||||
"language",
|
||||
"log",
|
||||
@@ -5167,8 +5170,12 @@ dependencies = [
|
||||
"anyhow",
|
||||
"collections",
|
||||
"db",
|
||||
"editor",
|
||||
"futures 0.3.31",
|
||||
"git",
|
||||
"gpui",
|
||||
"language",
|
||||
"menu",
|
||||
"project",
|
||||
"schemars",
|
||||
"serde",
|
||||
@@ -6982,7 +6989,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"windows-targets 0.48.5",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7695,6 +7702,7 @@ dependencies = [
|
||||
"sum_tree",
|
||||
"text",
|
||||
"theme",
|
||||
"tree-sitter",
|
||||
"util",
|
||||
]
|
||||
|
||||
@@ -13975,6 +13983,8 @@ dependencies = [
|
||||
"futures-lite 1.13.0",
|
||||
"git2",
|
||||
"globset",
|
||||
"itertools 0.13.0",
|
||||
"libc",
|
||||
"log",
|
||||
"rand 0.8.5",
|
||||
"regex",
|
||||
@@ -14979,7 +14989,7 @@ version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
|
||||
dependencies = [
|
||||
"windows-sys 0.48.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -337,7 +337,7 @@ zeta = { path = "crates/zeta" }
|
||||
#
|
||||
|
||||
aho-corasick = "1.1"
|
||||
alacritty_terminal = { git = "https://github.com/alacritty/alacritty", rev = "91d034ff8b53867143c005acfaa14609147c9a2c" }
|
||||
alacritty_terminal = "0.24"
|
||||
any_vec = "0.14"
|
||||
anyhow = "1.0.86"
|
||||
arrayvec = { version = "0.7.4", features = ["serde"] }
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11.7633 4.2078H4.23674L4.3551 5.5189H10.1429L9.99592 6.87645H6.20408L6.33877 8.16255H9.86939L9.66122 9.92379L8 10.3275L6.3102 9.92021L6.20408 8.86633H4.7102L4.87755 10.7955L8 11.6457L11.0694 10.8812L11.7633 4.2078ZM2 2H14L12.9061 12.7818L7.98775 14L3.09388 12.7818L2 2Z" fill="black"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.58 2H2.5V12.08C2.5 12.5892 2.70229 13.0776 3.06235 13.4376C3.42242 13.7977 3.91078 14 4.42 14H12.58C13.0892 14 13.5776 13.7977 13.9376 13.4376C14.2977 13.0776 14.5 12.5892 14.5 12.08V3.92C14.5 3.41078 14.2977 2.92242 13.9376 2.56235C13.5776 2.20229 13.0892 2 12.58 2ZM3.358 11.6285C3.34615 12.6668 3.96437 13.2311 4.96636 13.232H4.96621C6.06429 13.2456 6.70951 12.4798 6.63088 11.3867H5.48085C5.4899 11.601 5.47243 11.8974 5.36026 12.0313C5.27992 12.1441 5.16183 12.2005 5.00645 12.2005C4.67402 12.1952 4.50788 11.9534 4.50788 11.4753V9.19488C4.50788 8.94247 4.54407 8.75168 4.61645 8.62283C4.73423 8.38524 5.17961 8.3584 5.34825 8.58663C5.47804 8.71252 5.48974 9.04683 5.48101 9.26757H6.63104C6.66099 8.70582 6.53494 8.10381 6.20079 7.80913C5.65853 7.23521 4.37403 7.26765 3.82039 7.80102C3.51213 8.07495 3.358 8.47525 3.358 9.00159V11.6285ZM7.04116 11.3867C7.01043 12.4573 7.50713 13.2473 8.61739 13.232L8.61723 13.2317C10.1571 13.2967 10.5874 11.592 9.96023 10.4759C9.74995 10.1097 9.16994 9.80702 8.71379 9.62981C8.36155 9.46772 8.21038 9.3086 8.20711 8.92079C8.20711 8.55559 8.35983 8.37291 8.66543 8.37291C8.83688 8.37291 8.95357 8.42939 9.01519 8.54217C9.10317 8.6754 9.12454 9.0409 9.11565 9.26742H10.1612C10.1866 8.71627 10.0554 8.11739 9.75509 7.81303C9.26822 7.22257 7.99791 7.24909 7.5115 7.82504C7.0109 8.29179 6.97783 9.4437 7.3346 9.96848C7.49278 10.205 7.75143 10.409 8.1107 10.5809C8.15897 10.6051 8.21552 10.6314 8.27633 10.6598C8.53247 10.7792 8.86416 10.9338 8.97119 11.1046C9.16073 11.3241 9.13593 11.8913 9.00333 12.0877C8.9336 12.1952 8.81285 12.2489 8.64141 12.2489C8.25703 12.2785 8.09666 11.8534 8.12677 11.3867H7.04116ZM10.5474 11.3867C10.5167 12.4573 11.0134 13.2473 12.1236 13.232L12.1235 13.2317C13.6634 13.2967 14.0936 11.592 13.4665 10.4759C13.2562 10.1097 12.6762 9.80702 12.2201 9.62981C11.8678 9.46772 11.7166 9.3086 11.7134 8.92079C11.7134 8.55559 11.8661 8.37291 12.1717 8.37291C12.3431 8.37291 12.4598 8.42939 12.5214 8.54217C12.6094 8.6754 12.6308 9.0409 12.6219 9.26742H13.6674C13.6928 8.71627 13.5617 8.11739 13.2614 7.81303C12.7745 7.22257 11.5042 7.24909 11.0178 7.82504C10.5172 8.29179 10.4841 9.4437 10.8409 9.96848C10.999 10.205 11.2577 10.409 11.617 10.5809C11.6652 10.6051 11.7218 10.6314 11.7826 10.6598C12.0387 10.7792 12.3704 10.9338 12.4775 11.1046C12.667 11.3241 12.6422 11.8913 12.5096 12.0877C12.4399 12.1952 12.3191 12.2489 12.1477 12.2489C11.7633 12.2785 11.6029 11.8534 11.633 11.3867H10.5474Z" fill="black"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 403 B After Width: | Height: | Size: 2.6 KiB |
@@ -448,7 +448,6 @@
|
||||
"cmd-s": "workspace::Save",
|
||||
"cmd-k s": "workspace::SaveWithoutFormat",
|
||||
"cmd-shift-s": "workspace::SaveAs",
|
||||
"cmd-n": "workspace::NewFile",
|
||||
"cmd-shift-n": "workspace::NewWindow",
|
||||
"ctrl-`": "terminal_panel::ToggleFocus",
|
||||
"cmd-1": ["workspace::ActivatePane", 0],
|
||||
@@ -495,6 +494,7 @@
|
||||
"context": "Workspace && !Terminal",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-n": "workspace::NewFile",
|
||||
"cmd-shift-r": "task::Spawn",
|
||||
"cmd-alt-r": "task::Rerun",
|
||||
"ctrl-alt-shift-r": ["task::Spawn", { "reveal_target": "center" }]
|
||||
@@ -761,6 +761,7 @@
|
||||
"cmd-v": "terminal::Paste",
|
||||
"cmd-a": "editor::SelectAll",
|
||||
"cmd-k": "terminal::Clear",
|
||||
"cmd-n": "workspace::NewTerminal",
|
||||
"ctrl-enter": "assistant::InlineAssist",
|
||||
// Some nice conveniences
|
||||
"cmd-backspace": ["terminal::SendText", "\u0015"],
|
||||
|
||||
@@ -389,6 +389,9 @@
|
||||
"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",
|
||||
|
||||
@@ -1556,6 +1556,7 @@ impl ContextEditor {
|
||||
let mut editor = Editor::for_buffer(context.read(cx).buffer().clone(), None, cx);
|
||||
editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
|
||||
editor.set_show_line_numbers(false, cx);
|
||||
editor.set_show_scrollbars(false, cx);
|
||||
editor.set_show_git_diff_gutter(false, cx);
|
||||
editor.set_show_code_actions(false, cx);
|
||||
editor.set_show_runnables(false, cx);
|
||||
|
||||
@@ -50,6 +50,7 @@ parking_lot.workspace = true
|
||||
picker.workspace = true
|
||||
project.workspace = true
|
||||
proto.workspace = true
|
||||
release_channel.workspace = true
|
||||
rope.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
|
||||
@@ -3,8 +3,9 @@ use std::sync::Arc;
|
||||
use assistant_tool::ToolWorkingSet;
|
||||
use collections::HashMap;
|
||||
use gpui::{
|
||||
list, AnyElement, AppContext, Empty, ListAlignment, ListState, Model, StyleRefinement,
|
||||
Subscription, TextStyleRefinement, View, WeakView,
|
||||
list, AbsoluteLength, AnyElement, AppContext, CornersRefinement, DefiniteLength,
|
||||
EdgesRefinement, Empty, Length, ListAlignment, ListState, Model, StyleRefinement, Subscription,
|
||||
TextStyleRefinement, View, WeakView,
|
||||
};
|
||||
use language::LanguageRegistry;
|
||||
use language_model::Role;
|
||||
@@ -89,10 +90,11 @@ impl ActiveThread {
|
||||
self.list_state.splice(old_len..old_len, 1);
|
||||
|
||||
let theme_settings = ThemeSettings::get_global(cx);
|
||||
let colors = cx.theme().colors();
|
||||
let ui_font_size = TextSize::Default.rems(cx);
|
||||
let buffer_font_size = theme_settings.buffer_font_size;
|
||||
|
||||
let buffer_font_size = TextSize::Small.rems(cx);
|
||||
let mut text_style = cx.text_style();
|
||||
|
||||
text_style.refine(&TextStyleRefinement {
|
||||
font_family: Some(theme_settings.ui_font.family.clone()),
|
||||
font_size: Some(ui_font_size.into()),
|
||||
@@ -105,6 +107,32 @@ impl ActiveThread {
|
||||
syntax: cx.theme().syntax().clone(),
|
||||
selection_background_color: cx.theme().players().local().selection,
|
||||
code_block: StyleRefinement {
|
||||
margin: EdgesRefinement {
|
||||
top: Some(Length::Definite(rems(0.5).into())),
|
||||
left: Some(Length::Definite(rems(0.).into())),
|
||||
right: Some(Length::Definite(rems(0.).into())),
|
||||
bottom: Some(Length::Definite(rems(1.).into())),
|
||||
},
|
||||
padding: EdgesRefinement {
|
||||
top: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
|
||||
left: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
|
||||
right: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
|
||||
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_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()),
|
||||
@@ -114,8 +142,8 @@ impl ActiveThread {
|
||||
},
|
||||
inline_code: TextStyleRefinement {
|
||||
font_family: Some(theme_settings.buffer_font.family.clone()),
|
||||
font_size: Some(ui_font_size.into()),
|
||||
background_color: Some(cx.theme().colors().editor_background),
|
||||
font_size: Some(buffer_font_size.into()),
|
||||
background_color: Some(colors.editor_foreground.opacity(0.01)),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
@@ -204,11 +232,12 @@ impl ActiveThread {
|
||||
};
|
||||
|
||||
let context = self.thread.read(cx).context_for_message(message_id);
|
||||
let colors = cx.theme().colors();
|
||||
|
||||
let (role_icon, role_name) = match message.role {
|
||||
Role::User => (IconName::Person, "You"),
|
||||
Role::Assistant => (IconName::ZedAssistant, "Assistant"),
|
||||
Role::System => (IconName::Settings, "System"),
|
||||
let (role_icon, role_name, role_color) = match message.role {
|
||||
Role::User => (IconName::Person, "You", Color::Muted),
|
||||
Role::Assistant => (IconName::ZedAssistant, "Assistant", Color::Accent),
|
||||
Role::System => (IconName::Settings, "System", Color::Default),
|
||||
};
|
||||
|
||||
div()
|
||||
@@ -218,31 +247,35 @@ impl ActiveThread {
|
||||
.child(
|
||||
v_flex()
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.border_color(colors.border_variant)
|
||||
.bg(colors.editor_background)
|
||||
.rounded_md()
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_between()
|
||||
.py_1()
|
||||
.px_2()
|
||||
.py_1p5()
|
||||
.px_2p5()
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.border_color(colors.border_variant)
|
||||
.justify_between()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.child(
|
||||
Icon::new(role_icon)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
.color(role_color),
|
||||
)
|
||||
.child(Label::new(role_name).size(LabelSize::XSmall)),
|
||||
.child(
|
||||
Label::new(role_name)
|
||||
.size(LabelSize::XSmall)
|
||||
.color(role_color),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(v_flex().px_2().py_1().text_ui(cx).child(markdown.clone()))
|
||||
.child(v_flex().p_2p5().text_ui(cx).child(markdown.clone()))
|
||||
.when_some(context, |parent, context| {
|
||||
parent.child(
|
||||
h_flex().flex_wrap().gap_2().p_1p5().children(
|
||||
h_flex().flex_wrap().gap_1().p_1p5().children(
|
||||
context
|
||||
.iter()
|
||||
.map(|context| ContextPill::new(context.clone())),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
mod active_thread;
|
||||
mod assistant_model_selector;
|
||||
mod assistant_panel;
|
||||
mod assistant_settings;
|
||||
mod buffer_codegen;
|
||||
|
||||
85
crates/assistant2/src/assistant_model_selector.rs
Normal file
85
crates/assistant2/src/assistant_model_selector.rs
Normal file
@@ -0,0 +1,85 @@
|
||||
use fs::Fs;
|
||||
use gpui::View;
|
||||
use language_model::LanguageModelRegistry;
|
||||
use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu};
|
||||
use settings::update_settings_file;
|
||||
use std::sync::Arc;
|
||||
use ui::{prelude::*, ButtonLike, PopoverMenuHandle, Tooltip};
|
||||
|
||||
use crate::{assistant_settings::AssistantSettings, ToggleModelSelector};
|
||||
|
||||
pub struct AssistantModelSelector {
|
||||
selector: View<LanguageModelSelector>,
|
||||
menu_handle: PopoverMenuHandle<LanguageModelSelector>,
|
||||
}
|
||||
|
||||
impl AssistantModelSelector {
|
||||
pub(crate) fn new(
|
||||
fs: Arc<dyn Fs>,
|
||||
menu_handle: PopoverMenuHandle<LanguageModelSelector>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Self {
|
||||
Self {
|
||||
selector: cx.new_view(|cx| {
|
||||
let fs = fs.clone();
|
||||
LanguageModelSelector::new(
|
||||
move |model, cx| {
|
||||
update_settings_file::<AssistantSettings>(
|
||||
fs.clone(),
|
||||
cx,
|
||||
move |settings, _cx| settings.set_model(model.clone()),
|
||||
);
|
||||
},
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
menu_handle,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for AssistantModelSelector {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let active_model = LanguageModelRegistry::read_global(cx).active_model();
|
||||
let focus_handle = self.selector.focus_handle(cx).clone();
|
||||
|
||||
LanguageModelSelectorPopoverMenu::new(
|
||||
self.selector.clone(),
|
||||
ButtonLike::new("active-model")
|
||||
.style(ButtonStyle::Subtle)
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
div()
|
||||
.overflow_x_hidden()
|
||||
.flex_grow()
|
||||
.whitespace_nowrap()
|
||||
.child(match active_model {
|
||||
Some(model) => h_flex()
|
||||
.child(
|
||||
Label::new(model.name().0)
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.into_any_element(),
|
||||
_ => Label::new("No model selected")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.into_any_element(),
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Icon::new(IconName::ChevronDown)
|
||||
.color(Color::Muted)
|
||||
.size(IconSize::XSmall),
|
||||
),
|
||||
)
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::for_action_in("Change Model", &ToggleModelSelector, &focus_handle, cx)
|
||||
}),
|
||||
)
|
||||
.with_handle(self.menu_handle.clone())
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ use gpui::{
|
||||
WeakModel, WeakView,
|
||||
};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use release_channel::ReleaseChannel;
|
||||
use ui::{prelude::*, ListItem, ListItemSpacing};
|
||||
use util::ResultExt;
|
||||
use workspace::Workspace;
|
||||
@@ -50,23 +51,27 @@ impl ContextPicker {
|
||||
confirm_behavior: ConfirmBehavior,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let mut entries = vec![
|
||||
ContextPickerEntry {
|
||||
name: "File".into(),
|
||||
kind: ContextKind::File,
|
||||
icon: IconName::File,
|
||||
},
|
||||
ContextPickerEntry {
|
||||
let mut entries = Vec::new();
|
||||
entries.push(ContextPickerEntry {
|
||||
name: "File".into(),
|
||||
kind: ContextKind::File,
|
||||
icon: IconName::File,
|
||||
});
|
||||
let release_channel = ReleaseChannel::global(cx);
|
||||
// The directory context picker isn't fully implemented yet, so limit it
|
||||
// to development builds.
|
||||
if release_channel == ReleaseChannel::Dev {
|
||||
entries.push(ContextPickerEntry {
|
||||
name: "Folder".into(),
|
||||
kind: ContextKind::Directory,
|
||||
icon: IconName::Folder,
|
||||
},
|
||||
ContextPickerEntry {
|
||||
name: "Fetch".into(),
|
||||
kind: ContextKind::FetchedUrl,
|
||||
icon: IconName::Globe,
|
||||
},
|
||||
];
|
||||
});
|
||||
}
|
||||
entries.push(ContextPickerEntry {
|
||||
name: "Fetch".into(),
|
||||
kind: ContextKind::FetchedUrl,
|
||||
icon: IconName::Globe,
|
||||
});
|
||||
|
||||
if thread_store.is_some() {
|
||||
entries.push(ContextPickerEntry {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// TODO: Remove this once we've implemented the functionality.
|
||||
// TODO: Remove this when we finish the implementation.
|
||||
#![allow(unused)]
|
||||
|
||||
use std::path::Path;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::Arc;
|
||||
|
||||
use fuzzy::PathMatch;
|
||||
@@ -11,6 +13,7 @@ use ui::{prelude::*, ListItem};
|
||||
use util::ResultExt as _;
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::context::ContextKind;
|
||||
use crate::context_picker::{ConfirmBehavior, ContextPicker};
|
||||
use crate::context_store::ContextStore;
|
||||
|
||||
@@ -75,6 +78,65 @@ impl DirectoryContextPickerDelegate {
|
||||
selected_index: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn search(
|
||||
&mut self,
|
||||
query: String,
|
||||
cancellation_flag: Arc<AtomicBool>,
|
||||
workspace: &View<Workspace>,
|
||||
cx: &mut ViewContext<Picker<Self>>,
|
||||
) -> Task<Vec<PathMatch>> {
|
||||
if query.is_empty() {
|
||||
let workspace = workspace.read(cx);
|
||||
let project = workspace.project().read(cx);
|
||||
let directory_matches = project.worktrees(cx).flat_map(|worktree| {
|
||||
let worktree = worktree.read(cx);
|
||||
let path_prefix: Arc<str> = worktree.root_name().into();
|
||||
worktree.directories(false, 0).map(move |entry| PathMatch {
|
||||
score: 0.,
|
||||
positions: Vec::new(),
|
||||
worktree_id: worktree.id().to_usize(),
|
||||
path: entry.path.clone(),
|
||||
path_prefix: path_prefix.clone(),
|
||||
distance_to_relative_ancestor: 0,
|
||||
is_dir: true,
|
||||
})
|
||||
});
|
||||
|
||||
Task::ready(directory_matches.collect())
|
||||
} else {
|
||||
let worktrees = workspace.read(cx).visible_worktrees(cx).collect::<Vec<_>>();
|
||||
let candidate_sets = worktrees
|
||||
.into_iter()
|
||||
.map(|worktree| {
|
||||
let worktree = worktree.read(cx);
|
||||
|
||||
PathMatchCandidateSet {
|
||||
snapshot: worktree.snapshot(),
|
||||
include_ignored: worktree
|
||||
.root_entry()
|
||||
.map_or(false, |entry| entry.is_ignored),
|
||||
include_root_name: true,
|
||||
candidates: project::Candidates::Directories,
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let executor = cx.background_executor().clone();
|
||||
cx.foreground_executor().spawn(async move {
|
||||
fuzzy::match_path_sets(
|
||||
candidate_sets.as_slice(),
|
||||
query.as_str(),
|
||||
None,
|
||||
false,
|
||||
100,
|
||||
&cancellation_flag,
|
||||
executor,
|
||||
)
|
||||
.await
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PickerDelegate for DirectoryContextPickerDelegate {
|
||||
@@ -88,7 +150,7 @@ impl PickerDelegate for DirectoryContextPickerDelegate {
|
||||
self.selected_index
|
||||
}
|
||||
|
||||
fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>) {
|
||||
fn set_selected_index(&mut self, ix: usize, _cx: &mut ViewContext<Picker<Self>>) {
|
||||
self.selected_index = ix;
|
||||
}
|
||||
|
||||
@@ -96,17 +158,67 @@ impl PickerDelegate for DirectoryContextPickerDelegate {
|
||||
"Search folders…".into()
|
||||
}
|
||||
|
||||
fn update_matches(&mut self, _query: String, _cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
|
||||
// TODO: Implement this once we fix the issues with the file context picker.
|
||||
Task::ready(())
|
||||
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
|
||||
let Some(workspace) = self.workspace.upgrade() else {
|
||||
return Task::ready(());
|
||||
};
|
||||
|
||||
let search_task = self.search(query, Arc::<AtomicBool>::default(), &workspace, cx);
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let mut paths = search_task.await;
|
||||
let empty_path = Path::new("");
|
||||
paths.retain(|path_match| path_match.path.as_ref() != empty_path);
|
||||
|
||||
this.update(&mut cx, |this, _cx| {
|
||||
this.delegate.matches = paths;
|
||||
})
|
||||
.log_err();
|
||||
})
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
|
||||
// TODO: Implement this once we fix the issues with the file context picker.
|
||||
match self.confirm_behavior {
|
||||
ConfirmBehavior::KeepOpen => {}
|
||||
ConfirmBehavior::Close => self.dismissed(cx),
|
||||
}
|
||||
let Some(mat) = self.matches.get(self.selected_index) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let workspace = self.workspace.clone();
|
||||
let Some(project) = workspace
|
||||
.upgrade()
|
||||
.map(|workspace| workspace.read(cx).project().clone())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let path = mat.path.clone();
|
||||
let worktree_id = WorktreeId::from_usize(mat.worktree_id);
|
||||
let confirm_behavior = self.confirm_behavior;
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
let mut text = String::new();
|
||||
|
||||
// TODO: Add the files from the selected directory.
|
||||
|
||||
this.delegate
|
||||
.context_store
|
||||
.update(cx, |context_store, cx| {
|
||||
context_store.insert_context(
|
||||
ContextKind::Directory,
|
||||
path.to_string_lossy().to_string(),
|
||||
text,
|
||||
);
|
||||
})?;
|
||||
|
||||
match confirm_behavior {
|
||||
ConfirmBehavior::KeepOpen => {}
|
||||
ConfirmBehavior::Close => this.delegate.dismissed(cx),
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
})??;
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx)
|
||||
}
|
||||
|
||||
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
|
||||
@@ -120,10 +232,18 @@ impl PickerDelegate for DirectoryContextPickerDelegate {
|
||||
|
||||
fn render_match(
|
||||
&self,
|
||||
_ix: usize,
|
||||
_selected: bool,
|
||||
ix: usize,
|
||||
selected: bool,
|
||||
_cx: &mut ViewContext<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
None
|
||||
let path_match = &self.matches[ix];
|
||||
let directory_name = path_match.path.to_string_lossy().to_string();
|
||||
|
||||
Some(
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
.toggle_state(selected)
|
||||
.child(h_flex().gap_2().child(Label::new(directory_name))),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,7 +192,9 @@ impl PickerDelegate for FileContextPickerDelegate {
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
|
||||
let mat = &self.matches[self.selected_index];
|
||||
let Some(mat) = self.matches.get(self.selected_index) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let workspace = self.workspace.clone();
|
||||
let Some(project) = workspace
|
||||
|
||||
@@ -154,7 +154,9 @@ impl PickerDelegate for ThreadContextPickerDelegate {
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
|
||||
let entry = &self.matches[self.selected_index];
|
||||
let Some(entry) = self.matches.get(self.selected_index) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(thread_store) = self.thread_store.upgrade() else {
|
||||
return;
|
||||
@@ -216,7 +218,7 @@ impl PickerDelegate for ThreadContextPickerDelegate {
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
.toggle_state(selected)
|
||||
.child(thread.summary.clone()),
|
||||
.child(Label::new(thread.summary.clone())),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
use crate::assistant_model_selector::AssistantModelSelector;
|
||||
use crate::buffer_codegen::BufferCodegen;
|
||||
use crate::context_picker::ContextPicker;
|
||||
use crate::context_store::ContextStore;
|
||||
use crate::context_strip::ContextStrip;
|
||||
use crate::terminal_codegen::TerminalCodegen;
|
||||
use crate::thread_store::ThreadStore;
|
||||
use crate::ToggleContextPicker;
|
||||
use crate::{
|
||||
assistant_settings::AssistantSettings, CycleNextInlineAssist, CyclePreviousInlineAssist,
|
||||
};
|
||||
use crate::{CycleNextInlineAssist, CyclePreviousInlineAssist};
|
||||
use crate::{ToggleContextPicker, ToggleModelSelector};
|
||||
use client::ErrorExt;
|
||||
use collections::VecDeque;
|
||||
use editor::{
|
||||
@@ -22,9 +21,9 @@ use gpui::{
|
||||
WeakModel, WeakView, WindowContext,
|
||||
};
|
||||
use language_model::{LanguageModel, LanguageModelRegistry};
|
||||
use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu};
|
||||
use language_model_selector::LanguageModelSelector;
|
||||
use parking_lot::Mutex;
|
||||
use settings::{update_settings_file, Settings};
|
||||
use settings::Settings;
|
||||
use std::cmp;
|
||||
use std::sync::Arc;
|
||||
use theme::ThemeSettings;
|
||||
@@ -39,7 +38,8 @@ pub struct PromptEditor<T> {
|
||||
mode: PromptEditorMode,
|
||||
context_strip: View<ContextStrip>,
|
||||
context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
|
||||
language_model_selector: View<LanguageModelSelector>,
|
||||
model_selector: View<AssistantModelSelector>,
|
||||
model_selector_menu_handle: PopoverMenuHandle<LanguageModelSelector>,
|
||||
edited_since_done: bool,
|
||||
prompt_history: VecDeque<String>,
|
||||
prompt_history_ix: Option<usize>,
|
||||
@@ -56,7 +56,7 @@ impl<T: 'static> Render for PromptEditor<T> {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let mut buttons = Vec::new();
|
||||
|
||||
let spacing = match &self.mode {
|
||||
let left_gutter_spacing = match &self.mode {
|
||||
PromptEditorMode::Buffer {
|
||||
id: _,
|
||||
codegen,
|
||||
@@ -72,23 +72,36 @@ impl<T: 'static> Render for PromptEditor<T> {
|
||||
|
||||
gutter_dimensions.full_width() + (gutter_dimensions.margin / 2.0)
|
||||
}
|
||||
PromptEditorMode::Terminal { .. } => Pixels::ZERO,
|
||||
PromptEditorMode::Terminal { .. } => {
|
||||
// Give the equivalent of the same left-padding that we're using on the right
|
||||
Pixels::from(40.0)
|
||||
}
|
||||
};
|
||||
|
||||
let bottom_padding = match &self.mode {
|
||||
PromptEditorMode::Buffer { .. } => Pixels::from(0.),
|
||||
PromptEditorMode::Terminal { .. } => Pixels::from(8.0),
|
||||
};
|
||||
|
||||
buttons.extend(self.render_buttons(cx));
|
||||
|
||||
v_flex()
|
||||
.key_context("PromptEditor")
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.block_mouse_down()
|
||||
.gap_0p5()
|
||||
.border_y_1()
|
||||
.border_color(cx.theme().status().info_border)
|
||||
.size_full()
|
||||
.py(cx.line_height() / 2.5)
|
||||
.pt_0p5()
|
||||
.pb(bottom_padding)
|
||||
.pr_6()
|
||||
.child(
|
||||
h_flex()
|
||||
.key_context("PromptEditor")
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.block_mouse_down()
|
||||
.items_start()
|
||||
.cursor(CursorStyle::Arrow)
|
||||
.on_action(cx.listener(Self::toggle_context_picker))
|
||||
.on_action(cx.listener(Self::toggle_model_selector))
|
||||
.on_action(cx.listener(Self::confirm))
|
||||
.on_action(cx.listener(Self::cancel))
|
||||
.on_action(cx.listener(Self::move_up))
|
||||
@@ -97,30 +110,11 @@ impl<T: 'static> Render for PromptEditor<T> {
|
||||
.capture_action(cx.listener(Self::cycle_next))
|
||||
.child(
|
||||
h_flex()
|
||||
.w(spacing)
|
||||
.h_full()
|
||||
.w(left_gutter_spacing)
|
||||
.justify_center()
|
||||
.gap_2()
|
||||
.child(LanguageModelSelectorPopoverMenu::new(
|
||||
self.language_model_selector.clone(),
|
||||
IconButton::new("context", IconName::SettingsAlt)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::with_meta(
|
||||
format!(
|
||||
"Using {}",
|
||||
LanguageModelRegistry::read_global(cx)
|
||||
.active_model()
|
||||
.map(|model| model.name().0)
|
||||
.unwrap_or_else(|| "No model selected".into()),
|
||||
),
|
||||
None,
|
||||
"Change Model",
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
))
|
||||
.child(self.render_close_button(cx))
|
||||
.map(|el| {
|
||||
let CodegenStatus::Error(error) = self.codegen_status(cx) else {
|
||||
return el;
|
||||
@@ -172,13 +166,24 @@ impl<T: 'static> Render for PromptEditor<T> {
|
||||
}
|
||||
}),
|
||||
)
|
||||
.child(div().flex_1().child(self.render_editor(cx)))
|
||||
.child(h_flex().gap_2().pr_6().children(buttons)),
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.justify_between()
|
||||
.child(div().flex_1().child(self.render_editor(cx)))
|
||||
.child(h_flex().gap_1().children(buttons)),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.child(h_flex().w(spacing).justify_center().gap_2())
|
||||
.child(self.context_strip.clone()),
|
||||
h_flex().child(div().w(left_gutter_spacing)).child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.pl_1()
|
||||
.items_start()
|
||||
.justify_between()
|
||||
.child(self.context_strip.clone())
|
||||
.child(self.model_selector.clone()),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -311,6 +316,10 @@ impl<T: 'static> PromptEditor<T> {
|
||||
self.context_picker_menu_handle.toggle(cx);
|
||||
}
|
||||
|
||||
fn toggle_model_selector(&mut self, _: &ToggleModelSelector, cx: &mut ViewContext<Self>) {
|
||||
self.model_selector_menu_handle.toggle(cx);
|
||||
}
|
||||
|
||||
fn cancel(&mut self, _: &editor::actions::Cancel, cx: &mut ViewContext<Self>) {
|
||||
match self.codegen_status(cx) {
|
||||
CodegenStatus::Idle | CodegenStatus::Done | CodegenStatus::Error(_) => {
|
||||
@@ -400,73 +409,45 @@ impl<T: 'static> PromptEditor<T> {
|
||||
|
||||
match codegen_status {
|
||||
CodegenStatus::Idle => {
|
||||
vec![
|
||||
IconButton::new("cancel", IconName::Close)
|
||||
.icon_color(Color::Muted)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(|cx| Tooltip::for_action("Cancel Assist", &menu::Cancel, cx))
|
||||
.on_click(
|
||||
cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::CancelRequested)),
|
||||
)
|
||||
.into_any_element(),
|
||||
Button::new("start", mode.start_label())
|
||||
.icon(IconName::Return)
|
||||
.icon_color(Color::Muted)
|
||||
.on_click(
|
||||
cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::StartRequested)),
|
||||
)
|
||||
.into_any_element(),
|
||||
]
|
||||
vec![Button::new("start", mode.start_label())
|
||||
.label_size(LabelSize::Small)
|
||||
.icon(IconName::Return)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Muted)
|
||||
.on_click(cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::StartRequested)))
|
||||
.into_any_element()]
|
||||
}
|
||||
CodegenStatus::Pending => vec![
|
||||
IconButton::new("cancel", IconName::Close)
|
||||
.icon_color(Color::Muted)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(|cx| Tooltip::text("Cancel Assist", cx))
|
||||
.on_click(cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::CancelRequested)))
|
||||
.into_any_element(),
|
||||
IconButton::new("stop", IconName::Stop)
|
||||
.icon_color(Color::Error)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::with_meta(
|
||||
mode.tooltip_interrupt(),
|
||||
Some(&menu::Cancel),
|
||||
"Changes won't be discarded",
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.on_click(cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::StopRequested)))
|
||||
.into_any_element(),
|
||||
],
|
||||
CodegenStatus::Pending => vec![IconButton::new("stop", IconName::Stop)
|
||||
.icon_color(Color::Error)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::with_meta(
|
||||
mode.tooltip_interrupt(),
|
||||
Some(&menu::Cancel),
|
||||
"Changes won't be discarded",
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.on_click(cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::StopRequested)))
|
||||
.into_any_element()],
|
||||
CodegenStatus::Done | CodegenStatus::Error(_) => {
|
||||
let cancel = IconButton::new("cancel", IconName::Close)
|
||||
.icon_color(Color::Muted)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(|cx| Tooltip::for_action("Cancel Assist", &menu::Cancel, cx))
|
||||
.on_click(cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::CancelRequested)))
|
||||
.into_any_element();
|
||||
|
||||
let has_error = matches!(codegen_status, CodegenStatus::Error(_));
|
||||
if has_error || self.edited_since_done {
|
||||
vec![
|
||||
cancel,
|
||||
IconButton::new("restart", IconName::RotateCw)
|
||||
.icon_color(Color::Info)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::with_meta(
|
||||
mode.tooltip_restart(),
|
||||
Some(&menu::Confirm),
|
||||
"Changes will be discarded",
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.on_click(cx.listener(|_, _, cx| {
|
||||
cx.emit(PromptEditorEvent::StartRequested);
|
||||
}))
|
||||
.into_any_element(),
|
||||
]
|
||||
vec![IconButton::new("restart", IconName::RotateCw)
|
||||
.icon_color(Color::Info)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::with_meta(
|
||||
mode.tooltip_restart(),
|
||||
Some(&menu::Confirm),
|
||||
"Changes will be discarded",
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.on_click(cx.listener(|_, _, cx| {
|
||||
cx.emit(PromptEditorEvent::StartRequested);
|
||||
}))
|
||||
.into_any_element()]
|
||||
} else {
|
||||
let accept = IconButton::new("accept", IconName::Check)
|
||||
.icon_color(Color::Info)
|
||||
@@ -482,7 +463,6 @@ impl<T: 'static> PromptEditor<T> {
|
||||
match &self.mode {
|
||||
PromptEditorMode::Terminal { .. } => vec![
|
||||
accept,
|
||||
cancel,
|
||||
IconButton::new("confirm", IconName::Play)
|
||||
.icon_color(Color::Info)
|
||||
.shape(IconButtonShape::Square)
|
||||
@@ -498,7 +478,7 @@ impl<T: 'static> PromptEditor<T> {
|
||||
}))
|
||||
.into_any_element(),
|
||||
],
|
||||
PromptEditorMode::Buffer { .. } => vec![accept, cancel],
|
||||
PromptEditorMode::Buffer { .. } => vec![accept],
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -527,6 +507,15 @@ impl<T: 'static> PromptEditor<T> {
|
||||
}
|
||||
}
|
||||
|
||||
fn render_close_button(&self, cx: &ViewContext<Self>) -> AnyElement {
|
||||
IconButton::new("cancel", IconName::Close)
|
||||
.icon_color(Color::Muted)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(|cx| Tooltip::text("Close Assistant", cx))
|
||||
.on_click(cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::CancelRequested)))
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_cycle_controls(&self, codegen: &BufferCodegen, cx: &ViewContext<Self>) -> AnyElement {
|
||||
let disabled = matches!(codegen.status(cx), CodegenStatus::Idle);
|
||||
|
||||
@@ -690,20 +679,18 @@ impl<T: 'static> PromptEditor<T> {
|
||||
let font_size = TextSize::Default.rems(cx);
|
||||
let line_height = font_size.to_pixels(cx.rem_size()) * 1.3;
|
||||
|
||||
v_flex()
|
||||
div()
|
||||
.key_context("MessageEditor")
|
||||
.size_full()
|
||||
.gap_2()
|
||||
.p_2()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.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_family: settings.buffer_font.family.clone(),
|
||||
font_features: settings.buffer_font.features.clone(),
|
||||
font_size: font_size.into(),
|
||||
font_weight: settings.ui_font.weight,
|
||||
line_height: line_height.into(),
|
||||
..Default::default()
|
||||
};
|
||||
@@ -795,6 +782,7 @@ impl PromptEditor<BufferCodegen> {
|
||||
editor
|
||||
});
|
||||
let context_picker_menu_handle = PopoverMenuHandle::default();
|
||||
let model_selector_menu_handle = PopoverMenuHandle::default();
|
||||
|
||||
let mut this: PromptEditor<BufferCodegen> = PromptEditor {
|
||||
editor: prompt_editor.clone(),
|
||||
@@ -809,19 +797,10 @@ impl PromptEditor<BufferCodegen> {
|
||||
)
|
||||
}),
|
||||
context_picker_menu_handle,
|
||||
language_model_selector: cx.new_view(|cx| {
|
||||
let fs = fs.clone();
|
||||
LanguageModelSelector::new(
|
||||
move |model, cx| {
|
||||
update_settings_file::<AssistantSettings>(
|
||||
fs.clone(),
|
||||
cx,
|
||||
move |settings, _| settings.set_model(model.clone()),
|
||||
);
|
||||
},
|
||||
cx,
|
||||
)
|
||||
model_selector: cx.new_view(|cx| {
|
||||
AssistantModelSelector::new(fs, model_selector_menu_handle.clone(), cx)
|
||||
}),
|
||||
model_selector_menu_handle,
|
||||
edited_since_done: false,
|
||||
prompt_history,
|
||||
prompt_history_ix: None,
|
||||
@@ -942,6 +921,7 @@ impl PromptEditor<TerminalCodegen> {
|
||||
editor
|
||||
});
|
||||
let context_picker_menu_handle = PopoverMenuHandle::default();
|
||||
let model_selector_menu_handle = PopoverMenuHandle::default();
|
||||
|
||||
let mut this = Self {
|
||||
editor: prompt_editor.clone(),
|
||||
@@ -956,19 +936,10 @@ impl PromptEditor<TerminalCodegen> {
|
||||
)
|
||||
}),
|
||||
context_picker_menu_handle,
|
||||
language_model_selector: cx.new_view(|cx| {
|
||||
let fs = fs.clone();
|
||||
LanguageModelSelector::new(
|
||||
move |model, cx| {
|
||||
update_settings_file::<AssistantSettings>(
|
||||
fs.clone(),
|
||||
cx,
|
||||
move |settings, _| settings.set_model(model.clone()),
|
||||
);
|
||||
},
|
||||
cx,
|
||||
)
|
||||
model_selector: cx.new_view(|cx| {
|
||||
AssistantModelSelector::new(fs, model_selector_menu_handle.clone(), cx)
|
||||
}),
|
||||
model_selector_menu_handle,
|
||||
edited_since_done: false,
|
||||
prompt_history,
|
||||
prompt_history_ix: None,
|
||||
|
||||
@@ -7,17 +7,17 @@ use gpui::{
|
||||
WeakView,
|
||||
};
|
||||
use language_model::{LanguageModelRegistry, LanguageModelRequestTool};
|
||||
use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu};
|
||||
use language_model_selector::LanguageModelSelector;
|
||||
use rope::Point;
|
||||
use settings::{update_settings_file, Settings};
|
||||
use settings::Settings;
|
||||
use theme::ThemeSettings;
|
||||
use ui::{
|
||||
prelude::*, ButtonLike, CheckboxWithLabel, ElevationIndex, KeyBinding, PopoverMenu,
|
||||
PopoverMenuHandle, Tooltip,
|
||||
prelude::*, ButtonLike, ElevationIndex, KeyBinding, PopoverMenu, PopoverMenuHandle,
|
||||
SwitchWithLabel,
|
||||
};
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::assistant_settings::AssistantSettings;
|
||||
use crate::assistant_model_selector::AssistantModelSelector;
|
||||
use crate::context_picker::{ConfirmBehavior, ContextPicker};
|
||||
use crate::context_store::ContextStore;
|
||||
use crate::context_strip::ContextStrip;
|
||||
@@ -33,8 +33,8 @@ pub struct MessageEditor {
|
||||
context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
|
||||
inline_context_picker: View<ContextPicker>,
|
||||
inline_context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
|
||||
language_model_selector: View<LanguageModelSelector>,
|
||||
language_model_selector_menu_handle: PopoverMenuHandle<LanguageModelSelector>,
|
||||
model_selector: View<AssistantModelSelector>,
|
||||
model_selector_menu_handle: PopoverMenuHandle<LanguageModelSelector>,
|
||||
use_tools: bool,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
@@ -50,6 +50,7 @@ impl MessageEditor {
|
||||
let context_store = cx.new_model(|_cx| ContextStore::new());
|
||||
let context_picker_menu_handle = PopoverMenuHandle::default();
|
||||
let inline_context_picker_menu_handle = PopoverMenuHandle::default();
|
||||
let model_selector_menu_handle = PopoverMenuHandle::default();
|
||||
|
||||
let editor = cx.new_view(|cx| {
|
||||
let mut editor = Editor::auto_height(10, cx);
|
||||
@@ -92,27 +93,17 @@ impl MessageEditor {
|
||||
context_picker_menu_handle,
|
||||
inline_context_picker,
|
||||
inline_context_picker_menu_handle,
|
||||
language_model_selector: cx.new_view(|cx| {
|
||||
let fs = fs.clone();
|
||||
LanguageModelSelector::new(
|
||||
move |model, cx| {
|
||||
update_settings_file::<AssistantSettings>(
|
||||
fs.clone(),
|
||||
cx,
|
||||
move |settings, _cx| settings.set_model(model.clone()),
|
||||
);
|
||||
},
|
||||
cx,
|
||||
)
|
||||
model_selector: cx.new_view(|cx| {
|
||||
AssistantModelSelector::new(fs, model_selector_menu_handle.clone(), cx)
|
||||
}),
|
||||
language_model_selector_menu_handle: PopoverMenuHandle::default(),
|
||||
model_selector_menu_handle,
|
||||
use_tools: false,
|
||||
_subscriptions: subscriptions,
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle_model_selector(&mut self, _: &ToggleModelSelector, cx: &mut ViewContext<Self>) {
|
||||
self.language_model_selector_menu_handle.toggle(cx);
|
||||
self.model_selector_menu_handle.toggle(cx)
|
||||
}
|
||||
|
||||
fn toggle_context_picker(&mut self, _: &ToggleContextPicker, cx: &mut ViewContext<Self>) {
|
||||
@@ -203,50 +194,6 @@ impl MessageEditor {
|
||||
let editor_focus_handle = self.editor.focus_handle(cx);
|
||||
cx.focus(&editor_focus_handle);
|
||||
}
|
||||
|
||||
fn render_language_model_selector(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let active_model = LanguageModelRegistry::read_global(cx).active_model();
|
||||
let focus_handle = self.language_model_selector.focus_handle(cx).clone();
|
||||
|
||||
LanguageModelSelectorPopoverMenu::new(
|
||||
self.language_model_selector.clone(),
|
||||
ButtonLike::new("active-model")
|
||||
.style(ButtonStyle::Subtle)
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
div()
|
||||
.overflow_x_hidden()
|
||||
.flex_grow()
|
||||
.whitespace_nowrap()
|
||||
.child(match active_model {
|
||||
Some(model) => h_flex()
|
||||
.child(
|
||||
Label::new(model.name().0)
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.into_any_element(),
|
||||
_ => Label::new("No model selected")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.into_any_element(),
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Icon::new(IconName::ChevronDown)
|
||||
.color(Color::Muted)
|
||||
.size(IconSize::XSmall),
|
||||
),
|
||||
)
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::for_action_in("Change Model", &ToggleModelSelector, &focus_handle, cx)
|
||||
}),
|
||||
)
|
||||
.with_handle(self.language_model_selector_menu_handle.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl FocusableView for MessageEditor {
|
||||
@@ -309,7 +256,7 @@ impl Render for MessageEditor {
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_between()
|
||||
.child(CheckboxWithLabel::new(
|
||||
.child(SwitchWithLabel::new(
|
||||
"use-tools",
|
||||
Label::new("Tools"),
|
||||
self.use_tools.into(),
|
||||
@@ -321,22 +268,19 @@ impl Render for MessageEditor {
|
||||
}),
|
||||
))
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(self.render_language_model_selector(cx))
|
||||
.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);
|
||||
}),
|
||||
),
|
||||
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);
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -39,10 +39,17 @@ impl EventEmitter<ToolbarItemEvent> for Breadcrumbs {}
|
||||
impl Render for Breadcrumbs {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
const MAX_SEGMENTS: usize = 12;
|
||||
let element = h_flex().text_ui(cx);
|
||||
|
||||
let element = h_flex()
|
||||
.id("breadcrumb-container")
|
||||
.flex_grow()
|
||||
.overflow_x_scroll()
|
||||
.text_ui(cx);
|
||||
|
||||
let Some(active_item) = self.active_item.as_ref() else {
|
||||
return element;
|
||||
};
|
||||
|
||||
let Some(mut segments) = active_item.breadcrumbs(cx.theme(), cx) else {
|
||||
return element;
|
||||
};
|
||||
@@ -52,6 +59,7 @@ impl Render for Breadcrumbs {
|
||||
prefix_end_ix,
|
||||
segments.len().saturating_sub(MAX_SEGMENTS / 2),
|
||||
);
|
||||
|
||||
if suffix_start_ix > prefix_end_ix {
|
||||
segments.splice(
|
||||
prefix_end_ix..suffix_start_ix,
|
||||
@@ -82,6 +90,7 @@ impl Render for Breadcrumbs {
|
||||
});
|
||||
|
||||
let breadcrumbs_stack = h_flex().gap_1().children(breadcrumbs);
|
||||
|
||||
match active_item
|
||||
.downcast::<Editor>()
|
||||
.map(|editor| editor.downgrade())
|
||||
@@ -102,14 +111,14 @@ impl Render for Breadcrumbs {
|
||||
if let Some(editor) = editor.upgrade() {
|
||||
let focus_handle = editor.read(cx).focus_handle(cx);
|
||||
Tooltip::for_action_in(
|
||||
"Show symbol outline",
|
||||
"Show Symbol Outline",
|
||||
&editor::actions::ToggleOutline,
|
||||
&focus_handle,
|
||||
cx,
|
||||
)
|
||||
} else {
|
||||
Tooltip::for_action(
|
||||
"Show symbol outline",
|
||||
"Show Symbol Outline",
|
||||
&editor::actions::ToggleOutline,
|
||||
cx,
|
||||
)
|
||||
|
||||
@@ -18,6 +18,12 @@ use std::{
|
||||
use tempfile::NamedTempFile;
|
||||
use util::paths::PathWithPosition;
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
use {
|
||||
std::io::IsTerminal,
|
||||
util::{load_login_shell_environment, load_shell_from_passwd, ResultExt},
|
||||
};
|
||||
|
||||
struct Detect;
|
||||
|
||||
trait InstalledApp {
|
||||
@@ -161,7 +167,16 @@ fn main() -> Result<()> {
|
||||
None
|
||||
};
|
||||
|
||||
// On Linux, desktop entry uses `cli` to spawn `zed`, so we need to load env vars from the shell
|
||||
// since it doesn't inherit env vars from the terminal.
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
if !std::io::stdout().is_terminal() {
|
||||
load_shell_from_passwd().log_err();
|
||||
load_login_shell_environment().log_err();
|
||||
}
|
||||
|
||||
let env = Some(std::env::vars().collect::<HashMap<_, _>>());
|
||||
|
||||
let exit_status = Arc::new(Mutex::new(None));
|
||||
let mut paths = vec![];
|
||||
let mut urls = vec![];
|
||||
|
||||
@@ -252,7 +252,7 @@ spec:
|
||||
value: "${AUTO_JOIN_CHANNEL_ID}"
|
||||
securityContext:
|
||||
capabilities:
|
||||
# FIXME - Switch to the more restrictive `PERFMON` capability.
|
||||
# TODO - Switch to the more restrictive `PERFMON` capability.
|
||||
# This capability isn't yet available in a stable version of Debian.
|
||||
add: ["SYS_ADMIN"]
|
||||
terminationGracePeriodSeconds: 10
|
||||
|
||||
@@ -279,6 +279,7 @@ pub async fn post_panic(
|
||||
|
||||
let report: telemetry_events::PanicRequest = serde_json::from_slice(&body)
|
||||
.map_err(|_| Error::http(StatusCode::BAD_REQUEST, "invalid json".into()))?;
|
||||
let incident_id = uuid::Uuid::new_v4().to_string();
|
||||
let panic = report.panic;
|
||||
|
||||
if panic.os_name == "Linux" && panic.os_version == Some("1.0.0".to_string()) {
|
||||
@@ -288,11 +289,37 @@ pub async fn post_panic(
|
||||
))?;
|
||||
}
|
||||
|
||||
if let Some(blob_store_client) = app.blob_store_client.as_ref() {
|
||||
let response = blob_store_client
|
||||
.head_object()
|
||||
.bucket(CRASH_REPORTS_BUCKET)
|
||||
.key(incident_id.clone() + ".json")
|
||||
.send()
|
||||
.await;
|
||||
|
||||
if response.is_ok() {
|
||||
log::info!("We've already uploaded this crash");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
blob_store_client
|
||||
.put_object()
|
||||
.bucket(CRASH_REPORTS_BUCKET)
|
||||
.key(incident_id.clone() + ".json")
|
||||
.acl(aws_sdk_s3::types::ObjectCannedAcl::PublicRead)
|
||||
.body(ByteStream::from(body.to_vec()))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| log::error!("Failed to upload crash: {}", e))
|
||||
.ok();
|
||||
}
|
||||
|
||||
tracing::error!(
|
||||
service = "client",
|
||||
version = %panic.app_version,
|
||||
os_name = %panic.os_name,
|
||||
os_version = %panic.os_version.clone().unwrap_or_default(),
|
||||
incident_id = %incident_id,
|
||||
installation_id = %panic.installation_id.clone().unwrap_or_default(),
|
||||
description = %panic.payload,
|
||||
backtrace = %panic.backtrace.join("\n"),
|
||||
@@ -331,10 +358,19 @@ pub async fn post_panic(
|
||||
panic.app_version
|
||||
)))
|
||||
.add_field({
|
||||
let hostname = app.config.blob_store_url.clone().unwrap_or_default();
|
||||
let hostname = hostname.strip_prefix("https://").unwrap_or_else(|| {
|
||||
hostname.strip_prefix("http://").unwrap_or_default()
|
||||
});
|
||||
|
||||
slack::Text::markdown(format!(
|
||||
"*OS:*\n{} {}",
|
||||
"*{} {}:*\n<https://{}.{}/{}.json|{}…>",
|
||||
panic.os_name,
|
||||
panic.os_version.unwrap_or_default()
|
||||
panic.os_version.unwrap_or_default(),
|
||||
CRASH_REPORTS_BUCKET,
|
||||
hostname,
|
||||
incident_id,
|
||||
incident_id.chars().take(8).collect::<String>(),
|
||||
))
|
||||
})
|
||||
})
|
||||
@@ -361,6 +397,12 @@ pub async fn post_panic(
|
||||
}
|
||||
|
||||
fn report_to_slack(panic: &Panic) -> bool {
|
||||
// Panics on macOS should make their way to Slack as a crash report,
|
||||
// so we don't need to send them a second time via this channel.
|
||||
if panic.os_name == "macOS" {
|
||||
return false;
|
||||
}
|
||||
|
||||
if panic.payload.contains("ERROR_SURFACE_LOST_KHR") {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ collections.workspace = true
|
||||
ctor.workspace = true
|
||||
editor.workspace = true
|
||||
env_logger.workspace = true
|
||||
feature_flags.workspace = true
|
||||
gpui.workspace = true
|
||||
language.workspace = true
|
||||
log.workspace = true
|
||||
|
||||
@@ -14,6 +14,7 @@ use editor::{
|
||||
scroll::Autoscroll,
|
||||
Editor, EditorEvent, ExcerptId, ExcerptRange, MultiBuffer, ToOffset,
|
||||
};
|
||||
use feature_flags::FeatureFlagAppExt;
|
||||
use gpui::{
|
||||
actions, div, svg, AnyElement, AnyView, AppContext, Context, EventEmitter, FocusHandle,
|
||||
FocusableView, Global, HighlightStyle, InteractiveElement, IntoElement, Model, ParentElement,
|
||||
@@ -21,7 +22,8 @@ use gpui::{
|
||||
WeakView, WindowContext,
|
||||
};
|
||||
use language::{
|
||||
Bias, Buffer, Diagnostic, DiagnosticEntry, DiagnosticSeverity, Point, Selection, SelectionGoal,
|
||||
Bias, Buffer, BufferRow, BufferSnapshot, Diagnostic, DiagnosticEntry, DiagnosticSeverity,
|
||||
Point, Selection, SelectionGoal, ToTreeSitterPoint,
|
||||
};
|
||||
use lsp::LanguageServerId;
|
||||
use project::{DiagnosticSummary, Project, ProjectPath};
|
||||
@@ -29,9 +31,10 @@ use project_diagnostics_settings::ProjectDiagnosticsSettings;
|
||||
use settings::Settings;
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
cmp,
|
||||
cmp::Ordering,
|
||||
mem,
|
||||
ops::Range,
|
||||
ops::{Range, RangeInclusive},
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
@@ -422,31 +425,28 @@ impl ProjectDiagnosticsEditor {
|
||||
blocks: Default::default(),
|
||||
block_count: 0,
|
||||
};
|
||||
let mut pending_range: Option<(Range<Point>, usize)> = None;
|
||||
let mut pending_range: Option<(Range<Point>, Range<Point>, usize)> = None;
|
||||
let mut is_first_excerpt_for_group = true;
|
||||
for (ix, entry) in group.entries.iter().map(Some).chain([None]).enumerate() {
|
||||
let resolved_entry = entry.map(|e| e.resolve::<Point>(&snapshot));
|
||||
if let Some((range, start_ix)) = &mut pending_range {
|
||||
if let Some(entry) = resolved_entry.as_ref() {
|
||||
if entry.range.start.row <= range.end.row + 1 + self.context * 2 {
|
||||
range.end = range.end.max(entry.range.end);
|
||||
let expanded_range = resolved_entry.as_ref().map(|entry| {
|
||||
context_range_for_entry(entry, self.context, &snapshot, cx)
|
||||
});
|
||||
if let Some((range, context_range, start_ix)) = &mut pending_range {
|
||||
if let Some(expanded_range) = expanded_range.clone() {
|
||||
// If the entries are overlapping or next to each-other, merge them into one excerpt.
|
||||
if context_range.end.row + 1 >= expanded_range.start.row {
|
||||
context_range.end = context_range.end.max(expanded_range.end);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
let excerpt_start =
|
||||
Point::new(range.start.row.saturating_sub(self.context), 0);
|
||||
let excerpt_end = snapshot.clip_point(
|
||||
Point::new(range.end.row + self.context, u32::MAX),
|
||||
Bias::Left,
|
||||
);
|
||||
|
||||
let excerpt_id = excerpts
|
||||
.insert_excerpts_after(
|
||||
prev_excerpt_id,
|
||||
buffer.clone(),
|
||||
[ExcerptRange {
|
||||
context: excerpt_start..excerpt_end,
|
||||
context: context_range.clone(),
|
||||
primary: Some(range.clone()),
|
||||
}],
|
||||
cx,
|
||||
@@ -503,8 +503,9 @@ impl ProjectDiagnosticsEditor {
|
||||
pending_range.take();
|
||||
}
|
||||
|
||||
if let Some(entry) = resolved_entry {
|
||||
pending_range = Some((entry.range.clone(), ix));
|
||||
if let Some(entry) = resolved_entry.as_ref() {
|
||||
let range = entry.range.clone();
|
||||
pending_range = Some((range, expanded_range.unwrap(), ix));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -923,3 +924,169 @@ fn compare_diagnostics(
|
||||
})
|
||||
.then_with(|| old.diagnostic.message.cmp(&new.diagnostic.message))
|
||||
}
|
||||
|
||||
const DIAGNOSTIC_EXPANSION_ROW_LIMIT: u32 = 32;
|
||||
|
||||
fn context_range_for_entry(
|
||||
entry: &DiagnosticEntry<Point>,
|
||||
context: u32,
|
||||
snapshot: &BufferSnapshot,
|
||||
cx: &AppContext,
|
||||
) -> Range<Point> {
|
||||
if cx.is_staff() {
|
||||
if let Some(rows) = heuristic_syntactic_expand(
|
||||
entry.range.clone(),
|
||||
DIAGNOSTIC_EXPANSION_ROW_LIMIT,
|
||||
snapshot,
|
||||
cx,
|
||||
) {
|
||||
return Range {
|
||||
start: Point::new(*rows.start(), 0),
|
||||
end: snapshot.clip_point(Point::new(*rows.end(), u32::MAX), Bias::Left),
|
||||
};
|
||||
}
|
||||
}
|
||||
Range {
|
||||
start: Point::new(entry.range.start.row.saturating_sub(context), 0),
|
||||
end: snapshot.clip_point(
|
||||
Point::new(entry.range.end.row + context, u32::MAX),
|
||||
Bias::Left,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// Expands the input range using syntax information from TreeSitter. This expansion will be limited
|
||||
/// to the specified `max_row_count`.
|
||||
///
|
||||
/// If there is a containing outline item that is less than `max_row_count`, it will be returned.
|
||||
/// Otherwise fairly arbitrary heuristics are applied to attempt to return a logical block of code.
|
||||
fn heuristic_syntactic_expand<'a>(
|
||||
input_range: Range<Point>,
|
||||
max_row_count: u32,
|
||||
snapshot: &'a BufferSnapshot,
|
||||
cx: &'a AppContext,
|
||||
) -> Option<RangeInclusive<BufferRow>> {
|
||||
let input_row_count = input_range.end.row - input_range.start.row;
|
||||
if input_row_count > max_row_count {
|
||||
return None;
|
||||
}
|
||||
|
||||
// If the outline node contains the diagnostic and is small enough, just use that.
|
||||
let outline_range = snapshot.outline_range_containing(input_range.clone());
|
||||
if let Some(outline_range) = outline_range.clone() {
|
||||
// Remove blank lines from start and end
|
||||
if let Some(start_row) = (outline_range.start.row..outline_range.end.row)
|
||||
.find(|row| !snapshot.line_indent_for_row(*row).is_line_blank())
|
||||
{
|
||||
if let Some(end_row) = (outline_range.start.row..outline_range.end.row + 1)
|
||||
.rev()
|
||||
.find(|row| !snapshot.line_indent_for_row(*row).is_line_blank())
|
||||
{
|
||||
let row_count = end_row.saturating_sub(start_row);
|
||||
if row_count <= max_row_count {
|
||||
return Some(RangeInclusive::new(
|
||||
outline_range.start.row,
|
||||
outline_range.end.row,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut node = snapshot.syntax_ancestor(input_range.clone())?;
|
||||
loop {
|
||||
let node_start = Point::from_ts_point(node.start_position());
|
||||
let node_end = Point::from_ts_point(node.end_position());
|
||||
let node_range = node_start..node_end;
|
||||
let row_count = node_end.row - node_start.row + 1;
|
||||
|
||||
// Stop if we've exceeded the row count or reached an outline node. Then, find the interval
|
||||
// of node children which contains the query range. For example, this allows just returning
|
||||
// the header of a declaration rather than the entire declaration.
|
||||
if row_count > max_row_count || outline_range == Some(node_range.clone()) {
|
||||
let mut cursor = node.walk();
|
||||
let mut included_child_start = None;
|
||||
let mut included_child_end = None;
|
||||
let mut previous_end = node_start;
|
||||
if cursor.goto_first_child() {
|
||||
loop {
|
||||
let child_node = cursor.node();
|
||||
let child_range = previous_end..Point::from_ts_point(child_node.end_position());
|
||||
if included_child_start.is_none() && child_range.contains(&input_range.start) {
|
||||
included_child_start = Some(child_range.start);
|
||||
}
|
||||
if child_range.contains(&input_range.end) {
|
||||
included_child_end = Some(child_range.end);
|
||||
}
|
||||
previous_end = child_range.end;
|
||||
if !cursor.goto_next_sibling() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
let end = included_child_end.unwrap_or(node_range.end);
|
||||
if let Some(start) = included_child_start {
|
||||
let row_count = end.row - start.row;
|
||||
if row_count < max_row_count {
|
||||
return Some(RangeInclusive::new(start.row, end.row));
|
||||
}
|
||||
}
|
||||
|
||||
log::info!(
|
||||
"Expanding to ancestor started on {} node exceeding row limit of {max_row_count}.",
|
||||
node.grammar_name()
|
||||
);
|
||||
return None;
|
||||
}
|
||||
|
||||
let node_name = node.grammar_name();
|
||||
let node_row_range = RangeInclusive::new(node_range.start.row, node_range.end.row);
|
||||
if node_name.ends_with("block") {
|
||||
return Some(node_row_range);
|
||||
} else if node_name.ends_with("statement") || node_name.ends_with("declaration") {
|
||||
// Expand to the nearest dedent or blank line for statements and declarations.
|
||||
let tab_size = snapshot.settings_at(node_range.start, cx).tab_size.get();
|
||||
let indent_level = snapshot
|
||||
.line_indent_for_row(node_range.start.row)
|
||||
.len(tab_size);
|
||||
let rows_remaining = max_row_count.saturating_sub(row_count);
|
||||
let Some(start_row) = (node_range.start.row.saturating_sub(rows_remaining)
|
||||
..node_range.start.row)
|
||||
.rev()
|
||||
.find(|row| is_line_blank_or_indented_less(indent_level, *row, tab_size, snapshot))
|
||||
else {
|
||||
return Some(node_row_range);
|
||||
};
|
||||
let rows_remaining = max_row_count.saturating_sub(node_range.end.row - start_row);
|
||||
let Some(end_row) = (node_range.end.row + 1
|
||||
..cmp::min(
|
||||
node_range.end.row + rows_remaining + 1,
|
||||
snapshot.row_count(),
|
||||
))
|
||||
.find(|row| is_line_blank_or_indented_less(indent_level, *row, tab_size, snapshot))
|
||||
else {
|
||||
return Some(node_row_range);
|
||||
};
|
||||
return Some(RangeInclusive::new(start_row, end_row));
|
||||
}
|
||||
|
||||
// TODO: doing this instead of walking a cursor as that doesn't work - why?
|
||||
let Some(parent) = node.parent() else {
|
||||
log::info!(
|
||||
"Expanding to ancestor reached the top node, so using default context line count.",
|
||||
);
|
||||
return None;
|
||||
};
|
||||
node = parent;
|
||||
}
|
||||
}
|
||||
|
||||
fn is_line_blank_or_indented_less(
|
||||
indent_level: u32,
|
||||
row: u32,
|
||||
tab_size: u32,
|
||||
snapshot: &BufferSnapshot,
|
||||
) -> bool {
|
||||
let line_indent = snapshot.line_indent_for_row(row);
|
||||
line_indent.is_line_blank() || line_indent.len(tab_size) < indent_level
|
||||
}
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
use std::cell::RefCell;
|
||||
use std::{cmp::Reverse, ops::Range, rc::Rc};
|
||||
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use gpui::{
|
||||
div, px, uniform_list, AnyElement, BackgroundExecutor, Div, FontWeight, ListSizingBehavior,
|
||||
Model, ScrollStrategy, SharedString, StrikethroughStyle, StyledText, UniformListScrollHandle,
|
||||
ViewContext, WeakView,
|
||||
Model, ScrollStrategy, SharedString, Size, StrikethroughStyle, StyledText,
|
||||
UniformListScrollHandle, ViewContext, WeakView,
|
||||
};
|
||||
use language::Buffer;
|
||||
use language::{CodeLabel, Documentation};
|
||||
@@ -13,6 +10,13 @@ use lsp::LanguageServerId;
|
||||
use multi_buffer::{Anchor, ExcerptId};
|
||||
use ordered_float::OrderedFloat;
|
||||
use project::{CodeAction, Completion, TaskSourceKind};
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
cmp::{min, Reverse},
|
||||
iter,
|
||||
ops::Range,
|
||||
rc::Rc,
|
||||
};
|
||||
use task::ResolvedTask;
|
||||
use ui::{prelude::*, Color, IntoElement, ListItem, Pixels, Popover, Styled};
|
||||
use util::ResultExt;
|
||||
@@ -26,7 +30,10 @@ use crate::{
|
||||
};
|
||||
use crate::{AcceptInlineCompletion, InlineCompletionMenuHint, InlineCompletionText};
|
||||
|
||||
pub const MAX_COMPLETIONS_ASIDE_WIDTH: Pixels = px(500.);
|
||||
pub const MENU_GAP: Pixels = px(4.);
|
||||
pub const MENU_ASIDE_X_PADDING: Pixels = px(16.);
|
||||
pub const MENU_ASIDE_MIN_WIDTH: Pixels = px(260.);
|
||||
pub const MENU_ASIDE_MAX_WIDTH: Pixels = px(500.);
|
||||
|
||||
pub enum CodeContextMenu {
|
||||
Completions(CompletionsMenu),
|
||||
@@ -127,14 +134,12 @@ impl CodeContextMenu {
|
||||
pub fn render_aside(
|
||||
&self,
|
||||
style: &EditorStyle,
|
||||
max_height: Pixels,
|
||||
max_size: Size<Pixels>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) -> Option<AnyElement> {
|
||||
match self {
|
||||
CodeContextMenu::Completions(menu) => {
|
||||
menu.render_aside(style, max_height, workspace, cx)
|
||||
}
|
||||
CodeContextMenu::Completions(menu) => menu.render_aside(style, max_size, workspace, cx),
|
||||
CodeContextMenu::CodeActions(_) => None,
|
||||
}
|
||||
}
|
||||
@@ -158,6 +163,7 @@ pub struct CompletionsMenu {
|
||||
scroll_handle: UniformListScrollHandle,
|
||||
resolve_completions: bool,
|
||||
show_completion_documentation: bool,
|
||||
last_rendered_range: Rc<RefCell<Option<Range<usize>>>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -193,6 +199,7 @@ impl CompletionsMenu {
|
||||
selected_item: 0,
|
||||
scroll_handle: UniformListScrollHandle::new(),
|
||||
resolve_completions: true,
|
||||
last_rendered_range: RefCell::new(None).into(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -249,6 +256,7 @@ impl CompletionsMenu {
|
||||
scroll_handle: UniformListScrollHandle::new(),
|
||||
resolve_completions: false,
|
||||
show_completion_documentation: false,
|
||||
last_rendered_range: RefCell::new(None).into(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -257,11 +265,7 @@ impl CompletionsMenu {
|
||||
provider: Option<&dyn CompletionProvider>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) {
|
||||
self.selected_item = 0;
|
||||
self.scroll_handle
|
||||
.scroll_to_item(self.selected_item, ScrollStrategy::Top);
|
||||
self.resolve_selected_completion(provider, cx);
|
||||
cx.notify();
|
||||
self.update_selection_index(0, provider, cx);
|
||||
}
|
||||
|
||||
fn select_prev(
|
||||
@@ -269,15 +273,7 @@ impl CompletionsMenu {
|
||||
provider: Option<&dyn CompletionProvider>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) {
|
||||
if self.selected_item > 0 {
|
||||
self.selected_item -= 1;
|
||||
} else {
|
||||
self.selected_item = self.entries.len() - 1;
|
||||
}
|
||||
self.scroll_handle
|
||||
.scroll_to_item(self.selected_item, ScrollStrategy::Top);
|
||||
self.resolve_selected_completion(provider, cx);
|
||||
cx.notify();
|
||||
self.update_selection_index(self.prev_match_index(), provider, cx);
|
||||
}
|
||||
|
||||
fn select_next(
|
||||
@@ -285,15 +281,7 @@ impl CompletionsMenu {
|
||||
provider: Option<&dyn CompletionProvider>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) {
|
||||
if self.selected_item + 1 < self.entries.len() {
|
||||
self.selected_item += 1;
|
||||
} else {
|
||||
self.selected_item = 0;
|
||||
}
|
||||
self.scroll_handle
|
||||
.scroll_to_item(self.selected_item, ScrollStrategy::Top);
|
||||
self.resolve_selected_completion(provider, cx);
|
||||
cx.notify();
|
||||
self.update_selection_index(self.next_match_index(), provider, cx);
|
||||
}
|
||||
|
||||
fn select_last(
|
||||
@@ -301,11 +289,38 @@ impl CompletionsMenu {
|
||||
provider: Option<&dyn CompletionProvider>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) {
|
||||
self.selected_item = self.entries.len() - 1;
|
||||
self.scroll_handle
|
||||
.scroll_to_item(self.selected_item, ScrollStrategy::Top);
|
||||
self.resolve_selected_completion(provider, cx);
|
||||
cx.notify();
|
||||
self.update_selection_index(self.entries.len() - 1, provider, cx);
|
||||
}
|
||||
|
||||
fn update_selection_index(
|
||||
&mut self,
|
||||
match_index: usize,
|
||||
provider: Option<&dyn CompletionProvider>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) {
|
||||
if self.selected_item != match_index {
|
||||
self.selected_item = match_index;
|
||||
self.scroll_handle
|
||||
.scroll_to_item(self.selected_item, ScrollStrategy::Top);
|
||||
self.resolve_visible_completions(provider, cx);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
fn prev_match_index(&self) -> usize {
|
||||
if self.selected_item > 0 {
|
||||
self.selected_item - 1
|
||||
} else {
|
||||
self.entries.len() - 1
|
||||
}
|
||||
}
|
||||
|
||||
fn next_match_index(&self) -> usize {
|
||||
if self.selected_item + 1 < self.entries.len() {
|
||||
self.selected_item + 1
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
pub fn show_inline_completion_hint(&mut self, hint: InlineCompletionMenuHint) {
|
||||
@@ -330,7 +345,7 @@ impl CompletionsMenu {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resolve_selected_completion(
|
||||
pub fn resolve_visible_completions(
|
||||
&mut self,
|
||||
provider: Option<&dyn CompletionProvider>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
@@ -342,24 +357,76 @@ impl CompletionsMenu {
|
||||
return;
|
||||
};
|
||||
|
||||
match &self.entries[self.selected_item] {
|
||||
CompletionEntry::Match(entry) => {
|
||||
let completion_index = entry.candidate_id;
|
||||
let resolve_task = provider.resolve_completions(
|
||||
self.buffer.clone(),
|
||||
vec![completion_index],
|
||||
self.completions.clone(),
|
||||
cx,
|
||||
);
|
||||
// Attempt to resolve completions for every item that will be displayed. This matters
|
||||
// because single line documentation may be displayed inline with the completion.
|
||||
//
|
||||
// When navigating to the very beginning or end of completions, `last_rendered_range` may
|
||||
// have no overlap with the completions that will be displayed, so instead use a range based
|
||||
// on the last rendered count.
|
||||
const APPROXIMATE_VISIBLE_COUNT: usize = 12;
|
||||
let last_rendered_range = self.last_rendered_range.borrow().clone();
|
||||
let visible_count = last_rendered_range
|
||||
.clone()
|
||||
.map_or(APPROXIMATE_VISIBLE_COUNT, |range| range.count());
|
||||
let entry_range = if self.selected_item == 0 {
|
||||
0..min(visible_count, self.entries.len())
|
||||
} else if self.selected_item == self.entries.len() - 1 {
|
||||
self.entries.len().saturating_sub(visible_count)..self.entries.len()
|
||||
} else {
|
||||
last_rendered_range.map_or(0..0, |range| {
|
||||
min(range.start, self.entries.len())..min(range.end, self.entries.len())
|
||||
})
|
||||
};
|
||||
|
||||
cx.spawn(move |editor, mut cx| async move {
|
||||
if let Some(true) = resolve_task.await.log_err() {
|
||||
editor.update(&mut cx, |_, cx| cx.notify()).ok();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
// Expand the range to resolve more completions than are predicted to be visible, to reduce
|
||||
// jank on navigation.
|
||||
const EXTRA_TO_RESOLVE: usize = 4;
|
||||
let entry_indices = util::iterate_expanded_and_wrapped_usize_range(
|
||||
entry_range.clone(),
|
||||
EXTRA_TO_RESOLVE,
|
||||
EXTRA_TO_RESOLVE,
|
||||
self.entries.len(),
|
||||
);
|
||||
|
||||
// Avoid work by sometimes filtering out completions that already have documentation.
|
||||
// This filtering doesn't happen if the completions are currently being updated.
|
||||
let completions = self.completions.borrow();
|
||||
let candidate_ids = entry_indices
|
||||
.flat_map(|i| Self::entry_candidate_id(&self.entries[i]))
|
||||
.filter(|i| completions[*i].documentation.is_none());
|
||||
|
||||
// Current selection is always resolved even if it already has documentation, to handle
|
||||
// out-of-spec language servers that return more results later.
|
||||
let candidate_ids = match Self::entry_candidate_id(&self.entries[self.selected_item]) {
|
||||
None => candidate_ids.collect::<Vec<usize>>(),
|
||||
Some(selected_candidate_id) => iter::once(selected_candidate_id)
|
||||
.chain(candidate_ids.filter(|id| *id != selected_candidate_id))
|
||||
.collect::<Vec<usize>>(),
|
||||
};
|
||||
|
||||
if candidate_ids.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let resolve_task = provider.resolve_completions(
|
||||
self.buffer.clone(),
|
||||
candidate_ids,
|
||||
self.completions.clone(),
|
||||
cx,
|
||||
);
|
||||
|
||||
cx.spawn(move |editor, mut cx| async move {
|
||||
if let Some(true) = resolve_task.await.log_err() {
|
||||
editor.update(&mut cx, |_, cx| cx.notify()).ok();
|
||||
}
|
||||
CompletionEntry::InlineCompletionHint { .. } => {}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn entry_candidate_id(entry: &CompletionEntry) -> Option<usize> {
|
||||
match entry {
|
||||
CompletionEntry::Match(entry) => Some(entry.candidate_id),
|
||||
CompletionEntry::InlineCompletionHint { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -408,12 +475,14 @@ impl CompletionsMenu {
|
||||
let selected_item = self.selected_item;
|
||||
let completions = self.completions.clone();
|
||||
let matches = self.entries.clone();
|
||||
let last_rendered_range = self.last_rendered_range.clone();
|
||||
let style = style.clone();
|
||||
let list = uniform_list(
|
||||
cx.view().clone(),
|
||||
"completions",
|
||||
matches.len(),
|
||||
move |_editor, range, cx| {
|
||||
last_rendered_range.borrow_mut().replace(range.clone());
|
||||
let start_ix = range.start;
|
||||
let completions_guard = completions.borrow_mut();
|
||||
|
||||
@@ -545,7 +614,7 @@ impl CompletionsMenu {
|
||||
fn render_aside(
|
||||
&self,
|
||||
style: &EditorStyle,
|
||||
max_height: Pixels,
|
||||
max_size: Size<Pixels>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) -> Option<AnyElement> {
|
||||
@@ -595,10 +664,9 @@ impl CompletionsMenu {
|
||||
.child(
|
||||
multiline_docs
|
||||
.id("multiline_docs")
|
||||
.max_h(max_height)
|
||||
.px_2()
|
||||
.min_w(px(260.))
|
||||
.max_w(MAX_COMPLETIONS_ASIDE_WIDTH)
|
||||
.px(MENU_ASIDE_X_PADDING / 2.)
|
||||
.max_w(max_size.width)
|
||||
.max_h(max_size.height)
|
||||
.overflow_y_scroll()
|
||||
.occlude(),
|
||||
)
|
||||
|
||||
@@ -128,6 +128,7 @@ use multi_buffer::{
|
||||
ExpandExcerptDirection, MultiBufferDiffHunk, MultiBufferPoint, MultiBufferRow, ToOffsetUtf16,
|
||||
};
|
||||
use project::{
|
||||
buffer_store::BufferChangeSet,
|
||||
lsp_store::{FormatTarget, FormatTrigger, OpenLspBufferHandle},
|
||||
project_settings::{GitGutterSetting, ProjectSettings},
|
||||
CodeAction, Completion, CompletionIntent, DocumentHighlight, InlayHint, Location, LocationLink,
|
||||
@@ -605,6 +606,7 @@ pub struct Editor {
|
||||
mode: EditorMode,
|
||||
show_breadcrumbs: bool,
|
||||
show_gutter: bool,
|
||||
show_scrollbars: bool,
|
||||
show_line_numbers: Option<bool>,
|
||||
use_relative_line_numbers: Option<bool>,
|
||||
show_git_diff_gutter: Option<bool>,
|
||||
@@ -1234,6 +1236,7 @@ impl Editor {
|
||||
project,
|
||||
blink_manager: blink_manager.clone(),
|
||||
show_local_selections: true,
|
||||
show_scrollbars: true,
|
||||
mode,
|
||||
show_breadcrumbs: EditorSettings::get_global(cx).toolbar.breadcrumbs,
|
||||
show_gutter: mode == EditorMode::Full,
|
||||
@@ -3742,7 +3745,7 @@ impl Editor {
|
||||
|
||||
if editor.focus_handle.is_focused(cx) && menu.is_some() {
|
||||
let mut menu = menu.unwrap();
|
||||
menu.resolve_selected_completion(editor.completion_provider.as_deref(), cx);
|
||||
menu.resolve_visible_completions(editor.completion_provider.as_deref(), cx);
|
||||
|
||||
if editor.show_inline_completions_in_menu(cx) {
|
||||
if let Some(hint) = editor.inline_completion_menu_hint(cx) {
|
||||
@@ -5130,14 +5133,14 @@ impl Editor {
|
||||
fn render_context_menu_aside(
|
||||
&self,
|
||||
style: &EditorStyle,
|
||||
max_height: Pixels,
|
||||
max_size: Size<Pixels>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) -> Option<AnyElement> {
|
||||
self.context_menu.borrow().as_ref().and_then(|menu| {
|
||||
if menu.visible() {
|
||||
menu.render_aside(
|
||||
style,
|
||||
max_height,
|
||||
max_size,
|
||||
self.workspace.as_ref().map(|(w, _)| w.clone()),
|
||||
cx,
|
||||
)
|
||||
@@ -8774,9 +8777,10 @@ impl Editor {
|
||||
.map(|selection| {
|
||||
let old_range = selection.start..selection.end;
|
||||
let mut new_range = old_range.clone();
|
||||
while let Some(containing_range) =
|
||||
buffer.range_for_syntax_ancestor(new_range.clone())
|
||||
let mut new_node = None;
|
||||
while let Some((node, containing_range)) = buffer.syntax_ancestor(new_range.clone())
|
||||
{
|
||||
new_node = Some(node);
|
||||
new_range = containing_range;
|
||||
if !display_map.intersects_fold(new_range.start)
|
||||
&& !display_map.intersects_fold(new_range.end)
|
||||
@@ -8785,6 +8789,17 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(node) = new_node {
|
||||
// Log the ancestor, to support using this action as a way to explore TreeSitter
|
||||
// nodes. Parent and grandparent are also logged because this operation will not
|
||||
// visit nodes that have the same range as their parent.
|
||||
log::info!("Node: {node:?}");
|
||||
let parent = node.parent();
|
||||
log::info!("Parent: {parent:?}");
|
||||
let grandparent = parent.and_then(|x| x.parent());
|
||||
log::info!("Grandparent: {grandparent:?}");
|
||||
}
|
||||
|
||||
selected_larger_node |= new_range != old_range;
|
||||
Selection {
|
||||
id: selection.id,
|
||||
@@ -11247,6 +11262,11 @@ impl Editor {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn set_show_scrollbars(&mut self, show_scrollbars: bool, cx: &mut ViewContext<Self>) {
|
||||
self.show_scrollbars = show_scrollbars;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn set_show_line_numbers(&mut self, show_line_numbers: bool, cx: &mut ViewContext<Self>) {
|
||||
self.show_line_numbers = Some(show_line_numbers);
|
||||
cx.notify();
|
||||
@@ -12938,6 +12958,14 @@ impl Editor {
|
||||
.and_then(|item| item.to_any().downcast_ref::<T>())
|
||||
}
|
||||
|
||||
pub fn add_change_set(
|
||||
&mut self,
|
||||
change_set: Model<BufferChangeSet>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.diff_map.add_change_set(change_set, cx);
|
||||
}
|
||||
|
||||
fn character_size(&self, cx: &mut ViewContext<Self>) -> gpui::Point<Pixels> {
|
||||
let text_layout_details = self.text_layout_details(cx);
|
||||
let style = &text_layout_details.editor_style;
|
||||
|
||||
@@ -25,14 +25,18 @@ use language::{
|
||||
use language_settings::{Formatter, FormatterList, IndentGuideSettings};
|
||||
use multi_buffer::MultiBufferIndentGuide;
|
||||
use parking_lot::Mutex;
|
||||
use pretty_assertions::{assert_eq, assert_ne};
|
||||
use project::{buffer_store::BufferChangeSet, FakeFs};
|
||||
use project::{
|
||||
lsp_command::SIGNATURE_HELP_HIGHLIGHT_CURRENT,
|
||||
project_settings::{LspSettings, ProjectSettings},
|
||||
};
|
||||
use serde_json::{self, json};
|
||||
use std::sync::atomic::{self, AtomicBool, AtomicUsize};
|
||||
use std::{cell::RefCell, future::Future, rc::Rc, time::Instant};
|
||||
use std::{
|
||||
iter,
|
||||
sync::atomic::{self, AtomicUsize},
|
||||
};
|
||||
use test::{build_editor_with_project, editor_lsp_test_context::rust_lang};
|
||||
use unindent::Unindent;
|
||||
use util::{
|
||||
@@ -10787,6 +10791,62 @@ async fn test_completions_resolve_updates_labels_if_filter_text_matches(
|
||||
async fn test_completions_default_resolve_data_handling(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let item_0 = lsp::CompletionItem {
|
||||
label: "abs".into(),
|
||||
insert_text: Some("abs".into()),
|
||||
data: Some(json!({ "very": "special"})),
|
||||
insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
|
||||
text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
|
||||
lsp::InsertReplaceEdit {
|
||||
new_text: "abs".to_string(),
|
||||
insert: lsp::Range::default(),
|
||||
replace: lsp::Range::default(),
|
||||
},
|
||||
)),
|
||||
..lsp::CompletionItem::default()
|
||||
};
|
||||
let items = iter::once(item_0.clone())
|
||||
.chain((11..51).map(|i| lsp::CompletionItem {
|
||||
label: format!("item_{}", i),
|
||||
insert_text: Some(format!("item_{}", i)),
|
||||
insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
|
||||
..lsp::CompletionItem::default()
|
||||
}))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let default_commit_characters = vec!["?".to_string()];
|
||||
let default_data = json!({ "default": "data"});
|
||||
let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
|
||||
let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
|
||||
let default_edit_range = lsp::Range {
|
||||
start: lsp::Position {
|
||||
line: 0,
|
||||
character: 5,
|
||||
},
|
||||
end: lsp::Position {
|
||||
line: 0,
|
||||
character: 5,
|
||||
},
|
||||
};
|
||||
|
||||
let item_0_out = lsp::CompletionItem {
|
||||
commit_characters: Some(default_commit_characters.clone()),
|
||||
insert_text_format: Some(default_insert_text_format),
|
||||
..item_0
|
||||
};
|
||||
let items_out = iter::once(item_0_out)
|
||||
.chain(items[1..].iter().map(|item| lsp::CompletionItem {
|
||||
commit_characters: Some(default_commit_characters.clone()),
|
||||
data: Some(default_data.clone()),
|
||||
insert_text_mode: Some(default_insert_text_mode),
|
||||
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
|
||||
range: default_edit_range,
|
||||
new_text: item.label.clone(),
|
||||
})),
|
||||
..item.clone()
|
||||
}))
|
||||
.collect::<Vec<lsp::CompletionItem>>();
|
||||
|
||||
let mut cx = EditorLspTestContext::new_rust(
|
||||
lsp::ServerCapabilities {
|
||||
completion_provider: Some(lsp::CompletionOptions {
|
||||
@@ -10803,138 +10863,15 @@ async fn test_completions_default_resolve_data_handling(cx: &mut gpui::TestAppCo
|
||||
cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"});
|
||||
cx.simulate_keystroke(".");
|
||||
|
||||
let default_commit_characters = vec!["?".to_string()];
|
||||
let default_data = json!({ "very": "special"});
|
||||
let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
|
||||
let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
|
||||
let default_edit_range = lsp::Range {
|
||||
start: lsp::Position {
|
||||
line: 0,
|
||||
character: 5,
|
||||
},
|
||||
end: lsp::Position {
|
||||
line: 0,
|
||||
character: 5,
|
||||
},
|
||||
};
|
||||
|
||||
let resolve_requests_number = Arc::new(AtomicUsize::new(0));
|
||||
let expect_first_item = Arc::new(AtomicBool::new(true));
|
||||
cx.lsp
|
||||
.server
|
||||
.on_request::<lsp::request::ResolveCompletionItem, _, _>({
|
||||
let closure_default_data = default_data.clone();
|
||||
let closure_resolve_requests_number = resolve_requests_number.clone();
|
||||
let closure_expect_first_item = expect_first_item.clone();
|
||||
let closure_default_commit_characters = default_commit_characters.clone();
|
||||
move |item_to_resolve, _| {
|
||||
closure_resolve_requests_number.fetch_add(1, atomic::Ordering::Release);
|
||||
let default_data = closure_default_data.clone();
|
||||
let default_commit_characters = closure_default_commit_characters.clone();
|
||||
let expect_first_item = closure_expect_first_item.clone();
|
||||
async move {
|
||||
if expect_first_item.load(atomic::Ordering::Acquire) {
|
||||
assert_eq!(
|
||||
item_to_resolve.label, "Some(2)",
|
||||
"Should have selected the first item"
|
||||
);
|
||||
assert_eq!(
|
||||
item_to_resolve.data,
|
||||
Some(json!({ "very": "special"})),
|
||||
"First item should bring its own data for resolving"
|
||||
);
|
||||
assert_eq!(
|
||||
item_to_resolve.commit_characters,
|
||||
Some(default_commit_characters),
|
||||
"First item had no own commit characters and should inherit the default ones"
|
||||
);
|
||||
assert!(
|
||||
matches!(
|
||||
item_to_resolve.text_edit,
|
||||
Some(lsp::CompletionTextEdit::InsertAndReplace { .. })
|
||||
),
|
||||
"First item should bring its own edit range for resolving"
|
||||
);
|
||||
assert_eq!(
|
||||
item_to_resolve.insert_text_format,
|
||||
Some(default_insert_text_format),
|
||||
"First item had no own insert text format and should inherit the default one"
|
||||
);
|
||||
assert_eq!(
|
||||
item_to_resolve.insert_text_mode,
|
||||
Some(lsp::InsertTextMode::ADJUST_INDENTATION),
|
||||
"First item should bring its own insert text mode for resolving"
|
||||
);
|
||||
Ok(item_to_resolve)
|
||||
} else {
|
||||
assert_eq!(
|
||||
item_to_resolve.label, "vec![2]",
|
||||
"Should have selected the last item"
|
||||
);
|
||||
assert_eq!(
|
||||
item_to_resolve.data,
|
||||
Some(default_data),
|
||||
"Last item has no own resolve data and should inherit the default one"
|
||||
);
|
||||
assert_eq!(
|
||||
item_to_resolve.commit_characters,
|
||||
Some(default_commit_characters),
|
||||
"Last item had no own commit characters and should inherit the default ones"
|
||||
);
|
||||
assert_eq!(
|
||||
item_to_resolve.text_edit,
|
||||
Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
|
||||
range: default_edit_range,
|
||||
new_text: "vec![2]".to_string()
|
||||
})),
|
||||
"Last item had no own edit range and should inherit the default one"
|
||||
);
|
||||
assert_eq!(
|
||||
item_to_resolve.insert_text_format,
|
||||
Some(lsp::InsertTextFormat::PLAIN_TEXT),
|
||||
"Last item should bring its own insert text format for resolving"
|
||||
);
|
||||
assert_eq!(
|
||||
item_to_resolve.insert_text_mode,
|
||||
Some(default_insert_text_mode),
|
||||
"Last item had no own insert text mode and should inherit the default one"
|
||||
);
|
||||
|
||||
Ok(item_to_resolve)
|
||||
}
|
||||
}
|
||||
}
|
||||
}).detach();
|
||||
|
||||
let completion_data = default_data.clone();
|
||||
let completion_characters = default_commit_characters.clone();
|
||||
cx.handle_request::<lsp::request::Completion, _, _>(move |_, _, _| {
|
||||
let default_data = completion_data.clone();
|
||||
let default_commit_characters = completion_characters.clone();
|
||||
let items = items.clone();
|
||||
async move {
|
||||
Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
|
||||
items: vec![
|
||||
lsp::CompletionItem {
|
||||
label: "Some(2)".into(),
|
||||
insert_text: Some("Some(2)".into()),
|
||||
data: Some(json!({ "very": "special"})),
|
||||
insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
|
||||
text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
|
||||
lsp::InsertReplaceEdit {
|
||||
new_text: "Some(2)".to_string(),
|
||||
insert: lsp::Range::default(),
|
||||
replace: lsp::Range::default(),
|
||||
},
|
||||
)),
|
||||
..lsp::CompletionItem::default()
|
||||
},
|
||||
lsp::CompletionItem {
|
||||
label: "vec![2]".into(),
|
||||
insert_text: Some("vec![2]".into()),
|
||||
insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
|
||||
..lsp::CompletionItem::default()
|
||||
},
|
||||
],
|
||||
items,
|
||||
item_defaults: Some(lsp::CompletionListItemDefaults {
|
||||
data: Some(default_data.clone()),
|
||||
commit_characters: Some(default_commit_characters.clone()),
|
||||
@@ -10951,6 +10888,21 @@ async fn test_completions_default_resolve_data_handling(cx: &mut gpui::TestAppCo
|
||||
.next()
|
||||
.await;
|
||||
|
||||
let resolved_items = Arc::new(Mutex::new(Vec::new()));
|
||||
cx.lsp
|
||||
.server
|
||||
.on_request::<lsp::request::ResolveCompletionItem, _, _>({
|
||||
let closure_resolved_items = resolved_items.clone();
|
||||
move |item_to_resolve, _| {
|
||||
let closure_resolved_items = closure_resolved_items.clone();
|
||||
async move {
|
||||
closure_resolved_items.lock().push(item_to_resolve.clone());
|
||||
Ok(item_to_resolve)
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
cx.condition(|editor, _| editor.context_menu_visible())
|
||||
.await;
|
||||
cx.run_until_parked();
|
||||
@@ -10959,39 +10911,56 @@ async fn test_completions_default_resolve_data_handling(cx: &mut gpui::TestAppCo
|
||||
match menu.as_ref().expect("should have the completions menu") {
|
||||
CodeContextMenu::Completions(completions_menu) => {
|
||||
assert_eq!(
|
||||
completion_menu_entries(&completions_menu.entries),
|
||||
vec!["Some(2)", "vec![2]"]
|
||||
completions_menu
|
||||
.entries
|
||||
.iter()
|
||||
.flat_map(|c| match c {
|
||||
CompletionEntry::Match(mat) => Some(mat.string.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Vec<String>>(),
|
||||
items_out
|
||||
.iter()
|
||||
.map(|completion| completion.label.clone())
|
||||
.collect::<Vec<String>>()
|
||||
);
|
||||
}
|
||||
CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
|
||||
}
|
||||
});
|
||||
// Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
|
||||
// with 4 from the end.
|
||||
assert_eq!(
|
||||
resolve_requests_number.load(atomic::Ordering::Acquire),
|
||||
1,
|
||||
"While there are 2 items in the completion list, only 1 resolve request should have been sent, for the selected item"
|
||||
*resolved_items.lock(),
|
||||
[
|
||||
&items_out[0..16],
|
||||
&items_out[items_out.len() - 4..items_out.len()]
|
||||
]
|
||||
.concat()
|
||||
.iter()
|
||||
.cloned()
|
||||
.collect::<Vec<lsp::CompletionItem>>()
|
||||
);
|
||||
resolved_items.lock().clear();
|
||||
|
||||
cx.update_editor(|editor, cx| {
|
||||
editor.context_menu_first(&ContextMenuFirst, cx);
|
||||
editor.context_menu_prev(&ContextMenuPrev, cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
// Completions that have already been resolved are skipped.
|
||||
assert_eq!(
|
||||
resolve_requests_number.load(atomic::Ordering::Acquire),
|
||||
2,
|
||||
"After re-selecting the first item, another resolve request should have been sent"
|
||||
);
|
||||
|
||||
expect_first_item.store(false, atomic::Ordering::Release);
|
||||
cx.update_editor(|editor, cx| {
|
||||
editor.context_menu_last(&ContextMenuLast, cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
assert_eq!(
|
||||
resolve_requests_number.load(atomic::Ordering::Acquire),
|
||||
3,
|
||||
"After selecting the other item, another resolve request should have been sent"
|
||||
*resolved_items.lock(),
|
||||
[
|
||||
// Selected item is always resolved even if it was resolved before.
|
||||
&items_out[items_out.len() - 1..items_out.len()],
|
||||
&items_out[items_out.len() - 16..items_out.len() - 4]
|
||||
]
|
||||
.concat()
|
||||
.iter()
|
||||
.cloned()
|
||||
.collect::<Vec<lsp::CompletionItem>>()
|
||||
);
|
||||
resolved_items.lock().clear();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
blame_entry_tooltip::{blame_entry_relative_timestamp, BlameEntryTooltip},
|
||||
code_context_menus::{CodeActionsMenu, MAX_COMPLETIONS_ASIDE_WIDTH},
|
||||
code_context_menus::{CodeActionsMenu, MENU_ASIDE_MAX_WIDTH, MENU_ASIDE_MIN_WIDTH, MENU_GAP},
|
||||
display_map::{
|
||||
Block, BlockContext, BlockStyle, DisplaySnapshot, HighlightedChunk, ToDisplayPoint,
|
||||
},
|
||||
@@ -1032,6 +1032,7 @@ impl EditorElement {
|
||||
scroll_pixel_position: gpui::Point<Pixels>,
|
||||
line_height: Pixels,
|
||||
em_width: Pixels,
|
||||
em_advance: Pixels,
|
||||
autoscroll_containing_element: bool,
|
||||
cx: &mut WindowContext,
|
||||
) -> Vec<CursorLayout> {
|
||||
@@ -1058,7 +1059,7 @@ impl EditorElement {
|
||||
let mut block_width =
|
||||
cursor_row_layout.x_for_index(cursor_column + 1) - cursor_character_x;
|
||||
if block_width == Pixels::ZERO {
|
||||
block_width = em_width;
|
||||
block_width = em_advance;
|
||||
}
|
||||
let block_text = if let CursorShape::Block = selection.cursor_shape {
|
||||
snapshot
|
||||
@@ -1191,12 +1192,13 @@ impl EditorElement {
|
||||
);
|
||||
|
||||
let scrollbar_settings = EditorSettings::get_global(cx).scrollbar;
|
||||
let show_scrollbars = match scrollbar_settings.show {
|
||||
ShowScrollbar::Auto => {
|
||||
let editor = self.editor.read(cx);
|
||||
let is_singleton = editor.is_singleton(cx);
|
||||
// Git
|
||||
(is_singleton && scrollbar_settings.git_diff && !snapshot.diff_map.is_empty())
|
||||
let show_scrollbars = self.editor.read(cx).show_scrollbars
|
||||
&& match scrollbar_settings.show {
|
||||
ShowScrollbar::Auto => {
|
||||
let editor = self.editor.read(cx);
|
||||
let is_singleton = editor.is_singleton(cx);
|
||||
// Git
|
||||
(is_singleton && scrollbar_settings.git_diff && !snapshot.diff_map.is_empty())
|
||||
||
|
||||
// Buffer Search Results
|
||||
(is_singleton && scrollbar_settings.search_results && editor.has_background_highlights::<BufferSearchHighlights>())
|
||||
@@ -1212,11 +1214,11 @@ impl EditorElement {
|
||||
||
|
||||
// Scrollmanager
|
||||
editor.scroll_manager.scrollbars_visible()
|
||||
}
|
||||
ShowScrollbar::System => self.editor.read(cx).scroll_manager.scrollbars_visible(),
|
||||
ShowScrollbar::Always => true,
|
||||
ShowScrollbar::Never => false,
|
||||
};
|
||||
}
|
||||
ShowScrollbar::System => self.editor.read(cx).scroll_manager.scrollbars_visible(),
|
||||
ShowScrollbar::Always => true,
|
||||
ShowScrollbar::Never => false,
|
||||
};
|
||||
|
||||
let axes: AxisPair<bool> = scrollbar_settings.axes.into();
|
||||
|
||||
@@ -2929,6 +2931,11 @@ impl EditorElement {
|
||||
}
|
||||
};
|
||||
|
||||
let viewport_bounds = Bounds::new(Default::default(), cx.viewport_size()).extend(Edges {
|
||||
right: -Self::SCROLLBAR_WIDTH - MENU_GAP,
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
// If the context menu's max height won't fit below, then flip it above the line and display
|
||||
// it in reverse order. If the available space above is less than below.
|
||||
let unconstrained_max_height = line_height * 12. + POPOVER_Y_PADDING;
|
||||
@@ -2950,7 +2957,7 @@ impl EditorElement {
|
||||
// If less than 3 lines fit within the text bounds, instead fit within the window.
|
||||
if height < min_height {
|
||||
let available_above = bottom_y_when_flipped;
|
||||
let available_below = cx.viewport_size().height - target_position.y;
|
||||
let available_below = viewport_bounds.bottom() - target_position.y;
|
||||
if available_below > 3. * line_height {
|
||||
y_is_flipped = false;
|
||||
height = min_height;
|
||||
@@ -2968,6 +2975,7 @@ impl EditorElement {
|
||||
|
||||
let max_height_in_lines = ((height - POPOVER_Y_PADDING) / line_height).floor() as u32;
|
||||
|
||||
// TODO(mgsloan): use viewport_bounds.width as a max width when rendering menu.
|
||||
let Some(mut menu_element) = self.editor.update(cx, |editor, cx| {
|
||||
editor.render_context_menu(&self.style, max_height_in_lines, cx)
|
||||
}) else {
|
||||
@@ -2976,13 +2984,11 @@ impl EditorElement {
|
||||
|
||||
let menu_size = menu_element.layout_as_root(AvailableSpace::min_size(), cx);
|
||||
let menu_position = gpui::Point {
|
||||
x: if target_position.x + menu_size.width > cx.viewport_size().width {
|
||||
// Snap the right edge of the list to the right edge of the window if its horizontal bounds
|
||||
// overflow.
|
||||
(cx.viewport_size().width - menu_size.width).max(Pixels::ZERO)
|
||||
} else {
|
||||
target_position.x
|
||||
},
|
||||
// Snap the right edge of the list to the right edge of the window if its horizontal bounds
|
||||
// overflow. Include space for the scrollbar.
|
||||
x: target_position
|
||||
.x
|
||||
.min((viewport_bounds.right() - menu_size.width).max(Pixels::ZERO)),
|
||||
y: if y_is_flipped {
|
||||
bottom_y_when_flipped - menu_size.height
|
||||
} else {
|
||||
@@ -2991,43 +2997,31 @@ impl EditorElement {
|
||||
};
|
||||
cx.defer_draw(menu_element, menu_position, 1);
|
||||
|
||||
let aside_element = self.editor.update(cx, |editor, cx| {
|
||||
editor.render_context_menu_aside(&self.style, unconstrained_max_height, cx)
|
||||
});
|
||||
if let Some(aside_element) = aside_element {
|
||||
let menu_bounds = Bounds::new(menu_position, menu_size);
|
||||
let max_menu_size = size(menu_size.width, unconstrained_max_height);
|
||||
let max_menu_bounds = if y_is_flipped {
|
||||
Bounds::new(
|
||||
point(
|
||||
menu_position.x,
|
||||
bottom_y_when_flipped - max_menu_size.height,
|
||||
),
|
||||
max_menu_size,
|
||||
)
|
||||
} else {
|
||||
Bounds::new(target_position, max_menu_size)
|
||||
};
|
||||
|
||||
// Pad the target by 4 pixels to create a gap.
|
||||
let mut extend_amount = Edges::all(px(4.));
|
||||
// Extend to include the cursored line to avoid overlapping it.
|
||||
if y_is_flipped {
|
||||
extend_amount.bottom = line_height;
|
||||
} else {
|
||||
extend_amount.top = line_height;
|
||||
}
|
||||
self.layout_context_menu_aside(
|
||||
text_hitbox,
|
||||
y_is_flipped,
|
||||
menu_position,
|
||||
menu_bounds.extend(extend_amount),
|
||||
max_menu_bounds.extend(extend_amount),
|
||||
unconstrained_max_height,
|
||||
aside_element,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
// Layout documentation aside
|
||||
let menu_bounds = Bounds::new(menu_position, menu_size);
|
||||
let max_menu_size = size(menu_size.width, unconstrained_max_height);
|
||||
let max_menu_bounds = if y_is_flipped {
|
||||
Bounds::new(
|
||||
point(
|
||||
menu_position.x,
|
||||
bottom_y_when_flipped - max_menu_size.height,
|
||||
),
|
||||
max_menu_size,
|
||||
)
|
||||
} else {
|
||||
Bounds::new(target_position, max_menu_size)
|
||||
};
|
||||
self.layout_context_menu_aside(
|
||||
text_hitbox,
|
||||
y_is_flipped,
|
||||
menu_position,
|
||||
menu_bounds,
|
||||
max_menu_bounds,
|
||||
unconstrained_max_height,
|
||||
line_height,
|
||||
viewport_bounds,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
@@ -3036,66 +3030,106 @@ impl EditorElement {
|
||||
text_hitbox: &Hitbox,
|
||||
y_is_flipped: bool,
|
||||
menu_position: gpui::Point<Pixels>,
|
||||
target_bounds: Bounds<Pixels>,
|
||||
max_target_bounds: Bounds<Pixels>,
|
||||
menu_bounds: Bounds<Pixels>,
|
||||
max_menu_bounds: Bounds<Pixels>,
|
||||
max_height: Pixels,
|
||||
aside: AnyElement,
|
||||
line_height: Pixels,
|
||||
viewport_bounds: Bounds<Pixels>,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
let mut aside = aside;
|
||||
let actual_size = aside.layout_as_root(AvailableSpace::min_size(), cx);
|
||||
|
||||
// Snap to right side of window if it would overflow.
|
||||
let aside_x = cmp::min(
|
||||
menu_position.x,
|
||||
cx.viewport_size().width - actual_size.width,
|
||||
);
|
||||
if aside_x < px(0.) {
|
||||
// Not enough space, skip drawing.
|
||||
return;
|
||||
let mut extend_amount = Edges::all(MENU_GAP);
|
||||
// Extend to include the cursored line to avoid overlapping it.
|
||||
if y_is_flipped {
|
||||
extend_amount.bottom = line_height;
|
||||
} else {
|
||||
extend_amount.top = line_height;
|
||||
}
|
||||
let target_bounds = menu_bounds.extend(extend_amount);
|
||||
let max_target_bounds = max_menu_bounds.extend(extend_amount);
|
||||
|
||||
let top_position = point(aside_x, target_bounds.top() - actual_size.height);
|
||||
let bottom_position = point(aside_x, target_bounds.bottom());
|
||||
let right_position = point(target_bounds.right(), menu_position.y);
|
||||
let available_within_viewport = target_bounds.space_within(&viewport_bounds);
|
||||
let positioned_aside = if available_within_viewport.right >= MENU_ASIDE_MIN_WIDTH {
|
||||
let max_width = cmp::min(
|
||||
available_within_viewport.right - px(1.),
|
||||
MENU_ASIDE_MAX_WIDTH,
|
||||
);
|
||||
let Some(mut aside) =
|
||||
self.render_context_menu_aside(size(max_width, max_height - POPOVER_Y_PADDING), cx)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
aside.layout_as_root(AvailableSpace::min_size(), cx);
|
||||
let right_position = point(target_bounds.right(), menu_position.y);
|
||||
Some((aside, right_position))
|
||||
} else {
|
||||
let max_size = size(
|
||||
// TODO(mgsloan): Once the menu is bounded by viewport width the bound on viewport
|
||||
// won't be needed here.
|
||||
cmp::min(
|
||||
cmp::max(menu_bounds.size.width - px(2.), MENU_ASIDE_MIN_WIDTH),
|
||||
viewport_bounds.right(),
|
||||
),
|
||||
cmp::min(
|
||||
max_height,
|
||||
cmp::max(
|
||||
available_within_viewport.top,
|
||||
available_within_viewport.bottom,
|
||||
),
|
||||
) - POPOVER_Y_PADDING,
|
||||
);
|
||||
let Some(mut aside) = self.render_context_menu_aside(max_size, cx) else {
|
||||
return;
|
||||
};
|
||||
let actual_size = aside.layout_as_root(AvailableSpace::min_size(), cx);
|
||||
|
||||
let fit_horizontally_within = |available: Edges<Pixels>, wanted: Size<Pixels>| {
|
||||
// Prefer to fit to the right, then on the same side of the line as the menu, then on
|
||||
// the other side of the line.
|
||||
if wanted.width < available.right {
|
||||
Some(right_position)
|
||||
} else if !y_is_flipped && wanted.height < available.bottom {
|
||||
Some(bottom_position)
|
||||
} else if !y_is_flipped && wanted.height < available.top {
|
||||
Some(top_position)
|
||||
} else if y_is_flipped && wanted.height < available.top {
|
||||
Some(top_position)
|
||||
} else if y_is_flipped && wanted.height < available.bottom {
|
||||
Some(bottom_position)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
let top_position = point(menu_position.x, target_bounds.top() - actual_size.height);
|
||||
let bottom_position = point(menu_position.x, target_bounds.bottom());
|
||||
|
||||
let fit_within = |available: Edges<Pixels>, wanted: Size<Pixels>| {
|
||||
// Prefer to fit on the same side of the line as the menu, then on the other side of
|
||||
// the line.
|
||||
if !y_is_flipped && wanted.height < available.bottom {
|
||||
Some(bottom_position)
|
||||
} else if !y_is_flipped && wanted.height < available.top {
|
||||
Some(top_position)
|
||||
} else if y_is_flipped && wanted.height < available.top {
|
||||
Some(top_position)
|
||||
} else if y_is_flipped && wanted.height < available.bottom {
|
||||
Some(bottom_position)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
// Prefer choosing a direction using max sizes rather than actual size for stability.
|
||||
let available_within_text = max_target_bounds.space_within(&text_hitbox.bounds);
|
||||
let wanted = size(MENU_ASIDE_MAX_WIDTH, max_height);
|
||||
let aside_position = fit_within(available_within_text, wanted)
|
||||
// Fallback: fit max size in window.
|
||||
.or_else(|| fit_within(max_target_bounds.space_within(&viewport_bounds), wanted))
|
||||
// Fallback: fit actual size in window.
|
||||
.or_else(|| fit_within(available_within_viewport, actual_size));
|
||||
|
||||
aside_position.map(|position| (aside, position))
|
||||
};
|
||||
|
||||
// Prefer choosing a direction using max sizes rather than actual size for stability.
|
||||
let mut available = max_target_bounds.space_within(&text_hitbox.bounds);
|
||||
let mut wanted = size(MAX_COMPLETIONS_ASIDE_WIDTH, max_height);
|
||||
let aside_position = fit_horizontally_within(available, wanted)
|
||||
.or_else(|| {
|
||||
// Fallback: fit max size in window.
|
||||
available = max_target_bounds
|
||||
.space_within(&Bounds::new(Default::default(), cx.viewport_size()));
|
||||
fit_horizontally_within(available, wanted)
|
||||
})
|
||||
.or_else(|| {
|
||||
// Fallback: fit actual size in window.
|
||||
wanted = actual_size;
|
||||
fit_horizontally_within(available, wanted)
|
||||
});
|
||||
|
||||
// Skip drawing if it doesn't fit anywhere.
|
||||
if let Some(aside_position) = aside_position {
|
||||
cx.defer_draw(aside, aside_position, 1);
|
||||
if let Some((aside, position)) = positioned_aside {
|
||||
cx.defer_draw(aside, position, 1);
|
||||
}
|
||||
}
|
||||
|
||||
fn render_context_menu_aside(
|
||||
&self,
|
||||
max_size: Size<Pixels>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Option<AnyElement> {
|
||||
if max_size.width < px(100.) || max_size.height < px(12.) {
|
||||
None
|
||||
} else {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.render_context_menu_aside(&self.style, max_size, cx)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6246,6 +6280,7 @@ impl Element for EditorElement {
|
||||
scroll_pixel_position,
|
||||
line_height,
|
||||
em_width,
|
||||
em_advance,
|
||||
autoscroll_containing_element,
|
||||
cx,
|
||||
);
|
||||
|
||||
@@ -429,7 +429,7 @@ fn show_hover(
|
||||
})
|
||||
.or_else(|| {
|
||||
let snapshot = &snapshot.buffer_snapshot;
|
||||
let offset_range = snapshot.range_for_syntax_ancestor(anchor..anchor)?;
|
||||
let offset_range = snapshot.syntax_ancestor(anchor..anchor)?.1;
|
||||
Some(
|
||||
snapshot.anchor_before(offset_range.start)
|
||||
..snapshot.anchor_after(offset_range.end),
|
||||
|
||||
@@ -1155,6 +1155,11 @@ fn editor_with_deleted_text(
|
||||
editor.set_soft_wrap_mode(language::language_settings::SoftWrap::None, cx);
|
||||
editor.set_show_wrap_guides(false, cx);
|
||||
editor.set_show_gutter(false, cx);
|
||||
editor.set_show_line_numbers(false, cx);
|
||||
editor.set_show_scrollbars(false, cx);
|
||||
editor.set_show_runnables(false, cx);
|
||||
editor.set_show_git_diff_gutter(false, cx);
|
||||
editor.set_show_code_actions(false, cx);
|
||||
editor.scroll_manager.set_forbid_vertical_scroll(true);
|
||||
editor.set_read_only(true);
|
||||
editor.set_show_inline_completions(Some(false), cx);
|
||||
@@ -1166,7 +1171,7 @@ fn editor_with_deleted_text(
|
||||
false,
|
||||
cx,
|
||||
);
|
||||
editor.set_current_line_highlight(Some(CurrentLineHighlight::None)); //
|
||||
editor.set_current_line_highlight(Some(CurrentLineHighlight::None));
|
||||
editor
|
||||
._subscriptions
|
||||
.extend([cx.on_blur(&editor.focus_handle, |editor, cx| {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use std::collections::hash_map::Entry;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::Editor;
|
||||
use collections::HashMap;
|
||||
use gpui::{Model, WindowContext};
|
||||
use language::Buffer;
|
||||
use language::Language;
|
||||
@@ -20,6 +22,7 @@ where
|
||||
return None;
|
||||
};
|
||||
let multibuffer = editor.buffer().read(cx);
|
||||
let mut language_servers_for = HashMap::default();
|
||||
editor
|
||||
.selections
|
||||
.disjoint_anchors()
|
||||
@@ -28,27 +31,36 @@ where
|
||||
.filter_map(|selection| Some((selection.start.buffer_id?, selection.start)))
|
||||
.filter_map(|(buffer_id, trigger_anchor)| {
|
||||
let buffer = multibuffer.buffer(buffer_id)?;
|
||||
let server_id = *match language_servers_for.entry(buffer_id) {
|
||||
Entry::Occupied(occupied_entry) => occupied_entry.into_mut(),
|
||||
Entry::Vacant(vacant_entry) => {
|
||||
let language_server_id = project
|
||||
.read(cx)
|
||||
.language_servers_for_local_buffer(buffer.read(cx), cx)
|
||||
.find_map(|(adapter, server)| {
|
||||
if adapter.name.0.as_ref() == language_server_name {
|
||||
Some(server.server_id())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
vacant_entry.insert(language_server_id)
|
||||
}
|
||||
}
|
||||
.as_ref()?;
|
||||
|
||||
Some((buffer, trigger_anchor, server_id))
|
||||
})
|
||||
.find_map(|(buffer, trigger_anchor, server_id)| {
|
||||
let language = buffer.read(cx).language_at(trigger_anchor.text_anchor)?;
|
||||
if !filter_language(&language) {
|
||||
return None;
|
||||
}
|
||||
Some((trigger_anchor, language, buffer))
|
||||
})
|
||||
.find_map(|(trigger_anchor, language, buffer)| {
|
||||
project
|
||||
.read(cx)
|
||||
.language_servers_for_local_buffer(buffer.read(cx), cx)
|
||||
.find_map(|(adapter, server)| {
|
||||
if adapter.name.0.as_ref() == language_server_name {
|
||||
Some((
|
||||
trigger_anchor,
|
||||
Arc::clone(&language),
|
||||
server.server_id(),
|
||||
buffer.clone(),
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
Some((
|
||||
trigger_anchor,
|
||||
Arc::clone(&language),
|
||||
server_id,
|
||||
buffer.clone(),
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -51,8 +51,8 @@ impl FileIcons {
|
||||
pub fn get_icon(path: &Path, cx: &AppContext) -> Option<SharedString> {
|
||||
let this = cx.try_global::<Self>()?;
|
||||
|
||||
// FIXME: Associate a type with the languages and have the file's language
|
||||
// override these associations
|
||||
// TODO: Associate a type with the languages and have the file's language
|
||||
// override these associations
|
||||
maybe!({
|
||||
let suffix = path.icon_stem_or_suffix()?;
|
||||
|
||||
|
||||
@@ -15,7 +15,11 @@ path = "src/git_ui.rs"
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
db.workspace = true
|
||||
editor.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
language.workspace = true
|
||||
menu.workspace = true
|
||||
project.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
|
||||
@@ -1,28 +1,42 @@
|
||||
use anyhow::{Context as _, Result};
|
||||
use collections::HashMap;
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
use editor::{
|
||||
scroll::{Autoscroll, AutoscrollStrategy},
|
||||
Editor, MultiBuffer, DEFAULT_MULTIBUFFER_CONTEXT,
|
||||
};
|
||||
use git::{diff::DiffHunk, repository::GitFileStatus};
|
||||
use gpui::{
|
||||
actions, prelude::*, uniform_list, Action, AppContext, AsyncWindowContext, ClickEvent,
|
||||
CursorStyle, EventEmitter, FocusHandle, FocusableView, KeyContext,
|
||||
ListHorizontalSizingBehavior, ListSizingBehavior, Model, Modifiers, ModifiersChangedEvent,
|
||||
MouseButton, ScrollStrategy, Stateful, Task, UniformListScrollHandle, View, WeakView,
|
||||
};
|
||||
use language::{Buffer, BufferRow, OffsetRangeExt};
|
||||
use menu::{SelectNext, SelectPrev};
|
||||
use project::{Entry, EntryKind, Fs, Project, ProjectEntryId, WorktreeId};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::Settings as _;
|
||||
use std::{
|
||||
cell::OnceCell,
|
||||
collections::HashSet,
|
||||
ffi::OsStr,
|
||||
ops::Range,
|
||||
ops::{Deref, Range},
|
||||
path::{Path, PathBuf},
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
usize,
|
||||
};
|
||||
|
||||
use git::repository::GitFileStatus;
|
||||
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
use gpui::*;
|
||||
use project::{Entry, EntryKind, Fs, Project, ProjectEntryId, WorktreeId};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::Settings as _;
|
||||
use ui::{
|
||||
prelude::*, Checkbox, Divider, DividerColor, ElevationIndex, Scrollbar, ScrollbarState, Tooltip,
|
||||
prelude::*, Checkbox, Divider, DividerColor, ElevationIndex, ListItem, Scrollbar,
|
||||
ScrollbarState, Tooltip,
|
||||
};
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
use workspace::{
|
||||
dock::{DockPosition, Panel, PanelEvent},
|
||||
ItemHandle, Workspace,
|
||||
};
|
||||
use workspace::dock::{DockPosition, Panel, PanelEvent};
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::{git_status_icon, settings::GitPanelSettings};
|
||||
use crate::{CommitAllChanges, CommitStagedChanges, DiscardAll, StageAll, UnstageAll};
|
||||
@@ -31,6 +45,8 @@ actions!(git_panel, [ToggleFocus]);
|
||||
|
||||
const GIT_PANEL_KEY: &str = "GitPanel";
|
||||
|
||||
const UPDATE_DEBOUNCE: Duration = Duration::from_millis(50);
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.observe_new_views(
|
||||
|workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>| {
|
||||
@@ -58,6 +74,8 @@ struct EntryDetails {
|
||||
depth: usize,
|
||||
is_expanded: bool,
|
||||
status: Option<GitFileStatus>,
|
||||
hunks: Rc<OnceCell<Vec<DiffHunk>>>,
|
||||
index: usize,
|
||||
}
|
||||
|
||||
impl EntryDetails {
|
||||
@@ -72,7 +90,7 @@ struct SerializedGitPanel {
|
||||
}
|
||||
|
||||
pub struct GitPanel {
|
||||
_workspace: WeakView<Workspace>,
|
||||
workspace: WeakView<Workspace>,
|
||||
current_modifiers: Modifiers,
|
||||
focus_handle: FocusHandle,
|
||||
fs: Arc<dyn Fs>,
|
||||
@@ -87,8 +105,43 @@ pub struct GitPanel {
|
||||
|
||||
// The entries that are currently shown in the panel, aka
|
||||
// not hidden by folding or such
|
||||
visible_entries: Vec<(WorktreeId, Vec<Entry>, OnceCell<HashSet<Arc<Path>>>)>,
|
||||
visible_entries: Vec<WorktreeEntries>,
|
||||
width: Option<Pixels>,
|
||||
git_diff_editor: View<Editor>,
|
||||
git_diff_editor_updates: Task<()>,
|
||||
reveal_in_editor: Task<()>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct WorktreeEntries {
|
||||
worktree_id: WorktreeId,
|
||||
visible_entries: Vec<GitPanelEntry>,
|
||||
paths: Rc<OnceCell<HashSet<Arc<Path>>>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct GitPanelEntry {
|
||||
entry: Entry,
|
||||
hunks: Rc<OnceCell<Vec<DiffHunk>>>,
|
||||
}
|
||||
|
||||
impl Deref for GitPanelEntry {
|
||||
type Target = Entry;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.entry
|
||||
}
|
||||
}
|
||||
|
||||
impl WorktreeEntries {
|
||||
fn paths(&self) -> &HashSet<Arc<Path>> {
|
||||
self.paths.get_or_init(|| {
|
||||
self.visible_entries
|
||||
.iter()
|
||||
.map(|e| (e.entry.path.clone()))
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl GitPanel {
|
||||
@@ -115,18 +168,28 @@ impl GitPanel {
|
||||
this.hide_scrollbar(cx);
|
||||
})
|
||||
.detach();
|
||||
cx.subscribe(&project, |this, _project, event, cx| match event {
|
||||
cx.subscribe(&project, |this, project, event, cx| match event {
|
||||
project::Event::WorktreeRemoved(id) => {
|
||||
this.expanded_dir_ids.remove(id);
|
||||
this.update_visible_entries(None, cx);
|
||||
this.update_visible_entries(None, None, cx);
|
||||
cx.notify();
|
||||
}
|
||||
project::Event::WorktreeUpdatedEntries(_, _)
|
||||
| project::Event::WorktreeAdded(_)
|
||||
| project::Event::WorktreeOrderChanged => {
|
||||
this.update_visible_entries(None, cx);
|
||||
project::Event::WorktreeOrderChanged => {
|
||||
this.update_visible_entries(None, None, cx);
|
||||
cx.notify();
|
||||
}
|
||||
project::Event::WorktreeUpdatedEntries(id, _)
|
||||
| project::Event::WorktreeAdded(id)
|
||||
| project::Event::WorktreeUpdatedGitRepositories(id) => {
|
||||
this.update_visible_entries(Some(*id), None, cx);
|
||||
cx.notify();
|
||||
}
|
||||
project::Event::Closed => {
|
||||
this.git_diff_editor_updates = Task::ready(());
|
||||
this.expanded_dir_ids.clear();
|
||||
this.visible_entries.clear();
|
||||
this.git_diff_editor = diff_display_editor(project.clone(), cx);
|
||||
}
|
||||
_ => {}
|
||||
})
|
||||
.detach();
|
||||
@@ -134,11 +197,10 @@ impl GitPanel {
|
||||
let scroll_handle = UniformListScrollHandle::new();
|
||||
|
||||
let mut this = Self {
|
||||
_workspace: weak_workspace,
|
||||
workspace: weak_workspace,
|
||||
focus_handle: cx.focus_handle(),
|
||||
fs,
|
||||
pending_serialization: Task::ready(None),
|
||||
project,
|
||||
visible_entries: Vec::new(),
|
||||
current_modifiers: cx.modifiers(),
|
||||
expanded_dir_ids: Default::default(),
|
||||
@@ -149,8 +211,12 @@ impl GitPanel {
|
||||
selected_item: None,
|
||||
show_scrollbar: !Self::should_autohide_scrollbar(cx),
|
||||
hide_scrollbar_task: None,
|
||||
git_diff_editor: diff_display_editor(project.clone(), cx),
|
||||
git_diff_editor_updates: Task::ready(()),
|
||||
reveal_in_editor: Task::ready(()),
|
||||
project,
|
||||
};
|
||||
this.update_visible_entries(None, cx);
|
||||
this.update_visible_entries(None, None, cx);
|
||||
this
|
||||
});
|
||||
|
||||
@@ -251,6 +317,82 @@ impl GitPanel {
|
||||
|
||||
(depth, difference)
|
||||
}
|
||||
|
||||
fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
|
||||
let item_count = self
|
||||
.visible_entries
|
||||
.iter()
|
||||
.map(|worktree_entries| worktree_entries.visible_entries.len())
|
||||
.sum::<usize>();
|
||||
if item_count == 0 {
|
||||
return;
|
||||
}
|
||||
let selection = match self.selected_item {
|
||||
Some(i) => {
|
||||
if i < item_count - 1 {
|
||||
self.selected_item = Some(i + 1);
|
||||
i + 1
|
||||
} else {
|
||||
self.selected_item = Some(0);
|
||||
0
|
||||
}
|
||||
}
|
||||
None => {
|
||||
self.selected_item = Some(0);
|
||||
0
|
||||
}
|
||||
};
|
||||
self.scroll_handle
|
||||
.scroll_to_item(selection, ScrollStrategy::Center);
|
||||
|
||||
let mut hunks = None;
|
||||
self.for_each_visible_entry(selection..selection + 1, cx, |_, entry, _| {
|
||||
hunks = Some(entry.hunks.clone());
|
||||
});
|
||||
if let Some(hunks) = hunks {
|
||||
self.reveal_entry_in_git_editor(hunks, false, Some(UPDATE_DEBOUNCE), cx);
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
|
||||
let item_count = self
|
||||
.visible_entries
|
||||
.iter()
|
||||
.map(|worktree_entries| worktree_entries.visible_entries.len())
|
||||
.sum::<usize>();
|
||||
if item_count == 0 {
|
||||
return;
|
||||
}
|
||||
let selection = match self.selected_item {
|
||||
Some(i) => {
|
||||
if i > 0 {
|
||||
self.selected_item = Some(i - 1);
|
||||
i - 1
|
||||
} else {
|
||||
self.selected_item = Some(item_count - 1);
|
||||
item_count - 1
|
||||
}
|
||||
}
|
||||
None => {
|
||||
self.selected_item = Some(0);
|
||||
0
|
||||
}
|
||||
};
|
||||
self.scroll_handle
|
||||
.scroll_to_item(selection, ScrollStrategy::Center);
|
||||
|
||||
let mut hunks = None;
|
||||
self.for_each_visible_entry(selection..selection + 1, cx, |_, entry, _| {
|
||||
hunks = Some(entry.hunks.clone());
|
||||
});
|
||||
if let Some(hunks) = hunks {
|
||||
self.reveal_entry_in_git_editor(hunks, false, Some(UPDATE_DEBOUNCE), cx);
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
impl GitPanel {
|
||||
@@ -293,8 +435,9 @@ impl GitPanel {
|
||||
fn entry_count(&self) -> usize {
|
||||
self.visible_entries
|
||||
.iter()
|
||||
.map(|(_, entries, _)| {
|
||||
entries
|
||||
.map(|worktree_entries| {
|
||||
worktree_entries
|
||||
.visible_entries
|
||||
.iter()
|
||||
.filter(|entry| entry.git_status.is_some())
|
||||
.count()
|
||||
@@ -309,19 +452,23 @@ impl GitPanel {
|
||||
mut callback: impl FnMut(ProjectEntryId, EntryDetails, &mut ViewContext<Self>),
|
||||
) {
|
||||
let mut ix = 0;
|
||||
for (worktree_id, visible_worktree_entries, entries_paths) in &self.visible_entries {
|
||||
for worktree_entries in &self.visible_entries {
|
||||
if ix >= range.end {
|
||||
return;
|
||||
}
|
||||
|
||||
if ix + visible_worktree_entries.len() <= range.start {
|
||||
ix += visible_worktree_entries.len();
|
||||
if ix + worktree_entries.visible_entries.len() <= range.start {
|
||||
ix += worktree_entries.visible_entries.len();
|
||||
continue;
|
||||
}
|
||||
|
||||
let end_ix = range.end.min(ix + visible_worktree_entries.len());
|
||||
let end_ix = range.end.min(ix + worktree_entries.visible_entries.len());
|
||||
// let entry_range = range.start.saturating_sub(ix)..end_ix - ix;
|
||||
if let Some(worktree) = self.project.read(cx).worktree_for_id(*worktree_id, cx) {
|
||||
if let Some(worktree) = self
|
||||
.project
|
||||
.read(cx)
|
||||
.worktree_for_id(worktree_entries.worktree_id, cx)
|
||||
{
|
||||
let snapshot = worktree.read(cx).snapshot();
|
||||
let root_name = OsStr::new(snapshot.root_name());
|
||||
let expanded_entry_ids = self
|
||||
@@ -331,14 +478,14 @@ impl GitPanel {
|
||||
.unwrap_or(&[]);
|
||||
|
||||
let entry_range = range.start.saturating_sub(ix)..end_ix - ix;
|
||||
let entries = entries_paths.get_or_init(|| {
|
||||
visible_worktree_entries
|
||||
.iter()
|
||||
.map(|e| (e.path.clone()))
|
||||
.collect()
|
||||
});
|
||||
let entries = worktree_entries.paths();
|
||||
|
||||
for entry in visible_worktree_entries[entry_range].iter() {
|
||||
let index_start = entry_range.start;
|
||||
for (i, entry) in worktree_entries.visible_entries[entry_range]
|
||||
.iter()
|
||||
.enumerate()
|
||||
{
|
||||
let index = index_start + i;
|
||||
let status = entry.git_status;
|
||||
let is_expanded = expanded_entry_ids.binary_search(&entry.id).is_ok();
|
||||
|
||||
@@ -360,16 +507,16 @@ impl GitPanel {
|
||||
.unwrap_or_else(|| root_name.to_string_lossy().to_string()),
|
||||
};
|
||||
|
||||
let display_name = entry.path.to_string_lossy().into_owned();
|
||||
|
||||
let details = EntryDetails {
|
||||
filename,
|
||||
display_name,
|
||||
display_name: entry.path.to_string_lossy().into_owned(),
|
||||
kind: entry.kind,
|
||||
is_expanded,
|
||||
path: entry.path.clone(),
|
||||
status,
|
||||
hunks: entry.hunks.clone(),
|
||||
depth,
|
||||
index,
|
||||
};
|
||||
callback(entry.id, details, cx);
|
||||
}
|
||||
@@ -379,44 +526,75 @@ impl GitPanel {
|
||||
}
|
||||
|
||||
// TODO: Update expanded directory state
|
||||
// TODO: Updates happen in the main loop, could be long for large workspaces
|
||||
fn update_visible_entries(
|
||||
&mut self,
|
||||
for_worktree: Option<WorktreeId>,
|
||||
new_selected_entry: Option<(WorktreeId, ProjectEntryId)>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let project = self.project.read(cx);
|
||||
self.visible_entries.clear();
|
||||
for worktree in project.visible_worktrees(cx) {
|
||||
let snapshot = worktree.read(cx).snapshot();
|
||||
let worktree_id = snapshot.id();
|
||||
|
||||
let mut visible_worktree_entries = Vec::new();
|
||||
let mut entry_iter = snapshot.entries(true, 0);
|
||||
while let Some(entry) = entry_iter.entry() {
|
||||
// Only include entries with a git status
|
||||
if entry.git_status.is_some() {
|
||||
visible_worktree_entries.push(entry.clone());
|
||||
let mut old_entries_removed = false;
|
||||
let mut after_update = Vec::new();
|
||||
self.visible_entries
|
||||
.retain(|worktree_entries| match for_worktree {
|
||||
Some(for_worktree) => {
|
||||
if worktree_entries.worktree_id == for_worktree {
|
||||
old_entries_removed = true;
|
||||
false
|
||||
} else if old_entries_removed {
|
||||
after_update.push(worktree_entries.clone());
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
entry_iter.advance();
|
||||
None => false,
|
||||
});
|
||||
for worktree in project.visible_worktrees(cx) {
|
||||
let worktree_id = worktree.read(cx).id();
|
||||
if for_worktree.is_some() && for_worktree != Some(worktree_id) {
|
||||
continue;
|
||||
}
|
||||
let snapshot = worktree.read(cx).snapshot();
|
||||
|
||||
let mut visible_worktree_entries = snapshot
|
||||
.entries(false, 0)
|
||||
.filter(|entry| !entry.is_external)
|
||||
.filter(|entry| entry.git_status.is_some())
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
snapshot.propagate_git_statuses(&mut visible_worktree_entries);
|
||||
project::sort_worktree_entries(&mut visible_worktree_entries);
|
||||
|
||||
if !visible_worktree_entries.is_empty() {
|
||||
self.visible_entries
|
||||
.push((worktree_id, visible_worktree_entries, OnceCell::new()));
|
||||
self.visible_entries.push(WorktreeEntries {
|
||||
worktree_id,
|
||||
visible_entries: visible_worktree_entries
|
||||
.into_iter()
|
||||
.map(|entry| GitPanelEntry {
|
||||
entry,
|
||||
hunks: Rc::default(),
|
||||
})
|
||||
.collect(),
|
||||
paths: Rc::default(),
|
||||
});
|
||||
}
|
||||
}
|
||||
self.visible_entries.extend(after_update);
|
||||
|
||||
if let Some((worktree_id, entry_id)) = new_selected_entry {
|
||||
self.selected_item = self.visible_entries.iter().enumerate().find_map(
|
||||
|(worktree_index, (id, entries, _))| {
|
||||
if *id == worktree_id {
|
||||
entries
|
||||
|(worktree_index, worktree_entries)| {
|
||||
if worktree_entries.worktree_id == worktree_id {
|
||||
worktree_entries
|
||||
.visible_entries
|
||||
.iter()
|
||||
.position(|entry| entry.id == entry_id)
|
||||
.map(|entry_index| worktree_index * entries.len() + entry_index)
|
||||
.map(|entry_index| {
|
||||
worktree_index * worktree_entries.visible_entries.len()
|
||||
+ entry_index
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -424,6 +602,163 @@ impl GitPanel {
|
||||
);
|
||||
}
|
||||
|
||||
let project = self.project.clone();
|
||||
self.git_diff_editor_updates = cx.spawn(|git_panel, mut cx| async move {
|
||||
cx.background_executor()
|
||||
.timer(UPDATE_DEBOUNCE)
|
||||
.await;
|
||||
let Some(project_buffers) = git_panel
|
||||
.update(&mut cx, |git_panel, cx| {
|
||||
futures::future::join_all(git_panel.visible_entries.iter_mut().flat_map(
|
||||
move |worktree_entries| {
|
||||
worktree_entries
|
||||
.visible_entries
|
||||
.iter()
|
||||
.filter_map(|entry| {
|
||||
let git_status = entry.git_status()?;
|
||||
let entry_hunks = entry.hunks.clone();
|
||||
let (entry_path, unstaged_changes_task) =
|
||||
project.update(cx, |project, cx| {
|
||||
let entry_path =
|
||||
project.path_for_entry(entry.id, cx)?;
|
||||
let open_task =
|
||||
project.open_path(entry_path.clone(), cx);
|
||||
let unstaged_changes_task =
|
||||
cx.spawn(|project, mut cx| async move {
|
||||
let (_, opened_model) = open_task
|
||||
.await
|
||||
.context("opening buffer")?;
|
||||
let buffer = opened_model
|
||||
.downcast::<Buffer>()
|
||||
.map_err(|_| {
|
||||
anyhow::anyhow!(
|
||||
"accessing buffer for entry"
|
||||
)
|
||||
})?;
|
||||
// TODO added files have noop changes and those are not expanded properly in the multi buffer
|
||||
let unstaged_changes = project
|
||||
.update(&mut cx, |project, cx| {
|
||||
project.open_unstaged_changes(
|
||||
buffer.clone(),
|
||||
cx,
|
||||
)
|
||||
})?
|
||||
.await
|
||||
.context("opening unstaged changes")?;
|
||||
|
||||
let hunks = cx.update(|cx| {
|
||||
entry_hunks
|
||||
.get_or_init(|| {
|
||||
match git_status {
|
||||
GitFileStatus::Added => {
|
||||
let buffer_snapshot = buffer.read(cx).snapshot();
|
||||
let entire_buffer_range =
|
||||
buffer_snapshot.anchor_after(0)
|
||||
..buffer_snapshot
|
||||
.anchor_before(
|
||||
buffer_snapshot.len(),
|
||||
);
|
||||
let entire_buffer_point_range =
|
||||
entire_buffer_range
|
||||
.clone()
|
||||
.to_point(&buffer_snapshot);
|
||||
|
||||
vec![DiffHunk {
|
||||
row_range: entire_buffer_point_range
|
||||
.start
|
||||
.row
|
||||
..entire_buffer_point_range
|
||||
.end
|
||||
.row,
|
||||
buffer_range: entire_buffer_range,
|
||||
diff_base_byte_range: 0..0,
|
||||
}]
|
||||
}
|
||||
GitFileStatus::Modified => {
|
||||
let buffer_snapshot =
|
||||
buffer.read(cx).snapshot();
|
||||
unstaged_changes.read(cx)
|
||||
.diff_to_buffer
|
||||
.hunks_in_row_range(
|
||||
0..BufferRow::MAX,
|
||||
&buffer_snapshot,
|
||||
)
|
||||
.collect()
|
||||
}
|
||||
// TODO support conflicts display
|
||||
GitFileStatus::Conflict => Vec::new(),
|
||||
}
|
||||
}).clone()
|
||||
})?;
|
||||
|
||||
anyhow::Ok((buffer, unstaged_changes, hunks))
|
||||
});
|
||||
Some((entry_path, unstaged_changes_task))
|
||||
})?;
|
||||
Some((entry_path, unstaged_changes_task))
|
||||
})
|
||||
.map(|(entry_path, open_task)| async move {
|
||||
(entry_path, open_task.await)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
},
|
||||
))
|
||||
})
|
||||
.ok()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let project_buffers = project_buffers.await;
|
||||
if project_buffers.is_empty() {
|
||||
return;
|
||||
}
|
||||
let mut change_sets = Vec::with_capacity(project_buffers.len());
|
||||
if let Some(buffer_update_task) = git_panel
|
||||
.update(&mut cx, |git_panel, cx| {
|
||||
let editor = git_panel.git_diff_editor.clone();
|
||||
let multi_buffer = editor.read(cx).buffer().clone();
|
||||
let mut buffers_with_ranges = Vec::with_capacity(project_buffers.len());
|
||||
for (buffer_path, open_result) in project_buffers {
|
||||
if let Some((buffer, unstaged_changes, diff_hunks)) = open_result
|
||||
.with_context(|| format!("opening buffer {buffer_path:?}"))
|
||||
.log_err()
|
||||
{
|
||||
change_sets.push(unstaged_changes);
|
||||
buffers_with_ranges.push((
|
||||
buffer,
|
||||
diff_hunks
|
||||
.into_iter()
|
||||
.map(|hunk| hunk.buffer_range)
|
||||
.collect(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
multi_buffer.update(cx, |multi_buffer, cx| {
|
||||
multi_buffer.clear(cx);
|
||||
multi_buffer.push_multiple_excerpts_with_context_lines(
|
||||
buffers_with_ranges,
|
||||
DEFAULT_MULTIBUFFER_CONTEXT,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})
|
||||
.ok()
|
||||
{
|
||||
buffer_update_task.await;
|
||||
git_panel
|
||||
.update(&mut cx, |git_panel, cx| {
|
||||
git_panel.git_diff_editor.update(cx, |editor, cx| {
|
||||
for change_set in change_sets {
|
||||
editor.add_change_set(change_set, cx);
|
||||
}
|
||||
})
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
});
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
@@ -626,17 +961,23 @@ impl GitPanel {
|
||||
let item_count = self
|
||||
.visible_entries
|
||||
.iter()
|
||||
.map(|(_, worktree_entries, _)| worktree_entries.len())
|
||||
.map(|worktree_entries| worktree_entries.visible_entries.len())
|
||||
.sum();
|
||||
let selected_entry = self.selected_item;
|
||||
h_flex()
|
||||
.size_full()
|
||||
.overflow_hidden()
|
||||
.child(
|
||||
uniform_list(cx.view().clone(), "entries", item_count, {
|
||||
|this, range, cx| {
|
||||
move |git_panel, range, cx| {
|
||||
let mut items = Vec::with_capacity(range.end - range.start);
|
||||
this.for_each_visible_entry(range, cx, |id, details, cx| {
|
||||
items.push(this.render_entry(id, details, cx));
|
||||
git_panel.for_each_visible_entry(range, cx, |id, details, cx| {
|
||||
items.push(git_panel.render_entry(
|
||||
id,
|
||||
Some(details.index) == selected_entry,
|
||||
details,
|
||||
cx,
|
||||
));
|
||||
});
|
||||
items
|
||||
}
|
||||
@@ -653,12 +994,14 @@ impl GitPanel {
|
||||
fn render_entry(
|
||||
&self,
|
||||
id: ProjectEntryId,
|
||||
selected: bool,
|
||||
details: EntryDetails,
|
||||
cx: &ViewContext<Self>,
|
||||
) -> impl IntoElement {
|
||||
let id = id.to_proto() as usize;
|
||||
let checkbox_id = ElementId::Name(format!("checkbox_{}", id).into());
|
||||
let is_staged = ToggleState::Selected;
|
||||
let handle = cx.view().clone();
|
||||
|
||||
h_flex()
|
||||
.id(id)
|
||||
@@ -676,7 +1019,113 @@ impl GitPanel {
|
||||
.when_some(details.status, |this, status| {
|
||||
this.child(git_status_icon(status))
|
||||
})
|
||||
.child(h_flex().gap_1p5().child(details.display_name.clone()))
|
||||
.child(
|
||||
ListItem::new(("label", id))
|
||||
.toggle_state(selected)
|
||||
.child(h_flex().gap_1p5().child(details.display_name.clone()))
|
||||
.on_click(move |e, cx| {
|
||||
handle.update(cx, |git_panel, cx| {
|
||||
git_panel.selected_item = Some(details.index);
|
||||
let change_focus = e.down.click_count > 1;
|
||||
git_panel.reveal_entry_in_git_editor(
|
||||
details.hunks.clone(),
|
||||
change_focus,
|
||||
None,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
fn reveal_entry_in_git_editor(
|
||||
&mut self,
|
||||
hunks: Rc<OnceCell<Vec<DiffHunk>>>,
|
||||
change_focus: bool,
|
||||
debounce: Option<Duration>,
|
||||
cx: &mut ViewContext<'_, Self>,
|
||||
) {
|
||||
let workspace = self.workspace.clone();
|
||||
let diff_editor = self.git_diff_editor.clone();
|
||||
self.reveal_in_editor = cx.spawn(|_, mut cx| async move {
|
||||
if let Some(debounce) = debounce {
|
||||
cx.background_executor().timer(debounce).await;
|
||||
}
|
||||
|
||||
let Some(editor) = workspace
|
||||
.update(&mut cx, |workspace, cx| {
|
||||
let git_diff_editor = workspace
|
||||
.items_of_type::<Editor>(cx)
|
||||
.find(|editor| &diff_editor == editor);
|
||||
match git_diff_editor {
|
||||
Some(existing_editor) => {
|
||||
workspace.activate_item(&existing_editor, true, change_focus, cx);
|
||||
existing_editor
|
||||
}
|
||||
None => {
|
||||
workspace.active_pane().update(cx, |pane, cx| {
|
||||
pane.add_item(
|
||||
diff_editor.boxed_clone(),
|
||||
true,
|
||||
change_focus,
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
diff_editor.clone()
|
||||
}
|
||||
}
|
||||
})
|
||||
.ok()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some(first_hunk) = hunks.get().and_then(|hunks| hunks.first()) {
|
||||
let hunk_buffer_range = &first_hunk.buffer_range;
|
||||
if let Some(buffer_id) = hunk_buffer_range
|
||||
.start
|
||||
.buffer_id
|
||||
.or_else(|| first_hunk.buffer_range.end.buffer_id)
|
||||
{
|
||||
editor
|
||||
.update(&mut cx, |editor, cx| {
|
||||
let multi_buffer = editor.buffer().read(cx);
|
||||
let buffer = multi_buffer.buffer(buffer_id)?;
|
||||
let buffer_snapshot = buffer.read(cx).snapshot();
|
||||
let (excerpt_id, _) = multi_buffer
|
||||
.excerpts_for_buffer(&buffer, cx)
|
||||
.into_iter()
|
||||
.find(|(_, excerpt)| {
|
||||
hunk_buffer_range
|
||||
.start
|
||||
.cmp(&excerpt.context.start, &buffer_snapshot)
|
||||
.is_ge()
|
||||
&& hunk_buffer_range
|
||||
.end
|
||||
.cmp(&excerpt.context.end, &buffer_snapshot)
|
||||
.is_le()
|
||||
})?;
|
||||
let multi_buffer_hunk_start = multi_buffer
|
||||
.snapshot(cx)
|
||||
.anchor_in_excerpt(excerpt_id, hunk_buffer_range.start)?;
|
||||
editor.change_selections(
|
||||
Some(Autoscroll::Strategy(AutoscrollStrategy::Center)),
|
||||
cx,
|
||||
|s| {
|
||||
s.select_ranges(Some(
|
||||
multi_buffer_hunk_start..multi_buffer_hunk_start,
|
||||
))
|
||||
},
|
||||
);
|
||||
cx.notify();
|
||||
Some(())
|
||||
})
|
||||
.ok()
|
||||
.flatten();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -704,6 +1153,8 @@ impl Render for GitPanel {
|
||||
this.commit_all_changes(&CommitAllChanges, cx)
|
||||
}))
|
||||
})
|
||||
.on_action(cx.listener(Self::select_next))
|
||||
.on_action(cx.listener(Self::select_prev))
|
||||
.on_hover(cx.listener(|this, hovered, cx| {
|
||||
if *hovered {
|
||||
this.show_scrollbar = true;
|
||||
@@ -784,3 +1235,14 @@ impl Panel for GitPanel {
|
||||
Box::new(ToggleFocus)
|
||||
}
|
||||
}
|
||||
|
||||
fn diff_display_editor(project: Model<Project>, cx: &mut WindowContext) -> View<Editor> {
|
||||
cx.new_view(|cx| {
|
||||
let multi_buffer = cx.new_model(|cx| {
|
||||
MultiBuffer::new(project.read(cx).capability()).with_title("Project diff".to_string())
|
||||
});
|
||||
let mut editor = Editor::for_multibuffer(multi_buffer, Some(project), true, cx);
|
||||
editor.set_expand_all_diff_hunks();
|
||||
editor
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use gpui::*;
|
||||
use anyhow::Result;
|
||||
use gpui::{
|
||||
black, bounce, div, ease_in_out, percentage, prelude::*, px, rgb, size, svg, Animation,
|
||||
AnimationExt as _, App, AppContext, AssetSource, Bounds, SharedString, Transformation,
|
||||
ViewContext, WindowBounds, WindowOptions,
|
||||
};
|
||||
|
||||
struct Assets {}
|
||||
|
||||
@@ -37,7 +42,7 @@ impl Render for AnimationExample {
|
||||
div()
|
||||
.flex()
|
||||
.bg(rgb(0x2e7d32))
|
||||
.size(Length::Definite(Pixels(300.0).into()))
|
||||
.size(px(300.0))
|
||||
.justify_center()
|
||||
.items_center()
|
||||
.shadow_lg()
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
use gpui::*;
|
||||
use gpui::{
|
||||
div, prelude::*, px, rgb, size, App, AppContext, Bounds, SharedString, ViewContext,
|
||||
WindowBounds, WindowOptions,
|
||||
};
|
||||
|
||||
struct HelloWorld {
|
||||
text: SharedString,
|
||||
@@ -11,7 +14,7 @@ impl Render for HelloWorld {
|
||||
.flex_col()
|
||||
.gap_3()
|
||||
.bg(rgb(0x505050))
|
||||
.size(Length::Definite(Pixels(500.0).into()))
|
||||
.size(px(500.0))
|
||||
.justify_center()
|
||||
.items_center()
|
||||
.shadow_lg()
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
|
||||
use gpui::*;
|
||||
use std::fs;
|
||||
use anyhow::Result;
|
||||
use gpui::{
|
||||
actions, div, img, prelude::*, px, rgb, size, App, AppContext, AssetSource, Bounds,
|
||||
ImageSource, KeyBinding, Menu, MenuItem, Point, SharedString, SharedUri, TitlebarOptions,
|
||||
ViewContext, WindowBounds, WindowContext, WindowOptions,
|
||||
};
|
||||
|
||||
struct Assets {
|
||||
base: PathBuf,
|
||||
@@ -55,7 +60,7 @@ impl RenderOnce for ImageContainer {
|
||||
.size_full()
|
||||
.gap_4()
|
||||
.child(self.text)
|
||||
.child(img(self.src).w(px(256.0)).h(px(256.0))),
|
||||
.child(img(self.src).size(px(256.0))),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -75,7 +80,7 @@ impl Render for ImageShowcase {
|
||||
.justify_center()
|
||||
.items_center()
|
||||
.gap_8()
|
||||
.bg(rgb(0xFFFFFF))
|
||||
.bg(rgb(0xffffff))
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
use std::ops::Range;
|
||||
|
||||
use gpui::*;
|
||||
use gpui::{
|
||||
actions, black, div, fill, hsla, opaque_grey, point, prelude::*, px, relative, rgb, rgba, size,
|
||||
white, yellow, App, AppContext, Bounds, ClipboardItem, CursorStyle, ElementId,
|
||||
ElementInputHandler, FocusHandle, FocusableView, GlobalElementId, KeyBinding, Keystroke,
|
||||
LayoutId, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, Pixels, Point,
|
||||
ShapedLine, SharedString, Style, TextRun, UTF16Selection, UnderlineStyle, View, ViewContext,
|
||||
ViewInputHandler, WindowBounds, WindowContext, WindowOptions,
|
||||
};
|
||||
use unicode_segmentation::*;
|
||||
|
||||
actions!(
|
||||
@@ -463,7 +470,7 @@ impl Element for TextElement {
|
||||
bounds.bottom(),
|
||||
),
|
||||
),
|
||||
rgba(0x3311FF30),
|
||||
rgba(0x3311ff30),
|
||||
)),
|
||||
None,
|
||||
)
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
use std::{fs, path::PathBuf, time::Duration};
|
||||
|
||||
use gpui::*;
|
||||
use anyhow::Result;
|
||||
use gpui::{
|
||||
div, hsla, img, point, prelude::*, px, rgb, size, svg, App, AppContext, AssetSource, Bounds,
|
||||
BoxShadow, ClickEvent, SharedString, Task, Timer, ViewContext, WindowBounds, WindowOptions,
|
||||
};
|
||||
|
||||
struct Assets {
|
||||
base: PathBuf,
|
||||
@@ -76,7 +80,7 @@ impl Render for HelloWorld {
|
||||
.flex()
|
||||
.flex_row()
|
||||
.size_full()
|
||||
.bg(rgb(0xE0E0E0))
|
||||
.bg(rgb(0xe0e0e0))
|
||||
.text_xl()
|
||||
.child(
|
||||
div()
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use gpui::*;
|
||||
use gpui::{
|
||||
actions, div, prelude::*, rgb, App, AppContext, Menu, MenuItem, ViewContext, WindowOptions,
|
||||
};
|
||||
|
||||
struct SetMenus;
|
||||
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
use gpui::*;
|
||||
use gpui::{
|
||||
div, prelude::*, px, rgb, size, App, AppContext, Bounds, ViewContext, WindowBounds,
|
||||
WindowOptions,
|
||||
};
|
||||
|
||||
struct Shadow {}
|
||||
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use gpui::*;
|
||||
use std::fs;
|
||||
use anyhow::Result;
|
||||
use gpui::{
|
||||
div, prelude::*, px, rgb, size, svg, App, AppContext, AssetSource, Bounds, SharedString,
|
||||
ViewContext, WindowBounds, WindowOptions,
|
||||
};
|
||||
|
||||
struct Assets {
|
||||
base: PathBuf,
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use gpui::*;
|
||||
use gpui::{
|
||||
div, prelude::*, px, size, App, AppContext, Bounds, ViewContext, WindowBounds, WindowOptions,
|
||||
};
|
||||
|
||||
struct HelloWorld {}
|
||||
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
use gpui::*;
|
||||
use gpui::{
|
||||
div, prelude::*, px, rgb, size, uniform_list, App, AppContext, Bounds, ViewContext,
|
||||
WindowBounds, WindowOptions,
|
||||
};
|
||||
|
||||
struct UniformListExample {}
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use gpui::*;
|
||||
use prelude::FluentBuilder as _;
|
||||
use gpui::{
|
||||
div, prelude::*, px, rgb, size, App, AppContext, Bounds, SharedString, Timer, ViewContext,
|
||||
WindowBounds, WindowContext, WindowKind, WindowOptions,
|
||||
};
|
||||
|
||||
struct SubWindow {
|
||||
custom_titlebar: bool,
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
use gpui::*;
|
||||
use gpui::{
|
||||
div, point, prelude::*, px, rgb, App, AppContext, Bounds, DisplayId, Hsla, Pixels,
|
||||
SharedString, Size, ViewContext, WindowBackgroundAppearance, WindowBounds, WindowKind,
|
||||
WindowOptions,
|
||||
};
|
||||
|
||||
struct WindowContent {
|
||||
text: SharedString,
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
use gpui::*;
|
||||
use prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
black, canvas, div, green, point, prelude::*, px, rgb, size, transparent_black, white, App,
|
||||
AppContext, Bounds, CursorStyle, Decorations, Hsla, MouseButton, Pixels, Point, ResizeEdge,
|
||||
Size, ViewContext, WindowBackgroundAppearance, WindowBounds, WindowDecorations, WindowOptions,
|
||||
};
|
||||
|
||||
struct WindowShadow {}
|
||||
|
||||
/*
|
||||
Things to do:
|
||||
1. We need a way of calculating which edge or corner the mouse is on,
|
||||
and then dispatch on that
|
||||
2. We need to improve the shadow rendering significantly
|
||||
3. We need to implement the techniques in here in Zed
|
||||
*/
|
||||
// Things to do:
|
||||
// 1. We need a way of calculating which edge or corner the mouse is on,
|
||||
// and then dispatch on that
|
||||
// 2. We need to improve the shadow rendering significantly
|
||||
// 3. We need to implement the techniques in here in Zed
|
||||
|
||||
impl Render for WindowShadow {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
@@ -128,7 +129,7 @@ impl Render for WindowShadow {
|
||||
div()
|
||||
.flex()
|
||||
.bg(white())
|
||||
.size(Length::Definite(Pixels(300.0).into()))
|
||||
.size(px(300.0))
|
||||
.justify_center()
|
||||
.items_center()
|
||||
.shadow_lg()
|
||||
|
||||
@@ -420,6 +420,9 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
|
||||
fn get_raw_handle(&self) -> windows::HWND;
|
||||
|
||||
// Linux specific methods
|
||||
fn inner_window_bounds(&self) -> WindowBounds {
|
||||
self.window_bounds()
|
||||
}
|
||||
fn request_decorations(&self, _decorations: WindowDecorations) {}
|
||||
fn show_window_menu(&self, _position: Point<Pixels>) {}
|
||||
fn start_window_move(&self) {}
|
||||
|
||||
@@ -651,7 +651,7 @@ impl CursorStyle {
|
||||
// and https://github.com/KDE/breeze (KDE). Both of them seem to be also derived from
|
||||
// Web CSS cursor names: https://developer.mozilla.org/en-US/docs/Web/CSS/cursor#values
|
||||
match self {
|
||||
CursorStyle::Arrow => "arrow",
|
||||
CursorStyle::Arrow => "left_ptr",
|
||||
CursorStyle::IBeam => "text",
|
||||
CursorStyle::Crosshair => "crosshair",
|
||||
CursorStyle::ClosedHand => "grabbing",
|
||||
|
||||
@@ -781,6 +781,19 @@ impl PlatformWindow for WaylandWindow {
|
||||
}
|
||||
}
|
||||
|
||||
fn inner_window_bounds(&self) -> WindowBounds {
|
||||
let state = self.borrow();
|
||||
if state.fullscreen {
|
||||
WindowBounds::Fullscreen(state.window_bounds)
|
||||
} else if state.maximized {
|
||||
WindowBounds::Maximized(state.window_bounds)
|
||||
} else {
|
||||
let inset = state.inset.unwrap_or(px(0.));
|
||||
drop(state);
|
||||
WindowBounds::Windowed(self.bounds().inset(inset))
|
||||
}
|
||||
}
|
||||
|
||||
fn content_size(&self) -> Size<Pixels> {
|
||||
self.borrow().bounds.size
|
||||
}
|
||||
|
||||
@@ -1100,6 +1100,30 @@ impl PlatformWindow for X11Window {
|
||||
}
|
||||
}
|
||||
|
||||
fn inner_window_bounds(&self) -> WindowBounds {
|
||||
let state = self.0.state.borrow();
|
||||
if self.is_maximized() {
|
||||
WindowBounds::Maximized(state.bounds)
|
||||
} else {
|
||||
let mut bounds = state.bounds;
|
||||
let [left, right, top, bottom] = state.last_insets;
|
||||
|
||||
let [left, right, top, bottom] = [
|
||||
Pixels((left as f32) / state.scale_factor),
|
||||
Pixels((right as f32) / state.scale_factor),
|
||||
Pixels((top as f32) / state.scale_factor),
|
||||
Pixels((bottom as f32) / state.scale_factor),
|
||||
];
|
||||
|
||||
bounds.origin.x += left;
|
||||
bounds.origin.y += top;
|
||||
bounds.size.width -= left + right;
|
||||
bounds.size.height -= top + bottom;
|
||||
|
||||
WindowBounds::Windowed(bounds)
|
||||
}
|
||||
}
|
||||
|
||||
fn content_size(&self) -> Size<Pixels> {
|
||||
// We divide by the scale factor here because this value is queried to determine how much to draw,
|
||||
// but it will be multiplied later by the scale to adjust for scaling.
|
||||
|
||||
@@ -357,7 +357,7 @@ impl From<ImageFormat> for image::ImageFormat {
|
||||
ImageFormat::Jpeg => image::ImageFormat::Jpeg,
|
||||
ImageFormat::Webp => image::ImageFormat::WebP,
|
||||
ImageFormat::Gif => image::ImageFormat::Gif,
|
||||
// ImageFormat::Svg => todo!(),
|
||||
// TODO: ImageFormat::Svg
|
||||
ImageFormat::Bmp => image::ImageFormat::Bmp,
|
||||
ImageFormat::Tiff => image::ImageFormat::Tiff,
|
||||
_ => unreachable!(),
|
||||
|
||||
@@ -48,6 +48,7 @@ pub(crate) struct WindowsPlatform {
|
||||
|
||||
pub(crate) struct WindowsPlatformState {
|
||||
callbacks: PlatformCallbacks,
|
||||
menus: Vec<OwnedMenu>,
|
||||
// NOTE: standard cursor handles don't need to close.
|
||||
pub(crate) current_cursor: HCURSOR,
|
||||
}
|
||||
@@ -70,6 +71,7 @@ impl WindowsPlatformState {
|
||||
Self {
|
||||
callbacks,
|
||||
current_cursor,
|
||||
menus: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -449,8 +451,15 @@ impl Platform for WindowsPlatform {
|
||||
self.state.borrow_mut().callbacks.reopen = Some(callback);
|
||||
}
|
||||
|
||||
fn set_menus(&self, menus: Vec<Menu>, _keymap: &Keymap) {
|
||||
self.state.borrow_mut().menus = menus.into_iter().map(|menu| menu.owned()).collect();
|
||||
}
|
||||
|
||||
fn get_menus(&self) -> Option<Vec<OwnedMenu>> {
|
||||
Some(self.state.borrow().menus.clone())
|
||||
}
|
||||
|
||||
// todo(windows)
|
||||
fn set_menus(&self, _menus: Vec<Menu>, _keymap: &Keymap) {}
|
||||
fn set_dock_menu(&self, _menus: Vec<MenuItem>, _keymap: &Keymap) {}
|
||||
|
||||
fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>) {
|
||||
|
||||
@@ -999,6 +999,11 @@ impl<'a> WindowContext<'a> {
|
||||
self.window.platform_window.window_bounds()
|
||||
}
|
||||
|
||||
/// Return the `WindowBounds` excluding insets (Wayland and X11)
|
||||
pub fn inner_window_bounds(&self) -> WindowBounds {
|
||||
self.window.platform_window.inner_window_bounds()
|
||||
}
|
||||
|
||||
/// Dispatch the given action on the currently focused element.
|
||||
pub fn dispatch_action(&mut self, action: Box<dyn Action>) {
|
||||
let focus_handle = self.focused();
|
||||
|
||||
@@ -68,7 +68,7 @@ pub use text::{
|
||||
use theme::SyntaxTheme;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
use util::RandomCharIter;
|
||||
use util::{debug_panic, RangeExt};
|
||||
use util::{debug_panic, maybe, RangeExt};
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub use {tree_sitter_rust, tree_sitter_typescript};
|
||||
@@ -2923,10 +2923,13 @@ impl BufferSnapshot {
|
||||
(start..end, word_kind)
|
||||
}
|
||||
|
||||
/// Returns the range for the closes syntax node enclosing the given range.
|
||||
pub fn range_for_syntax_ancestor<T: ToOffset>(&self, range: Range<T>) -> Option<Range<usize>> {
|
||||
/// Returns the closest syntax node enclosing the given range.
|
||||
pub fn syntax_ancestor<'a, T: ToOffset>(
|
||||
&'a self,
|
||||
range: Range<T>,
|
||||
) -> Option<tree_sitter::Node<'a>> {
|
||||
let range = range.start.to_offset(self)..range.end.to_offset(self);
|
||||
let mut result: Option<Range<usize>> = None;
|
||||
let mut result: Option<tree_sitter::Node<'a>> = None;
|
||||
'outer: for layer in self
|
||||
.syntax
|
||||
.layers_for_range(range.clone(), &self.text, true)
|
||||
@@ -2956,7 +2959,7 @@ impl BufferSnapshot {
|
||||
}
|
||||
|
||||
let left_node = cursor.node();
|
||||
let mut layer_result = left_node.byte_range();
|
||||
let mut layer_result = left_node;
|
||||
|
||||
// For an empty range, try to find another node immediately to the right of the range.
|
||||
if left_node.end_byte() == range.start {
|
||||
@@ -2979,13 +2982,13 @@ impl BufferSnapshot {
|
||||
// If both nodes are the same in that regard, favor the right one.
|
||||
if let Some(right_node) = right_node {
|
||||
if right_node.is_named() || !left_node.is_named() {
|
||||
layer_result = right_node.byte_range();
|
||||
layer_result = right_node;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(previous_result) = &result {
|
||||
if previous_result.len() < layer_result.len() {
|
||||
if previous_result.byte_range().len() < layer_result.byte_range().len() {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -3028,6 +3031,48 @@ impl BufferSnapshot {
|
||||
Some(items)
|
||||
}
|
||||
|
||||
pub fn outline_range_containing<T: ToOffset>(&self, range: Range<T>) -> Option<Range<Point>> {
|
||||
let range = range.to_offset(self);
|
||||
let mut matches = self.syntax.matches(range.clone(), &self.text, |grammar| {
|
||||
grammar.outline_config.as_ref().map(|c| &c.query)
|
||||
});
|
||||
let configs = matches
|
||||
.grammars()
|
||||
.iter()
|
||||
.map(|g| g.outline_config.as_ref().unwrap())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
while let Some(mat) = matches.peek() {
|
||||
let config = &configs[mat.grammar_index];
|
||||
let containing_item_node = maybe!({
|
||||
let item_node = mat.captures.iter().find_map(|cap| {
|
||||
if cap.index == config.item_capture_ix {
|
||||
Some(cap.node)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})?;
|
||||
|
||||
let item_byte_range = item_node.byte_range();
|
||||
if item_byte_range.end < range.start || item_byte_range.start > range.end {
|
||||
None
|
||||
} else {
|
||||
Some(item_node)
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(item_node) = containing_item_node {
|
||||
return Some(
|
||||
Point::from_ts_point(item_node.start_position())
|
||||
..Point::from_ts_point(item_node.end_position()),
|
||||
);
|
||||
}
|
||||
|
||||
matches.advance();
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn outline_items_containing<T: ToOffset>(
|
||||
&self,
|
||||
range: Range<T>,
|
||||
|
||||
@@ -1104,20 +1104,32 @@ fn test_range_for_syntax_ancestor(cx: &mut AppContext) {
|
||||
let snapshot = buffer.snapshot();
|
||||
|
||||
assert_eq!(
|
||||
snapshot.range_for_syntax_ancestor(empty_range_at(text, "|")),
|
||||
Some(range_of(text, "|"))
|
||||
snapshot
|
||||
.syntax_ancestor(empty_range_at(text, "|"))
|
||||
.unwrap()
|
||||
.byte_range(),
|
||||
range_of(text, "|")
|
||||
);
|
||||
assert_eq!(
|
||||
snapshot.range_for_syntax_ancestor(range_of(text, "|")),
|
||||
Some(range_of(text, "|c|"))
|
||||
snapshot
|
||||
.syntax_ancestor(range_of(text, "|"))
|
||||
.unwrap()
|
||||
.byte_range(),
|
||||
range_of(text, "|c|")
|
||||
);
|
||||
assert_eq!(
|
||||
snapshot.range_for_syntax_ancestor(range_of(text, "|c|")),
|
||||
Some(range_of(text, "|c| {}"))
|
||||
snapshot
|
||||
.syntax_ancestor(range_of(text, "|c|"))
|
||||
.unwrap()
|
||||
.byte_range(),
|
||||
range_of(text, "|c| {}")
|
||||
);
|
||||
assert_eq!(
|
||||
snapshot.range_for_syntax_ancestor(range_of(text, "|c| {}")),
|
||||
Some(range_of(text, "(|c| {})"))
|
||||
snapshot
|
||||
.syntax_ancestor(range_of(text, "|c| {}"))
|
||||
.unwrap()
|
||||
.byte_range(),
|
||||
range_of(text, "(|c| {})")
|
||||
);
|
||||
|
||||
buffer
|
||||
|
||||
@@ -78,7 +78,7 @@ pub use language_registry::{
|
||||
};
|
||||
pub use lsp::LanguageServerId;
|
||||
pub use outline::*;
|
||||
pub use syntax_map::{OwnedSyntaxLayer, SyntaxLayer, TreeSitterOptions};
|
||||
pub use syntax_map::{OwnedSyntaxLayer, SyntaxLayer, ToTreeSitterPoint, TreeSitterOptions};
|
||||
pub use text::{AnchorRangeExt, LineEnding};
|
||||
pub use tree_sitter::{Node, Parser, Tree, TreeCursor};
|
||||
|
||||
@@ -385,6 +385,15 @@ pub trait LspAdapter: 'static + Send + Sync {
|
||||
None
|
||||
}
|
||||
|
||||
async fn check_if_version_installed(
|
||||
&self,
|
||||
_version: &(dyn 'static + Send + Any),
|
||||
_container_dir: &PathBuf,
|
||||
_delegate: &dyn LspAdapterDelegate,
|
||||
) -> Option<LanguageServerBinary> {
|
||||
None
|
||||
}
|
||||
|
||||
async fn fetch_server_binary(
|
||||
&self,
|
||||
latest_version: Box<dyn 'static + Send + Any>,
|
||||
@@ -516,14 +525,23 @@ async fn try_fetch_server_binary<L: LspAdapter + 'static + Send + Sync + ?Sized>
|
||||
.fetch_latest_server_version(delegate.as_ref())
|
||||
.await?;
|
||||
|
||||
log::info!("downloading language server {:?}", name.0);
|
||||
delegate.update_status(adapter.name(), LanguageServerBinaryStatus::Downloading);
|
||||
let binary = adapter
|
||||
.fetch_server_binary(latest_version, container_dir, delegate.as_ref())
|
||||
.await;
|
||||
if let Some(binary) = adapter
|
||||
.check_if_version_installed(latest_version.as_ref(), &container_dir, delegate.as_ref())
|
||||
.await
|
||||
{
|
||||
log::info!("language server {:?} is already installed", name.0);
|
||||
delegate.update_status(name.clone(), LanguageServerBinaryStatus::None);
|
||||
Ok(binary)
|
||||
} else {
|
||||
log::info!("downloading language server {:?}", name.0);
|
||||
delegate.update_status(adapter.name(), LanguageServerBinaryStatus::Downloading);
|
||||
let binary = adapter
|
||||
.fetch_server_binary(latest_version, container_dir, delegate.as_ref())
|
||||
.await;
|
||||
|
||||
delegate.update_status(name.clone(), LanguageServerBinaryStatus::None);
|
||||
binary
|
||||
delegate.update_status(name.clone(), LanguageServerBinaryStatus::None);
|
||||
binary
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
|
||||
@@ -1845,7 +1845,7 @@ impl Drop for QueryCursorHandle {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) trait ToTreeSitterPoint {
|
||||
pub trait ToTreeSitterPoint {
|
||||
fn to_ts_point(self) -> tree_sitter::Point;
|
||||
fn from_ts_point(point: tree_sitter::Point) -> Self;
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ pub struct CssLspAdapter {
|
||||
}
|
||||
|
||||
impl CssLspAdapter {
|
||||
const PACKAGE_NAME: &str = "vscode-langservers-extracted";
|
||||
pub fn new(node: NodeRuntime) -> Self {
|
||||
CssLspAdapter { node }
|
||||
}
|
||||
@@ -56,18 +57,13 @@ impl LspAdapter for CssLspAdapter {
|
||||
) -> Result<LanguageServerBinary> {
|
||||
let latest_version = latest_version.downcast::<String>().unwrap();
|
||||
let server_path = container_dir.join(SERVER_PATH);
|
||||
let package_name = "vscode-langservers-extracted";
|
||||
|
||||
let should_install_language_server = self
|
||||
.node
|
||||
.should_install_npm_package(package_name, &server_path, &container_dir, &latest_version)
|
||||
.await;
|
||||
|
||||
if should_install_language_server {
|
||||
self.node
|
||||
.npm_install_packages(&container_dir, &[(package_name, latest_version.as_str())])
|
||||
.await?;
|
||||
}
|
||||
self.node
|
||||
.npm_install_packages(
|
||||
&container_dir,
|
||||
&[(Self::PACKAGE_NAME, latest_version.as_str())],
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(LanguageServerBinary {
|
||||
path: self.node.binary_path().await?,
|
||||
@@ -76,6 +72,31 @@ impl LspAdapter for CssLspAdapter {
|
||||
})
|
||||
}
|
||||
|
||||
async fn check_if_version_installed(
|
||||
&self,
|
||||
version: &(dyn 'static + Send + Any),
|
||||
container_dir: &PathBuf,
|
||||
_: &dyn LspAdapterDelegate,
|
||||
) -> Option<LanguageServerBinary> {
|
||||
let version = version.downcast_ref::<String>().unwrap();
|
||||
let server_path = container_dir.join(SERVER_PATH);
|
||||
|
||||
let should_install_language_server = self
|
||||
.node
|
||||
.should_install_npm_package(Self::PACKAGE_NAME, &server_path, &container_dir, &version)
|
||||
.await;
|
||||
|
||||
if should_install_language_server {
|
||||
None
|
||||
} else {
|
||||
Some(LanguageServerBinary {
|
||||
path: self.node.binary_path().await.ok()?,
|
||||
env: None,
|
||||
arguments: server_binary_arguments(&server_path),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async fn cached_server_binary(
|
||||
&self,
|
||||
container_dir: PathBuf,
|
||||
|
||||
@@ -64,6 +64,8 @@ pub struct JsonLspAdapter {
|
||||
}
|
||||
|
||||
impl JsonLspAdapter {
|
||||
const PACKAGE_NAME: &str = "vscode-langservers-extracted";
|
||||
|
||||
pub fn new(node: NodeRuntime, languages: Arc<LanguageRegistry>) -> Self {
|
||||
Self {
|
||||
node,
|
||||
@@ -142,11 +144,36 @@ impl LspAdapter for JsonLspAdapter {
|
||||
) -> Result<Box<dyn 'static + Send + Any>> {
|
||||
Ok(Box::new(
|
||||
self.node
|
||||
.npm_package_latest_version("vscode-langservers-extracted")
|
||||
.npm_package_latest_version(Self::PACKAGE_NAME)
|
||||
.await?,
|
||||
) as Box<_>)
|
||||
}
|
||||
|
||||
async fn check_if_version_installed(
|
||||
&self,
|
||||
version: &(dyn 'static + Send + Any),
|
||||
container_dir: &PathBuf,
|
||||
_: &dyn LspAdapterDelegate,
|
||||
) -> Option<LanguageServerBinary> {
|
||||
let version = version.downcast_ref::<String>().unwrap();
|
||||
let server_path = container_dir.join(SERVER_PATH);
|
||||
|
||||
let should_install_language_server = self
|
||||
.node
|
||||
.should_install_npm_package(Self::PACKAGE_NAME, &server_path, &container_dir, &version)
|
||||
.await;
|
||||
|
||||
if should_install_language_server {
|
||||
None
|
||||
} else {
|
||||
Some(LanguageServerBinary {
|
||||
path: self.node.binary_path().await.ok()?,
|
||||
env: None,
|
||||
arguments: server_binary_arguments(&server_path),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async fn fetch_server_binary(
|
||||
&self,
|
||||
latest_version: Box<dyn 'static + Send + Any>,
|
||||
@@ -155,18 +182,13 @@ impl LspAdapter for JsonLspAdapter {
|
||||
) -> Result<LanguageServerBinary> {
|
||||
let latest_version = latest_version.downcast::<String>().unwrap();
|
||||
let server_path = container_dir.join(SERVER_PATH);
|
||||
let package_name = "vscode-langservers-extracted";
|
||||
|
||||
let should_install_language_server = self
|
||||
.node
|
||||
.should_install_npm_package(package_name, &server_path, &container_dir, &latest_version)
|
||||
.await;
|
||||
|
||||
if should_install_language_server {
|
||||
self.node
|
||||
.npm_install_packages(&container_dir, &[(package_name, latest_version.as_str())])
|
||||
.await?;
|
||||
}
|
||||
self.node
|
||||
.npm_install_packages(
|
||||
&container_dir,
|
||||
&[(Self::PACKAGE_NAME, latest_version.as_str())],
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(LanguageServerBinary {
|
||||
path: self.node.binary_path().await?,
|
||||
|
||||
@@ -117,24 +117,12 @@ impl LspAdapter for PythonLspAdapter {
|
||||
let latest_version = latest_version.downcast::<String>().unwrap();
|
||||
let server_path = container_dir.join(SERVER_PATH);
|
||||
|
||||
let should_install_language_server = self
|
||||
.node
|
||||
.should_install_npm_package(
|
||||
Self::SERVER_NAME.as_ref(),
|
||||
&server_path,
|
||||
self.node
|
||||
.npm_install_packages(
|
||||
&container_dir,
|
||||
&latest_version,
|
||||
&[(Self::SERVER_NAME.as_ref(), latest_version.as_str())],
|
||||
)
|
||||
.await;
|
||||
|
||||
if should_install_language_server {
|
||||
self.node
|
||||
.npm_install_packages(
|
||||
&container_dir,
|
||||
&[(Self::SERVER_NAME.as_ref(), latest_version.as_str())],
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
.await?;
|
||||
|
||||
Ok(LanguageServerBinary {
|
||||
path: self.node.binary_path().await?,
|
||||
@@ -143,6 +131,36 @@ impl LspAdapter for PythonLspAdapter {
|
||||
})
|
||||
}
|
||||
|
||||
async fn check_if_version_installed(
|
||||
&self,
|
||||
version: &(dyn 'static + Send + Any),
|
||||
container_dir: &PathBuf,
|
||||
_: &dyn LspAdapterDelegate,
|
||||
) -> Option<LanguageServerBinary> {
|
||||
let version = version.downcast_ref::<String>().unwrap();
|
||||
let server_path = container_dir.join(SERVER_PATH);
|
||||
|
||||
let should_install_language_server = self
|
||||
.node
|
||||
.should_install_npm_package(
|
||||
Self::SERVER_NAME.as_ref(),
|
||||
&server_path,
|
||||
&container_dir,
|
||||
&version,
|
||||
)
|
||||
.await;
|
||||
|
||||
if should_install_language_server {
|
||||
None
|
||||
} else {
|
||||
Some(LanguageServerBinary {
|
||||
path: self.node.binary_path().await.ok()?,
|
||||
env: None,
|
||||
arguments: server_binary_arguments(&server_path),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async fn cached_server_binary(
|
||||
&self,
|
||||
container_dir: PathBuf,
|
||||
|
||||
@@ -36,11 +36,25 @@
|
||||
(function_definition
|
||||
name: (identifier) @run @_pytest_method_name
|
||||
(#match? @_pytest_method_name "^test_")
|
||||
) @python-pytest-method
|
||||
) @_python-pytest-method
|
||||
)
|
||||
(#set! tag python-pytest-method)
|
||||
)
|
||||
|
||||
; decorated pytest functions
|
||||
(
|
||||
(module
|
||||
(decorated_definition
|
||||
(decorator)+ @_decorator
|
||||
definition: (function_definition
|
||||
name: (identifier) @run @_pytest_method_name
|
||||
(#match? @_pytest_method_name "^test_")
|
||||
)
|
||||
) @_python-pytest-method
|
||||
)
|
||||
(#set! tag python-pytest-method)
|
||||
)
|
||||
|
||||
; pytest classes
|
||||
(
|
||||
(module
|
||||
@@ -62,7 +76,7 @@
|
||||
(function_definition
|
||||
name: (identifier) @run @_pytest_method_name
|
||||
(#match? @_pytest_method_name "^test")
|
||||
) @python-pytest-method
|
||||
) @_python-pytest-method
|
||||
(#set! tag python-pytest-method)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -34,6 +34,7 @@ pub struct TailwindLspAdapter {
|
||||
impl TailwindLspAdapter {
|
||||
const SERVER_NAME: LanguageServerName =
|
||||
LanguageServerName::new_static("tailwindcss-language-server");
|
||||
const PACKAGE_NAME: &str = "@tailwindcss/language-server";
|
||||
|
||||
pub fn new(node: NodeRuntime) -> Self {
|
||||
TailwindLspAdapter { node }
|
||||
@@ -52,7 +53,7 @@ impl LspAdapter for TailwindLspAdapter {
|
||||
) -> Result<Box<dyn 'static + Any + Send>> {
|
||||
Ok(Box::new(
|
||||
self.node
|
||||
.npm_package_latest_version("@tailwindcss/language-server")
|
||||
.npm_package_latest_version(Self::PACKAGE_NAME)
|
||||
.await?,
|
||||
) as Box<_>)
|
||||
}
|
||||
@@ -65,18 +66,13 @@ impl LspAdapter for TailwindLspAdapter {
|
||||
) -> Result<LanguageServerBinary> {
|
||||
let latest_version = latest_version.downcast::<String>().unwrap();
|
||||
let server_path = container_dir.join(SERVER_PATH);
|
||||
let package_name = "@tailwindcss/language-server";
|
||||
|
||||
let should_install_language_server = self
|
||||
.node
|
||||
.should_install_npm_package(package_name, &server_path, &container_dir, &latest_version)
|
||||
.await;
|
||||
|
||||
if should_install_language_server {
|
||||
self.node
|
||||
.npm_install_packages(&container_dir, &[(package_name, latest_version.as_str())])
|
||||
.await?;
|
||||
}
|
||||
self.node
|
||||
.npm_install_packages(
|
||||
&container_dir,
|
||||
&[(Self::PACKAGE_NAME, latest_version.as_str())],
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(LanguageServerBinary {
|
||||
path: self.node.binary_path().await?,
|
||||
@@ -85,6 +81,31 @@ impl LspAdapter for TailwindLspAdapter {
|
||||
})
|
||||
}
|
||||
|
||||
async fn check_if_version_installed(
|
||||
&self,
|
||||
version: &(dyn 'static + Send + Any),
|
||||
container_dir: &PathBuf,
|
||||
_: &dyn LspAdapterDelegate,
|
||||
) -> Option<LanguageServerBinary> {
|
||||
let version = version.downcast_ref::<String>().unwrap();
|
||||
let server_path = container_dir.join(SERVER_PATH);
|
||||
|
||||
let should_install_language_server = self
|
||||
.node
|
||||
.should_install_npm_package(Self::PACKAGE_NAME, &server_path, &container_dir, &version)
|
||||
.await;
|
||||
|
||||
if should_install_language_server {
|
||||
None
|
||||
} else {
|
||||
Some(LanguageServerBinary {
|
||||
path: self.node.binary_path().await.ok()?,
|
||||
env: None,
|
||||
arguments: server_binary_arguments(&server_path),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async fn cached_server_binary(
|
||||
&self,
|
||||
container_dir: PathBuf,
|
||||
|
||||
@@ -73,6 +73,7 @@ impl TypeScriptLspAdapter {
|
||||
const NEW_SERVER_PATH: &'static str = "node_modules/typescript-language-server/lib/cli.mjs";
|
||||
const SERVER_NAME: LanguageServerName =
|
||||
LanguageServerName::new_static("typescript-language-server");
|
||||
const PACKAGE_NAME: &str = "typescript";
|
||||
pub fn new(node: NodeRuntime) -> Self {
|
||||
TypeScriptLspAdapter { node }
|
||||
}
|
||||
@@ -114,6 +115,36 @@ impl LspAdapter for TypeScriptLspAdapter {
|
||||
}) as Box<_>)
|
||||
}
|
||||
|
||||
async fn check_if_version_installed(
|
||||
&self,
|
||||
version: &(dyn 'static + Send + Any),
|
||||
container_dir: &PathBuf,
|
||||
_: &dyn LspAdapterDelegate,
|
||||
) -> Option<LanguageServerBinary> {
|
||||
let version = version.downcast_ref::<TypeScriptVersions>().unwrap();
|
||||
let server_path = container_dir.join(Self::NEW_SERVER_PATH);
|
||||
|
||||
let should_install_language_server = self
|
||||
.node
|
||||
.should_install_npm_package(
|
||||
Self::PACKAGE_NAME,
|
||||
&server_path,
|
||||
&container_dir,
|
||||
version.typescript_version.as_str(),
|
||||
)
|
||||
.await;
|
||||
|
||||
if should_install_language_server {
|
||||
None
|
||||
} else {
|
||||
Some(LanguageServerBinary {
|
||||
path: self.node.binary_path().await.ok()?,
|
||||
env: None,
|
||||
arguments: typescript_server_binary_arguments(&server_path),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async fn fetch_server_binary(
|
||||
&self,
|
||||
latest_version: Box<dyn 'static + Send + Any>,
|
||||
@@ -122,32 +153,22 @@ impl LspAdapter for TypeScriptLspAdapter {
|
||||
) -> Result<LanguageServerBinary> {
|
||||
let latest_version = latest_version.downcast::<TypeScriptVersions>().unwrap();
|
||||
let server_path = container_dir.join(Self::NEW_SERVER_PATH);
|
||||
let package_name = "typescript";
|
||||
|
||||
let should_install_language_server = self
|
||||
.node
|
||||
.should_install_npm_package(
|
||||
package_name,
|
||||
&server_path,
|
||||
self.node
|
||||
.npm_install_packages(
|
||||
&container_dir,
|
||||
latest_version.typescript_version.as_str(),
|
||||
&[
|
||||
(
|
||||
Self::PACKAGE_NAME,
|
||||
latest_version.typescript_version.as_str(),
|
||||
),
|
||||
(
|
||||
"typescript-language-server",
|
||||
latest_version.server_version.as_str(),
|
||||
),
|
||||
],
|
||||
)
|
||||
.await;
|
||||
|
||||
if should_install_language_server {
|
||||
self.node
|
||||
.npm_install_packages(
|
||||
&container_dir,
|
||||
&[
|
||||
(package_name, latest_version.typescript_version.as_str()),
|
||||
(
|
||||
"typescript-language-server",
|
||||
latest_version.server_version.as_str(),
|
||||
),
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
.await?;
|
||||
|
||||
Ok(LanguageServerBinary {
|
||||
path: self.node.binary_path().await?,
|
||||
|
||||
@@ -31,6 +31,7 @@ pub struct YamlLspAdapter {
|
||||
|
||||
impl YamlLspAdapter {
|
||||
const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("yaml-language-server");
|
||||
const PACKAGE_NAME: &str = "yaml-language-server";
|
||||
pub fn new(node: NodeRuntime) -> Self {
|
||||
YamlLspAdapter { node }
|
||||
}
|
||||
@@ -61,18 +62,13 @@ impl LspAdapter for YamlLspAdapter {
|
||||
) -> Result<LanguageServerBinary> {
|
||||
let latest_version = latest_version.downcast::<String>().unwrap();
|
||||
let server_path = container_dir.join(SERVER_PATH);
|
||||
let package_name = "yaml-language-server";
|
||||
|
||||
let should_install_language_server = self
|
||||
.node
|
||||
.should_install_npm_package(package_name, &server_path, &container_dir, &latest_version)
|
||||
.await;
|
||||
|
||||
if should_install_language_server {
|
||||
self.node
|
||||
.npm_install_packages(&container_dir, &[(package_name, latest_version.as_str())])
|
||||
.await?;
|
||||
}
|
||||
self.node
|
||||
.npm_install_packages(
|
||||
&container_dir,
|
||||
&[(Self::PACKAGE_NAME, latest_version.as_str())],
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(LanguageServerBinary {
|
||||
path: self.node.binary_path().await?,
|
||||
@@ -81,6 +77,31 @@ impl LspAdapter for YamlLspAdapter {
|
||||
})
|
||||
}
|
||||
|
||||
async fn check_if_version_installed(
|
||||
&self,
|
||||
version: &(dyn 'static + Send + Any),
|
||||
container_dir: &PathBuf,
|
||||
_: &dyn LspAdapterDelegate,
|
||||
) -> Option<LanguageServerBinary> {
|
||||
let version = version.downcast_ref::<String>().unwrap();
|
||||
let server_path = container_dir.join(SERVER_PATH);
|
||||
|
||||
let should_install_language_server = self
|
||||
.node
|
||||
.should_install_npm_package(Self::PACKAGE_NAME, &server_path, &container_dir, &version)
|
||||
.await;
|
||||
|
||||
if should_install_language_server {
|
||||
None
|
||||
} else {
|
||||
Some(LanguageServerBinary {
|
||||
path: self.node.binary_path().await.ok()?,
|
||||
env: None,
|
||||
arguments: server_binary_arguments(&server_path),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async fn cached_server_binary(
|
||||
&self,
|
||||
container_dir: PathBuf,
|
||||
|
||||
@@ -54,7 +54,7 @@ They can also be detected automatically, for example https://zed.dev/blog.
|
||||
## Images
|
||||
Images are like links, but with an exclamation mark `!` in front.
|
||||
|
||||
```todo!
|
||||
```markdown
|
||||

|
||||
```
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use assets::Assets;
|
||||
use gpui::*;
|
||||
use gpui::{rgb, App, KeyBinding, Length, StyleRefinement, View, WindowOptions};
|
||||
use language::{language_settings::AllLanguageSettings, LanguageRegistry};
|
||||
use markdown::{Markdown, MarkdownStyle};
|
||||
use node_runtime::NodeRuntime;
|
||||
|
||||
@@ -39,6 +39,7 @@ smallvec.workspace = true
|
||||
sum_tree.workspace = true
|
||||
text.workspace = true
|
||||
theme.workspace = true
|
||||
tree-sitter.workspace = true
|
||||
util.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -3920,15 +3920,16 @@ impl MultiBufferSnapshot {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn range_for_syntax_ancestor<T: ToOffset>(&self, range: Range<T>) -> Option<Range<usize>> {
|
||||
pub fn syntax_ancestor<T: ToOffset>(
|
||||
&self,
|
||||
range: Range<T>,
|
||||
) -> Option<(tree_sitter::Node, Range<usize>)> {
|
||||
let range = range.start.to_offset(self)..range.end.to_offset(self);
|
||||
let excerpt = self.excerpt_containing(range.clone())?;
|
||||
|
||||
let ancestor_buffer_range = excerpt
|
||||
let node = excerpt
|
||||
.buffer()
|
||||
.range_for_syntax_ancestor(excerpt.map_range_to_buffer(range))?;
|
||||
|
||||
Some(excerpt.map_range_from_buffer(ancestor_buffer_range))
|
||||
.syntax_ancestor(excerpt.map_range_to_buffer(range))?;
|
||||
Some((node, excerpt.map_range_from_buffer(node.byte_range())))
|
||||
}
|
||||
|
||||
pub fn outline(&self, theme: Option<&SyntaxTheme>) -> Option<Outline<Anchor>> {
|
||||
|
||||
@@ -1668,7 +1668,7 @@ impl BufferStore {
|
||||
.log_err();
|
||||
}
|
||||
|
||||
// todo!(max): do something
|
||||
// TODO(max): do something
|
||||
// client
|
||||
// .send(proto::UpdateStagedText {
|
||||
// project_id,
|
||||
|
||||
@@ -3142,7 +3142,7 @@ impl LspStore {
|
||||
fn request_workspace_config_refresh(&mut self) {
|
||||
*self._maintain_workspace_config.1.borrow_mut() = ();
|
||||
}
|
||||
// todo!
|
||||
|
||||
pub fn prettier_store(&self) -> Option<Model<PrettierStore>> {
|
||||
self.as_local().map(|local| local.prettier_store.clone())
|
||||
}
|
||||
|
||||
@@ -3200,7 +3200,7 @@ impl ProjectPanel {
|
||||
item_colors.default
|
||||
};
|
||||
|
||||
let bg_hover_color = if self.mouse_down {
|
||||
let bg_hover_color = if self.mouse_down || is_marked || is_active {
|
||||
item_colors.marked_active
|
||||
} else {
|
||||
item_colors.hover
|
||||
@@ -3224,9 +3224,7 @@ impl ProjectPanel {
|
||||
.border_1()
|
||||
.border_r_2()
|
||||
.border_color(border_color)
|
||||
.when(!is_marked && !is_active, |div| {
|
||||
div.hover(|style| style.bg(bg_hover_color))
|
||||
})
|
||||
.hover(|style| style.bg(bg_hover_color))
|
||||
.when(is_local, |div| {
|
||||
div.on_drag_move::<ExternalPaths>(cx.listener(
|
||||
move |this, event: &DragMoveEvent<ExternalPaths>, cx| {
|
||||
|
||||
@@ -9,6 +9,10 @@ fn main() {
|
||||
"cargo:rustc-env=ZED_PKG_VERSION={}",
|
||||
zed_cargo_toml.package.unwrap().version.unwrap()
|
||||
);
|
||||
println!(
|
||||
"cargo:rustc-env=TARGET={}",
|
||||
std::env::var("TARGET").unwrap()
|
||||
);
|
||||
|
||||
// If we're building this for nightly, we want to set the ZED_COMMIT_SHA
|
||||
if let Some(release_channel) = std::env::var("ZED_RELEASE_CHANNEL").ok() {
|
||||
|
||||
@@ -160,6 +160,7 @@ fn init_panic_hook() {
|
||||
option_env!("ZED_COMMIT_SHA").unwrap_or(&env!("ZED_PKG_VERSION"))
|
||||
),
|
||||
release_channel: release_channel::RELEASE_CHANNEL.display_name().into(),
|
||||
target: env!("TARGET").to_owned().into(),
|
||||
os_name: telemetry::os_name(),
|
||||
os_version: Some(telemetry::os_version()),
|
||||
architecture: env::consts::ARCH.into(),
|
||||
|
||||
@@ -515,7 +515,7 @@ impl project::ProjectItem for NotebookItem {
|
||||
Ok(nbformat::Notebook::V4(notebook)) => notebook,
|
||||
// 4.1 - 4.4 are converted to 4.5
|
||||
Ok(nbformat::Notebook::Legacy(legacy_notebook)) => {
|
||||
// todo!(): Decide if we want to mutate the notebook by including Cell IDs
|
||||
// TODO: Decide if we want to mutate the notebook by including Cell IDs
|
||||
// and any other conversions
|
||||
let notebook = nbformat::upgrade_legacy_notebook(legacy_notebook)?;
|
||||
notebook
|
||||
|
||||
@@ -57,7 +57,7 @@ impl OutputContent for MarkdownView {
|
||||
|
||||
fn buffer_content(&mut self, cx: &mut WindowContext) -> Option<Model<Buffer>> {
|
||||
let buffer = cx.new_model(|cx| {
|
||||
// todo!(): Bring in the language registry so we can set the language to markdown
|
||||
// TODO: Bring in the language registry so we can set the language to markdown
|
||||
let mut buffer = Buffer::local(self.raw_text.clone(), cx)
|
||||
.with_language(language::PLAIN_TEXT.clone(), cx);
|
||||
buffer.set_capability(language::Capability::ReadOnly, cx);
|
||||
|
||||
@@ -610,7 +610,7 @@ impl Session {
|
||||
|
||||
// Start a new kernel
|
||||
this.update(&mut cx, |session, cx| {
|
||||
// todo!(): Differentiate between restart and restart+clear-outputs
|
||||
// TODO: Differentiate between restart and restart+clear-outputs
|
||||
session.clear_outputs(cx);
|
||||
session.start_kernel(cx);
|
||||
})
|
||||
|
||||
@@ -269,6 +269,7 @@ pub struct Panic {
|
||||
pub app_version: String,
|
||||
/// Zed release channel (stable, preview, dev)
|
||||
pub release_channel: String,
|
||||
pub target: Option<String>,
|
||||
pub os_name: String,
|
||||
pub os_version: Option<String>,
|
||||
pub architecture: String,
|
||||
|
||||
@@ -1183,10 +1183,10 @@ impl Terminal {
|
||||
}
|
||||
|
||||
let motion: Option<ViMotion> = match key.as_str() {
|
||||
"h" => Some(ViMotion::Left),
|
||||
"j" => Some(ViMotion::Down),
|
||||
"k" => Some(ViMotion::Up),
|
||||
"l" => Some(ViMotion::Right),
|
||||
"h" | "left" => Some(ViMotion::Left),
|
||||
"j" | "down" => Some(ViMotion::Down),
|
||||
"k" | "up" => Some(ViMotion::Up),
|
||||
"l" | "right" => Some(ViMotion::Right),
|
||||
"w" => Some(ViMotion::WordRight),
|
||||
"b" if !keystroke.modifiers.control => Some(ViMotion::WordLeft),
|
||||
"e" => Some(ViMotion::WordRightEnd),
|
||||
|
||||
@@ -134,18 +134,19 @@ impl Render for TitleBar {
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.when_some(self.application_menu.clone(), |this, menu| {
|
||||
let is_any_menu_deployed = menu.read(cx).is_any_deployed();
|
||||
this.child(menu).when(!is_any_menu_deployed, |this| {
|
||||
this.children(self.render_project_host(cx))
|
||||
.child(self.render_project_name(cx))
|
||||
.children(self.render_project_branch(cx))
|
||||
})
|
||||
})
|
||||
.when(self.application_menu.is_none(), |this| {
|
||||
this.children(self.render_project_host(cx))
|
||||
.child(self.render_project_name(cx))
|
||||
.children(self.render_project_branch(cx))
|
||||
.map(|title_bar| {
|
||||
let mut render_project_items = true;
|
||||
title_bar
|
||||
.when_some(self.application_menu.clone(), |title_bar, menu| {
|
||||
render_project_items = !menu.read(cx).is_any_deployed();
|
||||
title_bar.child(menu)
|
||||
})
|
||||
.when(render_project_items, |title_bar| {
|
||||
title_bar
|
||||
.children(self.render_project_host(cx))
|
||||
.child(self.render_project_name(cx))
|
||||
.children(self.render_project_branch(cx))
|
||||
})
|
||||
})
|
||||
.on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation()),
|
||||
)
|
||||
@@ -318,21 +319,24 @@ impl TitleBar {
|
||||
Some(
|
||||
ButtonLike::new("ssh-server-icon")
|
||||
.child(
|
||||
IconWithIndicator::new(
|
||||
Icon::new(IconName::Server)
|
||||
.size(IconSize::XSmall)
|
||||
.color(icon_color),
|
||||
Some(Indicator::dot().color(indicator_color)),
|
||||
)
|
||||
.indicator_border_color(Some(cx.theme().colors().title_bar_background))
|
||||
.into_any_element(),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.max_w_32()
|
||||
.overflow_hidden()
|
||||
.text_ellipsis()
|
||||
.child(Label::new(nickname.clone()).size(LabelSize::Small)),
|
||||
.child(
|
||||
IconWithIndicator::new(
|
||||
Icon::new(IconName::Server)
|
||||
.size(IconSize::XSmall)
|
||||
.color(icon_color),
|
||||
Some(Indicator::dot().color(indicator_color)),
|
||||
)
|
||||
.indicator_border_color(Some(cx.theme().colors().title_bar_background))
|
||||
.into_any_element(),
|
||||
)
|
||||
.child(
|
||||
Label::new(nickname.clone())
|
||||
.size(LabelSize::Small)
|
||||
.text_ellipsis(),
|
||||
),
|
||||
)
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::with_meta("Remote Project", Some(&OpenRemote), meta.clone(), cx)
|
||||
|
||||
@@ -282,6 +282,52 @@ impl RenderOnce for Switch {
|
||||
}
|
||||
}
|
||||
|
||||
/// A [`Switch`] that has a [`Label`].
|
||||
#[derive(IntoElement)]
|
||||
pub struct SwitchWithLabel {
|
||||
id: ElementId,
|
||||
label: Label,
|
||||
checked: ToggleState,
|
||||
on_click: Arc<dyn Fn(&ToggleState, &mut WindowContext) + 'static>,
|
||||
}
|
||||
|
||||
impl SwitchWithLabel {
|
||||
pub fn new(
|
||||
id: impl Into<ElementId>,
|
||||
label: Label,
|
||||
checked: ToggleState,
|
||||
on_click: impl Fn(&ToggleState, &mut WindowContext) + 'static,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
label,
|
||||
checked,
|
||||
on_click: Arc::new(on_click),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for SwitchWithLabel {
|
||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||
h_flex()
|
||||
.gap(DynamicSpacing::Base08.rems(cx))
|
||||
.child(Switch::new(self.id.clone(), self.checked).on_click({
|
||||
let on_click = self.on_click.clone();
|
||||
move |checked, cx| {
|
||||
(on_click)(checked, cx);
|
||||
}
|
||||
}))
|
||||
.child(
|
||||
div()
|
||||
.id(SharedString::from(format!("{}-label", self.id)))
|
||||
.on_click(move |_event, cx| {
|
||||
(self.on_click)(&self.checked.inverse(), cx);
|
||||
})
|
||||
.child(self.label),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl ComponentPreview for Checkbox {
|
||||
fn description() -> impl Into<Option<&'static str>> {
|
||||
"A checkbox lets people choose between a pair of opposing states, like enabled and disabled, using a different appearance to indicate each state."
|
||||
@@ -407,3 +453,32 @@ impl ComponentPreview for CheckboxWithLabel {
|
||||
])]
|
||||
}
|
||||
}
|
||||
|
||||
impl ComponentPreview for SwitchWithLabel {
|
||||
fn description() -> impl Into<Option<&'static str>> {
|
||||
"A switch with an associated label, allowing users to select an option while providing a descriptive text."
|
||||
}
|
||||
|
||||
fn examples(_: &mut WindowContext) -> Vec<ComponentExampleGroup<Self>> {
|
||||
vec![example_group(vec![
|
||||
single_example(
|
||||
"Off",
|
||||
SwitchWithLabel::new(
|
||||
"switch_with_label_unselected",
|
||||
Label::new("Always save on quit"),
|
||||
ToggleState::Unselected,
|
||||
|_, _| {},
|
||||
),
|
||||
),
|
||||
single_example(
|
||||
"On",
|
||||
SwitchWithLabel::new(
|
||||
"switch_with_label_selected",
|
||||
Label::new("Always save on quit"),
|
||||
ToggleState::Selected,
|
||||
|_, _| {},
|
||||
),
|
||||
),
|
||||
])]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
#![allow(missing_docs)]
|
||||
|
||||
use gpui::Axis;
|
||||
|
||||
use crate::prelude::*;
|
||||
use gpui::*;
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct ToolStrip {
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
//! It can't be located in the `ui` crate because it depends on `editor`.
|
||||
//!
|
||||
|
||||
use editor::*;
|
||||
use gpui::*;
|
||||
use editor::{Editor, EditorElement, EditorStyle};
|
||||
use gpui::{AppContext, FocusHandle, FocusableView, FontStyle, Hsla, TextStyle, View};
|
||||
use settings::Settings;
|
||||
use theme::ThemeSettings;
|
||||
use ui::*;
|
||||
use ui::prelude::*;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum FieldLabelLayout {
|
||||
|
||||
@@ -24,6 +24,7 @@ futures-lite.workspace = true
|
||||
futures.workspace = true
|
||||
git2 = { workspace = true, optional = true }
|
||||
globset.workspace = true
|
||||
itertools.workspace = true
|
||||
log.workspace = true
|
||||
rand = { workspace = true, optional = true }
|
||||
regex.workspace = true
|
||||
@@ -35,6 +36,9 @@ take-until = "0.2.0"
|
||||
tempfile = { workspace = true, optional = true }
|
||||
unicase.workspace = true
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
libc.workspace = true
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
tendril = "0.4.3"
|
||||
dunce = "1.0"
|
||||
|
||||
@@ -6,8 +6,10 @@ pub mod serde;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub mod test;
|
||||
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use futures::Future;
|
||||
|
||||
use itertools::Either;
|
||||
use regex::Regex;
|
||||
use std::sync::{LazyLock, OnceLock};
|
||||
use std::{
|
||||
@@ -109,6 +111,106 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
pub fn load_shell_from_passwd() -> Result<()> {
|
||||
let buflen = match unsafe { libc::sysconf(libc::_SC_GETPW_R_SIZE_MAX) } {
|
||||
n if n < 0 => 1024,
|
||||
n => n as usize,
|
||||
};
|
||||
let mut buffer = Vec::with_capacity(buflen);
|
||||
|
||||
let mut pwd: std::mem::MaybeUninit<libc::passwd> = std::mem::MaybeUninit::uninit();
|
||||
let mut result: *mut libc::passwd = std::ptr::null_mut();
|
||||
|
||||
let uid = unsafe { libc::getuid() };
|
||||
let status = unsafe {
|
||||
libc::getpwuid_r(
|
||||
uid,
|
||||
pwd.as_mut_ptr(),
|
||||
buffer.as_mut_ptr() as *mut libc::c_char,
|
||||
buflen,
|
||||
&mut result,
|
||||
)
|
||||
};
|
||||
let entry = unsafe { pwd.assume_init() };
|
||||
|
||||
anyhow::ensure!(
|
||||
status == 0,
|
||||
"call to getpwuid_r failed. uid: {}, status: {}",
|
||||
uid,
|
||||
status
|
||||
);
|
||||
anyhow::ensure!(!result.is_null(), "passwd entry for uid {} not found", uid);
|
||||
anyhow::ensure!(
|
||||
entry.pw_uid == uid,
|
||||
"passwd entry has different uid ({}) than getuid ({}) returned",
|
||||
entry.pw_uid,
|
||||
uid,
|
||||
);
|
||||
|
||||
let shell = unsafe { std::ffi::CStr::from_ptr(entry.pw_shell).to_str().unwrap() };
|
||||
if env::var("SHELL").map_or(true, |shell_env| shell_env != shell) {
|
||||
log::info!(
|
||||
"updating SHELL environment variable to value from passwd entry: {:?}",
|
||||
shell,
|
||||
);
|
||||
env::set_var("SHELL", shell);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn load_login_shell_environment() -> Result<()> {
|
||||
let marker = "ZED_LOGIN_SHELL_START";
|
||||
let shell = env::var("SHELL").context(
|
||||
"SHELL environment variable is not assigned so we can't source login environment variables",
|
||||
)?;
|
||||
|
||||
// If possible, we want to `cd` in the user's `$HOME` to trigger programs
|
||||
// such as direnv, asdf, mise, ... to adjust the PATH. These tools often hook
|
||||
// into shell's `cd` command (and hooks) to manipulate env.
|
||||
// We do this so that we get the env a user would have when spawning a shell
|
||||
// in home directory.
|
||||
let shell_cmd_prefix = std::env::var_os("HOME")
|
||||
.and_then(|home| home.into_string().ok())
|
||||
.map(|home| format!("cd '{home}';"));
|
||||
|
||||
// The `exit 0` is the result of hours of debugging, trying to find out
|
||||
// why running this command here, without `exit 0`, would mess
|
||||
// up signal process for our process so that `ctrl-c` doesn't work
|
||||
// anymore.
|
||||
// We still don't know why `$SHELL -l -i -c '/usr/bin/env -0'` would
|
||||
// do that, but it does, and `exit 0` helps.
|
||||
let shell_cmd = format!(
|
||||
"{}printf '%s' {marker}; /usr/bin/env; exit 0;",
|
||||
shell_cmd_prefix.as_deref().unwrap_or("")
|
||||
);
|
||||
|
||||
let output = std::process::Command::new(&shell)
|
||||
.args(["-l", "-i", "-c", &shell_cmd])
|
||||
.output()
|
||||
.context("failed to spawn login shell to source login environment variables")?;
|
||||
if !output.status.success() {
|
||||
Err(anyhow!("login shell exited with error"))?;
|
||||
}
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
|
||||
if let Some(env_output_start) = stdout.find(marker) {
|
||||
let env_output = &stdout[env_output_start + marker.len()..];
|
||||
|
||||
parse_env_output(env_output, |key, value| env::set_var(key, value));
|
||||
|
||||
log::info!(
|
||||
"set environment variables from shell:{}, path:{}",
|
||||
shell,
|
||||
env::var("PATH").unwrap_or_default(),
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Parse the result of calling `usr/bin/env` with no arguments
|
||||
pub fn parse_env_output(env: &str, mut f: impl FnMut(String, String)) {
|
||||
let mut current_key: Option<String> = None;
|
||||
@@ -199,6 +301,35 @@ pub fn measure<R>(label: &str, f: impl FnOnce() -> R) -> R {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn iterate_expanded_and_wrapped_usize_range(
|
||||
range: Range<usize>,
|
||||
additional_before: usize,
|
||||
additional_after: usize,
|
||||
wrap_length: usize,
|
||||
) -> impl Iterator<Item = usize> {
|
||||
let start_wraps = range.start < additional_before;
|
||||
let end_wraps = wrap_length < range.end + additional_after;
|
||||
if start_wraps && end_wraps {
|
||||
Either::Left(0..wrap_length)
|
||||
} else if start_wraps {
|
||||
let wrapped_start = (range.start + wrap_length).saturating_sub(additional_before);
|
||||
if wrapped_start <= range.end {
|
||||
Either::Left(0..wrap_length)
|
||||
} else {
|
||||
Either::Right((0..range.end + additional_after).chain(wrapped_start..wrap_length))
|
||||
}
|
||||
} else if end_wraps {
|
||||
let wrapped_end = range.end + additional_after - wrap_length;
|
||||
if range.start <= wrapped_end {
|
||||
Either::Left(0..wrap_length)
|
||||
} else {
|
||||
Either::Right((0..wrapped_end).chain(range.start - additional_before..wrap_length))
|
||||
}
|
||||
} else {
|
||||
Either::Left((range.start - additional_before)..(range.end + additional_after))
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ResultExt<E> {
|
||||
type Ok;
|
||||
|
||||
@@ -734,4 +865,48 @@ Line 2
|
||||
Line 3"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_iterate_expanded_and_wrapped_usize_range() {
|
||||
// Neither wrap
|
||||
assert_eq!(
|
||||
iterate_expanded_and_wrapped_usize_range(2..4, 1, 1, 8).collect::<Vec<usize>>(),
|
||||
(1..5).collect::<Vec<usize>>()
|
||||
);
|
||||
// Start wraps
|
||||
assert_eq!(
|
||||
iterate_expanded_and_wrapped_usize_range(2..4, 3, 1, 8).collect::<Vec<usize>>(),
|
||||
((0..5).chain(7..8)).collect::<Vec<usize>>()
|
||||
);
|
||||
// Start wraps all the way around
|
||||
assert_eq!(
|
||||
iterate_expanded_and_wrapped_usize_range(2..4, 5, 1, 8).collect::<Vec<usize>>(),
|
||||
(0..8).collect::<Vec<usize>>()
|
||||
);
|
||||
// Start wraps all the way around and past 0
|
||||
assert_eq!(
|
||||
iterate_expanded_and_wrapped_usize_range(2..4, 10, 1, 8).collect::<Vec<usize>>(),
|
||||
(0..8).collect::<Vec<usize>>()
|
||||
);
|
||||
// End wraps
|
||||
assert_eq!(
|
||||
iterate_expanded_and_wrapped_usize_range(3..5, 1, 4, 8).collect::<Vec<usize>>(),
|
||||
(0..1).chain(2..8).collect::<Vec<usize>>()
|
||||
);
|
||||
// End wraps all the way around
|
||||
assert_eq!(
|
||||
iterate_expanded_and_wrapped_usize_range(3..5, 1, 5, 8).collect::<Vec<usize>>(),
|
||||
(0..8).collect::<Vec<usize>>()
|
||||
);
|
||||
// End wraps all the way around and past the end
|
||||
assert_eq!(
|
||||
iterate_expanded_and_wrapped_usize_range(3..5, 1, 10, 8).collect::<Vec<usize>>(),
|
||||
(0..8).collect::<Vec<usize>>()
|
||||
);
|
||||
// Both start and end wrap
|
||||
assert_eq!(
|
||||
iterate_expanded_and_wrapped_usize_range(3..5, 4, 4, 8).collect::<Vec<usize>>(),
|
||||
(0..8).collect::<Vec<usize>>()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -585,8 +585,6 @@ impl Motion {
|
||||
| NextLineStart
|
||||
| PreviousLineStart
|
||||
| StartOfLineDownward
|
||||
| SentenceBackward
|
||||
| SentenceForward
|
||||
| StartOfParagraph
|
||||
| EndOfParagraph
|
||||
| WindowTop
|
||||
@@ -611,6 +609,8 @@ impl Motion {
|
||||
| Left
|
||||
| Backspace
|
||||
| Right
|
||||
| SentenceBackward
|
||||
| SentenceForward
|
||||
| Space
|
||||
| StartOfLine { .. }
|
||||
| EndOfLineDownward
|
||||
|
||||
@@ -28,23 +28,27 @@ impl Vim {
|
||||
original_columns.insert(selection.id, original_head.column());
|
||||
motion.expand_selection(map, selection, times, true, &text_layout_details);
|
||||
|
||||
let start_point = selection.start.to_point(map);
|
||||
let next_line = map
|
||||
.buffer_snapshot
|
||||
.clip_point(Point::new(start_point.row + 1, 0), Bias::Left)
|
||||
.to_display_point(map);
|
||||
match motion {
|
||||
// Motion::NextWordStart on an empty line should delete it.
|
||||
Motion::NextWordStart { .. } => {
|
||||
Motion::NextWordStart { .. }
|
||||
if selection.is_empty()
|
||||
&& map
|
||||
.buffer_snapshot
|
||||
.line_len(MultiBufferRow(selection.start.to_point(map).row))
|
||||
== 0
|
||||
{
|
||||
selection.end = map
|
||||
.buffer_snapshot
|
||||
.clip_point(
|
||||
Point::new(selection.start.to_point(map).row + 1, 0),
|
||||
Bias::Left,
|
||||
)
|
||||
.to_display_point(map)
|
||||
}
|
||||
.line_len(MultiBufferRow(start_point.row))
|
||||
== 0 =>
|
||||
{
|
||||
selection.end = next_line
|
||||
}
|
||||
// Sentence motions, when done from start of line, include the newline
|
||||
Motion::SentenceForward | Motion::SentenceBackward
|
||||
if selection.start.column() == 0 =>
|
||||
{
|
||||
selection.end = next_line
|
||||
}
|
||||
Motion::EndOfDocument {} => {
|
||||
// Deleting until the end of the document includes the last line, including
|
||||
@@ -604,4 +608,62 @@ mod test {
|
||||
cx.simulate("d t x", "ˇax").await.assert_matches();
|
||||
cx.simulate("d t x", "aˇx").await.assert_matches();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_delete_sentence(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
cx.simulate(
|
||||
"d )",
|
||||
indoc! {"
|
||||
Fiˇrst. Second. Third.
|
||||
Fourth.
|
||||
"},
|
||||
)
|
||||
.await
|
||||
.assert_matches();
|
||||
|
||||
cx.simulate(
|
||||
"d )",
|
||||
indoc! {"
|
||||
First. Secˇond. Third.
|
||||
Fourth.
|
||||
"},
|
||||
)
|
||||
.await
|
||||
.assert_matches();
|
||||
|
||||
// Two deletes
|
||||
cx.simulate(
|
||||
"d ) d )",
|
||||
indoc! {"
|
||||
First. Second. Thirˇd.
|
||||
Fourth.
|
||||
"},
|
||||
)
|
||||
.await
|
||||
.assert_matches();
|
||||
|
||||
// Should delete whole line if done on first column
|
||||
cx.simulate(
|
||||
"d )",
|
||||
indoc! {"
|
||||
ˇFirst.
|
||||
Fourth.
|
||||
"},
|
||||
)
|
||||
.await
|
||||
.assert_matches();
|
||||
|
||||
// Backwards it should also delete the whole first line
|
||||
cx.simulate(
|
||||
"d (",
|
||||
indoc! {"
|
||||
First.
|
||||
ˇSecond.
|
||||
Fourth.
|
||||
"},
|
||||
)
|
||||
.await
|
||||
.assert_matches();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ use serde::Deserialize;
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
|
||||
pub enum Object {
|
||||
Word { ignore_punctuation: bool },
|
||||
Subword { ignore_punctuation: bool },
|
||||
Sentence,
|
||||
Paragraph,
|
||||
Quotes,
|
||||
@@ -47,12 +48,18 @@ 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, IndentObj]);
|
||||
impl_actions!(vim, [Word, Subword, IndentObj]);
|
||||
|
||||
actions!(
|
||||
vim,
|
||||
@@ -83,6 +90,13 @@ 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)
|
||||
@@ -154,6 +168,7 @@ impl Object {
|
||||
pub fn is_multiline(self) -> bool {
|
||||
match self {
|
||||
Object::Word { .. }
|
||||
| Object::Subword { .. }
|
||||
| Object::Quotes
|
||||
| Object::BackQuotes
|
||||
| Object::VerticalBars
|
||||
@@ -176,6 +191,7 @@ impl Object {
|
||||
pub fn always_expands_both_ways(self) -> bool {
|
||||
match self {
|
||||
Object::Word { .. }
|
||||
| Object::Subword { .. }
|
||||
| Object::Sentence
|
||||
| Object::Paragraph
|
||||
| Object::Argument
|
||||
@@ -198,6 +214,7 @@ 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
|
||||
@@ -243,6 +260,13 @@ 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 => {
|
||||
@@ -350,6 +374,63 @@ 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,
|
||||
@@ -461,6 +542,40 @@ 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,
|
||||
|
||||
22
crates/vim/test_data/test_delete_sentence.json
Normal file
22
crates/vim/test_data/test_delete_sentence.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{"Put":{"state":"Fiˇrst. Second. Third.\nFourth.\n"}}
|
||||
{"Key":"d"}
|
||||
{"Key":")"}
|
||||
{"Get":{"state":"FiˇSecond. Third.\nFourth.\n","mode":"Normal"}}
|
||||
{"Put":{"state":"First. Secˇond. Third.\nFourth.\n"}}
|
||||
{"Key":"d"}
|
||||
{"Key":")"}
|
||||
{"Get":{"state":"First. SecˇThird.\nFourth.\n","mode":"Normal"}}
|
||||
{"Put":{"state":"First. Second. Thirˇd.\nFourth.\n"}}
|
||||
{"Key":"d"}
|
||||
{"Key":")"}
|
||||
{"Key":"d"}
|
||||
{"Key":")"}
|
||||
{"Get":{"state":"First. Second. Thˇi\n","mode":"Normal"}}
|
||||
{"Put":{"state":"ˇFirst.\nFourth.\n"}}
|
||||
{"Key":"d"}
|
||||
{"Key":")"}
|
||||
{"Get":{"state":"ˇFourth.\n","mode":"Normal"}}
|
||||
{"Put":{"state":"First.\nˇSecond.\nFourth.\n"}}
|
||||
{"Key":"d"}
|
||||
{"Key":"("}
|
||||
{"Get":{"state":"ˇSecond.\nFourth.\n","mode":"Normal"}}
|
||||
@@ -6,7 +6,7 @@ use ui::{
|
||||
element_cell, prelude::*, string_cell, utils::calculate_contrast_ratio, AudioStatus,
|
||||
Availability, Avatar, AvatarAudioStatusIndicator, AvatarAvailabilityIndicator, ButtonLike,
|
||||
Checkbox, CheckboxWithLabel, ContentGroup, DecoratedIcon, ElevationIndex, Facepile,
|
||||
IconDecoration, Indicator, Switch, Table, TintColor, Tooltip,
|
||||
IconDecoration, Indicator, Switch, SwitchWithLabel, Table, TintColor, Tooltip,
|
||||
};
|
||||
|
||||
use crate::{Item, Workspace};
|
||||
@@ -369,16 +369,17 @@ impl ThemePreview {
|
||||
.overflow_scroll()
|
||||
.size_full()
|
||||
.gap_2()
|
||||
.child(Switch::render_component_previews(cx))
|
||||
.child(ContentGroup::render_component_previews(cx))
|
||||
.child(IconDecoration::render_component_previews(cx))
|
||||
.child(DecoratedIcon::render_component_previews(cx))
|
||||
.child(Button::render_component_previews(cx))
|
||||
.child(Checkbox::render_component_previews(cx))
|
||||
.child(CheckboxWithLabel::render_component_previews(cx))
|
||||
.child(ContentGroup::render_component_previews(cx))
|
||||
.child(DecoratedIcon::render_component_previews(cx))
|
||||
.child(Facepile::render_component_previews(cx))
|
||||
.child(Button::render_component_previews(cx))
|
||||
.child(Indicator::render_component_previews(cx))
|
||||
.child(Icon::render_component_previews(cx))
|
||||
.child(IconDecoration::render_component_previews(cx))
|
||||
.child(Indicator::render_component_previews(cx))
|
||||
.child(Switch::render_component_previews(cx))
|
||||
.child(SwitchWithLabel::render_component_previews(cx))
|
||||
.child(Table::render_component_previews(cx))
|
||||
}
|
||||
|
||||
|
||||
@@ -995,7 +995,7 @@ impl Workspace {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
if let Some(display) = cx.display() {
|
||||
if let Ok(display_uuid) = display.uuid() {
|
||||
let window_bounds = cx.window_bounds();
|
||||
let window_bounds = cx.inner_window_bounds();
|
||||
if let Some(database_id) = workspace_id {
|
||||
cx.background_executor()
|
||||
.spawn(DB.set_window_open_status(
|
||||
|
||||
@@ -33,6 +33,10 @@ fn main() {
|
||||
|
||||
// Populate git sha environment variable if git is available
|
||||
println!("cargo:rerun-if-changed=../../.git/logs/HEAD");
|
||||
println!(
|
||||
"cargo:rustc-env=TARGET={}",
|
||||
std::env::var("TARGET").unwrap()
|
||||
);
|
||||
if let Ok(output) = Command::new("git").args(["rev-parse", "HEAD"]).output() {
|
||||
if output.status.success() {
|
||||
let git_sha = String::from_utf8_lossy(&output.stdout);
|
||||
|
||||
@@ -10,6 +10,7 @@ use clap::{command, Parser};
|
||||
use cli::FORCE_CLI_MODE_ENV_VAR_NAME;
|
||||
use client::{parse_zed_link, Client, ProxySettings, UserStore};
|
||||
use collab_ui::channel_view::ChannelView;
|
||||
use collections::HashMap;
|
||||
use db::kvp::{GLOBAL_KEY_VALUE_STORE, KEY_VALUE_STORE};
|
||||
use editor::Editor;
|
||||
use env_logger::Builder;
|
||||
@@ -37,18 +38,17 @@ use settings::{
|
||||
handle_settings_file_changes, watch_config_file, InvalidSettingsError, Settings, SettingsStore,
|
||||
};
|
||||
use simplelog::ConfigBuilder;
|
||||
use smol::process::Command;
|
||||
use std::{
|
||||
env,
|
||||
fs::OpenOptions,
|
||||
io::{IsTerminal, Write},
|
||||
io::{self, IsTerminal, Write},
|
||||
path::{Path, PathBuf},
|
||||
process,
|
||||
sync::Arc,
|
||||
};
|
||||
use theme::{ActiveTheme, SystemAppearance, ThemeRegistry, ThemeSettings};
|
||||
use time::UtcOffset;
|
||||
use util::{maybe, parse_env_output, ResultExt, TryFutureExt};
|
||||
use util::{load_login_shell_environment, maybe, ResultExt, TryFutureExt};
|
||||
use uuid::Uuid;
|
||||
use welcome::{show_welcome_view, BaseKeymap, FIRST_OPEN};
|
||||
use workspace::{
|
||||
@@ -63,26 +63,66 @@ use zed::{
|
||||
|
||||
use crate::zed::inline_completion_registry;
|
||||
|
||||
#[cfg(unix)]
|
||||
use util::load_shell_from_passwd;
|
||||
|
||||
#[cfg(feature = "mimalloc")]
|
||||
#[global_allocator]
|
||||
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
|
||||
|
||||
fn fail_to_launch(e: anyhow::Error) {
|
||||
eprintln!("Zed failed to launch: {e:?}");
|
||||
App::new().run(move |cx| {
|
||||
if let Ok(window) = cx.open_window(gpui::WindowOptions::default(), |cx| cx.new_view(|_| gpui::Empty)) {
|
||||
window.update(cx, |_, cx| {
|
||||
let response = cx.prompt(gpui::PromptLevel::Critical, "Zed failed to launch", Some(&format!("{e}\n\nFor help resolving this, please open an issue on https://github.com/zed-industries/zed")), &["Exit"]);
|
||||
fn files_not_createad_on_launch(errors: HashMap<io::ErrorKind, Vec<&Path>>) {
|
||||
let message = "Zed failed to launch";
|
||||
let error_details = errors
|
||||
.into_iter()
|
||||
.flat_map(|(kind, paths)| {
|
||||
#[allow(unused_mut)] // for non-unix platforms
|
||||
let mut error_kind_details = match paths.len() {
|
||||
0 => return None,
|
||||
1 => format!(
|
||||
"{kind} when creating directory {:?}",
|
||||
paths.first().expect("match arm checks for a single entry")
|
||||
),
|
||||
_many => format!("{kind} when creating directories {paths:?}"),
|
||||
};
|
||||
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
response.await?;
|
||||
cx.update(|cx| {
|
||||
cx.quit()
|
||||
#[cfg(unix)]
|
||||
{
|
||||
match kind {
|
||||
io::ErrorKind::PermissionDenied => {
|
||||
error_kind_details.push_str("\n\nConsider using chown and chmod tools for altering the directories permissions if your user has corresponding rights.\
|
||||
\nFor example, `sudo chown $(whoami):staff ~/.config` and `chmod +uwrx ~/.config`");
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
Some(error_kind_details)
|
||||
})
|
||||
.collect::<Vec<_>>().join("\n\n");
|
||||
|
||||
eprintln!("{message}: {error_details}");
|
||||
App::new().run(move |cx| {
|
||||
if let Ok(window) = cx.open_window(gpui::WindowOptions::default(), |cx| {
|
||||
cx.new_view(|_| gpui::Empty)
|
||||
}) {
|
||||
window
|
||||
.update(cx, |_, cx| {
|
||||
let response = cx.prompt(
|
||||
gpui::PromptLevel::Critical,
|
||||
message,
|
||||
Some(&error_details),
|
||||
&["Exit"],
|
||||
);
|
||||
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
response.await?;
|
||||
cx.update(|cx| cx.quit())
|
||||
})
|
||||
}).detach_and_log_err(cx);
|
||||
}).log_err();
|
||||
.detach_and_log_err(cx);
|
||||
})
|
||||
.log_err();
|
||||
} else {
|
||||
fail_to_open_window(e, cx)
|
||||
fail_to_open_window(anyhow::anyhow!("{message}: {error_details}"), cx)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -137,8 +177,9 @@ fn main() {
|
||||
menu::init();
|
||||
zed_actions::init();
|
||||
|
||||
if let Err(e) = init_paths() {
|
||||
fail_to_launch(e);
|
||||
let file_errors = init_paths();
|
||||
if !file_errors.is_empty() {
|
||||
files_not_createad_on_launch(file_errors);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -219,9 +260,9 @@ fn main() {
|
||||
.spawn(async {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
load_shell_from_passwd().await.log_err();
|
||||
load_shell_from_passwd().log_err();
|
||||
}
|
||||
load_login_shell_environment().await.log_err();
|
||||
load_login_shell_environment().log_err();
|
||||
})
|
||||
.detach()
|
||||
};
|
||||
@@ -905,8 +946,8 @@ pub(crate) async fn restorable_workspace_locations(
|
||||
}
|
||||
}
|
||||
|
||||
fn init_paths() -> anyhow::Result<()> {
|
||||
for path in [
|
||||
fn init_paths() -> HashMap<io::ErrorKind, Vec<&'static Path>> {
|
||||
[
|
||||
paths::config_dir(),
|
||||
paths::extensions_dir(),
|
||||
paths::languages_dir(),
|
||||
@@ -914,12 +955,13 @@ fn init_paths() -> anyhow::Result<()> {
|
||||
paths::logs_dir(),
|
||||
paths::temp_dir(),
|
||||
]
|
||||
.iter()
|
||||
{
|
||||
std::fs::create_dir_all(path)
|
||||
.map_err(|e| anyhow!("Could not create directory {:?}: {}", path, e))?;
|
||||
}
|
||||
Ok(())
|
||||
.into_iter()
|
||||
.fold(HashMap::default(), |mut errors, path| {
|
||||
if let Err(e) = std::fs::create_dir_all(path) {
|
||||
errors.entry(e.kind()).or_insert_with(Vec::new).push(path);
|
||||
}
|
||||
errors
|
||||
})
|
||||
}
|
||||
|
||||
fn init_logger() {
|
||||
@@ -998,109 +1040,8 @@ fn init_stdout_logger() {
|
||||
.init();
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
async fn load_shell_from_passwd() -> Result<()> {
|
||||
let buflen = match unsafe { libc::sysconf(libc::_SC_GETPW_R_SIZE_MAX) } {
|
||||
n if n < 0 => 1024,
|
||||
n => n as usize,
|
||||
};
|
||||
let mut buffer = Vec::with_capacity(buflen);
|
||||
|
||||
let mut pwd: std::mem::MaybeUninit<libc::passwd> = std::mem::MaybeUninit::uninit();
|
||||
let mut result: *mut libc::passwd = std::ptr::null_mut();
|
||||
|
||||
let uid = unsafe { libc::getuid() };
|
||||
let status = unsafe {
|
||||
libc::getpwuid_r(
|
||||
uid,
|
||||
pwd.as_mut_ptr(),
|
||||
buffer.as_mut_ptr() as *mut libc::c_char,
|
||||
buflen,
|
||||
&mut result,
|
||||
)
|
||||
};
|
||||
let entry = unsafe { pwd.assume_init() };
|
||||
|
||||
anyhow::ensure!(
|
||||
status == 0,
|
||||
"call to getpwuid_r failed. uid: {}, status: {}",
|
||||
uid,
|
||||
status
|
||||
);
|
||||
anyhow::ensure!(!result.is_null(), "passwd entry for uid {} not found", uid);
|
||||
anyhow::ensure!(
|
||||
entry.pw_uid == uid,
|
||||
"passwd entry has different uid ({}) than getuid ({}) returned",
|
||||
entry.pw_uid,
|
||||
uid,
|
||||
);
|
||||
|
||||
let shell = unsafe { std::ffi::CStr::from_ptr(entry.pw_shell).to_str().unwrap() };
|
||||
if env::var("SHELL").map_or(true, |shell_env| shell_env != shell) {
|
||||
log::info!(
|
||||
"updating SHELL environment variable to value from passwd entry: {:?}",
|
||||
shell,
|
||||
);
|
||||
env::set_var("SHELL", shell);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn load_login_shell_environment() -> Result<()> {
|
||||
let marker = "ZED_LOGIN_SHELL_START";
|
||||
let shell = env::var("SHELL").context(
|
||||
"SHELL environment variable is not assigned so we can't source login environment variables",
|
||||
)?;
|
||||
|
||||
// If possible, we want to `cd` in the user's `$HOME` to trigger programs
|
||||
// such as direnv, asdf, mise, ... to adjust the PATH. These tools often hook
|
||||
// into shell's `cd` command (and hooks) to manipulate env.
|
||||
// We do this so that we get the env a user would have when spawning a shell
|
||||
// in home directory.
|
||||
let shell_cmd_prefix = std::env::var_os("HOME")
|
||||
.and_then(|home| home.into_string().ok())
|
||||
.map(|home| format!("cd '{home}';"));
|
||||
|
||||
// The `exit 0` is the result of hours of debugging, trying to find out
|
||||
// why running this command here, without `exit 0`, would mess
|
||||
// up signal process for our process so that `ctrl-c` doesn't work
|
||||
// anymore.
|
||||
// We still don't know why `$SHELL -l -i -c '/usr/bin/env -0'` would
|
||||
// do that, but it does, and `exit 0` helps.
|
||||
let shell_cmd = format!(
|
||||
"{}printf '%s' {marker}; /usr/bin/env; exit 0;",
|
||||
shell_cmd_prefix.as_deref().unwrap_or("")
|
||||
);
|
||||
|
||||
let output = Command::new(&shell)
|
||||
.args(["-l", "-i", "-c", &shell_cmd])
|
||||
.output()
|
||||
.await
|
||||
.context("failed to spawn login shell to source login environment variables")?;
|
||||
if !output.status.success() {
|
||||
Err(anyhow!("login shell exited with error"))?;
|
||||
}
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
|
||||
if let Some(env_output_start) = stdout.find(marker) {
|
||||
let env_output = &stdout[env_output_start + marker.len()..];
|
||||
|
||||
parse_env_output(env_output, |key, value| env::set_var(key, value));
|
||||
|
||||
log::info!(
|
||||
"set environment variables from shell:{}, path:{}",
|
||||
shell,
|
||||
env::var("PATH").unwrap_or_default(),
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn stdout_is_a_pty() -> bool {
|
||||
std::env::var(FORCE_CLI_MODE_ENV_VAR_NAME).ok().is_none() && std::io::stdout().is_terminal()
|
||||
std::env::var(FORCE_CLI_MODE_ENV_VAR_NAME).ok().is_none() && io::stdout().is_terminal()
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
|
||||
@@ -1,31 +1,26 @@
|
||||
use crate::stdout_is_a_pty;
|
||||
use anyhow::{Context, Result};
|
||||
use backtrace::{self, Backtrace};
|
||||
use chrono::Utc;
|
||||
use client::{telemetry, TelemetrySettings};
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
use gpui::{AppContext, SemanticVersion};
|
||||
use http_client::{HttpRequestExt, Method};
|
||||
|
||||
use http_client::{self, HttpClient, HttpClientWithUrl};
|
||||
use http_client::{self, HttpClient, HttpClientWithUrl, HttpRequestExt, Method};
|
||||
use paths::{crashes_dir, crashes_retired_dir};
|
||||
use project::Project;
|
||||
use release_channel::ReleaseChannel;
|
||||
use release_channel::RELEASE_CHANNEL;
|
||||
use release_channel::{ReleaseChannel, RELEASE_CHANNEL};
|
||||
use settings::Settings;
|
||||
use smol::stream::StreamExt;
|
||||
use std::{
|
||||
env,
|
||||
ffi::OsStr,
|
||||
ffi::{c_void, OsStr},
|
||||
sync::{atomic::Ordering, Arc},
|
||||
};
|
||||
use std::{io::Write, panic, sync::atomic::AtomicU32, thread};
|
||||
use telemetry_events::LocationData;
|
||||
use telemetry_events::Panic;
|
||||
use telemetry_events::PanicRequest;
|
||||
use telemetry_events::{LocationData, Panic, PanicRequest};
|
||||
use url::Url;
|
||||
use util::ResultExt;
|
||||
|
||||
use crate::stdout_is_a_pty;
|
||||
static PANIC_COUNT: AtomicU32 = AtomicU32::new(0);
|
||||
|
||||
pub fn init_panic_hook(
|
||||
@@ -69,25 +64,35 @@ pub fn init_panic_hook(
|
||||
);
|
||||
std::process::exit(-1);
|
||||
}
|
||||
let main_module_base_address = get_main_module_base_address();
|
||||
|
||||
let backtrace = Backtrace::new();
|
||||
let mut backtrace = backtrace
|
||||
let mut symbols = backtrace
|
||||
.frames()
|
||||
.iter()
|
||||
.flat_map(|frame| {
|
||||
frame
|
||||
.symbols()
|
||||
.iter()
|
||||
.filter_map(|frame| Some(format!("{:#}", frame.name()?)))
|
||||
let base = frame
|
||||
.module_base_address()
|
||||
.unwrap_or(main_module_base_address);
|
||||
frame.symbols().iter().map(move |symbol| {
|
||||
format!(
|
||||
"{}+{}",
|
||||
symbol
|
||||
.name()
|
||||
.as_ref()
|
||||
.map_or("<unknown>".to_owned(), <_>::to_string),
|
||||
(frame.ip() as isize).saturating_sub(base as isize)
|
||||
)
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Strip out leading stack frames for rust panic-handling.
|
||||
if let Some(ix) = backtrace
|
||||
if let Some(ix) = symbols
|
||||
.iter()
|
||||
.position(|name| name == "rust_begin_unwind" || name == "_rust_begin_unwind")
|
||||
{
|
||||
backtrace.drain(0..=ix);
|
||||
symbols.drain(0..=ix);
|
||||
}
|
||||
|
||||
let panic_data = telemetry_events::Panic {
|
||||
@@ -98,12 +103,13 @@ pub fn init_panic_hook(
|
||||
line: location.line(),
|
||||
}),
|
||||
app_version: app_version.to_string(),
|
||||
release_channel: RELEASE_CHANNEL.display_name().into(),
|
||||
release_channel: RELEASE_CHANNEL.dev_name().into(),
|
||||
target: env!("TARGET").to_owned().into(),
|
||||
os_name: telemetry::os_name(),
|
||||
os_version: Some(telemetry::os_version()),
|
||||
architecture: env::consts::ARCH.into(),
|
||||
panicked_on: Utc::now().timestamp_millis(),
|
||||
backtrace,
|
||||
backtrace: symbols,
|
||||
system_id: system_id.clone(),
|
||||
installation_id: installation_id.clone(),
|
||||
session_id: session_id.clone(),
|
||||
@@ -133,6 +139,25 @@ pub fn init_panic_hook(
|
||||
}));
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
fn get_main_module_base_address() -> *mut c_void {
|
||||
let mut dl_info = libc::Dl_info {
|
||||
dli_fname: std::ptr::null(),
|
||||
dli_fbase: std::ptr::null_mut(),
|
||||
dli_sname: std::ptr::null(),
|
||||
dli_saddr: std::ptr::null_mut(),
|
||||
};
|
||||
unsafe {
|
||||
libc::dladdr(get_main_module_base_address as _, &mut dl_info);
|
||||
}
|
||||
dl_info.dli_fbase
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn get_main_module_base_address() -> *mut c_void {
|
||||
std::ptr::null_mut()
|
||||
}
|
||||
|
||||
pub fn init(
|
||||
http_client: Arc<HttpClientWithUrl>,
|
||||
system_id: Option<String>,
|
||||
|
||||
@@ -300,7 +300,6 @@ fn show_software_emulation_warning_if_needed(
|
||||
}
|
||||
|
||||
fn initialize_panels(prompt_builder: Arc<PromptBuilder>, cx: &mut ViewContext<Workspace>) {
|
||||
let release_channel = ReleaseChannel::global(cx);
|
||||
let assistant2_feature_flag = cx.wait_for_flag::<feature_flags::Assistant2FeatureFlag>();
|
||||
let git_ui_feature_flag = cx.wait_for_flag::<feature_flags::GitUiFeatureFlag>();
|
||||
|
||||
@@ -361,7 +360,7 @@ fn initialize_panels(prompt_builder: Arc<PromptBuilder>, cx: &mut ViewContext<Wo
|
||||
}
|
||||
})?;
|
||||
|
||||
let is_assistant2_enabled = if cfg!(test) || release_channel != ReleaseChannel::Dev {
|
||||
let is_assistant2_enabled = if cfg!(test) {
|
||||
false
|
||||
} else {
|
||||
assistant2_feature_flag.await
|
||||
|
||||
@@ -386,7 +386,7 @@ fn session_state(session: View<Session>, cx: &WindowContext) -> ReplMenuState {
|
||||
indicator: None,
|
||||
kernel_name: kernel_name.clone(),
|
||||
kernel_language: kernel_language.clone(),
|
||||
// todo!(): Technically not shutdown, but indeterminate
|
||||
// TODO: Technically not shutdown, but indeterminate
|
||||
status: KernelStatus::Shutdown,
|
||||
// current_delta: Duration::default(),
|
||||
}
|
||||
|
||||
@@ -269,6 +269,7 @@ impl RateCompletionModal {
|
||||
let mut editor = Editor::multi_line(cx);
|
||||
editor.set_soft_wrap_mode(language_settings::SoftWrap::EditorWidth, cx);
|
||||
editor.set_show_line_numbers(false, cx);
|
||||
editor.set_show_scrollbars(false, cx);
|
||||
editor.set_show_git_diff_gutter(false, cx);
|
||||
editor.set_show_code_actions(false, cx);
|
||||
editor.set_show_runnables(false, cx);
|
||||
|
||||
@@ -298,14 +298,21 @@ impl Zeta {
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let request_sent_at = Instant::now();
|
||||
|
||||
let mut input_events = String::new();
|
||||
for event in events {
|
||||
if !input_events.is_empty() {
|
||||
input_events.push('\n');
|
||||
input_events.push('\n');
|
||||
}
|
||||
input_events.push_str(&event.to_prompt());
|
||||
}
|
||||
let input_events = cx
|
||||
.background_executor()
|
||||
.spawn(async move {
|
||||
let mut input_events = String::new();
|
||||
for event in events {
|
||||
if !input_events.is_empty() {
|
||||
input_events.push('\n');
|
||||
input_events.push('\n');
|
||||
}
|
||||
input_events.push_str(&event.to_prompt());
|
||||
}
|
||||
input_events
|
||||
})
|
||||
.await;
|
||||
|
||||
let input_excerpt = prompt_for_excerpt(&snapshot, &excerpt_range, offset);
|
||||
let input_outline = prompt_for_outline(&snapshot);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Luau
|
||||
|
||||
[Luau](https://luau-lang.org/) is a fast, small, safe, gradually typed embeddable scripting language derived from Lua. Luau was developed by Roblox and available under the MIT license.
|
||||
[Luau](https://luau.org/) is a fast, small, safe, gradually typed, embeddable scripting language derived from Lua. Luau was developed by Roblox and is available under the MIT license.
|
||||
|
||||
Luau language support in Zed is provided by the community-maintained [Luau extension](https://github.com/4teapo/zed-luau).
|
||||
Report issues to: [https://github.com/4teapo/zed-luau/issues](https://github.com/4teapo/zed-luau/issues)
|
||||
@@ -33,7 +33,7 @@ Then add the following to your Zed `settings.json`:
|
||||
"formatter": {
|
||||
"external": {
|
||||
"command": "stylua",
|
||||
"arguments": ["--stdin-filepath", "{buffer_path}", "-"]
|
||||
"arguments": ["-"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -392,6 +392,20 @@ 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
|
||||
|
||||
2
docs/theme/css/chrome.css
vendored
2
docs/theme/css/chrome.css
vendored
@@ -327,7 +327,7 @@ pre > code {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
/* FIXME: ACE editors overlap their buttons because ACE does absolute
|
||||
/* TODO: ACE editors overlap their buttons because ACE does absolute
|
||||
positioning within the code block which breaks padding. The only solution I
|
||||
can think of is to move the padding to the outer pre tag (or insert a div
|
||||
wrapper), but that would require fixing a whole bunch of CSS rules.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user