Compare commits
57 Commits
conflict-v
...
python-dec
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a037c39868 | ||
|
|
b1a6c35e3f | ||
|
|
a61d89feba | ||
|
|
a829281841 | ||
|
|
592568ff87 | ||
|
|
83afe56a61 | ||
|
|
e468f9d2da | ||
|
|
1ce2652a89 | ||
|
|
784d51c40f | ||
|
|
0079c99c2c | ||
|
|
230eb12f72 | ||
|
|
dd3956eaf1 | ||
|
|
122d6c9e4d | ||
|
|
19e89a8b2d | ||
|
|
919ffe7655 | ||
|
|
841a4e35ea | ||
|
|
175ce05fd1 | ||
|
|
e518941445 | ||
|
|
10b8174c1b | ||
|
|
21fd1c8b80 | ||
|
|
c80bd698f8 | ||
|
|
03419da6f1 | ||
|
|
f56960ab5b | ||
|
|
4d827924f0 | ||
|
|
25b4591539 | ||
|
|
afbf527aa2 | ||
|
|
eb9ea20313 | ||
|
|
3d2ab4e58c | ||
|
|
ff0060aa36 | ||
|
|
d791c6cdb1 | ||
|
|
c7725e31d9 | ||
|
|
e26620d1cf | ||
|
|
9dabf491f0 | ||
|
|
f2dcc98216 | ||
|
|
23bbfc4b94 | ||
|
|
98aefcca83 | ||
|
|
9be1e9aab1 | ||
|
|
33b60bc16d | ||
|
|
0355b9dfab | ||
|
|
6bec76cd5d | ||
|
|
d4f47aa653 | ||
|
|
5112fcebeb | ||
|
|
dcf7f714f7 | ||
|
|
16f668b8e3 | ||
|
|
0f4e52bde8 | ||
|
|
dfe37b0a07 | ||
|
|
2da37988b5 | ||
|
|
05955e4faa | ||
|
|
1d043b37fb | ||
|
|
18d39e3f81 | ||
|
|
cc3a28a8e8 | ||
|
|
0f17e82154 | ||
|
|
a316428686 | ||
|
|
355266988d | ||
|
|
72007c9a62 | ||
|
|
c2feffac9d | ||
|
|
4b7b5db58c |
@@ -13,12 +13,6 @@ rustflags = ["-C", "link-arg=-fuse-ld=mold"]
|
||||
linker = "clang"
|
||||
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
|
||||
|
||||
[target.aarch64-apple-darwin]
|
||||
rustflags = ["-C", "link-args=-all_load"]
|
||||
|
||||
[target.x86_64-apple-darwin]
|
||||
rustflags = ["-C", "link-args=-all_load"]
|
||||
|
||||
[target.'cfg(target_os = "windows")']
|
||||
rustflags = [
|
||||
"--cfg",
|
||||
|
||||
@@ -30,3 +30,7 @@ ffdda588b41f7d9d270ffe76cab116f828ad545e
|
||||
# 2024-07-05 Improved formatting of default keymaps (single line per bind)
|
||||
# https://github.com/zed-industries/zed/pull/13887
|
||||
813cc3f5e537372fc86720b5e71b6e1c815440ab
|
||||
|
||||
# 2024-07-24 docs: Format docs
|
||||
# https://github.com/zed-industries/zed/pull/15352
|
||||
3a44a59f8ec114ac1ba22f7da1652717ef7e4e5c
|
||||
|
||||
20
.mailmap
20
.mailmap
@@ -19,6 +19,8 @@ amtoaer <amtoaer@gmail.com>
|
||||
amtoaer <amtoaer@gmail.com> <amtoaer@outlook.com>
|
||||
Andrei Zvonimir Crnković <andrei@0x7f.dev>
|
||||
Andrei Zvonimir Crnković <andrei@0x7f.dev> <andreicek@0x7f.dev>
|
||||
Angelk90 <angelo.k90@hotmail.it>
|
||||
Angelk90 <angelo.k90@hotmail.it> <20476002+Angelk90@users.noreply.github.com>
|
||||
Antonio Scandurra <me@as-cii.com>
|
||||
Antonio Scandurra <me@as-cii.com> <antonio@zed.dev>
|
||||
Ben Kunkle <ben@zed.dev>
|
||||
@@ -38,6 +40,8 @@ Dairon Medina <dairon.medina@gmail.com>
|
||||
Danilo Leal <danilo@zed.dev>
|
||||
Danilo Leal <danilo@zed.dev> <67129314+danilo-leal@users.noreply.github.com>
|
||||
Edwin Aronsson <75266237+4teapo@users.noreply.github.com>
|
||||
Elvis Pranskevichus <elvis@geldata.com>
|
||||
Elvis Pranskevichus <elvis@geldata.com> <elvis@magic.io>
|
||||
Evren Sen <nervenes@icloud.com>
|
||||
Evren Sen <nervenes@icloud.com> <146845123+evrensen467@users.noreply.github.com>
|
||||
Evren Sen <nervenes@icloud.com> <146845123+evrsen@users.noreply.github.com>
|
||||
@@ -69,6 +73,8 @@ Lilith Iris <itslirissama@gmail.com> <83819417+Irilith@users.noreply.github.com>
|
||||
LoganDark <contact@logandark.mozmail.com>
|
||||
LoganDark <contact@logandark.mozmail.com> <git@logandark.mozmail.com>
|
||||
LoganDark <contact@logandark.mozmail.com> <github@logandark.mozmail.com>
|
||||
Marko Kungla <marko.kungla@gmail.com>
|
||||
Marko Kungla <marko.kungla@gmail.com> <marko@mkungla.dev>
|
||||
Marshall Bowers <git@maxdeviant.com>
|
||||
Marshall Bowers <git@maxdeviant.com> <elliott.codes@gmail.com>
|
||||
Marshall Bowers <git@maxdeviant.com> <marshall@zed.dev>
|
||||
@@ -84,6 +90,7 @@ Michael Sloan <michael@zed.dev> <mgsloan@google.com>
|
||||
Mikayla Maki <mikayla@zed.dev>
|
||||
Mikayla Maki <mikayla@zed.dev> <mikayla.c.maki@gmail.com>
|
||||
Mikayla Maki <mikayla@zed.dev> <mikayla.c.maki@icloud.com>
|
||||
Morgan Krey <morgan@zed.dev>
|
||||
Muhammad Talal Anwar <mail@talal.io>
|
||||
Muhammad Talal Anwar <mail@talal.io> <talalanwar@outlook.com>
|
||||
Nate Butler <iamnbutler@gmail.com>
|
||||
@@ -116,11 +123,18 @@ Shish <webmaster@shishnet.org>
|
||||
Shish <webmaster@shishnet.org> <shish@shishnet.org>
|
||||
Smit Barmase <0xtimsb@gmail.com>
|
||||
Smit Barmase <0xtimsb@gmail.com> <smit@zed.dev>
|
||||
Thomas <github.thomaub@gmail.com>
|
||||
Thomas <github.thomaub@gmail.com> <thomas.aubry94@gmail.com>
|
||||
Thomas <github.thomaub@gmail.com> <thomas.aubry@paylead.fr>
|
||||
Thomas Heartman <thomasheartman+github@gmail.com>
|
||||
Thomas Heartman <thomasheartman+github@gmail.com> <thomas@getunleash.io>
|
||||
Thomas Mickley-Doyle <tmickleydoyle@gmail.com>
|
||||
Thomas Mickley-Doyle <tmickleydoyle@gmail.com> <thomas@zed.dev>
|
||||
Thorben Kröger <dev@thorben.net>
|
||||
Thorben Kröger <dev@thorben.net> <thorben.kroeger@hexagon.com>
|
||||
Thorsten Ball <thorsten@zed.dev>
|
||||
Thorsten Ball <thorsten@zed.dev> <me@thorstenball.com>
|
||||
Thorsten Ball <thorsten@zed.dev> <mrnugget@gmail.com>
|
||||
Thorsten Ball <mrnugget@gmail.com>
|
||||
Thorsten Ball <mrnugget@gmail.com> <me@thorstenball.com>
|
||||
Thorsten Ball <mrnugget@gmail.com> <thorsten@zed.dev>
|
||||
Tristan Hume <tris.hume@gmail.com>
|
||||
Tristan Hume <tris.hume@gmail.com> <tristan@anthropic.com>
|
||||
Uladzislau Kaminski <i@uladkaminski.com>
|
||||
|
||||
2
.rules
2
.rules
@@ -115,7 +115,7 @@ Other entities can then register a callback to handle these events by doing `cx.
|
||||
GPUI has had some changes to its APIs. Always write code using the new APIs:
|
||||
|
||||
* `spawn` methods now take async closures (`AsyncFn`), and so should be called like `cx.spawn(async move |cx| ...)`.
|
||||
* Use `Entity<T>`. This replaces `Model<T>` and `View<T>` which longer exists and should NEVER be used.
|
||||
* Use `Entity<T>`. This replaces `Model<T>` and `View<T>` which no longer exist and should NEVER be used.
|
||||
* Use `App` references. This replaces `AppContext` which no longer exists and should NEVER be used.
|
||||
* Use `Context<T>` references. This replaces `ModelContext<T>` which no longer exists and should NEVER be used.
|
||||
* `Window` is now passed around explicitly. The new interface adds a `Window` reference parameter to some methods, and adds some new "*_in" methods for plumbing `Window`. The old types `WindowContext` and `ViewContext<T>` should NEVER be used.
|
||||
|
||||
@@ -2,16 +2,14 @@
|
||||
{
|
||||
"label": "Debug Zed (CodeLLDB)",
|
||||
"adapter": "CodeLLDB",
|
||||
"program": "$ZED_WORKTREE_ROOT/target/debug/zed",
|
||||
"request": "launch",
|
||||
"cwd": "$ZED_WORKTREE_ROOT"
|
||||
"program": "target/debug/zed",
|
||||
"request": "launch"
|
||||
},
|
||||
{
|
||||
"label": "Debug Zed (GDB)",
|
||||
"adapter": "GDB",
|
||||
"program": "$ZED_WORKTREE_ROOT/target/debug/zed",
|
||||
"program": "target/debug/zed",
|
||||
"request": "launch",
|
||||
"cwd": "$ZED_WORKTREE_ROOT",
|
||||
"initialize_args": {
|
||||
"stopAtBeginningOfMainSubprogram": true
|
||||
}
|
||||
|
||||
1349
Cargo.lock
generated
1349
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
11
Cargo.toml
11
Cargo.toml
@@ -74,6 +74,8 @@ members = [
|
||||
"crates/inline_completion",
|
||||
"crates/inline_completion_button",
|
||||
"crates/install_cli",
|
||||
"crates/jj",
|
||||
"crates/jj_ui",
|
||||
"crates/journal",
|
||||
"crates/language",
|
||||
"crates/language_extension",
|
||||
@@ -279,6 +281,8 @@ indexed_docs = { path = "crates/indexed_docs" }
|
||||
inline_completion = { path = "crates/inline_completion" }
|
||||
inline_completion_button = { path = "crates/inline_completion_button" }
|
||||
install_cli = { path = "crates/install_cli" }
|
||||
jj = { path = "crates/jj" }
|
||||
jj_ui = { path = "crates/jj_ui" }
|
||||
journal = { path = "crates/journal" }
|
||||
language = { path = "crates/language" }
|
||||
language_extension = { path = "crates/language_extension" }
|
||||
@@ -458,6 +462,7 @@ indexmap = { version = "2.7.0", features = ["serde"] }
|
||||
indoc = "2"
|
||||
inventory = "0.3.19"
|
||||
itertools = "0.14.0"
|
||||
jj-lib = { git = "https://github.com/jj-vcs/jj", rev = "e18eb8e05efaa153fad5ef46576af145bba1807f" }
|
||||
jsonschema = "0.30.0"
|
||||
jsonwebtoken = "9.3"
|
||||
jupyter-protocol = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734" }
|
||||
@@ -548,7 +553,7 @@ syn = { version = "1.0.72", features = ["full", "extra-traits"] }
|
||||
sys-locale = "0.3.1"
|
||||
sysinfo = "0.31.0"
|
||||
take-until = "0.2.0"
|
||||
tempfile = "3.9.0"
|
||||
tempfile = "3.20.0"
|
||||
thiserror = "2.0.12"
|
||||
tiktoken-rs = "0.6.0"
|
||||
time = { version = "0.3", features = [
|
||||
@@ -594,6 +599,7 @@ url = "2.2"
|
||||
urlencoding = "2.1.2"
|
||||
uuid = { version = "1.1.2", features = ["v4", "v5", "v7", "serde"] }
|
||||
walkdir = "2.3"
|
||||
wasi-preview1-component-adapter-provider = "29"
|
||||
wasm-encoder = "0.221"
|
||||
wasmparser = "0.221"
|
||||
wasmtime = { version = "29", default-features = false, features = [
|
||||
@@ -787,6 +793,9 @@ let_underscore_future = "allow"
|
||||
# running afoul of the borrow checker.
|
||||
too_many_arguments = "allow"
|
||||
|
||||
# We often have large enum variants yet we rarely actually bother with splitting them up.
|
||||
large_enum_variant = "allow"
|
||||
|
||||
[workspace.metadata.cargo-machete]
|
||||
ignored = [
|
||||
"bindgen",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# syntax = docker/dockerfile:1.2
|
||||
|
||||
FROM rust:1.86-bookworm as builder
|
||||
FROM rust:1.87-bookworm as builder
|
||||
WORKDIR app
|
||||
COPY . .
|
||||
|
||||
|
||||
@@ -8,10 +8,6 @@ Welcome to Zed, a high-performance, multiplayer code editor from the creators of
|
||||
|
||||
### Installation
|
||||
|
||||
<a href="https://repology.org/project/zed-editor/versions">
|
||||
<img src="https://repology.org/badge/vertical-allrepos/zed-editor.svg?minversion=0.143.5" alt="Packaging status" align="right">
|
||||
</a>
|
||||
|
||||
On macOS and Linux you can [download Zed directly](https://zed.dev/download) or [install Zed via your local package manager](https://zed.dev/docs/linux#installing-via-a-package-manager).
|
||||
|
||||
Other platforms are not yet available:
|
||||
|
||||
@@ -766,7 +766,7 @@
|
||||
"alt-ctrl-r": "project_panel::RevealInFileManager",
|
||||
"ctrl-shift-enter": "project_panel::OpenWithSystem",
|
||||
"shift-find": "project_panel::NewSearchInDirectory",
|
||||
"ctrl-shift-f": "project_panel::NewSearchInDirectory",
|
||||
"ctrl-alt-shift-f": "project_panel::NewSearchInDirectory",
|
||||
"shift-down": "menu::SelectNext",
|
||||
"shift-up": "menu::SelectPrevious",
|
||||
"escape": "menu::Cancel"
|
||||
|
||||
@@ -825,7 +825,7 @@
|
||||
"alt-cmd-r": "project_panel::RevealInFileManager",
|
||||
"ctrl-shift-enter": "project_panel::OpenWithSystem",
|
||||
"cmd-alt-backspace": ["project_panel::Delete", { "skip_prompt": false }],
|
||||
"cmd-shift-f": "project_panel::NewSearchInDirectory",
|
||||
"cmd-alt-shift-f": "project_panel::NewSearchInDirectory",
|
||||
"shift-down": "menu::SelectNext",
|
||||
"shift-up": "menu::SelectPrevious",
|
||||
"escape": "menu::Cancel"
|
||||
|
||||
@@ -152,6 +152,7 @@
|
||||
"g end": ["vim::EndOfLine", { "display_lines": true }],
|
||||
"g 0": ["vim::StartOfLine", { "display_lines": true }],
|
||||
"g home": ["vim::StartOfLine", { "display_lines": true }],
|
||||
"g shift-m": ["vim::MiddleOfLine", { "display_lines": true }],
|
||||
"g ^": ["vim::FirstNonWhitespace", { "display_lines": true }],
|
||||
"g v": "vim::RestoreVisualSelection",
|
||||
"g ]": "editor::GoToDiagnostic",
|
||||
|
||||
@@ -1715,6 +1715,8 @@
|
||||
// }
|
||||
// ]
|
||||
"ssh_connections": [],
|
||||
// Whether to read ~/.ssh/config for ssh connection sources.
|
||||
"read_ssh_config": true,
|
||||
// Configures context servers for use by the agent.
|
||||
"context_servers": {},
|
||||
"debugger": {
|
||||
|
||||
@@ -99,9 +99,6 @@
|
||||
"version_control.added": "#27a657ff",
|
||||
"version_control.modified": "#d3b020ff",
|
||||
"version_control.deleted": "#e06c76ff",
|
||||
"version_control.conflict_marker.ours": "#a1c1811a",
|
||||
"version_control.conflict_marker.theirs": "#74ade81a",
|
||||
"version_control.conflict_marker.border": "#d3b0201f",
|
||||
"conflict": "#dec184ff",
|
||||
"conflict.background": "#dec1841a",
|
||||
"conflict.border": "#5d4c2fff",
|
||||
@@ -481,11 +478,6 @@
|
||||
"version_control.added": "#27a657ff",
|
||||
"version_control.modified": "#d3b020ff",
|
||||
"version_control.deleted": "#e06c76ff",
|
||||
"version_control.conflict.ours_background": "#FF0000",
|
||||
"version_control.conflict.ours_border": "#FF0000",
|
||||
"version_control.conflict.theirs_background": "#e2e2faff",
|
||||
"version_control.conflict.theirs_border": "#cbcdf6ff",
|
||||
"version_control.conflict.divider_background": "#faf2e6ff",
|
||||
"conflict": "#a48819ff",
|
||||
"conflict.background": "#faf2e6ff",
|
||||
"conflict.border": "#f4e7d1ff",
|
||||
|
||||
@@ -185,12 +185,14 @@ pub(crate) fn default_markdown_style(window: &Window, cx: &App) -> MarkdownStyle
|
||||
let ui_font_size = TextSize::Default.rems(cx);
|
||||
let buffer_font_size = TextSize::Small.rems(cx);
|
||||
let mut text_style = window.text_style();
|
||||
let line_height = buffer_font_size * 1.75;
|
||||
|
||||
text_style.refine(&TextStyleRefinement {
|
||||
font_family: Some(theme_settings.ui_font.family.clone()),
|
||||
font_fallbacks: theme_settings.ui_font.fallbacks.clone(),
|
||||
font_features: Some(theme_settings.ui_font.features.clone()),
|
||||
font_size: Some(ui_font_size.into()),
|
||||
line_height: Some(line_height.into()),
|
||||
color: Some(cx.theme().colors().text),
|
||||
..Default::default()
|
||||
});
|
||||
@@ -1012,6 +1014,7 @@ impl ActiveThread {
|
||||
self.push_message(message_id, &message_segments, window, cx);
|
||||
}
|
||||
|
||||
self.scroll_to_bottom(cx);
|
||||
self.save_thread(cx);
|
||||
cx.notify();
|
||||
}
|
||||
@@ -1025,6 +1028,7 @@ impl ActiveThread {
|
||||
self.edited_message(message_id, &message_segments, window, cx);
|
||||
}
|
||||
|
||||
self.scroll_to_bottom(cx);
|
||||
self.save_thread(cx);
|
||||
cx.notify();
|
||||
}
|
||||
@@ -1538,11 +1542,15 @@ impl ActiveThread {
|
||||
let project = self.thread.read(cx).project().clone();
|
||||
let prompt_store = self.thread_store.read(cx).prompt_store().clone();
|
||||
|
||||
let git_store = project.read(cx).git_store().clone();
|
||||
let checkpoint = git_store.update(cx, |git_store, cx| git_store.checkpoint(cx));
|
||||
|
||||
let load_context_task =
|
||||
crate::context::load_context(new_context, &project, &prompt_store, cx);
|
||||
self._load_edited_message_context_task =
|
||||
Some(cx.spawn_in(window, async move |this, cx| {
|
||||
let context = load_context_task.await;
|
||||
let (context, checkpoint) =
|
||||
futures::future::join(load_context_task, checkpoint).await;
|
||||
let _ = this
|
||||
.update_in(cx, |this, window, cx| {
|
||||
this.thread.update(cx, |thread, cx| {
|
||||
@@ -1551,6 +1559,7 @@ impl ActiveThread {
|
||||
Role::User,
|
||||
vec![MessageSegment::Text(edited_text)],
|
||||
Some(context.loaded_context),
|
||||
checkpoint.ok(),
|
||||
cx,
|
||||
);
|
||||
for message_id in this.messages_after(message_id) {
|
||||
@@ -1720,10 +1729,11 @@ impl ActiveThread {
|
||||
.on_action(cx.listener(Self::confirm_editing_message))
|
||||
.capture_action(cx.listener(Self::paste))
|
||||
.min_h_6()
|
||||
.flex_grow()
|
||||
.w_full()
|
||||
.flex_grow()
|
||||
.gap_2()
|
||||
.child(EditorElement::new(
|
||||
.child(state.context_strip.clone())
|
||||
.child(div().pt(px(-3.)).px_neg_0p5().child(EditorElement::new(
|
||||
&state.editor,
|
||||
EditorStyle {
|
||||
background: colors.editor_background,
|
||||
@@ -1732,8 +1742,7 @@ impl ActiveThread {
|
||||
syntax: cx.theme().syntax().clone(),
|
||||
..Default::default()
|
||||
},
|
||||
))
|
||||
.child(state.context_strip.clone())
|
||||
)))
|
||||
}
|
||||
|
||||
fn render_message(&self, ix: usize, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
|
||||
@@ -1921,16 +1930,6 @@ impl ActiveThread {
|
||||
v_flex()
|
||||
.w_full()
|
||||
.gap_1()
|
||||
.when(!message_is_empty, |parent| {
|
||||
parent.child(div().min_h_6().child(self.render_message_content(
|
||||
message_id,
|
||||
rendered_message,
|
||||
has_tool_uses,
|
||||
workspace.clone(),
|
||||
window,
|
||||
cx,
|
||||
)))
|
||||
})
|
||||
.when(!added_context.is_empty(), |parent| {
|
||||
parent.child(h_flex().flex_wrap().gap_1().children(
|
||||
added_context.into_iter().map(|added_context| {
|
||||
@@ -1949,6 +1948,16 @@ impl ActiveThread {
|
||||
}),
|
||||
))
|
||||
})
|
||||
.when(!message_is_empty, |parent| {
|
||||
parent.child(div().pt_0p5().min_h_6().child(self.render_message_content(
|
||||
message_id,
|
||||
rendered_message,
|
||||
has_tool_uses,
|
||||
workspace.clone(),
|
||||
window,
|
||||
cx,
|
||||
)))
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
});
|
||||
@@ -1974,6 +1983,7 @@ impl ActiveThread {
|
||||
h_flex()
|
||||
.p_2p5()
|
||||
.gap_1()
|
||||
.items_end()
|
||||
.children(message_content)
|
||||
.when_some(editing_message_state, |this, state| {
|
||||
let focus_handle = state.editor.focus_handle(cx).clone();
|
||||
@@ -1987,6 +1997,7 @@ impl ActiveThread {
|
||||
)
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.icon_color(Color::Error)
|
||||
.icon_size(IconSize::Small)
|
||||
.tooltip({
|
||||
let focus_handle = focus_handle.clone();
|
||||
move |window, cx| {
|
||||
@@ -2004,11 +2015,12 @@ impl ActiveThread {
|
||||
.child(
|
||||
IconButton::new(
|
||||
"confirm-edit-message",
|
||||
IconName::Check,
|
||||
IconName::Return,
|
||||
)
|
||||
.disabled(state.editor.read(cx).is_empty(cx))
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.icon_color(Color::Success)
|
||||
.icon_color(Color::Muted)
|
||||
.icon_size(IconSize::Small)
|
||||
.tooltip({
|
||||
let focus_handle = focus_handle.clone();
|
||||
move |window, cx| {
|
||||
@@ -2028,9 +2040,6 @@ impl ActiveThread {
|
||||
)
|
||||
}),
|
||||
)
|
||||
.when(editing_message_state.is_none(), |this| {
|
||||
this.tooltip(Tooltip::text("Click To Edit"))
|
||||
})
|
||||
.on_click(cx.listener({
|
||||
let message_segments = message.segments.clone();
|
||||
move |this, _, window, cx| {
|
||||
@@ -2071,6 +2080,16 @@ impl ActiveThread {
|
||||
|
||||
let panel_background = cx.theme().colors().panel_background;
|
||||
|
||||
let backdrop = div()
|
||||
.id("backdrop")
|
||||
.stop_mouse_events_except_scroll()
|
||||
.absolute()
|
||||
.inset_0()
|
||||
.size_full()
|
||||
.bg(panel_background)
|
||||
.opacity(0.8)
|
||||
.on_click(cx.listener(Self::handle_cancel_click));
|
||||
|
||||
v_flex()
|
||||
.w_full()
|
||||
.map(|parent| {
|
||||
@@ -2240,15 +2259,7 @@ impl ActiveThread {
|
||||
})
|
||||
.when(after_editing_message, |parent| {
|
||||
// Backdrop to dim out the whole thread below the editing user message
|
||||
parent.relative().child(
|
||||
div()
|
||||
.stop_mouse_events_except_scroll()
|
||||
.absolute()
|
||||
.inset_0()
|
||||
.size_full()
|
||||
.bg(panel_background)
|
||||
.opacity(0.8),
|
||||
)
|
||||
parent.relative().child(backdrop)
|
||||
})
|
||||
.into_any()
|
||||
}
|
||||
@@ -2363,6 +2374,7 @@ impl ActiveThread {
|
||||
move |el, range, metadata, _, cx| {
|
||||
let can_expand = metadata.line_count
|
||||
>= MAX_UNCOLLAPSED_LINES_IN_CODE_BLOCK;
|
||||
|
||||
if !can_expand {
|
||||
return el;
|
||||
}
|
||||
@@ -2370,6 +2382,7 @@ impl ActiveThread {
|
||||
let is_expanded = active_thread
|
||||
.read(cx)
|
||||
.is_codeblock_expanded(message_id, range.start);
|
||||
|
||||
if is_expanded {
|
||||
return el;
|
||||
}
|
||||
@@ -3392,16 +3405,21 @@ impl ActiveThread {
|
||||
self.expanded_code_blocks
|
||||
.get(&(message_id, ix))
|
||||
.copied()
|
||||
.unwrap_or(false)
|
||||
.unwrap_or(true)
|
||||
}
|
||||
|
||||
pub fn toggle_codeblock_expanded(&mut self, message_id: MessageId, ix: usize) {
|
||||
let is_expanded = self
|
||||
.expanded_code_blocks
|
||||
.entry((message_id, ix))
|
||||
.or_insert(false);
|
||||
.or_insert(true);
|
||||
*is_expanded = !*is_expanded;
|
||||
}
|
||||
|
||||
pub fn scroll_to_bottom(&mut self, cx: &mut Context<Self>) {
|
||||
self.list_state.reset(self.messages.len());
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
pub enum ActiveThreadEvent {
|
||||
@@ -3415,6 +3433,7 @@ impl Render for ActiveThread {
|
||||
v_flex()
|
||||
.size_full()
|
||||
.relative()
|
||||
.bg(cx.theme().colors().panel_background)
|
||||
.on_mouse_move(cx.listener(|this, _, _, cx| {
|
||||
this.show_scrollbar = true;
|
||||
this.hide_scrollbar_later(cx);
|
||||
|
||||
@@ -30,7 +30,6 @@ pub(crate) struct ConfigureContextServerModal {
|
||||
context_server_store: Entity<ContextServerStore>,
|
||||
}
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
enum Configuration {
|
||||
NotAvailable,
|
||||
Required(ConfigurationRequiredState),
|
||||
|
||||
@@ -567,6 +567,15 @@ impl AgentPanel {
|
||||
menu = menu.header("Recently Opened");
|
||||
|
||||
for entry in recently_opened.iter() {
|
||||
if let RecentEntry::Context(context) = entry {
|
||||
if context.read(cx).path().is_none() {
|
||||
log::error!(
|
||||
"bug: text thread in recent history list was never saved"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
let summary = entry.summary(cx);
|
||||
|
||||
menu = menu.entry_with_end_slot_on_hover(
|
||||
@@ -1290,14 +1299,26 @@ impl AgentPanel {
|
||||
let new_is_history = matches!(new_view, ActiveView::History);
|
||||
|
||||
match &self.active_view {
|
||||
ActiveView::Thread { thread, .. } => self.history_store.update(cx, |store, cx| {
|
||||
ActiveView::Thread { thread, .. } => {
|
||||
if let Some(thread) = thread.upgrade() {
|
||||
if thread.read(cx).is_empty() {
|
||||
let id = thread.read(cx).id().clone();
|
||||
store.remove_recently_opened_thread(id, cx);
|
||||
self.history_store.update(cx, |store, cx| {
|
||||
store.remove_recently_opened_thread(id, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
}),
|
||||
}
|
||||
ActiveView::PromptEditor { context_editor, .. } => {
|
||||
let context = context_editor.read(cx).context();
|
||||
// When switching away from an unsaved text thread, delete its entry.
|
||||
if context.read(cx).path().is_none() {
|
||||
let context = context.clone();
|
||||
self.history_store.update(cx, |store, cx| {
|
||||
store.remove_recently_opened_entry(&RecentEntry::Context(context), cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
@@ -2135,6 +2156,7 @@ impl AgentPanel {
|
||||
|
||||
v_flex()
|
||||
.size_full()
|
||||
.bg(cx.theme().colors().panel_background)
|
||||
.when(recent_history.is_empty(), |this| {
|
||||
let configuration_error_ref = &configuration_error;
|
||||
this.child(
|
||||
|
||||
@@ -84,6 +84,12 @@ impl ContextStrip {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether or not the context strip has items to display
|
||||
pub fn has_context_items(&self, cx: &App) -> bool {
|
||||
self.context_store.read(cx).context().next().is_some()
|
||||
|| self.suggested_context(cx).is_some()
|
||||
}
|
||||
|
||||
fn added_contexts(&self, cx: &App) -> Vec<AddedContext> {
|
||||
if let Some(workspace) = self.workspace.upgrade() {
|
||||
let project = workspace.read(cx).project().read(cx);
|
||||
@@ -104,14 +110,14 @@ impl ContextStrip {
|
||||
}
|
||||
}
|
||||
|
||||
fn suggested_context(&self, cx: &Context<Self>) -> Option<SuggestedContext> {
|
||||
fn suggested_context(&self, cx: &App) -> Option<SuggestedContext> {
|
||||
match self.suggest_context_kind {
|
||||
SuggestContextKind::File => self.suggested_file(cx),
|
||||
SuggestContextKind::Thread => self.suggested_thread(cx),
|
||||
}
|
||||
}
|
||||
|
||||
fn suggested_file(&self, cx: &Context<Self>) -> Option<SuggestedContext> {
|
||||
fn suggested_file(&self, cx: &App) -> Option<SuggestedContext> {
|
||||
let workspace = self.workspace.upgrade()?;
|
||||
let active_item = workspace.read(cx).active_item(cx)?;
|
||||
|
||||
@@ -138,7 +144,7 @@ impl ContextStrip {
|
||||
})
|
||||
}
|
||||
|
||||
fn suggested_thread(&self, cx: &Context<Self>) -> Option<SuggestedContext> {
|
||||
fn suggested_thread(&self, cx: &App) -> Option<SuggestedContext> {
|
||||
if !self.context_picker.read(cx).allow_threads() {
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -451,7 +451,7 @@ impl<T: 'static> PromptEditor<T> {
|
||||
editor.move_to_end(&Default::default(), window, cx)
|
||||
});
|
||||
}
|
||||
} else {
|
||||
} else if self.context_strip.read(cx).has_context_items(cx) {
|
||||
self.context_strip.focus_handle(cx).focus(window);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -401,7 +401,7 @@ impl MessageEditor {
|
||||
fn move_up(&mut self, _: &MoveUp, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if self.context_picker_menu_handle.is_deployed() {
|
||||
cx.propagate();
|
||||
} else {
|
||||
} else if self.context_strip.read(cx).has_context_items(cx) {
|
||||
self.context_strip.focus_handle(cx).focus(window);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,7 +214,7 @@ pub struct GitState {
|
||||
pub diff: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ThreadCheckpoint {
|
||||
message_id: MessageId,
|
||||
git_checkpoint: GitStoreCheckpoint,
|
||||
@@ -996,6 +996,7 @@ impl Thread {
|
||||
new_role: Role,
|
||||
new_segments: Vec<MessageSegment>,
|
||||
loaded_context: Option<LoadedContext>,
|
||||
checkpoint: Option<GitStoreCheckpoint>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> bool {
|
||||
let Some(message) = self.messages.iter_mut().find(|message| message.id == id) else {
|
||||
@@ -1006,6 +1007,15 @@ impl Thread {
|
||||
if let Some(context) = loaded_context {
|
||||
message.loaded_context = context;
|
||||
}
|
||||
if let Some(git_checkpoint) = checkpoint {
|
||||
self.checkpoints_by_message.insert(
|
||||
id,
|
||||
ThreadCheckpoint {
|
||||
message_id: id,
|
||||
git_checkpoint,
|
||||
},
|
||||
);
|
||||
}
|
||||
self.touch_updated_at();
|
||||
cx.emit(ThreadEvent::MessageEdited(id));
|
||||
true
|
||||
|
||||
@@ -425,16 +425,17 @@ impl ToolUseState {
|
||||
|
||||
let content = match tool_result {
|
||||
ToolResultContent::Text(text) => {
|
||||
let truncated = truncate_lines_to_byte_limit(&text, tool_output_limit);
|
||||
|
||||
LanguageModelToolResultContent::Text(
|
||||
let text = if text.len() < tool_output_limit {
|
||||
text
|
||||
} else {
|
||||
let truncated = truncate_lines_to_byte_limit(&text, tool_output_limit);
|
||||
format!(
|
||||
"Tool result too long. The first {} bytes:\n\n{}",
|
||||
truncated.len(),
|
||||
truncated
|
||||
)
|
||||
.into(),
|
||||
)
|
||||
};
|
||||
LanguageModelToolResultContent::Text(text.into())
|
||||
}
|
||||
ToolResultContent::Image(language_model_image) => {
|
||||
if language_model_image.estimate_tokens() < tool_output_limit {
|
||||
|
||||
@@ -163,8 +163,10 @@ impl AskPassSession {
|
||||
#[cfg(unix)]
|
||||
fn get_shell_safe_zed_path() -> anyhow::Result<String> {
|
||||
let zed_path = std::env::current_exe()
|
||||
.context("Failed to figure out current executable path for use in askpass")?
|
||||
.context("Failed to determine current executable path for use in askpass")?
|
||||
.to_string_lossy()
|
||||
// see https://github.com/rust-lang/rust/issues/69343
|
||||
.trim_end_matches(" (deleted)")
|
||||
.to_string();
|
||||
|
||||
// NOTE: this was previously enabled, however, it caused errors when it shouldn't have
|
||||
|
||||
@@ -692,7 +692,7 @@ impl JsonSchema for LanguageModelProviderSetting {
|
||||
schemars::schema::SchemaObject {
|
||||
enum_values: Some(vec![
|
||||
"anthropic".into(),
|
||||
"bedrock".into(),
|
||||
"amazon-bedrock".into(),
|
||||
"google".into(),
|
||||
"lmstudio".into(),
|
||||
"ollama".into(),
|
||||
|
||||
@@ -15,7 +15,7 @@ use gpui::{AppContext, TestAppContext};
|
||||
use indoc::{formatdoc, indoc};
|
||||
use language_model::{
|
||||
LanguageModelRegistry, LanguageModelRequestTool, LanguageModelToolResult,
|
||||
LanguageModelToolResultContent, LanguageModelToolUse, LanguageModelToolUseId,
|
||||
LanguageModelToolResultContent, LanguageModelToolUse, LanguageModelToolUseId, SelectedModel,
|
||||
};
|
||||
use project::Project;
|
||||
use rand::prelude::*;
|
||||
@@ -25,6 +25,7 @@ use std::{
|
||||
cmp::Reverse,
|
||||
fmt::{self, Display},
|
||||
io::Write as _,
|
||||
str::FromStr,
|
||||
sync::mpsc,
|
||||
};
|
||||
use util::path;
|
||||
@@ -1216,7 +1217,7 @@ fn report_progress(evaluated_count: usize, failed_count: usize, iterations: usiz
|
||||
passed_count as f64 / evaluated_count as f64
|
||||
};
|
||||
print!(
|
||||
"\r\x1b[KEvaluated {}/{} ({:.2}%)",
|
||||
"\r\x1b[KEvaluated {}/{} ({:.2}% passed)",
|
||||
evaluated_count,
|
||||
iterations,
|
||||
passed_ratio * 100.0
|
||||
@@ -1255,13 +1256,21 @@ impl EditAgentTest {
|
||||
|
||||
fs.insert_tree("/root", json!({})).await;
|
||||
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||
let agent_model = SelectedModel::from_str(
|
||||
&std::env::var("ZED_AGENT_MODEL")
|
||||
.unwrap_or("anthropic/claude-3-7-sonnet-latest".into()),
|
||||
)
|
||||
.unwrap();
|
||||
let judge_model = SelectedModel::from_str(
|
||||
&std::env::var("ZED_JUDGE_MODEL")
|
||||
.unwrap_or("anthropic/claude-3-7-sonnet-latest".into()),
|
||||
)
|
||||
.unwrap();
|
||||
let (agent_model, judge_model) = cx
|
||||
.update(|cx| {
|
||||
cx.spawn(async move |cx| {
|
||||
let agent_model =
|
||||
Self::load_model("anthropic", "claude-3-7-sonnet-latest", cx).await;
|
||||
let judge_model =
|
||||
Self::load_model("anthropic", "claude-3-7-sonnet-latest", cx).await;
|
||||
let agent_model = Self::load_model(&agent_model, cx).await;
|
||||
let judge_model = Self::load_model(&judge_model, cx).await;
|
||||
(agent_model.unwrap(), judge_model.unwrap())
|
||||
})
|
||||
})
|
||||
@@ -1276,15 +1285,17 @@ impl EditAgentTest {
|
||||
}
|
||||
|
||||
async fn load_model(
|
||||
provider: &str,
|
||||
id: &str,
|
||||
selected_model: &SelectedModel,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<Arc<dyn LanguageModel>> {
|
||||
let (provider, model) = cx.update(|cx| {
|
||||
let models = LanguageModelRegistry::read_global(cx);
|
||||
let model = models
|
||||
.available_models(cx)
|
||||
.find(|model| model.provider_id().0 == provider && model.id().0 == id)
|
||||
.find(|model| {
|
||||
model.provider_id() == selected_model.provider
|
||||
&& model.id() == selected_model.model
|
||||
})
|
||||
.unwrap();
|
||||
let provider = models.provider(&model.provider_id()).unwrap();
|
||||
(provider, model)
|
||||
|
||||
@@ -1249,7 +1249,7 @@ pub struct ActiveDiagnosticGroup {
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
|
||||
pub(crate) enum ActiveDiagnostic {
|
||||
None,
|
||||
All,
|
||||
|
||||
@@ -19,6 +19,7 @@ test-support = ["clock/test-support", "collections/test-support", "gpui/test-sup
|
||||
anyhow.workspace = true
|
||||
async-recursion = "0.3"
|
||||
async-tungstenite = { workspace = true, features = ["tokio", "tokio-rustls-manual-roots"] }
|
||||
base64.workspace = true
|
||||
chrono = { workspace = true, features = ["serde"] }
|
||||
clock.workspace = true
|
||||
collections.workspace = true
|
||||
@@ -29,6 +30,7 @@ gpui.workspace = true
|
||||
gpui_tokio.workspace = true
|
||||
http_client.workspace = true
|
||||
http_client_tls.workspace = true
|
||||
httparse = "1.10"
|
||||
log.workspace = true
|
||||
paths.workspace = true
|
||||
parking_lot.workspace = true
|
||||
@@ -47,6 +49,7 @@ text.workspace = true
|
||||
thiserror.workspace = true
|
||||
time.workspace = true
|
||||
tiny_http = "0.8"
|
||||
tokio-native-tls = "0.3"
|
||||
tokio-socks = { version = "0.5.2", default-features = false, features = ["futures-io"] }
|
||||
url.workspace = true
|
||||
util.workspace = true
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub mod test;
|
||||
|
||||
mod socks;
|
||||
mod proxy;
|
||||
pub mod telemetry;
|
||||
pub mod user;
|
||||
pub mod zed_urls;
|
||||
@@ -24,13 +24,13 @@ use gpui::{App, AsyncApp, Entity, Global, Task, WeakEntity, actions};
|
||||
use http_client::{AsyncBody, HttpClient, HttpClientWithUrl};
|
||||
use parking_lot::RwLock;
|
||||
use postage::watch;
|
||||
use proxy::connect_proxy_stream;
|
||||
use rand::prelude::*;
|
||||
use release_channel::{AppVersion, ReleaseChannel};
|
||||
use rpc::proto::{AnyTypedEnvelope, EnvelopedMessage, PeerId, RequestMessage};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsSources};
|
||||
use socks::connect_socks_proxy_stream;
|
||||
use std::pin::Pin;
|
||||
use std::{
|
||||
any::TypeId,
|
||||
@@ -1156,7 +1156,7 @@ impl Client {
|
||||
let handle = cx.update(|cx| gpui_tokio::Tokio::handle(cx)).ok().unwrap();
|
||||
let _guard = handle.enter();
|
||||
match proxy {
|
||||
Some(proxy) => connect_socks_proxy_stream(&proxy, rpc_host).await?,
|
||||
Some(proxy) => connect_proxy_stream(&proxy, rpc_host).await?,
|
||||
None => Box::new(TcpStream::connect(rpc_host).await?),
|
||||
}
|
||||
};
|
||||
|
||||
66
crates/client/src/proxy.rs
Normal file
66
crates/client/src/proxy.rs
Normal file
@@ -0,0 +1,66 @@
|
||||
//! client proxy
|
||||
|
||||
mod http_proxy;
|
||||
mod socks_proxy;
|
||||
|
||||
use anyhow::{Context, Result, anyhow};
|
||||
use http_client::Url;
|
||||
use http_proxy::{HttpProxyType, connect_http_proxy_stream, parse_http_proxy};
|
||||
use socks_proxy::{SocksVersion, connect_socks_proxy_stream, parse_socks_proxy};
|
||||
|
||||
pub(crate) async fn connect_proxy_stream(
|
||||
proxy: &Url,
|
||||
rpc_host: (&str, u16),
|
||||
) -> Result<Box<dyn AsyncReadWrite>> {
|
||||
let Some(((proxy_domain, proxy_port), proxy_type)) = parse_proxy_type(proxy) else {
|
||||
// If parsing the proxy URL fails, we must avoid falling back to an insecure connection.
|
||||
// SOCKS proxies are often used in contexts where security and privacy are critical,
|
||||
// so any fallback could expose users to significant risks.
|
||||
return Err(anyhow!("Parsing proxy url failed"));
|
||||
};
|
||||
|
||||
// Connect to proxy and wrap protocol later
|
||||
let stream = tokio::net::TcpStream::connect((proxy_domain.as_str(), proxy_port))
|
||||
.await
|
||||
.context("Failed to connect to proxy")?;
|
||||
|
||||
let proxy_stream = match proxy_type {
|
||||
ProxyType::SocksProxy(proxy) => connect_socks_proxy_stream(stream, proxy, rpc_host).await?,
|
||||
ProxyType::HttpProxy(proxy) => {
|
||||
connect_http_proxy_stream(stream, proxy, rpc_host, &proxy_domain).await?
|
||||
}
|
||||
};
|
||||
|
||||
Ok(proxy_stream)
|
||||
}
|
||||
|
||||
enum ProxyType<'t> {
|
||||
SocksProxy(SocksVersion<'t>),
|
||||
HttpProxy(HttpProxyType<'t>),
|
||||
}
|
||||
|
||||
fn parse_proxy_type<'t>(proxy: &'t Url) -> Option<((String, u16), ProxyType<'t>)> {
|
||||
let scheme = proxy.scheme();
|
||||
let host = proxy.host()?.to_string();
|
||||
let port = proxy.port_or_known_default()?;
|
||||
let proxy_type = match scheme {
|
||||
scheme if scheme.starts_with("socks") => {
|
||||
Some(ProxyType::SocksProxy(parse_socks_proxy(scheme, proxy)))
|
||||
}
|
||||
scheme if scheme.starts_with("http") => {
|
||||
Some(ProxyType::HttpProxy(parse_http_proxy(scheme, proxy)))
|
||||
}
|
||||
_ => None,
|
||||
}?;
|
||||
|
||||
Some(((host, port), proxy_type))
|
||||
}
|
||||
|
||||
pub(crate) trait AsyncReadWrite:
|
||||
tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + 'static
|
||||
{
|
||||
}
|
||||
impl<T: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + 'static> AsyncReadWrite
|
||||
for T
|
||||
{
|
||||
}
|
||||
171
crates/client/src/proxy/http_proxy.rs
Normal file
171
crates/client/src/proxy/http_proxy.rs
Normal file
@@ -0,0 +1,171 @@
|
||||
use anyhow::{Context, Result};
|
||||
use base64::Engine;
|
||||
use httparse::{EMPTY_HEADER, Response};
|
||||
use tokio::{
|
||||
io::{AsyncBufReadExt, AsyncWriteExt, BufStream},
|
||||
net::TcpStream,
|
||||
};
|
||||
use tokio_native_tls::{TlsConnector, native_tls};
|
||||
use url::Url;
|
||||
|
||||
use super::AsyncReadWrite;
|
||||
|
||||
pub(super) enum HttpProxyType<'t> {
|
||||
HTTP(Option<HttpProxyAuthorization<'t>>),
|
||||
HTTPS(Option<HttpProxyAuthorization<'t>>),
|
||||
}
|
||||
|
||||
pub(super) struct HttpProxyAuthorization<'t> {
|
||||
username: &'t str,
|
||||
password: &'t str,
|
||||
}
|
||||
|
||||
pub(super) fn parse_http_proxy<'t>(scheme: &str, proxy: &'t Url) -> HttpProxyType<'t> {
|
||||
let auth = proxy.password().map(|password| HttpProxyAuthorization {
|
||||
username: proxy.username(),
|
||||
password,
|
||||
});
|
||||
if scheme.starts_with("https") {
|
||||
HttpProxyType::HTTPS(auth)
|
||||
} else {
|
||||
HttpProxyType::HTTP(auth)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn connect_http_proxy_stream(
|
||||
stream: TcpStream,
|
||||
http_proxy: HttpProxyType<'_>,
|
||||
rpc_host: (&str, u16),
|
||||
proxy_domain: &str,
|
||||
) -> Result<Box<dyn AsyncReadWrite>> {
|
||||
match http_proxy {
|
||||
HttpProxyType::HTTP(auth) => http_connect(stream, rpc_host, auth).await,
|
||||
HttpProxyType::HTTPS(auth) => https_connect(stream, rpc_host, auth, proxy_domain).await,
|
||||
}
|
||||
.context("error connecting to http/https proxy")
|
||||
}
|
||||
|
||||
async fn http_connect<T>(
|
||||
stream: T,
|
||||
target: (&str, u16),
|
||||
auth: Option<HttpProxyAuthorization<'_>>,
|
||||
) -> Result<Box<dyn AsyncReadWrite>>
|
||||
where
|
||||
T: AsyncReadWrite,
|
||||
{
|
||||
let mut stream = BufStream::new(stream);
|
||||
let request = make_request(target, auth);
|
||||
stream.write_all(request.as_bytes()).await?;
|
||||
stream.flush().await?;
|
||||
check_response(&mut stream).await?;
|
||||
Ok(Box::new(stream))
|
||||
}
|
||||
|
||||
async fn https_connect<T>(
|
||||
stream: T,
|
||||
target: (&str, u16),
|
||||
auth: Option<HttpProxyAuthorization<'_>>,
|
||||
proxy_domain: &str,
|
||||
) -> Result<Box<dyn AsyncReadWrite>>
|
||||
where
|
||||
T: AsyncReadWrite,
|
||||
{
|
||||
let tls_connector = TlsConnector::from(native_tls::TlsConnector::new()?);
|
||||
let stream = tls_connector.connect(proxy_domain, stream).await?;
|
||||
http_connect(stream, target, auth).await
|
||||
}
|
||||
|
||||
fn make_request(target: (&str, u16), auth: Option<HttpProxyAuthorization<'_>>) -> String {
|
||||
let (host, port) = target;
|
||||
let mut request = format!(
|
||||
"CONNECT {host}:{port} HTTP/1.1\r\nHost: {host}:{port}\r\nProxy-Connection: Keep-Alive\r\n"
|
||||
);
|
||||
if let Some(HttpProxyAuthorization { username, password }) = auth {
|
||||
let auth =
|
||||
base64::prelude::BASE64_STANDARD.encode(format!("{username}:{password}").as_bytes());
|
||||
let auth = format!("Proxy-Authorization: Basic {auth}\r\n");
|
||||
request.push_str(&auth);
|
||||
}
|
||||
request.push_str("\r\n");
|
||||
request
|
||||
}
|
||||
|
||||
async fn check_response<T>(stream: &mut BufStream<T>) -> Result<()>
|
||||
where
|
||||
T: AsyncReadWrite,
|
||||
{
|
||||
let response = recv_response(stream).await?;
|
||||
let mut dummy_headers = [EMPTY_HEADER; MAX_RESPONSE_HEADERS];
|
||||
let mut parser = Response::new(&mut dummy_headers);
|
||||
parser.parse(response.as_bytes())?;
|
||||
|
||||
match parser.code {
|
||||
Some(code) => {
|
||||
if code == 200 {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(anyhow::anyhow!(
|
||||
"Proxy connection failed with HTTP code: {code}"
|
||||
))
|
||||
}
|
||||
}
|
||||
None => Err(anyhow::anyhow!(
|
||||
"Proxy connection failed with no HTTP code: {}",
|
||||
parser.reason.unwrap_or("Unknown reason")
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
const MAX_RESPONSE_HEADER_LENGTH: usize = 4096;
|
||||
const MAX_RESPONSE_HEADERS: usize = 16;
|
||||
|
||||
async fn recv_response<T>(stream: &mut BufStream<T>) -> Result<String>
|
||||
where
|
||||
T: AsyncReadWrite,
|
||||
{
|
||||
let mut response = String::new();
|
||||
loop {
|
||||
if stream.read_line(&mut response).await? == 0 {
|
||||
return Err(anyhow::anyhow!("End of stream"));
|
||||
}
|
||||
|
||||
if MAX_RESPONSE_HEADER_LENGTH < response.len() {
|
||||
return Err(anyhow::anyhow!("Maximum response header length exceeded"));
|
||||
}
|
||||
|
||||
if response.ends_with("\r\n\r\n") {
|
||||
return Ok(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use url::Url;
|
||||
|
||||
use super::{HttpProxyAuthorization, HttpProxyType, parse_http_proxy};
|
||||
|
||||
#[test]
|
||||
fn test_parse_http_proxy() {
|
||||
let proxy = Url::parse("http://proxy.example.com:1080").unwrap();
|
||||
let scheme = proxy.scheme();
|
||||
|
||||
let version = parse_http_proxy(scheme, &proxy);
|
||||
assert!(matches!(version, HttpProxyType::HTTP(None)))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_http_proxy_with_auth() {
|
||||
let proxy = Url::parse("http://username:password@proxy.example.com:1080").unwrap();
|
||||
let scheme = proxy.scheme();
|
||||
|
||||
let version = parse_http_proxy(scheme, &proxy);
|
||||
assert!(matches!(
|
||||
version,
|
||||
HttpProxyType::HTTP(Some(HttpProxyAuthorization {
|
||||
username: "username",
|
||||
password: "password"
|
||||
}))
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,19 @@
|
||||
//! socks proxy
|
||||
use anyhow::{Context, Result, anyhow};
|
||||
use http_client::Url;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use tokio::net::TcpStream;
|
||||
use tokio_socks::tcp::{Socks4Stream, Socks5Stream};
|
||||
use url::Url;
|
||||
|
||||
use super::AsyncReadWrite;
|
||||
|
||||
/// Identification to a Socks V4 Proxy
|
||||
struct Socks4Identification<'a> {
|
||||
pub(super) struct Socks4Identification<'a> {
|
||||
user_id: &'a str,
|
||||
}
|
||||
|
||||
/// Authorization to a Socks V5 Proxy
|
||||
struct Socks5Authorization<'a> {
|
||||
pub(super) struct Socks5Authorization<'a> {
|
||||
username: &'a str,
|
||||
password: &'a str,
|
||||
}
|
||||
@@ -18,45 +22,50 @@ struct Socks5Authorization<'a> {
|
||||
///
|
||||
/// V4 allows idenfication using a user_id
|
||||
/// V5 allows authorization using a username and password
|
||||
enum SocksVersion<'a> {
|
||||
pub(super) enum SocksVersion<'a> {
|
||||
V4(Option<Socks4Identification<'a>>),
|
||||
V5(Option<Socks5Authorization<'a>>),
|
||||
}
|
||||
|
||||
pub(crate) async fn connect_socks_proxy_stream(
|
||||
proxy: &Url,
|
||||
pub(super) fn parse_socks_proxy<'t>(scheme: &str, proxy: &'t Url) -> SocksVersion<'t> {
|
||||
if scheme.starts_with("socks4") {
|
||||
let identification = match proxy.username() {
|
||||
"" => None,
|
||||
username => Some(Socks4Identification { user_id: username }),
|
||||
};
|
||||
SocksVersion::V4(identification)
|
||||
} else {
|
||||
let authorization = proxy.password().map(|password| Socks5Authorization {
|
||||
username: proxy.username(),
|
||||
password,
|
||||
});
|
||||
SocksVersion::V5(authorization)
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) async fn connect_socks_proxy_stream(
|
||||
stream: TcpStream,
|
||||
socks_version: SocksVersion<'_>,
|
||||
rpc_host: (&str, u16),
|
||||
) -> Result<Box<dyn AsyncReadWrite>> {
|
||||
let Some((socks_proxy, version)) = parse_socks_proxy(proxy) else {
|
||||
// If parsing the proxy URL fails, we must avoid falling back to an insecure connection.
|
||||
// SOCKS proxies are often used in contexts where security and privacy are critical,
|
||||
// so any fallback could expose users to significant risks.
|
||||
return Err(anyhow!("Parsing proxy url failed"));
|
||||
};
|
||||
|
||||
// Connect to proxy and wrap protocol later
|
||||
let stream = tokio::net::TcpStream::connect(socks_proxy)
|
||||
.await
|
||||
.context("Failed to connect to socks proxy")?;
|
||||
|
||||
let socks: Box<dyn AsyncReadWrite> = match version {
|
||||
match socks_version {
|
||||
SocksVersion::V4(None) => {
|
||||
let socks = Socks4Stream::connect_with_socket(stream, rpc_host)
|
||||
.await
|
||||
.context("error connecting to socks")?;
|
||||
Box::new(socks)
|
||||
Ok(Box::new(socks))
|
||||
}
|
||||
SocksVersion::V4(Some(Socks4Identification { user_id })) => {
|
||||
let socks = Socks4Stream::connect_with_userid_and_socket(stream, rpc_host, user_id)
|
||||
.await
|
||||
.context("error connecting to socks")?;
|
||||
Box::new(socks)
|
||||
Ok(Box::new(socks))
|
||||
}
|
||||
SocksVersion::V5(None) => {
|
||||
let socks = Socks5Stream::connect_with_socket(stream, rpc_host)
|
||||
.await
|
||||
.context("error connecting to socks")?;
|
||||
Box::new(socks)
|
||||
Ok(Box::new(socks))
|
||||
}
|
||||
SocksVersion::V5(Some(Socks5Authorization { username, password })) => {
|
||||
let socks = Socks5Stream::connect_with_password_and_socket(
|
||||
@@ -64,44 +73,9 @@ pub(crate) async fn connect_socks_proxy_stream(
|
||||
)
|
||||
.await
|
||||
.context("error connecting to socks")?;
|
||||
Box::new(socks)
|
||||
Ok(Box::new(socks))
|
||||
}
|
||||
};
|
||||
|
||||
Ok(socks)
|
||||
}
|
||||
|
||||
fn parse_socks_proxy(proxy: &Url) -> Option<((String, u16), SocksVersion<'_>)> {
|
||||
let scheme = proxy.scheme();
|
||||
let socks_version = if scheme.starts_with("socks4") {
|
||||
let identification = match proxy.username() {
|
||||
"" => None,
|
||||
username => Some(Socks4Identification { user_id: username }),
|
||||
};
|
||||
SocksVersion::V4(identification)
|
||||
} else if scheme.starts_with("socks") {
|
||||
let authorization = proxy.password().map(|password| Socks5Authorization {
|
||||
username: proxy.username(),
|
||||
password,
|
||||
});
|
||||
SocksVersion::V5(authorization)
|
||||
} else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let host = proxy.host()?.to_string();
|
||||
let port = proxy.port_or_known_default()?;
|
||||
|
||||
Some(((host, port), socks_version))
|
||||
}
|
||||
|
||||
pub(crate) trait AsyncReadWrite:
|
||||
tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + 'static
|
||||
{
|
||||
}
|
||||
impl<T: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + 'static> AsyncReadWrite
|
||||
for T
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -113,20 +87,18 @@ mod tests {
|
||||
#[test]
|
||||
fn parse_socks4() {
|
||||
let proxy = Url::parse("socks4://proxy.example.com:1080").unwrap();
|
||||
let scheme = proxy.scheme();
|
||||
|
||||
let ((host, port), version) = parse_socks_proxy(&proxy).unwrap();
|
||||
assert_eq!(host, "proxy.example.com");
|
||||
assert_eq!(port, 1080);
|
||||
let version = parse_socks_proxy(scheme, &proxy);
|
||||
assert!(matches!(version, SocksVersion::V4(None)))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_socks4_with_identification() {
|
||||
let proxy = Url::parse("socks4://userid@proxy.example.com:1080").unwrap();
|
||||
let scheme = proxy.scheme();
|
||||
|
||||
let ((host, port), version) = parse_socks_proxy(&proxy).unwrap();
|
||||
assert_eq!(host, "proxy.example.com");
|
||||
assert_eq!(port, 1080);
|
||||
let version = parse_socks_proxy(scheme, &proxy);
|
||||
assert!(matches!(
|
||||
version,
|
||||
SocksVersion::V4(Some(Socks4Identification { user_id: "userid" }))
|
||||
@@ -136,20 +108,18 @@ mod tests {
|
||||
#[test]
|
||||
fn parse_socks5() {
|
||||
let proxy = Url::parse("socks5://proxy.example.com:1080").unwrap();
|
||||
let scheme = proxy.scheme();
|
||||
|
||||
let ((host, port), version) = parse_socks_proxy(&proxy).unwrap();
|
||||
assert_eq!(host, "proxy.example.com");
|
||||
assert_eq!(port, 1080);
|
||||
let version = parse_socks_proxy(scheme, &proxy);
|
||||
assert!(matches!(version, SocksVersion::V5(None)))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_socks5_with_authorization() {
|
||||
let proxy = Url::parse("socks5://username:password@proxy.example.com:1080").unwrap();
|
||||
let scheme = proxy.scheme();
|
||||
|
||||
let ((host, port), version) = parse_socks_proxy(&proxy).unwrap();
|
||||
assert_eq!(host, "proxy.example.com");
|
||||
assert_eq!(port, 1080);
|
||||
let version = parse_socks_proxy(scheme, &proxy);
|
||||
assert!(matches!(
|
||||
version,
|
||||
SocksVersion::V5(Some(Socks5Authorization {
|
||||
@@ -158,19 +128,4 @@ mod tests {
|
||||
}))
|
||||
))
|
||||
}
|
||||
|
||||
/// If parsing the proxy URL fails, we must avoid falling back to an insecure connection.
|
||||
/// SOCKS proxies are often used in contexts where security and privacy are critical,
|
||||
/// so any fallback could expose users to significant risks.
|
||||
#[tokio::test]
|
||||
async fn fails_on_bad_proxy() {
|
||||
// Should fail connecting because http is not a valid Socks proxy scheme
|
||||
let proxy = Url::parse("http://localhost:2313").unwrap();
|
||||
|
||||
let result = connect_socks_proxy_stream(&proxy, ("test", 1080)).await;
|
||||
match result {
|
||||
Err(e) => assert_eq!(e.to_string(), "Parsing proxy url failed"),
|
||||
Ok(_) => panic!("Connecting on bad proxy should fail"),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -543,7 +543,7 @@ pub struct MembershipUpdated {
|
||||
|
||||
/// The result of setting a member's role.
|
||||
#[derive(Debug)]
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
|
||||
pub enum SetMemberRoleResult {
|
||||
InviteUpdated(Channel),
|
||||
MembershipUpdated(MembershipUpdated),
|
||||
|
||||
@@ -36,6 +36,7 @@ use util::{ResultExt as _, maybe};
|
||||
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
const REVISION: Option<&'static str> = option_env!("GITHUB_SHA");
|
||||
|
||||
#[expect(clippy::result_large_err)]
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
if let Err(error) = env::load_dotenv() {
|
||||
|
||||
@@ -36,8 +36,8 @@ fn room_participants(room: &Entity<Room>, cx: &mut TestAppContext) -> RoomPartic
|
||||
room.read_with(cx, |room, _| {
|
||||
let mut remote = room
|
||||
.remote_participants()
|
||||
.iter()
|
||||
.map(|(_, participant)| participant.user.github_login.clone())
|
||||
.values()
|
||||
.map(|participant| participant.user.github_login.clone())
|
||||
.collect::<Vec<_>>();
|
||||
let mut pending = room
|
||||
.pending_participants()
|
||||
|
||||
@@ -7,11 +7,11 @@ use crate::notifications::collab_notification::CollabNotification;
|
||||
pub struct CollabNotificationStory;
|
||||
|
||||
impl Render for CollabNotificationStory {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let window_container = |width, height| div().w(px(width)).h(px(height));
|
||||
|
||||
Story::container()
|
||||
.child(Story::title_for::<CollabNotification>())
|
||||
Story::container(cx)
|
||||
.child(Story::title_for::<CollabNotification>(cx))
|
||||
.child(
|
||||
StorySection::new().child(StoryItem::new(
|
||||
"Incoming Call Notification",
|
||||
|
||||
@@ -21,8 +21,8 @@ use project::{
|
||||
use ui::{
|
||||
App, Clickable, Color, Context, Div, Icon, IconButton, IconName, Indicator, InteractiveElement,
|
||||
IntoElement, Label, LabelCommon, LabelSize, ListItem, ParentElement, Render, RenderOnce,
|
||||
Scrollbar, ScrollbarState, SharedString, StatefulInteractiveElement, Styled, Window, div,
|
||||
h_flex, px, v_flex,
|
||||
Scrollbar, ScrollbarState, SharedString, StatefulInteractiveElement, Styled, Tooltip, Window,
|
||||
div, h_flex, px, v_flex,
|
||||
};
|
||||
use util::{ResultExt, maybe};
|
||||
use workspace::Workspace;
|
||||
@@ -259,6 +259,11 @@ impl LineBreakpoint {
|
||||
dir, name, line
|
||||
)))
|
||||
.cursor_pointer()
|
||||
.tooltip(Tooltip::text(if breakpoint.state.is_enabled() {
|
||||
"Disable Breakpoint"
|
||||
} else {
|
||||
"Enable Breakpoint"
|
||||
}))
|
||||
.on_click({
|
||||
let weak = weak.clone();
|
||||
let path = path.clone();
|
||||
@@ -290,6 +295,9 @@ impl LineBreakpoint {
|
||||
)))
|
||||
.start_slot(indicator)
|
||||
.rounded()
|
||||
.on_secondary_mouse_down(|_, _, cx| {
|
||||
cx.stop_propagation();
|
||||
})
|
||||
.end_hover_slot(
|
||||
IconButton::new(
|
||||
SharedString::from(format!(
|
||||
@@ -423,12 +431,20 @@ impl ExceptionBreakpoint {
|
||||
self.id
|
||||
)))
|
||||
.rounded()
|
||||
.on_secondary_mouse_down(|_, _, cx| {
|
||||
cx.stop_propagation();
|
||||
})
|
||||
.start_slot(
|
||||
div()
|
||||
.id(SharedString::from(format!(
|
||||
"exception-breakpoint-ui-item-{}-click-handler",
|
||||
self.id
|
||||
)))
|
||||
.tooltip(Tooltip::text(if self.is_enabled {
|
||||
"Disable Exception Breakpoint"
|
||||
} else {
|
||||
"Enable Exception Breakpoint"
|
||||
}))
|
||||
.on_click(move |_, _, cx| {
|
||||
list.update(cx, |this, cx| {
|
||||
this.session.update(cx, |this, cx| {
|
||||
|
||||
@@ -129,6 +129,9 @@ impl ModuleList {
|
||||
.w_full()
|
||||
.group("")
|
||||
.id(("module-list", ix))
|
||||
.on_any_mouse_down(|_, _, cx| {
|
||||
cx.stop_propagation();
|
||||
})
|
||||
.when(module.path.is_some(), |this| {
|
||||
this.on_click({
|
||||
let path = module
|
||||
|
||||
@@ -39,7 +39,6 @@ pub struct StackFrameList {
|
||||
_refresh_task: Task<()>,
|
||||
}
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum StackFrameEntry {
|
||||
Normal(dap::StackFrame),
|
||||
@@ -394,6 +393,9 @@ impl StackFrameList {
|
||||
.when(is_selected_frame, |this| {
|
||||
this.bg(cx.theme().colors().element_hover)
|
||||
})
|
||||
.on_any_mouse_down(|_, _, cx| {
|
||||
cx.stop_propagation();
|
||||
})
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
this.selected_ix = Some(ix);
|
||||
this.activate_selected_entry(window, cx);
|
||||
@@ -481,6 +483,9 @@ impl StackFrameList {
|
||||
.when(is_selected, |this| {
|
||||
this.bg(cx.theme().colors().element_hover)
|
||||
})
|
||||
.on_any_mouse_down(|_, _, cx| {
|
||||
cx.stop_propagation();
|
||||
})
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
this.selected_ix = Some(ix);
|
||||
this.activate_selected_entry(window, cx);
|
||||
|
||||
@@ -79,6 +79,7 @@ theme.workspace = true
|
||||
tree-sitter-html = { workspace = true, optional = true }
|
||||
tree-sitter-rust = { workspace = true, optional = true }
|
||||
tree-sitter-typescript = { workspace = true, optional = true }
|
||||
tree-sitter-python = { workspace = true, optional = true }
|
||||
unicode-segmentation.workspace = true
|
||||
unicode-script.workspace = true
|
||||
unindent = { workspace = true, optional = true }
|
||||
|
||||
@@ -40,7 +40,6 @@ 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.);
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub enum CodeContextMenu {
|
||||
Completions(CompletionsMenu),
|
||||
CodeActions(CodeActionsMenu),
|
||||
@@ -928,7 +927,6 @@ impl CodeActionContents {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(Clone)]
|
||||
pub enum CodeActionsItem {
|
||||
Task(TaskSourceKind, ResolvedTask),
|
||||
|
||||
@@ -1026,9 +1026,7 @@ impl DisplaySnapshot {
|
||||
}
|
||||
|
||||
let font_size = editor_style.text.font_size.to_pixels(*rem_size);
|
||||
text_system
|
||||
.layout_line(&line, font_size, &runs)
|
||||
.expect("we expect the font to be loaded because it's rendered by the editor")
|
||||
text_system.layout_line(&line, font_size, &runs)
|
||||
}
|
||||
|
||||
pub fn x_for_display_point(
|
||||
|
||||
@@ -282,7 +282,6 @@ struct Transform {
|
||||
block: Option<Block>,
|
||||
}
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(Clone)]
|
||||
pub enum Block {
|
||||
Custom(Arc<CustomBlock>),
|
||||
|
||||
@@ -107,9 +107,9 @@ pub use items::MAX_TAB_TITLE_LEN;
|
||||
use itertools::Itertools;
|
||||
use language::{
|
||||
AutoindentMode, BracketMatch, BracketPair, Buffer, Capability, CharKind, CodeLabel,
|
||||
CursorShape, DiagnosticEntry, DiffOptions, EditPredictionsMode, EditPreview, HighlightedText,
|
||||
IndentKind, IndentSize, Language, OffsetRangeExt, Point, Selection, SelectionGoal, TextObject,
|
||||
TransactionId, TreeSitterOptions, WordsQuery,
|
||||
CursorShape, DiagnosticEntry, DiffOptions, DocumentationConfig, EditPredictionsMode,
|
||||
EditPreview, HighlightedText, IndentKind, IndentSize, Language, OffsetRangeExt, Point,
|
||||
Selection, SelectionGoal, TextObject, TransactionId, TreeSitterOptions, WordsQuery,
|
||||
language_settings::{
|
||||
self, InlayHintSettings, LspInsertMode, RewrapBehavior, WordsCompletionMode,
|
||||
all_language_settings, language_settings,
|
||||
@@ -1311,7 +1311,7 @@ pub struct ActiveDiagnosticGroup {
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
|
||||
pub(crate) enum ActiveDiagnostic {
|
||||
None,
|
||||
All,
|
||||
@@ -3912,7 +3912,7 @@ impl Editor {
|
||||
pub fn newline(&mut self, _: &Newline, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction);
|
||||
self.transact(window, cx, |this, window, cx| {
|
||||
let (edits, selection_fixup_info): (Vec<_>, Vec<_>) = {
|
||||
let (edits_with_flags, selection_info): (Vec<_>, Vec<_>) = {
|
||||
let selections = this.selections.all::<usize>(cx);
|
||||
let multi_buffer = this.buffer.read(cx);
|
||||
let buffer = multi_buffer.snapshot(cx);
|
||||
@@ -3920,17 +3920,21 @@ impl Editor {
|
||||
.iter()
|
||||
.map(|selection| {
|
||||
let start_point = selection.start.to_point(&buffer);
|
||||
let mut indent =
|
||||
let mut existing_indent =
|
||||
buffer.indent_size_for_line(MultiBufferRow(start_point.row));
|
||||
indent.len = cmp::min(indent.len, start_point.column);
|
||||
existing_indent.len = cmp::min(existing_indent.len, start_point.column);
|
||||
let start = selection.start;
|
||||
let end = selection.end;
|
||||
let selection_is_empty = start == end;
|
||||
let language_scope = buffer.language_scope_at(start);
|
||||
let (comment_delimiter, insert_extra_newline) = if let Some(language) =
|
||||
&language_scope
|
||||
{
|
||||
let insert_extra_newline =
|
||||
let (
|
||||
comment_delimiter,
|
||||
doc_delimiter,
|
||||
insert_extra_newline,
|
||||
indent_on_newline,
|
||||
indent_on_extra_newline,
|
||||
) = if let Some(language) = &language_scope {
|
||||
let mut insert_extra_newline =
|
||||
insert_extra_newline_brackets(&buffer, start..end, language)
|
||||
|| insert_extra_newline_tree_sitter(&buffer, start..end);
|
||||
|
||||
@@ -3950,63 +3954,208 @@ impl Editor {
|
||||
let (snapshot, range) =
|
||||
buffer.buffer_line_for_row(MultiBufferRow(start_point.row))?;
|
||||
|
||||
let mut index_of_first_non_whitespace = 0;
|
||||
let num_of_whitespaces = snapshot
|
||||
.chars_for_range(range.clone())
|
||||
.take_while(|c| c.is_whitespace())
|
||||
.count();
|
||||
let comment_candidate = snapshot
|
||||
.chars_for_range(range)
|
||||
.skip_while(|c| {
|
||||
let should_skip = c.is_whitespace();
|
||||
if should_skip {
|
||||
index_of_first_non_whitespace += 1;
|
||||
}
|
||||
should_skip
|
||||
})
|
||||
.skip(num_of_whitespaces)
|
||||
.take(max_len_of_delimiter)
|
||||
.collect::<String>();
|
||||
let comment_prefix = delimiters.iter().find(|comment_prefix| {
|
||||
comment_candidate.starts_with(comment_prefix.as_ref())
|
||||
})?;
|
||||
let (delimiter, trimmed_len) =
|
||||
delimiters.iter().find_map(|delimiter| {
|
||||
let trimmed = delimiter.trim_end();
|
||||
if comment_candidate.starts_with(trimmed) {
|
||||
Some((delimiter, trimmed.len()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})?;
|
||||
let cursor_is_placed_after_comment_marker =
|
||||
index_of_first_non_whitespace + comment_prefix.len()
|
||||
<= start_point.column as usize;
|
||||
num_of_whitespaces + trimmed_len <= start_point.column as usize;
|
||||
if cursor_is_placed_after_comment_marker {
|
||||
Some(comment_prefix.clone())
|
||||
Some(delimiter.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
(comment_delimiter, insert_extra_newline)
|
||||
|
||||
let mut indent_on_newline = IndentSize::spaces(0);
|
||||
let mut indent_on_extra_newline = IndentSize::spaces(0);
|
||||
|
||||
let doc_delimiter = maybe!({
|
||||
if !selection_is_empty {
|
||||
return None;
|
||||
}
|
||||
|
||||
if !multi_buffer.language_settings(cx).extend_comment_on_newline {
|
||||
return None;
|
||||
}
|
||||
|
||||
let DocumentationConfig {
|
||||
start: start_tag,
|
||||
end: end_tag,
|
||||
prefix: delimiter,
|
||||
tab_size: len,
|
||||
} = language.documentation()?;
|
||||
|
||||
let (snapshot, range) =
|
||||
buffer.buffer_line_for_row(MultiBufferRow(start_point.row))?;
|
||||
|
||||
let num_of_whitespaces = snapshot
|
||||
.chars_for_range(range.clone())
|
||||
.take_while(|c| c.is_whitespace())
|
||||
.count();
|
||||
|
||||
let cursor_is_after_start_tag = {
|
||||
let start_tag_len = start_tag.len();
|
||||
let start_tag_line = snapshot
|
||||
.chars_for_range(range.clone())
|
||||
.skip(num_of_whitespaces)
|
||||
.take(start_tag_len)
|
||||
.collect::<String>();
|
||||
if start_tag_line.starts_with(start_tag.as_ref()) {
|
||||
num_of_whitespaces + start_tag_len
|
||||
<= start_point.column as usize
|
||||
} else {
|
||||
false
|
||||
}
|
||||
};
|
||||
|
||||
let cursor_is_after_delimiter = {
|
||||
let delimiter_trim = delimiter.trim_end();
|
||||
let delimiter_line = snapshot
|
||||
.chars_for_range(range.clone())
|
||||
.skip(num_of_whitespaces)
|
||||
.take(delimiter_trim.len())
|
||||
.collect::<String>();
|
||||
if delimiter_line.starts_with(delimiter_trim) {
|
||||
num_of_whitespaces + delimiter_trim.len()
|
||||
<= start_point.column as usize
|
||||
} else {
|
||||
false
|
||||
}
|
||||
};
|
||||
|
||||
let cursor_is_before_end_tag_if_exists = {
|
||||
let num_of_whitespaces_rev = snapshot
|
||||
.reversed_chars_for_range(range.clone())
|
||||
.take_while(|c| c.is_whitespace())
|
||||
.count();
|
||||
let mut line_iter = snapshot
|
||||
.reversed_chars_for_range(range)
|
||||
.skip(num_of_whitespaces_rev);
|
||||
let end_tag_exists = end_tag
|
||||
.chars()
|
||||
.rev()
|
||||
.all(|char| line_iter.next() == Some(char));
|
||||
if end_tag_exists {
|
||||
let max_point = snapshot.line_len(start_point.row) as usize;
|
||||
let ordering = (num_of_whitespaces_rev
|
||||
+ end_tag.len()
|
||||
+ start_point.column as usize)
|
||||
.cmp(&max_point);
|
||||
let cursor_is_before_end_tag =
|
||||
ordering != Ordering::Greater;
|
||||
if cursor_is_after_start_tag {
|
||||
if cursor_is_before_end_tag {
|
||||
insert_extra_newline = true;
|
||||
}
|
||||
let cursor_is_at_start_of_end_tag =
|
||||
ordering == Ordering::Equal;
|
||||
if cursor_is_at_start_of_end_tag {
|
||||
indent_on_extra_newline.len = (*len).into();
|
||||
}
|
||||
}
|
||||
cursor_is_before_end_tag
|
||||
} else {
|
||||
true
|
||||
}
|
||||
};
|
||||
|
||||
if (cursor_is_after_start_tag || cursor_is_after_delimiter)
|
||||
&& cursor_is_before_end_tag_if_exists
|
||||
{
|
||||
if cursor_is_after_start_tag {
|
||||
indent_on_newline.len = (*len).into();
|
||||
}
|
||||
Some(delimiter.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
(
|
||||
comment_delimiter,
|
||||
doc_delimiter,
|
||||
insert_extra_newline,
|
||||
indent_on_newline,
|
||||
indent_on_extra_newline,
|
||||
)
|
||||
} else {
|
||||
(None, false)
|
||||
(
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
IndentSize::default(),
|
||||
IndentSize::default(),
|
||||
)
|
||||
};
|
||||
|
||||
let capacity_for_delimiter = comment_delimiter
|
||||
.as_deref()
|
||||
.map(str::len)
|
||||
.unwrap_or_default();
|
||||
let mut new_text =
|
||||
String::with_capacity(1 + capacity_for_delimiter + indent.len as usize);
|
||||
let prevent_auto_indent = doc_delimiter.is_some();
|
||||
let delimiter = comment_delimiter.or(doc_delimiter);
|
||||
|
||||
let capacity_for_delimiter =
|
||||
delimiter.as_deref().map(str::len).unwrap_or_default();
|
||||
let mut new_text = String::with_capacity(
|
||||
1 + capacity_for_delimiter
|
||||
+ existing_indent.len as usize
|
||||
+ indent_on_newline.len as usize
|
||||
+ indent_on_extra_newline.len as usize,
|
||||
);
|
||||
new_text.push('\n');
|
||||
new_text.extend(indent.chars());
|
||||
if let Some(delimiter) = &comment_delimiter {
|
||||
new_text.extend(existing_indent.chars());
|
||||
new_text.extend(indent_on_newline.chars());
|
||||
|
||||
if let Some(delimiter) = &delimiter {
|
||||
new_text.push_str(delimiter);
|
||||
}
|
||||
|
||||
if insert_extra_newline {
|
||||
new_text = new_text.repeat(2);
|
||||
new_text.push('\n');
|
||||
new_text.extend(existing_indent.chars());
|
||||
new_text.extend(indent_on_extra_newline.chars());
|
||||
}
|
||||
|
||||
let anchor = buffer.anchor_after(end);
|
||||
let new_selection = selection.map(|_| anchor);
|
||||
(
|
||||
(start..end, new_text),
|
||||
((start..end, new_text), prevent_auto_indent),
|
||||
(insert_extra_newline, new_selection),
|
||||
)
|
||||
})
|
||||
.unzip()
|
||||
};
|
||||
|
||||
this.edit_with_autoindent(edits, cx);
|
||||
let mut auto_indent_edits = Vec::new();
|
||||
let mut edits = Vec::new();
|
||||
for (edit, prevent_auto_indent) in edits_with_flags {
|
||||
if prevent_auto_indent {
|
||||
edits.push(edit);
|
||||
} else {
|
||||
auto_indent_edits.push(edit);
|
||||
}
|
||||
}
|
||||
if !edits.is_empty() {
|
||||
this.edit(edits, cx);
|
||||
}
|
||||
if !auto_indent_edits.is_empty() {
|
||||
this.edit_with_autoindent(auto_indent_edits, cx);
|
||||
}
|
||||
|
||||
let buffer = this.buffer.read(cx).snapshot(cx);
|
||||
let new_selections = selection_fixup_info
|
||||
let new_selections = selection_info
|
||||
.into_iter()
|
||||
.map(|(extra_newline_inserted, new_selection)| {
|
||||
let mut cursor = new_selection.end.to_point(&buffer);
|
||||
@@ -17918,10 +18067,6 @@ impl Editor {
|
||||
.and_then(|lines| lines.last().map(|line| line.range.start));
|
||||
|
||||
self.inline_value_cache.refresh_task = cx.spawn(async move |editor, cx| {
|
||||
let snapshot = editor
|
||||
.update(cx, |editor, cx| editor.buffer().read(cx).snapshot(cx))
|
||||
.ok()?;
|
||||
|
||||
let inline_values = editor
|
||||
.update(cx, |editor, cx| {
|
||||
let Some(current_execution_position) = current_execution_position else {
|
||||
@@ -17949,22 +18094,40 @@ impl Editor {
|
||||
.context("refreshing debugger inlays")
|
||||
.log_err()?;
|
||||
|
||||
let (excerpt_id, buffer_id) = snapshot
|
||||
.excerpts()
|
||||
.next()
|
||||
.map(|excerpt| (excerpt.0, excerpt.1.remote_id()))?;
|
||||
let mut buffer_inline_values: HashMap<BufferId, Vec<InlayHint>> = HashMap::default();
|
||||
|
||||
for (buffer_id, inline_value) in inline_values
|
||||
.into_iter()
|
||||
.filter_map(|hint| Some((hint.position.buffer_id?, hint)))
|
||||
{
|
||||
buffer_inline_values
|
||||
.entry(buffer_id)
|
||||
.or_default()
|
||||
.push(inline_value);
|
||||
}
|
||||
|
||||
editor
|
||||
.update(cx, |editor, cx| {
|
||||
let new_inlays = inline_values
|
||||
.into_iter()
|
||||
.map(|debugger_value| {
|
||||
Inlay::debugger_hint(
|
||||
post_inc(&mut editor.next_inlay_id),
|
||||
Anchor::in_buffer(excerpt_id, buffer_id, debugger_value.position),
|
||||
debugger_value.text(),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let snapshot = editor.buffer.read(cx).snapshot(cx);
|
||||
let mut new_inlays = Vec::default();
|
||||
|
||||
for (excerpt_id, buffer_snapshot, _) in snapshot.excerpts() {
|
||||
let buffer_id = buffer_snapshot.remote_id();
|
||||
buffer_inline_values
|
||||
.get(&buffer_id)
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.for_each(|hint| {
|
||||
let inlay = Inlay::debugger_hint(
|
||||
post_inc(&mut editor.next_inlay_id),
|
||||
Anchor::in_buffer(excerpt_id, buffer_id, hint.position),
|
||||
hint.text(),
|
||||
);
|
||||
|
||||
new_inlays.push(inlay);
|
||||
});
|
||||
}
|
||||
|
||||
let mut inlay_ids = new_inlays.iter().map(|inlay| inlay.id).collect();
|
||||
std::mem::swap(&mut editor.inline_value_cache.inlays, &mut inlay_ids);
|
||||
|
||||
@@ -20188,8 +20351,8 @@ impl EditorSnapshot {
|
||||
let participant_indices = collaboration_hub.user_participant_indices(cx);
|
||||
let collaborators_by_peer_id = collaboration_hub.collaborators(cx);
|
||||
let collaborators_by_replica_id = collaborators_by_peer_id
|
||||
.iter()
|
||||
.map(|(_, collaborator)| (collaborator.replica_id, collaborator))
|
||||
.values()
|
||||
.map(|collaborator| (collaborator.replica_id, collaborator))
|
||||
.collect::<HashMap<_, _>>();
|
||||
self.buffer_snapshot
|
||||
.selections_in_range(range, false)
|
||||
|
||||
@@ -26,6 +26,7 @@ use language::{
|
||||
AllLanguageSettings, AllLanguageSettingsContent, CompletionSettings,
|
||||
LanguageSettingsContent, LspInsertMode, PrettierSettings,
|
||||
},
|
||||
tree_sitter_python,
|
||||
};
|
||||
use language_settings::{Formatter, FormatterList, IndentGuideSettings};
|
||||
use lsp::CompletionParams;
|
||||
@@ -2755,7 +2756,7 @@ async fn test_newline_comments(cx: &mut TestAppContext) {
|
||||
|
||||
let language = Arc::new(Language::new(
|
||||
LanguageConfig {
|
||||
line_comments: vec!["//".into()],
|
||||
line_comments: vec!["// ".into()],
|
||||
..LanguageConfig::default()
|
||||
},
|
||||
None,
|
||||
@@ -2770,7 +2771,29 @@ async fn test_newline_comments(cx: &mut TestAppContext) {
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.assert_editor_state(indoc! {"
|
||||
// Foo
|
||||
// ˇ
|
||||
"});
|
||||
// Ensure that we add comment prefix when existing line contains space
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.assert_editor_state(
|
||||
indoc! {"
|
||||
// Foo
|
||||
//s
|
||||
// ˇ
|
||||
"}
|
||||
.replace("s", " ") // s is used as space placeholder to prevent format on save
|
||||
.as_str(),
|
||||
);
|
||||
// Ensure that we add comment prefix when existing line does not contain space
|
||||
cx.set_state(indoc! {"
|
||||
// Foo
|
||||
//ˇ
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.assert_editor_state(indoc! {"
|
||||
// Foo
|
||||
//
|
||||
// ˇ
|
||||
"});
|
||||
// Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
|
||||
cx.set_state(indoc! {"
|
||||
@@ -2797,6 +2820,177 @@ async fn test_newline_comments(cx: &mut TestAppContext) {
|
||||
"});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_newline_documentation_comments(cx: &mut TestAppContext) {
|
||||
init_test(cx, |settings| {
|
||||
settings.defaults.tab_size = NonZeroU32::new(4)
|
||||
});
|
||||
|
||||
let language = Arc::new(Language::new(
|
||||
LanguageConfig {
|
||||
documentation: Some(language::DocumentationConfig {
|
||||
start: "/**".into(),
|
||||
end: "*/".into(),
|
||||
prefix: "* ".into(),
|
||||
tab_size: NonZeroU32::new(1).unwrap(),
|
||||
}),
|
||||
..LanguageConfig::default()
|
||||
},
|
||||
None,
|
||||
));
|
||||
{
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
|
||||
cx.set_state(indoc! {"
|
||||
/**ˇ
|
||||
"});
|
||||
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.assert_editor_state(indoc! {"
|
||||
/**
|
||||
* ˇ
|
||||
"});
|
||||
// Ensure that if cursor is before the comment start,
|
||||
// we do not actually insert a comment prefix.
|
||||
cx.set_state(indoc! {"
|
||||
ˇ/**
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.assert_editor_state(indoc! {"
|
||||
|
||||
ˇ/**
|
||||
"});
|
||||
// Ensure that if cursor is between it doesn't add comment prefix.
|
||||
cx.set_state(indoc! {"
|
||||
/*ˇ*
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.assert_editor_state(indoc! {"
|
||||
/*
|
||||
ˇ*
|
||||
"});
|
||||
// Ensure that if suffix exists on same line after cursor it adds new line.
|
||||
cx.set_state(indoc! {"
|
||||
/**ˇ*/
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.assert_editor_state(indoc! {"
|
||||
/**
|
||||
* ˇ
|
||||
*/
|
||||
"});
|
||||
// Ensure that if suffix exists on same line after cursor with space it adds new line.
|
||||
cx.set_state(indoc! {"
|
||||
/**ˇ */
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.assert_editor_state(indoc! {"
|
||||
/**
|
||||
* ˇ
|
||||
*/
|
||||
"});
|
||||
// Ensure that if suffix exists on same line after cursor with space it adds new line.
|
||||
cx.set_state(indoc! {"
|
||||
/** ˇ*/
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.assert_editor_state(
|
||||
indoc! {"
|
||||
/**s
|
||||
* ˇ
|
||||
*/
|
||||
"}
|
||||
.replace("s", " ") // s is used as space placeholder to prevent format on save
|
||||
.as_str(),
|
||||
);
|
||||
// Ensure that delimiter space is preserved when newline on already
|
||||
// spaced delimiter.
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.assert_editor_state(
|
||||
indoc! {"
|
||||
/**s
|
||||
*s
|
||||
* ˇ
|
||||
*/
|
||||
"}
|
||||
.replace("s", " ") // s is used as space placeholder to prevent format on save
|
||||
.as_str(),
|
||||
);
|
||||
// Ensure that delimiter space is preserved when space is not
|
||||
// on existing delimiter.
|
||||
cx.set_state(indoc! {"
|
||||
/**
|
||||
*ˇ
|
||||
*/
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.assert_editor_state(indoc! {"
|
||||
/**
|
||||
*
|
||||
* ˇ
|
||||
*/
|
||||
"});
|
||||
// Ensure that if suffix exists on same line after cursor it
|
||||
// doesn't add extra new line if prefix is not on same line.
|
||||
cx.set_state(indoc! {"
|
||||
/**
|
||||
ˇ*/
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.assert_editor_state(indoc! {"
|
||||
/**
|
||||
|
||||
ˇ*/
|
||||
"});
|
||||
// Ensure that it detects suffix after existing prefix.
|
||||
cx.set_state(indoc! {"
|
||||
/**ˇ/
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.assert_editor_state(indoc! {"
|
||||
/**
|
||||
ˇ/
|
||||
"});
|
||||
// Ensure that if suffix exists on same line before
|
||||
// cursor it does not add comment prefix.
|
||||
cx.set_state(indoc! {"
|
||||
/** */ˇ
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.assert_editor_state(indoc! {"
|
||||
/** */
|
||||
ˇ
|
||||
"});
|
||||
// Ensure that if suffix exists on same line before
|
||||
// cursor it does not add comment prefix.
|
||||
cx.set_state(indoc! {"
|
||||
/**
|
||||
*
|
||||
*/ˇ
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.assert_editor_state(indoc! {"
|
||||
/**
|
||||
*
|
||||
*/
|
||||
ˇ
|
||||
"});
|
||||
}
|
||||
// Ensure that comment continuations can be disabled.
|
||||
update_test_language_settings(cx, |settings| {
|
||||
settings.defaults.extend_comment_on_newline = Some(false);
|
||||
});
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
cx.set_state(indoc! {"
|
||||
/**ˇ
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.assert_editor_state(indoc! {"
|
||||
/**
|
||||
ˇ
|
||||
"});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_insert_with_old_selections(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
@@ -20017,6 +20211,330 @@ async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
|
||||
cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
|
||||
|
||||
// test cursor move to start of each line on tab
|
||||
// for `if`, `elif`, `else`, `while`, `with` and `for`
|
||||
cx.set_state(indoc! {"
|
||||
def main():
|
||||
ˇ for item in items:
|
||||
ˇ while item.active:
|
||||
ˇ if item.value > 10:
|
||||
ˇ continue
|
||||
ˇ elif item.value < 0:
|
||||
ˇ break
|
||||
ˇ else:
|
||||
ˇ with item.context() as ctx:
|
||||
ˇ yield count
|
||||
ˇ else:
|
||||
ˇ log('while else')
|
||||
ˇ else:
|
||||
ˇ log('for else')
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
|
||||
cx.assert_editor_state(indoc! {"
|
||||
def main():
|
||||
ˇfor item in items:
|
||||
ˇwhile item.active:
|
||||
ˇif item.value > 10:
|
||||
ˇcontinue
|
||||
ˇelif item.value < 0:
|
||||
ˇbreak
|
||||
ˇelse:
|
||||
ˇwith item.context() as ctx:
|
||||
ˇyield count
|
||||
ˇelse:
|
||||
ˇlog('while else')
|
||||
ˇelse:
|
||||
ˇlog('for else')
|
||||
"});
|
||||
// test relative indent is preserved when tab
|
||||
// for `if`, `elif`, `else`, `while`, `with` and `for`
|
||||
cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
|
||||
cx.assert_editor_state(indoc! {"
|
||||
def main():
|
||||
ˇfor item in items:
|
||||
ˇwhile item.active:
|
||||
ˇif item.value > 10:
|
||||
ˇcontinue
|
||||
ˇelif item.value < 0:
|
||||
ˇbreak
|
||||
ˇelse:
|
||||
ˇwith item.context() as ctx:
|
||||
ˇyield count
|
||||
ˇelse:
|
||||
ˇlog('while else')
|
||||
ˇelse:
|
||||
ˇlog('for else')
|
||||
"});
|
||||
|
||||
// test cursor move to start of each line on tab
|
||||
// for `try`, `except`, `else`, `finally`, `match` and `def`
|
||||
cx.set_state(indoc! {"
|
||||
def main():
|
||||
ˇ try:
|
||||
ˇ fetch()
|
||||
ˇ except ValueError:
|
||||
ˇ handle_error()
|
||||
ˇ else:
|
||||
ˇ match value:
|
||||
ˇ case _:
|
||||
ˇ finally:
|
||||
ˇ def status():
|
||||
ˇ return 0
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
|
||||
cx.assert_editor_state(indoc! {"
|
||||
def main():
|
||||
ˇtry:
|
||||
ˇfetch()
|
||||
ˇexcept ValueError:
|
||||
ˇhandle_error()
|
||||
ˇelse:
|
||||
ˇmatch value:
|
||||
ˇcase _:
|
||||
ˇfinally:
|
||||
ˇdef status():
|
||||
ˇreturn 0
|
||||
"});
|
||||
// test relative indent is preserved when tab
|
||||
// for `try`, `except`, `else`, `finally`, `match` and `def`
|
||||
cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
|
||||
cx.assert_editor_state(indoc! {"
|
||||
def main():
|
||||
ˇtry:
|
||||
ˇfetch()
|
||||
ˇexcept ValueError:
|
||||
ˇhandle_error()
|
||||
ˇelse:
|
||||
ˇmatch value:
|
||||
ˇcase _:
|
||||
ˇfinally:
|
||||
ˇdef status():
|
||||
ˇreturn 0
|
||||
"});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
|
||||
cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
|
||||
|
||||
// test `else` auto outdents when typed inside `if` block
|
||||
cx.set_state(indoc! {"
|
||||
def main():
|
||||
if i == 2:
|
||||
return
|
||||
ˇ
|
||||
"});
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.handle_input("else:", window, cx);
|
||||
});
|
||||
cx.assert_editor_state(indoc! {"
|
||||
def main():
|
||||
if i == 2:
|
||||
return
|
||||
else:ˇ
|
||||
"});
|
||||
|
||||
// test `except` auto outdents when typed inside `try` block
|
||||
cx.set_state(indoc! {"
|
||||
def main():
|
||||
try:
|
||||
i = 2
|
||||
ˇ
|
||||
"});
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.handle_input("except:", window, cx);
|
||||
});
|
||||
cx.assert_editor_state(indoc! {"
|
||||
def main():
|
||||
try:
|
||||
i = 2
|
||||
except:ˇ
|
||||
"});
|
||||
|
||||
// test `else` auto outdents when typed inside `except` block
|
||||
cx.set_state(indoc! {"
|
||||
def main():
|
||||
try:
|
||||
i = 2
|
||||
except:
|
||||
j = 2
|
||||
ˇ
|
||||
"});
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.handle_input("else:", window, cx);
|
||||
});
|
||||
cx.assert_editor_state(indoc! {"
|
||||
def main():
|
||||
try:
|
||||
i = 2
|
||||
except:
|
||||
j = 2
|
||||
else:ˇ
|
||||
"});
|
||||
|
||||
// test `finally` auto outdents when typed inside `else` block
|
||||
cx.set_state(indoc! {"
|
||||
def main():
|
||||
try:
|
||||
i = 2
|
||||
except:
|
||||
j = 2
|
||||
else:
|
||||
k = 2
|
||||
ˇ
|
||||
"});
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.handle_input("finally:", window, cx);
|
||||
});
|
||||
cx.assert_editor_state(indoc! {"
|
||||
def main():
|
||||
try:
|
||||
i = 2
|
||||
except:
|
||||
j = 2
|
||||
else:
|
||||
k = 2
|
||||
finally:ˇ
|
||||
"});
|
||||
|
||||
// TODO: test `except` auto outdents when typed inside `try` block right after for block
|
||||
// cx.set_state(indoc! {"
|
||||
// def main():
|
||||
// try:
|
||||
// for i in range(n):
|
||||
// pass
|
||||
// ˇ
|
||||
// "});
|
||||
// cx.update_editor(|editor, window, cx| {
|
||||
// editor.handle_input("except:", window, cx);
|
||||
// });
|
||||
// cx.assert_editor_state(indoc! {"
|
||||
// def main():
|
||||
// try:
|
||||
// for i in range(n):
|
||||
// pass
|
||||
// except:ˇ
|
||||
// "});
|
||||
|
||||
// TODO: test `else` auto outdents when typed inside `except` block right after for block
|
||||
// cx.set_state(indoc! {"
|
||||
// def main():
|
||||
// try:
|
||||
// i = 2
|
||||
// except:
|
||||
// for i in range(n):
|
||||
// pass
|
||||
// ˇ
|
||||
// "});
|
||||
// cx.update_editor(|editor, window, cx| {
|
||||
// editor.handle_input("else:", window, cx);
|
||||
// });
|
||||
// cx.assert_editor_state(indoc! {"
|
||||
// def main():
|
||||
// try:
|
||||
// i = 2
|
||||
// except:
|
||||
// for i in range(n):
|
||||
// pass
|
||||
// else:ˇ
|
||||
// "});
|
||||
|
||||
// TODO: test `finally` auto outdents when typed inside `else` block right after for block
|
||||
// cx.set_state(indoc! {"
|
||||
// def main():
|
||||
// try:
|
||||
// i = 2
|
||||
// except:
|
||||
// j = 2
|
||||
// else:
|
||||
// for i in range(n):
|
||||
// pass
|
||||
// ˇ
|
||||
// "});
|
||||
// cx.update_editor(|editor, window, cx| {
|
||||
// editor.handle_input("finally:", window, cx);
|
||||
// });
|
||||
// cx.assert_editor_state(indoc! {"
|
||||
// def main():
|
||||
// try:
|
||||
// i = 2
|
||||
// except:
|
||||
// j = 2
|
||||
// else:
|
||||
// for i in range(n):
|
||||
// pass
|
||||
// finally:ˇ
|
||||
// "});
|
||||
|
||||
// test `else` stays at correct indent when typed after `for` block
|
||||
cx.set_state(indoc! {"
|
||||
def main():
|
||||
for i in range(10):
|
||||
if i == 3:
|
||||
break
|
||||
ˇ
|
||||
"});
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.handle_input("else:", window, cx);
|
||||
});
|
||||
cx.assert_editor_state(indoc! {"
|
||||
def main():
|
||||
for i in range(10):
|
||||
if i == 3:
|
||||
break
|
||||
else:ˇ
|
||||
"});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
update_test_language_settings(cx, |settings| {
|
||||
settings.defaults.extend_comment_on_newline = Some(false);
|
||||
});
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
|
||||
cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
|
||||
|
||||
// test correct indent after newline on comment
|
||||
cx.set_state(indoc! {"
|
||||
# COMMENT:ˇ
|
||||
"});
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.newline(&Newline, window, cx);
|
||||
});
|
||||
cx.assert_editor_state(indoc! {"
|
||||
# COMMENT:
|
||||
ˇ
|
||||
"});
|
||||
|
||||
// test correct indent after newline in curly brackets
|
||||
cx.set_state(indoc! {"
|
||||
{ˇ}
|
||||
"});
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.newline(&Newline, window, cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
cx.assert_editor_state(indoc! {"
|
||||
{
|
||||
ˇ
|
||||
}
|
||||
"});
|
||||
}
|
||||
|
||||
fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
|
||||
let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
|
||||
point..point
|
||||
|
||||
@@ -1343,7 +1343,7 @@ impl EditorElement {
|
||||
None
|
||||
}
|
||||
})
|
||||
.and_then(|text| {
|
||||
.map(|text| {
|
||||
let len = text.len();
|
||||
|
||||
let font = cursor_row_layout
|
||||
@@ -1369,21 +1369,18 @@ impl EditorElement {
|
||||
cx.theme().colors().editor_background
|
||||
};
|
||||
|
||||
window
|
||||
.text_system()
|
||||
.shape_line(
|
||||
text,
|
||||
cursor_row_layout.font_size,
|
||||
&[TextRun {
|
||||
len,
|
||||
font,
|
||||
color,
|
||||
background_color: None,
|
||||
strikethrough: None,
|
||||
underline: None,
|
||||
}],
|
||||
)
|
||||
.log_err()
|
||||
window.text_system().shape_line(
|
||||
text,
|
||||
cursor_row_layout.font_size,
|
||||
&[TextRun {
|
||||
len,
|
||||
font,
|
||||
color,
|
||||
background_color: None,
|
||||
strikethrough: None,
|
||||
underline: None,
|
||||
}],
|
||||
)
|
||||
})
|
||||
} else {
|
||||
None
|
||||
@@ -2690,9 +2687,8 @@ impl EditorElement {
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|| cx.theme().colors().editor_line_number);
|
||||
let shaped_line = self
|
||||
.shape_line_number(SharedString::from(&line_number), color, window)
|
||||
.log_err()?;
|
||||
let shaped_line =
|
||||
self.shape_line_number(SharedString::from(&line_number), color, window);
|
||||
let scroll_top = scroll_position.y * line_height;
|
||||
let line_origin = gutter_hitbox.map(|hitbox| {
|
||||
hitbox.origin
|
||||
@@ -2808,7 +2804,7 @@ impl EditorElement {
|
||||
.chain(iter::repeat(""))
|
||||
.take(rows.len());
|
||||
placeholder_lines
|
||||
.filter_map(move |line| {
|
||||
.map(move |line| {
|
||||
let run = TextRun {
|
||||
len: line.len(),
|
||||
font: style.text.font(),
|
||||
@@ -2817,17 +2813,17 @@ impl EditorElement {
|
||||
underline: None,
|
||||
strikethrough: None,
|
||||
};
|
||||
window
|
||||
.text_system()
|
||||
.shape_line(line.to_string().into(), font_size, &[run])
|
||||
.log_err()
|
||||
})
|
||||
.map(|line| LineWithInvisibles {
|
||||
width: line.width,
|
||||
len: line.len,
|
||||
fragments: smallvec![LineFragment::Text(line)],
|
||||
invisibles: Vec::new(),
|
||||
font_size,
|
||||
let line =
|
||||
window
|
||||
.text_system()
|
||||
.shape_line(line.to_string().into(), font_size, &[run]);
|
||||
LineWithInvisibles {
|
||||
width: line.width,
|
||||
len: line.len,
|
||||
fragments: smallvec![LineFragment::Text(line)],
|
||||
invisibles: Vec::new(),
|
||||
font_size,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
@@ -4764,13 +4760,7 @@ impl EditorElement {
|
||||
let Some(()) = (if !is_singleton && hitbox.is_hovered(window) {
|
||||
let color = cx.theme().colors().editor_hover_line_number;
|
||||
|
||||
let Some(line) = self
|
||||
.shape_line_number(shaped_line.text.clone(), color, window)
|
||||
.log_err()
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let line = self.shape_line_number(shaped_line.text.clone(), color, window);
|
||||
line.paint(hitbox.origin, line_height, window, cx).log_err()
|
||||
} else {
|
||||
shaped_line
|
||||
@@ -6137,21 +6127,18 @@ impl EditorElement {
|
||||
fn column_pixels(&self, column: usize, window: &mut Window, _: &mut App) -> Pixels {
|
||||
let style = &self.style;
|
||||
let font_size = style.text.font_size.to_pixels(window.rem_size());
|
||||
let layout = window
|
||||
.text_system()
|
||||
.shape_line(
|
||||
SharedString::from(" ".repeat(column)),
|
||||
font_size,
|
||||
&[TextRun {
|
||||
len: column,
|
||||
font: style.text.font(),
|
||||
color: Hsla::default(),
|
||||
background_color: None,
|
||||
underline: None,
|
||||
strikethrough: None,
|
||||
}],
|
||||
)
|
||||
.unwrap();
|
||||
let layout = window.text_system().shape_line(
|
||||
SharedString::from(" ".repeat(column)),
|
||||
font_size,
|
||||
&[TextRun {
|
||||
len: column,
|
||||
font: style.text.font(),
|
||||
color: Hsla::default(),
|
||||
background_color: None,
|
||||
underline: None,
|
||||
strikethrough: None,
|
||||
}],
|
||||
);
|
||||
|
||||
layout.width
|
||||
}
|
||||
@@ -6171,7 +6158,7 @@ impl EditorElement {
|
||||
text: SharedString,
|
||||
color: Hsla,
|
||||
window: &mut Window,
|
||||
) -> anyhow::Result<ShapedLine> {
|
||||
) -> ShapedLine {
|
||||
let run = TextRun {
|
||||
len: text.len(),
|
||||
font: self.style.text.font(),
|
||||
@@ -6393,7 +6380,6 @@ pub(crate) struct LineWithInvisibles {
|
||||
font_size: Pixels,
|
||||
}
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
enum LineFragment {
|
||||
Text(ShapedLine),
|
||||
Element {
|
||||
@@ -6452,10 +6438,10 @@ impl LineWithInvisibles {
|
||||
}]) {
|
||||
if let Some(replacement) = highlighted_chunk.replacement {
|
||||
if !line.is_empty() {
|
||||
let shaped_line = window
|
||||
.text_system()
|
||||
.shape_line(line.clone().into(), font_size, &styles)
|
||||
.unwrap();
|
||||
let shaped_line =
|
||||
window
|
||||
.text_system()
|
||||
.shape_line(line.clone().into(), font_size, &styles);
|
||||
width += shaped_line.width;
|
||||
len += shaped_line.len;
|
||||
fragments.push(LineFragment::Text(shaped_line));
|
||||
@@ -6471,14 +6457,11 @@ impl LineWithInvisibles {
|
||||
} else {
|
||||
SharedString::from(Arc::from(highlighted_chunk.text))
|
||||
};
|
||||
let shaped_line = window
|
||||
.text_system()
|
||||
.shape_line(
|
||||
chunk,
|
||||
font_size,
|
||||
&[text_style.to_run(highlighted_chunk.text.len())],
|
||||
)
|
||||
.unwrap();
|
||||
let shaped_line = window.text_system().shape_line(
|
||||
chunk,
|
||||
font_size,
|
||||
&[text_style.to_run(highlighted_chunk.text.len())],
|
||||
);
|
||||
AvailableSpace::Definite(shaped_line.width)
|
||||
} else {
|
||||
AvailableSpace::MinContent
|
||||
@@ -6523,7 +6506,6 @@ impl LineWithInvisibles {
|
||||
let line_layout = window
|
||||
.text_system()
|
||||
.shape_line(x, font_size, &[run])
|
||||
.unwrap()
|
||||
.with_len(highlighted_chunk.text.len());
|
||||
|
||||
width += line_layout.width;
|
||||
@@ -6534,10 +6516,11 @@ impl LineWithInvisibles {
|
||||
} else {
|
||||
for (ix, mut line_chunk) in highlighted_chunk.text.split('\n').enumerate() {
|
||||
if ix > 0 {
|
||||
let shaped_line = window
|
||||
.text_system()
|
||||
.shape_line(line.clone().into(), font_size, &styles)
|
||||
.unwrap();
|
||||
let shaped_line = window.text_system().shape_line(
|
||||
line.clone().into(),
|
||||
font_size,
|
||||
&styles,
|
||||
);
|
||||
width += shaped_line.width;
|
||||
len += shaped_line.len;
|
||||
fragments.push(LineFragment::Text(shaped_line));
|
||||
@@ -8039,36 +8022,30 @@ impl Element for EditorElement {
|
||||
});
|
||||
|
||||
let invisible_symbol_font_size = font_size / 2.;
|
||||
let tab_invisible = window
|
||||
.text_system()
|
||||
.shape_line(
|
||||
"→".into(),
|
||||
invisible_symbol_font_size,
|
||||
&[TextRun {
|
||||
len: "→".len(),
|
||||
font: self.style.text.font(),
|
||||
color: cx.theme().colors().editor_invisible,
|
||||
background_color: None,
|
||||
underline: None,
|
||||
strikethrough: None,
|
||||
}],
|
||||
)
|
||||
.unwrap();
|
||||
let space_invisible = window
|
||||
.text_system()
|
||||
.shape_line(
|
||||
"•".into(),
|
||||
invisible_symbol_font_size,
|
||||
&[TextRun {
|
||||
len: "•".len(),
|
||||
font: self.style.text.font(),
|
||||
color: cx.theme().colors().editor_invisible,
|
||||
background_color: None,
|
||||
underline: None,
|
||||
strikethrough: None,
|
||||
}],
|
||||
)
|
||||
.unwrap();
|
||||
let tab_invisible = window.text_system().shape_line(
|
||||
"→".into(),
|
||||
invisible_symbol_font_size,
|
||||
&[TextRun {
|
||||
len: "→".len(),
|
||||
font: self.style.text.font(),
|
||||
color: cx.theme().colors().editor_invisible,
|
||||
background_color: None,
|
||||
underline: None,
|
||||
strikethrough: None,
|
||||
}],
|
||||
);
|
||||
let space_invisible = window.text_system().shape_line(
|
||||
"•".into(),
|
||||
invisible_symbol_font_size,
|
||||
&[TextRun {
|
||||
len: "•".len(),
|
||||
font: self.style.text.font(),
|
||||
color: cx.theme().colors().editor_invisible,
|
||||
background_color: None,
|
||||
underline: None,
|
||||
strikethrough: None,
|
||||
}],
|
||||
);
|
||||
|
||||
let mode = snapshot.mode.clone();
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ serde_json.workspace = true
|
||||
task.workspace = true
|
||||
toml.workspace = true
|
||||
util.workspace = true
|
||||
wasi-preview1-component-adapter-provider.workspace = true
|
||||
wasm-encoder.workspace = true
|
||||
wasmparser.workspace = true
|
||||
wit-component.workspace = true
|
||||
|
||||
@@ -4,7 +4,6 @@ use crate::{
|
||||
use anyhow::{Context as _, Result, anyhow, bail};
|
||||
use async_compression::futures::bufread::GzipDecoder;
|
||||
use async_tar::Archive;
|
||||
use futures::AsyncReadExt;
|
||||
use futures::io::BufReader;
|
||||
use heck::ToSnakeCase;
|
||||
use http_client::{self, AsyncBody, HttpClient};
|
||||
@@ -15,6 +14,7 @@ use std::{
|
||||
process::Stdio,
|
||||
sync::Arc,
|
||||
};
|
||||
use wasi_preview1_component_adapter_provider::WASI_SNAPSHOT_PREVIEW1_REACTOR_ADAPTER;
|
||||
use wasm_encoder::{ComponentSectionId, Encode as _, RawSection, Section as _};
|
||||
use wasmparser::Parser;
|
||||
use wit_component::ComponentEncoder;
|
||||
@@ -26,7 +26,6 @@ use wit_component::ComponentEncoder;
|
||||
/// Once Rust 1.78 is released, there will be a `wasm32-wasip2` target available, so we will
|
||||
/// not need the adapter anymore.
|
||||
const RUST_TARGET: &str = "wasm32-wasip1";
|
||||
const WASI_ADAPTER_URL: &str = "https://github.com/bytecodealliance/wasmtime/releases/download/v18.0.2/wasi_snapshot_preview1.reactor.wasm";
|
||||
|
||||
/// Compiling Tree-sitter parsers from C to WASM requires Clang 17, and a WASM build of libc
|
||||
/// and clang's runtime library. The `wasi-sdk` provides these binaries.
|
||||
@@ -137,7 +136,6 @@ impl ExtensionBuilder {
|
||||
options: CompileExtensionOptions,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
self.install_rust_wasm_target_if_needed()?;
|
||||
let adapter_bytes = self.install_wasi_preview1_adapter_if_needed().await?;
|
||||
|
||||
let cargo_toml_content = fs::read_to_string(extension_dir.join("Cargo.toml"))?;
|
||||
let cargo_toml: CargoToml = toml::from_str(&cargo_toml_content)?;
|
||||
@@ -186,7 +184,10 @@ impl ExtensionBuilder {
|
||||
|
||||
let mut encoder = ComponentEncoder::default()
|
||||
.module(&wasm_bytes)?
|
||||
.adapter("wasi_snapshot_preview1", &adapter_bytes)
|
||||
.adapter(
|
||||
"wasi_snapshot_preview1",
|
||||
WASI_SNAPSHOT_PREVIEW1_REACTOR_ADAPTER,
|
||||
)
|
||||
.context("failed to load adapter module")?
|
||||
.validate(true);
|
||||
|
||||
@@ -395,38 +396,6 @@ impl ExtensionBuilder {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn install_wasi_preview1_adapter_if_needed(&self) -> Result<Vec<u8>> {
|
||||
let cache_path = self.cache_dir.join("wasi_snapshot_preview1.reactor.wasm");
|
||||
if let Ok(content) = fs::read(&cache_path) {
|
||||
if Parser::is_core_wasm(&content) {
|
||||
return Ok(content);
|
||||
}
|
||||
}
|
||||
|
||||
fs::remove_file(&cache_path).ok();
|
||||
|
||||
log::info!(
|
||||
"downloading wasi adapter module to {}",
|
||||
cache_path.display()
|
||||
);
|
||||
let mut response = self
|
||||
.http
|
||||
.get(WASI_ADAPTER_URL, AsyncBody::default(), true)
|
||||
.await?;
|
||||
|
||||
let mut content = Vec::new();
|
||||
let mut body = BufReader::new(response.body_mut());
|
||||
body.read_to_end(&mut content).await?;
|
||||
|
||||
fs::write(&cache_path, &content)
|
||||
.with_context(|| format!("failed to save file {}", cache_path.display()))?;
|
||||
|
||||
if !Parser::is_core_wasm(&content) {
|
||||
bail!("downloaded wasi adapter is invalid");
|
||||
}
|
||||
Ok(content)
|
||||
}
|
||||
|
||||
async fn install_wasi_sdk_if_needed(&self) -> Result<PathBuf> {
|
||||
let url = if let Some(asset_name) = WASI_SDK_ASSET_NAME {
|
||||
format!("{WASI_SDK_URL}/{asset_name}")
|
||||
|
||||
@@ -91,6 +91,12 @@ impl FeatureFlag for ThreadAutoCaptureFeatureFlag {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct JjUiFeatureFlag {}
|
||||
|
||||
impl FeatureFlag for JjUiFeatureFlag {
|
||||
const NAME: &'static str = "jj-ui";
|
||||
}
|
||||
|
||||
pub trait FeatureFlagViewExt<V: 'static> {
|
||||
fn observe_flag<T: FeatureFlag, F>(&mut self, window: &Window, callback: F) -> Subscription
|
||||
where
|
||||
|
||||
@@ -324,7 +324,8 @@ fn update_conflict_highlighting(
|
||||
cx: &mut Context<Editor>,
|
||||
) {
|
||||
log::debug!("update conflict highlighting for {conflict:?}");
|
||||
|
||||
let theme = cx.theme().clone();
|
||||
let colors = theme.colors();
|
||||
let outer_start = buffer
|
||||
.anchor_in_excerpt(excerpt_id, conflict.range.start)
|
||||
.unwrap();
|
||||
@@ -344,9 +345,11 @@ fn update_conflict_highlighting(
|
||||
.anchor_in_excerpt(excerpt_id, conflict.theirs.end)
|
||||
.unwrap();
|
||||
|
||||
let ours_background = cx.theme().colors().version_control_conflict_marker_ours;
|
||||
let theirs_background = cx.theme().colors().version_control_conflict_marker_theirs;
|
||||
let divider_background = cx.theme().colors().version_control_conflict_marker_border;
|
||||
let ours_background = colors.version_control_conflict_ours_background;
|
||||
let ours_marker = colors.version_control_conflict_ours_marker_background;
|
||||
let theirs_background = colors.version_control_conflict_theirs_background;
|
||||
let theirs_marker = colors.version_control_conflict_theirs_marker_background;
|
||||
let divider_background = colors.version_control_conflict_divider_background;
|
||||
|
||||
let options = RowHighlightOptions {
|
||||
include_gutter: false,
|
||||
@@ -354,14 +357,14 @@ fn update_conflict_highlighting(
|
||||
};
|
||||
|
||||
// Prevent diff hunk highlighting within the entire conflict region.
|
||||
editor.highlight_rows::<ConflictsOuter>(outer_start..outer_end, theirs_background, options, cx);
|
||||
editor.highlight_rows::<ConflictsOurs>(our_start..our_end, ours_background, options, cx);
|
||||
editor.highlight_rows::<ConflictsOursMarker>(
|
||||
outer_start..our_start,
|
||||
ours_background,
|
||||
editor.highlight_rows::<ConflictsOuter>(
|
||||
outer_start..outer_end,
|
||||
divider_background,
|
||||
options,
|
||||
cx,
|
||||
);
|
||||
editor.highlight_rows::<ConflictsOurs>(our_start..our_end, ours_background, options, cx);
|
||||
editor.highlight_rows::<ConflictsOursMarker>(outer_start..our_start, ours_marker, options, cx);
|
||||
editor.highlight_rows::<ConflictsTheirs>(
|
||||
their_start..their_end,
|
||||
theirs_background,
|
||||
@@ -370,7 +373,7 @@ fn update_conflict_highlighting(
|
||||
);
|
||||
editor.highlight_rows::<ConflictsTheirsMarker>(
|
||||
their_end..outer_end,
|
||||
theirs_background,
|
||||
theirs_marker,
|
||||
options,
|
||||
cx,
|
||||
);
|
||||
|
||||
@@ -257,6 +257,10 @@ path = "examples/image/image.rs"
|
||||
name = "input"
|
||||
path = "examples/input.rs"
|
||||
|
||||
[[example]]
|
||||
name = "on_window_close_quit"
|
||||
path = "examples/on_window_close_quit.rs"
|
||||
|
||||
[[example]]
|
||||
name = "opacity"
|
||||
path = "examples/opacity.rs"
|
||||
@@ -277,6 +281,10 @@ path = "examples/shadow.rs"
|
||||
name = "svg"
|
||||
path = "examples/svg/svg.rs"
|
||||
|
||||
[[example]]
|
||||
name = "text"
|
||||
path = "examples/text.rs"
|
||||
|
||||
[[example]]
|
||||
name = "text_wrapper"
|
||||
path = "examples/text_wrapper.rs"
|
||||
@@ -288,7 +296,3 @@ path = "examples/uniform_list.rs"
|
||||
[[example]]
|
||||
name = "window_shadow"
|
||||
path = "examples/window_shadow.rs"
|
||||
|
||||
[[example]]
|
||||
name = "on_window_close_quit"
|
||||
path = "examples/on_window_close_quit.rs"
|
||||
|
||||
@@ -481,8 +481,7 @@ impl Element for TextElement {
|
||||
let font_size = style.font_size.to_pixels(window.rem_size());
|
||||
let line = window
|
||||
.text_system()
|
||||
.shape_line(display_text, font_size, &runs)
|
||||
.unwrap();
|
||||
.shape_line(display_text, font_size, &runs);
|
||||
|
||||
let cursor_pos = line.x_for_index(cursor);
|
||||
let (selection, cursor) = if selected_range.is_empty() {
|
||||
|
||||
333
crates/gpui/examples/text.rs
Normal file
333
crates/gpui/examples/text.rs
Normal file
@@ -0,0 +1,333 @@
|
||||
use std::{
|
||||
ops::{Deref, DerefMut},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use gpui::{
|
||||
AbsoluteLength, App, Application, Context, DefiniteLength, ElementId, Global, Hsla, Menu,
|
||||
SharedString, TextStyle, TitlebarOptions, Window, WindowBounds, WindowOptions, bounds,
|
||||
colors::DefaultColors, div, point, prelude::*, px, relative, rgb, size,
|
||||
};
|
||||
use std::iter;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct TextContext {
|
||||
font_size: f32,
|
||||
line_height: f32,
|
||||
type_scale: f32,
|
||||
}
|
||||
|
||||
impl Default for TextContext {
|
||||
fn default() -> Self {
|
||||
TextContext {
|
||||
font_size: 16.0,
|
||||
line_height: 1.3,
|
||||
type_scale: 1.33,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TextContext {
|
||||
pub fn get_global(cx: &App) -> &Arc<TextContext> {
|
||||
&cx.global::<GlobalTextContext>().0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct GlobalTextContext(pub Arc<TextContext>);
|
||||
|
||||
impl Deref for GlobalTextContext {
|
||||
type Target = Arc<TextContext>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for GlobalTextContext {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Global for GlobalTextContext {}
|
||||
|
||||
pub trait ActiveTextContext {
|
||||
fn text_context(&self) -> &Arc<TextContext>;
|
||||
}
|
||||
|
||||
impl ActiveTextContext for App {
|
||||
fn text_context(&self) -> &Arc<TextContext> {
|
||||
&self.global::<GlobalTextContext>().0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct SpecimenTheme {
|
||||
pub bg: Hsla,
|
||||
pub fg: Hsla,
|
||||
}
|
||||
|
||||
impl Default for SpecimenTheme {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
bg: gpui::white(),
|
||||
fg: gpui::black(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SpecimenTheme {
|
||||
pub fn invert(&self) -> Self {
|
||||
Self {
|
||||
bg: self.fg,
|
||||
fg: self.bg,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, IntoElement)]
|
||||
struct Specimen {
|
||||
id: ElementId,
|
||||
scale: f32,
|
||||
text_style: Option<TextStyle>,
|
||||
string: SharedString,
|
||||
invert: bool,
|
||||
}
|
||||
|
||||
impl Specimen {
|
||||
pub fn new(id: usize) -> Self {
|
||||
let string = SharedString::new_static("The quick brown fox jumps over the lazy dog");
|
||||
let id_string = format!("specimen-{}", id);
|
||||
let id = ElementId::Name(id_string.into());
|
||||
Self {
|
||||
id,
|
||||
scale: 1.0,
|
||||
text_style: None,
|
||||
string,
|
||||
invert: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn invert(mut self) -> Self {
|
||||
self.invert = !self.invert;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn scale(mut self, scale: f32) -> Self {
|
||||
self.scale = scale;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for Specimen {
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let rem_size = window.rem_size();
|
||||
let scale = self.scale;
|
||||
let global_style = cx.text_context();
|
||||
|
||||
let style_override = self.text_style;
|
||||
|
||||
let mut font_size = global_style.font_size;
|
||||
let mut line_height = global_style.line_height;
|
||||
|
||||
if let Some(style_override) = style_override {
|
||||
font_size = style_override.font_size.to_pixels(rem_size).0;
|
||||
line_height = match style_override.line_height {
|
||||
DefiniteLength::Absolute(absolute_len) => match absolute_len {
|
||||
AbsoluteLength::Rems(absolute_len) => absolute_len.to_pixels(rem_size).0,
|
||||
AbsoluteLength::Pixels(absolute_len) => absolute_len.0,
|
||||
},
|
||||
DefiniteLength::Fraction(value) => value,
|
||||
};
|
||||
}
|
||||
|
||||
let mut theme = SpecimenTheme::default();
|
||||
|
||||
if self.invert {
|
||||
theme = theme.invert();
|
||||
}
|
||||
|
||||
div()
|
||||
.id(self.id)
|
||||
.bg(theme.bg)
|
||||
.text_color(theme.fg)
|
||||
.text_size(px(font_size * scale))
|
||||
.line_height(relative(line_height))
|
||||
.p(px(10.0))
|
||||
.child(self.string.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, IntoElement)]
|
||||
struct CharacterGrid {
|
||||
scale: f32,
|
||||
invert: bool,
|
||||
text_style: Option<TextStyle>,
|
||||
}
|
||||
|
||||
impl CharacterGrid {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
scale: 1.0,
|
||||
invert: false,
|
||||
text_style: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn scale(mut self, scale: f32) -> Self {
|
||||
self.scale = scale;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for CharacterGrid {
|
||||
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
|
||||
let mut theme = SpecimenTheme::default();
|
||||
|
||||
if self.invert {
|
||||
theme = theme.invert();
|
||||
}
|
||||
|
||||
let characters = vec![
|
||||
"1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "A", "B", "C", "D", "E", "F", "G",
|
||||
"H", "I", "J", "K", "L", "M", "N", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y",
|
||||
"Z", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "p", "q",
|
||||
"r", "s", "t", "u", "v", "w", "x", "y", "z", "ẞ", "ſ", "ß", "ð", "Þ", "þ", "α", "β",
|
||||
"Γ", "γ", "Δ", "δ", "η", "θ", "ι", "κ", "Λ", "λ", "μ", "ν", "ξ", "π", "τ", "υ", "φ",
|
||||
"χ", "ψ", "∂", "а", "в", "Ж", "ж", "З", "з", "К", "к", "л", "м", "Н", "н", "Р", "р",
|
||||
"У", "у", "ф", "ч", "ь", "ы", "Э", "э", "Я", "я", "ij", "öẋ", ".,", "⣝⣑", "~", "*",
|
||||
"_", "^", "`", "'", "(", "{", "«", "#", "&", "@", "$", "¢", "%", "|", "?", "¶", "µ",
|
||||
"❮", "<=", "!=", "==", "--", "++", "=>", "->",
|
||||
];
|
||||
|
||||
let columns = 11;
|
||||
let rows = characters.len().div_ceil(columns);
|
||||
|
||||
let grid_rows = (0..rows).map(|row_idx| {
|
||||
let start_idx = row_idx * columns;
|
||||
let end_idx = (start_idx + columns).min(characters.len());
|
||||
|
||||
div()
|
||||
.w_full()
|
||||
.flex()
|
||||
.flex_row()
|
||||
.children((start_idx..end_idx).map(|i| {
|
||||
div()
|
||||
.text_center()
|
||||
.size(px(62.))
|
||||
.bg(theme.bg)
|
||||
.text_color(theme.fg)
|
||||
.text_size(px(24.0))
|
||||
.line_height(relative(1.0))
|
||||
.child(characters[i])
|
||||
}))
|
||||
.when(end_idx - start_idx < columns, |d| {
|
||||
d.children(
|
||||
iter::repeat_with(|| div().flex_1()).take(columns - (end_idx - start_idx)),
|
||||
)
|
||||
})
|
||||
});
|
||||
|
||||
div().p_4().gap_2().flex().flex_col().children(grid_rows)
|
||||
}
|
||||
}
|
||||
|
||||
struct TextExample {
|
||||
next_id: usize,
|
||||
}
|
||||
|
||||
impl TextExample {
|
||||
fn next_id(&mut self) -> usize {
|
||||
self.next_id += 1;
|
||||
self.next_id
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for TextExample {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let tcx = cx.text_context();
|
||||
let colors = cx.default_colors().clone();
|
||||
|
||||
let type_scale = tcx.type_scale;
|
||||
|
||||
let step_down_2 = 1.0 / (type_scale * type_scale);
|
||||
let step_down_1 = 1.0 / type_scale;
|
||||
let base = 1.0;
|
||||
let step_up_1 = base * type_scale;
|
||||
let step_up_2 = step_up_1 * type_scale;
|
||||
let step_up_3 = step_up_2 * type_scale;
|
||||
let step_up_4 = step_up_3 * type_scale;
|
||||
let step_up_5 = step_up_4 * type_scale;
|
||||
let step_up_6 = step_up_5 * type_scale;
|
||||
|
||||
div()
|
||||
.size_full()
|
||||
.child(
|
||||
div()
|
||||
.id("text-example")
|
||||
.overflow_y_scroll()
|
||||
.overflow_x_hidden()
|
||||
.bg(rgb(0xffffff))
|
||||
.size_full()
|
||||
.child(div().child(CharacterGrid::new().scale(base)))
|
||||
.child(
|
||||
div()
|
||||
.child(Specimen::new(self.next_id()).scale(step_down_2))
|
||||
.child(Specimen::new(self.next_id()).scale(step_down_2).invert())
|
||||
.child(Specimen::new(self.next_id()).scale(step_down_1))
|
||||
.child(Specimen::new(self.next_id()).scale(step_down_1).invert())
|
||||
.child(Specimen::new(self.next_id()).scale(base))
|
||||
.child(Specimen::new(self.next_id()).scale(base).invert())
|
||||
.child(Specimen::new(self.next_id()).scale(step_up_1))
|
||||
.child(Specimen::new(self.next_id()).scale(step_up_1).invert())
|
||||
.child(Specimen::new(self.next_id()).scale(step_up_2))
|
||||
.child(Specimen::new(self.next_id()).scale(step_up_2).invert())
|
||||
.child(Specimen::new(self.next_id()).scale(step_up_3))
|
||||
.child(Specimen::new(self.next_id()).scale(step_up_3).invert())
|
||||
.child(Specimen::new(self.next_id()).scale(step_up_4))
|
||||
.child(Specimen::new(self.next_id()).scale(step_up_4).invert())
|
||||
.child(Specimen::new(self.next_id()).scale(step_up_5))
|
||||
.child(Specimen::new(self.next_id()).scale(step_up_5).invert())
|
||||
.child(Specimen::new(self.next_id()).scale(step_up_6))
|
||||
.child(Specimen::new(self.next_id()).scale(step_up_6).invert()),
|
||||
),
|
||||
)
|
||||
.child(div().w(px(240.)).h_full().bg(colors.container))
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
Application::new().run(|cx: &mut App| {
|
||||
cx.set_menus(vec![Menu {
|
||||
name: "GPUI Typography".into(),
|
||||
items: vec![],
|
||||
}]);
|
||||
|
||||
cx.init_colors();
|
||||
cx.set_global(GlobalTextContext(Arc::new(TextContext::default())));
|
||||
|
||||
let window = cx
|
||||
.open_window(
|
||||
WindowOptions {
|
||||
titlebar: Some(TitlebarOptions {
|
||||
title: Some("GPUI Typography".into()),
|
||||
..Default::default()
|
||||
}),
|
||||
window_bounds: Some(WindowBounds::Windowed(bounds(
|
||||
point(px(0.0), px(0.0)),
|
||||
size(px(920.), px(720.)),
|
||||
))),
|
||||
..Default::default()
|
||||
},
|
||||
|_window, cx| cx.new(|_cx| TextExample { next_id: 0 }),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
window
|
||||
.update(cx, |_view, _window, cx| {
|
||||
cx.activate(true);
|
||||
})
|
||||
.unwrap();
|
||||
});
|
||||
}
|
||||
@@ -38,7 +38,9 @@ use crate::{
|
||||
PlatformDisplay, PlatformKeyboardLayout, Point, PromptBuilder, PromptHandle, PromptLevel,
|
||||
Render, RenderImage, RenderablePromptHandle, Reservation, ScreenCaptureSource, SharedString,
|
||||
SubscriberSet, Subscription, SvgRenderer, Task, TextSystem, Window, WindowAppearance,
|
||||
WindowHandle, WindowId, WindowInvalidator, current_platform, hash, init_app_menus,
|
||||
WindowHandle, WindowId, WindowInvalidator,
|
||||
colors::{Colors, GlobalColors},
|
||||
current_platform, hash, init_app_menus,
|
||||
};
|
||||
|
||||
mod async_context;
|
||||
@@ -1656,6 +1658,13 @@ impl App {
|
||||
_ = window.drop_image(image);
|
||||
}
|
||||
}
|
||||
|
||||
/// Initializes gpui's default colors for the application.
|
||||
///
|
||||
/// These colors can be accessed through `cx.default_colors()`.
|
||||
pub fn init_colors(&mut self) {
|
||||
self.set_global(GlobalColors(Arc::new(Colors::default())));
|
||||
}
|
||||
}
|
||||
|
||||
impl AppContext for App {
|
||||
|
||||
122
crates/gpui/src/colors.rs
Normal file
122
crates/gpui/src/colors.rs
Normal file
@@ -0,0 +1,122 @@
|
||||
use crate::{App, Global, Rgba, Window, WindowAppearance, rgb};
|
||||
use std::ops::Deref;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// The default set of colors for gpui.
|
||||
///
|
||||
/// These are used for styling base components, examples and more.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Colors {
|
||||
/// Text color
|
||||
pub text: Rgba,
|
||||
/// Selected text color
|
||||
pub selected_text: Rgba,
|
||||
/// Background color
|
||||
pub background: Rgba,
|
||||
/// Disabled color
|
||||
pub disabled: Rgba,
|
||||
/// Selected color
|
||||
pub selected: Rgba,
|
||||
/// Border color
|
||||
pub border: Rgba,
|
||||
/// Separator color
|
||||
pub separator: Rgba,
|
||||
/// Container color
|
||||
pub container: Rgba,
|
||||
}
|
||||
|
||||
impl Default for Colors {
|
||||
fn default() -> Self {
|
||||
Self::light()
|
||||
}
|
||||
}
|
||||
|
||||
impl Colors {
|
||||
/// Returns the default colors for the given window appearance.
|
||||
pub fn for_appearance(window: &Window) -> Self {
|
||||
match window.appearance() {
|
||||
WindowAppearance::Light | WindowAppearance::VibrantLight => Self::light(),
|
||||
WindowAppearance::Dark | WindowAppearance::VibrantDark => Self::dark(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the default dark colors.
|
||||
pub fn dark() -> Self {
|
||||
Self {
|
||||
text: rgb(0xffffff),
|
||||
selected_text: rgb(0xffffff),
|
||||
disabled: rgb(0x565656),
|
||||
selected: rgb(0x2457ca),
|
||||
background: rgb(0x222222),
|
||||
border: rgb(0x000000),
|
||||
separator: rgb(0xd9d9d9),
|
||||
container: rgb(0x262626),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the default light colors.
|
||||
pub fn light() -> Self {
|
||||
Self {
|
||||
text: rgb(0x252525),
|
||||
selected_text: rgb(0xffffff),
|
||||
background: rgb(0xffffff),
|
||||
disabled: rgb(0xb0b0b0),
|
||||
selected: rgb(0x2a63d9),
|
||||
border: rgb(0xd9d9d9),
|
||||
separator: rgb(0xe6e6e6),
|
||||
container: rgb(0xf4f5f5),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get [Colors] from the global state
|
||||
pub fn get_global(cx: &App) -> &Arc<Colors> {
|
||||
&cx.global::<GlobalColors>().0
|
||||
}
|
||||
}
|
||||
|
||||
/// Get [Colors] from the global state
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct GlobalColors(pub Arc<Colors>);
|
||||
|
||||
impl Deref for GlobalColors {
|
||||
type Target = Arc<Colors>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Global for GlobalColors {}
|
||||
|
||||
/// Implement this trait to allow global [Color] access via `cx.default_colors()`.
|
||||
pub trait DefaultColors {
|
||||
/// Returns the default [`gpui::Colors`]
|
||||
fn default_colors(&self) -> &Arc<Colors>;
|
||||
}
|
||||
|
||||
impl DefaultColors for App {
|
||||
fn default_colors(&self) -> &Arc<Colors> {
|
||||
&self.global::<GlobalColors>().0
|
||||
}
|
||||
}
|
||||
|
||||
/// The appearance of the base GPUI colors, used to style GPUI elements
|
||||
///
|
||||
/// Varies based on the system's current [`WindowAppearance`].
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum DefaultAppearance {
|
||||
/// Use the set of colors for light appearances.
|
||||
#[default]
|
||||
Light,
|
||||
/// Use the set of colors for dark appearances.
|
||||
Dark,
|
||||
}
|
||||
|
||||
impl From<WindowAppearance> for DefaultAppearance {
|
||||
fn from(appearance: WindowAppearance) -> Self {
|
||||
match appearance {
|
||||
WindowAppearance::Light | WindowAppearance::VibrantLight => Self::Light,
|
||||
WindowAppearance::Dark | WindowAppearance::VibrantDark => Self::Dark,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
use crate::{Hsla, Rgba, WindowAppearance, rgb};
|
||||
|
||||
/// The appearance of the base GPUI colors, used to style GPUI elements
|
||||
///
|
||||
/// Varies based on the system's current [`WindowAppearance`].
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum DefaultThemeAppearance {
|
||||
/// Use the set of colors for light appearances.
|
||||
#[default]
|
||||
Light,
|
||||
/// Use the set of colors for dark appearances.
|
||||
Dark,
|
||||
}
|
||||
|
||||
impl From<WindowAppearance> for DefaultThemeAppearance {
|
||||
fn from(appearance: WindowAppearance) -> Self {
|
||||
match appearance {
|
||||
WindowAppearance::Light | WindowAppearance::VibrantLight => Self::Light,
|
||||
WindowAppearance::Dark | WindowAppearance::VibrantDark => Self::Dark,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the default colors for the given appearance.
|
||||
pub fn colors(appearance: DefaultThemeAppearance) -> DefaultColors {
|
||||
match appearance {
|
||||
DefaultThemeAppearance::Light => DefaultColors::light(),
|
||||
DefaultThemeAppearance::Dark => DefaultColors::dark(),
|
||||
}
|
||||
}
|
||||
|
||||
/// A collection of colors.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct DefaultColors {
|
||||
text: Rgba,
|
||||
selected_text: Rgba,
|
||||
background: Rgba,
|
||||
disabled: Rgba,
|
||||
selected: Rgba,
|
||||
border: Rgba,
|
||||
separator: Rgba,
|
||||
container: Rgba,
|
||||
}
|
||||
|
||||
impl DefaultColors {
|
||||
/// Returns the default dark colors.
|
||||
pub fn dark() -> Self {
|
||||
Self {
|
||||
text: rgb(0xffffff),
|
||||
selected_text: rgb(0xffffff),
|
||||
disabled: rgb(0x565656),
|
||||
selected: rgb(0x2457ca),
|
||||
background: rgb(0x222222),
|
||||
border: rgb(0x000000),
|
||||
separator: rgb(0xd9d9d9),
|
||||
container: rgb(0x262626),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the default light colors.
|
||||
pub fn light() -> Self {
|
||||
Self {
|
||||
text: rgb(0x252525),
|
||||
selected_text: rgb(0xffffff),
|
||||
background: rgb(0xffffff),
|
||||
disabled: rgb(0xb0b0b0),
|
||||
selected: rgb(0x2a63d9),
|
||||
border: rgb(0xd9d9d9),
|
||||
separator: rgb(0xe6e6e6),
|
||||
container: rgb(0xf4f5f5),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A default GPUI color.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, strum::EnumIter)]
|
||||
pub enum DefaultColor {
|
||||
/// Text color
|
||||
Text,
|
||||
/// Selected text color
|
||||
SelectedText,
|
||||
/// Background color
|
||||
Background,
|
||||
/// Disabled color
|
||||
Disabled,
|
||||
/// Selected color
|
||||
Selected,
|
||||
/// Border color
|
||||
Border,
|
||||
/// Separator color
|
||||
Separator,
|
||||
/// Container color
|
||||
Container,
|
||||
}
|
||||
|
||||
impl DefaultColor {
|
||||
/// Returns the RGBA color for the given color type.
|
||||
pub fn color(&self, colors: &DefaultColors) -> Rgba {
|
||||
match self {
|
||||
DefaultColor::Text => colors.text,
|
||||
DefaultColor::SelectedText => colors.selected_text,
|
||||
DefaultColor::Background => colors.background,
|
||||
DefaultColor::Disabled => colors.disabled,
|
||||
DefaultColor::Selected => colors.selected,
|
||||
DefaultColor::Border => colors.border,
|
||||
DefaultColor::Separator => colors.separator,
|
||||
DefaultColor::Container => colors.container,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the HSLA color for the given color type.
|
||||
pub fn hsla(&self, colors: &DefaultColors) -> Hsla {
|
||||
self.color(colors).into()
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
mod anchored;
|
||||
mod animation;
|
||||
mod canvas;
|
||||
mod common;
|
||||
mod deferred;
|
||||
mod div;
|
||||
mod image_cache;
|
||||
@@ -15,7 +14,6 @@ mod uniform_list;
|
||||
pub use anchored::*;
|
||||
pub use animation::*;
|
||||
pub use canvas::*;
|
||||
pub use common::*;
|
||||
pub use deferred::*;
|
||||
pub use div::*;
|
||||
pub use image_cache::*;
|
||||
|
||||
@@ -73,6 +73,8 @@ mod asset_cache;
|
||||
mod assets;
|
||||
mod bounds_tree;
|
||||
mod color;
|
||||
/// The default colors used by GPUI.
|
||||
pub mod colors;
|
||||
mod element;
|
||||
mod elements;
|
||||
mod executor;
|
||||
|
||||
@@ -426,8 +426,8 @@ impl<P: LinuxClient + 'static> Platform for P {
|
||||
|
||||
fn app_path(&self) -> Result<PathBuf> {
|
||||
// get the path of the executable of the current process
|
||||
let exe_path = env::current_exe()?;
|
||||
Ok(exe_path)
|
||||
let app_path = env::current_exe()?;
|
||||
return Ok(app_path);
|
||||
}
|
||||
|
||||
fn set_menus(&self, menus: Vec<Menu>, _keymap: &Keymap) {
|
||||
|
||||
@@ -983,7 +983,7 @@ impl Clipboard {
|
||||
// format that the contents can be converted to
|
||||
format_atoms[0..IMAGE_FORMAT_COUNT].copy_from_slice(&image_format_atoms);
|
||||
format_atoms[IMAGE_FORMAT_COUNT..].copy_from_slice(&text_format_atoms);
|
||||
debug_assert!(!format_atoms.iter().any(|&a| a == atom_none));
|
||||
debug_assert!(!format_atoms.contains(&atom_none));
|
||||
|
||||
let result = self.inner.read(&format_atoms, selection)?;
|
||||
|
||||
|
||||
@@ -137,10 +137,7 @@ fn add_recent_folders(
|
||||
let tasks: IObjectCollection =
|
||||
CoCreateInstance(&EnumerableObjectCollection, None, CLSCTX_INPROC_SERVER)?;
|
||||
|
||||
for folder_path in entries
|
||||
.iter()
|
||||
.filter(|path| !is_item_in_array(path, removed))
|
||||
{
|
||||
for folder_path in entries.iter().filter(|path| !removed.contains(path)) {
|
||||
let argument = HSTRING::from(
|
||||
folder_path
|
||||
.iter()
|
||||
@@ -181,11 +178,6 @@ fn add_recent_folders(
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn is_item_in_array(item: &SmallVec<[PathBuf; 2]>, removed: &Vec<SmallVec<[PathBuf; 2]>>) -> bool {
|
||||
removed.iter().any(|removed_item| removed_item == item)
|
||||
}
|
||||
|
||||
fn create_shell_link(
|
||||
argument: HSTRING,
|
||||
description: HSTRING,
|
||||
|
||||
@@ -293,37 +293,35 @@ fn handle_mouse_move_msg(
|
||||
start_tracking_mouse(handle, &state_ptr, TME_LEAVE);
|
||||
|
||||
let mut lock = state_ptr.state.borrow_mut();
|
||||
if let Some(mut callback) = lock.callbacks.input.take() {
|
||||
let scale_factor = lock.scale_factor;
|
||||
drop(lock);
|
||||
let pressed_button = match MODIFIERKEYS_FLAGS(wparam.loword() as u32) {
|
||||
flags if flags.contains(MK_LBUTTON) => Some(MouseButton::Left),
|
||||
flags if flags.contains(MK_RBUTTON) => Some(MouseButton::Right),
|
||||
flags if flags.contains(MK_MBUTTON) => Some(MouseButton::Middle),
|
||||
flags if flags.contains(MK_XBUTTON1) => {
|
||||
Some(MouseButton::Navigate(NavigationDirection::Back))
|
||||
}
|
||||
flags if flags.contains(MK_XBUTTON2) => {
|
||||
Some(MouseButton::Navigate(NavigationDirection::Forward))
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
let x = lparam.signed_loword() as f32;
|
||||
let y = lparam.signed_hiword() as f32;
|
||||
let event = MouseMoveEvent {
|
||||
position: logical_point(x, y, scale_factor),
|
||||
pressed_button,
|
||||
modifiers: current_modifiers(),
|
||||
};
|
||||
let result = if callback(PlatformInput::MouseMove(event)).default_prevented {
|
||||
Some(0)
|
||||
} else {
|
||||
Some(1)
|
||||
};
|
||||
state_ptr.state.borrow_mut().callbacks.input = Some(callback);
|
||||
return result;
|
||||
}
|
||||
Some(1)
|
||||
let Some(mut func) = lock.callbacks.input.take() else {
|
||||
return Some(1);
|
||||
};
|
||||
let scale_factor = lock.scale_factor;
|
||||
drop(lock);
|
||||
|
||||
let pressed_button = match MODIFIERKEYS_FLAGS(wparam.loword() as u32) {
|
||||
flags if flags.contains(MK_LBUTTON) => Some(MouseButton::Left),
|
||||
flags if flags.contains(MK_RBUTTON) => Some(MouseButton::Right),
|
||||
flags if flags.contains(MK_MBUTTON) => Some(MouseButton::Middle),
|
||||
flags if flags.contains(MK_XBUTTON1) => {
|
||||
Some(MouseButton::Navigate(NavigationDirection::Back))
|
||||
}
|
||||
flags if flags.contains(MK_XBUTTON2) => {
|
||||
Some(MouseButton::Navigate(NavigationDirection::Forward))
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
let x = lparam.signed_loword() as f32;
|
||||
let y = lparam.signed_hiword() as f32;
|
||||
let input = PlatformInput::MouseMove(MouseMoveEvent {
|
||||
position: logical_point(x, y, scale_factor),
|
||||
pressed_button,
|
||||
modifiers: current_modifiers(),
|
||||
});
|
||||
let handled = !func(input).propagate;
|
||||
state_ptr.state.borrow_mut().callbacks.input = Some(func);
|
||||
|
||||
if handled { Some(0) } else { Some(1) }
|
||||
}
|
||||
|
||||
fn handle_mouse_leave_msg(state_ptr: Rc<WindowsWindowStatePtr>) -> Option<isize> {
|
||||
@@ -439,14 +437,10 @@ fn handle_keyup_msg(
|
||||
};
|
||||
drop(lock);
|
||||
|
||||
let result = if func(input).default_prevented {
|
||||
Some(0)
|
||||
} else {
|
||||
Some(1)
|
||||
};
|
||||
let handled = !func(input).propagate;
|
||||
state_ptr.state.borrow_mut().callbacks.input = Some(func);
|
||||
|
||||
result
|
||||
if handled { Some(0) } else { Some(1) }
|
||||
}
|
||||
|
||||
fn handle_char_msg(wparam: WPARAM, state_ptr: Rc<WindowsWindowStatePtr>) -> Option<isize> {
|
||||
@@ -479,32 +473,27 @@ fn handle_mouse_down_msg(
|
||||
) -> Option<isize> {
|
||||
unsafe { SetCapture(handle) };
|
||||
let mut lock = state_ptr.state.borrow_mut();
|
||||
if let Some(mut callback) = lock.callbacks.input.take() {
|
||||
let x = lparam.signed_loword() as f32;
|
||||
let y = lparam.signed_hiword() as f32;
|
||||
let physical_point = point(DevicePixels(x as i32), DevicePixels(y as i32));
|
||||
let click_count = lock.click_state.update(button, physical_point);
|
||||
let scale_factor = lock.scale_factor;
|
||||
drop(lock);
|
||||
let Some(mut func) = lock.callbacks.input.take() else {
|
||||
return Some(1);
|
||||
};
|
||||
let x = lparam.signed_loword();
|
||||
let y = lparam.signed_hiword();
|
||||
let physical_point = point(DevicePixels(x as i32), DevicePixels(y as i32));
|
||||
let click_count = lock.click_state.update(button, physical_point);
|
||||
let scale_factor = lock.scale_factor;
|
||||
drop(lock);
|
||||
|
||||
let event = MouseDownEvent {
|
||||
button,
|
||||
position: logical_point(x, y, scale_factor),
|
||||
modifiers: current_modifiers(),
|
||||
click_count,
|
||||
first_mouse: false,
|
||||
};
|
||||
let result = if callback(PlatformInput::MouseDown(event)).default_prevented {
|
||||
Some(0)
|
||||
} else {
|
||||
Some(1)
|
||||
};
|
||||
state_ptr.state.borrow_mut().callbacks.input = Some(callback);
|
||||
let input = PlatformInput::MouseDown(MouseDownEvent {
|
||||
button,
|
||||
position: logical_point(x as f32, y as f32, scale_factor),
|
||||
modifiers: current_modifiers(),
|
||||
click_count,
|
||||
first_mouse: false,
|
||||
});
|
||||
let handled = !func(input).propagate;
|
||||
state_ptr.state.borrow_mut().callbacks.input = Some(func);
|
||||
|
||||
result
|
||||
} else {
|
||||
Some(1)
|
||||
}
|
||||
if handled { Some(0) } else { Some(1) }
|
||||
}
|
||||
|
||||
fn handle_mouse_up_msg(
|
||||
@@ -515,30 +504,25 @@ fn handle_mouse_up_msg(
|
||||
) -> Option<isize> {
|
||||
unsafe { ReleaseCapture().log_err() };
|
||||
let mut lock = state_ptr.state.borrow_mut();
|
||||
if let Some(mut callback) = lock.callbacks.input.take() {
|
||||
let x = lparam.signed_loword() as f32;
|
||||
let y = lparam.signed_hiword() as f32;
|
||||
let click_count = lock.click_state.current_count;
|
||||
let scale_factor = lock.scale_factor;
|
||||
drop(lock);
|
||||
let Some(mut func) = lock.callbacks.input.take() else {
|
||||
return Some(1);
|
||||
};
|
||||
let x = lparam.signed_loword() as f32;
|
||||
let y = lparam.signed_hiword() as f32;
|
||||
let click_count = lock.click_state.current_count;
|
||||
let scale_factor = lock.scale_factor;
|
||||
drop(lock);
|
||||
|
||||
let event = MouseUpEvent {
|
||||
button,
|
||||
position: logical_point(x, y, scale_factor),
|
||||
modifiers: current_modifiers(),
|
||||
click_count,
|
||||
};
|
||||
let result = if callback(PlatformInput::MouseUp(event)).default_prevented {
|
||||
Some(0)
|
||||
} else {
|
||||
Some(1)
|
||||
};
|
||||
state_ptr.state.borrow_mut().callbacks.input = Some(callback);
|
||||
let input = PlatformInput::MouseUp(MouseUpEvent {
|
||||
button,
|
||||
position: logical_point(x, y, scale_factor),
|
||||
modifiers: current_modifiers(),
|
||||
click_count,
|
||||
});
|
||||
let handled = !func(input).propagate;
|
||||
state_ptr.state.borrow_mut().callbacks.input = Some(func);
|
||||
|
||||
result
|
||||
} else {
|
||||
Some(1)
|
||||
}
|
||||
if handled { Some(0) } else { Some(1) }
|
||||
}
|
||||
|
||||
fn handle_xbutton_msg(
|
||||
@@ -564,46 +548,42 @@ fn handle_mouse_wheel_msg(
|
||||
) -> Option<isize> {
|
||||
let modifiers = current_modifiers();
|
||||
let mut lock = state_ptr.state.borrow_mut();
|
||||
if let Some(mut callback) = lock.callbacks.input.take() {
|
||||
let scale_factor = lock.scale_factor;
|
||||
let wheel_scroll_amount = match modifiers.shift {
|
||||
true => lock.system_settings.mouse_wheel_settings.wheel_scroll_chars,
|
||||
false => lock.system_settings.mouse_wheel_settings.wheel_scroll_lines,
|
||||
};
|
||||
drop(lock);
|
||||
let wheel_distance =
|
||||
(wparam.signed_hiword() as f32 / WHEEL_DELTA as f32) * wheel_scroll_amount as f32;
|
||||
let mut cursor_point = POINT {
|
||||
x: lparam.signed_loword().into(),
|
||||
y: lparam.signed_hiword().into(),
|
||||
};
|
||||
unsafe { ScreenToClient(handle, &mut cursor_point).ok().log_err() };
|
||||
let event = ScrollWheelEvent {
|
||||
position: logical_point(cursor_point.x as f32, cursor_point.y as f32, scale_factor),
|
||||
delta: ScrollDelta::Lines(match modifiers.shift {
|
||||
true => Point {
|
||||
x: wheel_distance,
|
||||
y: 0.0,
|
||||
},
|
||||
false => Point {
|
||||
y: wheel_distance,
|
||||
x: 0.0,
|
||||
},
|
||||
}),
|
||||
modifiers: current_modifiers(),
|
||||
touch_phase: TouchPhase::Moved,
|
||||
};
|
||||
let result = if callback(PlatformInput::ScrollWheel(event)).default_prevented {
|
||||
Some(0)
|
||||
} else {
|
||||
Some(1)
|
||||
};
|
||||
state_ptr.state.borrow_mut().callbacks.input = Some(callback);
|
||||
let Some(mut func) = lock.callbacks.input.take() else {
|
||||
return Some(1);
|
||||
};
|
||||
let scale_factor = lock.scale_factor;
|
||||
let wheel_scroll_amount = match modifiers.shift {
|
||||
true => lock.system_settings.mouse_wheel_settings.wheel_scroll_chars,
|
||||
false => lock.system_settings.mouse_wheel_settings.wheel_scroll_lines,
|
||||
};
|
||||
drop(lock);
|
||||
|
||||
result
|
||||
} else {
|
||||
Some(1)
|
||||
}
|
||||
let wheel_distance =
|
||||
(wparam.signed_hiword() as f32 / WHEEL_DELTA as f32) * wheel_scroll_amount as f32;
|
||||
let mut cursor_point = POINT {
|
||||
x: lparam.signed_loword().into(),
|
||||
y: lparam.signed_hiword().into(),
|
||||
};
|
||||
unsafe { ScreenToClient(handle, &mut cursor_point).ok().log_err() };
|
||||
let input = PlatformInput::ScrollWheel(ScrollWheelEvent {
|
||||
position: logical_point(cursor_point.x as f32, cursor_point.y as f32, scale_factor),
|
||||
delta: ScrollDelta::Lines(match modifiers.shift {
|
||||
true => Point {
|
||||
x: wheel_distance,
|
||||
y: 0.0,
|
||||
},
|
||||
false => Point {
|
||||
y: wheel_distance,
|
||||
x: 0.0,
|
||||
},
|
||||
}),
|
||||
modifiers,
|
||||
touch_phase: TouchPhase::Moved,
|
||||
});
|
||||
let handled = !func(input).propagate;
|
||||
state_ptr.state.borrow_mut().callbacks.input = Some(func);
|
||||
|
||||
if handled { Some(0) } else { Some(1) }
|
||||
}
|
||||
|
||||
fn handle_mouse_horizontal_wheel_msg(
|
||||
@@ -613,37 +593,33 @@ fn handle_mouse_horizontal_wheel_msg(
|
||||
state_ptr: Rc<WindowsWindowStatePtr>,
|
||||
) -> Option<isize> {
|
||||
let mut lock = state_ptr.state.borrow_mut();
|
||||
if let Some(mut callback) = lock.callbacks.input.take() {
|
||||
let scale_factor = lock.scale_factor;
|
||||
let wheel_scroll_chars = lock.system_settings.mouse_wheel_settings.wheel_scroll_chars;
|
||||
drop(lock);
|
||||
let wheel_distance =
|
||||
(-wparam.signed_hiword() as f32 / WHEEL_DELTA as f32) * wheel_scroll_chars as f32;
|
||||
let mut cursor_point = POINT {
|
||||
x: lparam.signed_loword().into(),
|
||||
y: lparam.signed_hiword().into(),
|
||||
};
|
||||
unsafe { ScreenToClient(handle, &mut cursor_point).ok().log_err() };
|
||||
let event = ScrollWheelEvent {
|
||||
position: logical_point(cursor_point.x as f32, cursor_point.y as f32, scale_factor),
|
||||
delta: ScrollDelta::Lines(Point {
|
||||
x: wheel_distance,
|
||||
y: 0.0,
|
||||
}),
|
||||
modifiers: current_modifiers(),
|
||||
touch_phase: TouchPhase::Moved,
|
||||
};
|
||||
let result = if callback(PlatformInput::ScrollWheel(event)).default_prevented {
|
||||
Some(0)
|
||||
} else {
|
||||
Some(1)
|
||||
};
|
||||
state_ptr.state.borrow_mut().callbacks.input = Some(callback);
|
||||
let Some(mut func) = lock.callbacks.input.take() else {
|
||||
return Some(1);
|
||||
};
|
||||
let scale_factor = lock.scale_factor;
|
||||
let wheel_scroll_chars = lock.system_settings.mouse_wheel_settings.wheel_scroll_chars;
|
||||
drop(lock);
|
||||
|
||||
result
|
||||
} else {
|
||||
Some(1)
|
||||
}
|
||||
let wheel_distance =
|
||||
(-wparam.signed_hiword() as f32 / WHEEL_DELTA as f32) * wheel_scroll_chars as f32;
|
||||
let mut cursor_point = POINT {
|
||||
x: lparam.signed_loword().into(),
|
||||
y: lparam.signed_hiword().into(),
|
||||
};
|
||||
unsafe { ScreenToClient(handle, &mut cursor_point).ok().log_err() };
|
||||
let event = PlatformInput::ScrollWheel(ScrollWheelEvent {
|
||||
position: logical_point(cursor_point.x as f32, cursor_point.y as f32, scale_factor),
|
||||
delta: ScrollDelta::Lines(Point {
|
||||
x: wheel_distance,
|
||||
y: 0.0,
|
||||
}),
|
||||
modifiers: current_modifiers(),
|
||||
touch_phase: TouchPhase::Moved,
|
||||
});
|
||||
let handled = !func(event).propagate;
|
||||
state_ptr.state.borrow_mut().callbacks.input = Some(func);
|
||||
|
||||
if handled { Some(0) } else { Some(1) }
|
||||
}
|
||||
|
||||
fn retrieve_caret_position(state_ptr: &Rc<WindowsWindowStatePtr>) -> Option<POINT> {
|
||||
@@ -808,10 +784,10 @@ fn handle_activate_msg(
|
||||
.executor
|
||||
.spawn(async move {
|
||||
let mut lock = this.state.borrow_mut();
|
||||
if let Some(mut cb) = lock.callbacks.active_status_change.take() {
|
||||
if let Some(mut func) = lock.callbacks.active_status_change.take() {
|
||||
drop(lock);
|
||||
cb(activated);
|
||||
this.state.borrow_mut().callbacks.active_status_change = Some(cb);
|
||||
func(activated);
|
||||
this.state.borrow_mut().callbacks.active_status_change = Some(func);
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
@@ -878,7 +854,7 @@ fn handle_display_change_msg(handle: HWND, state_ptr: Rc<WindowsWindowStatePtr>)
|
||||
// Because WM_DPICHANGED, WM_MOVE, WM_SIZE will come first, window reposition and resize
|
||||
// are handled there.
|
||||
// So we only care about if monitor is disconnected.
|
||||
let previous_monitor = state_ptr.as_ref().state.borrow().display;
|
||||
let previous_monitor = state_ptr.state.borrow().display;
|
||||
if WindowsDisplay::is_connected(previous_monitor.handle) {
|
||||
// we are fine, other display changed
|
||||
return None;
|
||||
@@ -896,7 +872,7 @@ fn handle_display_change_msg(handle: HWND, state_ptr: Rc<WindowsWindowStatePtr>)
|
||||
return None;
|
||||
}
|
||||
let new_display = WindowsDisplay::new_with_handle(new_monitor);
|
||||
state_ptr.as_ref().state.borrow_mut().display = new_display;
|
||||
state_ptr.state.borrow_mut().display = new_display;
|
||||
Some(0)
|
||||
}
|
||||
|
||||
@@ -980,30 +956,24 @@ fn handle_nc_mouse_move_msg(
|
||||
start_tracking_mouse(handle, &state_ptr, TME_LEAVE | TME_NONCLIENT);
|
||||
|
||||
let mut lock = state_ptr.state.borrow_mut();
|
||||
if let Some(mut callback) = lock.callbacks.input.take() {
|
||||
let scale_factor = lock.scale_factor;
|
||||
drop(lock);
|
||||
let mut cursor_point = POINT {
|
||||
x: lparam.signed_loword().into(),
|
||||
y: lparam.signed_hiword().into(),
|
||||
};
|
||||
unsafe { ScreenToClient(handle, &mut cursor_point).ok().log_err() };
|
||||
let event = MouseMoveEvent {
|
||||
position: logical_point(cursor_point.x as f32, cursor_point.y as f32, scale_factor),
|
||||
pressed_button: None,
|
||||
modifiers: current_modifiers(),
|
||||
};
|
||||
let result = if callback(PlatformInput::MouseMove(event)).default_prevented {
|
||||
Some(0)
|
||||
} else {
|
||||
Some(1)
|
||||
};
|
||||
state_ptr.state.borrow_mut().callbacks.input = Some(callback);
|
||||
let mut func = lock.callbacks.input.take()?;
|
||||
let scale_factor = lock.scale_factor;
|
||||
drop(lock);
|
||||
|
||||
result
|
||||
} else {
|
||||
None
|
||||
}
|
||||
let mut cursor_point = POINT {
|
||||
x: lparam.signed_loword().into(),
|
||||
y: lparam.signed_hiword().into(),
|
||||
};
|
||||
unsafe { ScreenToClient(handle, &mut cursor_point).ok().log_err() };
|
||||
let input = PlatformInput::MouseMove(MouseMoveEvent {
|
||||
position: logical_point(cursor_point.x as f32, cursor_point.y as f32, scale_factor),
|
||||
pressed_button: None,
|
||||
modifiers: current_modifiers(),
|
||||
});
|
||||
let handled = !func(input).propagate;
|
||||
state_ptr.state.borrow_mut().callbacks.input = Some(func);
|
||||
|
||||
if handled { Some(0) } else { None }
|
||||
}
|
||||
|
||||
fn handle_nc_mouse_down_msg(
|
||||
@@ -1018,7 +988,7 @@ fn handle_nc_mouse_down_msg(
|
||||
}
|
||||
|
||||
let mut lock = state_ptr.state.borrow_mut();
|
||||
if let Some(mut callback) = lock.callbacks.input.take() {
|
||||
if let Some(mut func) = lock.callbacks.input.take() {
|
||||
let scale_factor = lock.scale_factor;
|
||||
let mut cursor_point = POINT {
|
||||
x: lparam.signed_loword().into(),
|
||||
@@ -1028,22 +998,19 @@ fn handle_nc_mouse_down_msg(
|
||||
let physical_point = point(DevicePixels(cursor_point.x), DevicePixels(cursor_point.y));
|
||||
let click_count = lock.click_state.update(button, physical_point);
|
||||
drop(lock);
|
||||
let event = MouseDownEvent {
|
||||
|
||||
let input = PlatformInput::MouseDown(MouseDownEvent {
|
||||
button,
|
||||
position: logical_point(cursor_point.x as f32, cursor_point.y as f32, scale_factor),
|
||||
modifiers: current_modifiers(),
|
||||
click_count,
|
||||
first_mouse: false,
|
||||
};
|
||||
let result = if callback(PlatformInput::MouseDown(event)).default_prevented {
|
||||
Some(0)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
state_ptr.state.borrow_mut().callbacks.input = Some(callback);
|
||||
});
|
||||
let handled = !func(input).propagate;
|
||||
state_ptr.state.borrow_mut().callbacks.input = Some(func);
|
||||
|
||||
if result.is_some() {
|
||||
return result;
|
||||
if handled {
|
||||
return Some(0);
|
||||
}
|
||||
} else {
|
||||
drop(lock);
|
||||
@@ -1075,28 +1042,26 @@ fn handle_nc_mouse_up_msg(
|
||||
}
|
||||
|
||||
let mut lock = state_ptr.state.borrow_mut();
|
||||
if let Some(mut callback) = lock.callbacks.input.take() {
|
||||
if let Some(mut func) = lock.callbacks.input.take() {
|
||||
let scale_factor = lock.scale_factor;
|
||||
drop(lock);
|
||||
|
||||
let mut cursor_point = POINT {
|
||||
x: lparam.signed_loword().into(),
|
||||
y: lparam.signed_hiword().into(),
|
||||
};
|
||||
unsafe { ScreenToClient(handle, &mut cursor_point).ok().log_err() };
|
||||
let event = MouseUpEvent {
|
||||
let input = PlatformInput::MouseUp(MouseUpEvent {
|
||||
button,
|
||||
position: logical_point(cursor_point.x as f32, cursor_point.y as f32, scale_factor),
|
||||
modifiers: current_modifiers(),
|
||||
click_count: 1,
|
||||
};
|
||||
let result = if callback(PlatformInput::MouseUp(event)).default_prevented {
|
||||
Some(0)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
state_ptr.state.borrow_mut().callbacks.input = Some(callback);
|
||||
if result.is_some() {
|
||||
return result;
|
||||
});
|
||||
let handled = !func(input).propagate;
|
||||
state_ptr.state.borrow_mut().callbacks.input = Some(func);
|
||||
|
||||
if handled {
|
||||
return Some(0);
|
||||
}
|
||||
} else {
|
||||
drop(lock);
|
||||
@@ -1104,35 +1069,27 @@ fn handle_nc_mouse_up_msg(
|
||||
|
||||
let last_pressed = state_ptr.state.borrow_mut().nc_button_pressed.take();
|
||||
if button == MouseButton::Left && last_pressed.is_some() {
|
||||
let last_button = last_pressed.unwrap();
|
||||
let mut handled = false;
|
||||
match wparam.0 as u32 {
|
||||
HTMINBUTTON => {
|
||||
if last_button == HTMINBUTTON {
|
||||
unsafe { ShowWindowAsync(handle, SW_MINIMIZE).ok().log_err() };
|
||||
handled = true;
|
||||
}
|
||||
let handled = match (wparam.0 as u32, last_pressed.unwrap()) {
|
||||
(HTMINBUTTON, HTMINBUTTON) => {
|
||||
unsafe { ShowWindowAsync(handle, SW_MINIMIZE).ok().log_err() };
|
||||
true
|
||||
}
|
||||
HTMAXBUTTON => {
|
||||
if last_button == HTMAXBUTTON {
|
||||
if state_ptr.state.borrow().is_maximized() {
|
||||
unsafe { ShowWindowAsync(handle, SW_NORMAL).ok().log_err() };
|
||||
} else {
|
||||
unsafe { ShowWindowAsync(handle, SW_MAXIMIZE).ok().log_err() };
|
||||
}
|
||||
handled = true;
|
||||
(HTMAXBUTTON, HTMAXBUTTON) => {
|
||||
if state_ptr.state.borrow().is_maximized() {
|
||||
unsafe { ShowWindowAsync(handle, SW_NORMAL).ok().log_err() };
|
||||
} else {
|
||||
unsafe { ShowWindowAsync(handle, SW_MAXIMIZE).ok().log_err() };
|
||||
}
|
||||
true
|
||||
}
|
||||
HTCLOSE => {
|
||||
if last_button == HTCLOSE {
|
||||
unsafe {
|
||||
PostMessageW(Some(handle), WM_CLOSE, WPARAM::default(), LPARAM::default())
|
||||
.log_err()
|
||||
};
|
||||
handled = true;
|
||||
}
|
||||
(HTCLOSE, HTCLOSE) => {
|
||||
unsafe {
|
||||
PostMessageW(Some(handle), WM_CLOSE, WPARAM::default(), LPARAM::default())
|
||||
.log_err()
|
||||
};
|
||||
true
|
||||
}
|
||||
_ => {}
|
||||
_ => false,
|
||||
};
|
||||
if handled {
|
||||
return Some(0);
|
||||
|
||||
@@ -343,7 +343,7 @@ impl WindowTextSystem {
|
||||
text: SharedString,
|
||||
font_size: Pixels,
|
||||
runs: &[TextRun],
|
||||
) -> Result<ShapedLine> {
|
||||
) -> ShapedLine {
|
||||
debug_assert!(
|
||||
text.find('\n').is_none(),
|
||||
"text argument should not contain newlines"
|
||||
@@ -370,13 +370,13 @@ impl WindowTextSystem {
|
||||
});
|
||||
}
|
||||
|
||||
let layout = self.layout_line(&text, font_size, runs)?;
|
||||
let layout = self.layout_line(&text, font_size, runs);
|
||||
|
||||
Ok(ShapedLine {
|
||||
ShapedLine {
|
||||
layout,
|
||||
text,
|
||||
decoration_runs,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Shape a multi line string of text, at the given font_size, for painting to the screen.
|
||||
@@ -510,7 +510,7 @@ impl WindowTextSystem {
|
||||
text: Text,
|
||||
font_size: Pixels,
|
||||
runs: &[TextRun],
|
||||
) -> Result<Arc<LineLayout>>
|
||||
) -> Arc<LineLayout>
|
||||
where
|
||||
Text: AsRef<str>,
|
||||
SharedString: From<Text>,
|
||||
@@ -537,7 +537,7 @@ impl WindowTextSystem {
|
||||
font_runs.clear();
|
||||
self.font_runs_pool.lock().push(font_runs);
|
||||
|
||||
Ok(layout)
|
||||
layout
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
use proc_macro::TokenStream;
|
||||
use quote::quote;
|
||||
use syn::{Attribute, Data, DeriveInput, Lit, Meta, NestedMeta, parse_macro_input};
|
||||
|
||||
pub fn derive_path_static_str(input: TokenStream) -> TokenStream {
|
||||
let input = parse_macro_input!(input as DeriveInput);
|
||||
let name = &input.ident;
|
||||
|
||||
let prefix = get_attr_value(&input.attrs, "prefix").unwrap_or_else(|| "".to_string());
|
||||
let suffix = get_attr_value(&input.attrs, "suffix").unwrap_or_else(|| "".to_string());
|
||||
let delimiter = get_attr_value(&input.attrs, "delimiter").unwrap_or_else(|| "/".to_string());
|
||||
|
||||
let path_str_impl = impl_path_str(name, &input.data, &prefix, &suffix, &delimiter);
|
||||
|
||||
let expanded = quote! {
|
||||
impl #name {
|
||||
pub fn path_str(&self) -> &'static str {
|
||||
#path_str_impl
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
TokenStream::from(expanded)
|
||||
}
|
||||
|
||||
fn impl_path_str(
|
||||
name: &syn::Ident,
|
||||
data: &Data,
|
||||
prefix: &str,
|
||||
suffix: &str,
|
||||
delimiter: &str,
|
||||
) -> proc_macro2::TokenStream {
|
||||
match *data {
|
||||
Data::Enum(ref data) => {
|
||||
let match_arms = data.variants.iter().map(|variant| {
|
||||
let ident = &variant.ident;
|
||||
let path = format!("{}{}{}{}{}", prefix, delimiter, ident, delimiter, suffix);
|
||||
quote! {
|
||||
#name::#ident => #path,
|
||||
}
|
||||
});
|
||||
|
||||
quote! {
|
||||
match self {
|
||||
#(#match_arms)*
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => panic!("DerivePathStr only supports enums"),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_attr_value(attrs: &[Attribute], key: &str) -> Option<String> {
|
||||
attrs
|
||||
.iter()
|
||||
.filter(|attr| attr.path.is_ident("derive_path_static_str"))
|
||||
.find_map(|attr| {
|
||||
if let Ok(Meta::List(meta_list)) = attr.parse_meta() {
|
||||
meta_list.nested.iter().find_map(|nested_meta| {
|
||||
if let NestedMeta::Meta(Meta::NameValue(name_value)) = nested_meta {
|
||||
if name_value.path.is_ident(key) {
|
||||
if let Lit::Str(lit_str) = &name_value.lit {
|
||||
return Some(lit_str.value());
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
mod derive_app_context;
|
||||
mod derive_into_element;
|
||||
mod derive_path_static_str;
|
||||
mod derive_render;
|
||||
mod derive_visual_context;
|
||||
mod register_action;
|
||||
@@ -31,12 +30,6 @@ pub fn derive_render(input: TokenStream) -> TokenStream {
|
||||
derive_render::derive_render(input)
|
||||
}
|
||||
|
||||
#[proc_macro_derive(PathStaticStr)]
|
||||
#[doc(hidden)]
|
||||
pub fn derive_path_static_str(input: TokenStream) -> TokenStream {
|
||||
derive_path_static_str::derive_path_static_str(input)
|
||||
}
|
||||
|
||||
/// #[derive(AppContext)] is used to create a context out of anything that holds a `&mut App`
|
||||
/// Note that a `#[app]` attribute is required to identify the variable holding the &mut App.
|
||||
///
|
||||
|
||||
18
crates/jj/Cargo.toml
Normal file
18
crates/jj/Cargo.toml
Normal file
@@ -0,0 +1,18 @@
|
||||
[package]
|
||||
name = "jj"
|
||||
version = "0.1.0"
|
||||
publish.workspace = true
|
||||
edition.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/jj.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
gpui.workspace = true
|
||||
jj-lib.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
1
crates/jj/LICENSE-GPL
Symbolic link
1
crates/jj/LICENSE-GPL
Symbolic link
@@ -0,0 +1 @@
|
||||
../../LICENSE-GPL
|
||||
5
crates/jj/src/jj.rs
Normal file
5
crates/jj/src/jj.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
mod jj_repository;
|
||||
mod jj_store;
|
||||
|
||||
pub use jj_repository::*;
|
||||
pub use jj_store::*;
|
||||
72
crates/jj/src/jj_repository.rs
Normal file
72
crates/jj/src/jj_repository.rs
Normal file
@@ -0,0 +1,72 @@
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use gpui::SharedString;
|
||||
use jj_lib::config::StackedConfig;
|
||||
use jj_lib::repo::StoreFactories;
|
||||
use jj_lib::settings::UserSettings;
|
||||
use jj_lib::workspace::{self, DefaultWorkspaceLoaderFactory, WorkspaceLoaderFactory};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Bookmark {
|
||||
pub ref_name: SharedString,
|
||||
}
|
||||
|
||||
pub trait JujutsuRepository: Send + Sync {
|
||||
fn list_bookmarks(&self) -> Vec<Bookmark>;
|
||||
}
|
||||
|
||||
pub struct RealJujutsuRepository {
|
||||
repository: Arc<jj_lib::repo::ReadonlyRepo>,
|
||||
}
|
||||
|
||||
impl RealJujutsuRepository {
|
||||
pub fn new(cwd: &Path) -> Result<Self> {
|
||||
let workspace_loader_factory = DefaultWorkspaceLoaderFactory;
|
||||
let workspace_loader = workspace_loader_factory.create(Self::find_workspace_dir(cwd))?;
|
||||
|
||||
let config = StackedConfig::with_defaults();
|
||||
let settings = UserSettings::from_config(config)?;
|
||||
|
||||
let workspace = workspace_loader.load(
|
||||
&settings,
|
||||
&StoreFactories::default(),
|
||||
&workspace::default_working_copy_factories(),
|
||||
)?;
|
||||
|
||||
let repo_loader = workspace.repo_loader();
|
||||
let repository = repo_loader.load_at_head()?;
|
||||
|
||||
Ok(Self { repository })
|
||||
}
|
||||
|
||||
fn find_workspace_dir(cwd: &Path) -> &Path {
|
||||
cwd.ancestors()
|
||||
.find(|path| path.join(".jj").is_dir())
|
||||
.unwrap_or(cwd)
|
||||
}
|
||||
}
|
||||
|
||||
impl JujutsuRepository for RealJujutsuRepository {
|
||||
fn list_bookmarks(&self) -> Vec<Bookmark> {
|
||||
let bookmarks = self
|
||||
.repository
|
||||
.view()
|
||||
.bookmarks()
|
||||
.map(|(ref_name, _target)| Bookmark {
|
||||
ref_name: ref_name.as_str().to_string().into(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
bookmarks
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FakeJujutsuRepository {}
|
||||
|
||||
impl JujutsuRepository for FakeJujutsuRepository {
|
||||
fn list_bookmarks(&self) -> Vec<Bookmark> {
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
41
crates/jj/src/jj_store.rs
Normal file
41
crates/jj/src/jj_store.rs
Normal file
@@ -0,0 +1,41 @@
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
|
||||
use gpui::{App, Entity, Global, prelude::*};
|
||||
|
||||
use crate::{JujutsuRepository, RealJujutsuRepository};
|
||||
|
||||
/// Note: We won't ultimately be storing the jj store in a global, we're just doing this for exploration purposes.
|
||||
struct GlobalJujutsuStore(Entity<JujutsuStore>);
|
||||
|
||||
impl Global for GlobalJujutsuStore {}
|
||||
|
||||
pub struct JujutsuStore {
|
||||
repository: Arc<dyn JujutsuRepository>,
|
||||
}
|
||||
|
||||
impl JujutsuStore {
|
||||
pub fn init_global(cx: &mut App) {
|
||||
let Some(repository) = RealJujutsuRepository::new(&Path::new(".")).ok() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let repository = Arc::new(repository);
|
||||
let jj_store = cx.new(|cx| JujutsuStore::new(repository, cx));
|
||||
|
||||
cx.set_global(GlobalJujutsuStore(jj_store));
|
||||
}
|
||||
|
||||
pub fn try_global(cx: &App) -> Option<Entity<Self>> {
|
||||
cx.try_global::<GlobalJujutsuStore>()
|
||||
.map(|global| global.0.clone())
|
||||
}
|
||||
|
||||
pub fn new(repository: Arc<dyn JujutsuRepository>, _cx: &mut Context<Self>) -> Self {
|
||||
Self { repository }
|
||||
}
|
||||
|
||||
pub fn repository(&self) -> &Arc<dyn JujutsuRepository> {
|
||||
&self.repository
|
||||
}
|
||||
}
|
||||
25
crates/jj_ui/Cargo.toml
Normal file
25
crates/jj_ui/Cargo.toml
Normal file
@@ -0,0 +1,25 @@
|
||||
[package]
|
||||
name = "jj_ui"
|
||||
version = "0.1.0"
|
||||
publish.workspace = true
|
||||
edition.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/jj_ui.rs"
|
||||
|
||||
[dependencies]
|
||||
command_palette_hooks.workspace = true
|
||||
feature_flags.workspace = true
|
||||
fuzzy.workspace = true
|
||||
gpui.workspace = true
|
||||
jj.workspace = true
|
||||
picker.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
workspace.workspace = true
|
||||
zed_actions.workspace = true
|
||||
1
crates/jj_ui/LICENSE-GPL
Symbolic link
1
crates/jj_ui/LICENSE-GPL
Symbolic link
@@ -0,0 +1 @@
|
||||
../../LICENSE-GPL
|
||||
197
crates/jj_ui/src/bookmark_picker.rs
Normal file
197
crates/jj_ui/src/bookmark_picker.rs
Normal file
@@ -0,0 +1,197 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use fuzzy::{StringMatchCandidate, match_strings};
|
||||
use gpui::{
|
||||
App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Task, WeakEntity, Window,
|
||||
prelude::*,
|
||||
};
|
||||
use jj::{Bookmark, JujutsuStore};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use ui::{HighlightedLabel, ListItem, ListItemSpacing, prelude::*};
|
||||
use util::ResultExt as _;
|
||||
use workspace::{ModalView, Workspace};
|
||||
|
||||
pub fn register(workspace: &mut Workspace) {
|
||||
workspace.register_action(open);
|
||||
}
|
||||
|
||||
fn open(
|
||||
workspace: &mut Workspace,
|
||||
_: &zed_actions::jj::BookmarkList,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Workspace>,
|
||||
) {
|
||||
let Some(jj_store) = JujutsuStore::try_global(cx) else {
|
||||
return;
|
||||
};
|
||||
|
||||
workspace.toggle_modal(window, cx, |window, cx| {
|
||||
let delegate = BookmarkPickerDelegate::new(cx.entity().downgrade(), jj_store, cx);
|
||||
BookmarkPicker::new(delegate, window, cx)
|
||||
});
|
||||
}
|
||||
|
||||
pub struct BookmarkPicker {
|
||||
picker: Entity<Picker<BookmarkPickerDelegate>>,
|
||||
}
|
||||
|
||||
impl BookmarkPicker {
|
||||
pub fn new(
|
||||
delegate: BookmarkPickerDelegate,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
|
||||
Self { picker }
|
||||
}
|
||||
}
|
||||
|
||||
impl ModalView for BookmarkPicker {}
|
||||
|
||||
impl EventEmitter<DismissEvent> for BookmarkPicker {}
|
||||
|
||||
impl Focusable for BookmarkPicker {
|
||||
fn focus_handle(&self, cx: &App) -> FocusHandle {
|
||||
self.picker.focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for BookmarkPicker {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
v_flex().w(rems(34.)).child(self.picker.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct BookmarkEntry {
|
||||
bookmark: Bookmark,
|
||||
positions: Vec<usize>,
|
||||
}
|
||||
|
||||
pub struct BookmarkPickerDelegate {
|
||||
picker: WeakEntity<BookmarkPicker>,
|
||||
matches: Vec<BookmarkEntry>,
|
||||
all_bookmarks: Vec<Bookmark>,
|
||||
selected_index: usize,
|
||||
}
|
||||
|
||||
impl BookmarkPickerDelegate {
|
||||
fn new(
|
||||
picker: WeakEntity<BookmarkPicker>,
|
||||
jj_store: Entity<JujutsuStore>,
|
||||
cx: &mut Context<BookmarkPicker>,
|
||||
) -> Self {
|
||||
let bookmarks = jj_store.read(cx).repository().list_bookmarks();
|
||||
|
||||
Self {
|
||||
picker,
|
||||
matches: Vec::new(),
|
||||
all_bookmarks: bookmarks,
|
||||
selected_index: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PickerDelegate for BookmarkPickerDelegate {
|
||||
type ListItem = ListItem;
|
||||
|
||||
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
|
||||
"Select Bookmark…".into()
|
||||
}
|
||||
|
||||
fn match_count(&self) -> usize {
|
||||
self.matches.len()
|
||||
}
|
||||
|
||||
fn selected_index(&self) -> usize {
|
||||
self.selected_index
|
||||
}
|
||||
|
||||
fn set_selected_index(
|
||||
&mut self,
|
||||
ix: usize,
|
||||
_window: &mut Window,
|
||||
_cx: &mut Context<Picker<Self>>,
|
||||
) {
|
||||
self.selected_index = ix;
|
||||
}
|
||||
|
||||
fn update_matches(
|
||||
&mut self,
|
||||
query: String,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
) -> Task<()> {
|
||||
let background = cx.background_executor().clone();
|
||||
let all_bookmarks = self.all_bookmarks.clone();
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let matches = if query.is_empty() {
|
||||
all_bookmarks
|
||||
.into_iter()
|
||||
.map(|bookmark| BookmarkEntry {
|
||||
bookmark,
|
||||
positions: Vec::new(),
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
let candidates = all_bookmarks
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(ix, bookmark)| StringMatchCandidate::new(ix, &bookmark.ref_name))
|
||||
.collect::<Vec<_>>();
|
||||
match_strings(
|
||||
&candidates,
|
||||
&query,
|
||||
false,
|
||||
100,
|
||||
&Default::default(),
|
||||
background,
|
||||
)
|
||||
.await
|
||||
.into_iter()
|
||||
.map(|mat| BookmarkEntry {
|
||||
bookmark: all_bookmarks[mat.candidate_id].clone(),
|
||||
positions: mat.positions,
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
|
||||
this.update(cx, |this, _cx| {
|
||||
this.delegate.matches = matches;
|
||||
})
|
||||
.log_err();
|
||||
})
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _secondary: bool, _window: &mut Window, _cx: &mut Context<Picker<Self>>) {
|
||||
//
|
||||
}
|
||||
|
||||
fn dismissed(&mut self, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
|
||||
self.picker
|
||||
.update(cx, |_, cx| cx.emit(DismissEvent))
|
||||
.log_err();
|
||||
}
|
||||
|
||||
fn render_match(
|
||||
&self,
|
||||
ix: usize,
|
||||
selected: bool,
|
||||
_window: &mut Window,
|
||||
_cx: &mut Context<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
let entry = &self.matches[ix];
|
||||
|
||||
Some(
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.toggle_state(selected)
|
||||
.child(HighlightedLabel::new(
|
||||
entry.bookmark.ref_name.clone(),
|
||||
entry.positions.clone(),
|
||||
)),
|
||||
)
|
||||
}
|
||||
}
|
||||
39
crates/jj_ui/src/jj_ui.rs
Normal file
39
crates/jj_ui/src/jj_ui.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
mod bookmark_picker;
|
||||
|
||||
use command_palette_hooks::CommandPaletteFilter;
|
||||
use feature_flags::FeatureFlagAppExt as _;
|
||||
use gpui::App;
|
||||
use jj::JujutsuStore;
|
||||
use workspace::Workspace;
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
JujutsuStore::init_global(cx);
|
||||
|
||||
cx.observe_new(|workspace: &mut Workspace, _window, _cx| {
|
||||
bookmark_picker::register(workspace);
|
||||
})
|
||||
.detach();
|
||||
|
||||
feature_gate_jj_ui_actions(cx);
|
||||
}
|
||||
|
||||
fn feature_gate_jj_ui_actions(cx: &mut App) {
|
||||
const JJ_ACTION_NAMESPACE: &str = "jj";
|
||||
|
||||
CommandPaletteFilter::update_global(cx, |filter, _cx| {
|
||||
filter.hide_namespace(JJ_ACTION_NAMESPACE);
|
||||
});
|
||||
|
||||
cx.observe_flag::<feature_flags::JjUiFeatureFlag, _>({
|
||||
move |is_enabled, cx| {
|
||||
CommandPaletteFilter::update_global(cx, |filter, _cx| {
|
||||
if is_enabled {
|
||||
filter.show_namespace(JJ_ACTION_NAMESPACE);
|
||||
} else {
|
||||
filter.hide_namespace(JJ_ACTION_NAMESPACE);
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
@@ -144,7 +144,7 @@ struct BufferBranchState {
|
||||
/// state of a buffer.
|
||||
pub struct BufferSnapshot {
|
||||
pub text: text::BufferSnapshot,
|
||||
pub(crate) syntax: SyntaxSnapshot,
|
||||
pub syntax: SyntaxSnapshot,
|
||||
file: Option<Arc<dyn File>>,
|
||||
diagnostics: SmallVec<[(LanguageServerId, DiagnosticSet); 2]>,
|
||||
remote_selections: TreeMap<ReplicaId, SelectionSet>,
|
||||
|
||||
@@ -80,7 +80,9 @@ pub use language_registry::{
|
||||
};
|
||||
pub use lsp::{LanguageServerId, LanguageServerName};
|
||||
pub use outline::*;
|
||||
pub use syntax_map::{OwnedSyntaxLayer, SyntaxLayer, ToTreeSitterPoint, TreeSitterOptions};
|
||||
pub use syntax_map::{
|
||||
OwnedSyntaxLayer, SyntaxLayer, SyntaxMapMatch, ToTreeSitterPoint, TreeSitterOptions,
|
||||
};
|
||||
pub use text::{AnchorRangeExt, LineEnding};
|
||||
pub use tree_sitter::{Node, Parser, Tree, TreeCursor};
|
||||
|
||||
@@ -755,6 +757,10 @@ pub struct LanguageConfig {
|
||||
/// A list of preferred debuggers for this language.
|
||||
#[serde(default)]
|
||||
pub debuggers: IndexSet<SharedString>,
|
||||
/// Whether to treat documentation comment of this language differently by
|
||||
/// auto adding prefix on new line, adjusting the indenting , etc.
|
||||
#[serde(default)]
|
||||
pub documentation: Option<DocumentationConfig>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, Default, JsonSchema)]
|
||||
@@ -805,6 +811,19 @@ pub struct JsxTagAutoCloseConfig {
|
||||
pub erroneous_close_tag_name_node_name: Option<String>,
|
||||
}
|
||||
|
||||
/// The configuration for documentation block for this language.
|
||||
#[derive(Clone, Deserialize, JsonSchema)]
|
||||
pub struct DocumentationConfig {
|
||||
/// A start tag of documentation block.
|
||||
pub start: Arc<str>,
|
||||
/// A end tag of documentation block.
|
||||
pub end: Arc<str>,
|
||||
/// A character to add as a prefix when a new line is added to a documentation block.
|
||||
pub prefix: Arc<str>,
|
||||
/// A indent to add for prefix and end line upon new line.
|
||||
pub tab_size: NonZeroU32,
|
||||
}
|
||||
|
||||
/// Represents a language for the given range. Some languages (e.g. HTML)
|
||||
/// interleave several languages together, thus a single buffer might actually contain
|
||||
/// several nested scopes.
|
||||
@@ -883,6 +902,7 @@ impl Default for LanguageConfig {
|
||||
completion_query_characters: Default::default(),
|
||||
debuggers: Default::default(),
|
||||
significant_indentation: Default::default(),
|
||||
documentation: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1802,6 +1822,14 @@ impl LanguageScope {
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Returns config to documentation block for this language.
|
||||
///
|
||||
/// Used for documentation styles that require a leading character on each line,
|
||||
/// such as the asterisk in JSDoc, Javadoc, etc.
|
||||
pub fn documentation(&self) -> Option<&DocumentationConfig> {
|
||||
self.language.config.documentation.as_ref()
|
||||
}
|
||||
|
||||
/// Returns a list of bracket pairs for a given language with an additional
|
||||
/// piece of information about whether the particular bracket pair is currently active for a given language.
|
||||
pub fn brackets(&self) -> impl Iterator<Item = (&BracketPair, bool)> {
|
||||
@@ -1833,9 +1861,9 @@ impl LanguageScope {
|
||||
pub fn language_allowed(&self, name: &LanguageServerName) -> bool {
|
||||
let config = &self.language.config;
|
||||
let opt_in_servers = &config.scope_opt_in_language_servers;
|
||||
if opt_in_servers.iter().any(|o| *o == *name) {
|
||||
if opt_in_servers.contains(name) {
|
||||
if let Some(over) = self.config_override() {
|
||||
over.opt_into_language_servers.iter().any(|o| *o == *name)
|
||||
over.opt_into_language_servers.contains(name)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ use crate::{
|
||||
};
|
||||
use collections::BTreeMap;
|
||||
use gpui::{App, Context, Entity, EventEmitter, Global, prelude::*};
|
||||
use std::sync::Arc;
|
||||
use std::{str::FromStr, sync::Arc};
|
||||
use util::maybe;
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
@@ -27,11 +27,36 @@ pub struct LanguageModelRegistry {
|
||||
inline_alternatives: Vec<Arc<dyn LanguageModel>>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SelectedModel {
|
||||
pub provider: LanguageModelProviderId,
|
||||
pub model: LanguageModelId,
|
||||
}
|
||||
|
||||
impl FromStr for SelectedModel {
|
||||
type Err = String;
|
||||
|
||||
/// Parse string identifiers like `provider_id/model_id` into a `SelectedModel`
|
||||
fn from_str(id: &str) -> Result<SelectedModel, Self::Err> {
|
||||
let parts: Vec<&str> = id.split('/').collect();
|
||||
let [provider_id, model_id] = parts.as_slice() else {
|
||||
return Err(format!(
|
||||
"Invalid model identifier format: `{}`. Expected `provider_id/model_id`",
|
||||
id
|
||||
));
|
||||
};
|
||||
|
||||
if provider_id.is_empty() || model_id.is_empty() {
|
||||
return Err(format!("Provider and model ids can't be empty: `{}`", id));
|
||||
}
|
||||
|
||||
Ok(SelectedModel {
|
||||
provider: LanguageModelProviderId(provider_id.to_string().into()),
|
||||
model: LanguageModelId(model_id.to_string().into()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ConfiguredModel {
|
||||
pub provider: Arc<dyn LanguageModelProvider>,
|
||||
|
||||
@@ -60,10 +60,10 @@ impl State {
|
||||
impl CopilotChatLanguageModelProvider {
|
||||
pub fn new(cx: &mut App) -> Self {
|
||||
let state = cx.new(|cx| {
|
||||
let _copilot_chat_subscription = CopilotChat::global(cx)
|
||||
let copilot_chat_subscription = CopilotChat::global(cx)
|
||||
.map(|copilot_chat| cx.observe(&copilot_chat, |_, _, cx| cx.notify()));
|
||||
State {
|
||||
_copilot_chat_subscription,
|
||||
_copilot_chat_subscription: copilot_chat_subscription,
|
||||
_settings_subscription: cx.observe_global::<SettingsStore>(|_, cx| {
|
||||
cx.notify();
|
||||
}),
|
||||
|
||||
@@ -12,3 +12,4 @@ brackets = [
|
||||
{ start = "/*", end = " */", close = true, newline = false, not_in = ["string", "comment"] },
|
||||
]
|
||||
debuggers = ["CodeLLDB", "GDB"]
|
||||
documentation = { start = "/*", end = "*/", prefix = "* ", tab_size = 1 }
|
||||
|
||||
@@ -12,3 +12,4 @@ brackets = [
|
||||
{ start = "/*", end = " */", close = true, newline = false, not_in = ["string", "comment"] },
|
||||
]
|
||||
debuggers = ["CodeLLDB", "GDB"]
|
||||
documentation = { start = "/*", end = "*/", prefix = "* ", tab_size = 1 }
|
||||
|
||||
@@ -15,3 +15,4 @@ brackets = [
|
||||
tab_size = 4
|
||||
hard_tabs = true
|
||||
debuggers = ["Delve"]
|
||||
documentation = { start = "/*", end = "*/", prefix = "* ", tab_size = 1 }
|
||||
|
||||
@@ -20,6 +20,7 @@ tab_size = 2
|
||||
scope_opt_in_language_servers = ["tailwindcss-language-server", "emmet-language-server"]
|
||||
prettier_parser_name = "babel"
|
||||
debuggers = ["JavaScript"]
|
||||
documentation = { start = "/**", end = "*/", prefix = "* ", tab_size = 1 }
|
||||
|
||||
[jsx_tag_auto_close]
|
||||
open_tag_node_name = "jsx_opening_element"
|
||||
|
||||
@@ -2,6 +2,7 @@ use anyhow::Context as _;
|
||||
use gpui::{App, UpdateGlobal};
|
||||
use json::json_task_context;
|
||||
use node_runtime::NodeRuntime;
|
||||
use python::PyprojectTomlManifestProvider;
|
||||
use rust::CargoManifestProvider;
|
||||
use rust_embed::RustEmbed;
|
||||
use settings::SettingsStore;
|
||||
@@ -302,7 +303,13 @@ pub fn init(languages: Arc<LanguageRegistry>, node: NodeRuntime, cx: &mut App) {
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach();
|
||||
project::ManifestProviders::global(cx).register(Arc::from(CargoManifestProvider));
|
||||
let manifest_providers: [Arc<dyn ManifestProvider>; 2] = [
|
||||
Arc::from(CargoManifestProvider),
|
||||
Arc::from(PyprojectTomlManifestProvider),
|
||||
];
|
||||
for provider in manifest_providers {
|
||||
project::ManifestProviders::global(cx).register(provider);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
|
||||
@@ -4,13 +4,13 @@ use async_trait::async_trait;
|
||||
use collections::HashMap;
|
||||
use gpui::{App, Task};
|
||||
use gpui::{AsyncApp, SharedString};
|
||||
use language::LanguageName;
|
||||
use language::LanguageToolchainStore;
|
||||
use language::Toolchain;
|
||||
use language::ToolchainList;
|
||||
use language::ToolchainLister;
|
||||
use language::language_settings::language_settings;
|
||||
use language::{ContextProvider, LspAdapter, LspAdapterDelegate};
|
||||
use language::{LanguageName, ManifestName, ManifestProvider, ManifestQuery};
|
||||
use lsp::LanguageServerBinary;
|
||||
use lsp::LanguageServerName;
|
||||
use node_runtime::NodeRuntime;
|
||||
@@ -38,6 +38,32 @@ use std::{
|
||||
use task::{TaskTemplate, TaskTemplates, VariableName};
|
||||
use util::ResultExt;
|
||||
|
||||
pub(crate) struct PyprojectTomlManifestProvider;
|
||||
|
||||
impl ManifestProvider for PyprojectTomlManifestProvider {
|
||||
fn name(&self) -> ManifestName {
|
||||
SharedString::new_static("pyproject.toml").into()
|
||||
}
|
||||
|
||||
fn search(
|
||||
&self,
|
||||
ManifestQuery {
|
||||
path,
|
||||
depth,
|
||||
delegate,
|
||||
}: ManifestQuery,
|
||||
) -> Option<Arc<Path>> {
|
||||
for path in path.ancestors().take(depth) {
|
||||
let p = path.join("pyproject.toml");
|
||||
if delegate.exists(&p, Some(false)) {
|
||||
return Some(path.into());
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
const SERVER_PATH: &str = "node_modules/pyright/langserver.index.js";
|
||||
const NODE_MODULE_RELATIVE_SERVER_PATH: &str = "pyright/langserver.index.js";
|
||||
|
||||
@@ -301,6 +327,9 @@ impl LspAdapter for PythonLspAdapter {
|
||||
user_settings
|
||||
})
|
||||
}
|
||||
fn manifest_name(&self) -> Option<ManifestName> {
|
||||
Some(SharedString::new_static("pyproject.toml").into())
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_cached_server_binary(
|
||||
@@ -1142,6 +1171,9 @@ impl LspAdapter for PyLspAdapter {
|
||||
user_settings
|
||||
})
|
||||
}
|
||||
fn manifest_name(&self) -> Option<ManifestName> {
|
||||
Some(SharedString::new_static("pyproject.toml").into())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -52,6 +52,20 @@
|
||||
(function_definition
|
||||
name: (identifier) @function.definition)
|
||||
|
||||
((call
|
||||
function: (identifier) @_isinstance
|
||||
arguments: (argument_list
|
||||
(_)
|
||||
(identifier) @type))
|
||||
(#eq? @_isinstance "isinstance"))
|
||||
|
||||
((call
|
||||
function: (identifier) @_issubclass
|
||||
arguments: (argument_list
|
||||
(identifier) @type
|
||||
(identifier) @type))
|
||||
(#eq? @_issubclass "issubclass"))
|
||||
|
||||
; Function arguments
|
||||
(function_definition
|
||||
parameters: (parameters
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
(_ "[" "]" @end) @indent
|
||||
(_ "{" "}" @end) @indent
|
||||
|
||||
(function_definition
|
||||
":" @start
|
||||
body: (block) @indent
|
||||
|
||||
@@ -1,11 +1,52 @@
|
||||
; Capture decorators for standalone annotation highlighting
|
||||
(decorator) @annotation
|
||||
|
||||
(class_definition
|
||||
"class" @context
|
||||
name: (identifier) @name
|
||||
) @item
|
||||
; class definitions
|
||||
[
|
||||
(module
|
||||
(class_definition
|
||||
"class" @context
|
||||
name: (identifier) @name
|
||||
) @item)
|
||||
|
||||
(function_definition
|
||||
"async"? @context
|
||||
"def" @context
|
||||
name: (_) @name) @item
|
||||
(block
|
||||
(class_definition
|
||||
"class" @context
|
||||
name: (identifier) @name
|
||||
) @item)
|
||||
|
||||
(decorated_definition
|
||||
(decorator)+ @context.extra
|
||||
definition:
|
||||
(class_definition
|
||||
"class" @context
|
||||
name: (identifier) @name
|
||||
) )@item
|
||||
]
|
||||
|
||||
; function definitions
|
||||
[
|
||||
(module
|
||||
(function_definition
|
||||
"async"? @context
|
||||
"def" @context
|
||||
name: (_) @name
|
||||
) @item
|
||||
)
|
||||
|
||||
(block
|
||||
(function_definition
|
||||
"async"? @context
|
||||
"def" @context
|
||||
name: (_) @name
|
||||
) @item
|
||||
)
|
||||
|
||||
(decorated_definition
|
||||
(decorator)+ @context.extra
|
||||
definition: (function_definition
|
||||
"async"? @context
|
||||
"def" @context
|
||||
name: (_) @name )
|
||||
) @item
|
||||
]
|
||||
|
||||
@@ -686,6 +686,7 @@ impl ContextProvider for RustContextProvider {
|
||||
RUST_PACKAGE_TASK_VARIABLE.template_value(),
|
||||
"--".into(),
|
||||
"--nocapture".into(),
|
||||
"--include-ignored".into(),
|
||||
RUST_TEST_NAME_TASK_VARIABLE.template_value(),
|
||||
],
|
||||
tags: vec!["rust-test".to_owned()],
|
||||
@@ -706,6 +707,7 @@ impl ContextProvider for RustContextProvider {
|
||||
RUST_PACKAGE_TASK_VARIABLE.template_value(),
|
||||
"--".into(),
|
||||
"--nocapture".into(),
|
||||
"--include-ignored".into(),
|
||||
RUST_DOC_TEST_NAME_TASK_VARIABLE.template_value(),
|
||||
],
|
||||
tags: vec!["rust-doc-test".to_owned()],
|
||||
|
||||
@@ -16,3 +16,4 @@ brackets = [
|
||||
]
|
||||
collapsed_placeholder = " /* ... */ "
|
||||
debuggers = ["CodeLLDB", "GDB"]
|
||||
documentation = { start = "/*", end = "*/", prefix = "* ", tab_size = 1 }
|
||||
|
||||
@@ -18,6 +18,7 @@ scope_opt_in_language_servers = ["tailwindcss-language-server", "emmet-language-
|
||||
prettier_parser_name = "typescript"
|
||||
tab_size = 2
|
||||
debuggers = ["JavaScript"]
|
||||
documentation = { start = "/**", end = "*/", prefix = "* ", tab_size = 1 }
|
||||
|
||||
[jsx_tag_auto_close]
|
||||
open_tag_node_name = "jsx_opening_element"
|
||||
|
||||
@@ -18,6 +18,7 @@ word_characters = ["#", "$"]
|
||||
prettier_parser_name = "typescript"
|
||||
tab_size = 2
|
||||
debuggers = ["JavaScript"]
|
||||
documentation = { start = "/**", end = "*/", prefix = "* ", tab_size = 1 }
|
||||
|
||||
[overrides.string]
|
||||
completion_query_characters = ["."]
|
||||
|
||||
@@ -29,11 +29,11 @@ use fuzzy::{StringMatch, StringMatchCandidate, match_strings};
|
||||
use gpui::{
|
||||
Action, AnyElement, App, AppContext as _, AsyncWindowContext, Bounds, ClipboardItem, Context,
|
||||
DismissEvent, Div, ElementId, Entity, EventEmitter, FocusHandle, Focusable, HighlightStyle,
|
||||
InteractiveElement, IntoElement, KeyContext, ListHorizontalSizingBehavior, ListSizingBehavior,
|
||||
MouseButton, MouseDownEvent, ParentElement, Pixels, Point, Render, ScrollStrategy,
|
||||
SharedString, Stateful, StatefulInteractiveElement as _, Styled, Subscription, Task,
|
||||
UniformListScrollHandle, WeakEntity, Window, actions, anchored, deferred, div, point, px, size,
|
||||
uniform_list,
|
||||
InteractiveElement, IntoElement, KeyContext, ListAlignment, ListHorizontalSizingBehavior,
|
||||
ListSizingBehavior, ListState, MouseButton, MouseDownEvent, ParentElement, Pixels, Point,
|
||||
Render, ScrollStrategy, SharedString, Stateful, StatefulInteractiveElement as _, Styled,
|
||||
Subscription, Task, UniformListScrollHandle, WeakEntity, Window, actions, anchored, deferred,
|
||||
div, list, point, px, size, uniform_list,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use language::{BufferId, BufferSnapshot, OffsetRangeExt, OutlineItem};
|
||||
@@ -1620,7 +1620,7 @@ impl OutlinePanel {
|
||||
.get(&external_file.buffer_id)
|
||||
.into_iter()
|
||||
.flat_map(|excerpts| {
|
||||
excerpts.iter().map(|(excerpt_id, _)| {
|
||||
excerpts.keys().map(|excerpt_id| {
|
||||
CollapsedEntry::Excerpt(
|
||||
external_file.buffer_id,
|
||||
*excerpt_id,
|
||||
@@ -1641,7 +1641,7 @@ impl OutlinePanel {
|
||||
entries.extend(
|
||||
self.excerpts.get(&file.buffer_id).into_iter().flat_map(
|
||||
|excerpts| {
|
||||
excerpts.iter().map(|(excerpt_id, _)| {
|
||||
excerpts.keys().map(|excerpt_id| {
|
||||
CollapsedEntry::Excerpt(file.buffer_id, *excerpt_id)
|
||||
})
|
||||
},
|
||||
@@ -2302,7 +2302,7 @@ impl OutlinePanel {
|
||||
depth,
|
||||
Some(icon),
|
||||
is_active,
|
||||
label_element,
|
||||
label_element.into_any_element(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
@@ -2511,7 +2511,7 @@ impl OutlinePanel {
|
||||
.when_some(icon_element, |list_item, icon_element| {
|
||||
list_item.child(h_flex().child(icon_element))
|
||||
})
|
||||
.child(h_flex().h_6().child(label_element).ml_1())
|
||||
.child(h_flex().child(label_element).ml_1())
|
||||
.on_secondary_mouse_down(cx.listener(
|
||||
move |outline_panel, event: &MouseDownEvent, window, cx| {
|
||||
// Stop propagation to prevent the catch-all context menu for the project
|
||||
@@ -3259,13 +3259,13 @@ impl OutlinePanel {
|
||||
cx.spawn_in(window, async move |outline_panel, cx| {
|
||||
let fetched_outlines = cx
|
||||
.background_spawn(async move {
|
||||
buffer_snapshot
|
||||
.outline_items_containing(
|
||||
excerpt_range.context,
|
||||
false,
|
||||
Some(&syntax_theme),
|
||||
)
|
||||
.unwrap_or_default()
|
||||
outline_items_containing(
|
||||
&buffer_snapshot,
|
||||
excerpt_range.context,
|
||||
true,
|
||||
Some(&syntax_theme),
|
||||
)
|
||||
.unwrap_or_default()
|
||||
})
|
||||
.await;
|
||||
outline_panel
|
||||
@@ -4098,49 +4098,50 @@ impl OutlinePanel {
|
||||
query: Option<&str>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if let Some(excerpts) = self.excerpts.get(&buffer_id) {
|
||||
for &excerpt_id in entries_to_add {
|
||||
let Some(excerpt) = excerpts.get(&excerpt_id) else {
|
||||
continue;
|
||||
};
|
||||
let excerpt_depth = parent_depth + 1;
|
||||
let Some(excerpts) = self.excerpts.get(&buffer_id) else {
|
||||
return;
|
||||
};
|
||||
for &excerpt_id in entries_to_add {
|
||||
let Some(excerpt) = excerpts.get(&excerpt_id) else {
|
||||
continue;
|
||||
};
|
||||
let excerpt_depth = parent_depth + 1;
|
||||
self.push_entry(
|
||||
state,
|
||||
track_matches,
|
||||
PanelEntry::Outline(OutlineEntry::Excerpt(OutlineEntryExcerpt {
|
||||
buffer_id,
|
||||
id: excerpt_id,
|
||||
range: excerpt.range.clone(),
|
||||
})),
|
||||
excerpt_depth,
|
||||
cx,
|
||||
);
|
||||
|
||||
let mut outline_base_depth = excerpt_depth + 1;
|
||||
if is_singleton {
|
||||
outline_base_depth = 0;
|
||||
state.clear();
|
||||
} else if query.is_none()
|
||||
&& self
|
||||
.collapsed_entries
|
||||
.contains(&CollapsedEntry::Excerpt(buffer_id, excerpt_id))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
for outline in excerpt.iter_outlines() {
|
||||
self.push_entry(
|
||||
state,
|
||||
track_matches,
|
||||
PanelEntry::Outline(OutlineEntry::Excerpt(OutlineEntryExcerpt {
|
||||
PanelEntry::Outline(OutlineEntry::Outline(OutlineEntryOutline {
|
||||
buffer_id,
|
||||
id: excerpt_id,
|
||||
range: excerpt.range.clone(),
|
||||
excerpt_id,
|
||||
outline: outline.clone(),
|
||||
})),
|
||||
excerpt_depth,
|
||||
outline_base_depth + outline.depth,
|
||||
cx,
|
||||
);
|
||||
|
||||
let mut outline_base_depth = excerpt_depth + 1;
|
||||
if is_singleton {
|
||||
outline_base_depth = 0;
|
||||
state.clear();
|
||||
} else if query.is_none()
|
||||
&& self
|
||||
.collapsed_entries
|
||||
.contains(&CollapsedEntry::Excerpt(buffer_id, excerpt_id))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
for outline in excerpt.iter_outlines() {
|
||||
self.push_entry(
|
||||
state,
|
||||
track_matches,
|
||||
PanelEntry::Outline(OutlineEntry::Outline(OutlineEntryOutline {
|
||||
buffer_id,
|
||||
excerpt_id,
|
||||
outline: outline.clone(),
|
||||
})),
|
||||
outline_base_depth + outline.depth,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4537,121 +4538,130 @@ impl OutlinePanel {
|
||||
let multi_buffer_snapshot = self
|
||||
.active_editor()
|
||||
.map(|editor| editor.read(cx).buffer().read(cx).snapshot(cx));
|
||||
uniform_list(cx.entity().clone(), "entries", items_len, {
|
||||
move |outline_panel, range, window, cx| {
|
||||
let entries = outline_panel.cached_entries.get(range);
|
||||
entries
|
||||
.map(|entries| entries.to_vec())
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.filter_map(|cached_entry| match cached_entry.entry {
|
||||
PanelEntry::Fs(entry) => Some(outline_panel.render_entry(
|
||||
&entry,
|
||||
cached_entry.depth,
|
||||
cached_entry.string_match.as_ref(),
|
||||
window,
|
||||
cx,
|
||||
)),
|
||||
PanelEntry::FoldedDirs(folded_dirs_entry) => {
|
||||
Some(outline_panel.render_folded_dirs(
|
||||
&folded_dirs_entry,
|
||||
cached_entry.depth,
|
||||
cached_entry.string_match.as_ref(),
|
||||
window,
|
||||
cx,
|
||||
))
|
||||
}
|
||||
PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => {
|
||||
outline_panel.render_excerpt(
|
||||
&excerpt,
|
||||
cached_entry.depth,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
PanelEntry::Outline(OutlineEntry::Outline(entry)) => {
|
||||
Some(outline_panel.render_outline(
|
||||
let outline_panel = cx.entity();
|
||||
let render_item = {
|
||||
move |i: usize, window: &mut Window, cx: &mut App| {
|
||||
outline_panel.update(cx, |outline_panel, cx| {
|
||||
let entry = outline_panel.cached_entries.get(i).cloned();
|
||||
let item = entry
|
||||
.and_then(|cached_entry| match cached_entry.entry {
|
||||
PanelEntry::Fs(entry) => Some(outline_panel.render_entry(
|
||||
&entry,
|
||||
cached_entry.depth,
|
||||
cached_entry.string_match.as_ref(),
|
||||
window,
|
||||
cx,
|
||||
))
|
||||
}
|
||||
PanelEntry::Search(SearchEntry {
|
||||
match_range,
|
||||
render_data,
|
||||
kind,
|
||||
..
|
||||
}) => outline_panel.render_search_match(
|
||||
multi_buffer_snapshot.as_ref(),
|
||||
&match_range,
|
||||
&render_data,
|
||||
kind,
|
||||
cached_entry.depth,
|
||||
cached_entry.string_match.as_ref(),
|
||||
window,
|
||||
cx,
|
||||
),
|
||||
})
|
||||
.collect()
|
||||
)),
|
||||
PanelEntry::FoldedDirs(folded_dirs_entry) => {
|
||||
Some(outline_panel.render_folded_dirs(
|
||||
&folded_dirs_entry,
|
||||
cached_entry.depth,
|
||||
cached_entry.string_match.as_ref(),
|
||||
window,
|
||||
cx,
|
||||
))
|
||||
}
|
||||
PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => {
|
||||
outline_panel.render_excerpt(
|
||||
&excerpt,
|
||||
cached_entry.depth,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
PanelEntry::Outline(OutlineEntry::Outline(entry)) => {
|
||||
Some(outline_panel.render_outline(
|
||||
&entry,
|
||||
cached_entry.depth,
|
||||
cached_entry.string_match.as_ref(),
|
||||
window,
|
||||
cx,
|
||||
))
|
||||
}
|
||||
PanelEntry::Search(SearchEntry {
|
||||
match_range,
|
||||
render_data,
|
||||
kind,
|
||||
..
|
||||
}) => outline_panel.render_search_match(
|
||||
multi_buffer_snapshot.as_ref(),
|
||||
&match_range,
|
||||
&render_data,
|
||||
kind,
|
||||
cached_entry.depth,
|
||||
cached_entry.string_match.as_ref(),
|
||||
window,
|
||||
cx,
|
||||
),
|
||||
})
|
||||
.map(|div| div.into_any_element())
|
||||
.unwrap_or_else(|| div().into_any_element());
|
||||
item
|
||||
})
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
list(ListState::new(
|
||||
items_len,
|
||||
ListAlignment::Top,
|
||||
px(50.0),
|
||||
render_item,
|
||||
))
|
||||
.with_sizing_behavior(ListSizingBehavior::Infer)
|
||||
.with_horizontal_sizing_behavior(ListHorizontalSizingBehavior::Unconstrained)
|
||||
.with_width_from_item(self.max_width_item_index)
|
||||
.track_scroll(self.scroll_handle.clone())
|
||||
.when(show_indent_guides, |list| {
|
||||
list.with_decoration(
|
||||
ui::indent_guides(
|
||||
cx.entity().clone(),
|
||||
px(indent_size),
|
||||
IndentGuideColors::panel(cx),
|
||||
|outline_panel, range, _, _| {
|
||||
let entries = outline_panel.cached_entries.get(range);
|
||||
if let Some(entries) = entries {
|
||||
entries.into_iter().map(|item| item.depth).collect()
|
||||
} else {
|
||||
smallvec::SmallVec::new()
|
||||
}
|
||||
},
|
||||
)
|
||||
.with_render_fn(
|
||||
cx.entity().clone(),
|
||||
move |outline_panel, params, _, _| {
|
||||
const LEFT_OFFSET: Pixels = px(14.);
|
||||
// .with_horizontal_sizing_behavior(ListHorizontalSizingBehavior::Unconstrained)
|
||||
// .with_width_from_item(self.max_width_item_index)
|
||||
// .track_scroll(self.scroll_handle.clone())
|
||||
// .when(show_indent_guides, |list| {
|
||||
// list.with_decoration(
|
||||
// ui::indent_guides(
|
||||
// cx.entity().clone(),
|
||||
// px(indent_size),
|
||||
// IndentGuideColors::panel(cx),
|
||||
// |outline_panel, range, _, _| {
|
||||
// let entries = outline_panel.cached_entries.get(range);
|
||||
// if let Some(entries) = entries {
|
||||
// entries.into_iter().map(|item| item.depth).collect()
|
||||
// } else {
|
||||
// smallvec::SmallVec::new()
|
||||
// }
|
||||
// },
|
||||
// )
|
||||
// .with_render_fn(
|
||||
// cx.entity().clone(),
|
||||
// move |outline_panel, params, _, _| {
|
||||
// const LEFT_OFFSET: Pixels = px(14.);
|
||||
|
||||
let indent_size = params.indent_size;
|
||||
let item_height = params.item_height;
|
||||
let active_indent_guide_ix = find_active_indent_guide_ix(
|
||||
outline_panel,
|
||||
¶ms.indent_guides,
|
||||
);
|
||||
// let indent_size = params.indent_size;
|
||||
// let item_height = params.item_height;
|
||||
// let active_indent_guide_ix = find_active_indent_guide_ix(
|
||||
// outline_panel,
|
||||
// ¶ms.indent_guides,
|
||||
// );
|
||||
|
||||
params
|
||||
.indent_guides
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(ix, layout)| {
|
||||
let bounds = Bounds::new(
|
||||
point(
|
||||
layout.offset.x * indent_size + LEFT_OFFSET,
|
||||
layout.offset.y * item_height,
|
||||
),
|
||||
size(px(1.), layout.length * item_height),
|
||||
);
|
||||
ui::RenderedIndentGuide {
|
||||
bounds,
|
||||
layout,
|
||||
is_active: active_indent_guide_ix == Some(ix),
|
||||
hitbox: None,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
},
|
||||
),
|
||||
)
|
||||
})
|
||||
// params
|
||||
// .indent_guides
|
||||
// .into_iter()
|
||||
// .enumerate()
|
||||
// .map(|(ix, layout)| {
|
||||
// let bounds = Bounds::new(
|
||||
// point(
|
||||
// layout.offset.x * indent_size + LEFT_OFFSET,
|
||||
// layout.offset.y * item_height,
|
||||
// ),
|
||||
// size(px(1.), layout.length * item_height),
|
||||
// );
|
||||
// ui::RenderedIndentGuide {
|
||||
// bounds,
|
||||
// layout,
|
||||
// is_active: active_indent_guide_ix == Some(ix),
|
||||
// hitbox: None,
|
||||
// }
|
||||
// })
|
||||
// .collect()
|
||||
// },
|
||||
// ),
|
||||
// )
|
||||
// })
|
||||
};
|
||||
|
||||
v_flex()
|
||||
@@ -4795,6 +4805,248 @@ fn file_name(path: &Path) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
fn outline_items_containing<T: language::ToOffset>(
|
||||
buffer_snapshot: &BufferSnapshot,
|
||||
range: Range<T>,
|
||||
include_extra_context: bool,
|
||||
theme: Option<&SyntaxTheme>,
|
||||
) -> Option<Vec<OutlineItem<language::Anchor>>> {
|
||||
fn next_outline_item_linewise(
|
||||
buffer_snapshot: &BufferSnapshot,
|
||||
config: &language::OutlineConfig,
|
||||
mat: &language::SyntaxMapMatch,
|
||||
range: &Range<usize>,
|
||||
include_extra_context: bool,
|
||||
theme: Option<&SyntaxTheme>,
|
||||
) -> Option<OutlineItem<language::Point>> {
|
||||
use language::ToTreeSitterPoint;
|
||||
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 {
|
||||
return None;
|
||||
}
|
||||
let beg = language::Point::from_ts_point(item_node.start_position());
|
||||
let end = language::Point::from_ts_point(item_node.end_position());
|
||||
let item_point_range = beg..end;
|
||||
|
||||
let mut open_point = None;
|
||||
let mut close_point = None;
|
||||
let mut buffer_ranges = Vec::new();
|
||||
let mut prev_row = None;
|
||||
for capture in mat.captures {
|
||||
let node_is_name;
|
||||
if capture.index == config.name_capture_ix {
|
||||
node_is_name = true;
|
||||
} else if Some(capture.index) == config.context_capture_ix
|
||||
|| (Some(capture.index) == config.extra_context_capture_ix && include_extra_context)
|
||||
{
|
||||
node_is_name = false;
|
||||
} else {
|
||||
if Some(capture.index) == config.open_capture_ix {
|
||||
open_point = Some(language::Point::from_ts_point(capture.node.end_position()));
|
||||
} else if Some(capture.index) == config.close_capture_ix {
|
||||
close_point = Some(language::Point::from_ts_point(
|
||||
capture.node.start_position(),
|
||||
));
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut range = capture.node.start_byte()..capture.node.end_byte();
|
||||
let start = capture.node.start_position();
|
||||
if capture.node.end_position().row > start.row {
|
||||
range.end = range.start + buffer_snapshot.line_len(start.row as u32) as usize
|
||||
- start.column;
|
||||
}
|
||||
|
||||
let is_new_line = prev_row.is_some_and(|prev| prev != start.row);
|
||||
prev_row.replace(capture.node.end_position().row);
|
||||
|
||||
if !range.is_empty() {
|
||||
buffer_ranges.push((range, node_is_name, is_new_line));
|
||||
}
|
||||
}
|
||||
if buffer_ranges.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let mut text = String::new();
|
||||
let mut highlight_ranges = Vec::new();
|
||||
let mut name_ranges = Vec::new();
|
||||
let mut chunks = buffer_snapshot.chunks(
|
||||
buffer_ranges.first().unwrap().0.start..buffer_ranges.last().unwrap().0.end,
|
||||
true,
|
||||
);
|
||||
let mut last_buffer_range_end = 0;
|
||||
|
||||
for (buffer_range, is_name, is_new_line) in buffer_ranges {
|
||||
let space_added = if !is_new_line {
|
||||
let space_added = !text.is_empty() && buffer_range.start > last_buffer_range_end;
|
||||
if space_added {
|
||||
text.push(' ');
|
||||
}
|
||||
space_added
|
||||
} else {
|
||||
text.push('\n');
|
||||
true
|
||||
};
|
||||
let before_append_len = text.len();
|
||||
let mut offset = buffer_range.start;
|
||||
chunks.seek(buffer_range.clone());
|
||||
for mut chunk in chunks.by_ref() {
|
||||
if chunk.text.len() > buffer_range.end - offset {
|
||||
chunk.text = &chunk.text[0..(buffer_range.end - offset)];
|
||||
offset = buffer_range.end;
|
||||
} else {
|
||||
offset += chunk.text.len();
|
||||
}
|
||||
let style = chunk
|
||||
.syntax_highlight_id
|
||||
.zip(theme)
|
||||
.and_then(|(highlight, theme)| highlight.style(theme));
|
||||
if let Some(style) = style {
|
||||
let start = text.len();
|
||||
let end = start + chunk.text.len();
|
||||
highlight_ranges.push((start..end, style));
|
||||
}
|
||||
text.push_str(chunk.text);
|
||||
if offset >= buffer_range.end {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if is_name {
|
||||
let after_append_len = text.len();
|
||||
let start = if space_added && !name_ranges.is_empty() {
|
||||
before_append_len - 1
|
||||
} else {
|
||||
before_append_len
|
||||
};
|
||||
name_ranges.push(start..after_append_len);
|
||||
}
|
||||
last_buffer_range_end = buffer_range.end;
|
||||
}
|
||||
|
||||
Some(OutlineItem {
|
||||
depth: 0, // We'll calculate the depth later
|
||||
range: item_point_range,
|
||||
text,
|
||||
highlight_ranges,
|
||||
name_ranges,
|
||||
body_range: open_point.zip(close_point).map(|(start, end)| start..end),
|
||||
annotation_range: None,
|
||||
})
|
||||
}
|
||||
let range = range.to_offset(buffer_snapshot);
|
||||
let mut matches =
|
||||
buffer_snapshot
|
||||
.syntax
|
||||
.matches(range.clone(), &buffer_snapshot.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<_>>();
|
||||
|
||||
let mut items = Vec::new();
|
||||
let mut annotation_row_ranges: Vec<Range<u32>> = Vec::new();
|
||||
while let Some(mat) = matches.peek() {
|
||||
let config = &configs[mat.grammar_index];
|
||||
if let Some(item) = next_outline_item_linewise(
|
||||
&buffer_snapshot,
|
||||
config,
|
||||
&mat,
|
||||
&range,
|
||||
include_extra_context,
|
||||
theme,
|
||||
) {
|
||||
items.push(item);
|
||||
} else if let Some(capture) = mat
|
||||
.captures
|
||||
.iter()
|
||||
.find(|capture| Some(capture.index) == config.annotation_capture_ix)
|
||||
{
|
||||
let capture_range = capture.node.start_position()..capture.node.end_position();
|
||||
let mut capture_row_range =
|
||||
capture_range.start.row as u32..capture_range.end.row as u32;
|
||||
if capture_range.end.row > capture_range.start.row && capture_range.end.column == 0 {
|
||||
capture_row_range.end -= 1;
|
||||
}
|
||||
if let Some(last_row_range) = annotation_row_ranges.last_mut() {
|
||||
if last_row_range.end >= capture_row_range.start.saturating_sub(1) {
|
||||
last_row_range.end = capture_row_range.end;
|
||||
} else {
|
||||
annotation_row_ranges.push(capture_row_range);
|
||||
}
|
||||
} else {
|
||||
annotation_row_ranges.push(capture_row_range);
|
||||
}
|
||||
}
|
||||
matches.advance();
|
||||
}
|
||||
|
||||
items.sort_by_key(|item| (item.range.start, std::cmp::Reverse(item.range.end)));
|
||||
|
||||
// Assign depths based on containment relationships and convert to anchors.
|
||||
let mut item_ends_stack = Vec::<language::Point>::new();
|
||||
let mut anchor_items = Vec::new();
|
||||
let mut annotation_row_ranges = annotation_row_ranges.into_iter().peekable();
|
||||
for item in items {
|
||||
while let Some(last_end) = item_ends_stack.last().copied() {
|
||||
if last_end < item.range.end {
|
||||
item_ends_stack.pop();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let mut annotation_row_range = None;
|
||||
while let Some(next_annotation_row_range) = annotation_row_ranges.peek() {
|
||||
let row_preceding_item = item.range.start.row.saturating_sub(1);
|
||||
if next_annotation_row_range.end < row_preceding_item {
|
||||
annotation_row_ranges.next();
|
||||
} else {
|
||||
if next_annotation_row_range.end == row_preceding_item {
|
||||
annotation_row_range = Some(next_annotation_row_range.clone());
|
||||
annotation_row_ranges.next();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
anchor_items.push(OutlineItem {
|
||||
depth: item_ends_stack.len(),
|
||||
range: buffer_snapshot.anchor_after(item.range.start)
|
||||
..buffer_snapshot.anchor_before(item.range.end),
|
||||
text: item.text,
|
||||
highlight_ranges: item.highlight_ranges,
|
||||
name_ranges: item.name_ranges,
|
||||
body_range: item.body_range.map(|body_range| {
|
||||
buffer_snapshot.anchor_after(body_range.start)
|
||||
..buffer_snapshot.anchor_before(body_range.end)
|
||||
}),
|
||||
annotation_range: annotation_row_range.map(|annotation_range| {
|
||||
buffer_snapshot.anchor_after(language::Point::new(annotation_range.start, 0))
|
||||
..buffer_snapshot.anchor_before(language::Point::new(
|
||||
annotation_range.end,
|
||||
buffer_snapshot.line_len(annotation_range.end),
|
||||
))
|
||||
}),
|
||||
});
|
||||
item_ends_stack.push(item.range.end);
|
||||
}
|
||||
|
||||
Some(anchor_items)
|
||||
}
|
||||
|
||||
impl Panel for OutlinePanel {
|
||||
fn persistent_name() -> &'static str {
|
||||
"Outline Panel"
|
||||
@@ -4991,7 +5243,7 @@ impl Render for OutlinePanel {
|
||||
.border_color(cx.theme().colors().border)
|
||||
.gap_0p5()
|
||||
.child(Label::new("Searching:").color(Color::Muted))
|
||||
.child(Label::new(search_state.query.to_string())),
|
||||
.child(Label::new(format!("'{}'", search_state.query))),
|
||||
)
|
||||
})
|
||||
.child(self.render_main_contents(query, show_indent_guides, indent_size, window, cx))
|
||||
|
||||
@@ -417,6 +417,14 @@ pub fn local_vscode_launch_file_relative_path() -> &'static Path {
|
||||
Path::new(".vscode/launch.json")
|
||||
}
|
||||
|
||||
pub fn user_ssh_config_file() -> PathBuf {
|
||||
home_dir().join(".ssh/config")
|
||||
}
|
||||
|
||||
pub fn global_ssh_config_file() -> &'static Path {
|
||||
Path::new("/etc/ssh/ssh_config")
|
||||
}
|
||||
|
||||
/// Returns the path to the vscode user settings file
|
||||
pub fn vscode_settings_file() -> &'static PathBuf {
|
||||
static LOGS_DIR: OnceLock<PathBuf> = OnceLock::new();
|
||||
|
||||
@@ -64,7 +64,6 @@ pub enum DapStoreEvent {
|
||||
RemoteHasInitialized,
|
||||
}
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
enum DapStoreMode {
|
||||
Local(LocalDapStore),
|
||||
Ssh(SshDapStore),
|
||||
|
||||
@@ -163,7 +163,7 @@ struct LocalDownstreamState {
|
||||
_task: Task<Result<()>>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct GitStoreCheckpoint {
|
||||
checkpoints_by_work_dir_abs_path: HashMap<Arc<Path>, GitRepositoryCheckpoint>,
|
||||
}
|
||||
|
||||
@@ -3415,7 +3415,6 @@ pub struct RemoteLspStore {
|
||||
upstream_project_id: u64,
|
||||
}
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub(crate) enum LspStoreMode {
|
||||
Local(LocalLspStore), // ssh host and collab host
|
||||
Remote(RemoteLspStore), // collab guest
|
||||
@@ -8806,9 +8805,10 @@ impl LspStore {
|
||||
})
|
||||
});
|
||||
|
||||
let is_unnecessary = diagnostic.tags.as_ref().map_or(false, |tags| {
|
||||
tags.iter().any(|tag| *tag == DiagnosticTag::UNNECESSARY)
|
||||
});
|
||||
let is_unnecessary = diagnostic
|
||||
.tags
|
||||
.as_ref()
|
||||
.map_or(false, |tags| tags.contains(&DiagnosticTag::UNNECESSARY));
|
||||
|
||||
if is_supporting {
|
||||
supporting_diagnostics.insert(
|
||||
|
||||
@@ -98,7 +98,7 @@ impl<Label: Ord + Clone> RootPathTrie<Label> {
|
||||
};
|
||||
}
|
||||
if !current.labels.is_empty() {
|
||||
(callback)(¤t.worktree_relative_path, ¤t.labels);
|
||||
let _ = (callback)(¤t.worktree_relative_path, ¤t.labels);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3653,7 +3653,7 @@ impl Project {
|
||||
let mut buffer_count = 0;
|
||||
let mut limit_reached = false;
|
||||
let query = Arc::new(query);
|
||||
let mut chunks = matching_buffers_rx.ready_chunks(64);
|
||||
let chunks = matching_buffers_rx.ready_chunks(64);
|
||||
|
||||
// Now that we know what paths match the query, we will load at most
|
||||
// 64 buffers at a time to avoid overwhelming the main thread. For each
|
||||
@@ -4392,7 +4392,7 @@ impl Project {
|
||||
envelope: TypedEnvelope<proto::LanguageServerPromptRequest>,
|
||||
mut cx: AsyncApp,
|
||||
) -> Result<proto::LanguageServerPromptResponse> {
|
||||
let (tx, mut rx) = smol::channel::bounded(1);
|
||||
let (tx, rx) = smol::channel::bounded(1);
|
||||
let actions: Vec<_> = envelope
|
||||
.payload
|
||||
.actions
|
||||
|
||||
@@ -21,7 +21,7 @@ use crate::{
|
||||
worktree_store::WorktreeStore,
|
||||
};
|
||||
|
||||
#[allow(clippy::large_enum_variant)] // platform-dependent warning
|
||||
// platform-dependent warning
|
||||
pub enum TaskStore {
|
||||
Functional(StoreState),
|
||||
Noop,
|
||||
|
||||
@@ -24,7 +24,7 @@ pub struct Terminals {
|
||||
}
|
||||
|
||||
/// Terminals are opened either for the users shell, or to run a task.
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum TerminalKind {
|
||||
/// Run a shell at the given path (or $HOME if None)
|
||||
|
||||
@@ -876,7 +876,7 @@ impl WorktreeStore {
|
||||
|
||||
async fn filter_paths(
|
||||
fs: &Arc<dyn Fs>,
|
||||
mut input: Receiver<MatchingEntry>,
|
||||
input: Receiver<MatchingEntry>,
|
||||
query: &SearchQuery,
|
||||
) -> Result<()> {
|
||||
let mut input = pin!(input);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user