Compare commits
47 Commits
inline-ass
...
v0.209.7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8f1d050bfa | ||
|
|
d54e024f94 | ||
|
|
3d61fb08a0 | ||
|
|
419f9a865c | ||
|
|
68bb196071 | ||
|
|
12acc1db10 | ||
|
|
34a35b8443 | ||
|
|
bdd0f2fb0a | ||
|
|
4c8d47e425 | ||
|
|
35b2a6797f | ||
|
|
4cf4794ac7 | ||
|
|
d2b0c6002b | ||
|
|
898975660a | ||
|
|
be4bfa3bfe | ||
|
|
55ffc41b1c | ||
|
|
b631e813e4 | ||
|
|
5e64237b59 | ||
|
|
e82a6b5c3d | ||
|
|
8ae69036b8 | ||
|
|
562edf45ef | ||
|
|
3eb1952a6c | ||
|
|
a808ecb503 | ||
|
|
b69d0ab1be | ||
|
|
eb33d3009c | ||
|
|
caa5d624ea | ||
|
|
de7e0b47ba | ||
|
|
bebf4b0497 | ||
|
|
fe00b6cb53 | ||
|
|
70d197876e | ||
|
|
10c540bf20 | ||
|
|
bb7de4ee04 | ||
|
|
87b9b9f452 | ||
|
|
e17609c938 | ||
|
|
353e936a7a | ||
|
|
1d89ddb776 | ||
|
|
c8903a0010 | ||
|
|
ac002d0a7f | ||
|
|
66112d81b0 | ||
|
|
c3eaa757e8 | ||
|
|
2793dd77ad | ||
|
|
10720d64a3 | ||
|
|
2adb979ed7 | ||
|
|
bec6cd94a4 | ||
|
|
f6c0fa43ef | ||
|
|
62da0dc402 | ||
|
|
154405ff3b | ||
|
|
b558313181 |
3
.github/workflows/ci.yml
vendored
3
.github/workflows/ci.yml
vendored
@@ -882,7 +882,8 @@ jobs:
|
||||
auto-release-preview:
|
||||
name: Auto release preview
|
||||
if: |
|
||||
startsWith(github.ref, 'refs/tags/v')
|
||||
false
|
||||
&& startsWith(github.ref, 'refs/tags/v')
|
||||
&& endsWith(github.ref, '-pre') && !endsWith(github.ref, '.0-pre')
|
||||
needs: [bundle-mac, bundle-linux-x86_x64, bundle-linux-aarch64, bundle-windows-x64]
|
||||
runs-on:
|
||||
|
||||
20
.github/workflows/community_release_actions.yml
vendored
20
.github/workflows/community_release_actions.yml
vendored
@@ -38,6 +38,26 @@ jobs:
|
||||
webhook-url: ${{ secrets.DISCORD_WEBHOOK_RELEASE_NOTES }}
|
||||
content: ${{ steps.get-content.outputs.string }}
|
||||
|
||||
publish-winget:
|
||||
runs-on:
|
||||
- ubuntu-latest
|
||||
steps:
|
||||
- name: Set Package Name
|
||||
id: set-package-name
|
||||
run: |
|
||||
if [ "${{ github.event.release.prerelease }}" == "true" ]; then
|
||||
PACKAGE_NAME=ZedIndustries.Zed.Preview
|
||||
else
|
||||
PACKAGE_NAME=ZedIndustries.Zed
|
||||
fi
|
||||
|
||||
echo "PACKAGE_NAME=$PACKAGE_NAME" >> "$GITHUB_OUTPUT"
|
||||
- uses: vedantmgoyal9/winget-releaser@19e706d4c9121098010096f9c495a70a7518b30f # v2
|
||||
with:
|
||||
identifier: ${{ steps.set-package-name.outputs.PACKAGE_NAME }}
|
||||
max-versions-to-keep: 5
|
||||
token: ${{ secrets.WINGET_TOKEN }}
|
||||
|
||||
send_release_notes_email:
|
||||
if: false && github.repository_owner == 'zed-industries' && !github.event.release.prerelease
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
5
Cargo.lock
generated
5
Cargo.lock
generated
@@ -1336,8 +1336,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "async-tar"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a42f905d4f623faf634bbd1e001e84e0efc24694afa64be9ad239bf6ca49e1f8"
|
||||
source = "git+https://github.com/zed-industries/async-tar?rev=8af312477196311c9ea4097f2a22022f6d609bf6#8af312477196311c9ea4097f2a22022f6d609bf6"
|
||||
dependencies = [
|
||||
"async-std",
|
||||
"filetime",
|
||||
@@ -21203,7 +21202,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.209.0"
|
||||
version = "0.209.7"
|
||||
dependencies = [
|
||||
"acp_tools",
|
||||
"activity_indicator",
|
||||
|
||||
@@ -457,7 +457,7 @@ async-dispatcher = "0.1"
|
||||
async-fs = "2.1"
|
||||
async-pipe = { git = "https://github.com/zed-industries/async-pipe-rs", rev = "82d00a04211cf4e1236029aa03e6b6ce2a74c553" }
|
||||
async-recursion = "1.0.0"
|
||||
async-tar = "0.5.0"
|
||||
async-tar = { git = "https://github.com/zed-industries/async-tar", rev = "8af312477196311c9ea4097f2a22022f6d609bf6" }
|
||||
async-task = "4.7"
|
||||
async-trait = "0.1"
|
||||
async-tungstenite = "0.29.1"
|
||||
|
||||
@@ -1527,6 +1527,7 @@
|
||||
// A value of 45 preserves colorful themes while ensuring legibility.
|
||||
"minimum_contrast": 45
|
||||
},
|
||||
"code_actions_on_format": {},
|
||||
// Settings related to running tasks.
|
||||
"tasks": {
|
||||
"variables": {},
|
||||
@@ -1696,7 +1697,9 @@
|
||||
"preferred_line_length": 72
|
||||
},
|
||||
"Go": {
|
||||
"formatter": [{ "code_action": "source.organizeImports" }, "language_server"],
|
||||
"code_actions_on_format": {
|
||||
"source.organizeImports": true
|
||||
},
|
||||
"debuggers": ["Delve"]
|
||||
},
|
||||
"GraphQL": {
|
||||
@@ -1735,7 +1738,7 @@
|
||||
}
|
||||
},
|
||||
"Kotlin": {
|
||||
"language_servers": ["kotlin-language-server", "!kotlin-lsp", "..."]
|
||||
"language_servers": ["!kotlin-language-server", "kotlin-lsp", "..."]
|
||||
},
|
||||
"LaTeX": {
|
||||
"formatter": "language_server",
|
||||
@@ -1813,10 +1816,11 @@
|
||||
},
|
||||
"SystemVerilog": {
|
||||
"format_on_save": "off",
|
||||
"language_servers": ["!slang", "..."],
|
||||
"use_on_type_format": false
|
||||
},
|
||||
"Vue.js": {
|
||||
"language_servers": ["vue-language-server", "..."],
|
||||
"language_servers": ["vue-language-server", "vtsls", "..."],
|
||||
"prettier": {
|
||||
"allowed": true
|
||||
}
|
||||
|
||||
@@ -328,7 +328,7 @@ impl ToolCall {
|
||||
location: acp::ToolCallLocation,
|
||||
project: WeakEntity<Project>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Option<AgentLocation> {
|
||||
) -> Option<ResolvedLocation> {
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| {
|
||||
project
|
||||
@@ -350,17 +350,14 @@ impl ToolCall {
|
||||
})
|
||||
.ok()?;
|
||||
|
||||
Some(AgentLocation {
|
||||
buffer: buffer.downgrade(),
|
||||
position,
|
||||
})
|
||||
Some(ResolvedLocation { buffer, position })
|
||||
}
|
||||
|
||||
fn resolve_locations(
|
||||
&self,
|
||||
project: Entity<Project>,
|
||||
cx: &mut App,
|
||||
) -> Task<Vec<Option<AgentLocation>>> {
|
||||
) -> Task<Vec<Option<ResolvedLocation>>> {
|
||||
let locations = self.locations.clone();
|
||||
project.update(cx, |_, cx| {
|
||||
cx.spawn(async move |project, cx| {
|
||||
@@ -374,6 +371,23 @@ impl ToolCall {
|
||||
}
|
||||
}
|
||||
|
||||
// Separate so we can hold a strong reference to the buffer
|
||||
// for saving on the thread
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct ResolvedLocation {
|
||||
buffer: Entity<Buffer>,
|
||||
position: Anchor,
|
||||
}
|
||||
|
||||
impl From<&ResolvedLocation> for AgentLocation {
|
||||
fn from(value: &ResolvedLocation) -> Self {
|
||||
Self {
|
||||
buffer: value.buffer.downgrade(),
|
||||
position: value.position,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ToolCallStatus {
|
||||
/// The tool call hasn't started running yet, but we start showing it to
|
||||
@@ -1393,35 +1407,46 @@ impl AcpThread {
|
||||
let task = tool_call.resolve_locations(project, cx);
|
||||
cx.spawn(async move |this, cx| {
|
||||
let resolved_locations = task.await;
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
let project = this.project.clone();
|
||||
|
||||
for location in resolved_locations.iter().flatten() {
|
||||
this.shared_buffers
|
||||
.insert(location.buffer.clone(), location.buffer.read(cx).snapshot());
|
||||
}
|
||||
let Some((ix, tool_call)) = this.tool_call_mut(&id) else {
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some(Some(location)) = resolved_locations.last() {
|
||||
project.update(cx, |project, cx| {
|
||||
if let Some(agent_location) = project.agent_location() {
|
||||
let should_ignore = agent_location.buffer == location.buffer
|
||||
&& location
|
||||
.buffer
|
||||
.update(cx, |buffer, _| {
|
||||
let snapshot = buffer.snapshot();
|
||||
let old_position =
|
||||
agent_location.position.to_point(&snapshot);
|
||||
let new_position = location.position.to_point(&snapshot);
|
||||
// ignore this so that when we get updates from the edit tool
|
||||
// the position doesn't reset to the startof line
|
||||
old_position.row == new_position.row
|
||||
&& old_position.column > new_position.column
|
||||
})
|
||||
.ok()
|
||||
.unwrap_or_default();
|
||||
if !should_ignore {
|
||||
project.set_agent_location(Some(location.clone()), cx);
|
||||
}
|
||||
let should_ignore = if let Some(agent_location) = project
|
||||
.agent_location()
|
||||
.filter(|agent_location| agent_location.buffer == location.buffer)
|
||||
{
|
||||
let snapshot = location.buffer.read(cx).snapshot();
|
||||
let old_position = agent_location.position.to_point(&snapshot);
|
||||
let new_position = location.position.to_point(&snapshot);
|
||||
|
||||
// ignore this so that when we get updates from the edit tool
|
||||
// the position doesn't reset to the startof line
|
||||
old_position.row == new_position.row
|
||||
&& old_position.column > new_position.column
|
||||
} else {
|
||||
false
|
||||
};
|
||||
if !should_ignore {
|
||||
project.set_agent_location(Some(location.into()), cx);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let resolved_locations = resolved_locations
|
||||
.iter()
|
||||
.map(|l| l.as_ref().map(|l| AgentLocation::from(l)))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if tool_call.resolved_locations != resolved_locations {
|
||||
tool_call.resolved_locations = resolved_locations;
|
||||
cx.emit(AcpThreadEvent::EntryUpdated(ix));
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
use std::{any::Any, path::Path};
|
||||
|
||||
use crate::{AgentServer, AgentServerDelegate, load_proxy_env};
|
||||
use acp_thread::AgentConnection;
|
||||
use agent_client_protocol as acp;
|
||||
use anyhow::{Context as _, Result};
|
||||
use gpui::{App, SharedString, Task};
|
||||
use project::agent_server_store::CODEX_NAME;
|
||||
use fs::Fs;
|
||||
use gpui::{App, AppContext as _, SharedString, Task};
|
||||
use project::agent_server_store::{AllAgentServersSettings, CODEX_NAME};
|
||||
use settings::{SettingsStore, update_settings_file};
|
||||
|
||||
use crate::{AgentServer, AgentServerDelegate, load_proxy_env};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Codex;
|
||||
@@ -30,6 +35,27 @@ impl AgentServer for Codex {
|
||||
ui::IconName::AiOpenAi
|
||||
}
|
||||
|
||||
fn default_mode(&self, cx: &mut App) -> Option<acp::SessionModeId> {
|
||||
let settings = cx.read_global(|settings: &SettingsStore, _| {
|
||||
settings.get::<AllAgentServersSettings>(None).codex.clone()
|
||||
});
|
||||
|
||||
settings
|
||||
.as_ref()
|
||||
.and_then(|s| s.default_mode.clone().map(|m| acp::SessionModeId(m.into())))
|
||||
}
|
||||
|
||||
fn set_default_mode(&self, mode_id: Option<acp::SessionModeId>, fs: Arc<dyn Fs>, cx: &mut App) {
|
||||
update_settings_file(fs, cx, |settings, _| {
|
||||
settings
|
||||
.agent_servers
|
||||
.get_or_insert_default()
|
||||
.codex
|
||||
.get_or_insert_default()
|
||||
.default_mode = mode_id.map(|m| m.to_string())
|
||||
});
|
||||
}
|
||||
|
||||
fn connect(
|
||||
&self,
|
||||
root_dir: Option<&Path>,
|
||||
|
||||
@@ -278,7 +278,7 @@ pub struct AcpThreadView {
|
||||
thread_feedback: ThreadFeedbackState,
|
||||
list_state: ListState,
|
||||
auth_task: Option<Task<()>>,
|
||||
collapsed_tool_calls: HashSet<acp::ToolCallId>,
|
||||
expanded_tool_calls: HashSet<acp::ToolCallId>,
|
||||
expanded_thinking_blocks: HashSet<(usize, usize)>,
|
||||
edits_expanded: bool,
|
||||
plan_expanded: bool,
|
||||
@@ -292,6 +292,8 @@ pub struct AcpThreadView {
|
||||
resume_thread_metadata: Option<DbThreadMetadata>,
|
||||
_cancel_task: Option<Task<()>>,
|
||||
_subscriptions: [Subscription; 5],
|
||||
#[cfg(target_os = "windows")]
|
||||
show_codex_windows_warning: bool,
|
||||
}
|
||||
|
||||
enum ThreadState {
|
||||
@@ -394,6 +396,10 @@ impl AcpThreadView {
|
||||
),
|
||||
];
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
let show_codex_windows_warning = crate::ExternalAgent::parse_built_in(agent.as_ref())
|
||||
== Some(crate::ExternalAgent::Codex);
|
||||
|
||||
Self {
|
||||
agent: agent.clone(),
|
||||
workspace: workspace.clone(),
|
||||
@@ -419,7 +425,7 @@ impl AcpThreadView {
|
||||
thread_error: None,
|
||||
thread_feedback: Default::default(),
|
||||
auth_task: None,
|
||||
collapsed_tool_calls: HashSet::default(),
|
||||
expanded_tool_calls: HashSet::default(),
|
||||
expanded_thinking_blocks: HashSet::default(),
|
||||
editing_message: None,
|
||||
edits_expanded: false,
|
||||
@@ -436,6 +442,8 @@ impl AcpThreadView {
|
||||
focus_handle: cx.focus_handle(),
|
||||
new_server_version_available: None,
|
||||
resume_thread_metadata: resume_thread,
|
||||
#[cfg(target_os = "windows")]
|
||||
show_codex_windows_warning,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -954,17 +962,17 @@ impl AcpThreadView {
|
||||
) {
|
||||
match &event.view_event {
|
||||
ViewEvent::NewDiff(tool_call_id) => {
|
||||
if !AgentSettings::get_global(cx).expand_edit_card {
|
||||
self.collapsed_tool_calls.insert(tool_call_id.clone());
|
||||
if AgentSettings::get_global(cx).expand_edit_card {
|
||||
self.expanded_tool_calls.insert(tool_call_id.clone());
|
||||
}
|
||||
}
|
||||
ViewEvent::NewTerminal(tool_call_id) => {
|
||||
if !AgentSettings::get_global(cx).expand_terminal_card {
|
||||
self.collapsed_tool_calls.insert(tool_call_id.clone());
|
||||
if AgentSettings::get_global(cx).expand_terminal_card {
|
||||
self.expanded_tool_calls.insert(tool_call_id.clone());
|
||||
}
|
||||
}
|
||||
ViewEvent::TerminalMovedToBackground(tool_call_id) => {
|
||||
self.collapsed_tool_calls.insert(tool_call_id.clone());
|
||||
self.expanded_tool_calls.remove(tool_call_id);
|
||||
}
|
||||
ViewEvent::MessageEditorEvent(_editor, MessageEditorEvent::Focus) => {
|
||||
if let Some(thread) = self.thread()
|
||||
@@ -1045,9 +1053,6 @@ impl AcpThreadView {
|
||||
return;
|
||||
};
|
||||
|
||||
self.message_editor
|
||||
.update(cx, |editor, cx| editor.clear(window, cx));
|
||||
|
||||
let connection = thread.read(cx).connection().clone();
|
||||
let can_login = !connection.auth_methods().is_empty() || self.login.is_some();
|
||||
// Does the agent have a specific logout command? Prefer that in case they need to reset internal state.
|
||||
@@ -1058,6 +1063,9 @@ impl AcpThreadView {
|
||||
.iter()
|
||||
.any(|command| command.name == "logout");
|
||||
if can_login && !logout_supported {
|
||||
self.message_editor
|
||||
.update(cx, |editor, cx| editor.clear(window, cx));
|
||||
|
||||
let this = cx.weak_entity();
|
||||
let agent = self.agent.clone();
|
||||
window.defer(cx, |window, cx| {
|
||||
@@ -2119,7 +2127,7 @@ impl AcpThreadView {
|
||||
|
||||
let is_collapsible = !tool_call.content.is_empty() && !needs_confirmation;
|
||||
|
||||
let is_open = needs_confirmation || !self.collapsed_tool_calls.contains(&tool_call.id);
|
||||
let is_open = needs_confirmation || self.expanded_tool_calls.contains(&tool_call.id);
|
||||
|
||||
let tool_output_display =
|
||||
if is_open {
|
||||
@@ -2269,9 +2277,9 @@ impl AcpThreadView {
|
||||
let id = tool_call.id.clone();
|
||||
move |this: &mut Self, _, _, cx: &mut Context<Self>| {
|
||||
if is_open {
|
||||
this.collapsed_tool_calls.insert(id.clone());
|
||||
this.expanded_tool_calls.remove(&id);
|
||||
} else {
|
||||
this.collapsed_tool_calls.remove(&id);
|
||||
this.expanded_tool_calls.insert(id.clone());
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
@@ -2473,7 +2481,7 @@ impl AcpThreadView {
|
||||
.icon_color(Color::Muted)
|
||||
.on_click(cx.listener({
|
||||
move |this: &mut Self, _, _, cx: &mut Context<Self>| {
|
||||
this.collapsed_tool_calls.insert(tool_call_id.clone());
|
||||
this.expanded_tool_calls.remove(&tool_call_id);
|
||||
cx.notify();
|
||||
}
|
||||
})),
|
||||
@@ -2751,7 +2759,7 @@ impl AcpThreadView {
|
||||
.map(|path| path.display().to_string())
|
||||
.unwrap_or_else(|| "current directory".to_string());
|
||||
|
||||
let is_expanded = !self.collapsed_tool_calls.contains(&tool_call.id);
|
||||
let is_expanded = self.expanded_tool_calls.contains(&tool_call.id);
|
||||
|
||||
let header = h_flex()
|
||||
.id(header_id)
|
||||
@@ -2886,9 +2894,9 @@ impl AcpThreadView {
|
||||
let id = tool_call.id.clone();
|
||||
move |this, _event, _window, _cx| {
|
||||
if is_expanded {
|
||||
this.collapsed_tool_calls.insert(id.clone());
|
||||
this.expanded_tool_calls.remove(&id);
|
||||
} else {
|
||||
this.collapsed_tool_calls.remove(&id);
|
||||
this.expanded_tool_calls.insert(id.clone());
|
||||
}
|
||||
}
|
||||
})),
|
||||
@@ -5025,6 +5033,49 @@ impl AcpThreadView {
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn render_codex_windows_warning(&self, cx: &mut Context<Self>) -> Option<Callout> {
|
||||
if self.show_codex_windows_warning {
|
||||
Some(
|
||||
Callout::new()
|
||||
.icon(IconName::Warning)
|
||||
.severity(Severity::Warning)
|
||||
.title("Codex on Windows")
|
||||
.description(
|
||||
"For best performance, run Codex in Windows Subsystem for Linux (WSL2)",
|
||||
)
|
||||
.actions_slot(
|
||||
Button::new("open-wsl-modal", "Open in WSL")
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.on_click(cx.listener({
|
||||
move |_, _, window, cx| {
|
||||
window.dispatch_action(
|
||||
zed_actions::wsl_actions::OpenWsl::default().boxed_clone(),
|
||||
cx,
|
||||
);
|
||||
cx.notify();
|
||||
}
|
||||
})),
|
||||
)
|
||||
.dismiss_action(
|
||||
IconButton::new("dismiss", IconName::Close)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.tooltip(Tooltip::text("Dismiss Warning"))
|
||||
.on_click(cx.listener({
|
||||
move |this, _, _, cx| {
|
||||
this.show_codex_windows_warning = false;
|
||||
cx.notify();
|
||||
}
|
||||
})),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn render_thread_error(&self, window: &mut Window, cx: &mut Context<Self>) -> Option<Div> {
|
||||
let content = match self.thread_error.as_ref()? {
|
||||
ThreadError::Other(error) => self.render_any_thread_error(error.clone(), cx),
|
||||
@@ -5512,6 +5563,16 @@ impl Render for AcpThreadView {
|
||||
_ => this,
|
||||
})
|
||||
.children(self.render_thread_retry_status_callout(window, cx))
|
||||
.children({
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
self.render_codex_windows_warning(cx)
|
||||
}
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
Vec::<Empty>::new()
|
||||
}
|
||||
})
|
||||
.children(self.render_thread_error(window, cx))
|
||||
.when_some(
|
||||
self.new_server_version_available.as_ref().filter(|_| {
|
||||
|
||||
@@ -222,12 +222,11 @@ enum WhichFontSize {
|
||||
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum AgentType {
|
||||
#[default]
|
||||
Zed,
|
||||
NativeAgent,
|
||||
TextThread,
|
||||
Gemini,
|
||||
ClaudeCode,
|
||||
Codex,
|
||||
NativeAgent,
|
||||
Custom {
|
||||
name: SharedString,
|
||||
command: AgentServerCommand,
|
||||
@@ -237,8 +236,7 @@ pub enum AgentType {
|
||||
impl AgentType {
|
||||
fn label(&self) -> SharedString {
|
||||
match self {
|
||||
Self::Zed | Self::TextThread => "Zed Agent".into(),
|
||||
Self::NativeAgent => "Agent 2".into(),
|
||||
Self::NativeAgent | Self::TextThread => "Zed Agent".into(),
|
||||
Self::Gemini => "Gemini CLI".into(),
|
||||
Self::ClaudeCode => "Claude Code".into(),
|
||||
Self::Codex => "Codex".into(),
|
||||
@@ -248,7 +246,7 @@ impl AgentType {
|
||||
|
||||
fn icon(&self) -> Option<IconName> {
|
||||
match self {
|
||||
Self::Zed | Self::NativeAgent | Self::TextThread => None,
|
||||
Self::NativeAgent | Self::TextThread => None,
|
||||
Self::Gemini => Some(IconName::AiGemini),
|
||||
Self::ClaudeCode => Some(IconName::AiClaude),
|
||||
Self::Codex => Some(IconName::AiOpenAi),
|
||||
@@ -813,7 +811,7 @@ impl AgentPanel {
|
||||
|
||||
const LAST_USED_EXTERNAL_AGENT_KEY: &str = "agent_panel__last_used_external_agent";
|
||||
|
||||
#[derive(Default, Serialize, Deserialize)]
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct LastUsedExternalAgent {
|
||||
agent: crate::ExternalAgent,
|
||||
}
|
||||
@@ -854,18 +852,18 @@ impl AgentPanel {
|
||||
.and_then(|value| {
|
||||
serde_json::from_str::<LastUsedExternalAgent>(&value).log_err()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
.agent
|
||||
.map(|agent| agent.agent)
|
||||
.unwrap_or(ExternalAgent::NativeAgent)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if !loading {
|
||||
telemetry::event!("Agent Thread Started", agent = ext_agent.name());
|
||||
}
|
||||
|
||||
let server = ext_agent.server(fs, history);
|
||||
|
||||
if !loading {
|
||||
telemetry::event!("Agent Thread Started", agent = server.telemetry_id());
|
||||
}
|
||||
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
let selected_agent = ext_agent.into();
|
||||
if this.selected_agent != selected_agent {
|
||||
@@ -1345,15 +1343,6 @@ impl AgentPanel {
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
match agent {
|
||||
AgentType::Zed => {
|
||||
window.dispatch_action(
|
||||
NewThread {
|
||||
from_thread_id: None,
|
||||
}
|
||||
.boxed_clone(),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
AgentType::TextThread => {
|
||||
window.dispatch_action(NewTextThread.boxed_clone(), cx);
|
||||
}
|
||||
|
||||
@@ -161,10 +161,9 @@ pub struct NewNativeAgentThreadFromSummary {
|
||||
}
|
||||
|
||||
// TODO unify this with AgentType
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
enum ExternalAgent {
|
||||
#[default]
|
||||
pub enum ExternalAgent {
|
||||
Gemini,
|
||||
ClaudeCode,
|
||||
Codex,
|
||||
@@ -184,13 +183,13 @@ fn placeholder_command() -> AgentServerCommand {
|
||||
}
|
||||
|
||||
impl ExternalAgent {
|
||||
fn name(&self) -> &'static str {
|
||||
match self {
|
||||
Self::NativeAgent => "zed",
|
||||
Self::Gemini => "gemini-cli",
|
||||
Self::ClaudeCode => "claude-code",
|
||||
Self::Codex => "codex",
|
||||
Self::Custom { .. } => "custom",
|
||||
pub fn parse_built_in(server: &dyn agent_servers::AgentServer) -> Option<Self> {
|
||||
match server.telemetry_id() {
|
||||
"gemini-cli" => Some(Self::Gemini),
|
||||
"claude-code" => Some(Self::ClaudeCode),
|
||||
"codex" => Some(Self::Codex),
|
||||
"zed" => Some(Self::NativeAgent),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -84,10 +84,32 @@ impl ZedAiOnboarding {
|
||||
self
|
||||
}
|
||||
|
||||
fn render_dismiss_button(&self) -> Option<AnyElement> {
|
||||
self.dismiss_onboarding.as_ref().map(|dismiss_callback| {
|
||||
let callback = dismiss_callback.clone();
|
||||
|
||||
h_flex()
|
||||
.absolute()
|
||||
.top_0()
|
||||
.right_0()
|
||||
.child(
|
||||
IconButton::new("dismiss_onboarding", IconName::Close)
|
||||
.icon_size(IconSize::Small)
|
||||
.tooltip(Tooltip::text("Dismiss"))
|
||||
.on_click(move |_, window, cx| {
|
||||
telemetry::event!("Banner Dismissed", source = "AI Onboarding",);
|
||||
callback(window, cx)
|
||||
}),
|
||||
)
|
||||
.into_any_element()
|
||||
})
|
||||
}
|
||||
|
||||
fn render_sign_in_disclaimer(&self, _cx: &mut App) -> AnyElement {
|
||||
let signing_in = matches!(self.sign_in_status, SignInStatus::SigningIn);
|
||||
|
||||
v_flex()
|
||||
.relative()
|
||||
.gap_1()
|
||||
.child(Headline::new("Welcome to Zed AI"))
|
||||
.child(
|
||||
@@ -109,6 +131,7 @@ impl ZedAiOnboarding {
|
||||
}
|
||||
}),
|
||||
)
|
||||
.children(self.render_dismiss_button())
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
@@ -180,27 +203,7 @@ impl ZedAiOnboarding {
|
||||
)
|
||||
.child(PlanDefinitions.free_plan(is_v2)),
|
||||
)
|
||||
.when_some(
|
||||
self.dismiss_onboarding.as_ref(),
|
||||
|this, dismiss_callback| {
|
||||
let callback = dismiss_callback.clone();
|
||||
|
||||
this.child(
|
||||
h_flex().absolute().top_0().right_0().child(
|
||||
IconButton::new("dismiss_onboarding", IconName::Close)
|
||||
.icon_size(IconSize::Small)
|
||||
.tooltip(Tooltip::text("Dismiss"))
|
||||
.on_click(move |_, window, cx| {
|
||||
telemetry::event!(
|
||||
"Banner Dismissed",
|
||||
source = "AI Onboarding",
|
||||
);
|
||||
callback(window, cx)
|
||||
}),
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
.children(self.render_dismiss_button())
|
||||
.child(
|
||||
v_flex()
|
||||
.mt_2()
|
||||
@@ -245,26 +248,7 @@ impl ZedAiOnboarding {
|
||||
.mb_2(),
|
||||
)
|
||||
.child(PlanDefinitions.pro_trial(is_v2, false))
|
||||
.when_some(
|
||||
self.dismiss_onboarding.as_ref(),
|
||||
|this, dismiss_callback| {
|
||||
let callback = dismiss_callback.clone();
|
||||
this.child(
|
||||
h_flex().absolute().top_0().right_0().child(
|
||||
IconButton::new("dismiss_onboarding", IconName::Close)
|
||||
.icon_size(IconSize::Small)
|
||||
.tooltip(Tooltip::text("Dismiss"))
|
||||
.on_click(move |_, window, cx| {
|
||||
telemetry::event!(
|
||||
"Banner Dismissed",
|
||||
source = "AI Onboarding",
|
||||
);
|
||||
callback(window, cx)
|
||||
}),
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
.children(self.render_dismiss_button())
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
@@ -278,26 +262,7 @@ impl ZedAiOnboarding {
|
||||
.mb_2(),
|
||||
)
|
||||
.child(PlanDefinitions.pro_plan(is_v2, false))
|
||||
.when_some(
|
||||
self.dismiss_onboarding.as_ref(),
|
||||
|this, dismiss_callback| {
|
||||
let callback = dismiss_callback.clone();
|
||||
this.child(
|
||||
h_flex().absolute().top_0().right_0().child(
|
||||
IconButton::new("dismiss_onboarding", IconName::Close)
|
||||
.icon_size(IconSize::Small)
|
||||
.tooltip(Tooltip::text("Dismiss"))
|
||||
.on_click(move |_, window, cx| {
|
||||
telemetry::event!(
|
||||
"Banner Dismissed",
|
||||
source = "AI Onboarding",
|
||||
);
|
||||
callback(window, cx)
|
||||
}),
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
.children(self.render_dismiss_button())
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2703,7 +2703,7 @@ async fn test_lsp_pull_diagnostics(
|
||||
let (closure_workspace_diagnostic_received_tx, workspace_diagnostic_received_rx) =
|
||||
smol::channel::bounded::<()>(1);
|
||||
let expected_workspace_diagnostic_token = lsp::ProgressToken::String(format!(
|
||||
"workspace/diagnostic-{}-1",
|
||||
"workspace/diagnostic/{}/1",
|
||||
fake_language_server.server.server_id()
|
||||
));
|
||||
let closure_expected_workspace_diagnostic_token = expected_workspace_diagnostic_token.clone();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use collections::HashMap;
|
||||
use gpui::{Entity, WeakEntity};
|
||||
use gpui::{Corner, Entity, WeakEntity};
|
||||
use project::debugger::session::{ThreadId, ThreadStatus};
|
||||
use ui::{CommonAnimationExt, ContextMenu, DropdownMenu, DropdownStyle, Indicator, prelude::*};
|
||||
use util::{maybe, truncate_and_trailoff};
|
||||
@@ -211,6 +211,7 @@ impl DebugPanel {
|
||||
this
|
||||
}),
|
||||
)
|
||||
.attach(Corner::BottomLeft)
|
||||
.style(DropdownStyle::Ghost)
|
||||
.handle(self.session_picker_menu_handle.clone());
|
||||
|
||||
@@ -322,6 +323,7 @@ impl DebugPanel {
|
||||
this
|
||||
}),
|
||||
)
|
||||
.attach(Corner::BottomLeft)
|
||||
.disabled(session_terminated)
|
||||
.style(DropdownStyle::Ghost)
|
||||
.handle(self.thread_picker_menu_handle.clone()),
|
||||
|
||||
@@ -594,7 +594,11 @@ impl DisplayMap {
|
||||
self.block_map.read(snapshot, edits);
|
||||
}
|
||||
|
||||
pub fn remove_inlays_for_excerpts(&mut self, excerpts_removed: &[ExcerptId]) {
|
||||
pub fn remove_inlays_for_excerpts(
|
||||
&mut self,
|
||||
excerpts_removed: &[ExcerptId],
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let to_remove = self
|
||||
.inlay_map
|
||||
.current_inlays()
|
||||
@@ -606,7 +610,7 @@ impl DisplayMap {
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
self.inlay_map.splice(&to_remove, Vec::new());
|
||||
self.splice_inlays(&to_remove, Vec::new(), cx);
|
||||
}
|
||||
|
||||
fn tab_size(buffer: &Entity<MultiBuffer>, cx: &App) -> NonZeroU32 {
|
||||
|
||||
@@ -3899,6 +3899,9 @@ impl Editor {
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
if selection_ranges.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let ranges = match columnar_state {
|
||||
ColumnarSelectionState::FromMouse { .. } => {
|
||||
@@ -5288,8 +5291,8 @@ impl Editor {
|
||||
{
|
||||
self.splice_inlays(&to_remove, to_insert, cx);
|
||||
}
|
||||
self.display_map.update(cx, |display_map, _| {
|
||||
display_map.remove_inlays_for_excerpts(&excerpts_removed)
|
||||
self.display_map.update(cx, |display_map, cx| {
|
||||
display_map.remove_inlays_for_excerpts(&excerpts_removed, cx)
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -651,7 +651,6 @@ impl EditorElement {
|
||||
fn mouse_left_down(
|
||||
editor: &mut Editor,
|
||||
event: &MouseDownEvent,
|
||||
hovered_hunk: Option<Range<Anchor>>,
|
||||
position_map: &PositionMap,
|
||||
line_numbers: &HashMap<MultiBufferRow, LineNumberLayout>,
|
||||
window: &mut Window,
|
||||
@@ -667,7 +666,20 @@ impl EditorElement {
|
||||
let mut click_count = event.click_count;
|
||||
let mut modifiers = event.modifiers;
|
||||
|
||||
if let Some(hovered_hunk) = hovered_hunk {
|
||||
if let Some(hovered_hunk) =
|
||||
position_map
|
||||
.display_hunks
|
||||
.iter()
|
||||
.find_map(|(hunk, hunk_hitbox)| match hunk {
|
||||
DisplayDiffHunk::Folded { .. } => None,
|
||||
DisplayDiffHunk::Unfolded {
|
||||
multi_buffer_range, ..
|
||||
} => hunk_hitbox
|
||||
.as_ref()
|
||||
.is_some_and(|hitbox| hitbox.is_hovered(window))
|
||||
.then(|| multi_buffer_range.clone()),
|
||||
})
|
||||
{
|
||||
editor.toggle_single_diff_hunk(hovered_hunk, cx);
|
||||
cx.notify();
|
||||
return;
|
||||
@@ -7247,26 +7259,6 @@ impl EditorElement {
|
||||
window.on_mouse_event({
|
||||
let position_map = layout.position_map.clone();
|
||||
let editor = self.editor.clone();
|
||||
let diff_hunk_range =
|
||||
layout
|
||||
.display_hunks
|
||||
.iter()
|
||||
.find_map(|(hunk, hunk_hitbox)| match hunk {
|
||||
DisplayDiffHunk::Folded { .. } => None,
|
||||
DisplayDiffHunk::Unfolded {
|
||||
multi_buffer_range, ..
|
||||
} => {
|
||||
if hunk_hitbox
|
||||
.as_ref()
|
||||
.map(|hitbox| hitbox.is_hovered(window))
|
||||
.unwrap_or(false)
|
||||
{
|
||||
Some(multi_buffer_range.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
});
|
||||
let line_numbers = layout.line_numbers.clone();
|
||||
|
||||
move |event: &MouseDownEvent, phase, window, cx| {
|
||||
@@ -7283,7 +7275,6 @@ impl EditorElement {
|
||||
Self::mouse_left_down(
|
||||
editor,
|
||||
event,
|
||||
diff_hunk_range.clone(),
|
||||
&position_map,
|
||||
line_numbers.as_ref(),
|
||||
window,
|
||||
|
||||
@@ -493,22 +493,15 @@ pub fn show_link_definition(
|
||||
}
|
||||
|
||||
let trigger_anchor = trigger_point.anchor();
|
||||
let Some((buffer, buffer_position)) = editor
|
||||
.buffer
|
||||
.read(cx)
|
||||
.text_anchor_for_position(*trigger_anchor, cx)
|
||||
else {
|
||||
let anchor = snapshot.buffer_snapshot().anchor_before(*trigger_anchor);
|
||||
let Some(buffer) = editor.buffer().read(cx).buffer_for_anchor(anchor, cx) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some((excerpt_id, _, _)) = editor
|
||||
.buffer()
|
||||
.read(cx)
|
||||
.excerpt_containing(*trigger_anchor, cx)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Anchor {
|
||||
excerpt_id,
|
||||
text_anchor,
|
||||
..
|
||||
} = anchor;
|
||||
let same_kind = hovered_link_state.preferred_kind == preferred_kind
|
||||
|| hovered_link_state
|
||||
.links
|
||||
@@ -538,7 +531,7 @@ pub fn show_link_definition(
|
||||
async move {
|
||||
let result = match &trigger_point {
|
||||
TriggerPoint::Text(_) => {
|
||||
if let Some((url_range, url)) = find_url(&buffer, buffer_position, cx.clone()) {
|
||||
if let Some((url_range, url)) = find_url(&buffer, text_anchor, cx.clone()) {
|
||||
this.read_with(cx, |_, _| {
|
||||
let range = maybe!({
|
||||
let start =
|
||||
@@ -550,7 +543,7 @@ pub fn show_link_definition(
|
||||
})
|
||||
.ok()
|
||||
} else if let Some((filename_range, filename)) =
|
||||
find_file(&buffer, project.clone(), buffer_position, cx).await
|
||||
find_file(&buffer, project.clone(), text_anchor, cx).await
|
||||
{
|
||||
let range = maybe!({
|
||||
let start =
|
||||
@@ -562,7 +555,7 @@ pub fn show_link_definition(
|
||||
Some((range, vec![HoverLink::File(filename)]))
|
||||
} else if let Some(provider) = provider {
|
||||
let task = cx.update(|_, cx| {
|
||||
provider.definitions(&buffer, buffer_position, preferred_kind, cx)
|
||||
provider.definitions(&buffer, text_anchor, preferred_kind, cx)
|
||||
})?;
|
||||
if let Some(task) = task {
|
||||
task.await.ok().flatten().map(|definition_result| {
|
||||
|
||||
@@ -610,21 +610,32 @@ impl<'a> MutableSelectionsCollection<'a> {
|
||||
self.select(selections);
|
||||
}
|
||||
|
||||
pub fn select<T>(&mut self, mut selections: Vec<Selection<T>>)
|
||||
pub fn select<T>(&mut self, selections: Vec<Selection<T>>)
|
||||
where
|
||||
T: ToOffset + ToPoint + Ord + std::marker::Copy + std::fmt::Debug,
|
||||
T: ToOffset + std::marker::Copy + std::fmt::Debug,
|
||||
{
|
||||
let buffer = self.buffer.read(self.cx).snapshot(self.cx);
|
||||
let mut selections = selections
|
||||
.into_iter()
|
||||
.map(|selection| selection.map(|it| it.to_offset(&buffer)))
|
||||
.map(|mut selection| {
|
||||
if selection.start > selection.end {
|
||||
mem::swap(&mut selection.start, &mut selection.end);
|
||||
selection.reversed = true
|
||||
}
|
||||
selection
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
selections.sort_unstable_by_key(|s| s.start);
|
||||
// Merge overlapping selections.
|
||||
let mut i = 1;
|
||||
while i < selections.len() {
|
||||
if selections[i - 1].end >= selections[i].start {
|
||||
if selections[i].start <= selections[i - 1].end {
|
||||
let removed = selections.remove(i);
|
||||
if removed.start < selections[i - 1].start {
|
||||
selections[i - 1].start = removed.start;
|
||||
}
|
||||
if removed.end > selections[i - 1].end {
|
||||
if selections[i - 1].end < removed.end {
|
||||
selections[i - 1].end = removed.end;
|
||||
}
|
||||
} else {
|
||||
@@ -968,13 +979,10 @@ impl DerefMut for MutableSelectionsCollection<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
fn selection_to_anchor_selection<T>(
|
||||
selection: Selection<T>,
|
||||
fn selection_to_anchor_selection(
|
||||
selection: Selection<usize>,
|
||||
buffer: &MultiBufferSnapshot,
|
||||
) -> Selection<Anchor>
|
||||
where
|
||||
T: ToOffset + Ord,
|
||||
{
|
||||
) -> Selection<Anchor> {
|
||||
let end_bias = if selection.start == selection.end {
|
||||
Bias::Right
|
||||
} else {
|
||||
@@ -1012,7 +1020,7 @@ fn resolve_selections_point<'a>(
|
||||
})
|
||||
}
|
||||
|
||||
// Panics if passed selections are not in order
|
||||
/// Panics if passed selections are not in order
|
||||
fn resolve_selections_display<'a>(
|
||||
selections: impl 'a + IntoIterator<Item = &'a Selection<Anchor>>,
|
||||
map: &'a DisplaySnapshot,
|
||||
@@ -1044,7 +1052,7 @@ fn resolve_selections_display<'a>(
|
||||
coalesce_selections(selections)
|
||||
}
|
||||
|
||||
// Panics if passed selections are not in order
|
||||
/// Panics if passed selections are not in order
|
||||
pub(crate) fn resolve_selections<'a, D, I>(
|
||||
selections: I,
|
||||
map: &'a DisplaySnapshot,
|
||||
|
||||
@@ -669,7 +669,7 @@ impl PickerDelegate for OpenPathDelegate {
|
||||
) -> Option<Self::ListItem> {
|
||||
let settings = FileFinderSettings::get_global(cx);
|
||||
let candidate = self.get_entry(ix)?;
|
||||
let match_positions = match &self.directory_state {
|
||||
let mut match_positions = match &self.directory_state {
|
||||
DirectoryState::List { .. } => self.string_matches.get(ix)?.positions.clone(),
|
||||
DirectoryState::Create { user_input, .. } => {
|
||||
if let Some(user_input) = user_input {
|
||||
@@ -710,29 +710,38 @@ impl PickerDelegate for OpenPathDelegate {
|
||||
});
|
||||
|
||||
match &self.directory_state {
|
||||
DirectoryState::List { parent_path, .. } => Some(
|
||||
ListItem::new(ix)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.start_slot::<Icon>(file_icon)
|
||||
.inset(true)
|
||||
.toggle_state(selected)
|
||||
.child(HighlightedLabel::new(
|
||||
if parent_path == &self.prompt_root {
|
||||
format!("{}{}", self.prompt_root, candidate.path.string)
|
||||
} else if is_current_dir_candidate {
|
||||
"open this directory".to_string()
|
||||
} else {
|
||||
candidate.path.string
|
||||
},
|
||||
DirectoryState::List { parent_path, .. } => {
|
||||
let (label, indices) = if *parent_path == self.prompt_root {
|
||||
match_positions.iter_mut().for_each(|position| {
|
||||
*position += self.prompt_root.len();
|
||||
});
|
||||
(
|
||||
format!("{}{}", self.prompt_root, candidate.path.string),
|
||||
match_positions,
|
||||
)),
|
||||
),
|
||||
)
|
||||
} else if is_current_dir_candidate {
|
||||
("open this directory".to_string(), vec![])
|
||||
} else {
|
||||
(candidate.path.string, match_positions)
|
||||
};
|
||||
Some(
|
||||
ListItem::new(ix)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.start_slot::<Icon>(file_icon)
|
||||
.inset(true)
|
||||
.toggle_state(selected)
|
||||
.child(HighlightedLabel::new(label, indices)),
|
||||
)
|
||||
}
|
||||
DirectoryState::Create {
|
||||
parent_path,
|
||||
user_input,
|
||||
..
|
||||
} => {
|
||||
let (label, delta) = if parent_path == &self.prompt_root {
|
||||
let (label, delta) = if *parent_path == self.prompt_root {
|
||||
match_positions.iter_mut().for_each(|position| {
|
||||
*position += self.prompt_root.len();
|
||||
});
|
||||
(
|
||||
format!("{}{}", self.prompt_root, candidate.path.string),
|
||||
self.prompt_root.len(),
|
||||
@@ -740,10 +749,10 @@ impl PickerDelegate for OpenPathDelegate {
|
||||
} else {
|
||||
(candidate.path.string.clone(), 0)
|
||||
};
|
||||
let label_len = label.len();
|
||||
|
||||
let label_with_highlights = match user_input {
|
||||
Some(user_input) => {
|
||||
let label_len = label.len();
|
||||
if user_input.file.string == candidate.path.string {
|
||||
if user_input.exists {
|
||||
let label = if user_input.is_dir {
|
||||
@@ -772,20 +781,10 @@ impl PickerDelegate for OpenPathDelegate {
|
||||
.into_any_element()
|
||||
}
|
||||
} else {
|
||||
let mut highlight_positions = match_positions;
|
||||
highlight_positions.iter_mut().for_each(|position| {
|
||||
*position += delta;
|
||||
});
|
||||
HighlightedLabel::new(label, highlight_positions).into_any_element()
|
||||
HighlightedLabel::new(label, match_positions).into_any_element()
|
||||
}
|
||||
}
|
||||
None => {
|
||||
let mut highlight_positions = match_positions;
|
||||
highlight_positions.iter_mut().for_each(|position| {
|
||||
*position += delta;
|
||||
});
|
||||
HighlightedLabel::new(label, highlight_positions).into_any_element()
|
||||
}
|
||||
None => HighlightedLabel::new(label, match_positions).into_any_element(),
|
||||
};
|
||||
|
||||
Some(
|
||||
|
||||
@@ -228,7 +228,7 @@ impl PickerDelegate for PickerPromptDelegate {
|
||||
let highlights: Vec<_> = hit
|
||||
.positions
|
||||
.iter()
|
||||
.filter(|index| index < &&self.max_match_length)
|
||||
.filter(|&&index| index < self.max_match_length)
|
||||
.copied()
|
||||
.collect();
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use gpui::{
|
||||
App, Application, Bounds, Context, Window, WindowBounds, WindowOptions, div, prelude::*, px,
|
||||
size,
|
||||
App, Application, Bounds, Context, FontStyle, FontWeight, StyledText, Window, WindowBounds,
|
||||
WindowOptions, div, prelude::*, px, size,
|
||||
};
|
||||
|
||||
struct HelloWorld {}
|
||||
@@ -71,6 +71,12 @@ impl Render for HelloWorld {
|
||||
.child("100%"),
|
||||
),
|
||||
)
|
||||
.child(div().flex().gap_2().justify_between().child(
|
||||
StyledText::new("ABCD").with_highlights([
|
||||
(0..1, FontWeight::EXTRA_BOLD.into()),
|
||||
(2..3, FontStyle::Italic.into()),
|
||||
]),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -449,11 +449,12 @@ impl MacTextSystemState {
|
||||
// to prevent core text from forming ligatures between them
|
||||
let needs_zwnj = last_font_run.replace(run.font_id) == Some(run.font_id);
|
||||
|
||||
let n_zwnjs = self.zwnjs_scratch_space.len();
|
||||
let utf16_start = ix_converter.utf16_ix + n_zwnjs * ZWNJ_SIZE_16;
|
||||
let n_zwnjs = self.zwnjs_scratch_space.len(); // from previous loop
|
||||
let utf16_start = string.char_len(); // insert at end of string
|
||||
ix_converter.advance_to_utf8_ix(ix_converter.utf8_ix + run.len);
|
||||
|
||||
string.replace_str(&CFString::new(text), CFRange::init(utf16_start as isize, 0));
|
||||
// note: replace_str may silently ignore codepoints it dislikes (e.g., BOM at start of string)
|
||||
string.replace_str(&CFString::new(text), CFRange::init(utf16_start, 0));
|
||||
if needs_zwnj {
|
||||
let zwnjs_pos = string.char_len();
|
||||
self.zwnjs_scratch_space.push((n_zwnjs, zwnjs_pos as usize));
|
||||
@@ -462,10 +463,9 @@ impl MacTextSystemState {
|
||||
CFRange::init(zwnjs_pos, 0),
|
||||
);
|
||||
}
|
||||
let utf16_end = string.char_len() as usize;
|
||||
let utf16_end = string.char_len();
|
||||
|
||||
let cf_range =
|
||||
CFRange::init(utf16_start as isize, (utf16_end - utf16_start) as isize);
|
||||
let cf_range = CFRange::init(utf16_start, utf16_end - utf16_start);
|
||||
let font = &self.fonts[run.font_id.0];
|
||||
|
||||
let font_metrics = font.metrics();
|
||||
@@ -548,10 +548,12 @@ impl MacTextSystemState {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Debug, Clone)]
|
||||
struct StringIndexConverter<'a> {
|
||||
text: &'a str,
|
||||
/// Index in UTF-8 bytes
|
||||
utf8_ix: usize,
|
||||
/// Index in UTF-16 code units
|
||||
utf16_ix: usize,
|
||||
}
|
||||
|
||||
@@ -732,6 +734,25 @@ mod tests {
|
||||
assert_eq!(layout.runs[0].glyphs[0].id, GlyphId(68u32)); // a
|
||||
// There's no glyph for \u{feff}
|
||||
assert_eq!(layout.runs[0].glyphs[1].id, GlyphId(69u32)); // b
|
||||
|
||||
let line = "\u{feff}ab";
|
||||
let font_runs = &[
|
||||
FontRun {
|
||||
len: "\u{feff}".len(),
|
||||
font_id,
|
||||
},
|
||||
FontRun {
|
||||
len: "ab".len(),
|
||||
font_id,
|
||||
},
|
||||
];
|
||||
let layout = fonts.layout_line(line, px(16.), font_runs);
|
||||
assert_eq!(layout.len, line.len());
|
||||
assert_eq!(layout.runs.len(), 1);
|
||||
assert_eq!(layout.runs[0].glyphs.len(), 2);
|
||||
// There's no glyph for \u{feff}
|
||||
assert_eq!(layout.runs[0].glyphs[0].id, GlyphId(68u32)); // a
|
||||
assert_eq!(layout.runs[0].glyphs[1].id, GlyphId(69u32)); // b
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -530,8 +530,18 @@ impl WindowsWindowInner {
|
||||
};
|
||||
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,
|
||||
true => {
|
||||
self.system_settings
|
||||
.borrow()
|
||||
.mouse_wheel_settings
|
||||
.wheel_scroll_chars
|
||||
}
|
||||
false => {
|
||||
self.system_settings
|
||||
.borrow()
|
||||
.mouse_wheel_settings
|
||||
.wheel_scroll_lines
|
||||
}
|
||||
};
|
||||
drop(lock);
|
||||
|
||||
@@ -574,7 +584,11 @@ impl WindowsWindowInner {
|
||||
return Some(1);
|
||||
};
|
||||
let scale_factor = lock.scale_factor;
|
||||
let wheel_scroll_chars = lock.system_settings.mouse_wheel_settings.wheel_scroll_chars;
|
||||
let wheel_scroll_chars = self
|
||||
.system_settings
|
||||
.borrow()
|
||||
.mouse_wheel_settings
|
||||
.wheel_scroll_chars;
|
||||
drop(lock);
|
||||
|
||||
let wheel_distance =
|
||||
@@ -707,11 +721,8 @@ impl WindowsWindowInner {
|
||||
// used by Chrome. However, it may result in one row of pixels being obscured
|
||||
// in our client area. But as Chrome says, "there seems to be no better solution."
|
||||
if is_maximized
|
||||
&& let Some(ref taskbar_position) = self
|
||||
.state
|
||||
.borrow()
|
||||
.system_settings
|
||||
.auto_hide_taskbar_position
|
||||
&& let Some(ref taskbar_position) =
|
||||
self.system_settings.borrow().auto_hide_taskbar_position
|
||||
{
|
||||
// For the auto-hide taskbar, adjust in by 1 pixel on taskbar edge,
|
||||
// so the window isn't treated as a "fullscreen app", which would cause
|
||||
@@ -1101,9 +1112,11 @@ impl WindowsWindowInner {
|
||||
if wparam.0 != 0 {
|
||||
let mut lock = self.state.borrow_mut();
|
||||
let display = lock.display;
|
||||
lock.system_settings.update(display, wparam.0);
|
||||
lock.click_state.system_update(wparam.0);
|
||||
lock.border_offset.update(handle).log_err();
|
||||
// system settings may emit a window message which wants to take the refcell lock, so drop it
|
||||
drop(lock);
|
||||
self.system_settings.borrow_mut().update(display, wparam.0);
|
||||
} else {
|
||||
self.handle_system_theme_changed(handle, lparam)?;
|
||||
};
|
||||
|
||||
@@ -51,7 +51,6 @@ pub struct WindowsWindowState {
|
||||
pub renderer: DirectXRenderer,
|
||||
|
||||
pub click_state: ClickState,
|
||||
pub system_settings: WindowsSystemSettings,
|
||||
pub current_cursor: Option<HCURSOR>,
|
||||
pub nc_button_pressed: Option<u32>,
|
||||
|
||||
@@ -66,6 +65,7 @@ pub(crate) struct WindowsWindowInner {
|
||||
pub(super) this: Weak<Self>,
|
||||
drop_target_helper: IDropTargetHelper,
|
||||
pub(crate) state: RefCell<WindowsWindowState>,
|
||||
pub(crate) system_settings: RefCell<WindowsSystemSettings>,
|
||||
pub(crate) handle: AnyWindowHandle,
|
||||
pub(crate) hide_title_bar: bool,
|
||||
pub(crate) is_movable: bool,
|
||||
@@ -115,7 +115,6 @@ impl WindowsWindowState {
|
||||
let system_key_handled = false;
|
||||
let hovered = false;
|
||||
let click_state = ClickState::new();
|
||||
let system_settings = WindowsSystemSettings::new(display);
|
||||
let nc_button_pressed = None;
|
||||
let fullscreen = None;
|
||||
let initial_placement = None;
|
||||
@@ -138,7 +137,6 @@ impl WindowsWindowState {
|
||||
hovered,
|
||||
renderer,
|
||||
click_state,
|
||||
system_settings,
|
||||
current_cursor,
|
||||
nc_button_pressed,
|
||||
display,
|
||||
@@ -231,6 +229,7 @@ impl WindowsWindowInner {
|
||||
validation_number: context.validation_number,
|
||||
main_receiver: context.main_receiver.clone(),
|
||||
platform_window_handle: context.platform_window_handle,
|
||||
system_settings: RefCell::new(WindowsSystemSettings::new(context.display)),
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -644,10 +643,12 @@ impl PlatformWindow for WindowsWindow {
|
||||
let mut btn_encoded = Vec::new();
|
||||
for (index, btn) in answers.iter().enumerate() {
|
||||
let encoded = HSTRING::from(btn.label().as_ref());
|
||||
let button_id = if btn.is_cancel() {
|
||||
IDCANCEL.0
|
||||
} else {
|
||||
index as i32 - 100
|
||||
let button_id = match btn {
|
||||
PromptButton::Ok(_) => IDOK.0,
|
||||
PromptButton::Cancel(_) => IDCANCEL.0,
|
||||
// the first few low integer values are reserved for known buttons
|
||||
// so for simplicity we just go backwards from -1
|
||||
PromptButton::Other(_) => -(index as i32) - 1,
|
||||
};
|
||||
button_id_map.push(button_id);
|
||||
buttons.push(TASKDIALOG_BUTTON {
|
||||
@@ -665,11 +666,11 @@ impl PlatformWindow for WindowsWindow {
|
||||
.context("unable to create task dialog")
|
||||
.log_err();
|
||||
|
||||
let clicked = button_id_map
|
||||
.iter()
|
||||
.position(|&button_id| button_id == res)
|
||||
.unwrap();
|
||||
let _ = done_tx.send(clicked);
|
||||
if let Some(clicked) =
|
||||
button_id_map.iter().position(|&button_id| button_id == res)
|
||||
{
|
||||
let _ = done_tx.send(clicked);
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
@@ -424,7 +424,6 @@ impl WindowTextSystem {
|
||||
font_runs.clear();
|
||||
let line_end = line_start + line_text.len();
|
||||
|
||||
let mut last_font: Option<FontId> = None;
|
||||
let mut decoration_runs = SmallVec::<[DecorationRun; 32]>::new();
|
||||
let mut run_start = line_start;
|
||||
while run_start < line_end {
|
||||
@@ -453,14 +452,13 @@ impl WindowTextSystem {
|
||||
true
|
||||
};
|
||||
|
||||
let font_id = self.resolve_font(&run.font);
|
||||
if let Some(font_run) = font_runs.last_mut()
|
||||
&& Some(font_run.font_id) == last_font
|
||||
&& font_id == font_run.font_id
|
||||
&& !decoration_changed
|
||||
{
|
||||
font_run.len += run_len_within_line;
|
||||
} else {
|
||||
let font_id = self.resolve_font(&run.font);
|
||||
last_font = Some(font_id);
|
||||
font_runs.push(FontRun {
|
||||
len: run_len_within_line,
|
||||
font_id,
|
||||
|
||||
@@ -142,6 +142,8 @@ pub struct LanguageSettings {
|
||||
pub auto_indent_on_paste: bool,
|
||||
/// Controls how the editor handles the autoclosed characters.
|
||||
pub always_treat_brackets_as_autoclosed: bool,
|
||||
/// Which code actions to run on save
|
||||
pub code_actions_on_format: HashMap<String, bool>,
|
||||
/// Whether to perform linked edits
|
||||
pub linked_edits: bool,
|
||||
/// Task configuration for this language.
|
||||
@@ -576,6 +578,7 @@ impl settings::Settings for AllLanguageSettings {
|
||||
always_treat_brackets_as_autoclosed: settings
|
||||
.always_treat_brackets_as_autoclosed
|
||||
.unwrap(),
|
||||
code_actions_on_format: settings.code_actions_on_format.unwrap(),
|
||||
linked_edits: settings.linked_edits.unwrap(),
|
||||
tasks: LanguageTaskSettings {
|
||||
variables: tasks.variables.unwrap_or_default(),
|
||||
|
||||
@@ -222,7 +222,7 @@ impl LspAdapter for GoLspAdapter {
|
||||
Some((lsp::CompletionItemKind::MODULE, detail)) => {
|
||||
let text = format!("{label} {detail}");
|
||||
let source = Rope::from(format!("import {text}").as_str());
|
||||
let runs = language.highlight_text(&source, 7..7 + text.len());
|
||||
let runs = language.highlight_text(&source, 7..7 + text[name_offset..].len());
|
||||
let filter_range = completion
|
||||
.filter_text
|
||||
.as_deref()
|
||||
@@ -246,7 +246,7 @@ impl LspAdapter for GoLspAdapter {
|
||||
Rope::from(format!("var {} {}", &text[name_offset..], detail).as_str());
|
||||
let runs = adjust_runs(
|
||||
name_offset,
|
||||
language.highlight_text(&source, 4..4 + text.len()),
|
||||
language.highlight_text(&source, 4..4 + text[name_offset..].len()),
|
||||
);
|
||||
let filter_range = completion
|
||||
.filter_text
|
||||
@@ -267,7 +267,7 @@ impl LspAdapter for GoLspAdapter {
|
||||
let source = Rope::from(format!("type {}", &text[name_offset..]).as_str());
|
||||
let runs = adjust_runs(
|
||||
name_offset,
|
||||
language.highlight_text(&source, 5..5 + text.len()),
|
||||
language.highlight_text(&source, 5..5 + text[name_offset..].len()),
|
||||
);
|
||||
let filter_range = completion
|
||||
.filter_text
|
||||
@@ -288,7 +288,7 @@ impl LspAdapter for GoLspAdapter {
|
||||
let source = Rope::from(format!("type {}", &text[name_offset..]).as_str());
|
||||
let runs = adjust_runs(
|
||||
name_offset,
|
||||
language.highlight_text(&source, 5..5 + text.len()),
|
||||
language.highlight_text(&source, 5..5 + text[name_offset..].len()),
|
||||
);
|
||||
let filter_range = completion
|
||||
.filter_text
|
||||
@@ -310,7 +310,7 @@ impl LspAdapter for GoLspAdapter {
|
||||
Rope::from(format!("type T struct {{ {} }}", &text[name_offset..]).as_str());
|
||||
let runs = adjust_runs(
|
||||
name_offset,
|
||||
language.highlight_text(&source, 16..16 + text.len()),
|
||||
language.highlight_text(&source, 16..16 + text[name_offset..].len()),
|
||||
);
|
||||
let filter_range = completion
|
||||
.filter_text
|
||||
@@ -332,7 +332,7 @@ impl LspAdapter for GoLspAdapter {
|
||||
let source = Rope::from(format!("func {} {{}}", &text[name_offset..]).as_str());
|
||||
let runs = adjust_runs(
|
||||
name_offset,
|
||||
language.highlight_text(&source, 5..5 + text.len()),
|
||||
language.highlight_text(&source, 5..5 + text[name_offset..].len()),
|
||||
);
|
||||
let filter_range = completion
|
||||
.filter_text
|
||||
|
||||
@@ -57,29 +57,65 @@ impl RustLspAdapter {
|
||||
|
||||
const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("rust-analyzer");
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
enum LibcType {
|
||||
Gnu,
|
||||
Musl,
|
||||
}
|
||||
|
||||
impl RustLspAdapter {
|
||||
#[cfg(target_os = "linux")]
|
||||
fn build_arch_server_name_linux() -> String {
|
||||
enum LibcType {
|
||||
Gnu,
|
||||
Musl,
|
||||
async fn determine_libc_type() -> LibcType {
|
||||
use futures::pin_mut;
|
||||
use smol::process::Command;
|
||||
|
||||
async fn from_ldd_version() -> Option<LibcType> {
|
||||
let ldd_output = Command::new("ldd").arg("--version").output().await.ok()?;
|
||||
let ldd_version = String::from_utf8_lossy(&ldd_output.stdout);
|
||||
|
||||
if ldd_version.contains("GNU libc") || ldd_version.contains("GLIBC") {
|
||||
Some(LibcType::Gnu)
|
||||
} else if ldd_version.contains("musl") {
|
||||
Some(LibcType::Musl)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
let has_musl = std::fs::exists(&format!("/lib/ld-musl-{}.so.1", std::env::consts::ARCH))
|
||||
.unwrap_or(false);
|
||||
let has_gnu = std::fs::exists(&format!("/lib/ld-linux-{}.so.1", std::env::consts::ARCH))
|
||||
.unwrap_or(false);
|
||||
if let Some(libc_type) = from_ldd_version().await {
|
||||
return libc_type;
|
||||
}
|
||||
|
||||
let libc_type = match (has_musl, has_gnu) {
|
||||
let Ok(dir_entries) = smol::fs::read_dir("/lib").await else {
|
||||
// defaulting to gnu because nix doesn't have /lib files due to not following FHS
|
||||
return LibcType::Gnu;
|
||||
};
|
||||
let dir_entries = dir_entries.filter_map(async move |e| e.ok());
|
||||
pin_mut!(dir_entries);
|
||||
|
||||
let mut has_musl = false;
|
||||
let mut has_gnu = false;
|
||||
|
||||
while let Some(entry) = dir_entries.next().await {
|
||||
let file_name = entry.file_name();
|
||||
let file_name = file_name.to_string_lossy();
|
||||
if file_name.starts_with("ld-musl-") {
|
||||
has_musl = true;
|
||||
} else if file_name.starts_with("ld-linux-") {
|
||||
has_gnu = true;
|
||||
}
|
||||
}
|
||||
|
||||
match (has_musl, has_gnu) {
|
||||
(true, _) => LibcType::Musl,
|
||||
(_, true) => LibcType::Gnu,
|
||||
_ => {
|
||||
// defaulting to gnu because nix doesn't have either of those files due to not following FHS
|
||||
LibcType::Gnu
|
||||
}
|
||||
};
|
||||
_ => LibcType::Gnu,
|
||||
}
|
||||
}
|
||||
|
||||
let libc = match libc_type {
|
||||
#[cfg(target_os = "linux")]
|
||||
async fn build_arch_server_name_linux() -> String {
|
||||
let libc = match Self::determine_libc_type().await {
|
||||
LibcType::Musl => "musl",
|
||||
LibcType::Gnu => "gnu",
|
||||
};
|
||||
@@ -87,7 +123,7 @@ impl RustLspAdapter {
|
||||
format!("{}-{}", Self::ARCH_SERVER_NAME, libc)
|
||||
}
|
||||
|
||||
fn build_asset_name() -> String {
|
||||
async fn build_asset_name() -> String {
|
||||
let extension = match Self::GITHUB_ASSET_KIND {
|
||||
AssetKind::TarGz => "tar.gz",
|
||||
AssetKind::Gz => "gz",
|
||||
@@ -95,7 +131,7 @@ impl RustLspAdapter {
|
||||
};
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
let arch_server_name = Self::build_arch_server_name_linux();
|
||||
let arch_server_name = Self::build_arch_server_name_linux().await;
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
let arch_server_name = Self::ARCH_SERVER_NAME.to_string();
|
||||
|
||||
@@ -447,7 +483,7 @@ impl LspInstaller for RustLspAdapter {
|
||||
delegate.http_client(),
|
||||
)
|
||||
.await?;
|
||||
let asset_name = Self::build_asset_name();
|
||||
let asset_name = Self::build_asset_name().await;
|
||||
let asset = release
|
||||
.assets
|
||||
.into_iter()
|
||||
|
||||
@@ -9,7 +9,9 @@ use html5ever::{ParseOpts, local_name, parse_document, tendril::TendrilSink};
|
||||
use language::LanguageRegistry;
|
||||
use markup5ever_rcdom::RcDom;
|
||||
use pulldown_cmark::{Alignment, Event, Options, Parser, Tag, TagEnd};
|
||||
use std::{cell::RefCell, collections::HashMap, ops::Range, path::PathBuf, rc::Rc, sync::Arc, vec};
|
||||
use std::{
|
||||
cell::RefCell, collections::HashMap, mem, ops::Range, path::PathBuf, rc::Rc, sync::Arc, vec,
|
||||
};
|
||||
|
||||
pub async fn parse_markdown(
|
||||
markdown_input: &str,
|
||||
@@ -407,6 +409,9 @@ impl<'a> MarkdownParser<'a> {
|
||||
if let Some(mut image) = image.take() {
|
||||
if !text.is_empty() {
|
||||
image.set_alt_text(std::mem::take(&mut text).into());
|
||||
mem::take(&mut highlights);
|
||||
mem::take(&mut region_ranges);
|
||||
mem::take(&mut regions);
|
||||
}
|
||||
markdown_text_like.push(MarkdownParagraphChunk::Image(image));
|
||||
}
|
||||
@@ -1275,17 +1280,40 @@ mod tests {
|
||||
panic!("Expected a paragraph");
|
||||
};
|
||||
assert_eq!(
|
||||
paragraph[0],
|
||||
MarkdownParagraphChunk::Image(Image {
|
||||
source_range: 0..111,
|
||||
link: Link::Web {
|
||||
url: "https://blog.logrocket.com/wp-content/uploads/2024/04/exploring-zed-open-source-code-editor-rust-2.png".to_string(),
|
||||
},
|
||||
alt_text: Some("test".into()),
|
||||
height: None,
|
||||
width: None,
|
||||
},)
|
||||
);
|
||||
paragraph[0],
|
||||
MarkdownParagraphChunk::Image(Image {
|
||||
source_range: 0..111,
|
||||
link: Link::Web {
|
||||
url: "https://blog.logrocket.com/wp-content/uploads/2024/04/exploring-zed-open-source-code-editor-rust-2.png".to_string(),
|
||||
},
|
||||
alt_text: Some("test".into()),
|
||||
height: None,
|
||||
width: None,
|
||||
},)
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_image_alt_text() {
|
||||
let parsed = parse("[](https://zed.dev)\n ").await;
|
||||
|
||||
let paragraph = if let ParsedMarkdownElement::Paragraph(text) = &parsed.children[0] {
|
||||
text
|
||||
} else {
|
||||
panic!("Expected a paragraph");
|
||||
};
|
||||
assert_eq!(
|
||||
paragraph[0],
|
||||
MarkdownParagraphChunk::Image(Image {
|
||||
source_range: 0..142,
|
||||
link: Link::Web {
|
||||
url: "https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/zed-industries/zed/main/assets/badge/v0.json".to_string(),
|
||||
},
|
||||
alt_text: Some("Zed".into()),
|
||||
height: None,
|
||||
width: None,
|
||||
},)
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
|
||||
@@ -118,8 +118,8 @@ pub(crate) mod m_2025_10_03 {
|
||||
pub(crate) use settings::SETTINGS_PATTERNS;
|
||||
}
|
||||
|
||||
pub(crate) mod m_2025_10_10 {
|
||||
pub(crate) mod m_2025_10_16 {
|
||||
mod settings;
|
||||
|
||||
pub(crate) use settings::remove_code_actions_on_format;
|
||||
pub(crate) use settings::restore_code_actions_on_format;
|
||||
}
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use serde_json::Value;
|
||||
|
||||
pub fn remove_code_actions_on_format(value: &mut Value) -> Result<()> {
|
||||
remove_code_actions_on_format_inner(value, &[])?;
|
||||
let languages = value
|
||||
.as_object_mut()
|
||||
.and_then(|obj| obj.get_mut("languages"))
|
||||
.and_then(|languages| languages.as_object_mut());
|
||||
if let Some(languages) = languages {
|
||||
for (language_name, language) in languages.iter_mut() {
|
||||
let path = vec!["languages", language_name];
|
||||
remove_code_actions_on_format_inner(language, &path)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn remove_code_actions_on_format_inner(value: &mut Value, path: &[&str]) -> Result<()> {
|
||||
let Some(obj) = value.as_object_mut() else {
|
||||
return Ok(());
|
||||
};
|
||||
let Some(code_actions_on_format) = obj.get("code_actions_on_format").cloned() else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
fn fmt_path(path: &[&str], key: &str) -> String {
|
||||
let mut path = path.to_vec();
|
||||
path.push(key);
|
||||
path.join(".")
|
||||
}
|
||||
|
||||
anyhow::ensure!(
|
||||
code_actions_on_format.is_object(),
|
||||
r#"The `code_actions_on_format` setting is deprecated, but it is in an invalid state and cannot be migrated at {}. Please ensure the code_actions_on_format setting is a Map<String, bool>"#,
|
||||
fmt_path(path, "code_actions_on_format"),
|
||||
);
|
||||
|
||||
let code_actions_map = code_actions_on_format.as_object().unwrap();
|
||||
let mut code_actions = vec![];
|
||||
for (code_action, code_action_enabled) in code_actions_map {
|
||||
if code_action_enabled.as_bool().map_or(false, |b| !b) {
|
||||
continue;
|
||||
}
|
||||
code_actions.push(code_action.clone());
|
||||
}
|
||||
|
||||
let mut formatter_array = vec![];
|
||||
if let Some(formatter) = obj.get("formatter") {
|
||||
if let Some(array) = formatter.as_array() {
|
||||
formatter_array = array.clone();
|
||||
} else {
|
||||
formatter_array.insert(0, formatter.clone());
|
||||
}
|
||||
};
|
||||
let found_code_actions = !code_actions.is_empty();
|
||||
formatter_array.splice(
|
||||
0..0,
|
||||
code_actions
|
||||
.into_iter()
|
||||
.map(|code_action| serde_json::json!({"code_action": code_action})),
|
||||
);
|
||||
|
||||
obj.remove("code_actions_on_format");
|
||||
if found_code_actions {
|
||||
obj.insert("formatter".to_string(), Value::Array(formatter_array));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
71
crates/migrator/src/migrations/m_2025_10_16/settings.rs
Normal file
71
crates/migrator/src/migrations/m_2025_10_16/settings.rs
Normal file
@@ -0,0 +1,71 @@
|
||||
use anyhow::Result;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::patterns::migrate_language_setting;
|
||||
|
||||
pub fn restore_code_actions_on_format(value: &mut Value) -> Result<()> {
|
||||
migrate_language_setting(value, restore_code_actions_on_format_inner)
|
||||
}
|
||||
|
||||
fn restore_code_actions_on_format_inner(value: &mut Value, path: &[&str]) -> Result<()> {
|
||||
let Some(obj) = value.as_object_mut() else {
|
||||
return Ok(());
|
||||
};
|
||||
let code_actions_on_format = obj
|
||||
.get("code_actions_on_format")
|
||||
.cloned()
|
||||
.unwrap_or_else(|| Value::Object(Default::default()));
|
||||
|
||||
fn fmt_path(path: &[&str], key: &str) -> String {
|
||||
let mut path = path.to_vec();
|
||||
path.push(key);
|
||||
path.join(".")
|
||||
}
|
||||
|
||||
let Some(mut code_actions_map) = code_actions_on_format.as_object().cloned() else {
|
||||
anyhow::bail!(
|
||||
r#"The `code_actions_on_format` is in an invalid state and cannot be migrated at {}. Please ensure the code_actions_on_format setting is a Map<String, bool>"#,
|
||||
fmt_path(path, "code_actions_on_format"),
|
||||
);
|
||||
};
|
||||
|
||||
let Some(formatter) = obj.get("formatter") else {
|
||||
return Ok(());
|
||||
};
|
||||
let formatter_array = if let Some(array) = formatter.as_array() {
|
||||
array.clone()
|
||||
} else {
|
||||
vec![formatter.clone()]
|
||||
};
|
||||
if formatter_array.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
let mut code_action_formatters = Vec::new();
|
||||
for formatter in formatter_array {
|
||||
let Some(code_action) = formatter.get("code_action") else {
|
||||
return Ok(());
|
||||
};
|
||||
let Some(code_action_name) = code_action.as_str() else {
|
||||
anyhow::bail!(
|
||||
r#"The `code_action` is in an invalid state and cannot be migrated at {}. Please ensure the code_action setting is a String"#,
|
||||
fmt_path(path, "formatter"),
|
||||
);
|
||||
};
|
||||
code_action_formatters.push(code_action_name.to_string());
|
||||
}
|
||||
|
||||
code_actions_map.extend(
|
||||
code_action_formatters
|
||||
.into_iter()
|
||||
.rev()
|
||||
.map(|code_action| (code_action, Value::Bool(true))),
|
||||
);
|
||||
|
||||
obj.remove("formatter");
|
||||
obj.insert(
|
||||
"code_actions_on_format".into(),
|
||||
Value::Object(code_actions_map),
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -213,7 +213,7 @@ pub fn migrate_settings(text: &str) -> Result<Option<String>> {
|
||||
migrations::m_2025_10_03::SETTINGS_PATTERNS,
|
||||
&SETTINGS_QUERY_2025_10_03,
|
||||
),
|
||||
MigrationType::Json(migrations::m_2025_10_10::remove_code_actions_on_format),
|
||||
MigrationType::Json(migrations::m_2025_10_16::restore_code_actions_on_format),
|
||||
];
|
||||
run_migrations(text, migrations)
|
||||
}
|
||||
@@ -367,6 +367,7 @@ mod tests {
|
||||
pretty_assertions::assert_eq!(migrated.as_deref(), output);
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn assert_migrate_settings(input: &str, output: Option<&str>) {
|
||||
let migrated = migrate_settings(input).unwrap();
|
||||
assert_migrated_correctly(migrated, output);
|
||||
@@ -1341,7 +1342,11 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_flatten_code_action_formatters_basic_array() {
|
||||
assert_migrate_settings(
|
||||
assert_migrate_settings_with_migrations(
|
||||
&[MigrationType::TreeSitter(
|
||||
migrations::m_2025_10_01::SETTINGS_PATTERNS,
|
||||
&SETTINGS_QUERY_2025_10_01,
|
||||
)],
|
||||
&r#"{
|
||||
"formatter": [
|
||||
{
|
||||
@@ -1368,7 +1373,11 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_flatten_code_action_formatters_basic_object() {
|
||||
assert_migrate_settings(
|
||||
assert_migrate_settings_with_migrations(
|
||||
&[MigrationType::TreeSitter(
|
||||
migrations::m_2025_10_01::SETTINGS_PATTERNS,
|
||||
&SETTINGS_QUERY_2025_10_01,
|
||||
)],
|
||||
&r#"{
|
||||
"formatter": {
|
||||
"code_actions": {
|
||||
@@ -1500,7 +1509,11 @@ mod tests {
|
||||
#[test]
|
||||
fn test_flatten_code_action_formatters_array_with_multiple_action_blocks_in_defaults_and_multiple_languages()
|
||||
{
|
||||
assert_migrate_settings(
|
||||
assert_migrate_settings_with_migrations(
|
||||
&[MigrationType::TreeSitter(
|
||||
migrations::m_2025_10_01::SETTINGS_PATTERNS,
|
||||
&SETTINGS_QUERY_2025_10_01,
|
||||
)],
|
||||
&r#"{
|
||||
"formatter": {
|
||||
"code_actions": {
|
||||
@@ -1916,300 +1929,109 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_code_actions_on_format_migration_basic() {
|
||||
fn test_restore_code_actions_on_format() {
|
||||
assert_migrate_settings_with_migrations(
|
||||
&[MigrationType::Json(
|
||||
migrations::m_2025_10_10::remove_code_actions_on_format,
|
||||
migrations::m_2025_10_16::restore_code_actions_on_format,
|
||||
)],
|
||||
&r#"{
|
||||
"code_actions_on_format": {
|
||||
"source.organizeImports": true,
|
||||
"source.fixAll": true
|
||||
"formatter": {
|
||||
"code_action": "foo"
|
||||
}
|
||||
}"#
|
||||
.unindent(),
|
||||
Some(
|
||||
&r#"{
|
||||
"formatter": [
|
||||
{
|
||||
"code_action": "source.organizeImports"
|
||||
},
|
||||
{
|
||||
"code_action": "source.fixAll"
|
||||
}
|
||||
]
|
||||
"code_actions_on_format": {
|
||||
"foo": true
|
||||
}
|
||||
}
|
||||
"#
|
||||
.unindent(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_code_actions_on_format_migration_filters_false_values() {
|
||||
assert_migrate_settings_with_migrations(
|
||||
&[MigrationType::Json(
|
||||
migrations::m_2025_10_10::remove_code_actions_on_format,
|
||||
migrations::m_2025_10_16::restore_code_actions_on_format,
|
||||
)],
|
||||
&r#"{
|
||||
"code_actions_on_format": {
|
||||
"a": true,
|
||||
"b": false,
|
||||
"c": true
|
||||
}
|
||||
"formatter": [
|
||||
{ "code_action": "foo" },
|
||||
"auto"
|
||||
]
|
||||
}"#
|
||||
.unindent(),
|
||||
Some(
|
||||
&r#"{
|
||||
"formatter": [
|
||||
{
|
||||
"code_action": "a"
|
||||
},
|
||||
{
|
||||
"code_action": "c"
|
||||
}
|
||||
]
|
||||
}
|
||||
"#
|
||||
.unindent(),
|
||||
),
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_code_actions_on_format_migration_with_existing_formatter_object() {
|
||||
assert_migrate_settings_with_migrations(
|
||||
&[MigrationType::Json(
|
||||
migrations::m_2025_10_10::remove_code_actions_on_format,
|
||||
migrations::m_2025_10_16::restore_code_actions_on_format,
|
||||
)],
|
||||
&r#"{
|
||||
"formatter": "prettier",
|
||||
"code_actions_on_format": {
|
||||
"source.organizeImports": true
|
||||
}
|
||||
}"#
|
||||
.unindent(),
|
||||
Some(
|
||||
&r#"{
|
||||
"formatter": [
|
||||
{
|
||||
"code_action": "source.organizeImports"
|
||||
},
|
||||
"prettier"
|
||||
]
|
||||
}"#
|
||||
.unindent(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_code_actions_on_format_migration_with_existing_formatter_array() {
|
||||
assert_migrate_settings_with_migrations(
|
||||
&[MigrationType::Json(
|
||||
migrations::m_2025_10_10::remove_code_actions_on_format,
|
||||
)],
|
||||
&r#"{
|
||||
"formatter": ["prettier", {"language_server": "eslint"}],
|
||||
"code_actions_on_format": {
|
||||
"source.organizeImports": true,
|
||||
"source.fixAll": true
|
||||
}
|
||||
}"#
|
||||
.unindent(),
|
||||
Some(
|
||||
&r#"{
|
||||
"formatter": [
|
||||
{
|
||||
"code_action": "source.organizeImports"
|
||||
},
|
||||
{
|
||||
"code_action": "source.fixAll"
|
||||
},
|
||||
"prettier",
|
||||
{
|
||||
"language_server": "eslint"
|
||||
}
|
||||
]
|
||||
}"#
|
||||
.unindent(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_code_actions_on_format_migration_in_languages() {
|
||||
assert_migrate_settings_with_migrations(
|
||||
&[MigrationType::Json(
|
||||
migrations::m_2025_10_10::remove_code_actions_on_format,
|
||||
)],
|
||||
&r#"{
|
||||
"languages": {
|
||||
"JavaScript": {
|
||||
"code_actions_on_format": {
|
||||
"source.fixAll.eslint": true
|
||||
}
|
||||
},
|
||||
"Go": {
|
||||
"code_actions_on_format": {
|
||||
"source.organizeImports": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}"#
|
||||
.unindent(),
|
||||
Some(
|
||||
&r#"{
|
||||
"languages": {
|
||||
"JavaScript": {
|
||||
"formatter": [
|
||||
{
|
||||
"code_action": "source.fixAll.eslint"
|
||||
}
|
||||
]
|
||||
},
|
||||
"Go": {
|
||||
"formatter": [
|
||||
{
|
||||
"code_action": "source.organizeImports"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}"#
|
||||
.unindent(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_code_actions_on_format_migration_in_languages_with_existing_formatter() {
|
||||
assert_migrate_settings_with_migrations(
|
||||
&[MigrationType::Json(
|
||||
migrations::m_2025_10_10::remove_code_actions_on_format,
|
||||
)],
|
||||
&r#"{
|
||||
"languages": {
|
||||
"JavaScript": {
|
||||
"formatter": "prettier",
|
||||
"code_actions_on_format": {
|
||||
"source.fixAll.eslint": true,
|
||||
"source.organizeImports": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}"#
|
||||
.unindent(),
|
||||
Some(
|
||||
&r#"{
|
||||
"languages": {
|
||||
"JavaScript": {
|
||||
"formatter": [
|
||||
{
|
||||
"code_action": "source.fixAll.eslint"
|
||||
},
|
||||
"prettier"
|
||||
]
|
||||
}
|
||||
}
|
||||
}"#
|
||||
.unindent(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_code_actions_on_format_migration_mixed_global_and_languages() {
|
||||
assert_migrate_settings_with_migrations(
|
||||
&[MigrationType::Json(
|
||||
migrations::m_2025_10_10::remove_code_actions_on_format,
|
||||
)],
|
||||
&r#"{
|
||||
"formatter": "prettier",
|
||||
"code_actions_on_format": {
|
||||
"source.fixAll": true
|
||||
},
|
||||
"languages": {
|
||||
"Rust": {
|
||||
"formatter": "rust-analyzer",
|
||||
"code_actions_on_format": {
|
||||
"source.organizeImports": true
|
||||
}
|
||||
"formatter": {
|
||||
"code_action": "foo"
|
||||
},
|
||||
"Python": {
|
||||
"code_actions_on_format": {
|
||||
"source.organizeImports": true,
|
||||
"source.fixAll": false
|
||||
}
|
||||
"code_actions_on_format": {
|
||||
"bar": true,
|
||||
"baz": false
|
||||
}
|
||||
}
|
||||
}"#
|
||||
.unindent(),
|
||||
Some(
|
||||
&r#"{
|
||||
"formatter": [
|
||||
{
|
||||
"code_action": "source.fixAll"
|
||||
},
|
||||
"prettier"
|
||||
],
|
||||
"languages": {
|
||||
"Rust": {
|
||||
"formatter": [
|
||||
{
|
||||
"code_action": "source.organizeImports"
|
||||
},
|
||||
"rust-analyzer"
|
||||
]
|
||||
},
|
||||
"Python": {
|
||||
"formatter": [
|
||||
{
|
||||
"code_action": "source.organizeImports"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
"code_actions_on_format": {
|
||||
"foo": true,
|
||||
"bar": true,
|
||||
"baz": false
|
||||
}
|
||||
}"#
|
||||
.unindent(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_code_actions_on_format_no_migration_when_not_present() {
|
||||
assert_migrate_settings_with_migrations(
|
||||
&[MigrationType::Json(
|
||||
migrations::m_2025_10_10::remove_code_actions_on_format,
|
||||
migrations::m_2025_10_16::restore_code_actions_on_format,
|
||||
)],
|
||||
&r#"{
|
||||
"formatter": ["prettier"]
|
||||
"formatter": [
|
||||
{ "code_action": "foo" },
|
||||
{ "code_action": "qux" },
|
||||
],
|
||||
"code_actions_on_format": {
|
||||
"bar": true,
|
||||
"baz": false
|
||||
}
|
||||
}"#
|
||||
.unindent(),
|
||||
Some(
|
||||
&r#"{
|
||||
"code_actions_on_format": {
|
||||
"foo": true,
|
||||
"qux": true,
|
||||
"bar": true,
|
||||
"baz": false
|
||||
}
|
||||
}"#
|
||||
.unindent(),
|
||||
),
|
||||
);
|
||||
|
||||
assert_migrate_settings_with_migrations(
|
||||
&[MigrationType::Json(
|
||||
migrations::m_2025_10_16::restore_code_actions_on_format,
|
||||
)],
|
||||
&r#"{
|
||||
"formatter": [],
|
||||
"code_actions_on_format": {
|
||||
"bar": true,
|
||||
"baz": false
|
||||
}
|
||||
}"#
|
||||
.unindent(),
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_code_actions_on_format_migration_all_false_values() {
|
||||
assert_migrate_settings_with_migrations(
|
||||
&[MigrationType::Json(
|
||||
migrations::m_2025_10_10::remove_code_actions_on_format,
|
||||
)],
|
||||
&r#"{
|
||||
"code_actions_on_format": {
|
||||
"a": false,
|
||||
"b": false
|
||||
},
|
||||
"formatter": "prettier"
|
||||
}"#
|
||||
.unindent(),
|
||||
Some(
|
||||
&r#"{
|
||||
"formatter": "prettier"
|
||||
}"#
|
||||
.unindent(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,4 +10,5 @@ pub(crate) use settings::{
|
||||
SETTINGS_ASSISTANT_PATTERN, SETTINGS_ASSISTANT_TOOLS_PATTERN,
|
||||
SETTINGS_DUPLICATED_AGENT_PATTERN, SETTINGS_EDIT_PREDICTIONS_ASSISTANT_PATTERN,
|
||||
SETTINGS_LANGUAGES_PATTERN, SETTINGS_NESTED_KEY_VALUE_PATTERN, SETTINGS_ROOT_KEY_VALUE_PATTERN,
|
||||
migrate_language_setting,
|
||||
};
|
||||
|
||||
@@ -108,3 +108,24 @@ pub const SETTINGS_DUPLICATED_AGENT_PATTERN: &str = r#"(document
|
||||
(#eq? @agent1 "agent")
|
||||
(#eq? @agent2 "agent")
|
||||
)"#;
|
||||
|
||||
/// Migrate language settings,
|
||||
/// calls `migrate_fn` with the top level object as well as all language settings under the "languages" key
|
||||
/// Fails early if `migrate_fn` returns an error at any point
|
||||
pub fn migrate_language_setting(
|
||||
value: &mut serde_json::Value,
|
||||
migrate_fn: fn(&mut serde_json::Value, path: &[&str]) -> anyhow::Result<()>,
|
||||
) -> anyhow::Result<()> {
|
||||
migrate_fn(value, &[])?;
|
||||
let languages = value
|
||||
.as_object_mut()
|
||||
.and_then(|obj| obj.get_mut("languages"))
|
||||
.and_then(|languages| languages.as_object_mut());
|
||||
if let Some(languages) = languages {
|
||||
for (language_name, language) in languages.iter_mut() {
|
||||
let path = vec!["languages", language_name];
|
||||
migrate_fn(language, &path)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -6165,22 +6165,20 @@ impl MultiBufferSnapshot {
|
||||
) -> SmallVec<[Locator; 1]> {
|
||||
let mut sorted_ids = ids.into_iter().collect::<SmallVec<[_; 1]>>();
|
||||
sorted_ids.sort_unstable();
|
||||
sorted_ids.dedup();
|
||||
let mut locators = SmallVec::new();
|
||||
|
||||
while sorted_ids.last() == Some(&ExcerptId::max()) {
|
||||
sorted_ids.pop();
|
||||
if let Some(mapping) = self.excerpt_ids.last() {
|
||||
locators.push(mapping.locator.clone());
|
||||
}
|
||||
locators.push(Locator::max());
|
||||
}
|
||||
|
||||
let mut sorted_ids = sorted_ids.into_iter().dedup().peekable();
|
||||
if sorted_ids.peek() == Some(&ExcerptId::min()) {
|
||||
sorted_ids.next();
|
||||
if let Some(mapping) = self.excerpt_ids.first() {
|
||||
locators.push(mapping.locator.clone());
|
||||
}
|
||||
}
|
||||
let mut sorted_ids = sorted_ids.into_iter().peekable();
|
||||
locators.extend(
|
||||
sorted_ids
|
||||
.peeking_take_while(|excerpt| *excerpt == ExcerptId::min())
|
||||
.map(|_| Locator::min()),
|
||||
);
|
||||
|
||||
let mut cursor = self.excerpt_ids.cursor::<ExcerptId>(());
|
||||
for id in sorted_ids {
|
||||
|
||||
@@ -577,7 +577,6 @@ fn get_or_npm_install_builtin_agent(
|
||||
package_name: SharedString,
|
||||
entrypoint_path: PathBuf,
|
||||
minimum_version: Option<semver::Version>,
|
||||
channel: &'static str,
|
||||
status_tx: Option<watch::Sender<SharedString>>,
|
||||
new_version_available: Option<watch::Sender<Option<String>>>,
|
||||
fs: Arc<dyn Fs>,
|
||||
@@ -649,12 +648,10 @@ fn get_or_npm_install_builtin_agent(
|
||||
let dir = dir.clone();
|
||||
let fs = fs.clone();
|
||||
async move {
|
||||
// TODO remove the filter
|
||||
let latest_version = node_runtime
|
||||
.npm_package_latest_version(&package_name)
|
||||
.await
|
||||
.ok()
|
||||
.filter(|_| channel == "latest");
|
||||
.ok();
|
||||
if let Some(latest_version) = latest_version
|
||||
&& &latest_version != &file_name.to_string_lossy()
|
||||
{
|
||||
@@ -663,7 +660,6 @@ fn get_or_npm_install_builtin_agent(
|
||||
dir.clone(),
|
||||
node_runtime,
|
||||
package_name.clone(),
|
||||
channel,
|
||||
)
|
||||
.await
|
||||
.log_err();
|
||||
@@ -687,7 +683,6 @@ fn get_or_npm_install_builtin_agent(
|
||||
dir.clone(),
|
||||
node_runtime,
|
||||
package_name.clone(),
|
||||
channel,
|
||||
))
|
||||
.await?
|
||||
.into()
|
||||
@@ -736,14 +731,13 @@ async fn download_latest_version(
|
||||
dir: PathBuf,
|
||||
node_runtime: NodeRuntime,
|
||||
package_name: SharedString,
|
||||
channel: &'static str,
|
||||
) -> Result<String> {
|
||||
log::debug!("downloading latest version of {package_name}");
|
||||
|
||||
let tmp_dir = tempfile::tempdir_in(&dir)?;
|
||||
|
||||
node_runtime
|
||||
.npm_install_packages(tmp_dir.path(), &[(&package_name, channel)])
|
||||
.npm_install_packages(tmp_dir.path(), &[(&package_name, "latest")])
|
||||
.await?;
|
||||
|
||||
let version = node_runtime
|
||||
@@ -886,17 +880,12 @@ impl ExternalAgentServer for LocalGemini {
|
||||
GEMINI_NAME.into(),
|
||||
"@google/gemini-cli".into(),
|
||||
"node_modules/@google/gemini-cli/dist/index.js".into(),
|
||||
// TODO remove these windows-specific workarounds once v0.9.0 stable is released
|
||||
if cfg!(windows) {
|
||||
Some("0.9.0-preview.4".parse().unwrap())
|
||||
// v0.8.x on Windows has a bug that causes the initialize request to hang forever
|
||||
Some("0.9.0".parse().unwrap())
|
||||
} else {
|
||||
Some("0.2.1".parse().unwrap())
|
||||
},
|
||||
if cfg!(windows) {
|
||||
"0.9.0-preview.4"
|
||||
} else {
|
||||
"latest"
|
||||
},
|
||||
status_tx,
|
||||
new_version_available_tx,
|
||||
fs,
|
||||
@@ -980,7 +969,6 @@ impl ExternalAgentServer for LocalClaudeCode {
|
||||
"@zed-industries/claude-code-acp".into(),
|
||||
"node_modules/@zed-industries/claude-code-acp/dist/index.js".into(),
|
||||
Some("0.5.2".parse().unwrap()),
|
||||
"latest",
|
||||
status_tx,
|
||||
new_version_available_tx,
|
||||
fs,
|
||||
|
||||
@@ -14,12 +14,13 @@ use super::dap_command::{
|
||||
TerminateCommand, TerminateThreadsCommand, ThreadsCommand, VariablesCommand,
|
||||
};
|
||||
use super::dap_store::DapStore;
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use anyhow::{Context as _, Result, anyhow, bail};
|
||||
use base64::Engine;
|
||||
use collections::{HashMap, HashSet, IndexMap};
|
||||
use dap::adapters::{DebugAdapterBinary, DebugAdapterName};
|
||||
use dap::messages::Response;
|
||||
use dap::requests::{Request, RunInTerminal, StartDebugging};
|
||||
use dap::transport::TcpTransport;
|
||||
use dap::{
|
||||
Capabilities, ContinueArguments, EvaluateArgumentsContext, Module, Source, StackFrameId,
|
||||
SteppingGranularity, StoppedEvent, VariableReference,
|
||||
@@ -47,12 +48,14 @@ use remote::RemoteClient;
|
||||
use rpc::ErrorExt;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use smol::net::TcpListener;
|
||||
use smol::net::{TcpListener, TcpStream};
|
||||
use std::any::TypeId;
|
||||
use std::collections::BTreeMap;
|
||||
use std::net::Ipv4Addr;
|
||||
use std::ops::RangeInclusive;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Stdio;
|
||||
use std::time::Duration;
|
||||
use std::u64;
|
||||
use std::{
|
||||
any::Any,
|
||||
@@ -63,6 +66,7 @@ use std::{
|
||||
};
|
||||
use task::TaskContext;
|
||||
use text::{PointUtf16, ToPointUtf16};
|
||||
use url::Url;
|
||||
use util::command::new_smol_command;
|
||||
use util::{ResultExt, debug_panic, maybe};
|
||||
use worktree::Worktree;
|
||||
@@ -2768,31 +2772,42 @@ impl Session {
|
||||
|
||||
let mut console_output = self.console_output(cx);
|
||||
let task = cx.spawn(async move |this, cx| {
|
||||
let (dap_port, _child) =
|
||||
if remote_client.read_with(cx, |client, _| client.shares_network_interface())? {
|
||||
(request.server_port, None)
|
||||
} else {
|
||||
let port = {
|
||||
let listener = TcpListener::bind("127.0.0.1:0")
|
||||
.await
|
||||
.context("getting port for DAP")?;
|
||||
listener.local_addr()?.port()
|
||||
};
|
||||
let child = remote_client.update(cx, |client, _| {
|
||||
let command = client.build_forward_port_command(
|
||||
port,
|
||||
"localhost".into(),
|
||||
request.server_port,
|
||||
)?;
|
||||
let child = new_smol_command(command.program)
|
||||
.args(command.args)
|
||||
.envs(command.env)
|
||||
.spawn()
|
||||
.context("spawning port forwarding process")?;
|
||||
anyhow::Ok(child)
|
||||
})??;
|
||||
(port, Some(child))
|
||||
};
|
||||
let forward_ports_process = if remote_client
|
||||
.read_with(cx, |client, _| client.shares_network_interface())?
|
||||
{
|
||||
request.other.insert(
|
||||
"proxyUri".into(),
|
||||
format!("127.0.0.1:{}", request.server_port).into(),
|
||||
);
|
||||
None
|
||||
} else {
|
||||
let port = TcpTransport::unused_port(Ipv4Addr::LOCALHOST)
|
||||
.await
|
||||
.context("getting port for DAP")?;
|
||||
request
|
||||
.other
|
||||
.insert("proxyUri".into(), format!("127.0.0.1:{port}").into());
|
||||
let mut port_forwards = vec![(port, "localhost".to_owned(), request.server_port)];
|
||||
|
||||
if let Some(value) = request.params.get("url")
|
||||
&& let Some(url) = value.as_str()
|
||||
&& let Some(url) = Url::parse(url).ok()
|
||||
&& let Some(frontend_port) = url.port()
|
||||
{
|
||||
port_forwards.push((frontend_port, "localhost".to_owned(), frontend_port));
|
||||
}
|
||||
|
||||
let child = remote_client.update(cx, |client, _| {
|
||||
let command = client.build_forward_ports_command(port_forwards)?;
|
||||
let child = new_smol_command(command.program)
|
||||
.args(command.args)
|
||||
.envs(command.env)
|
||||
.spawn()
|
||||
.context("spawning port forwarding process")?;
|
||||
anyhow::Ok(child)
|
||||
})??;
|
||||
Some(child)
|
||||
};
|
||||
|
||||
let mut companion_process = None;
|
||||
let companion_port =
|
||||
@@ -2814,14 +2829,17 @@ impl Session {
|
||||
}
|
||||
}
|
||||
};
|
||||
this.update(cx, |this, cx| {
|
||||
this.companion_port = Some(companion_port);
|
||||
let Some(mut child) = companion_process else {
|
||||
return;
|
||||
};
|
||||
if let Some(stderr) = child.stderr.take() {
|
||||
|
||||
let mut background_tasks = Vec::new();
|
||||
if let Some(mut forward_ports_process) = forward_ports_process {
|
||||
background_tasks.push(cx.spawn(async move |_| {
|
||||
forward_ports_process.status().await.log_err();
|
||||
}));
|
||||
};
|
||||
if let Some(mut companion_process) = companion_process {
|
||||
if let Some(stderr) = companion_process.stderr.take() {
|
||||
let mut console_output = console_output.clone();
|
||||
this.background_tasks.push(cx.spawn(async move |_, _| {
|
||||
background_tasks.push(cx.spawn(async move |_| {
|
||||
let mut stderr = BufReader::new(stderr);
|
||||
let mut line = String::new();
|
||||
while let Ok(n) = stderr.read_line(&mut line).await
|
||||
@@ -2835,9 +2853,9 @@ impl Session {
|
||||
}
|
||||
}));
|
||||
}
|
||||
this.background_tasks.push(cx.spawn({
|
||||
background_tasks.push(cx.spawn({
|
||||
let mut console_output = console_output.clone();
|
||||
async move |_, _| match child.status().await {
|
||||
async move |_| match companion_process.status().await {
|
||||
Ok(status) => {
|
||||
if status.success() {
|
||||
console_output
|
||||
@@ -2860,17 +2878,33 @@ impl Session {
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
}))
|
||||
})?;
|
||||
}));
|
||||
}
|
||||
|
||||
request
|
||||
.other
|
||||
.insert("proxyUri".into(), format!("127.0.0.1:{dap_port}").into());
|
||||
// TODO pass wslInfo as needed
|
||||
|
||||
let companion_address = format!("127.0.0.1:{companion_port}");
|
||||
let mut companion_started = false;
|
||||
for _ in 0..10 {
|
||||
if TcpStream::connect(&companion_address).await.is_ok() {
|
||||
companion_started = true;
|
||||
break;
|
||||
}
|
||||
cx.background_executor()
|
||||
.timer(Duration::from_millis(100))
|
||||
.await;
|
||||
}
|
||||
if !companion_started {
|
||||
console_output
|
||||
.send("Browser companion failed to start".into())
|
||||
.await
|
||||
.ok();
|
||||
bail!("Browser companion failed to start");
|
||||
}
|
||||
|
||||
let response = http_client
|
||||
.post_json(
|
||||
&format!("http://127.0.0.1:{companion_port}/launch-and-attach"),
|
||||
&format!("http://{companion_address}/launch-and-attach"),
|
||||
serde_json::to_string(&request)
|
||||
.context("serializing request")?
|
||||
.into(),
|
||||
@@ -2895,6 +2929,11 @@ impl Session {
|
||||
}
|
||||
}
|
||||
|
||||
this.update(cx, |this, _| {
|
||||
this.background_tasks.extend(background_tasks);
|
||||
this.companion_port = Some(companion_port);
|
||||
})?;
|
||||
|
||||
anyhow::Ok(())
|
||||
});
|
||||
self.background_tasks.push(cx.spawn(async move |_, _| {
|
||||
@@ -2926,15 +2965,16 @@ impl Session {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct LaunchBrowserInCompanionParams {
|
||||
server_port: u16,
|
||||
params: HashMap<String, serde_json::Value>,
|
||||
#[serde(flatten)]
|
||||
other: HashMap<String, serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct KillCompanionBrowserParams {
|
||||
launch_id: u64,
|
||||
|
||||
@@ -26,8 +26,8 @@ use language::{
|
||||
use lsp::{
|
||||
AdapterServerCapabilities, CodeActionKind, CodeActionOptions, CodeDescription,
|
||||
CompletionContext, CompletionListItemDefaultsEditRange, CompletionTriggerKind,
|
||||
DocumentHighlightKind, LanguageServer, LanguageServerId, LinkedEditingRangeServerCapabilities,
|
||||
OneOf, RenameOptions, ServerCapabilities,
|
||||
DiagnosticServerCapabilities, DocumentHighlightKind, LanguageServer, LanguageServerId,
|
||||
LinkedEditingRangeServerCapabilities, OneOf, RenameOptions, ServerCapabilities,
|
||||
};
|
||||
use serde_json::Value;
|
||||
use signature_help::{lsp_to_proto_signature, proto_to_lsp_signature};
|
||||
@@ -262,6 +262,9 @@ pub(crate) struct LinkedEditingRange {
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct GetDocumentDiagnostics {
|
||||
/// We cannot blindly rely on server's capabilities.diagnostic_provider, as they're a singular field, whereas
|
||||
/// a server can register multiple diagnostic providers post-mortem.
|
||||
pub dynamic_caps: DiagnosticServerCapabilities,
|
||||
pub previous_result_id: Option<String>,
|
||||
}
|
||||
|
||||
@@ -4019,26 +4022,22 @@ impl LspCommand for GetDocumentDiagnostics {
|
||||
"Get diagnostics"
|
||||
}
|
||||
|
||||
fn check_capabilities(&self, server_capabilities: AdapterServerCapabilities) -> bool {
|
||||
server_capabilities
|
||||
.server_capabilities
|
||||
.diagnostic_provider
|
||||
.is_some()
|
||||
fn check_capabilities(&self, _: AdapterServerCapabilities) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn to_lsp(
|
||||
&self,
|
||||
path: &Path,
|
||||
_: &Buffer,
|
||||
language_server: &Arc<LanguageServer>,
|
||||
_: &Arc<LanguageServer>,
|
||||
_: &App,
|
||||
) -> Result<lsp::DocumentDiagnosticParams> {
|
||||
let identifier = match language_server.capabilities().diagnostic_provider {
|
||||
Some(lsp::DiagnosticServerCapabilities::Options(options)) => options.identifier,
|
||||
Some(lsp::DiagnosticServerCapabilities::RegistrationOptions(options)) => {
|
||||
options.diagnostic_options.identifier
|
||||
let identifier = match &self.dynamic_caps {
|
||||
lsp::DiagnosticServerCapabilities::Options(options) => options.identifier.clone(),
|
||||
lsp::DiagnosticServerCapabilities::RegistrationOptions(options) => {
|
||||
options.diagnostic_options.identifier.clone()
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
|
||||
Ok(lsp::DocumentDiagnosticParams {
|
||||
|
||||
@@ -14,6 +14,7 @@ pub mod json_language_server_ext;
|
||||
pub mod log_store;
|
||||
pub mod lsp_ext_command;
|
||||
pub mod rust_analyzer_ext;
|
||||
pub mod vue_language_server_ext;
|
||||
|
||||
use crate::{
|
||||
CodeAction, ColorPresentation, Completion, CompletionDisplayOptions, CompletionResponse,
|
||||
@@ -70,12 +71,12 @@ use language::{
|
||||
range_from_lsp, range_to_lsp,
|
||||
};
|
||||
use lsp::{
|
||||
AdapterServerCapabilities, CodeActionKind, CompletionContext, DiagnosticSeverity,
|
||||
DiagnosticTag, DidChangeWatchedFilesRegistrationOptions, Edit, FileOperationFilter,
|
||||
FileOperationPatternKind, FileOperationRegistrationOptions, FileRename, FileSystemWatcher,
|
||||
LSP_REQUEST_TIMEOUT, LanguageServer, LanguageServerBinary, LanguageServerBinaryOptions,
|
||||
LanguageServerId, LanguageServerName, LanguageServerSelector, LspRequestFuture,
|
||||
MessageActionItem, MessageType, OneOf, RenameFilesParams, SymbolKind,
|
||||
AdapterServerCapabilities, CodeActionKind, CompletionContext, DiagnosticServerCapabilities,
|
||||
DiagnosticSeverity, DiagnosticTag, DidChangeWatchedFilesRegistrationOptions, Edit,
|
||||
FileOperationFilter, FileOperationPatternKind, FileOperationRegistrationOptions, FileRename,
|
||||
FileSystemWatcher, LSP_REQUEST_TIMEOUT, LanguageServer, LanguageServerBinary,
|
||||
LanguageServerBinaryOptions, LanguageServerId, LanguageServerName, LanguageServerSelector,
|
||||
LspRequestFuture, MessageActionItem, MessageType, OneOf, RenameFilesParams, SymbolKind,
|
||||
TextDocumentSyncSaveOptions, TextEdit, Uri, WillRenameFiles, WorkDoneProgressCancelParams,
|
||||
WorkspaceFolder, notification::DidRenameFiles,
|
||||
};
|
||||
@@ -181,6 +182,12 @@ pub struct DocumentDiagnostics {
|
||||
version: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct DynamicRegistrations {
|
||||
did_change_watched_files: HashMap<String, Vec<FileSystemWatcher>>,
|
||||
diagnostics: HashMap<Option<String>, DiagnosticServerCapabilities>,
|
||||
}
|
||||
|
||||
pub struct LocalLspStore {
|
||||
weak: WeakEntity<LspStore>,
|
||||
worktree_store: Entity<WorktreeStore>,
|
||||
@@ -198,8 +205,7 @@ pub struct LocalLspStore {
|
||||
watched_manifest_filenames: HashSet<ManifestName>,
|
||||
language_server_paths_watched_for_rename:
|
||||
HashMap<LanguageServerId, RenamePathsWatchedForServer>,
|
||||
language_server_watcher_registrations:
|
||||
HashMap<LanguageServerId, HashMap<String, Vec<FileSystemWatcher>>>,
|
||||
language_server_dynamic_registrations: HashMap<LanguageServerId, DynamicRegistrations>,
|
||||
supplementary_language_servers:
|
||||
HashMap<LanguageServerId, (LanguageServerName, Arc<LanguageServer>)>,
|
||||
prettier_store: Entity<PrettierStore>,
|
||||
@@ -987,6 +993,7 @@ impl LocalLspStore {
|
||||
})
|
||||
.detach();
|
||||
|
||||
vue_language_server_ext::register_requests(this.clone(), language_server);
|
||||
json_language_server_ext::register_requests(this.clone(), language_server);
|
||||
rust_analyzer_ext::register_notifications(this.clone(), language_server);
|
||||
clangd_ext::register_notifications(this, language_server, adapter);
|
||||
@@ -1333,6 +1340,32 @@ impl LocalLspStore {
|
||||
})?;
|
||||
}
|
||||
|
||||
// Formatter for `code_actions_on_format` that runs before
|
||||
// the rest of the formatters
|
||||
let mut code_actions_on_format_formatters = None;
|
||||
let should_run_code_actions_on_format = !matches!(
|
||||
(trigger, &settings.format_on_save),
|
||||
(FormatTrigger::Save, &FormatOnSave::Off)
|
||||
);
|
||||
if should_run_code_actions_on_format {
|
||||
let have_code_actions_to_run_on_format = settings
|
||||
.code_actions_on_format
|
||||
.values()
|
||||
.any(|enabled| *enabled);
|
||||
if have_code_actions_to_run_on_format {
|
||||
zlog::trace!(logger => "going to run code actions on format");
|
||||
code_actions_on_format_formatters = Some(
|
||||
settings
|
||||
.code_actions_on_format
|
||||
.iter()
|
||||
.filter_map(|(action, enabled)| enabled.then_some(action))
|
||||
.cloned()
|
||||
.map(Formatter::CodeAction)
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let formatters = match (trigger, &settings.format_on_save) {
|
||||
(FormatTrigger::Save, FormatOnSave::Off) => &[],
|
||||
(FormatTrigger::Manual, _) | (FormatTrigger::Save, FormatOnSave::On) => {
|
||||
@@ -1340,6 +1373,11 @@ impl LocalLspStore {
|
||||
}
|
||||
};
|
||||
|
||||
let formatters = code_actions_on_format_formatters
|
||||
.iter()
|
||||
.flatten()
|
||||
.chain(formatters);
|
||||
|
||||
for formatter in formatters {
|
||||
let formatter = if formatter == &Formatter::Auto {
|
||||
if settings.prettier.allowed {
|
||||
@@ -3142,7 +3180,7 @@ impl LocalLspStore {
|
||||
|
||||
for watcher in watchers {
|
||||
if let Some((worktree, literal_prefix, pattern)) =
|
||||
self.worktree_and_path_for_file_watcher(&worktrees, watcher, cx)
|
||||
Self::worktree_and_path_for_file_watcher(&worktrees, watcher, cx)
|
||||
{
|
||||
worktree.update(cx, |worktree, _| {
|
||||
if let Some((tree, glob)) =
|
||||
@@ -3240,7 +3278,6 @@ impl LocalLspStore {
|
||||
}
|
||||
|
||||
fn worktree_and_path_for_file_watcher(
|
||||
&self,
|
||||
worktrees: &[Entity<Worktree>],
|
||||
watcher: &FileSystemWatcher,
|
||||
cx: &App,
|
||||
@@ -3288,15 +3325,18 @@ impl LocalLspStore {
|
||||
language_server_id: LanguageServerId,
|
||||
cx: &mut Context<LspStore>,
|
||||
) {
|
||||
let Some(watchers) = self
|
||||
.language_server_watcher_registrations
|
||||
let Some(registrations) = self
|
||||
.language_server_dynamic_registrations
|
||||
.get(&language_server_id)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let watch_builder =
|
||||
self.rebuild_watched_paths_inner(language_server_id, watchers.values().flatten(), cx);
|
||||
let watch_builder = self.rebuild_watched_paths_inner(
|
||||
language_server_id,
|
||||
registrations.did_change_watched_files.values().flatten(),
|
||||
cx,
|
||||
);
|
||||
let watcher = watch_builder.build(self.fs.clone(), language_server_id, cx);
|
||||
self.language_server_watched_paths
|
||||
.insert(language_server_id, watcher);
|
||||
@@ -3312,11 +3352,13 @@ impl LocalLspStore {
|
||||
cx: &mut Context<LspStore>,
|
||||
) {
|
||||
let registrations = self
|
||||
.language_server_watcher_registrations
|
||||
.language_server_dynamic_registrations
|
||||
.entry(language_server_id)
|
||||
.or_default();
|
||||
|
||||
registrations.insert(registration_id.to_string(), params.watchers);
|
||||
registrations
|
||||
.did_change_watched_files
|
||||
.insert(registration_id.to_string(), params.watchers);
|
||||
|
||||
self.rebuild_watched_paths(language_server_id, cx);
|
||||
}
|
||||
@@ -3328,11 +3370,15 @@ impl LocalLspStore {
|
||||
cx: &mut Context<LspStore>,
|
||||
) {
|
||||
let registrations = self
|
||||
.language_server_watcher_registrations
|
||||
.language_server_dynamic_registrations
|
||||
.entry(language_server_id)
|
||||
.or_default();
|
||||
|
||||
if registrations.remove(registration_id).is_some() {
|
||||
if registrations
|
||||
.did_change_watched_files
|
||||
.remove(registration_id)
|
||||
.is_some()
|
||||
{
|
||||
log::info!(
|
||||
"language server {}: unregistered workspace/DidChangeWatchedFiles capability with id {}",
|
||||
language_server_id,
|
||||
@@ -3703,7 +3749,7 @@ impl LspStore {
|
||||
last_workspace_edits_by_language_server: Default::default(),
|
||||
language_server_watched_paths: Default::default(),
|
||||
language_server_paths_watched_for_rename: Default::default(),
|
||||
language_server_watcher_registrations: Default::default(),
|
||||
language_server_dynamic_registrations: Default::default(),
|
||||
buffers_being_formatted: Default::default(),
|
||||
buffer_snapshots: Default::default(),
|
||||
prettier_store,
|
||||
@@ -4291,7 +4337,7 @@ impl LspStore {
|
||||
cx: &Context<Self>,
|
||||
) -> bool
|
||||
where
|
||||
F: Fn(&lsp::ServerCapabilities) -> bool,
|
||||
F: FnMut(&lsp::ServerCapabilities) -> bool,
|
||||
{
|
||||
let Some(language) = buffer.read(cx).language().cloned() else {
|
||||
return false;
|
||||
@@ -6293,12 +6339,30 @@ impl LspStore {
|
||||
let buffer_id = buffer.read(cx).remote_id();
|
||||
|
||||
if let Some((client, upstream_project_id)) = self.upstream_client() {
|
||||
let mut suitable_capabilities = None;
|
||||
// Are we capable for proto request?
|
||||
let any_server_has_diagnostics_provider = self.check_if_capable_for_proto_request(
|
||||
&buffer,
|
||||
|capabilities| {
|
||||
if let Some(caps) = &capabilities.diagnostic_provider {
|
||||
suitable_capabilities = Some(caps.clone());
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
},
|
||||
cx,
|
||||
);
|
||||
// We don't really care which caps are passed into the request, as they're ignored by RPC anyways.
|
||||
let Some(dynamic_caps) = suitable_capabilities else {
|
||||
return Task::ready(Ok(None));
|
||||
};
|
||||
assert!(any_server_has_diagnostics_provider);
|
||||
|
||||
let request = GetDocumentDiagnostics {
|
||||
previous_result_id: None,
|
||||
dynamic_caps,
|
||||
};
|
||||
if !self.is_capable_for_proto_request(&buffer, &request, cx) {
|
||||
return Task::ready(Ok(None));
|
||||
}
|
||||
let request_task = client.request_lsp(
|
||||
upstream_project_id,
|
||||
LSP_REQUEST_TIMEOUT,
|
||||
@@ -6313,23 +6377,44 @@ impl LspStore {
|
||||
Ok(None)
|
||||
})
|
||||
} else {
|
||||
let server_ids = buffer.update(cx, |buffer, cx| {
|
||||
let servers = buffer.update(cx, |buffer, cx| {
|
||||
self.language_servers_for_local_buffer(buffer, cx)
|
||||
.map(|(_, server)| server.server_id())
|
||||
.map(|(_, server)| server.clone())
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
let pull_diagnostics = server_ids
|
||||
|
||||
let pull_diagnostics = servers
|
||||
.into_iter()
|
||||
.map(|server_id| {
|
||||
let result_id = self.result_id(server_id, buffer_id, cx);
|
||||
self.request_lsp(
|
||||
buffer.clone(),
|
||||
LanguageServerToQuery::Other(server_id),
|
||||
GetDocumentDiagnostics {
|
||||
previous_result_id: result_id,
|
||||
},
|
||||
cx,
|
||||
)
|
||||
.flat_map(|server| {
|
||||
let result = maybe!({
|
||||
let local = self.as_local()?;
|
||||
let server_id = server.server_id();
|
||||
let providers_with_identifiers = local
|
||||
.language_server_dynamic_registrations
|
||||
.get(&server_id)
|
||||
.into_iter()
|
||||
.flat_map(|registrations| registrations.diagnostics.values().cloned())
|
||||
.collect::<Vec<_>>();
|
||||
Some(
|
||||
providers_with_identifiers
|
||||
.into_iter()
|
||||
.map(|dynamic_caps| {
|
||||
let result_id = self.result_id(server_id, buffer_id, cx);
|
||||
self.request_lsp(
|
||||
buffer.clone(),
|
||||
LanguageServerToQuery::Other(server_id),
|
||||
GetDocumentDiagnostics {
|
||||
previous_result_id: result_id,
|
||||
dynamic_caps,
|
||||
},
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
});
|
||||
|
||||
result.unwrap_or_default()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
@@ -8862,14 +8947,17 @@ impl LspStore {
|
||||
);
|
||||
}
|
||||
lsp::ProgressParamsValue::WorkspaceDiagnostic(report) => {
|
||||
let identifier = token.split_once("id:").map(|(_, id)| id.to_owned());
|
||||
if let Some(LanguageServerState::Running {
|
||||
workspace_refresh_task: Some(workspace_refresh_task),
|
||||
workspace_diagnostics_refresh_tasks,
|
||||
..
|
||||
}) = self
|
||||
.as_local_mut()
|
||||
.and_then(|local| local.language_servers.get_mut(&language_server_id))
|
||||
&& let Some(workspace_diagnostics) =
|
||||
workspace_diagnostics_refresh_tasks.get_mut(&identifier)
|
||||
{
|
||||
workspace_refresh_task.progress_tx.try_send(()).ok();
|
||||
workspace_diagnostics.progress_tx.try_send(()).ok();
|
||||
self.apply_workspace_diagnostic_report(language_server_id, report, cx)
|
||||
}
|
||||
}
|
||||
@@ -10375,13 +10463,31 @@ impl LspStore {
|
||||
let workspace_folders = workspace_folders.lock().clone();
|
||||
language_server.set_workspace_folders(workspace_folders);
|
||||
|
||||
let workspace_diagnostics_refresh_tasks = language_server
|
||||
.capabilities()
|
||||
.diagnostic_provider
|
||||
.and_then(|provider| {
|
||||
let workspace_refresher = lsp_workspace_diagnostics_refresh(
|
||||
None,
|
||||
provider.clone(),
|
||||
language_server.clone(),
|
||||
cx,
|
||||
)?;
|
||||
local
|
||||
.language_server_dynamic_registrations
|
||||
.entry(server_id)
|
||||
.or_default()
|
||||
.diagnostics
|
||||
.entry(None)
|
||||
.or_insert(provider);
|
||||
Some((None, workspace_refresher))
|
||||
})
|
||||
.into_iter()
|
||||
.collect();
|
||||
local.language_servers.insert(
|
||||
server_id,
|
||||
LanguageServerState::Running {
|
||||
workspace_refresh_task: lsp_workspace_diagnostics_refresh(
|
||||
language_server.clone(),
|
||||
cx,
|
||||
),
|
||||
workspace_diagnostics_refresh_tasks,
|
||||
adapter: adapter.clone(),
|
||||
server: language_server.clone(),
|
||||
simulate_disk_based_diagnostics_completion: None,
|
||||
@@ -11091,13 +11197,15 @@ impl LspStore {
|
||||
|
||||
pub fn pull_workspace_diagnostics(&mut self, server_id: LanguageServerId) {
|
||||
if let Some(LanguageServerState::Running {
|
||||
workspace_refresh_task: Some(workspace_refresh_task),
|
||||
workspace_diagnostics_refresh_tasks,
|
||||
..
|
||||
}) = self
|
||||
.as_local_mut()
|
||||
.and_then(|local| local.language_servers.get_mut(&server_id))
|
||||
{
|
||||
workspace_refresh_task.refresh_tx.try_send(()).ok();
|
||||
for diagnostics in workspace_diagnostics_refresh_tasks.values_mut() {
|
||||
diagnostics.refresh_tx.try_send(()).ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11113,11 +11221,13 @@ impl LspStore {
|
||||
local.language_server_ids_for_buffer(buffer, cx)
|
||||
}) {
|
||||
if let Some(LanguageServerState::Running {
|
||||
workspace_refresh_task: Some(workspace_refresh_task),
|
||||
workspace_diagnostics_refresh_tasks,
|
||||
..
|
||||
}) = local.language_servers.get_mut(&server_id)
|
||||
{
|
||||
workspace_refresh_task.refresh_tx.try_send(()).ok();
|
||||
for diagnostics in workspace_diagnostics_refresh_tasks.values_mut() {
|
||||
diagnostics.refresh_tx.try_send(()).ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11443,26 +11553,49 @@ impl LspStore {
|
||||
"textDocument/diagnostic" => {
|
||||
if let Some(caps) = reg
|
||||
.register_options
|
||||
.map(serde_json::from_value)
|
||||
.map(serde_json::from_value::<DiagnosticServerCapabilities>)
|
||||
.transpose()?
|
||||
{
|
||||
let state = self
|
||||
let local = self
|
||||
.as_local_mut()
|
||||
.context("Expected LSP Store to be local")?
|
||||
.context("Expected LSP Store to be local")?;
|
||||
let state = local
|
||||
.language_servers
|
||||
.get_mut(&server_id)
|
||||
.context("Could not obtain Language Servers state")?;
|
||||
server.update_capabilities(|capabilities| {
|
||||
capabilities.diagnostic_provider = Some(caps);
|
||||
});
|
||||
local
|
||||
.language_server_dynamic_registrations
|
||||
.get_mut(&server_id)
|
||||
.and_then(|registrations| {
|
||||
registrations
|
||||
.diagnostics
|
||||
.insert(Some(reg.id.clone()), caps.clone())
|
||||
});
|
||||
|
||||
let mut can_now_provide_diagnostics = false;
|
||||
if let LanguageServerState::Running {
|
||||
workspace_refresh_task,
|
||||
workspace_diagnostics_refresh_tasks,
|
||||
..
|
||||
} = state
|
||||
&& workspace_refresh_task.is_none()
|
||||
&& let Some(task) = lsp_workspace_diagnostics_refresh(
|
||||
Some(reg.id.clone()),
|
||||
caps.clone(),
|
||||
server.clone(),
|
||||
cx,
|
||||
)
|
||||
{
|
||||
*workspace_refresh_task =
|
||||
lsp_workspace_diagnostics_refresh(server.clone(), cx)
|
||||
workspace_diagnostics_refresh_tasks.insert(Some(reg.id), task);
|
||||
can_now_provide_diagnostics = true;
|
||||
}
|
||||
|
||||
// We don't actually care about capabilities.diagnostic_provider, but it IS relevant for the remote peer
|
||||
// to know that there's at least one provider. Otherwise, it will never ask us to issue documentdiagnostic calls on their behalf,
|
||||
// as it'll think that they're not supported.
|
||||
if can_now_provide_diagnostics {
|
||||
server.update_capabilities(|capabilities| {
|
||||
debug_assert!(capabilities.diagnostic_provider.is_none());
|
||||
capabilities.diagnostic_provider = Some(caps);
|
||||
});
|
||||
}
|
||||
|
||||
notify_server_capabilities_updated(&server, cx);
|
||||
@@ -11625,22 +11758,45 @@ impl LspStore {
|
||||
notify_server_capabilities_updated(&server, cx);
|
||||
}
|
||||
"textDocument/diagnostic" => {
|
||||
server.update_capabilities(|capabilities| {
|
||||
capabilities.diagnostic_provider = None;
|
||||
});
|
||||
let state = self
|
||||
let local = self
|
||||
.as_local_mut()
|
||||
.context("Expected LSP Store to be local")?
|
||||
.context("Expected LSP Store to be local")?;
|
||||
|
||||
let state = local
|
||||
.language_servers
|
||||
.get_mut(&server_id)
|
||||
.context("Could not obtain Language Servers state")?;
|
||||
if let LanguageServerState::Running {
|
||||
workspace_refresh_task,
|
||||
..
|
||||
} = state
|
||||
let options = local
|
||||
.language_server_dynamic_registrations
|
||||
.get_mut(&server_id)
|
||||
.with_context(|| {
|
||||
format!("Expected dynamic registration to exist for server {server_id}")
|
||||
})?.diagnostics
|
||||
.remove(&Some(unreg.id.clone()))
|
||||
.with_context(|| format!(
|
||||
"Attempted to unregister non-existent diagnostic registration with ID {}",
|
||||
unreg.id)
|
||||
)?;
|
||||
|
||||
let mut has_any_diagnostic_providers_still = true;
|
||||
if let Some(identifier) = diagnostic_identifier(&options)
|
||||
&& let LanguageServerState::Running {
|
||||
workspace_diagnostics_refresh_tasks,
|
||||
..
|
||||
} = state
|
||||
{
|
||||
_ = workspace_refresh_task.take();
|
||||
workspace_diagnostics_refresh_tasks.remove(&identifier);
|
||||
has_any_diagnostic_providers_still =
|
||||
!workspace_diagnostics_refresh_tasks.is_empty();
|
||||
}
|
||||
|
||||
if !has_any_diagnostic_providers_still {
|
||||
server.update_capabilities(|capabilities| {
|
||||
debug_assert!(capabilities.diagnostic_provider.is_some());
|
||||
capabilities.diagnostic_provider = None;
|
||||
});
|
||||
}
|
||||
|
||||
notify_server_capabilities_updated(&server, cx);
|
||||
}
|
||||
"textDocument/documentColor" => {
|
||||
@@ -11826,24 +11982,12 @@ fn subscribe_to_binary_statuses(
|
||||
}
|
||||
|
||||
fn lsp_workspace_diagnostics_refresh(
|
||||
registration_id: Option<String>,
|
||||
options: DiagnosticServerCapabilities,
|
||||
server: Arc<LanguageServer>,
|
||||
cx: &mut Context<'_, LspStore>,
|
||||
) -> Option<WorkspaceRefreshTask> {
|
||||
let identifier = match server.capabilities().diagnostic_provider? {
|
||||
lsp::DiagnosticServerCapabilities::Options(diagnostic_options) => {
|
||||
if !diagnostic_options.workspace_diagnostics {
|
||||
return None;
|
||||
}
|
||||
diagnostic_options.identifier
|
||||
}
|
||||
lsp::DiagnosticServerCapabilities::RegistrationOptions(registration_options) => {
|
||||
let diagnostic_options = registration_options.diagnostic_options;
|
||||
if !diagnostic_options.workspace_diagnostics {
|
||||
return None;
|
||||
}
|
||||
diagnostic_options.identifier
|
||||
}
|
||||
};
|
||||
let identifier = diagnostic_identifier(&options)?;
|
||||
|
||||
let (progress_tx, mut progress_rx) = mpsc::channel(1);
|
||||
let (mut refresh_tx, mut refresh_rx) = mpsc::channel(1);
|
||||
@@ -11889,7 +12033,14 @@ fn lsp_workspace_diagnostics_refresh(
|
||||
return;
|
||||
};
|
||||
|
||||
let token = format!("workspace/diagnostic-{}-{}", server.server_id(), requests);
|
||||
let token = if let Some(identifier) = ®istration_id {
|
||||
format!(
|
||||
"workspace/diagnostic/{}/{requests}/id:{identifier}",
|
||||
server.server_id(),
|
||||
)
|
||||
} else {
|
||||
format!("workspace/diagnostic/{}/{requests}", server.server_id())
|
||||
};
|
||||
|
||||
progress_rx.try_recv().ok();
|
||||
let timer =
|
||||
@@ -11955,6 +12106,24 @@ fn lsp_workspace_diagnostics_refresh(
|
||||
})
|
||||
}
|
||||
|
||||
fn diagnostic_identifier(options: &DiagnosticServerCapabilities) -> Option<Option<String>> {
|
||||
match &options {
|
||||
lsp::DiagnosticServerCapabilities::Options(diagnostic_options) => {
|
||||
if !diagnostic_options.workspace_diagnostics {
|
||||
return None;
|
||||
}
|
||||
Some(diagnostic_options.identifier.clone())
|
||||
}
|
||||
lsp::DiagnosticServerCapabilities::RegistrationOptions(registration_options) => {
|
||||
let diagnostic_options = ®istration_options.diagnostic_options;
|
||||
if !diagnostic_options.workspace_diagnostics {
|
||||
return None;
|
||||
}
|
||||
Some(diagnostic_options.identifier.clone())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_word_completion(snapshot: &BufferSnapshot, completion: &mut Completion) {
|
||||
let CompletionSource::BufferWord {
|
||||
word_range,
|
||||
@@ -12359,7 +12528,7 @@ pub enum LanguageServerState {
|
||||
adapter: Arc<CachedLspAdapter>,
|
||||
server: Arc<LanguageServer>,
|
||||
simulate_disk_based_diagnostics_completion: Option<Task<()>>,
|
||||
workspace_refresh_task: Option<WorkspaceRefreshTask>,
|
||||
workspace_diagnostics_refresh_tasks: HashMap<Option<String>, WorkspaceRefreshTask>,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
124
crates/project/src/lsp_store/vue_language_server_ext.rs
Normal file
124
crates/project/src/lsp_store/vue_language_server_ext.rs
Normal file
@@ -0,0 +1,124 @@
|
||||
use anyhow::Context as _;
|
||||
use gpui::{AppContext, WeakEntity};
|
||||
use lsp::{LanguageServer, LanguageServerName};
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::LspStore;
|
||||
|
||||
struct VueServerRequest;
|
||||
struct TypescriptServerResponse;
|
||||
|
||||
impl lsp::notification::Notification for VueServerRequest {
|
||||
type Params = Vec<(u64, String, serde_json::Value)>;
|
||||
|
||||
const METHOD: &'static str = "tsserver/request";
|
||||
}
|
||||
|
||||
impl lsp::notification::Notification for TypescriptServerResponse {
|
||||
type Params = Vec<(u64, serde_json::Value)>;
|
||||
|
||||
const METHOD: &'static str = "tsserver/response";
|
||||
}
|
||||
|
||||
const VUE_SERVER_NAME: LanguageServerName = LanguageServerName::new_static("vue-language-server");
|
||||
const VTSLS: LanguageServerName = LanguageServerName::new_static("vtsls");
|
||||
const TS_LS: LanguageServerName = LanguageServerName::new_static("typescript-language-server");
|
||||
|
||||
pub fn register_requests(lsp_store: WeakEntity<LspStore>, language_server: &LanguageServer) {
|
||||
let language_server_name = language_server.name();
|
||||
if language_server_name == VUE_SERVER_NAME {
|
||||
let vue_server_id = language_server.server_id();
|
||||
language_server
|
||||
.on_notification::<VueServerRequest, _>({
|
||||
move |params, cx| {
|
||||
let lsp_store = lsp_store.clone();
|
||||
let Ok(Some(vue_server)) = lsp_store.read_with(cx, |this, _| {
|
||||
this.language_server_for_id(vue_server_id)
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let requests = params;
|
||||
let target_server = match lsp_store.read_with(cx, |this, _| {
|
||||
let language_server_id = this
|
||||
.as_local()
|
||||
.and_then(|local| {
|
||||
local.language_server_ids.iter().find_map(|(seed, v)| {
|
||||
[VTSLS, TS_LS].contains(&seed.name).then_some(v.id)
|
||||
})
|
||||
})
|
||||
.context("Could not find language server")?;
|
||||
|
||||
this.language_server_for_id(language_server_id)
|
||||
.context("language server not found")
|
||||
}) {
|
||||
Ok(Ok(server)) => server,
|
||||
other => {
|
||||
log::warn!(
|
||||
"vue-language-server forwarding skipped: {other:?}. \
|
||||
Returning null tsserver responses"
|
||||
);
|
||||
if !requests.is_empty() {
|
||||
let null_responses = requests
|
||||
.into_iter()
|
||||
.map(|(id, _, _)| (id, Value::Null))
|
||||
.collect::<Vec<_>>();
|
||||
let _ = vue_server
|
||||
.notify::<TypescriptServerResponse>(null_responses);
|
||||
}
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let cx = cx.clone();
|
||||
for (request_id, command, payload) in requests.into_iter() {
|
||||
let target_server = target_server.clone();
|
||||
let vue_server = vue_server.clone();
|
||||
cx.background_spawn(async move {
|
||||
let response = target_server
|
||||
.request::<lsp::request::ExecuteCommand>(
|
||||
lsp::ExecuteCommandParams {
|
||||
command: "typescript.tsserverRequest".to_owned(),
|
||||
arguments: vec![Value::String(command), payload],
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
let response_body = match response {
|
||||
util::ConnectionResult::Result(Ok(result)) => match result {
|
||||
Some(Value::Object(mut map)) => map
|
||||
.remove("body")
|
||||
.unwrap_or(Value::Object(map)),
|
||||
Some(other) => other,
|
||||
None => Value::Null,
|
||||
},
|
||||
util::ConnectionResult::Result(Err(error)) => {
|
||||
log::warn!(
|
||||
"typescript.tsserverRequest failed: {error:?} for request {request_id}"
|
||||
);
|
||||
Value::Null
|
||||
}
|
||||
other => {
|
||||
log::warn!(
|
||||
"typescript.tsserverRequest did not return a response: {other:?} for request {request_id}"
|
||||
);
|
||||
Value::Null
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(err) = vue_server
|
||||
.notify::<TypescriptServerResponse>(vec![(request_id, response_body)])
|
||||
{
|
||||
log::warn!(
|
||||
"Failed to notify vue-language-server of tsserver response: {err:?}"
|
||||
);
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ use std::{
|
||||
sync::{Arc, atomic::AtomicUsize},
|
||||
};
|
||||
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use anyhow::{Context as _, Result, anyhow, bail};
|
||||
use collections::{HashMap, HashSet};
|
||||
use fs::{Fs, copy_recursive};
|
||||
use futures::{
|
||||
@@ -1194,6 +1194,16 @@ impl WorktreeStore {
|
||||
RelPath::from_proto(&envelope.payload.new_path)?,
|
||||
);
|
||||
let (scan_id, entry) = this.update(&mut cx, |this, cx| {
|
||||
let Some((_, project_id)) = this.downstream_client else {
|
||||
bail!("no downstream client")
|
||||
};
|
||||
let Some(entry) = this.entry_for_id(entry_id, cx) else {
|
||||
bail!("no such entry");
|
||||
};
|
||||
if entry.is_private && project_id != REMOTE_SERVER_PROJECT_ID {
|
||||
bail!("entry is private")
|
||||
}
|
||||
|
||||
let new_worktree = this
|
||||
.worktree_for_id(new_worktree_id, cx)
|
||||
.context("no such worktree")?;
|
||||
@@ -1217,6 +1227,15 @@ impl WorktreeStore {
|
||||
) -> Result<proto::ProjectEntryResponse> {
|
||||
let entry_id = ProjectEntryId::from_proto(envelope.payload.entry_id);
|
||||
let worktree = this.update(&mut cx, |this, cx| {
|
||||
let Some((_, project_id)) = this.downstream_client else {
|
||||
bail!("no downstream client")
|
||||
};
|
||||
let Some(entry) = this.entry_for_id(entry_id, cx) else {
|
||||
bail!("no entry")
|
||||
};
|
||||
if entry.is_private && project_id != REMOTE_SERVER_PROJECT_ID {
|
||||
bail!("entry is private")
|
||||
}
|
||||
this.worktree_for_entry(entry_id, cx)
|
||||
.context("worktree not found")
|
||||
})??;
|
||||
@@ -1237,6 +1256,18 @@ impl WorktreeStore {
|
||||
let worktree = this
|
||||
.worktree_for_entry(entry_id, cx)
|
||||
.context("no such worktree")?;
|
||||
|
||||
let Some((_, project_id)) = this.downstream_client else {
|
||||
bail!("no downstream client")
|
||||
};
|
||||
let entry = worktree
|
||||
.read(cx)
|
||||
.entry_for_id(entry_id)
|
||||
.ok_or_else(|| anyhow!("missing entry"))?;
|
||||
if entry.is_private && project_id != REMOTE_SERVER_PROJECT_ID {
|
||||
bail!("entry is private")
|
||||
}
|
||||
|
||||
let scan_id = worktree.read(cx).scan_id();
|
||||
anyhow::Ok((
|
||||
scan_id,
|
||||
|
||||
@@ -64,7 +64,7 @@ use workspace::{
|
||||
DraggedSelection, OpenInTerminal, OpenOptions, OpenVisible, PreviewTabsSettings, SelectedEntry,
|
||||
SplitDirection, Workspace,
|
||||
dock::{DockPosition, Panel, PanelEvent},
|
||||
notifications::{DetachAndPromptErr, NotifyTaskExt},
|
||||
notifications::{DetachAndPromptErr, NotifyResultExt, NotifyTaskExt},
|
||||
};
|
||||
use worktree::CreatedEntry;
|
||||
use zed_actions::workspace::OpenWithSystem;
|
||||
@@ -2677,12 +2677,14 @@ impl ProjectPanel {
|
||||
for task in paste_tasks {
|
||||
match task {
|
||||
PasteTask::Rename(task) => {
|
||||
if let Some(CreatedEntry::Included(entry)) = task.await.log_err() {
|
||||
if let Some(CreatedEntry::Included(entry)) =
|
||||
task.await.notify_async_err(cx)
|
||||
{
|
||||
last_succeed = Some(entry);
|
||||
}
|
||||
}
|
||||
PasteTask::Copy(task) => {
|
||||
if let Some(Some(entry)) = task.await.log_err() {
|
||||
if let Some(Some(entry)) = task.await.notify_async_err(cx) {
|
||||
last_succeed = Some(entry);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1385,14 +1385,21 @@ impl RemoteServerProjects {
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.update_settings_file(cx, move |setting, _| {
|
||||
setting
|
||||
.wsl_connections
|
||||
.get_or_insert(Default::default())
|
||||
.push(settings::WslConnection {
|
||||
distro_name: SharedString::from(connection_options.distro_name),
|
||||
user: connection_options.user,
|
||||
let connections = setting.wsl_connections.get_or_insert(Default::default());
|
||||
|
||||
let distro_name = SharedString::from(connection_options.distro_name);
|
||||
let user = connection_options.user;
|
||||
|
||||
if !connections
|
||||
.iter()
|
||||
.any(|conn| conn.distro_name == distro_name && conn.user == user)
|
||||
{
|
||||
connections.push(settings::WslConnection {
|
||||
distro_name,
|
||||
user,
|
||||
projects: BTreeSet::new(),
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -836,16 +836,14 @@ impl RemoteClient {
|
||||
connection.build_command(program, args, env, working_dir, port_forward)
|
||||
}
|
||||
|
||||
pub fn build_forward_port_command(
|
||||
pub fn build_forward_ports_command(
|
||||
&self,
|
||||
local_port: u16,
|
||||
host: String,
|
||||
remote_port: u16,
|
||||
forwards: Vec<(u16, String, u16)>,
|
||||
) -> Result<CommandTemplate> {
|
||||
let Some(connection) = self.remote_connection() else {
|
||||
return Err(anyhow!("no ssh connection"));
|
||||
};
|
||||
connection.build_forward_port_command(local_port, host, remote_port)
|
||||
connection.build_forward_ports_command(forwards)
|
||||
}
|
||||
|
||||
pub fn upload_directory(
|
||||
@@ -1116,11 +1114,9 @@ pub(crate) trait RemoteConnection: Send + Sync {
|
||||
working_dir: Option<String>,
|
||||
port_forward: Option<(u16, String, u16)>,
|
||||
) -> Result<CommandTemplate>;
|
||||
fn build_forward_port_command(
|
||||
fn build_forward_ports_command(
|
||||
&self,
|
||||
local_port: u16,
|
||||
remote: String,
|
||||
remote_port: u16,
|
||||
forwards: Vec<(u16, String, u16)>,
|
||||
) -> Result<CommandTemplate>;
|
||||
fn connection_options(&self) -> RemoteConnectionOptions;
|
||||
fn path_style(&self) -> PathStyle;
|
||||
@@ -1551,19 +1547,17 @@ mod fake {
|
||||
})
|
||||
}
|
||||
|
||||
fn build_forward_port_command(
|
||||
fn build_forward_ports_command(
|
||||
&self,
|
||||
local_port: u16,
|
||||
host: String,
|
||||
remote_port: u16,
|
||||
forwards: Vec<(u16, String, u16)>,
|
||||
) -> anyhow::Result<CommandTemplate> {
|
||||
Ok(CommandTemplate {
|
||||
program: "ssh".into(),
|
||||
args: vec![
|
||||
"-N".into(),
|
||||
"-L".into(),
|
||||
format!("{local_port}:{host}:{remote_port}"),
|
||||
],
|
||||
args: std::iter::once("-N".to_owned())
|
||||
.chain(forwards.into_iter().map(|(local_port, host, remote_port)| {
|
||||
format!("{local_port}:{host}:{remote_port}")
|
||||
}))
|
||||
.collect(),
|
||||
env: Default::default(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -146,19 +146,20 @@ impl RemoteConnection for SshRemoteConnection {
|
||||
)
|
||||
}
|
||||
|
||||
fn build_forward_port_command(
|
||||
fn build_forward_ports_command(
|
||||
&self,
|
||||
local_port: u16,
|
||||
host: String,
|
||||
remote_port: u16,
|
||||
forwards: Vec<(u16, String, u16)>,
|
||||
) -> Result<CommandTemplate> {
|
||||
let Self { socket, .. } = self;
|
||||
let mut args = socket.ssh_args();
|
||||
args.push("-N".into());
|
||||
for (local_port, host, remote_port) in forwards {
|
||||
args.push("-L".into());
|
||||
args.push(format!("{local_port}:{host}:{remote_port}"));
|
||||
}
|
||||
Ok(CommandTemplate {
|
||||
program: "ssh".into(),
|
||||
args: vec![
|
||||
"-N".into(),
|
||||
"-L".into(),
|
||||
format!("{local_port}:{host}:{remote_port}"),
|
||||
],
|
||||
args,
|
||||
env: Default::default(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ impl WslRemoteConnection {
|
||||
this.can_exec = this.detect_can_exec(shell).await?;
|
||||
this.platform = this.detect_platform(shell).await?;
|
||||
this.remote_binary_path = Some(
|
||||
this.ensure_server_binary(&delegate, release_channel, version, commit, cx)
|
||||
this.ensure_server_binary(&delegate, release_channel, version, commit, shell, cx)
|
||||
.await?,
|
||||
);
|
||||
log::debug!("Detected WSL environment: {this:#?}");
|
||||
@@ -163,6 +163,7 @@ impl WslRemoteConnection {
|
||||
release_channel: ReleaseChannel,
|
||||
version: SemanticVersion,
|
||||
commit: Option<AppCommitSha>,
|
||||
shell: ShellKind,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<Arc<RelPath>> {
|
||||
let version_str = match release_channel {
|
||||
@@ -184,9 +185,13 @@ impl WslRemoteConnection {
|
||||
paths::remote_wsl_server_dir_relative().join(RelPath::unix(&binary_name).unwrap());
|
||||
|
||||
if let Some(parent) = dst_path.parent() {
|
||||
self.run_wsl_command("mkdir", &["-p", &parent.display(PathStyle::Posix)])
|
||||
.await
|
||||
.map_err(|e| anyhow!("Failed to create directory: {}", e))?;
|
||||
let parent = parent.display(PathStyle::Posix);
|
||||
if shell == ShellKind::Nushell {
|
||||
self.run_wsl_command("mkdir", &[&parent]).await
|
||||
} else {
|
||||
self.run_wsl_command("mkdir", &["-p", &parent]).await
|
||||
}
|
||||
.map_err(|e| anyhow!("Failed to create directory: {}", e))?;
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
@@ -485,11 +490,9 @@ impl RemoteConnection for WslRemoteConnection {
|
||||
})
|
||||
}
|
||||
|
||||
fn build_forward_port_command(
|
||||
fn build_forward_ports_command(
|
||||
&self,
|
||||
_: u16,
|
||||
_: String,
|
||||
_: u16,
|
||||
_: Vec<(u16, String, u16)>,
|
||||
) -> anyhow::Result<CommandTemplate> {
|
||||
Err(anyhow!("WSL shares a network interface with the host"))
|
||||
}
|
||||
|
||||
@@ -103,7 +103,9 @@ fn init_logging_server(log_file_path: PathBuf) -> Result<Receiver<Vec<u8>>> {
|
||||
buffer: Vec::new(),
|
||||
});
|
||||
|
||||
env_logger::Builder::from_default_env()
|
||||
env_logger::Builder::new()
|
||||
.filter_level(log::LevelFilter::Info)
|
||||
.parse_default_env()
|
||||
.target(env_logger::Target::Pipe(target))
|
||||
.format(|buf, record| {
|
||||
let mut log_record = LogRecord::new(record);
|
||||
|
||||
@@ -318,6 +318,11 @@ pub struct LanguageSettingsContent {
|
||||
///
|
||||
/// Default: true
|
||||
pub use_on_type_format: Option<bool>,
|
||||
/// Which code actions to run on save after the formatter.
|
||||
/// These are not run if formatting is off.
|
||||
///
|
||||
/// Default: {} (or {"source.organizeImports": true} for Go).
|
||||
pub code_actions_on_format: Option<HashMap<String, bool>>,
|
||||
/// Whether to perform linked edits of associated ranges, if the language server supports it.
|
||||
/// For example, when editing opening <html> tag, the contents of the closing </html> tag will be edited as well.
|
||||
///
|
||||
|
||||
@@ -254,9 +254,9 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
|
||||
),
|
||||
metadata: None,
|
||||
}),
|
||||
SettingsPageItem::SectionHeader("Fonts"),
|
||||
SettingsPageItem::SectionHeader("Buffer Font"),
|
||||
SettingsPageItem::SettingItem(SettingItem {
|
||||
title: "Buffer Font Family",
|
||||
title: "Font Family",
|
||||
description: "Font family for editor text",
|
||||
field: Box::new(SettingField {
|
||||
pick: |settings_content| &settings_content.theme.buffer_font_family,
|
||||
@@ -266,7 +266,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
|
||||
files: USER,
|
||||
}),
|
||||
SettingsPageItem::SettingItem(SettingItem {
|
||||
title: "Buffer Font Size",
|
||||
title: "Font Size",
|
||||
description: "Font size for editor text",
|
||||
field: Box::new(SettingField {
|
||||
pick: |settings_content| &settings_content.theme.buffer_font_size,
|
||||
@@ -276,7 +276,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
|
||||
files: USER,
|
||||
}),
|
||||
SettingsPageItem::SettingItem(SettingItem {
|
||||
title: "Buffer Font Weight",
|
||||
title: "Font Weight",
|
||||
description: "Font weight for editor text (100-900)",
|
||||
field: Box::new(SettingField {
|
||||
pick: |settings_content| &settings_content.theme.buffer_font_weight,
|
||||
@@ -288,7 +288,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
|
||||
// todo(settings_ui): This needs custom ui
|
||||
SettingsPageItem::SettingItem(SettingItem {
|
||||
files: USER,
|
||||
title: "Buffer Line Height",
|
||||
title: "Line Height",
|
||||
description: "Line height for editor text",
|
||||
field: Box::new(
|
||||
SettingField {
|
||||
@@ -303,7 +303,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
|
||||
}),
|
||||
SettingsPageItem::SettingItem(SettingItem {
|
||||
files: USER,
|
||||
title: "Buffer Font Features",
|
||||
title: "Font Features",
|
||||
description: "The OpenType features to enable for rendering in text buffers.",
|
||||
field: Box::new(
|
||||
SettingField {
|
||||
@@ -318,7 +318,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
|
||||
}),
|
||||
SettingsPageItem::SettingItem(SettingItem {
|
||||
files: USER,
|
||||
title: "Buffer Font Fallbacks",
|
||||
title: "Font Fallbacks",
|
||||
description: "The font fallbacks to use for rendering in text buffers.",
|
||||
field: Box::new(
|
||||
SettingField {
|
||||
@@ -331,8 +331,9 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
|
||||
),
|
||||
metadata: None,
|
||||
}),
|
||||
SettingsPageItem::SectionHeader("UI Font"),
|
||||
SettingsPageItem::SettingItem(SettingItem {
|
||||
title: "UI Font Family",
|
||||
title: "Font Family",
|
||||
description: "Font family for UI elements",
|
||||
field: Box::new(SettingField {
|
||||
pick: |settings_content| &settings_content.theme.ui_font_family,
|
||||
@@ -342,7 +343,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
|
||||
files: USER,
|
||||
}),
|
||||
SettingsPageItem::SettingItem(SettingItem {
|
||||
title: "UI Font Size",
|
||||
title: "Font Size",
|
||||
description: "Font size for UI elements",
|
||||
field: Box::new(SettingField {
|
||||
pick: |settings_content| &settings_content.theme.ui_font_size,
|
||||
@@ -352,7 +353,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
|
||||
files: USER,
|
||||
}),
|
||||
SettingsPageItem::SettingItem(SettingItem {
|
||||
title: "UI Font Weight",
|
||||
title: "Font Weight",
|
||||
description: "Font weight for UI elements (100-900)",
|
||||
field: Box::new(SettingField {
|
||||
pick: |settings_content| &settings_content.theme.ui_font_weight,
|
||||
@@ -363,7 +364,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
|
||||
}),
|
||||
SettingsPageItem::SettingItem(SettingItem {
|
||||
files: USER,
|
||||
title: "UI Font Features",
|
||||
title: "Font Features",
|
||||
description: "The OpenType features to enable for rendering in UI elements.",
|
||||
field: Box::new(
|
||||
SettingField {
|
||||
@@ -378,7 +379,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
|
||||
}),
|
||||
SettingsPageItem::SettingItem(SettingItem {
|
||||
files: USER,
|
||||
title: "UI Font Fallbacks",
|
||||
title: "Font Fallbacks",
|
||||
description: "The font fallbacks to use for rendering in the UI.",
|
||||
field: Box::new(
|
||||
SettingField {
|
||||
@@ -391,8 +392,9 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
|
||||
),
|
||||
metadata: None,
|
||||
}),
|
||||
SettingsPageItem::SectionHeader("Agent Panel Font"),
|
||||
SettingsPageItem::SettingItem(SettingItem {
|
||||
title: "Agent Panel UI Font Size",
|
||||
title: "UI Font Size",
|
||||
description: "Font size for agent response text in the agent panel. Falls back to the regular UI font size.",
|
||||
field: Box::new(SettingField {
|
||||
pick: |settings_content| {
|
||||
@@ -408,7 +410,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
|
||||
files: USER,
|
||||
}),
|
||||
SettingsPageItem::SettingItem(SettingItem {
|
||||
title: "Agent Panel Buffer Font Size",
|
||||
title: "Buffer Font Size",
|
||||
description: "Font size for user messages text in the agent panel",
|
||||
field: Box::new(SettingField {
|
||||
pick: |settings_content| &settings_content.theme.agent_buffer_font_size,
|
||||
@@ -419,6 +421,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
|
||||
metadata: None,
|
||||
files: USER,
|
||||
}),
|
||||
SettingsPageItem::SectionHeader("Cursor"),
|
||||
SettingsPageItem::SettingItem(SettingItem {
|
||||
title: "Multi Cursor Modifier",
|
||||
description: "Modifier key for adding multiple cursors",
|
||||
@@ -431,7 +434,6 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
|
||||
metadata: None,
|
||||
files: USER,
|
||||
}),
|
||||
SettingsPageItem::SectionHeader("Cursor"),
|
||||
SettingsPageItem::SettingItem(SettingItem {
|
||||
title: "Cursor Blink",
|
||||
description: "Whether the cursor blinks in the editor",
|
||||
@@ -808,9 +810,9 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
|
||||
metadata: None,
|
||||
files: USER,
|
||||
}),
|
||||
SettingsPageItem::SectionHeader("Hover"),
|
||||
SettingsPageItem::SectionHeader("Hover Popover"),
|
||||
SettingsPageItem::SettingItem(SettingItem {
|
||||
title: "Hover Popover Enabled",
|
||||
title: "Enabled",
|
||||
description: "Show the informational hover box when moving the mouse over symbols in the editor",
|
||||
field: Box::new(SettingField {
|
||||
pick: |settings_content| &settings_content.editor.hover_popover_enabled,
|
||||
@@ -823,7 +825,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
|
||||
}),
|
||||
// todo(settings ui): add units to this number input
|
||||
SettingsPageItem::SettingItem(SettingItem {
|
||||
title: "Hover Popover Delay",
|
||||
title: "Delay",
|
||||
description: "Time to wait in milliseconds before showing the informational hover box",
|
||||
field: Box::new(SettingField {
|
||||
pick: |settings_content| &settings_content.editor.hover_popover_delay,
|
||||
@@ -834,21 +836,9 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
|
||||
metadata: None,
|
||||
files: USER,
|
||||
}),
|
||||
SettingsPageItem::SectionHeader("Code Actions & Selection"),
|
||||
SettingsPageItem::SectionHeader("Drag And Drop Selection"),
|
||||
SettingsPageItem::SettingItem(SettingItem {
|
||||
title: "Inline Code Actions",
|
||||
description: "Show code action button at start of buffer line",
|
||||
field: Box::new(SettingField {
|
||||
pick: |settings_content| &settings_content.editor.inline_code_actions,
|
||||
pick_mut: |settings_content| {
|
||||
&mut settings_content.editor.inline_code_actions
|
||||
},
|
||||
}),
|
||||
metadata: None,
|
||||
files: USER,
|
||||
}),
|
||||
SettingsPageItem::SettingItem(SettingItem {
|
||||
title: "Drag And Drop Selection",
|
||||
title: "Enabled",
|
||||
description: "Enable drag and drop selection",
|
||||
field: Box::new(SettingField {
|
||||
pick: |settings_content| {
|
||||
@@ -872,7 +862,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
|
||||
files: USER,
|
||||
}),
|
||||
SettingsPageItem::SettingItem(SettingItem {
|
||||
title: "Drag And Drop Selection Delay",
|
||||
title: "Delay",
|
||||
description: "Delay in milliseconds before drag and drop selection starts",
|
||||
field: Box::new(SettingField {
|
||||
pick: |settings_content| {
|
||||
@@ -1014,6 +1004,18 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
|
||||
metadata: None,
|
||||
files: USER,
|
||||
}),
|
||||
SettingsPageItem::SettingItem(SettingItem {
|
||||
title: "Inline Code Actions",
|
||||
description: "Show code action button at start of buffer line",
|
||||
field: Box::new(SettingField {
|
||||
pick: |settings_content| &settings_content.editor.inline_code_actions,
|
||||
pick_mut: |settings_content| {
|
||||
&mut settings_content.editor.inline_code_actions
|
||||
},
|
||||
}),
|
||||
metadata: None,
|
||||
files: USER,
|
||||
}),
|
||||
SettingsPageItem::SectionHeader("Scrollbar"),
|
||||
SettingsPageItem::SettingItem(SettingItem {
|
||||
title: "Show",
|
||||
@@ -5414,6 +5416,27 @@ fn language_settings_data() -> Vec<SettingsPageItem> {
|
||||
metadata: None,
|
||||
files: USER | LOCAL,
|
||||
}),
|
||||
SettingsPageItem::SettingItem(SettingItem {
|
||||
title: "Code Actions On Format",
|
||||
description: "Additional Code Actions To Run When Formatting",
|
||||
field: Box::new(
|
||||
SettingField {
|
||||
pick: |settings_content| {
|
||||
language_settings_field(settings_content, |language| {
|
||||
&language.code_actions_on_format
|
||||
})
|
||||
},
|
||||
pick_mut: |settings_content| {
|
||||
language_settings_field_mut(settings_content, |language| {
|
||||
&mut language.code_actions_on_format
|
||||
})
|
||||
},
|
||||
}
|
||||
.unimplemented(),
|
||||
),
|
||||
metadata: None,
|
||||
files: USER | LOCAL,
|
||||
}),
|
||||
SettingsPageItem::SectionHeader("Autoclose"),
|
||||
SettingsPageItem::SettingItem(SettingItem {
|
||||
title: "Use Autoclose",
|
||||
|
||||
@@ -15,7 +15,7 @@ use heck::ToTitleCase as _;
|
||||
use project::WorktreeId;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use settings::{SettingsContent, SettingsStore};
|
||||
use settings::{Settings, SettingsContent, SettingsStore};
|
||||
use std::{
|
||||
any::{Any, TypeId, type_name},
|
||||
cell::RefCell,
|
||||
@@ -466,6 +466,13 @@ pub fn open_settings_editor(
|
||||
// We have to defer this to get the workspace off the stack.
|
||||
|
||||
cx.defer(move |cx| {
|
||||
let current_rem_size: f32 = theme::ThemeSettings::get_global(cx).ui_font_size(cx).into();
|
||||
|
||||
let default_bounds = size(px(900.), px(750.)); // 4:3 Aspect Ratio
|
||||
let default_rem_size = 16.0;
|
||||
let scale_factor = current_rem_size / default_rem_size;
|
||||
let scaled_bounds: gpui::Size<Pixels> = default_bounds.map(|axis| axis * scale_factor);
|
||||
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
titlebar: Some(TitlebarOptions {
|
||||
@@ -478,8 +485,8 @@ pub fn open_settings_editor(
|
||||
is_movable: true,
|
||||
kind: gpui::WindowKind::Floating,
|
||||
window_background: cx.theme().window_background_appearance(),
|
||||
window_min_size: Some(size(px(900.), px(750.))), // 4:3 Aspect Ratio
|
||||
window_bounds: Some(WindowBounds::centered(size(px(900.), px(750.)), cx)),
|
||||
window_min_size: Some(scaled_bounds),
|
||||
window_bounds: Some(WindowBounds::centered(scaled_bounds, cx)),
|
||||
..Default::default()
|
||||
},
|
||||
|window, cx| cx.new(|cx| SettingsWindow::new(Some(workspace_handle), window, cx)),
|
||||
@@ -1695,7 +1702,7 @@ impl SettingsWindow {
|
||||
};
|
||||
|
||||
v_flex()
|
||||
.w_64()
|
||||
.w_56()
|
||||
.p_2p5()
|
||||
.when(cfg!(target_os = "macos"), |c| c.pt_10())
|
||||
.h_full()
|
||||
@@ -2130,7 +2137,7 @@ impl SettingsWindow {
|
||||
}
|
||||
|
||||
return v_flex()
|
||||
.size_full()
|
||||
.flex_1()
|
||||
.pt_6()
|
||||
.pb_8()
|
||||
.px_8()
|
||||
|
||||
@@ -97,6 +97,7 @@ impl Render for PlatformTitleBar {
|
||||
})
|
||||
// this border is to avoid a transparent gap in the rounded corners
|
||||
.mt(px(-1.))
|
||||
.mb(px(-1.))
|
||||
.border(px(1.))
|
||||
.border_color(titlebar_color),
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use gpui::{Corner, Entity, Pixels, Point};
|
||||
|
||||
use crate::{ContextMenu, PopoverMenu, prelude::*};
|
||||
use crate::{ButtonLike, ContextMenu, PopoverMenu, prelude::*};
|
||||
|
||||
use super::PopoverMenuHandle;
|
||||
|
||||
@@ -137,36 +137,52 @@ impl RenderOnce for DropdownMenu {
|
||||
let full_width = self.full_width;
|
||||
let trigger_size = self.trigger_size;
|
||||
|
||||
let button = match self.label {
|
||||
LabelKind::Text(text) => Button::new(self.id.clone(), text)
|
||||
.style(button_style)
|
||||
.when(self.chevron, |this| {
|
||||
this.icon(IconName::ChevronUpDown)
|
||||
.icon_position(IconPosition::End)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Muted)
|
||||
})
|
||||
.when(full_width, |this| this.full_width())
|
||||
.size(trigger_size)
|
||||
.disabled(self.disabled),
|
||||
LabelKind::Element(_element) => Button::new(self.id.clone(), "")
|
||||
.style(button_style)
|
||||
.when(self.chevron, |this| {
|
||||
this.icon(IconName::ChevronUpDown)
|
||||
.icon_position(IconPosition::End)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Muted)
|
||||
})
|
||||
.when(full_width, |this| this.full_width())
|
||||
.size(trigger_size)
|
||||
.disabled(self.disabled),
|
||||
}
|
||||
.when_some(self.tab_index, |this, tab_index| this.tab_index(tab_index));
|
||||
let (text_button, element_button) = match self.label {
|
||||
LabelKind::Text(text) => (
|
||||
Some(
|
||||
Button::new(self.id.clone(), text)
|
||||
.style(button_style)
|
||||
.when(self.chevron, |this| {
|
||||
this.icon(IconName::ChevronUpDown)
|
||||
.icon_position(IconPosition::End)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Muted)
|
||||
})
|
||||
.when(full_width, |this| this.full_width())
|
||||
.size(trigger_size)
|
||||
.disabled(self.disabled)
|
||||
.when_some(self.tab_index, |this, tab_index| this.tab_index(tab_index)),
|
||||
),
|
||||
None,
|
||||
),
|
||||
LabelKind::Element(element) => (
|
||||
None,
|
||||
Some(
|
||||
ButtonLike::new(self.id.clone())
|
||||
.child(element)
|
||||
.style(button_style)
|
||||
.when(self.chevron, |this| {
|
||||
this.child(
|
||||
Icon::new(IconName::ChevronUpDown)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
})
|
||||
.when(full_width, |this| this.full_width())
|
||||
.size(trigger_size)
|
||||
.disabled(self.disabled)
|
||||
.when_some(self.tab_index, |this, tab_index| this.tab_index(tab_index)),
|
||||
),
|
||||
),
|
||||
};
|
||||
|
||||
PopoverMenu::new((self.id.clone(), "popover"))
|
||||
.full_width(self.full_width)
|
||||
.menu(move |_window, _cx| Some(self.menu.clone()))
|
||||
.trigger(button)
|
||||
.when_some(text_button, |this, text_button| this.trigger(text_button))
|
||||
.when_some(element_button, |this, element_button| {
|
||||
this.trigger(element_button)
|
||||
})
|
||||
.attach(match self.attach {
|
||||
Some(attach) => attach,
|
||||
None => Corner::BottomRight,
|
||||
|
||||
@@ -15,9 +15,16 @@ impl HighlightedLabel {
|
||||
/// Constructs a label with the given characters highlighted.
|
||||
/// Characters are identified by UTF-8 byte position.
|
||||
pub fn new(label: impl Into<SharedString>, highlight_indices: Vec<usize>) -> Self {
|
||||
let label = label.into();
|
||||
for &run in &highlight_indices {
|
||||
assert!(
|
||||
label.is_char_boundary(run),
|
||||
"highlight index {run} is not a valid UTF-8 boundary"
|
||||
);
|
||||
}
|
||||
Self {
|
||||
base: LabelLike::new(),
|
||||
label: label.into(),
|
||||
label,
|
||||
highlight_indices,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -369,6 +369,8 @@ impl Vim {
|
||||
|
||||
let mut selections = Vec::new();
|
||||
let mut row = tail.row();
|
||||
let going_up = tail.row() > head.row();
|
||||
let direction = if going_up { -1 } else { 1 };
|
||||
|
||||
loop {
|
||||
let laid_out_line = map.layout_row(row, &text_layout_details);
|
||||
@@ -399,13 +401,18 @@ impl Vim {
|
||||
|
||||
selections.push(selection);
|
||||
}
|
||||
if row == head.row() {
|
||||
|
||||
// When dealing with soft wrapped lines, it's possible that
|
||||
// `row` ends up being set to a value other than `head.row()` as
|
||||
// `head.row()` might be a `DisplayPoint` mapped to a soft
|
||||
// wrapped line, hence the need for `<=` and `>=` instead of
|
||||
// `==`.
|
||||
if going_up && row <= head.row() || !going_up && row >= head.row() {
|
||||
break;
|
||||
}
|
||||
|
||||
// Move to the next or previous buffer row, ensuring that
|
||||
// wrapped lines are handled correctly.
|
||||
let direction = if tail.row() > head.row() { -1 } else { 1 };
|
||||
// Find the next or previous buffer row where the `row` should
|
||||
// be moved to, so that wrapped lines are skipped.
|
||||
row = start_of_relative_buffer_row(map, DisplayPoint::new(row, 0), direction).row();
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
description = "The fast, collaborative code editor."
|
||||
edition.workspace = true
|
||||
name = "zed"
|
||||
version = "0.209.0"
|
||||
version = "0.209.7"
|
||||
publish.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
authors = ["Zed Team <hi@zed.dev>"]
|
||||
|
||||
@@ -1 +1 @@
|
||||
dev
|
||||
stable
|
||||
@@ -49,8 +49,8 @@ You can configure Zed to format code using `eslint --fix` by running the ESLint
|
||||
{
|
||||
"languages": {
|
||||
"JavaScript": {
|
||||
"formatter": {
|
||||
"code_action": "source.fixAll.eslint"
|
||||
"code_actions_on_format": {
|
||||
"source.fixAll.eslint": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -63,8 +63,8 @@ You can also only execute a single ESLint rule when using `fixAll`:
|
||||
{
|
||||
"languages": {
|
||||
"JavaScript": {
|
||||
"formatter": {
|
||||
"code_action": "source.fixAll.eslint"
|
||||
"code_actions_on_format": {
|
||||
"source.fixAll.eslint": true
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -92,8 +92,8 @@ the formatter:
|
||||
{
|
||||
"languages": {
|
||||
"JavaScript": {
|
||||
"formatter": {
|
||||
"code_action": "source.fixAll.eslint"
|
||||
"code_actions_on_format": {
|
||||
"source.fixAll.eslint": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user