Compare commits
13 commits
main
...
remote-lsp
Author | SHA1 | Date | |
---|---|---|---|
![]() |
03ba256d96 | ||
![]() |
679c24282d | ||
![]() |
e75e30b25f | ||
![]() |
d433406103 | ||
![]() |
775160a199 | ||
![]() |
cc680d280d | ||
![]() |
25ed82c994 | ||
![]() |
9d1e2f5278 | ||
![]() |
dc6377b8bc | ||
![]() |
13c8d4e052 | ||
![]() |
1cc491a919 | ||
![]() |
ab5da3af83 | ||
![]() |
848d1101d3 |
91 changed files with 3190 additions and 6400 deletions
2
.github/ISSUE_TEMPLATE/10_bug_report.yml
vendored
2
.github/ISSUE_TEMPLATE/10_bug_report.yml
vendored
|
@ -14,7 +14,7 @@ body:
|
|||
### Description
|
||||
<!-- Describe with sufficient detail to reproduce from a clean Zed install.
|
||||
- Any code must be sufficient to reproduce (include context!)
|
||||
- Include code as text, not just as a screenshot.
|
||||
- Code must as text, not just as a screenshot.
|
||||
- Issues with insufficient detail may be summarily closed.
|
||||
-->
|
||||
|
||||
|
|
3
Cargo.lock
generated
3
Cargo.lock
generated
|
@ -9211,8 +9211,10 @@ dependencies = [
|
|||
"gpui",
|
||||
"itertools 0.14.0",
|
||||
"language",
|
||||
"log",
|
||||
"lsp",
|
||||
"project",
|
||||
"proto",
|
||||
"release_channel",
|
||||
"serde_json",
|
||||
"settings",
|
||||
|
@ -13500,6 +13502,7 @@ dependencies = [
|
|||
"language",
|
||||
"language_extension",
|
||||
"language_model",
|
||||
"language_tools",
|
||||
"languages",
|
||||
"libc",
|
||||
"log",
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8 12.375H13" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M3 11.125L6.75003 7.375L3 3.62497" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 336 B |
File diff suppressed because it is too large
Load diff
Before Width: | Height: | Size: 176 KiB |
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="160" height="61" fill="none"><g clip-path="url(#a)"><path fill="#000" d="M130.75.385c5.428 0 10.297 2.81 13.011 7.511l14.214 24.618-.013-.005c2.599 4.504 2.707 9.932.28 14.513-2.618 4.944-7.862 8.015-13.679 8.015h-31.811c-.452 0-.873-.242-1.103-.637a1.268 1.268 0 0 1 0-1.274l3.919-6.78c.223-.394.65-.636 1.102-.636h28.288a5.622 5.622 0 0 0 4.925-2.849 5.615 5.615 0 0 0 0-5.69l-14.214-24.617a5.621 5.621 0 0 0-4.925-2.848 5.621 5.621 0 0 0-4.925 2.848l-14.214 24.618a6.267 6.267 0 0 0-.319.643.998.998 0 0 1-.069.14L101.724 54.4l-.823 1.313-2.529 4.39a1.27 1.27 0 0 1-1.103.636h-7.83c-.452 0-.873-.242-1.102-.637-.23-.394-.23-.879 0-1.274l2.188-3.791H66.803c-3.32 0-6.454-1.122-8.818-3.167a17.141 17.141 0 0 1-3.394-3.96 1.261 1.261 0 0 1-.091-.137L34.2 12.573a5.622 5.622 0 0 0-4.925-2.849 5.621 5.621 0 0 0-4.924 2.85L10.137 37.19a5.615 5.615 0 0 0 0 5.69 5.63 5.63 0 0 0 4.925 2.841h29.862a1.276 1.276 0 0 1 1.102 1.912l-3.912 6.778a1.27 1.27 0 0 1-1.102.638H14.495c-3.32 0-6.454-1.128-8.817-3.173-5.906-5.104-7.36-12.883-3.62-19.363L16.267 7.89C18.872 3.385 23.517.583 28.697.39c.184-.006.356-.006.534-.006 5.378 0 10.45 3.007 13.246 7.85l12.986 22.372L68.58 7.891C71.186 3.385 75.83.582 81.01.39c.185-.006.358-.006.536-.006 4.453 0 8.71 2.039 11.672 5.588.337.407.388.98.127 1.446l-3.765 6.6a1.268 1.268 0 0 1-2.205.006l-.847-1.465a5.623 5.623 0 0 0-4.926-2.848 5.622 5.622 0 0 0-4.924 2.848L62.464 37.18a5.614 5.614 0 0 0 0 5.689 5.628 5.628 0 0 0 4.925 2.842H95.91L117.76 7.87c2.714-4.683 7.575-7.486 12.99-7.486Z"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 .385h160v60.36H0z"/></clipPath></defs></svg>
|
Before Width: | Height: | Size: 1.6 KiB |
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 14 KiB |
|
@ -40,7 +40,7 @@
|
|||
"shift-f11": "debugger::StepOut",
|
||||
"f11": "zed::ToggleFullScreen",
|
||||
"ctrl-alt-z": "edit_prediction::RateCompletions",
|
||||
"ctrl-alt-shift-i": "edit_prediction::ToggleMenu",
|
||||
"ctrl-shift-i": "edit_prediction::ToggleMenu",
|
||||
"ctrl-alt-l": "lsp_tool::ToggleMenu"
|
||||
}
|
||||
},
|
||||
|
@ -120,7 +120,7 @@
|
|||
"alt-g m": "git::OpenModifiedFiles",
|
||||
"menu": "editor::OpenContextMenu",
|
||||
"shift-f10": "editor::OpenContextMenu",
|
||||
"ctrl-alt-shift-e": "editor::ToggleEditPrediction",
|
||||
"ctrl-shift-e": "editor::ToggleEditPrediction",
|
||||
"f9": "editor::ToggleBreakpoint",
|
||||
"shift-f9": "editor::EditLogBreakpoint"
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -38,7 +38,6 @@
|
|||
"alt-;": ["editor::ToggleComments", { "advance_downwards": false }],
|
||||
"ctrl-x ctrl-;": "editor::ToggleComments",
|
||||
"alt-.": "editor::GoToDefinition", // xref-find-definitions
|
||||
"alt-?": "editor::FindAllReferences", // xref-find-references
|
||||
"alt-,": "pane::GoBack", // xref-pop-marker-stack
|
||||
"ctrl-x h": "editor::SelectAll", // mark-whole-buffer
|
||||
"ctrl-d": "editor::Delete", // delete-char
|
||||
|
|
|
@ -38,7 +38,6 @@
|
|||
"alt-;": ["editor::ToggleComments", { "advance_downwards": false }],
|
||||
"ctrl-x ctrl-;": "editor::ToggleComments",
|
||||
"alt-.": "editor::GoToDefinition", // xref-find-definitions
|
||||
"alt-?": "editor::FindAllReferences", // xref-find-references
|
||||
"alt-,": "pane::GoBack", // xref-pop-marker-stack
|
||||
"ctrl-x h": "editor::SelectAll", // mark-whole-buffer
|
||||
"ctrl-d": "editor::Delete", // delete-char
|
||||
|
|
|
@ -653,8 +653,6 @@
|
|||
// "never"
|
||||
"show": "always"
|
||||
},
|
||||
// Whether to enable drag-and-drop operations in the project panel.
|
||||
"drag_and_drop": true,
|
||||
// Whether to hide the root entry when only one folder is open in the window.
|
||||
"hide_root": false
|
||||
},
|
||||
|
|
|
@ -43,8 +43,8 @@
|
|||
// "args": ["--login"]
|
||||
// }
|
||||
// }
|
||||
"shell": "system"
|
||||
"shell": "system",
|
||||
// Represents the tags for inline runnable indicators, or spawning multiple tasks at once.
|
||||
// "tags": []
|
||||
"tags": []
|
||||
}
|
||||
]
|
||||
|
|
|
@ -664,7 +664,7 @@ impl Thread {
|
|||
}
|
||||
|
||||
pub fn get_or_init_configured_model(&mut self, cx: &App) -> Option<ConfiguredModel> {
|
||||
if self.configured_model.is_none() {
|
||||
if self.configured_model.is_none() || self.messages.is_empty() {
|
||||
self.configured_model = LanguageModelRegistry::read_global(cx).default_model();
|
||||
}
|
||||
self.configured_model.clone()
|
||||
|
@ -2097,7 +2097,7 @@ impl Thread {
|
|||
}
|
||||
|
||||
pub fn summarize(&mut self, cx: &mut Context<Self>) {
|
||||
let Some(model) = LanguageModelRegistry::read_global(cx).thread_summary_model() else {
|
||||
let Some(model) = LanguageModelRegistry::read_global(cx).thread_summary_model(cx) else {
|
||||
println!("No thread summary model");
|
||||
return;
|
||||
};
|
||||
|
@ -2416,7 +2416,7 @@ impl Thread {
|
|||
}
|
||||
|
||||
let Some(ConfiguredModel { model, provider }) =
|
||||
LanguageModelRegistry::read_global(cx).thread_summary_model()
|
||||
LanguageModelRegistry::read_global(cx).thread_summary_model(cx)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
@ -5410,13 +5410,10 @@ fn main() {{
|
|||
}),
|
||||
cx,
|
||||
);
|
||||
registry.set_thread_summary_model(
|
||||
Some(ConfiguredModel {
|
||||
provider,
|
||||
model: model.clone(),
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
registry.set_thread_summary_model(Some(ConfiguredModel {
|
||||
provider,
|
||||
model: model.clone(),
|
||||
}));
|
||||
})
|
||||
});
|
||||
|
||||
|
|
|
@ -228,7 +228,7 @@ impl NativeAgent {
|
|||
) -> Entity<AcpThread> {
|
||||
let connection = Rc::new(NativeAgentConnection(cx.entity()));
|
||||
let registry = LanguageModelRegistry::read_global(cx);
|
||||
let summarization_model = registry.thread_summary_model().map(|c| c.model);
|
||||
let summarization_model = registry.thread_summary_model(cx).map(|c| c.model);
|
||||
|
||||
thread_handle.update(cx, |thread, cx| {
|
||||
thread.set_summarization_model(summarization_model, cx);
|
||||
|
@ -524,7 +524,7 @@ impl NativeAgent {
|
|||
|
||||
let registry = LanguageModelRegistry::read_global(cx);
|
||||
let default_model = registry.default_model().map(|m| m.model);
|
||||
let summarization_model = registry.thread_summary_model().map(|m| m.model);
|
||||
let summarization_model = registry.thread_summary_model(cx).map(|m| m.model);
|
||||
|
||||
for session in self.sessions.values_mut() {
|
||||
session.thread.update(cx, |thread, cx| {
|
||||
|
|
|
@ -472,7 +472,7 @@ async fn test_tool_authorization(cx: &mut TestAppContext) {
|
|||
tool_name: ToolRequiringPermission::name().into(),
|
||||
is_error: true,
|
||||
content: "Permission to run tool denied by user".into(),
|
||||
output: Some("Permission to run tool denied by user".into())
|
||||
output: None
|
||||
})
|
||||
]
|
||||
);
|
||||
|
@ -1822,11 +1822,11 @@ async fn test_agent_connection(cx: &mut TestAppContext) {
|
|||
let clock = Arc::new(clock::FakeSystemClock::new());
|
||||
let client = Client::new(clock, http_client, cx);
|
||||
let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
|
||||
Project::init_settings(cx);
|
||||
agent_settings::init(cx);
|
||||
language_model::init(client.clone(), cx);
|
||||
language_models::init(user_store, client.clone(), cx);
|
||||
Project::init_settings(cx);
|
||||
LanguageModelRegistry::test(cx);
|
||||
agent_settings::init(cx);
|
||||
});
|
||||
cx.executor().forbid_parking();
|
||||
|
||||
|
|
|
@ -732,17 +732,7 @@ impl Thread {
|
|||
stream.update_tool_call_fields(
|
||||
&tool_use.id,
|
||||
acp::ToolCallUpdateFields {
|
||||
status: Some(
|
||||
tool_result
|
||||
.as_ref()
|
||||
.map_or(acp::ToolCallStatus::Failed, |result| {
|
||||
if result.is_error {
|
||||
acp::ToolCallStatus::Failed
|
||||
} else {
|
||||
acp::ToolCallStatus::Completed
|
||||
}
|
||||
}),
|
||||
),
|
||||
status: Some(acp::ToolCallStatus::Completed),
|
||||
raw_output: output,
|
||||
..Default::default()
|
||||
},
|
||||
|
@ -1567,7 +1557,7 @@ impl Thread {
|
|||
tool_name: tool_use.name,
|
||||
is_error: true,
|
||||
content: LanguageModelToolResultContent::Text(Arc::from(error.to_string())),
|
||||
output: Some(error.to_string().into()),
|
||||
output: None,
|
||||
},
|
||||
}
|
||||
}))
|
||||
|
@ -2469,30 +2459,6 @@ impl ToolCallEventStreamReceiver {
|
|||
}
|
||||
}
|
||||
|
||||
pub async fn expect_update_fields(&mut self) -> acp::ToolCallUpdateFields {
|
||||
let event = self.0.next().await;
|
||||
if let Some(Ok(ThreadEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateFields(
|
||||
update,
|
||||
)))) = event
|
||||
{
|
||||
update.fields
|
||||
} else {
|
||||
panic!("Expected update fields but got: {:?}", event);
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn expect_diff(&mut self) -> Entity<acp_thread::Diff> {
|
||||
let event = self.0.next().await;
|
||||
if let Some(Ok(ThreadEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateDiff(
|
||||
update,
|
||||
)))) = event
|
||||
{
|
||||
update.diff
|
||||
} else {
|
||||
panic!("Expected diff but got: {:?}", event);
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn expect_terminal(&mut self) -> Entity<acp_thread::Terminal> {
|
||||
let event = self.0.next().await;
|
||||
if let Some(Ok(ThreadEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateTerminal(
|
||||
|
|
|
@ -273,13 +273,6 @@ impl AgentTool for EditFileTool {
|
|||
|
||||
let diff = cx.new(|cx| Diff::new(buffer.clone(), cx))?;
|
||||
event_stream.update_diff(diff.clone());
|
||||
let _finalize_diff = util::defer({
|
||||
let diff = diff.downgrade();
|
||||
let mut cx = cx.clone();
|
||||
move || {
|
||||
diff.update(&mut cx, |diff, cx| diff.finalize(cx)).ok();
|
||||
}
|
||||
});
|
||||
|
||||
let old_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
|
||||
let old_text = cx
|
||||
|
@ -396,6 +389,8 @@ impl AgentTool for EditFileTool {
|
|||
})
|
||||
.await;
|
||||
|
||||
diff.update(cx, |diff, cx| diff.finalize(cx)).ok();
|
||||
|
||||
let input_path = input.path.display();
|
||||
if unified_diff.is_empty() {
|
||||
anyhow::ensure!(
|
||||
|
@ -1550,100 +1545,6 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_diff_finalization(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let fs = project::FakeFs::new(cx.executor());
|
||||
fs.insert_tree("/", json!({"main.rs": ""})).await;
|
||||
|
||||
let project = Project::test(fs.clone(), [path!("/").as_ref()], cx).await;
|
||||
let languages = project.read_with(cx, |project, _cx| project.languages().clone());
|
||||
let context_server_registry =
|
||||
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
|
||||
let model = Arc::new(FakeLanguageModel::default());
|
||||
let thread = cx.new(|cx| {
|
||||
Thread::new(
|
||||
project.clone(),
|
||||
cx.new(|_cx| ProjectContext::default()),
|
||||
context_server_registry.clone(),
|
||||
Templates::new(),
|
||||
Some(model.clone()),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
// Ensure the diff is finalized after the edit completes.
|
||||
{
|
||||
let tool = Arc::new(EditFileTool::new(thread.downgrade(), languages.clone()));
|
||||
let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
|
||||
let edit = cx.update(|cx| {
|
||||
tool.run(
|
||||
EditFileToolInput {
|
||||
display_description: "Edit file".into(),
|
||||
path: path!("/main.rs").into(),
|
||||
mode: EditFileMode::Edit,
|
||||
},
|
||||
stream_tx,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
stream_rx.expect_update_fields().await;
|
||||
let diff = stream_rx.expect_diff().await;
|
||||
diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Pending(_))));
|
||||
cx.run_until_parked();
|
||||
model.end_last_completion_stream();
|
||||
edit.await.unwrap();
|
||||
diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Finalized(_))));
|
||||
}
|
||||
|
||||
// Ensure the diff is finalized if an error occurs while editing.
|
||||
{
|
||||
model.forbid_requests();
|
||||
let tool = Arc::new(EditFileTool::new(thread.downgrade(), languages.clone()));
|
||||
let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
|
||||
let edit = cx.update(|cx| {
|
||||
tool.run(
|
||||
EditFileToolInput {
|
||||
display_description: "Edit file".into(),
|
||||
path: path!("/main.rs").into(),
|
||||
mode: EditFileMode::Edit,
|
||||
},
|
||||
stream_tx,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
stream_rx.expect_update_fields().await;
|
||||
let diff = stream_rx.expect_diff().await;
|
||||
diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Pending(_))));
|
||||
edit.await.unwrap_err();
|
||||
diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Finalized(_))));
|
||||
model.allow_requests();
|
||||
}
|
||||
|
||||
// Ensure the diff is finalized if the tool call gets dropped.
|
||||
{
|
||||
let tool = Arc::new(EditFileTool::new(thread.downgrade(), languages.clone()));
|
||||
let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
|
||||
let edit = cx.update(|cx| {
|
||||
tool.run(
|
||||
EditFileToolInput {
|
||||
display_description: "Edit file".into(),
|
||||
path: path!("/main.rs").into(),
|
||||
mode: EditFileMode::Edit,
|
||||
},
|
||||
stream_tx,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
stream_rx.expect_update_fields().await;
|
||||
let diff = stream_rx.expect_diff().await;
|
||||
diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Pending(_))));
|
||||
drop(edit);
|
||||
cx.run_until_parked();
|
||||
diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Finalized(_))));
|
||||
}
|
||||
}
|
||||
|
||||
fn init_test(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
|
|
|
@ -6,7 +6,7 @@ use agent2::HistoryStore;
|
|||
use collections::HashMap;
|
||||
use editor::{Editor, EditorMode, MinimapVisibility};
|
||||
use gpui::{
|
||||
AnyEntity, App, AppContext as _, Entity, EntityId, EventEmitter, Focusable, ScrollHandle,
|
||||
AnyEntity, App, AppContext as _, Entity, EntityId, EventEmitter, Focusable,
|
||||
TextStyleRefinement, WeakEntity, Window,
|
||||
};
|
||||
use language::language_settings::SoftWrap;
|
||||
|
@ -154,22 +154,10 @@ impl EntryViewState {
|
|||
});
|
||||
}
|
||||
}
|
||||
AgentThreadEntry::AssistantMessage(message) => {
|
||||
let entry = if let Some(Entry::AssistantMessage(entry)) =
|
||||
self.entries.get_mut(index)
|
||||
{
|
||||
entry
|
||||
} else {
|
||||
self.set_entry(
|
||||
index,
|
||||
Entry::AssistantMessage(AssistantMessageEntry::default()),
|
||||
);
|
||||
let Some(Entry::AssistantMessage(entry)) = self.entries.get_mut(index) else {
|
||||
unreachable!()
|
||||
};
|
||||
entry
|
||||
};
|
||||
entry.sync(message);
|
||||
AgentThreadEntry::AssistantMessage(_) => {
|
||||
if index == self.entries.len() {
|
||||
self.entries.push(Entry::empty())
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -189,7 +177,7 @@ impl EntryViewState {
|
|||
pub fn settings_changed(&mut self, cx: &mut App) {
|
||||
for entry in self.entries.iter() {
|
||||
match entry {
|
||||
Entry::UserMessage { .. } | Entry::AssistantMessage { .. } => {}
|
||||
Entry::UserMessage { .. } => {}
|
||||
Entry::Content(response_views) => {
|
||||
for view in response_views.values() {
|
||||
if let Ok(diff_editor) = view.clone().downcast::<Editor>() {
|
||||
|
@ -220,29 +208,9 @@ pub enum ViewEvent {
|
|||
MessageEditorEvent(Entity<MessageEditor>, MessageEditorEvent),
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub struct AssistantMessageEntry {
|
||||
scroll_handles_by_chunk_index: HashMap<usize, ScrollHandle>,
|
||||
}
|
||||
|
||||
impl AssistantMessageEntry {
|
||||
pub fn scroll_handle_for_chunk(&self, ix: usize) -> Option<ScrollHandle> {
|
||||
self.scroll_handles_by_chunk_index.get(&ix).cloned()
|
||||
}
|
||||
|
||||
pub fn sync(&mut self, message: &acp_thread::AssistantMessage) {
|
||||
if let Some(acp_thread::AssistantMessageChunk::Thought { .. }) = message.chunks.last() {
|
||||
let ix = message.chunks.len() - 1;
|
||||
let handle = self.scroll_handles_by_chunk_index.entry(ix).or_default();
|
||||
handle.scroll_to_bottom();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Entry {
|
||||
UserMessage(Entity<MessageEditor>),
|
||||
AssistantMessage(AssistantMessageEntry),
|
||||
Content(HashMap<EntityId, AnyEntity>),
|
||||
}
|
||||
|
||||
|
@ -250,7 +218,7 @@ impl Entry {
|
|||
pub fn message_editor(&self) -> Option<&Entity<MessageEditor>> {
|
||||
match self {
|
||||
Self::UserMessage(editor) => Some(editor),
|
||||
Self::AssistantMessage(_) | Self::Content(_) => None,
|
||||
Entry::Content(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -271,16 +239,6 @@ impl Entry {
|
|||
.map(|entity| entity.downcast::<TerminalView>().unwrap())
|
||||
}
|
||||
|
||||
pub fn scroll_handle_for_assistant_message_chunk(
|
||||
&self,
|
||||
chunk_ix: usize,
|
||||
) -> Option<ScrollHandle> {
|
||||
match self {
|
||||
Self::AssistantMessage(message) => message.scroll_handle_for_chunk(chunk_ix),
|
||||
Self::UserMessage(_) | Self::Content(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn content_map(&self) -> Option<&HashMap<EntityId, AnyEntity>> {
|
||||
match self {
|
||||
Self::Content(map) => Some(map),
|
||||
|
@ -296,7 +254,7 @@ impl Entry {
|
|||
pub fn has_content(&self) -> bool {
|
||||
match self {
|
||||
Self::Content(map) => !map.is_empty(),
|
||||
Self::UserMessage(_) | Self::AssistantMessage(_) => false,
|
||||
Self::UserMessage(_) => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -3,23 +3,20 @@ mod configure_context_server_modal;
|
|||
mod manage_profiles_modal;
|
||||
mod tool_picker;
|
||||
|
||||
use std::{ops::Range, sync::Arc, time::Duration};
|
||||
use std::{sync::Arc, time::Duration};
|
||||
|
||||
use agent_servers::{AgentServerCommand, AgentServerSettings, AllAgentServersSettings, Gemini};
|
||||
use agent_servers::{AgentServerCommand, AllAgentServersSettings, Gemini};
|
||||
use agent_settings::AgentSettings;
|
||||
use anyhow::Result;
|
||||
use assistant_tool::{ToolSource, ToolWorkingSet};
|
||||
use cloud_llm_client::Plan;
|
||||
use collections::HashMap;
|
||||
use context_server::ContextServerId;
|
||||
use editor::{Editor, SelectionEffects, scroll::Autoscroll};
|
||||
use extension::ExtensionManifest;
|
||||
use extension_host::ExtensionStore;
|
||||
use fs::Fs;
|
||||
use gpui::{
|
||||
Action, Animation, AnimationExt as _, AnyView, App, AsyncWindowContext, Corner, Entity,
|
||||
EventEmitter, FocusHandle, Focusable, Hsla, ScrollHandle, Subscription, Task, Transformation,
|
||||
WeakEntity, percentage,
|
||||
Action, Animation, AnimationExt as _, AnyView, App, Corner, Entity, EventEmitter, FocusHandle,
|
||||
Focusable, Hsla, ScrollHandle, Subscription, Task, Transformation, WeakEntity, percentage,
|
||||
};
|
||||
use language::LanguageRegistry;
|
||||
use language_model::{
|
||||
|
@ -37,7 +34,7 @@ use ui::{
|
|||
Scrollbar, ScrollbarState, Switch, SwitchColor, SwitchField, Tooltip, prelude::*,
|
||||
};
|
||||
use util::ResultExt as _;
|
||||
use workspace::{Workspace, create_and_open_local_file};
|
||||
use workspace::Workspace;
|
||||
use zed_actions::ExtensionCategoryFilter;
|
||||
|
||||
pub(crate) use configure_context_server_modal::ConfigureContextServerModal;
|
||||
|
@ -1061,39 +1058,10 @@ impl AgentConfiguration {
|
|||
.child(
|
||||
v_flex()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.gap_2()
|
||||
.justify_between()
|
||||
.child(Headline::new("External Agents"))
|
||||
.child(
|
||||
Button::new("add-agent", "Add Agent")
|
||||
.icon_position(IconPosition::Start)
|
||||
.icon(IconName::Plus)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.label_size(LabelSize::Small)
|
||||
.on_click(
|
||||
move |_, window, cx| {
|
||||
if let Some(workspace) = window.root().flatten() {
|
||||
let workspace = workspace.downgrade();
|
||||
window
|
||||
.spawn(cx, async |cx| {
|
||||
open_new_agent_servers_entry_in_settings_editor(
|
||||
workspace,
|
||||
cx,
|
||||
).await
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
}
|
||||
),
|
||||
)
|
||||
)
|
||||
.child(Headline::new("External Agents"))
|
||||
.child(
|
||||
Label::new(
|
||||
"Bring the agent of your choice to Zed via our new Agent Client Protocol.",
|
||||
"Use the full power of Zed's UI with your favorite agent, connected via the Agent Client Protocol.",
|
||||
)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
|
@ -1356,109 +1324,3 @@ fn show_unable_to_uninstall_extension_with_context_server(
|
|||
|
||||
workspace.toggle_status_toast(status_toast, cx);
|
||||
}
|
||||
|
||||
async fn open_new_agent_servers_entry_in_settings_editor(
|
||||
workspace: WeakEntity<Workspace>,
|
||||
cx: &mut AsyncWindowContext,
|
||||
) -> Result<()> {
|
||||
let settings_editor = workspace
|
||||
.update_in(cx, |_, window, cx| {
|
||||
create_and_open_local_file(paths::settings_file(), window, cx, || {
|
||||
settings::initial_user_settings_content().as_ref().into()
|
||||
})
|
||||
})?
|
||||
.await?
|
||||
.downcast::<Editor>()
|
||||
.unwrap();
|
||||
|
||||
settings_editor
|
||||
.downgrade()
|
||||
.update_in(cx, |item, window, cx| {
|
||||
let text = item.buffer().read(cx).snapshot(cx).text();
|
||||
|
||||
let settings = cx.global::<SettingsStore>();
|
||||
|
||||
let mut unique_server_name = None;
|
||||
let edits = settings.edits_for_update::<AllAgentServersSettings>(&text, |file| {
|
||||
let server_name: Option<SharedString> = (0..u8::MAX)
|
||||
.map(|i| {
|
||||
if i == 0 {
|
||||
"your_agent".into()
|
||||
} else {
|
||||
format!("your_agent_{}", i).into()
|
||||
}
|
||||
})
|
||||
.find(|name| !file.custom.contains_key(name));
|
||||
if let Some(server_name) = server_name {
|
||||
unique_server_name = Some(server_name.clone());
|
||||
file.custom.insert(
|
||||
server_name,
|
||||
AgentServerSettings {
|
||||
command: AgentServerCommand {
|
||||
path: "path_to_executable".into(),
|
||||
args: vec![],
|
||||
env: Some(HashMap::default()),
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
if edits.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let ranges = edits
|
||||
.iter()
|
||||
.map(|(range, _)| range.clone())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
item.edit(edits, cx);
|
||||
if let Some((unique_server_name, buffer)) =
|
||||
unique_server_name.zip(item.buffer().read(cx).as_singleton())
|
||||
{
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
if let Some(range) =
|
||||
find_text_in_buffer(&unique_server_name, ranges[0].start, &snapshot)
|
||||
{
|
||||
item.change_selections(
|
||||
SelectionEffects::scroll(Autoscroll::newest()),
|
||||
window,
|
||||
cx,
|
||||
|selections| {
|
||||
selections.select_ranges(vec![range]);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn find_text_in_buffer(
|
||||
text: &str,
|
||||
start: usize,
|
||||
snapshot: &language::BufferSnapshot,
|
||||
) -> Option<Range<usize>> {
|
||||
let chars = text.chars().collect::<Vec<char>>();
|
||||
|
||||
let mut offset = start;
|
||||
let mut char_offset = 0;
|
||||
for c in snapshot.chars_at(start) {
|
||||
if char_offset >= chars.len() {
|
||||
break;
|
||||
}
|
||||
offset += 1;
|
||||
|
||||
if c == chars[char_offset] {
|
||||
char_offset += 1;
|
||||
} else {
|
||||
char_offset = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if char_offset == chars.len() {
|
||||
Some(offset.saturating_sub(chars.len())..offset)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,7 +14,6 @@ use zed_actions::agent::ReauthenticateAgent;
|
|||
|
||||
use crate::acp::{AcpThreadHistory, ThreadHistoryEvent};
|
||||
use crate::agent_diff::AgentDiffThread;
|
||||
use crate::ui::AcpOnboardingModal;
|
||||
use crate::{
|
||||
AddContextServer, AgentDiffPane, ContinueThread, ContinueWithBurnMode,
|
||||
DeleteRecentlyOpenThread, ExpandMessageEditor, Follow, InlineAssistant, NewTextThread,
|
||||
|
@ -78,10 +77,7 @@ use workspace::{
|
|||
};
|
||||
use zed_actions::{
|
||||
DecreaseBufferFontSize, IncreaseBufferFontSize, ResetBufferFontSize,
|
||||
agent::{
|
||||
OpenAcpOnboardingModal, OpenOnboardingModal, OpenSettings, ResetOnboarding,
|
||||
ToggleModelSelector,
|
||||
},
|
||||
agent::{OpenOnboardingModal, OpenSettings, ResetOnboarding, ToggleModelSelector},
|
||||
assistant::{OpenRulesLibrary, ToggleFocus},
|
||||
};
|
||||
|
||||
|
@ -205,9 +201,6 @@ pub fn init(cx: &mut App) {
|
|||
.register_action(|workspace, _: &OpenOnboardingModal, window, cx| {
|
||||
AgentOnboardingModal::toggle(workspace, window, cx)
|
||||
})
|
||||
.register_action(|workspace, _: &OpenAcpOnboardingModal, window, cx| {
|
||||
AcpOnboardingModal::toggle(workspace, window, cx)
|
||||
})
|
||||
.register_action(|_workspace, _: &ResetOnboarding, window, cx| {
|
||||
window.dispatch_action(workspace::RestoreBanner.boxed_clone(), cx);
|
||||
window.refresh();
|
||||
|
@ -598,6 +591,17 @@ impl AgentPanel {
|
|||
None
|
||||
};
|
||||
|
||||
// Wait for the Gemini/Native feature flag to be available.
|
||||
let client = workspace.read_with(cx, |workspace, _| workspace.client().clone())?;
|
||||
if !client.status().borrow().is_signed_out() {
|
||||
cx.update(|_, cx| {
|
||||
cx.wait_for_flag_or_timeout::<feature_flags::GeminiAndNativeFeatureFlag>(
|
||||
Duration::from_secs(2),
|
||||
)
|
||||
})?
|
||||
.await;
|
||||
}
|
||||
|
||||
let panel = workspace.update_in(cx, |workspace, window, cx| {
|
||||
let panel = cx.new(|cx| {
|
||||
Self::new(
|
||||
|
@ -1848,6 +1852,19 @@ impl AgentPanel {
|
|||
menu
|
||||
}
|
||||
|
||||
pub fn set_selected_agent(
|
||||
&mut self,
|
||||
agent: AgentType,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if self.selected_agent != agent {
|
||||
self.selected_agent = agent.clone();
|
||||
self.serialize(cx);
|
||||
}
|
||||
self.new_agent_thread(agent, window, cx);
|
||||
}
|
||||
|
||||
pub fn selected_agent(&self) -> AgentType {
|
||||
self.selected_agent.clone()
|
||||
}
|
||||
|
@ -1858,11 +1875,6 @@ impl AgentPanel {
|
|||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if self.selected_agent != agent {
|
||||
self.selected_agent = agent.clone();
|
||||
self.serialize(cx);
|
||||
}
|
||||
|
||||
match agent {
|
||||
AgentType::Zed => {
|
||||
window.dispatch_action(
|
||||
|
@ -2543,7 +2555,7 @@ impl AgentPanel {
|
|||
workspace.panel::<AgentPanel>(cx)
|
||||
{
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.new_agent_thread(
|
||||
panel.set_selected_agent(
|
||||
AgentType::NativeAgent,
|
||||
window,
|
||||
cx,
|
||||
|
@ -2569,7 +2581,7 @@ impl AgentPanel {
|
|||
workspace.panel::<AgentPanel>(cx)
|
||||
{
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.new_agent_thread(
|
||||
panel.set_selected_agent(
|
||||
AgentType::TextThread,
|
||||
window,
|
||||
cx,
|
||||
|
@ -2597,7 +2609,7 @@ impl AgentPanel {
|
|||
workspace.panel::<AgentPanel>(cx)
|
||||
{
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.new_agent_thread(
|
||||
panel.set_selected_agent(
|
||||
AgentType::Gemini,
|
||||
window,
|
||||
cx,
|
||||
|
@ -2624,7 +2636,7 @@ impl AgentPanel {
|
|||
workspace.panel::<AgentPanel>(cx)
|
||||
{
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.new_agent_thread(
|
||||
panel.set_selected_agent(
|
||||
AgentType::ClaudeCode,
|
||||
window,
|
||||
cx,
|
||||
|
@ -2657,7 +2669,7 @@ impl AgentPanel {
|
|||
workspace.panel::<AgentPanel>(cx)
|
||||
{
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.new_agent_thread(
|
||||
panel.set_selected_agent(
|
||||
AgentType::Custom {
|
||||
name: agent_name
|
||||
.clone(),
|
||||
|
@ -2681,9 +2693,9 @@ impl AgentPanel {
|
|||
})
|
||||
.when(cx.has_flag::<GeminiAndNativeFeatureFlag>(), |menu| {
|
||||
menu.separator().link(
|
||||
"Add Other Agents",
|
||||
"Add Your Own Agent",
|
||||
OpenBrowser {
|
||||
url: zed_urls::external_agents_docs(cx),
|
||||
url: "https://agentclientprotocol.com/".into(),
|
||||
}
|
||||
.boxed_clone(),
|
||||
)
|
||||
|
|
|
@ -6,8 +6,7 @@ use feature_flags::ZedProFeatureFlag;
|
|||
use fuzzy::{StringMatch, StringMatchCandidate, match_strings};
|
||||
use gpui::{Action, AnyElement, App, BackgroundExecutor, DismissEvent, Subscription, Task};
|
||||
use language_model::{
|
||||
AuthenticateError, ConfiguredModel, LanguageModel, LanguageModelProviderId,
|
||||
LanguageModelRegistry,
|
||||
ConfiguredModel, LanguageModel, LanguageModelProviderId, LanguageModelRegistry,
|
||||
};
|
||||
use ordered_float::OrderedFloat;
|
||||
use picker::{Picker, PickerDelegate};
|
||||
|
@ -77,7 +76,6 @@ pub struct LanguageModelPickerDelegate {
|
|||
all_models: Arc<GroupedModels>,
|
||||
filtered_entries: Vec<LanguageModelPickerEntry>,
|
||||
selected_index: usize,
|
||||
_authenticate_all_providers_task: Task<()>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
|
@ -98,7 +96,6 @@ impl LanguageModelPickerDelegate {
|
|||
selected_index: Self::get_active_model_index(&entries, get_active_model(cx)),
|
||||
filtered_entries: entries,
|
||||
get_active_model: Arc::new(get_active_model),
|
||||
_authenticate_all_providers_task: Self::authenticate_all_providers(cx),
|
||||
_subscriptions: vec![cx.subscribe_in(
|
||||
&LanguageModelRegistry::global(cx),
|
||||
window,
|
||||
|
@ -142,56 +139,6 @@ impl LanguageModelPickerDelegate {
|
|||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
/// Authenticates all providers in the [`LanguageModelRegistry`].
|
||||
///
|
||||
/// We do this so that we can populate the language selector with all of the
|
||||
/// models from the configured providers.
|
||||
fn authenticate_all_providers(cx: &mut App) -> Task<()> {
|
||||
let authenticate_all_providers = LanguageModelRegistry::global(cx)
|
||||
.read(cx)
|
||||
.providers()
|
||||
.iter()
|
||||
.map(|provider| (provider.id(), provider.name(), provider.authenticate(cx)))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
cx.spawn(async move |_cx| {
|
||||
for (provider_id, provider_name, authenticate_task) in authenticate_all_providers {
|
||||
if let Err(err) = authenticate_task.await {
|
||||
if matches!(err, AuthenticateError::CredentialsNotFound) {
|
||||
// Since we're authenticating these providers in the
|
||||
// background for the purposes of populating the
|
||||
// language selector, we don't care about providers
|
||||
// where the credentials are not found.
|
||||
} else {
|
||||
// Some providers have noisy failure states that we
|
||||
// don't want to spam the logs with every time the
|
||||
// language model selector is initialized.
|
||||
//
|
||||
// Ideally these should have more clear failure modes
|
||||
// that we know are safe to ignore here, like what we do
|
||||
// with `CredentialsNotFound` above.
|
||||
match provider_id.0.as_ref() {
|
||||
"lmstudio" | "ollama" => {
|
||||
// LM Studio and Ollama both make fetch requests to the local APIs to determine if they are "authenticated".
|
||||
//
|
||||
// These fail noisily, so we don't log them.
|
||||
}
|
||||
"copilot_chat" => {
|
||||
// Copilot Chat returns an error if Copilot is not enabled, so we don't log those errors.
|
||||
}
|
||||
_ => {
|
||||
log::error!(
|
||||
"Failed to authenticate provider: {}: {err}",
|
||||
provider_name.0
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn active_model(&self, cx: &App) -> Option<ConfiguredModel> {
|
||||
(self.get_active_model)(cx)
|
||||
}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
mod acp_onboarding_modal;
|
||||
mod agent_notification;
|
||||
mod burn_mode_tooltip;
|
||||
mod context_pill;
|
||||
|
@ -7,7 +6,6 @@ mod onboarding_modal;
|
|||
pub mod preview;
|
||||
mod unavailable_editing_tooltip;
|
||||
|
||||
pub use acp_onboarding_modal::*;
|
||||
pub use agent_notification::*;
|
||||
pub use burn_mode_tooltip::*;
|
||||
pub use context_pill::*;
|
||||
|
|
|
@ -1,254 +0,0 @@
|
|||
use client::zed_urls;
|
||||
use gpui::{
|
||||
ClickEvent, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, MouseDownEvent, Render,
|
||||
linear_color_stop, linear_gradient,
|
||||
};
|
||||
use ui::{TintColor, Vector, VectorName, prelude::*};
|
||||
use workspace::{ModalView, Workspace};
|
||||
|
||||
use crate::agent_panel::{AgentPanel, AgentType};
|
||||
|
||||
macro_rules! acp_onboarding_event {
|
||||
($name:expr) => {
|
||||
telemetry::event!($name, source = "ACP Onboarding");
|
||||
};
|
||||
($name:expr, $($key:ident $(= $value:expr)?),+ $(,)?) => {
|
||||
telemetry::event!($name, source = "ACP Onboarding", $($key $(= $value)?),+);
|
||||
};
|
||||
}
|
||||
|
||||
pub struct AcpOnboardingModal {
|
||||
focus_handle: FocusHandle,
|
||||
workspace: Entity<Workspace>,
|
||||
}
|
||||
|
||||
impl AcpOnboardingModal {
|
||||
pub fn toggle(workspace: &mut Workspace, window: &mut Window, cx: &mut Context<Workspace>) {
|
||||
let workspace_entity = cx.entity();
|
||||
workspace.toggle_modal(window, cx, |_window, cx| Self {
|
||||
workspace: workspace_entity,
|
||||
focus_handle: cx.focus_handle(),
|
||||
});
|
||||
}
|
||||
|
||||
fn open_panel(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.workspace.update(cx, |workspace, cx| {
|
||||
workspace.focus_panel::<AgentPanel>(window, cx);
|
||||
|
||||
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.new_agent_thread(AgentType::Gemini, window, cx);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
cx.emit(DismissEvent);
|
||||
|
||||
acp_onboarding_event!("Open Panel Clicked");
|
||||
}
|
||||
|
||||
fn view_docs(&mut self, _: &ClickEvent, _: &mut Window, cx: &mut Context<Self>) {
|
||||
cx.open_url(&zed_urls::external_agents_docs(cx));
|
||||
cx.notify();
|
||||
|
||||
acp_onboarding_event!("Documentation Link Clicked");
|
||||
}
|
||||
|
||||
fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<DismissEvent> for AcpOnboardingModal {}
|
||||
|
||||
impl Focusable for AcpOnboardingModal {
|
||||
fn focus_handle(&self, _cx: &App) -> FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl ModalView for AcpOnboardingModal {}
|
||||
|
||||
impl Render for AcpOnboardingModal {
|
||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let illustration_element = |label: bool, opacity: f32| {
|
||||
h_flex()
|
||||
.px_1()
|
||||
.py_0p5()
|
||||
.gap_1()
|
||||
.rounded_sm()
|
||||
.bg(cx.theme().colors().element_active.opacity(0.05))
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.border_dashed()
|
||||
.child(
|
||||
Icon::new(IconName::Stop)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Custom(cx.theme().colors().text_muted.opacity(0.15))),
|
||||
)
|
||||
.map(|this| {
|
||||
if label {
|
||||
this.child(
|
||||
Label::new("Your Agent Here")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
} else {
|
||||
this.child(
|
||||
div().w_16().h_1().rounded_full().bg(cx
|
||||
.theme()
|
||||
.colors()
|
||||
.element_active
|
||||
.opacity(0.6)),
|
||||
)
|
||||
}
|
||||
})
|
||||
.opacity(opacity)
|
||||
};
|
||||
|
||||
let illustration = h_flex()
|
||||
.relative()
|
||||
.h(rems_from_px(126.))
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.justify_center()
|
||||
.gap_8()
|
||||
.rounded_t_md()
|
||||
.overflow_hidden()
|
||||
.child(
|
||||
div().absolute().inset_0().w(px(515.)).h(px(126.)).child(
|
||||
Vector::new(VectorName::AcpGrid, rems_from_px(515.), rems_from_px(126.))
|
||||
.color(ui::Color::Custom(cx.theme().colors().text.opacity(0.02))),
|
||||
),
|
||||
)
|
||||
.child(div().absolute().inset_0().size_full().bg(linear_gradient(
|
||||
0.,
|
||||
linear_color_stop(
|
||||
cx.theme().colors().elevated_surface_background.opacity(0.1),
|
||||
0.9,
|
||||
),
|
||||
linear_color_stop(
|
||||
cx.theme().colors().elevated_surface_background.opacity(0.),
|
||||
0.,
|
||||
),
|
||||
)))
|
||||
.child(
|
||||
div()
|
||||
.absolute()
|
||||
.inset_0()
|
||||
.size_full()
|
||||
.bg(gpui::black().opacity(0.15)),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_4()
|
||||
.child(
|
||||
Vector::new(VectorName::AcpLogo, rems_from_px(106.), rems_from_px(40.))
|
||||
.color(ui::Color::Custom(cx.theme().colors().text.opacity(0.8))),
|
||||
)
|
||||
.child(
|
||||
Vector::new(
|
||||
VectorName::AcpLogoSerif,
|
||||
rems_from_px(111.),
|
||||
rems_from_px(41.),
|
||||
)
|
||||
.color(ui::Color::Custom(cx.theme().colors().text.opacity(0.8))),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_1p5()
|
||||
.child(illustration_element(false, 0.15))
|
||||
.child(illustration_element(true, 0.3))
|
||||
.child(
|
||||
h_flex()
|
||||
.pl_1()
|
||||
.pr_2()
|
||||
.py_0p5()
|
||||
.gap_1()
|
||||
.rounded_sm()
|
||||
.bg(cx.theme().colors().element_active.opacity(0.2))
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.child(
|
||||
Icon::new(IconName::AiGemini)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(Label::new("New Gemini CLI Thread").size(LabelSize::Small)),
|
||||
)
|
||||
.child(illustration_element(true, 0.3))
|
||||
.child(illustration_element(false, 0.15)),
|
||||
);
|
||||
|
||||
let heading = v_flex()
|
||||
.w_full()
|
||||
.gap_1()
|
||||
.child(
|
||||
Label::new("Now Available")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(Headline::new("Bring Your Own Agent to Zed").size(HeadlineSize::Large));
|
||||
|
||||
let copy = "Bring the agent of your choice to Zed via our new Agent Client Protocol (ACP), starting with Google's Gemini CLI integration.";
|
||||
|
||||
let open_panel_button = Button::new("open-panel", "Start with Gemini CLI")
|
||||
.icon_size(IconSize::Indicator)
|
||||
.style(ButtonStyle::Tinted(TintColor::Accent))
|
||||
.full_width()
|
||||
.on_click(cx.listener(Self::open_panel));
|
||||
|
||||
let docs_button = Button::new("add-other-agents", "Add Other Agents")
|
||||
.icon(IconName::ArrowUpRight)
|
||||
.icon_size(IconSize::Indicator)
|
||||
.icon_color(Color::Muted)
|
||||
.full_width()
|
||||
.on_click(cx.listener(Self::view_docs));
|
||||
|
||||
let close_button = h_flex().absolute().top_2().right_2().child(
|
||||
IconButton::new("cancel", IconName::Close).on_click(cx.listener(
|
||||
|_, _: &ClickEvent, _window, cx| {
|
||||
acp_onboarding_event!("Canceled", trigger = "X click");
|
||||
cx.emit(DismissEvent);
|
||||
},
|
||||
)),
|
||||
);
|
||||
|
||||
v_flex()
|
||||
.id("acp-onboarding")
|
||||
.key_context("AcpOnboardingModal")
|
||||
.relative()
|
||||
.w(rems(34.))
|
||||
.h_full()
|
||||
.elevation_3(cx)
|
||||
.track_focus(&self.focus_handle(cx))
|
||||
.overflow_hidden()
|
||||
.on_action(cx.listener(Self::cancel))
|
||||
.on_action(cx.listener(|_, _: &menu::Cancel, _window, cx| {
|
||||
acp_onboarding_event!("Canceled", trigger = "Action");
|
||||
cx.emit(DismissEvent);
|
||||
}))
|
||||
.on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, _cx| {
|
||||
this.focus_handle.focus(window);
|
||||
}))
|
||||
.child(illustration)
|
||||
.child(
|
||||
v_flex()
|
||||
.p_4()
|
||||
.gap_2()
|
||||
.child(heading)
|
||||
.child(Label::new(copy).color(Color::Muted))
|
||||
.child(
|
||||
v_flex()
|
||||
.w_full()
|
||||
.mt_2()
|
||||
.gap_1()
|
||||
.child(open_panel_button)
|
||||
.child(docs_button),
|
||||
),
|
||||
)
|
||||
.child(close_button)
|
||||
}
|
||||
}
|
|
@ -25,7 +25,7 @@ parking_lot.workspace = true
|
|||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
ui.workspace = true
|
||||
workspace.workspace = true
|
||||
workspace = { path = "../workspace", default-features = false }
|
||||
workspace-hack.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
|
|
|
@ -28,7 +28,7 @@ serde.workspace = true
|
|||
serde_json.workspace = true
|
||||
text.workspace = true
|
||||
util.workspace = true
|
||||
workspace.workspace = true
|
||||
workspace = { path = "../workspace", default-features = false }
|
||||
workspace-hack.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
|
|
|
@ -19,7 +19,7 @@ itertools.workspace = true
|
|||
settings.workspace = true
|
||||
theme.workspace = true
|
||||
ui.workspace = true
|
||||
workspace.workspace = true
|
||||
workspace = { path = "../workspace", default-features = false }
|
||||
zed_actions.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
||||
|
|
|
@ -394,6 +394,16 @@ impl ActiveCall {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "call"))]
|
||||
pub fn unshare_project(
|
||||
&mut self,
|
||||
_project: Entity<Project>,
|
||||
_cx: &mut Context<Self>,
|
||||
) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "call")]
|
||||
pub fn unshare_project(
|
||||
&mut self,
|
||||
project: Entity<Project>,
|
||||
|
|
|
@ -43,11 +43,3 @@ pub fn ai_privacy_and_security(cx: &App) -> String {
|
|||
server_url = server_url(cx)
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns the URL to Zed AI's external agents documentation.
|
||||
pub fn external_agents_docs(cx: &App) -> String {
|
||||
format!(
|
||||
"{server_url}/docs/ai/external-agents",
|
||||
server_url = server_url(cx)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -476,7 +476,8 @@ impl Server {
|
|||
.add_request_handler(forward_mutating_project_request::<proto::GitChangeBranch>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::CheckForPushedCommits>)
|
||||
.add_message_handler(broadcast_project_message_from_host::<proto::AdvertiseContexts>)
|
||||
.add_message_handler(update_context);
|
||||
.add_message_handler(update_context)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::ToggleLspLogs>);
|
||||
|
||||
Arc::new(server)
|
||||
}
|
||||
|
|
|
@ -50,7 +50,7 @@ sum_tree.workspace = true
|
|||
task.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
workspace.workspace = true
|
||||
workspace = { path = "../workspace", default-features = false }
|
||||
workspace-hack.workspace = true
|
||||
itertools.workspace = true
|
||||
|
||||
|
|
|
@ -19,10 +19,6 @@ static KEYMAP_LINUX: LazyLock<KeymapFile> = LazyLock::new(|| {
|
|||
load_keymap("keymaps/default-linux.json").expect("Failed to load Linux keymap")
|
||||
});
|
||||
|
||||
static KEYMAP_WINDOWS: LazyLock<KeymapFile> = LazyLock::new(|| {
|
||||
load_keymap("keymaps/default-windows.json").expect("Failed to load Windows keymap")
|
||||
});
|
||||
|
||||
static ALL_ACTIONS: LazyLock<Vec<ActionDef>> = LazyLock::new(dump_all_gpui_actions);
|
||||
|
||||
const FRONT_MATTER_COMMENT: &str = "<!-- ZED_META {} -->";
|
||||
|
@ -220,7 +216,6 @@ fn find_binding(os: &str, action: &str) -> Option<String> {
|
|||
let keymap = match os {
|
||||
"macos" => &KEYMAP_MACOS,
|
||||
"linux" | "freebsd" => &KEYMAP_LINUX,
|
||||
"windows" => &KEYMAP_WINDOWS,
|
||||
_ => unreachable!("Not a valid OS: {}", os),
|
||||
};
|
||||
|
||||
|
|
|
@ -89,7 +89,7 @@ ui.workspace = true
|
|||
url.workspace = true
|
||||
util.workspace = true
|
||||
uuid.workspace = true
|
||||
workspace.workspace = true
|
||||
workspace = { path = "../workspace", default-features = false }
|
||||
zed_actions.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
||||
|
|
|
@ -2588,7 +2588,7 @@ impl Editor {
|
|||
|| binding
|
||||
.keystrokes()
|
||||
.first()
|
||||
.is_some_and(|keystroke| keystroke.display_modifiers.modified())
|
||||
.is_some_and(|keystroke| keystroke.modifiers.modified())
|
||||
}))
|
||||
}
|
||||
|
||||
|
@ -7686,16 +7686,16 @@ impl Editor {
|
|||
.keystroke()
|
||||
{
|
||||
modifiers_held = modifiers_held
|
||||
|| (&accept_keystroke.display_modifiers == modifiers
|
||||
&& accept_keystroke.display_modifiers.modified());
|
||||
|| (&accept_keystroke.modifiers == modifiers
|
||||
&& accept_keystroke.modifiers.modified());
|
||||
};
|
||||
if let Some(accept_partial_keystroke) = self
|
||||
.accept_edit_prediction_keybind(true, window, cx)
|
||||
.keystroke()
|
||||
{
|
||||
modifiers_held = modifiers_held
|
||||
|| (&accept_partial_keystroke.display_modifiers == modifiers
|
||||
&& accept_partial_keystroke.display_modifiers.modified());
|
||||
|| (&accept_partial_keystroke.modifiers == modifiers
|
||||
&& accept_partial_keystroke.modifiers.modified());
|
||||
}
|
||||
|
||||
if modifiers_held {
|
||||
|
@ -9044,7 +9044,7 @@ impl Editor {
|
|||
|
||||
let is_platform_style_mac = PlatformStyle::platform() == PlatformStyle::Mac;
|
||||
|
||||
let modifiers_color = if accept_keystroke.display_modifiers == window.modifiers() {
|
||||
let modifiers_color = if accept_keystroke.modifiers == window.modifiers() {
|
||||
Color::Accent
|
||||
} else {
|
||||
Color::Muted
|
||||
|
@ -9056,19 +9056,19 @@ impl Editor {
|
|||
.font(theme::ThemeSettings::get_global(cx).buffer_font.clone())
|
||||
.text_size(TextSize::XSmall.rems(cx))
|
||||
.child(h_flex().children(ui::render_modifiers(
|
||||
&accept_keystroke.display_modifiers,
|
||||
&accept_keystroke.modifiers,
|
||||
PlatformStyle::platform(),
|
||||
Some(modifiers_color),
|
||||
Some(IconSize::XSmall.rems().into()),
|
||||
true,
|
||||
)))
|
||||
.when(is_platform_style_mac, |parent| {
|
||||
parent.child(accept_keystroke.display_key.clone())
|
||||
parent.child(accept_keystroke.key.clone())
|
||||
})
|
||||
.when(!is_platform_style_mac, |parent| {
|
||||
parent.child(
|
||||
Key::new(
|
||||
util::capitalize(&accept_keystroke.display_key),
|
||||
util::capitalize(&accept_keystroke.key),
|
||||
Some(Color::Default),
|
||||
)
|
||||
.size(Some(IconSize::XSmall.rems().into())),
|
||||
|
@ -9171,7 +9171,7 @@ impl Editor {
|
|||
max_width: Pixels,
|
||||
cursor_point: Point,
|
||||
style: &EditorStyle,
|
||||
accept_keystroke: Option<&gpui::KeybindingKeystroke>,
|
||||
accept_keystroke: Option<&gpui::Keystroke>,
|
||||
_window: &Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) -> Option<AnyElement> {
|
||||
|
@ -9249,7 +9249,7 @@ impl Editor {
|
|||
accept_keystroke.as_ref(),
|
||||
|el, accept_keystroke| {
|
||||
el.child(h_flex().children(ui::render_modifiers(
|
||||
&accept_keystroke.display_modifiers,
|
||||
&accept_keystroke.modifiers,
|
||||
PlatformStyle::platform(),
|
||||
Some(Color::Default),
|
||||
Some(IconSize::XSmall.rems().into()),
|
||||
|
@ -9319,7 +9319,7 @@ impl Editor {
|
|||
.child(completion),
|
||||
)
|
||||
.when_some(accept_keystroke, |el, accept_keystroke| {
|
||||
if !accept_keystroke.display_modifiers.modified() {
|
||||
if !accept_keystroke.modifiers.modified() {
|
||||
return el;
|
||||
}
|
||||
|
||||
|
@ -9338,7 +9338,7 @@ impl Editor {
|
|||
.font(theme::ThemeSettings::get_global(cx).buffer_font.clone())
|
||||
.when(is_platform_style_mac, |parent| parent.gap_1())
|
||||
.child(h_flex().children(ui::render_modifiers(
|
||||
&accept_keystroke.display_modifiers,
|
||||
&accept_keystroke.modifiers,
|
||||
PlatformStyle::platform(),
|
||||
Some(if !has_completion {
|
||||
Color::Muted
|
||||
|
|
|
@ -43,10 +43,10 @@ use gpui::{
|
|||
Bounds, ClickEvent, ClipboardItem, ContentMask, Context, Corner, Corners, CursorStyle,
|
||||
DispatchPhase, Edges, Element, ElementInputHandler, Entity, Focusable as _, FontId,
|
||||
GlobalElementId, Hitbox, HitboxBehavior, Hsla, InteractiveElement, IntoElement, IsZero,
|
||||
KeybindingKeystroke, Length, ModifiersChangedEvent, MouseButton, MouseClickEvent,
|
||||
MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, ParentElement, Pixels, ScrollDelta,
|
||||
ScrollHandle, ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveElement,
|
||||
Style, Styled, TextRun, TextStyleRefinement, WeakEntity, Window, anchored, deferred, div, fill,
|
||||
Keystroke, Length, ModifiersChangedEvent, MouseButton, MouseClickEvent, MouseDownEvent,
|
||||
MouseMoveEvent, MouseUpEvent, PaintQuad, ParentElement, Pixels, ScrollDelta, ScrollHandle,
|
||||
ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveElement, Style, Styled,
|
||||
TextRun, TextStyleRefinement, WeakEntity, Window, anchored, deferred, div, fill,
|
||||
linear_color_stop, linear_gradient, outline, point, px, quad, relative, size, solid_background,
|
||||
transparent_black,
|
||||
};
|
||||
|
@ -7150,7 +7150,7 @@ fn header_jump_data(
|
|||
pub struct AcceptEditPredictionBinding(pub(crate) Option<gpui::KeyBinding>);
|
||||
|
||||
impl AcceptEditPredictionBinding {
|
||||
pub fn keystroke(&self) -> Option<&KeybindingKeystroke> {
|
||||
pub fn keystroke(&self) -> Option<&Keystroke> {
|
||||
if let Some(binding) = self.0.as_ref() {
|
||||
match &binding.keystrokes() {
|
||||
[keystroke, ..] => Some(keystroke),
|
||||
|
|
|
@ -98,10 +98,6 @@ impl FeatureFlag for GeminiAndNativeFeatureFlag {
|
|||
// integration too, and we'd like to turn Gemini/Native on in new builds
|
||||
// without enabling Claude Code in old builds.
|
||||
const NAME: &'static str = "gemini-and-native";
|
||||
|
||||
fn enabled_for_all() -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ClaudeCodeFeatureFlag;
|
||||
|
@ -205,7 +201,7 @@ impl FeatureFlagAppExt for App {
|
|||
fn has_flag<T: FeatureFlag>(&self) -> bool {
|
||||
self.try_global::<FeatureFlags>()
|
||||
.map(|flags| flags.has_flag::<T>())
|
||||
.unwrap_or(T::enabled_for_all())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn is_staff(&self) -> bool {
|
||||
|
|
|
@ -4466,7 +4466,7 @@ fn current_language_model(cx: &Context<'_, GitPanel>) -> Option<Arc<dyn Language
|
|||
is_enabled
|
||||
.then(|| {
|
||||
let ConfiguredModel { provider, model } =
|
||||
LanguageModelRegistry::read_global(cx).commit_message_model()?;
|
||||
LanguageModelRegistry::read_global(cx).commit_message_model(cx)?;
|
||||
|
||||
provider.is_authenticated(cx).then(|| model)
|
||||
})
|
||||
|
|
|
@ -37,10 +37,10 @@ use crate::{
|
|||
AssetSource, BackgroundExecutor, Bounds, ClipboardItem, CursorStyle, DispatchPhase, DisplayId,
|
||||
EventEmitter, FocusHandle, FocusMap, ForegroundExecutor, Global, KeyBinding, KeyContext,
|
||||
Keymap, Keystroke, LayoutId, Menu, MenuItem, OwnedMenu, PathPromptOptions, Pixels, Platform,
|
||||
PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper, Point, PromptBuilder,
|
||||
PromptButton, PromptHandle, PromptLevel, Render, RenderImage, RenderablePromptHandle,
|
||||
Reservation, ScreenCaptureSource, SubscriberSet, Subscription, SvgRenderer, Task, TextSystem,
|
||||
Window, WindowAppearance, WindowHandle, WindowId, WindowInvalidator,
|
||||
PlatformDisplay, PlatformKeyboardLayout, Point, PromptBuilder, PromptButton, PromptHandle,
|
||||
PromptLevel, Render, RenderImage, RenderablePromptHandle, Reservation, ScreenCaptureSource,
|
||||
SubscriberSet, Subscription, SvgRenderer, Task, TextSystem, Window, WindowAppearance,
|
||||
WindowHandle, WindowId, WindowInvalidator,
|
||||
colors::{Colors, GlobalColors},
|
||||
current_platform, hash, init_app_menus,
|
||||
};
|
||||
|
@ -263,7 +263,6 @@ pub struct App {
|
|||
pub(crate) focus_handles: Arc<FocusMap>,
|
||||
pub(crate) keymap: Rc<RefCell<Keymap>>,
|
||||
pub(crate) keyboard_layout: Box<dyn PlatformKeyboardLayout>,
|
||||
pub(crate) keyboard_mapper: Rc<dyn PlatformKeyboardMapper>,
|
||||
pub(crate) global_action_listeners:
|
||||
FxHashMap<TypeId, Vec<Rc<dyn Fn(&dyn Any, DispatchPhase, &mut Self)>>>,
|
||||
pending_effects: VecDeque<Effect>,
|
||||
|
@ -313,7 +312,6 @@ impl App {
|
|||
let text_system = Arc::new(TextSystem::new(platform.text_system()));
|
||||
let entities = EntityMap::new();
|
||||
let keyboard_layout = platform.keyboard_layout();
|
||||
let keyboard_mapper = platform.keyboard_mapper();
|
||||
|
||||
let app = Rc::new_cyclic(|this| AppCell {
|
||||
app: RefCell::new(App {
|
||||
|
@ -339,7 +337,6 @@ impl App {
|
|||
focus_handles: Arc::new(RwLock::new(SlotMap::with_key())),
|
||||
keymap: Rc::new(RefCell::new(Keymap::default())),
|
||||
keyboard_layout,
|
||||
keyboard_mapper,
|
||||
global_action_listeners: FxHashMap::default(),
|
||||
pending_effects: VecDeque::new(),
|
||||
pending_notifications: FxHashSet::default(),
|
||||
|
@ -379,7 +376,6 @@ impl App {
|
|||
if let Some(app) = app.upgrade() {
|
||||
let cx = &mut app.borrow_mut();
|
||||
cx.keyboard_layout = cx.platform.keyboard_layout();
|
||||
cx.keyboard_mapper = cx.platform.keyboard_mapper();
|
||||
cx.keyboard_layout_observers
|
||||
.clone()
|
||||
.retain(&(), move |callback| (callback)(cx));
|
||||
|
@ -428,11 +424,6 @@ impl App {
|
|||
self.keyboard_layout.as_ref()
|
||||
}
|
||||
|
||||
/// Get the current keyboard mapper.
|
||||
pub fn keyboard_mapper(&self) -> &Rc<dyn PlatformKeyboardMapper> {
|
||||
&self.keyboard_mapper
|
||||
}
|
||||
|
||||
/// Invokes a handler when the current keyboard layout changes
|
||||
pub fn on_keyboard_layout_change<F>(&self, mut callback: F) -> Subscription
|
||||
where
|
||||
|
|
|
@ -4,7 +4,7 @@ mod context;
|
|||
pub use binding::*;
|
||||
pub use context::*;
|
||||
|
||||
use crate::{Action, AsKeystroke, Keystroke, is_no_action};
|
||||
use crate::{Action, Keystroke, is_no_action};
|
||||
use collections::{HashMap, HashSet};
|
||||
use smallvec::SmallVec;
|
||||
use std::any::TypeId;
|
||||
|
@ -141,7 +141,7 @@ impl Keymap {
|
|||
/// only.
|
||||
pub fn bindings_for_input(
|
||||
&self,
|
||||
input: &[impl AsKeystroke],
|
||||
input: &[Keystroke],
|
||||
context_stack: &[KeyContext],
|
||||
) -> (SmallVec<[KeyBinding; 1]>, bool) {
|
||||
let mut matched_bindings = SmallVec::<[(usize, BindingIndex, &KeyBinding); 1]>::new();
|
||||
|
@ -192,6 +192,7 @@ impl Keymap {
|
|||
|
||||
(bindings, !pending.is_empty())
|
||||
}
|
||||
|
||||
/// Check if the given binding is enabled, given a certain key context.
|
||||
/// Returns the deepest depth at which the binding matches, or None if it doesn't match.
|
||||
fn binding_enabled(&self, binding: &KeyBinding, contexts: &[KeyContext]) -> Option<usize> {
|
||||
|
@ -638,7 +639,7 @@ mod tests {
|
|||
fn assert_bindings(keymap: &Keymap, action: &dyn Action, expected: &[&str]) {
|
||||
let actual = keymap
|
||||
.bindings_for_action(action)
|
||||
.map(|binding| binding.keystrokes[0].inner.unparse())
|
||||
.map(|binding| binding.keystrokes[0].unparse())
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(actual, expected, "{:?}", action);
|
||||
}
|
||||
|
|
|
@ -1,15 +1,14 @@
|
|||
use std::rc::Rc;
|
||||
|
||||
use crate::{
|
||||
Action, AsKeystroke, DummyKeyboardMapper, InvalidKeystrokeError, KeyBindingContextPredicate,
|
||||
KeybindingKeystroke, Keystroke, PlatformKeyboardMapper, SharedString,
|
||||
};
|
||||
use collections::HashMap;
|
||||
|
||||
use crate::{Action, InvalidKeystrokeError, KeyBindingContextPredicate, Keystroke, SharedString};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
/// A keybinding and its associated metadata, from the keymap.
|
||||
pub struct KeyBinding {
|
||||
pub(crate) action: Box<dyn Action>,
|
||||
pub(crate) keystrokes: SmallVec<[KeybindingKeystroke; 2]>,
|
||||
pub(crate) keystrokes: SmallVec<[Keystroke; 2]>,
|
||||
pub(crate) context_predicate: Option<Rc<KeyBindingContextPredicate>>,
|
||||
pub(crate) meta: Option<KeyBindingMetaIndex>,
|
||||
/// The json input string used when building the keybinding, if any
|
||||
|
@ -33,15 +32,7 @@ impl KeyBinding {
|
|||
pub fn new<A: Action>(keystrokes: &str, action: A, context: Option<&str>) -> Self {
|
||||
let context_predicate =
|
||||
context.map(|context| KeyBindingContextPredicate::parse(context).unwrap().into());
|
||||
Self::load(
|
||||
keystrokes,
|
||||
Box::new(action),
|
||||
context_predicate,
|
||||
false,
|
||||
None,
|
||||
&DummyKeyboardMapper,
|
||||
)
|
||||
.unwrap()
|
||||
Self::load(keystrokes, Box::new(action), context_predicate, None, None).unwrap()
|
||||
}
|
||||
|
||||
/// Load a keybinding from the given raw data.
|
||||
|
@ -49,22 +40,24 @@ impl KeyBinding {
|
|||
keystrokes: &str,
|
||||
action: Box<dyn Action>,
|
||||
context_predicate: Option<Rc<KeyBindingContextPredicate>>,
|
||||
use_key_equivalents: bool,
|
||||
key_equivalents: Option<&HashMap<char, char>>,
|
||||
action_input: Option<SharedString>,
|
||||
keyboard_mapper: &dyn PlatformKeyboardMapper,
|
||||
) -> std::result::Result<Self, InvalidKeystrokeError> {
|
||||
let keystrokes: SmallVec<[KeybindingKeystroke; 2]> = keystrokes
|
||||
let mut keystrokes: SmallVec<[Keystroke; 2]> = keystrokes
|
||||
.split_whitespace()
|
||||
.map(|source| {
|
||||
let keystroke = Keystroke::parse(source)?;
|
||||
Ok(KeybindingKeystroke::new(
|
||||
keystroke,
|
||||
use_key_equivalents,
|
||||
keyboard_mapper,
|
||||
))
|
||||
})
|
||||
.map(Keystroke::parse)
|
||||
.collect::<std::result::Result<_, _>>()?;
|
||||
|
||||
if let Some(equivalents) = key_equivalents {
|
||||
for keystroke in keystrokes.iter_mut() {
|
||||
if keystroke.key.chars().count() == 1
|
||||
&& let Some(key) = equivalents.get(&keystroke.key.chars().next().unwrap())
|
||||
{
|
||||
keystroke.key = key.to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
keystrokes,
|
||||
action,
|
||||
|
@ -86,13 +79,13 @@ impl KeyBinding {
|
|||
}
|
||||
|
||||
/// Check if the given keystrokes match this binding.
|
||||
pub fn match_keystrokes(&self, typed: &[impl AsKeystroke]) -> Option<bool> {
|
||||
pub fn match_keystrokes(&self, typed: &[Keystroke]) -> Option<bool> {
|
||||
if self.keystrokes.len() < typed.len() {
|
||||
return None;
|
||||
}
|
||||
|
||||
for (target, typed) in self.keystrokes.iter().zip(typed.iter()) {
|
||||
if !typed.as_keystroke().should_match(target) {
|
||||
if !typed.should_match(target) {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
@ -101,7 +94,7 @@ impl KeyBinding {
|
|||
}
|
||||
|
||||
/// Get the keystrokes associated with this binding
|
||||
pub fn keystrokes(&self) -> &[KeybindingKeystroke] {
|
||||
pub fn keystrokes(&self) -> &[Keystroke] {
|
||||
self.keystrokes.as_slice()
|
||||
}
|
||||
|
||||
|
|
|
@ -231,6 +231,7 @@ pub(crate) trait Platform: 'static {
|
|||
|
||||
fn on_quit(&self, callback: Box<dyn FnMut()>);
|
||||
fn on_reopen(&self, callback: Box<dyn FnMut()>);
|
||||
fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>);
|
||||
|
||||
fn set_menus(&self, menus: Vec<Menu>, keymap: &Keymap);
|
||||
fn get_menus(&self) -> Option<Vec<OwnedMenu>> {
|
||||
|
@ -250,6 +251,7 @@ pub(crate) trait Platform: 'static {
|
|||
fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>);
|
||||
fn on_will_open_app_menu(&self, callback: Box<dyn FnMut()>);
|
||||
fn on_validate_app_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>);
|
||||
fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout>;
|
||||
|
||||
fn compositor_name(&self) -> &'static str {
|
||||
""
|
||||
|
@ -270,10 +272,6 @@ pub(crate) trait Platform: 'static {
|
|||
fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task<Result<()>>;
|
||||
fn read_credentials(&self, url: &str) -> Task<Result<Option<(String, Vec<u8>)>>>;
|
||||
fn delete_credentials(&self, url: &str) -> Task<Result<()>>;
|
||||
|
||||
fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout>;
|
||||
fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper>;
|
||||
fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>);
|
||||
}
|
||||
|
||||
/// A handle to a platform's display, e.g. a monitor or laptop screen.
|
||||
|
|
|
@ -1,7 +1,3 @@
|
|||
use collections::HashMap;
|
||||
|
||||
use crate::{KeybindingKeystroke, Keystroke};
|
||||
|
||||
/// A trait for platform-specific keyboard layouts
|
||||
pub trait PlatformKeyboardLayout {
|
||||
/// Get the keyboard layout ID, which should be unique to the layout
|
||||
|
@ -9,33 +5,3 @@ pub trait PlatformKeyboardLayout {
|
|||
/// Get the keyboard layout display name
|
||||
fn name(&self) -> &str;
|
||||
}
|
||||
|
||||
/// A trait for platform-specific keyboard mappings
|
||||
pub trait PlatformKeyboardMapper {
|
||||
/// Map a key equivalent to its platform-specific representation
|
||||
fn map_key_equivalent(
|
||||
&self,
|
||||
keystroke: Keystroke,
|
||||
use_key_equivalents: bool,
|
||||
) -> KeybindingKeystroke;
|
||||
/// Get the key equivalents for the current keyboard layout,
|
||||
/// only used on macOS
|
||||
fn get_key_equivalents(&self) -> Option<&HashMap<char, char>>;
|
||||
}
|
||||
|
||||
/// A dummy implementation of the platform keyboard mapper
|
||||
pub struct DummyKeyboardMapper;
|
||||
|
||||
impl PlatformKeyboardMapper for DummyKeyboardMapper {
|
||||
fn map_key_equivalent(
|
||||
&self,
|
||||
keystroke: Keystroke,
|
||||
_use_key_equivalents: bool,
|
||||
) -> KeybindingKeystroke {
|
||||
KeybindingKeystroke::from_keystroke(keystroke)
|
||||
}
|
||||
|
||||
fn get_key_equivalents(&self) -> Option<&HashMap<char, char>> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,14 +5,6 @@ use std::{
|
|||
fmt::{Display, Write},
|
||||
};
|
||||
|
||||
use crate::PlatformKeyboardMapper;
|
||||
|
||||
/// This is a helper trait so that we can simplify the implementation of some functions
|
||||
pub trait AsKeystroke {
|
||||
/// Returns the GPUI representation of the keystroke.
|
||||
fn as_keystroke(&self) -> &Keystroke;
|
||||
}
|
||||
|
||||
/// A keystroke and associated metadata generated by the platform
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Default, Deserialize, Hash)]
|
||||
pub struct Keystroke {
|
||||
|
@ -32,17 +24,6 @@ pub struct Keystroke {
|
|||
pub key_char: Option<String>,
|
||||
}
|
||||
|
||||
/// Represents a keystroke that can be used in keybindings and displayed to the user.
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct KeybindingKeystroke {
|
||||
/// The GPUI representation of the keystroke.
|
||||
pub inner: Keystroke,
|
||||
/// The modifiers to display.
|
||||
pub display_modifiers: Modifiers,
|
||||
/// The key to display.
|
||||
pub display_key: String,
|
||||
}
|
||||
|
||||
/// Error type for `Keystroke::parse`. This is used instead of `anyhow::Error` so that Zed can use
|
||||
/// markdown to display it.
|
||||
#[derive(Debug)]
|
||||
|
@ -77,7 +58,7 @@ impl Keystroke {
|
|||
///
|
||||
/// This method assumes that `self` was typed and `target' is in the keymap, and checks
|
||||
/// both possibilities for self against the target.
|
||||
pub fn should_match(&self, target: &KeybindingKeystroke) -> bool {
|
||||
pub fn should_match(&self, target: &Keystroke) -> bool {
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
if let Some(key_char) = self
|
||||
.key_char
|
||||
|
@ -90,7 +71,7 @@ impl Keystroke {
|
|||
..Default::default()
|
||||
};
|
||||
|
||||
if &target.inner.key == key_char && target.inner.modifiers == ime_modifiers {
|
||||
if &target.key == key_char && target.modifiers == ime_modifiers {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -102,12 +83,12 @@ impl Keystroke {
|
|||
.filter(|key_char| key_char != &&self.key)
|
||||
{
|
||||
// On Windows, if key_char is set, then the typed keystroke produced the key_char
|
||||
if &target.inner.key == key_char && target.inner.modifiers == Modifiers::none() {
|
||||
if &target.key == key_char && target.modifiers == Modifiers::none() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
target.inner.modifiers == self.modifiers && target.inner.key == self.key
|
||||
target.modifiers == self.modifiers && target.key == self.key
|
||||
}
|
||||
|
||||
/// key syntax is:
|
||||
|
@ -219,7 +200,31 @@ impl Keystroke {
|
|||
|
||||
/// Produces a representation of this key that Parse can understand.
|
||||
pub fn unparse(&self) -> String {
|
||||
unparse(&self.modifiers, &self.key)
|
||||
let mut str = String::new();
|
||||
if self.modifiers.function {
|
||||
str.push_str("fn-");
|
||||
}
|
||||
if self.modifiers.control {
|
||||
str.push_str("ctrl-");
|
||||
}
|
||||
if self.modifiers.alt {
|
||||
str.push_str("alt-");
|
||||
}
|
||||
if self.modifiers.platform {
|
||||
#[cfg(target_os = "macos")]
|
||||
str.push_str("cmd-");
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
str.push_str("super-");
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
str.push_str("win-");
|
||||
}
|
||||
if self.modifiers.shift {
|
||||
str.push_str("shift-");
|
||||
}
|
||||
str.push_str(&self.key);
|
||||
str
|
||||
}
|
||||
|
||||
/// Returns true if this keystroke left
|
||||
|
@ -261,32 +266,6 @@ impl Keystroke {
|
|||
}
|
||||
}
|
||||
|
||||
impl KeybindingKeystroke {
|
||||
/// Create a new keybinding keystroke from the given keystroke
|
||||
pub fn new(
|
||||
inner: Keystroke,
|
||||
use_key_equivalents: bool,
|
||||
keyboard_mapper: &dyn PlatformKeyboardMapper,
|
||||
) -> Self {
|
||||
keyboard_mapper.map_key_equivalent(inner, use_key_equivalents)
|
||||
}
|
||||
|
||||
pub(crate) fn from_keystroke(keystroke: Keystroke) -> Self {
|
||||
let key = keystroke.key.clone();
|
||||
let modifiers = keystroke.modifiers;
|
||||
KeybindingKeystroke {
|
||||
inner: keystroke,
|
||||
display_modifiers: modifiers,
|
||||
display_key: key,
|
||||
}
|
||||
}
|
||||
|
||||
/// Produces a representation of this key that Parse can understand.
|
||||
pub fn unparse(&self) -> String {
|
||||
unparse(&self.display_modifiers, &self.display_key)
|
||||
}
|
||||
}
|
||||
|
||||
fn is_printable_key(key: &str) -> bool {
|
||||
!matches!(
|
||||
key,
|
||||
|
@ -343,15 +322,65 @@ fn is_printable_key(key: &str) -> bool {
|
|||
|
||||
impl std::fmt::Display for Keystroke {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
display_modifiers(&self.modifiers, f)?;
|
||||
display_key(&self.key, f)
|
||||
}
|
||||
}
|
||||
if self.modifiers.control {
|
||||
#[cfg(target_os = "macos")]
|
||||
f.write_char('^')?;
|
||||
|
||||
impl std::fmt::Display for KeybindingKeystroke {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
display_modifiers(&self.display_modifiers, f)?;
|
||||
display_key(&self.display_key, f)
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
write!(f, "ctrl-")?;
|
||||
}
|
||||
if self.modifiers.alt {
|
||||
#[cfg(target_os = "macos")]
|
||||
f.write_char('⌥')?;
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
write!(f, "alt-")?;
|
||||
}
|
||||
if self.modifiers.platform {
|
||||
#[cfg(target_os = "macos")]
|
||||
f.write_char('⌘')?;
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
f.write_char('❖')?;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
f.write_char('⊞')?;
|
||||
}
|
||||
if self.modifiers.shift {
|
||||
#[cfg(target_os = "macos")]
|
||||
f.write_char('⇧')?;
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
write!(f, "shift-")?;
|
||||
}
|
||||
let key = match self.key.as_str() {
|
||||
#[cfg(target_os = "macos")]
|
||||
"backspace" => '⌫',
|
||||
#[cfg(target_os = "macos")]
|
||||
"up" => '↑',
|
||||
#[cfg(target_os = "macos")]
|
||||
"down" => '↓',
|
||||
#[cfg(target_os = "macos")]
|
||||
"left" => '←',
|
||||
#[cfg(target_os = "macos")]
|
||||
"right" => '→',
|
||||
#[cfg(target_os = "macos")]
|
||||
"tab" => '⇥',
|
||||
#[cfg(target_os = "macos")]
|
||||
"escape" => '⎋',
|
||||
#[cfg(target_os = "macos")]
|
||||
"shift" => '⇧',
|
||||
#[cfg(target_os = "macos")]
|
||||
"control" => '⌃',
|
||||
#[cfg(target_os = "macos")]
|
||||
"alt" => '⌥',
|
||||
#[cfg(target_os = "macos")]
|
||||
"platform" => '⌘',
|
||||
|
||||
key if key.len() == 1 => key.chars().next().unwrap().to_ascii_uppercase(),
|
||||
key => return f.write_str(key),
|
||||
};
|
||||
f.write_char(key)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -571,110 +600,3 @@ pub struct Capslock {
|
|||
#[serde(default)]
|
||||
pub on: bool,
|
||||
}
|
||||
|
||||
impl AsKeystroke for Keystroke {
|
||||
fn as_keystroke(&self) -> &Keystroke {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl AsKeystroke for KeybindingKeystroke {
|
||||
fn as_keystroke(&self) -> &Keystroke {
|
||||
&self.inner
|
||||
}
|
||||
}
|
||||
|
||||
fn display_modifiers(modifiers: &Modifiers, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
if modifiers.control {
|
||||
#[cfg(target_os = "macos")]
|
||||
f.write_char('^')?;
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
write!(f, "ctrl-")?;
|
||||
}
|
||||
if modifiers.alt {
|
||||
#[cfg(target_os = "macos")]
|
||||
f.write_char('⌥')?;
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
write!(f, "alt-")?;
|
||||
}
|
||||
if modifiers.platform {
|
||||
#[cfg(target_os = "macos")]
|
||||
f.write_char('⌘')?;
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
f.write_char('❖')?;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
f.write_char('⊞')?;
|
||||
}
|
||||
if modifiers.shift {
|
||||
#[cfg(target_os = "macos")]
|
||||
f.write_char('⇧')?;
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
write!(f, "shift-")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn display_key(key: &str, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let key = match key {
|
||||
#[cfg(target_os = "macos")]
|
||||
"backspace" => '⌫',
|
||||
#[cfg(target_os = "macos")]
|
||||
"up" => '↑',
|
||||
#[cfg(target_os = "macos")]
|
||||
"down" => '↓',
|
||||
#[cfg(target_os = "macos")]
|
||||
"left" => '←',
|
||||
#[cfg(target_os = "macos")]
|
||||
"right" => '→',
|
||||
#[cfg(target_os = "macos")]
|
||||
"tab" => '⇥',
|
||||
#[cfg(target_os = "macos")]
|
||||
"escape" => '⎋',
|
||||
#[cfg(target_os = "macos")]
|
||||
"shift" => '⇧',
|
||||
#[cfg(target_os = "macos")]
|
||||
"control" => '⌃',
|
||||
#[cfg(target_os = "macos")]
|
||||
"alt" => '⌥',
|
||||
#[cfg(target_os = "macos")]
|
||||
"platform" => '⌘',
|
||||
|
||||
key if key.len() == 1 => key.chars().next().unwrap().to_ascii_uppercase(),
|
||||
key => return f.write_str(key),
|
||||
};
|
||||
f.write_char(key)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn unparse(modifiers: &Modifiers, key: &str) -> String {
|
||||
let mut result = String::new();
|
||||
if modifiers.function {
|
||||
result.push_str("fn-");
|
||||
}
|
||||
if modifiers.control {
|
||||
result.push_str("ctrl-");
|
||||
}
|
||||
if modifiers.alt {
|
||||
result.push_str("alt-");
|
||||
}
|
||||
if modifiers.platform {
|
||||
#[cfg(target_os = "macos")]
|
||||
result.push_str("cmd-");
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
result.push_str("super-");
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
result.push_str("win-");
|
||||
}
|
||||
if modifiers.shift {
|
||||
result.push_str("shift-");
|
||||
}
|
||||
result.push_str(&key);
|
||||
result
|
||||
}
|
||||
|
|
|
@ -25,8 +25,8 @@ use xkbcommon::xkb::{self, Keycode, Keysym, State};
|
|||
use crate::{
|
||||
Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId,
|
||||
ForegroundExecutor, Keymap, LinuxDispatcher, Menu, MenuItem, OwnedMenu, PathPromptOptions,
|
||||
Pixels, Platform, PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper,
|
||||
PlatformTextSystem, PlatformWindow, Point, Result, Task, WindowAppearance, WindowParams, px,
|
||||
Pixels, Platform, PlatformDisplay, PlatformKeyboardLayout, PlatformTextSystem, PlatformWindow,
|
||||
Point, Result, Task, WindowAppearance, WindowParams, px,
|
||||
};
|
||||
|
||||
#[cfg(any(feature = "wayland", feature = "x11"))]
|
||||
|
@ -144,10 +144,6 @@ impl<P: LinuxClient + 'static> Platform for P {
|
|||
self.keyboard_layout()
|
||||
}
|
||||
|
||||
fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper> {
|
||||
Rc::new(crate::DummyKeyboardMapper)
|
||||
}
|
||||
|
||||
fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>) {
|
||||
self.with_common(|common| common.callbacks.keyboard_layout_change = Some(callback));
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,5 +1,5 @@
|
|||
use super::{
|
||||
BoolExt, MacKeyboardLayout, MacKeyboardMapper,
|
||||
BoolExt, MacKeyboardLayout,
|
||||
attributed_string::{NSAttributedString, NSMutableAttributedString},
|
||||
events::key_to_native,
|
||||
renderer,
|
||||
|
@ -8,9 +8,8 @@ use crate::{
|
|||
Action, AnyWindowHandle, BackgroundExecutor, ClipboardEntry, ClipboardItem, ClipboardString,
|
||||
CursorStyle, ForegroundExecutor, Image, ImageFormat, KeyContext, Keymap, MacDispatcher,
|
||||
MacDisplay, MacWindow, Menu, MenuItem, OsMenu, OwnedMenu, PathPromptOptions, Platform,
|
||||
PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem,
|
||||
PlatformWindow, Result, SemanticVersion, SystemMenuType, Task, WindowAppearance, WindowParams,
|
||||
hash,
|
||||
PlatformDisplay, PlatformKeyboardLayout, PlatformTextSystem, PlatformWindow, Result,
|
||||
SemanticVersion, SystemMenuType, Task, WindowAppearance, WindowParams, hash,
|
||||
};
|
||||
use anyhow::{Context as _, anyhow};
|
||||
use block::ConcreteBlock;
|
||||
|
@ -172,7 +171,6 @@ pub(crate) struct MacPlatformState {
|
|||
finish_launching: Option<Box<dyn FnOnce()>>,
|
||||
dock_menu: Option<id>,
|
||||
menus: Option<Vec<OwnedMenu>>,
|
||||
keyboard_mapper: Rc<MacKeyboardMapper>,
|
||||
}
|
||||
|
||||
impl Default for MacPlatform {
|
||||
|
@ -191,9 +189,6 @@ impl MacPlatform {
|
|||
#[cfg(not(feature = "font-kit"))]
|
||||
let text_system = Arc::new(crate::NoopTextSystem::new());
|
||||
|
||||
let keyboard_layout = MacKeyboardLayout::new();
|
||||
let keyboard_mapper = Rc::new(MacKeyboardMapper::new(keyboard_layout.id()));
|
||||
|
||||
Self(Mutex::new(MacPlatformState {
|
||||
headless,
|
||||
text_system,
|
||||
|
@ -214,7 +209,6 @@ impl MacPlatform {
|
|||
dock_menu: None,
|
||||
on_keyboard_layout_change: None,
|
||||
menus: None,
|
||||
keyboard_mapper,
|
||||
}))
|
||||
}
|
||||
|
||||
|
@ -354,19 +348,19 @@ impl MacPlatform {
|
|||
let mut mask = NSEventModifierFlags::empty();
|
||||
for (modifier, flag) in &[
|
||||
(
|
||||
keystroke.display_modifiers.platform,
|
||||
keystroke.modifiers.platform,
|
||||
NSEventModifierFlags::NSCommandKeyMask,
|
||||
),
|
||||
(
|
||||
keystroke.display_modifiers.control,
|
||||
keystroke.modifiers.control,
|
||||
NSEventModifierFlags::NSControlKeyMask,
|
||||
),
|
||||
(
|
||||
keystroke.display_modifiers.alt,
|
||||
keystroke.modifiers.alt,
|
||||
NSEventModifierFlags::NSAlternateKeyMask,
|
||||
),
|
||||
(
|
||||
keystroke.display_modifiers.shift,
|
||||
keystroke.modifiers.shift,
|
||||
NSEventModifierFlags::NSShiftKeyMask,
|
||||
),
|
||||
] {
|
||||
|
@ -379,7 +373,7 @@ impl MacPlatform {
|
|||
.initWithTitle_action_keyEquivalent_(
|
||||
ns_string(name),
|
||||
selector,
|
||||
ns_string(key_to_native(&keystroke.display_key).as_ref()),
|
||||
ns_string(key_to_native(&keystroke.key).as_ref()),
|
||||
)
|
||||
.autorelease();
|
||||
if Self::os_version() >= SemanticVersion::new(12, 0, 0) {
|
||||
|
@ -888,10 +882,6 @@ impl Platform for MacPlatform {
|
|||
Box::new(MacKeyboardLayout::new())
|
||||
}
|
||||
|
||||
fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper> {
|
||||
self.0.lock().keyboard_mapper.clone()
|
||||
}
|
||||
|
||||
fn app_path(&self) -> Result<PathBuf> {
|
||||
unsafe {
|
||||
let bundle: id = NSBundle::mainBundle();
|
||||
|
@ -1403,8 +1393,6 @@ extern "C" fn will_terminate(this: &mut Object, _: Sel, _: id) {
|
|||
extern "C" fn on_keyboard_layout_change(this: &mut Object, _: Sel, _: id) {
|
||||
let platform = unsafe { get_mac_platform(this) };
|
||||
let mut lock = platform.0.lock();
|
||||
let keyboard_layout = MacKeyboardLayout::new();
|
||||
lock.keyboard_mapper = Rc::new(MacKeyboardMapper::new(keyboard_layout.id()));
|
||||
if let Some(mut callback) = lock.on_keyboard_layout_change.take() {
|
||||
drop(lock);
|
||||
callback();
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
use crate::{
|
||||
AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DevicePixels,
|
||||
DummyKeyboardMapper, ForegroundExecutor, Keymap, NoopTextSystem, Platform, PlatformDisplay,
|
||||
PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem, PromptButton,
|
||||
ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream, SourceMetadata, Task,
|
||||
TestDisplay, TestWindow, WindowAppearance, WindowParams, size,
|
||||
ForegroundExecutor, Keymap, NoopTextSystem, Platform, PlatformDisplay, PlatformKeyboardLayout,
|
||||
PlatformTextSystem, PromptButton, ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream,
|
||||
SourceMetadata, Task, TestDisplay, TestWindow, WindowAppearance, WindowParams, size,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use collections::VecDeque;
|
||||
|
@ -238,10 +237,6 @@ impl Platform for TestPlatform {
|
|||
Box::new(TestKeyboardLayout)
|
||||
}
|
||||
|
||||
fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper> {
|
||||
Rc::new(DummyKeyboardMapper)
|
||||
}
|
||||
|
||||
fn on_keyboard_layout_change(&self, _: Box<dyn FnMut()>) {}
|
||||
|
||||
fn run(&self, _on_finish_launching: Box<dyn FnOnce()>) {
|
||||
|
|
|
@ -1,31 +1,22 @@
|
|||
use anyhow::Result;
|
||||
use collections::HashMap;
|
||||
use windows::Win32::UI::{
|
||||
Input::KeyboardAndMouse::{
|
||||
GetKeyboardLayoutNameW, MAPVK_VK_TO_CHAR, MAPVK_VK_TO_VSC, MapVirtualKeyW, ToUnicode,
|
||||
VIRTUAL_KEY, VK_0, VK_1, VK_2, VK_3, VK_4, VK_5, VK_6, VK_7, VK_8, VK_9, VK_ABNT_C1,
|
||||
VK_CONTROL, VK_MENU, VK_OEM_1, VK_OEM_2, VK_OEM_3, VK_OEM_4, VK_OEM_5, VK_OEM_6, VK_OEM_7,
|
||||
VK_OEM_8, VK_OEM_102, VK_OEM_COMMA, VK_OEM_MINUS, VK_OEM_PERIOD, VK_OEM_PLUS, VK_SHIFT,
|
||||
GetKeyboardLayoutNameW, MAPVK_VK_TO_CHAR, MapVirtualKeyW, ToUnicode, VIRTUAL_KEY, VK_0,
|
||||
VK_1, VK_2, VK_3, VK_4, VK_5, VK_6, VK_7, VK_8, VK_9, VK_ABNT_C1, VK_CONTROL, VK_MENU,
|
||||
VK_OEM_1, VK_OEM_2, VK_OEM_3, VK_OEM_4, VK_OEM_5, VK_OEM_6, VK_OEM_7, VK_OEM_8, VK_OEM_102,
|
||||
VK_OEM_COMMA, VK_OEM_MINUS, VK_OEM_PERIOD, VK_OEM_PLUS, VK_SHIFT,
|
||||
},
|
||||
WindowsAndMessaging::KL_NAMELENGTH,
|
||||
};
|
||||
use windows_core::HSTRING;
|
||||
|
||||
use crate::{
|
||||
KeybindingKeystroke, Keystroke, Modifiers, PlatformKeyboardLayout, PlatformKeyboardMapper,
|
||||
};
|
||||
use crate::{Modifiers, PlatformKeyboardLayout};
|
||||
|
||||
pub(crate) struct WindowsKeyboardLayout {
|
||||
id: String,
|
||||
name: String,
|
||||
}
|
||||
|
||||
pub(crate) struct WindowsKeyboardMapper {
|
||||
key_to_vkey: HashMap<String, (u16, bool)>,
|
||||
vkey_to_key: HashMap<u16, String>,
|
||||
vkey_to_shifted: HashMap<u16, String>,
|
||||
}
|
||||
|
||||
impl PlatformKeyboardLayout for WindowsKeyboardLayout {
|
||||
fn id(&self) -> &str {
|
||||
&self.id
|
||||
|
@ -36,65 +27,6 @@ impl PlatformKeyboardLayout for WindowsKeyboardLayout {
|
|||
}
|
||||
}
|
||||
|
||||
impl PlatformKeyboardMapper for WindowsKeyboardMapper {
|
||||
fn map_key_equivalent(
|
||||
&self,
|
||||
mut keystroke: Keystroke,
|
||||
use_key_equivalents: bool,
|
||||
) -> KeybindingKeystroke {
|
||||
let Some((vkey, shifted_key)) = self.get_vkey_from_key(&keystroke.key, use_key_equivalents)
|
||||
else {
|
||||
return KeybindingKeystroke::from_keystroke(keystroke);
|
||||
};
|
||||
if shifted_key && keystroke.modifiers.shift {
|
||||
log::warn!(
|
||||
"Keystroke '{}' has both shift and a shifted key, this is likely a bug",
|
||||
keystroke.key
|
||||
);
|
||||
}
|
||||
|
||||
let shift = shifted_key || keystroke.modifiers.shift;
|
||||
keystroke.modifiers.shift = false;
|
||||
|
||||
let Some(key) = self.vkey_to_key.get(&vkey).cloned() else {
|
||||
log::error!(
|
||||
"Failed to map key equivalent '{:?}' to a valid key",
|
||||
keystroke
|
||||
);
|
||||
return KeybindingKeystroke::from_keystroke(keystroke);
|
||||
};
|
||||
|
||||
keystroke.key = if shift {
|
||||
let Some(shifted_key) = self.vkey_to_shifted.get(&vkey).cloned() else {
|
||||
log::error!(
|
||||
"Failed to map keystroke {:?} with virtual key '{:?}' to a shifted key",
|
||||
keystroke,
|
||||
vkey
|
||||
);
|
||||
return KeybindingKeystroke::from_keystroke(keystroke);
|
||||
};
|
||||
shifted_key
|
||||
} else {
|
||||
key.clone()
|
||||
};
|
||||
|
||||
let modifiers = Modifiers {
|
||||
shift,
|
||||
..keystroke.modifiers
|
||||
};
|
||||
|
||||
KeybindingKeystroke {
|
||||
inner: keystroke,
|
||||
display_modifiers: modifiers,
|
||||
display_key: key,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_key_equivalents(&self) -> Option<&HashMap<char, char>> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl WindowsKeyboardLayout {
|
||||
pub(crate) fn new() -> Result<Self> {
|
||||
let mut buffer = [0u16; KL_NAMELENGTH as usize];
|
||||
|
@ -116,41 +48,6 @@ impl WindowsKeyboardLayout {
|
|||
}
|
||||
}
|
||||
|
||||
impl WindowsKeyboardMapper {
|
||||
pub(crate) fn new() -> Self {
|
||||
let mut key_to_vkey = HashMap::default();
|
||||
let mut vkey_to_key = HashMap::default();
|
||||
let mut vkey_to_shifted = HashMap::default();
|
||||
for vkey in CANDIDATE_VKEYS {
|
||||
if let Some(key) = get_key_from_vkey(*vkey) {
|
||||
key_to_vkey.insert(key.clone(), (vkey.0, false));
|
||||
vkey_to_key.insert(vkey.0, key);
|
||||
}
|
||||
let scan_code = unsafe { MapVirtualKeyW(vkey.0 as u32, MAPVK_VK_TO_VSC) };
|
||||
if scan_code == 0 {
|
||||
continue;
|
||||
}
|
||||
if let Some(shifted_key) = get_shifted_key(*vkey, scan_code) {
|
||||
key_to_vkey.insert(shifted_key.clone(), (vkey.0, true));
|
||||
vkey_to_shifted.insert(vkey.0, shifted_key);
|
||||
}
|
||||
}
|
||||
Self {
|
||||
key_to_vkey,
|
||||
vkey_to_key,
|
||||
vkey_to_shifted,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_vkey_from_key(&self, key: &str, use_key_equivalents: bool) -> Option<(u16, bool)> {
|
||||
if use_key_equivalents {
|
||||
get_vkey_from_key_with_us_layout(key)
|
||||
} else {
|
||||
self.key_to_vkey.get(key).cloned()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_keystroke_key(
|
||||
vkey: VIRTUAL_KEY,
|
||||
scan_code: u32,
|
||||
|
@ -243,134 +140,3 @@ pub(crate) fn generate_key_char(
|
|||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_vkey_from_key_with_us_layout(key: &str) -> Option<(u16, bool)> {
|
||||
match key {
|
||||
// ` => VK_OEM_3
|
||||
"`" => Some((VK_OEM_3.0, false)),
|
||||
"~" => Some((VK_OEM_3.0, true)),
|
||||
"1" => Some((VK_1.0, false)),
|
||||
"!" => Some((VK_1.0, true)),
|
||||
"2" => Some((VK_2.0, false)),
|
||||
"@" => Some((VK_2.0, true)),
|
||||
"3" => Some((VK_3.0, false)),
|
||||
"#" => Some((VK_3.0, true)),
|
||||
"4" => Some((VK_4.0, false)),
|
||||
"$" => Some((VK_4.0, true)),
|
||||
"5" => Some((VK_5.0, false)),
|
||||
"%" => Some((VK_5.0, true)),
|
||||
"6" => Some((VK_6.0, false)),
|
||||
"^" => Some((VK_6.0, true)),
|
||||
"7" => Some((VK_7.0, false)),
|
||||
"&" => Some((VK_7.0, true)),
|
||||
"8" => Some((VK_8.0, false)),
|
||||
"*" => Some((VK_8.0, true)),
|
||||
"9" => Some((VK_9.0, false)),
|
||||
"(" => Some((VK_9.0, true)),
|
||||
"0" => Some((VK_0.0, false)),
|
||||
")" => Some((VK_0.0, true)),
|
||||
"-" => Some((VK_OEM_MINUS.0, false)),
|
||||
"_" => Some((VK_OEM_MINUS.0, true)),
|
||||
"=" => Some((VK_OEM_PLUS.0, false)),
|
||||
"+" => Some((VK_OEM_PLUS.0, true)),
|
||||
"[" => Some((VK_OEM_4.0, false)),
|
||||
"{" => Some((VK_OEM_4.0, true)),
|
||||
"]" => Some((VK_OEM_6.0, false)),
|
||||
"}" => Some((VK_OEM_6.0, true)),
|
||||
"\\" => Some((VK_OEM_5.0, false)),
|
||||
"|" => Some((VK_OEM_5.0, true)),
|
||||
";" => Some((VK_OEM_1.0, false)),
|
||||
":" => Some((VK_OEM_1.0, true)),
|
||||
"'" => Some((VK_OEM_7.0, false)),
|
||||
"\"" => Some((VK_OEM_7.0, true)),
|
||||
"," => Some((VK_OEM_COMMA.0, false)),
|
||||
"<" => Some((VK_OEM_COMMA.0, true)),
|
||||
"." => Some((VK_OEM_PERIOD.0, false)),
|
||||
">" => Some((VK_OEM_PERIOD.0, true)),
|
||||
"/" => Some((VK_OEM_2.0, false)),
|
||||
"?" => Some((VK_OEM_2.0, true)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
const CANDIDATE_VKEYS: &[VIRTUAL_KEY] = &[
|
||||
VK_OEM_3,
|
||||
VK_OEM_MINUS,
|
||||
VK_OEM_PLUS,
|
||||
VK_OEM_4,
|
||||
VK_OEM_5,
|
||||
VK_OEM_6,
|
||||
VK_OEM_1,
|
||||
VK_OEM_7,
|
||||
VK_OEM_COMMA,
|
||||
VK_OEM_PERIOD,
|
||||
VK_OEM_2,
|
||||
VK_OEM_102,
|
||||
VK_OEM_8,
|
||||
VK_ABNT_C1,
|
||||
VK_0,
|
||||
VK_1,
|
||||
VK_2,
|
||||
VK_3,
|
||||
VK_4,
|
||||
VK_5,
|
||||
VK_6,
|
||||
VK_7,
|
||||
VK_8,
|
||||
VK_9,
|
||||
];
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{Keystroke, Modifiers, PlatformKeyboardMapper, WindowsKeyboardMapper};
|
||||
|
||||
#[test]
|
||||
fn test_keyboard_mapper() {
|
||||
let mapper = WindowsKeyboardMapper::new();
|
||||
|
||||
// Normal case
|
||||
let keystroke = Keystroke {
|
||||
modifiers: Modifiers::control(),
|
||||
key: "a".to_string(),
|
||||
key_char: None,
|
||||
};
|
||||
let mapped = mapper.map_key_equivalent(keystroke.clone(), true);
|
||||
assert_eq!(mapped.inner, keystroke);
|
||||
assert_eq!(mapped.display_key, "a");
|
||||
assert_eq!(mapped.display_modifiers, Modifiers::control());
|
||||
|
||||
// Shifted case, ctrl-$
|
||||
let keystroke = Keystroke {
|
||||
modifiers: Modifiers::control(),
|
||||
key: "$".to_string(),
|
||||
key_char: None,
|
||||
};
|
||||
let mapped = mapper.map_key_equivalent(keystroke.clone(), true);
|
||||
assert_eq!(mapped.inner, keystroke);
|
||||
assert_eq!(mapped.display_key, "4");
|
||||
assert_eq!(mapped.display_modifiers, Modifiers::control_shift());
|
||||
|
||||
// Shifted case, but shift is true
|
||||
let keystroke = Keystroke {
|
||||
modifiers: Modifiers::control_shift(),
|
||||
key: "$".to_string(),
|
||||
key_char: None,
|
||||
};
|
||||
let mapped = mapper.map_key_equivalent(keystroke, true);
|
||||
assert_eq!(mapped.inner.modifiers, Modifiers::control());
|
||||
assert_eq!(mapped.display_key, "4");
|
||||
assert_eq!(mapped.display_modifiers, Modifiers::control_shift());
|
||||
|
||||
// Windows style
|
||||
let keystroke = Keystroke {
|
||||
modifiers: Modifiers::control_shift(),
|
||||
key: "4".to_string(),
|
||||
key_char: None,
|
||||
};
|
||||
let mapped = mapper.map_key_equivalent(keystroke, true);
|
||||
assert_eq!(mapped.inner.modifiers, Modifiers::control());
|
||||
assert_eq!(mapped.inner.key, "$");
|
||||
assert_eq!(mapped.display_key, "4");
|
||||
assert_eq!(mapped.display_modifiers, Modifiers::control_shift());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -351,10 +351,6 @@ impl Platform for WindowsPlatform {
|
|||
)
|
||||
}
|
||||
|
||||
fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper> {
|
||||
Rc::new(WindowsKeyboardMapper::new())
|
||||
}
|
||||
|
||||
fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>) {
|
||||
self.state.borrow_mut().callbacks.keyboard_layout_change = Some(callback);
|
||||
}
|
||||
|
|
|
@ -215,7 +215,6 @@ pub enum IconName {
|
|||
Tab,
|
||||
Terminal,
|
||||
TerminalAlt,
|
||||
TerminalGhost,
|
||||
TextSnippet,
|
||||
TextThread,
|
||||
Thread,
|
||||
|
|
|
@ -4,16 +4,12 @@ use crate::{
|
|||
LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
|
||||
LanguageModelRequest, LanguageModelToolChoice,
|
||||
};
|
||||
use anyhow::anyhow;
|
||||
use futures::{FutureExt, channel::mpsc, future::BoxFuture, stream::BoxStream};
|
||||
use gpui::{AnyView, App, AsyncApp, Entity, Task, Window};
|
||||
use http_client::Result;
|
||||
use parking_lot::Mutex;
|
||||
use smol::stream::StreamExt;
|
||||
use std::sync::{
|
||||
Arc,
|
||||
atomic::{AtomicBool, Ordering::SeqCst},
|
||||
};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct FakeLanguageModelProvider {
|
||||
|
@ -110,7 +106,6 @@ pub struct FakeLanguageModel {
|
|||
>,
|
||||
)>,
|
||||
>,
|
||||
forbid_requests: AtomicBool,
|
||||
}
|
||||
|
||||
impl Default for FakeLanguageModel {
|
||||
|
@ -119,20 +114,11 @@ impl Default for FakeLanguageModel {
|
|||
provider_id: LanguageModelProviderId::from("fake".to_string()),
|
||||
provider_name: LanguageModelProviderName::from("Fake".to_string()),
|
||||
current_completion_txs: Mutex::new(Vec::new()),
|
||||
forbid_requests: AtomicBool::new(false),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FakeLanguageModel {
|
||||
pub fn allow_requests(&self) {
|
||||
self.forbid_requests.store(false, SeqCst);
|
||||
}
|
||||
|
||||
pub fn forbid_requests(&self) {
|
||||
self.forbid_requests.store(true, SeqCst);
|
||||
}
|
||||
|
||||
pub fn pending_completions(&self) -> Vec<LanguageModelRequest> {
|
||||
self.current_completion_txs
|
||||
.lock()
|
||||
|
@ -265,18 +251,9 @@ impl LanguageModel for FakeLanguageModel {
|
|||
LanguageModelCompletionError,
|
||||
>,
|
||||
> {
|
||||
if self.forbid_requests.load(SeqCst) {
|
||||
async move {
|
||||
Err(LanguageModelCompletionError::Other(anyhow!(
|
||||
"requests are forbidden"
|
||||
)))
|
||||
}
|
||||
.boxed()
|
||||
} else {
|
||||
let (tx, rx) = mpsc::unbounded();
|
||||
self.current_completion_txs.lock().push((request, tx));
|
||||
async move { Ok(rx.boxed()) }.boxed()
|
||||
}
|
||||
let (tx, rx) = mpsc::unbounded();
|
||||
self.current_completion_txs.lock().push((request, tx));
|
||||
async move { Ok(rx.boxed()) }.boxed()
|
||||
}
|
||||
|
||||
fn as_fake(&self) -> &Self {
|
||||
|
|
|
@ -6,7 +6,6 @@ use collections::BTreeMap;
|
|||
use gpui::{App, Context, Entity, EventEmitter, Global, prelude::*};
|
||||
use std::{str::FromStr, sync::Arc};
|
||||
use thiserror::Error;
|
||||
use util::maybe;
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
let registry = cx.new(|_cx| LanguageModelRegistry::default());
|
||||
|
@ -42,7 +41,9 @@ impl std::fmt::Debug for ConfigurationError {
|
|||
#[derive(Default)]
|
||||
pub struct LanguageModelRegistry {
|
||||
default_model: Option<ConfiguredModel>,
|
||||
default_fast_model: Option<ConfiguredModel>,
|
||||
/// This model is automatically configured by a user's environment after
|
||||
/// authenticating all providers. It's only used when default_model is not available.
|
||||
environment_fallback_model: Option<ConfiguredModel>,
|
||||
inline_assistant_model: Option<ConfiguredModel>,
|
||||
commit_message_model: Option<ConfiguredModel>,
|
||||
thread_summary_model: Option<ConfiguredModel>,
|
||||
|
@ -98,9 +99,6 @@ impl ConfiguredModel {
|
|||
|
||||
pub enum Event {
|
||||
DefaultModelChanged,
|
||||
InlineAssistantModelChanged,
|
||||
CommitMessageModelChanged,
|
||||
ThreadSummaryModelChanged,
|
||||
ProviderStateChanged(LanguageModelProviderId),
|
||||
AddedProvider(LanguageModelProviderId),
|
||||
RemovedProvider(LanguageModelProviderId),
|
||||
|
@ -226,7 +224,7 @@ impl LanguageModelRegistry {
|
|||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let configured_model = model.and_then(|model| self.select_model(model, cx));
|
||||
self.set_inline_assistant_model(configured_model, cx);
|
||||
self.set_inline_assistant_model(configured_model);
|
||||
}
|
||||
|
||||
pub fn select_commit_message_model(
|
||||
|
@ -235,7 +233,7 @@ impl LanguageModelRegistry {
|
|||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let configured_model = model.and_then(|model| self.select_model(model, cx));
|
||||
self.set_commit_message_model(configured_model, cx);
|
||||
self.set_commit_message_model(configured_model);
|
||||
}
|
||||
|
||||
pub fn select_thread_summary_model(
|
||||
|
@ -244,7 +242,7 @@ impl LanguageModelRegistry {
|
|||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let configured_model = model.and_then(|model| self.select_model(model, cx));
|
||||
self.set_thread_summary_model(configured_model, cx);
|
||||
self.set_thread_summary_model(configured_model);
|
||||
}
|
||||
|
||||
/// Selects and sets the inline alternatives for language models based on
|
||||
|
@ -278,68 +276,60 @@ impl LanguageModelRegistry {
|
|||
}
|
||||
|
||||
pub fn set_default_model(&mut self, model: Option<ConfiguredModel>, cx: &mut Context<Self>) {
|
||||
match (self.default_model.as_ref(), model.as_ref()) {
|
||||
match (self.default_model(), model.as_ref()) {
|
||||
(Some(old), Some(new)) if old.is_same_as(new) => {}
|
||||
(None, None) => {}
|
||||
_ => cx.emit(Event::DefaultModelChanged),
|
||||
}
|
||||
self.default_fast_model = maybe!({
|
||||
let provider = &model.as_ref()?.provider;
|
||||
let fast_model = provider.default_fast_model(cx)?;
|
||||
Some(ConfiguredModel {
|
||||
provider: provider.clone(),
|
||||
model: fast_model,
|
||||
})
|
||||
});
|
||||
self.default_model = model;
|
||||
}
|
||||
|
||||
pub fn set_inline_assistant_model(
|
||||
pub fn set_environment_fallback_model(
|
||||
&mut self,
|
||||
model: Option<ConfiguredModel>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
match (self.inline_assistant_model.as_ref(), model.as_ref()) {
|
||||
(Some(old), Some(new)) if old.is_same_as(new) => {}
|
||||
(None, None) => {}
|
||||
_ => cx.emit(Event::InlineAssistantModelChanged),
|
||||
if self.default_model.is_none() {
|
||||
match (self.environment_fallback_model.as_ref(), model.as_ref()) {
|
||||
(Some(old), Some(new)) if old.is_same_as(new) => {}
|
||||
(None, None) => {}
|
||||
_ => cx.emit(Event::DefaultModelChanged),
|
||||
}
|
||||
}
|
||||
self.environment_fallback_model = model;
|
||||
}
|
||||
|
||||
pub fn set_inline_assistant_model(&mut self, model: Option<ConfiguredModel>) {
|
||||
self.inline_assistant_model = model;
|
||||
}
|
||||
|
||||
pub fn set_commit_message_model(
|
||||
&mut self,
|
||||
model: Option<ConfiguredModel>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
match (self.commit_message_model.as_ref(), model.as_ref()) {
|
||||
(Some(old), Some(new)) if old.is_same_as(new) => {}
|
||||
(None, None) => {}
|
||||
_ => cx.emit(Event::CommitMessageModelChanged),
|
||||
}
|
||||
pub fn set_commit_message_model(&mut self, model: Option<ConfiguredModel>) {
|
||||
self.commit_message_model = model;
|
||||
}
|
||||
|
||||
pub fn set_thread_summary_model(
|
||||
&mut self,
|
||||
model: Option<ConfiguredModel>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
match (self.thread_summary_model.as_ref(), model.as_ref()) {
|
||||
(Some(old), Some(new)) if old.is_same_as(new) => {}
|
||||
(None, None) => {}
|
||||
_ => cx.emit(Event::ThreadSummaryModelChanged),
|
||||
}
|
||||
pub fn set_thread_summary_model(&mut self, model: Option<ConfiguredModel>) {
|
||||
self.thread_summary_model = model;
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn default_model(&self) -> Option<ConfiguredModel> {
|
||||
#[cfg(debug_assertions)]
|
||||
if std::env::var("ZED_SIMULATE_NO_LLM_PROVIDER").is_ok() {
|
||||
return None;
|
||||
}
|
||||
|
||||
self.default_model.clone()
|
||||
self.default_model
|
||||
.clone()
|
||||
.or_else(|| self.environment_fallback_model.clone())
|
||||
}
|
||||
|
||||
pub fn default_fast_model(&self, cx: &App) -> Option<ConfiguredModel> {
|
||||
let provider = self.default_model()?.provider;
|
||||
let fast_model = provider.default_fast_model(cx)?;
|
||||
Some(ConfiguredModel {
|
||||
provider,
|
||||
model: fast_model,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn inline_assistant_model(&self) -> Option<ConfiguredModel> {
|
||||
|
@ -353,7 +343,7 @@ impl LanguageModelRegistry {
|
|||
.or_else(|| self.default_model.clone())
|
||||
}
|
||||
|
||||
pub fn commit_message_model(&self) -> Option<ConfiguredModel> {
|
||||
pub fn commit_message_model(&self, cx: &App) -> Option<ConfiguredModel> {
|
||||
#[cfg(debug_assertions)]
|
||||
if std::env::var("ZED_SIMULATE_NO_LLM_PROVIDER").is_ok() {
|
||||
return None;
|
||||
|
@ -361,11 +351,11 @@ impl LanguageModelRegistry {
|
|||
|
||||
self.commit_message_model
|
||||
.clone()
|
||||
.or_else(|| self.default_fast_model.clone())
|
||||
.or_else(|| self.default_fast_model(cx))
|
||||
.or_else(|| self.default_model.clone())
|
||||
}
|
||||
|
||||
pub fn thread_summary_model(&self) -> Option<ConfiguredModel> {
|
||||
pub fn thread_summary_model(&self, cx: &App) -> Option<ConfiguredModel> {
|
||||
#[cfg(debug_assertions)]
|
||||
if std::env::var("ZED_SIMULATE_NO_LLM_PROVIDER").is_ok() {
|
||||
return None;
|
||||
|
@ -373,7 +363,7 @@ impl LanguageModelRegistry {
|
|||
|
||||
self.thread_summary_model
|
||||
.clone()
|
||||
.or_else(|| self.default_fast_model.clone())
|
||||
.or_else(|| self.default_fast_model(cx))
|
||||
.or_else(|| self.default_model.clone())
|
||||
}
|
||||
|
||||
|
@ -410,4 +400,34 @@ mod tests {
|
|||
let providers = registry.read(cx).providers();
|
||||
assert!(providers.is_empty());
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_configure_environment_fallback_model(cx: &mut gpui::TestAppContext) {
|
||||
let registry = cx.new(|_| LanguageModelRegistry::default());
|
||||
|
||||
let provider = FakeLanguageModelProvider::default();
|
||||
registry.update(cx, |registry, cx| {
|
||||
registry.register_provider(provider.clone(), cx);
|
||||
});
|
||||
|
||||
cx.update(|cx| provider.authenticate(cx)).await.unwrap();
|
||||
|
||||
registry.update(cx, |registry, cx| {
|
||||
let provider = registry.provider(&provider.id()).unwrap();
|
||||
|
||||
registry.set_environment_fallback_model(
|
||||
Some(ConfiguredModel {
|
||||
provider: provider.clone(),
|
||||
model: provider.default_model(cx).unwrap(),
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
|
||||
let default_model = registry.default_model().unwrap();
|
||||
let fallback_model = registry.environment_fallback_model.clone().unwrap();
|
||||
|
||||
assert_eq!(default_model.model.id(), fallback_model.model.id());
|
||||
assert_eq!(default_model.provider.id(), fallback_model.provider.id());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -44,6 +44,7 @@ ollama = { workspace = true, features = ["schemars"] }
|
|||
open_ai = { workspace = true, features = ["schemars"] }
|
||||
open_router = { workspace = true, features = ["schemars"] }
|
||||
partial-json-fixer.workspace = true
|
||||
project.workspace = true
|
||||
release_channel.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
|
|
|
@ -3,8 +3,12 @@ use std::sync::Arc;
|
|||
use ::settings::{Settings, SettingsStore};
|
||||
use client::{Client, UserStore};
|
||||
use collections::HashSet;
|
||||
use gpui::{App, Context, Entity};
|
||||
use language_model::{LanguageModelProviderId, LanguageModelRegistry};
|
||||
use futures::future;
|
||||
use gpui::{App, AppContext as _, Context, Entity};
|
||||
use language_model::{
|
||||
AuthenticateError, ConfiguredModel, LanguageModelProviderId, LanguageModelRegistry,
|
||||
};
|
||||
use project::DisableAiSettings;
|
||||
use provider::deepseek::DeepSeekLanguageModelProvider;
|
||||
|
||||
pub mod provider;
|
||||
|
@ -13,7 +17,7 @@ pub mod ui;
|
|||
|
||||
use crate::provider::anthropic::AnthropicLanguageModelProvider;
|
||||
use crate::provider::bedrock::BedrockLanguageModelProvider;
|
||||
use crate::provider::cloud::CloudLanguageModelProvider;
|
||||
use crate::provider::cloud::{self, CloudLanguageModelProvider};
|
||||
use crate::provider::copilot_chat::CopilotChatLanguageModelProvider;
|
||||
use crate::provider::google::GoogleLanguageModelProvider;
|
||||
use crate::provider::lmstudio::LmStudioLanguageModelProvider;
|
||||
|
@ -48,6 +52,13 @@ pub fn init(user_store: Entity<UserStore>, client: Arc<Client>, cx: &mut App) {
|
|||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
let mut already_authenticated = false;
|
||||
if !DisableAiSettings::get_global(cx).disable_ai {
|
||||
authenticate_all_providers(registry.clone(), cx);
|
||||
already_authenticated = true;
|
||||
}
|
||||
|
||||
cx.observe_global::<SettingsStore>(move |cx| {
|
||||
let openai_compatible_providers_new = AllLanguageModelSettings::get_global(cx)
|
||||
.openai_compatible
|
||||
|
@ -65,6 +76,12 @@ pub fn init(user_store: Entity<UserStore>, client: Arc<Client>, cx: &mut App) {
|
|||
);
|
||||
});
|
||||
openai_compatible_providers = openai_compatible_providers_new;
|
||||
already_authenticated = false;
|
||||
}
|
||||
|
||||
if !DisableAiSettings::get_global(cx).disable_ai && !already_authenticated {
|
||||
authenticate_all_providers(registry.clone(), cx);
|
||||
already_authenticated = true;
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
@ -151,3 +168,83 @@ fn register_language_model_providers(
|
|||
registry.register_provider(XAiLanguageModelProvider::new(client.http_client(), cx), cx);
|
||||
registry.register_provider(CopilotChatLanguageModelProvider::new(cx), cx);
|
||||
}
|
||||
|
||||
/// Authenticates all providers in the [`LanguageModelRegistry`].
|
||||
///
|
||||
/// We do this so that we can populate the language selector with all of the
|
||||
/// models from the configured providers.
|
||||
///
|
||||
/// This function won't do anything if AI is disabled.
|
||||
fn authenticate_all_providers(registry: Entity<LanguageModelRegistry>, cx: &mut App) {
|
||||
let providers_to_authenticate = registry
|
||||
.read(cx)
|
||||
.providers()
|
||||
.iter()
|
||||
.map(|provider| (provider.id(), provider.name(), provider.authenticate(cx)))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut tasks = Vec::with_capacity(providers_to_authenticate.len());
|
||||
|
||||
for (provider_id, provider_name, authenticate_task) in providers_to_authenticate {
|
||||
tasks.push(cx.background_spawn(async move {
|
||||
if let Err(err) = authenticate_task.await {
|
||||
if matches!(err, AuthenticateError::CredentialsNotFound) {
|
||||
// Since we're authenticating these providers in the
|
||||
// background for the purposes of populating the
|
||||
// language selector, we don't care about providers
|
||||
// where the credentials are not found.
|
||||
} else {
|
||||
// Some providers have noisy failure states that we
|
||||
// don't want to spam the logs with every time the
|
||||
// language model selector is initialized.
|
||||
//
|
||||
// Ideally these should have more clear failure modes
|
||||
// that we know are safe to ignore here, like what we do
|
||||
// with `CredentialsNotFound` above.
|
||||
match provider_id.0.as_ref() {
|
||||
"lmstudio" | "ollama" => {
|
||||
// LM Studio and Ollama both make fetch requests to the local APIs to determine if they are "authenticated".
|
||||
//
|
||||
// These fail noisily, so we don't log them.
|
||||
}
|
||||
"copilot_chat" => {
|
||||
// Copilot Chat returns an error if Copilot is not enabled, so we don't log those errors.
|
||||
}
|
||||
_ => {
|
||||
log::error!(
|
||||
"Failed to authenticate provider: {}: {err}",
|
||||
provider_name.0
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
let all_authenticated_future = future::join_all(tasks);
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
all_authenticated_future.await;
|
||||
|
||||
registry
|
||||
.update(cx, |registry, cx| {
|
||||
let cloud_provider = registry.provider(&cloud::PROVIDER_ID);
|
||||
let fallback_model = cloud_provider
|
||||
.iter()
|
||||
.chain(registry.providers().iter())
|
||||
.find(|provider| provider.is_authenticated(cx))
|
||||
.and_then(|provider| {
|
||||
Some(ConfiguredModel {
|
||||
provider: provider.clone(),
|
||||
model: provider
|
||||
.default_model(cx)
|
||||
.or_else(|| provider.recommended_models(cx).first().cloned())?,
|
||||
})
|
||||
});
|
||||
registry.set_environment_fallback_model(fallback_model, cx);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
|
|
@ -44,8 +44,8 @@ use crate::provider::anthropic::{AnthropicEventMapper, count_anthropic_tokens, i
|
|||
use crate::provider::google::{GoogleEventMapper, into_google};
|
||||
use crate::provider::open_ai::{OpenAiEventMapper, count_open_ai_tokens, into_open_ai};
|
||||
|
||||
const PROVIDER_ID: LanguageModelProviderId = language_model::ZED_CLOUD_PROVIDER_ID;
|
||||
const PROVIDER_NAME: LanguageModelProviderName = language_model::ZED_CLOUD_PROVIDER_NAME;
|
||||
pub const PROVIDER_ID: LanguageModelProviderId = language_model::ZED_CLOUD_PROVIDER_ID;
|
||||
pub const PROVIDER_NAME: LanguageModelProviderName = language_model::ZED_CLOUD_PROVIDER_NAME;
|
||||
|
||||
#[derive(Default, Clone, Debug, PartialEq)]
|
||||
pub struct ZedDotDevSettings {
|
||||
|
@ -146,7 +146,7 @@ impl State {
|
|||
default_fast_model: None,
|
||||
recommended_models: Vec::new(),
|
||||
_fetch_models_task: cx.spawn(async move |this, cx| {
|
||||
maybe!(async move {
|
||||
maybe!(async {
|
||||
let (client, llm_api_token) = this
|
||||
.read_with(cx, |this, _cx| (client.clone(), this.llm_api_token.clone()))?;
|
||||
|
||||
|
|
|
@ -22,15 +22,17 @@ futures.workspace = true
|
|||
gpui.workspace = true
|
||||
itertools.workspace = true
|
||||
language.workspace = true
|
||||
log.workspace = true
|
||||
lsp.workspace = true
|
||||
project.workspace = true
|
||||
proto.workspace = true
|
||||
serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
theme.workspace = true
|
||||
tree-sitter.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
workspace.workspace = true
|
||||
workspace = { path = "../workspace", default-features = false }
|
||||
zed_actions.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ use gpui::{
|
|||
};
|
||||
use itertools::Itertools;
|
||||
use serde_json::json;
|
||||
use settings::get_key_equivalents;
|
||||
use ui::{Button, ButtonStyle};
|
||||
use ui::{
|
||||
ButtonCommon, Clickable, Context, FluentBuilder, InteractiveElement, Label, LabelCommon,
|
||||
|
@ -168,8 +169,7 @@ impl Item for KeyContextView {
|
|||
impl Render for KeyContextView {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
|
||||
use itertools::Itertools;
|
||||
|
||||
let key_equivalents = cx.keyboard_mapper().get_key_equivalents();
|
||||
let key_equivalents = get_key_equivalents(cx.keyboard_layout().id());
|
||||
v_flex()
|
||||
.id("key-context-view")
|
||||
.overflow_scroll()
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
mod key_context_view;
|
||||
mod lsp_log;
|
||||
pub mod lsp_log;
|
||||
pub mod lsp_tool;
|
||||
mod syntax_tree_view;
|
||||
|
||||
|
@ -14,7 +14,7 @@ use ui::{Context, Window};
|
|||
use workspace::{Item, ItemHandle, SplitDirection, Workspace};
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
lsp_log::init(cx);
|
||||
lsp_log::init(true, cx);
|
||||
syntax_tree_view::init(cx);
|
||||
key_context_view::init(cx);
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -51,7 +51,7 @@ async fn test_lsp_logs(cx: &mut TestAppContext) {
|
|||
},
|
||||
);
|
||||
|
||||
let log_store = cx.new(LogStore::new);
|
||||
let log_store = cx.new(|cx| LogStore::new(true, cx));
|
||||
log_store.update(cx, |store, cx| store.add_project(&project, cx));
|
||||
|
||||
let _rust_buffer = project
|
||||
|
|
|
@ -122,8 +122,7 @@ impl LanguageServerState {
|
|||
let lsp_logs = cx
|
||||
.try_global::<GlobalLogStore>()
|
||||
.and_then(|lsp_logs| lsp_logs.0.upgrade());
|
||||
let lsp_store = self.lsp_store.upgrade();
|
||||
let Some((lsp_logs, lsp_store)) = lsp_logs.zip(lsp_store) else {
|
||||
let Some(lsp_logs) = lsp_logs else {
|
||||
return menu;
|
||||
};
|
||||
|
||||
|
@ -210,10 +209,7 @@ impl LanguageServerState {
|
|||
};
|
||||
|
||||
let server_selector = server_info.server_selector();
|
||||
// TODO currently, Zed remote does not work well with the LSP logs
|
||||
// https://github.com/zed-industries/zed/issues/28557
|
||||
let has_logs = lsp_store.read(cx).as_local().is_some()
|
||||
&& lsp_logs.read(cx).has_server_logs(&server_selector);
|
||||
let has_logs = lsp_logs.read(cx).has_server_logs(&server_selector);
|
||||
|
||||
let status_color = server_info
|
||||
.binary_status
|
||||
|
|
|
@ -1323,7 +1323,7 @@ fn render_copy_code_block_button(
|
|||
.icon_size(IconSize::Small)
|
||||
.style(ButtonStyle::Filled)
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.tooltip(Tooltip::text("Copy"))
|
||||
.tooltip(Tooltip::text("Copy Code"))
|
||||
.on_click({
|
||||
let markdown = markdown;
|
||||
move |_event, _window, cx| {
|
||||
|
|
|
@ -977,7 +977,9 @@ impl LocalLspStore {
|
|||
this.update(&mut cx, |_, cx| {
|
||||
cx.emit(LspStoreEvent::LanguageServerLog(
|
||||
server_id,
|
||||
LanguageServerLogType::Trace(params.verbose),
|
||||
LanguageServerLogType::Trace {
|
||||
verbose_info: params.verbose,
|
||||
},
|
||||
params.message,
|
||||
));
|
||||
})
|
||||
|
@ -3482,13 +3484,13 @@ pub struct LspStore {
|
|||
buffer_store: Entity<BufferStore>,
|
||||
worktree_store: Entity<WorktreeStore>,
|
||||
pub languages: Arc<LanguageRegistry>,
|
||||
language_server_statuses: BTreeMap<LanguageServerId, LanguageServerStatus>,
|
||||
pub language_server_statuses: BTreeMap<LanguageServerId, LanguageServerStatus>,
|
||||
active_entry: Option<ProjectEntryId>,
|
||||
_maintain_workspace_config: (Task<Result<()>>, watch::Sender<()>),
|
||||
_maintain_buffer_languages: Task<()>,
|
||||
diagnostic_summaries:
|
||||
HashMap<WorktreeId, HashMap<Arc<Path>, HashMap<LanguageServerId, DiagnosticSummary>>>,
|
||||
pub(super) lsp_server_capabilities: HashMap<LanguageServerId, lsp::ServerCapabilities>,
|
||||
pub lsp_server_capabilities: HashMap<LanguageServerId, lsp::ServerCapabilities>,
|
||||
lsp_document_colors: HashMap<BufferId, DocumentColorData>,
|
||||
lsp_code_lens: HashMap<BufferId, CodeLensData>,
|
||||
running_lsp_requests: HashMap<TypeId, (Global, HashMap<LspRequestId, Task<()>>)>,
|
||||
|
@ -12168,6 +12170,10 @@ impl LspStore {
|
|||
let data = self.lsp_code_lens.get_mut(&buffer_id)?;
|
||||
Some(data.update.take()?.1)
|
||||
}
|
||||
|
||||
pub fn downstream_client(&self) -> Option<(AnyProtoClient, u64)> {
|
||||
self.downstream_client.clone()
|
||||
}
|
||||
}
|
||||
|
||||
// Registration with registerOptions as null, should fallback to true.
|
||||
|
@ -12677,45 +12683,69 @@ impl PartialEq for LanguageServerPromptRequest {
|
|||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum LanguageServerLogType {
|
||||
Log(MessageType),
|
||||
Trace(Option<String>),
|
||||
Trace { verbose_info: Option<String> },
|
||||
Rpc { received: bool },
|
||||
}
|
||||
|
||||
impl LanguageServerLogType {
|
||||
pub fn to_proto(&self) -> proto::language_server_log::LogType {
|
||||
match self {
|
||||
Self::Log(log_type) => {
|
||||
let message_type = match *log_type {
|
||||
MessageType::ERROR => 1,
|
||||
MessageType::WARNING => 2,
|
||||
MessageType::INFO => 3,
|
||||
MessageType::LOG => 4,
|
||||
use proto::log_message::LogLevel;
|
||||
let level = match *log_type {
|
||||
MessageType::ERROR => LogLevel::Error,
|
||||
MessageType::WARNING => LogLevel::Warning,
|
||||
MessageType::INFO => LogLevel::Info,
|
||||
MessageType::LOG => LogLevel::Log,
|
||||
other => {
|
||||
log::warn!("Unknown lsp log message type: {:?}", other);
|
||||
4
|
||||
log::warn!("Unknown lsp log message type: {other:?}");
|
||||
LogLevel::Log
|
||||
}
|
||||
};
|
||||
proto::language_server_log::LogType::LogMessageType(message_type)
|
||||
}
|
||||
Self::Trace(message) => {
|
||||
proto::language_server_log::LogType::LogTrace(proto::LspLogTrace {
|
||||
message: message.clone(),
|
||||
proto::language_server_log::LogType::Log(proto::LogMessage {
|
||||
level: level as i32,
|
||||
})
|
||||
}
|
||||
Self::Trace { verbose_info } => {
|
||||
proto::language_server_log::LogType::Trace(proto::TraceMessage {
|
||||
verbose_info: verbose_info.to_owned(),
|
||||
})
|
||||
}
|
||||
Self::Rpc { received } => {
|
||||
let kind = if *received {
|
||||
proto::rpc_message::Kind::Received
|
||||
} else {
|
||||
proto::rpc_message::Kind::Sent
|
||||
};
|
||||
let kind = kind as i32;
|
||||
proto::language_server_log::LogType::Rpc(proto::RpcMessage { kind })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_proto(log_type: proto::language_server_log::LogType) -> Self {
|
||||
use proto::log_message::LogLevel;
|
||||
use proto::rpc_message;
|
||||
match log_type {
|
||||
proto::language_server_log::LogType::LogMessageType(message_type) => {
|
||||
Self::Log(match message_type {
|
||||
1 => MessageType::ERROR,
|
||||
2 => MessageType::WARNING,
|
||||
3 => MessageType::INFO,
|
||||
4 => MessageType::LOG,
|
||||
_ => MessageType::LOG,
|
||||
})
|
||||
}
|
||||
proto::language_server_log::LogType::LogTrace(trace) => Self::Trace(trace.message),
|
||||
proto::language_server_log::LogType::Log(message_type) => Self::Log(
|
||||
match LogLevel::from_i32(message_type.level).unwrap_or(LogLevel::Log) {
|
||||
LogLevel::Error => MessageType::ERROR,
|
||||
LogLevel::Warning => MessageType::WARNING,
|
||||
LogLevel::Info => MessageType::INFO,
|
||||
LogLevel::Log => MessageType::LOG,
|
||||
},
|
||||
),
|
||||
proto::language_server_log::LogType::Trace(trace_message) => Self::Trace {
|
||||
verbose_info: trace_message.verbose_info,
|
||||
},
|
||||
proto::language_server_log::LogType::Rpc(message) => Self::Rpc {
|
||||
received: match rpc_message::Kind::from_i32(message.kind)
|
||||
.unwrap_or(rpc_message::Kind::Received)
|
||||
{
|
||||
rpc_message::Kind::Received => true,
|
||||
rpc_message::Kind::Sent => false,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1355,6 +1355,7 @@ impl Project {
|
|||
ssh_proto.subscribe_to_entity(SSH_PROJECT_ID, &this.settings_observer);
|
||||
ssh_proto.subscribe_to_entity(SSH_PROJECT_ID, &this.git_store);
|
||||
|
||||
ssh_proto.add_entity_message_handler(Self::handle_toggle_lsp_logs);
|
||||
ssh_proto.add_entity_message_handler(Self::handle_create_buffer_for_peer);
|
||||
ssh_proto.add_entity_message_handler(Self::handle_update_worktree);
|
||||
ssh_proto.add_entity_message_handler(Self::handle_update_project);
|
||||
|
@ -4629,6 +4630,16 @@ impl Project {
|
|||
})?
|
||||
}
|
||||
|
||||
// TODO kb
|
||||
async fn handle_toggle_lsp_logs(
|
||||
_this: Entity<Self>,
|
||||
_envelope: TypedEnvelope<proto::ToggleLspLogs>,
|
||||
_cx: AsyncApp,
|
||||
) -> Result<()> {
|
||||
log::error!("##########PPPPPPPPPPPPPPPproject##########################");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_update_buffer_from_ssh(
|
||||
this: Entity<Self>,
|
||||
envelope: TypedEnvelope<proto::UpdateBuffer>,
|
||||
|
|
|
@ -4089,7 +4089,6 @@ impl ProjectPanel {
|
|||
.when(!is_sticky, |this| {
|
||||
this
|
||||
.when(is_highlighted && folded_directory_drag_target.is_none(), |this| this.border_color(transparent_white()).bg(item_colors.drag_over))
|
||||
.when(settings.drag_and_drop, |this| this
|
||||
.on_drag_move::<ExternalPaths>(cx.listener(
|
||||
move |this, event: &DragMoveEvent<ExternalPaths>, _, cx| {
|
||||
let is_current_target = this.drag_target_entry.as_ref()
|
||||
|
@ -4223,7 +4222,7 @@ impl ProjectPanel {
|
|||
}
|
||||
this.drag_onto(selections, entry_id, kind.is_file(), window, cx);
|
||||
}),
|
||||
))
|
||||
)
|
||||
})
|
||||
.on_mouse_down(
|
||||
MouseButton::Left,
|
||||
|
@ -4434,7 +4433,6 @@ impl ProjectPanel {
|
|||
div()
|
||||
.when(!is_sticky, |div| {
|
||||
div
|
||||
.when(settings.drag_and_drop, |div| div
|
||||
.on_drop(cx.listener(move |this, selections: &DraggedSelection, window, cx| {
|
||||
this.hover_scroll_task.take();
|
||||
this.drag_target_entry = None;
|
||||
|
@ -4466,7 +4464,7 @@ impl ProjectPanel {
|
|||
}
|
||||
|
||||
},
|
||||
)))
|
||||
))
|
||||
})
|
||||
.child(
|
||||
Label::new(DELIMITER.clone())
|
||||
|
@ -4486,7 +4484,6 @@ impl ProjectPanel {
|
|||
.when(index != components_len - 1, |div|{
|
||||
let target_entry_id = folded_ancestors.ancestors.get(components_len - 1 - index).cloned();
|
||||
div
|
||||
.when(settings.drag_and_drop, |div| div
|
||||
.on_drag_move(cx.listener(
|
||||
move |this, event: &DragMoveEvent<DraggedSelection>, _, _| {
|
||||
if event.bounds.contains(&event.event.position) {
|
||||
|
@ -4524,7 +4521,7 @@ impl ProjectPanel {
|
|||
target.index == index
|
||||
), |this| {
|
||||
this.bg(item_colors.drag_over)
|
||||
}))
|
||||
})
|
||||
})
|
||||
})
|
||||
.on_click(cx.listener(move |this, _, _, cx| {
|
||||
|
@ -5032,8 +5029,7 @@ impl ProjectPanel {
|
|||
|
||||
sticky_parents.reverse();
|
||||
|
||||
let panel_settings = ProjectPanelSettings::get_global(cx);
|
||||
let git_status_enabled = panel_settings.git_status;
|
||||
let git_status_enabled = ProjectPanelSettings::get_global(cx).git_status;
|
||||
let root_name = OsStr::new(worktree.root_name());
|
||||
|
||||
let git_summaries_by_id = if git_status_enabled {
|
||||
|
@ -5117,11 +5113,11 @@ impl Render for ProjectPanel {
|
|||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let has_worktree = !self.visible_entries.is_empty();
|
||||
let project = self.project.read(cx);
|
||||
let panel_settings = ProjectPanelSettings::get_global(cx);
|
||||
let indent_size = panel_settings.indent_size;
|
||||
let show_indent_guides = panel_settings.indent_guides.show == ShowIndentGuides::Always;
|
||||
let indent_size = ProjectPanelSettings::get_global(cx).indent_size;
|
||||
let show_indent_guides =
|
||||
ProjectPanelSettings::get_global(cx).indent_guides.show == ShowIndentGuides::Always;
|
||||
let show_sticky_entries = {
|
||||
if panel_settings.sticky_scroll {
|
||||
if ProjectPanelSettings::get_global(cx).sticky_scroll {
|
||||
let is_scrollable = self.scroll_handle.is_scrollable();
|
||||
let is_scrolled = self.scroll_handle.offset().y < px(0.);
|
||||
is_scrollable && is_scrolled
|
||||
|
@ -5209,10 +5205,8 @@ impl Render for ProjectPanel {
|
|||
h_flex()
|
||||
.id("project-panel")
|
||||
.group("project-panel")
|
||||
.when(panel_settings.drag_and_drop, |this| {
|
||||
this.on_drag_move(cx.listener(handle_drag_move::<ExternalPaths>))
|
||||
.on_drag_move(cx.listener(handle_drag_move::<DraggedSelection>))
|
||||
})
|
||||
.on_drag_move(cx.listener(handle_drag_move::<ExternalPaths>))
|
||||
.on_drag_move(cx.listener(handle_drag_move::<DraggedSelection>))
|
||||
.size_full()
|
||||
.relative()
|
||||
.on_modifiers_changed(cx.listener(
|
||||
|
@ -5550,32 +5544,30 @@ impl Render for ProjectPanel {
|
|||
})),
|
||||
)
|
||||
.when(is_local, |div| {
|
||||
div.when(panel_settings.drag_and_drop, |div| {
|
||||
div.drag_over::<ExternalPaths>(|style, _, _, cx| {
|
||||
style.bg(cx.theme().colors().drop_target_background)
|
||||
})
|
||||
.on_drop(cx.listener(
|
||||
move |this, external_paths: &ExternalPaths, window, cx| {
|
||||
this.drag_target_entry = None;
|
||||
this.hover_scroll_task.take();
|
||||
if let Some(task) = this
|
||||
.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.open_workspace_for_paths(
|
||||
true,
|
||||
external_paths.paths().to_owned(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.log_err()
|
||||
{
|
||||
task.detach_and_log_err(cx);
|
||||
}
|
||||
cx.stop_propagation();
|
||||
},
|
||||
))
|
||||
div.drag_over::<ExternalPaths>(|style, _, _, cx| {
|
||||
style.bg(cx.theme().colors().drop_target_background)
|
||||
})
|
||||
.on_drop(cx.listener(
|
||||
move |this, external_paths: &ExternalPaths, window, cx| {
|
||||
this.drag_target_entry = None;
|
||||
this.hover_scroll_task.take();
|
||||
if let Some(task) = this
|
||||
.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.open_workspace_for_paths(
|
||||
true,
|
||||
external_paths.paths().to_owned(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.log_err()
|
||||
{
|
||||
task.detach_and_log_err(cx);
|
||||
}
|
||||
cx.stop_propagation();
|
||||
},
|
||||
))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -47,7 +47,6 @@ pub struct ProjectPanelSettings {
|
|||
pub scrollbar: ScrollbarSettings,
|
||||
pub show_diagnostics: ShowDiagnostics,
|
||||
pub hide_root: bool,
|
||||
pub drag_and_drop: bool,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
|
||||
|
@ -161,10 +160,6 @@ pub struct ProjectPanelSettingsContent {
|
|||
///
|
||||
/// Default: true
|
||||
pub sticky_scroll: Option<bool>,
|
||||
/// Whether to enable drag-and-drop operations in the project panel.
|
||||
///
|
||||
/// Default: true
|
||||
pub drag_and_drop: Option<bool>,
|
||||
}
|
||||
|
||||
impl Settings for ProjectPanelSettings {
|
||||
|
|
|
@ -610,11 +610,36 @@ message ServerMetadataUpdated {
|
|||
message LanguageServerLog {
|
||||
uint64 project_id = 1;
|
||||
uint64 language_server_id = 2;
|
||||
string message = 3;
|
||||
oneof log_type {
|
||||
uint32 log_message_type = 3;
|
||||
LspLogTrace log_trace = 4;
|
||||
LogMessage log = 4;
|
||||
TraceMessage trace = 5;
|
||||
RpcMessage rpc = 6;
|
||||
}
|
||||
}
|
||||
|
||||
message LogMessage {
|
||||
LogLevel level = 1;
|
||||
|
||||
enum LogLevel {
|
||||
LOG = 0;
|
||||
INFO = 1;
|
||||
WARNING = 2;
|
||||
ERROR = 3;
|
||||
}
|
||||
}
|
||||
|
||||
message TraceMessage {
|
||||
optional string verbose_info = 1;
|
||||
}
|
||||
|
||||
message RpcMessage {
|
||||
Kind kind = 1;
|
||||
|
||||
enum Kind {
|
||||
RECEIVED = 0;
|
||||
SENT = 1;
|
||||
}
|
||||
string message = 5;
|
||||
}
|
||||
|
||||
message LspLogTrace {
|
||||
|
@ -932,3 +957,16 @@ message MultiLspQuery {
|
|||
message MultiLspQueryResponse {
|
||||
repeated LspResponse responses = 1;
|
||||
}
|
||||
|
||||
message ToggleLspLogs {
|
||||
uint64 project_id = 1;
|
||||
LogType log_type = 2;
|
||||
uint64 server_id = 3;
|
||||
bool enabled = 4;
|
||||
|
||||
enum LogType {
|
||||
LOG = 0;
|
||||
TRACE = 1;
|
||||
RPC = 2;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -396,7 +396,8 @@ message Envelope {
|
|||
GitCloneResponse git_clone_response = 364;
|
||||
|
||||
LspQuery lsp_query = 365;
|
||||
LspQueryResponse lsp_query_response = 366; // current max
|
||||
LspQueryResponse lsp_query_response = 366;
|
||||
ToggleLspLogs toggle_lsp_logs = 367; // current max
|
||||
}
|
||||
|
||||
reserved 87 to 88;
|
||||
|
|
|
@ -312,7 +312,8 @@ messages!(
|
|||
(GetDefaultBranch, Background),
|
||||
(GetDefaultBranchResponse, Background),
|
||||
(GitClone, Background),
|
||||
(GitCloneResponse, Background)
|
||||
(GitCloneResponse, Background),
|
||||
(ToggleLspLogs, Background),
|
||||
);
|
||||
|
||||
request_messages!(
|
||||
|
@ -481,7 +482,8 @@ request_messages!(
|
|||
(GetDocumentDiagnostics, GetDocumentDiagnosticsResponse),
|
||||
(PullWorkspaceDiagnostics, Ack),
|
||||
(GetDefaultBranch, GetDefaultBranchResponse),
|
||||
(GitClone, GitCloneResponse)
|
||||
(GitClone, GitCloneResponse),
|
||||
(ToggleLspLogs, Ack),
|
||||
);
|
||||
|
||||
lsp_messages!(
|
||||
|
@ -612,6 +614,7 @@ entity_messages!(
|
|||
GitReset,
|
||||
GitCheckoutFiles,
|
||||
SetIndexText,
|
||||
ToggleLspLogs,
|
||||
|
||||
Push,
|
||||
Fetch,
|
||||
|
|
|
@ -43,6 +43,7 @@ gpui_tokio.workspace = true
|
|||
http_client.workspace = true
|
||||
language.workspace = true
|
||||
language_extension.workspace = true
|
||||
language_tools.workspace = true
|
||||
languages.workspace = true
|
||||
log.workspace = true
|
||||
lsp.workspace = true
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
use ::proto::{FromProto, ToProto};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use language_tools::lsp_log::{GlobalLogStore, LanguageServerKind};
|
||||
use lsp::LanguageServerId;
|
||||
|
||||
use extension::ExtensionHostProxy;
|
||||
use extension_host::headless_host::HeadlessExtensionStore;
|
||||
|
@ -65,6 +67,7 @@ impl HeadlessProject {
|
|||
settings::init(cx);
|
||||
language::init(cx);
|
||||
project::Project::init_settings(cx);
|
||||
language_tools::lsp_log::init(false, cx);
|
||||
}
|
||||
|
||||
pub fn new(
|
||||
|
@ -235,6 +238,7 @@ impl HeadlessProject {
|
|||
session.add_entity_request_handler(Self::handle_open_new_buffer);
|
||||
session.add_entity_request_handler(Self::handle_find_search_candidates);
|
||||
session.add_entity_request_handler(Self::handle_open_server_settings);
|
||||
session.add_entity_message_handler(Self::handle_toggle_lsp_logs);
|
||||
|
||||
session.add_entity_request_handler(BufferStore::handle_update_buffer);
|
||||
session.add_entity_message_handler(BufferStore::handle_close_buffer);
|
||||
|
@ -298,11 +302,40 @@ impl HeadlessProject {
|
|||
|
||||
fn on_lsp_store_event(
|
||||
&mut self,
|
||||
_lsp_store: Entity<LspStore>,
|
||||
lsp_store: Entity<LspStore>,
|
||||
event: &LspStoreEvent,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
match event {
|
||||
LspStoreEvent::LanguageServerAdded(id, name, worktree_id) => {
|
||||
let log_store = cx
|
||||
.try_global::<GlobalLogStore>()
|
||||
.and_then(|lsp_logs| lsp_logs.0.upgrade());
|
||||
if let Some(log_store) = log_store {
|
||||
log_store.update(cx, |log_store, cx| {
|
||||
log_store.add_language_server(
|
||||
LanguageServerKind::LocalSsh {
|
||||
lsp_store: self.lsp_store.downgrade(),
|
||||
},
|
||||
*id,
|
||||
Some(name.clone()),
|
||||
*worktree_id,
|
||||
lsp_store.read(cx).language_server_for_id(*id),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
LspStoreEvent::LanguageServerRemoved(id) => {
|
||||
let log_store = cx
|
||||
.try_global::<GlobalLogStore>()
|
||||
.and_then(|lsp_logs| lsp_logs.0.upgrade());
|
||||
if let Some(log_store) = log_store {
|
||||
log_store.update(cx, |log_store, cx| {
|
||||
log_store.remove_language_server(*id, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
LspStoreEvent::LanguageServerUpdate {
|
||||
language_server_id,
|
||||
name,
|
||||
|
@ -326,16 +359,6 @@ impl HeadlessProject {
|
|||
})
|
||||
.log_err();
|
||||
}
|
||||
LspStoreEvent::LanguageServerLog(language_server_id, log_type, message) => {
|
||||
self.session
|
||||
.send(proto::LanguageServerLog {
|
||||
project_id: SSH_PROJECT_ID,
|
||||
language_server_id: language_server_id.to_proto(),
|
||||
message: message.clone(),
|
||||
log_type: Some(log_type.to_proto()),
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
LspStoreEvent::LanguageServerPrompt(prompt) => {
|
||||
let request = self.session.request(proto::LanguageServerPromptRequest {
|
||||
project_id: SSH_PROJECT_ID,
|
||||
|
@ -509,7 +532,31 @@ impl HeadlessProject {
|
|||
})
|
||||
}
|
||||
|
||||
pub async fn handle_open_server_settings(
|
||||
async fn handle_toggle_lsp_logs(
|
||||
_: Entity<Self>,
|
||||
envelope: TypedEnvelope<proto::ToggleLspLogs>,
|
||||
mut cx: AsyncApp,
|
||||
) -> Result<()> {
|
||||
let server_id = LanguageServerId::from_proto(envelope.payload.server_id);
|
||||
let lsp_logs = cx
|
||||
.update(|cx| {
|
||||
cx.try_global::<GlobalLogStore>()
|
||||
.and_then(|lsp_logs| lsp_logs.0.upgrade())
|
||||
})?
|
||||
.context("lsp logs store is missing")?;
|
||||
|
||||
lsp_logs.update(&mut cx, |lsp_logs, _| {
|
||||
// we do not support any other log toggling yet
|
||||
if envelope.payload.enabled {
|
||||
lsp_logs.enable_rpc_trace_for_language_server(server_id);
|
||||
} else {
|
||||
lsp_logs.disable_rpc_trace_for_language_server(server_id);
|
||||
}
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_open_server_settings(
|
||||
this: Entity<Self>,
|
||||
_: TypedEnvelope<proto::OpenServerSettings>,
|
||||
mut cx: AsyncApp,
|
||||
|
@ -562,7 +609,7 @@ impl HeadlessProject {
|
|||
})
|
||||
}
|
||||
|
||||
pub async fn handle_find_search_candidates(
|
||||
async fn handle_find_search_candidates(
|
||||
this: Entity<Self>,
|
||||
envelope: TypedEnvelope<proto::FindSearchCandidates>,
|
||||
mut cx: AsyncApp,
|
||||
|
@ -594,7 +641,7 @@ impl HeadlessProject {
|
|||
Ok(response)
|
||||
}
|
||||
|
||||
pub async fn handle_list_remote_directory(
|
||||
async fn handle_list_remote_directory(
|
||||
this: Entity<Self>,
|
||||
envelope: TypedEnvelope<proto::ListRemoteDirectory>,
|
||||
cx: AsyncApp,
|
||||
|
@ -626,7 +673,7 @@ impl HeadlessProject {
|
|||
})
|
||||
}
|
||||
|
||||
pub async fn handle_get_path_metadata(
|
||||
async fn handle_get_path_metadata(
|
||||
this: Entity<Self>,
|
||||
envelope: TypedEnvelope<proto::GetPathMetadata>,
|
||||
cx: AsyncApp,
|
||||
|
@ -644,7 +691,7 @@ impl HeadlessProject {
|
|||
})
|
||||
}
|
||||
|
||||
pub async fn handle_shutdown_remote_server(
|
||||
async fn handle_shutdown_remote_server(
|
||||
_this: Entity<Self>,
|
||||
_envelope: TypedEnvelope<proto::ShutdownRemoteServer>,
|
||||
cx: AsyncApp,
|
||||
|
|
1424
crates/settings/src/key_equivalents.rs
Normal file
1424
crates/settings/src/key_equivalents.rs
Normal file
File diff suppressed because it is too large
Load diff
|
@ -3,8 +3,7 @@ use collections::{BTreeMap, HashMap, IndexMap};
|
|||
use fs::Fs;
|
||||
use gpui::{
|
||||
Action, ActionBuildError, App, InvalidKeystrokeError, KEYSTROKE_PARSE_EXPECTED_MESSAGE,
|
||||
KeyBinding, KeyBindingContextPredicate, KeyBindingMetaIndex, KeybindingKeystroke, Keystroke,
|
||||
NoAction, SharedString,
|
||||
KeyBinding, KeyBindingContextPredicate, KeyBindingMetaIndex, Keystroke, NoAction, SharedString,
|
||||
};
|
||||
use schemars::{JsonSchema, json_schema};
|
||||
use serde::Deserialize;
|
||||
|
@ -212,6 +211,9 @@ impl KeymapFile {
|
|||
}
|
||||
|
||||
pub fn load(content: &str, cx: &App) -> KeymapFileLoadResult {
|
||||
let key_equivalents =
|
||||
crate::key_equivalents::get_key_equivalents(cx.keyboard_layout().id());
|
||||
|
||||
if content.is_empty() {
|
||||
return KeymapFileLoadResult::Success {
|
||||
key_bindings: Vec::new(),
|
||||
|
@ -253,6 +255,12 @@ impl KeymapFile {
|
|||
}
|
||||
};
|
||||
|
||||
let key_equivalents = if *use_key_equivalents {
|
||||
key_equivalents.as_ref()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mut section_errors = String::new();
|
||||
|
||||
if !unrecognized_fields.is_empty() {
|
||||
|
@ -270,7 +278,7 @@ impl KeymapFile {
|
|||
keystrokes,
|
||||
action,
|
||||
context_predicate.clone(),
|
||||
*use_key_equivalents,
|
||||
key_equivalents,
|
||||
cx,
|
||||
);
|
||||
match result {
|
||||
|
@ -328,7 +336,7 @@ impl KeymapFile {
|
|||
keystrokes: &str,
|
||||
action: &KeymapAction,
|
||||
context: Option<Rc<KeyBindingContextPredicate>>,
|
||||
use_key_equivalents: bool,
|
||||
key_equivalents: Option<&HashMap<char, char>>,
|
||||
cx: &App,
|
||||
) -> std::result::Result<KeyBinding, String> {
|
||||
let (build_result, action_input_string) = match &action.0 {
|
||||
|
@ -396,9 +404,8 @@ impl KeymapFile {
|
|||
keystrokes,
|
||||
action,
|
||||
context,
|
||||
use_key_equivalents,
|
||||
key_equivalents,
|
||||
action_input_string.map(SharedString::from),
|
||||
cx.keyboard_mapper().as_ref(),
|
||||
) {
|
||||
Ok(key_binding) => key_binding,
|
||||
Err(InvalidKeystrokeError { keystroke }) => {
|
||||
|
@ -600,7 +607,6 @@ impl KeymapFile {
|
|||
mut operation: KeybindUpdateOperation<'a>,
|
||||
mut keymap_contents: String,
|
||||
tab_size: usize,
|
||||
keyboard_mapper: &dyn gpui::PlatformKeyboardMapper,
|
||||
) -> Result<String> {
|
||||
match operation {
|
||||
// if trying to replace a keybinding that is not user-defined, treat it as an add operation
|
||||
|
@ -640,7 +646,7 @@ impl KeymapFile {
|
|||
.action_value()
|
||||
.context("Failed to generate target action JSON value")?;
|
||||
let Some((index, keystrokes_str)) =
|
||||
find_binding(&keymap, &target, &target_action_value, keyboard_mapper)
|
||||
find_binding(&keymap, &target, &target_action_value)
|
||||
else {
|
||||
anyhow::bail!("Failed to find keybinding to remove");
|
||||
};
|
||||
|
@ -675,7 +681,7 @@ impl KeymapFile {
|
|||
.context("Failed to generate source action JSON value")?;
|
||||
|
||||
if let Some((index, keystrokes_str)) =
|
||||
find_binding(&keymap, &target, &target_action_value, keyboard_mapper)
|
||||
find_binding(&keymap, &target, &target_action_value)
|
||||
{
|
||||
if target.context == source.context {
|
||||
// if we are only changing the keybinding (common case)
|
||||
|
@ -775,7 +781,7 @@ impl KeymapFile {
|
|||
}
|
||||
let use_key_equivalents = from.and_then(|from| {
|
||||
let action_value = from.action_value().context("Failed to serialize action value. `use_key_equivalents` on new keybinding may be incorrect.").log_err()?;
|
||||
let (index, _) = find_binding(&keymap, &from, &action_value, keyboard_mapper)?;
|
||||
let (index, _) = find_binding(&keymap, &from, &action_value)?;
|
||||
Some(keymap.0[index].use_key_equivalents)
|
||||
}).unwrap_or(false);
|
||||
if use_key_equivalents {
|
||||
|
@ -802,7 +808,6 @@ impl KeymapFile {
|
|||
keymap: &'b KeymapFile,
|
||||
target: &KeybindUpdateTarget<'a>,
|
||||
target_action_value: &Value,
|
||||
keyboard_mapper: &dyn gpui::PlatformKeyboardMapper,
|
||||
) -> Option<(usize, &'b str)> {
|
||||
let target_context_parsed =
|
||||
KeyBindingContextPredicate::parse(target.context.unwrap_or("")).ok();
|
||||
|
@ -818,11 +823,8 @@ impl KeymapFile {
|
|||
for (keystrokes_str, action) in bindings {
|
||||
let Ok(keystrokes) = keystrokes_str
|
||||
.split_whitespace()
|
||||
.map(|source| {
|
||||
let keystroke = Keystroke::parse(source)?;
|
||||
Ok(KeybindingKeystroke::new(keystroke, false, keyboard_mapper))
|
||||
})
|
||||
.collect::<Result<Vec<_>, InvalidKeystrokeError>>()
|
||||
.map(Keystroke::parse)
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
@ -830,7 +832,7 @@ impl KeymapFile {
|
|||
|| !keystrokes
|
||||
.iter()
|
||||
.zip(target.keystrokes)
|
||||
.all(|(a, b)| a.inner.should_match(b))
|
||||
.all(|(a, b)| a.should_match(b))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
@ -845,7 +847,7 @@ impl KeymapFile {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone)]
|
||||
pub enum KeybindUpdateOperation<'a> {
|
||||
Replace {
|
||||
/// Describes the keybind to create
|
||||
|
@ -914,7 +916,7 @@ impl<'a> KeybindUpdateOperation<'a> {
|
|||
#[derive(Debug, Clone)]
|
||||
pub struct KeybindUpdateTarget<'a> {
|
||||
pub context: Option<&'a str>,
|
||||
pub keystrokes: &'a [KeybindingKeystroke],
|
||||
pub keystrokes: &'a [Keystroke],
|
||||
pub action_name: &'a str,
|
||||
pub action_arguments: Option<&'a str>,
|
||||
}
|
||||
|
@ -939,9 +941,6 @@ impl<'a> KeybindUpdateTarget<'a> {
|
|||
fn keystrokes_unparsed(&self) -> String {
|
||||
let mut keystrokes = String::with_capacity(self.keystrokes.len() * 8);
|
||||
for keystroke in self.keystrokes {
|
||||
// The reason use `keystroke.unparse()` instead of `keystroke.inner.unparse()`
|
||||
// here is that, we want the user to use `ctrl-shift-4` instead of `ctrl-$`
|
||||
// by default on Windows.
|
||||
keystrokes.push_str(&keystroke.unparse());
|
||||
keystrokes.push(' ');
|
||||
}
|
||||
|
@ -960,7 +959,7 @@ impl<'a> KeybindUpdateTarget<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Debug)]
|
||||
#[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum KeybindSource {
|
||||
User,
|
||||
Vim,
|
||||
|
@ -1021,7 +1020,7 @@ impl From<KeybindSource> for KeyBindingMetaIndex {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use gpui::{DummyKeyboardMapper, KeybindingKeystroke, Keystroke};
|
||||
use gpui::Keystroke;
|
||||
use unindent::Unindent;
|
||||
|
||||
use crate::{
|
||||
|
@ -1050,27 +1049,16 @@ mod tests {
|
|||
operation: KeybindUpdateOperation,
|
||||
expected: impl ToString,
|
||||
) {
|
||||
let result = KeymapFile::update_keybinding(
|
||||
operation,
|
||||
input.to_string(),
|
||||
4,
|
||||
&gpui::DummyKeyboardMapper,
|
||||
)
|
||||
.expect("Update succeeded");
|
||||
let result = KeymapFile::update_keybinding(operation, input.to_string(), 4)
|
||||
.expect("Update succeeded");
|
||||
pretty_assertions::assert_eq!(expected.to_string(), result);
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn parse_keystrokes(keystrokes: &str) -> Vec<KeybindingKeystroke> {
|
||||
fn parse_keystrokes(keystrokes: &str) -> Vec<Keystroke> {
|
||||
keystrokes
|
||||
.split(' ')
|
||||
.map(|s| {
|
||||
KeybindingKeystroke::new(
|
||||
Keystroke::parse(s).expect("Keystrokes valid"),
|
||||
false,
|
||||
&DummyKeyboardMapper,
|
||||
)
|
||||
})
|
||||
.map(|s| Keystroke::parse(s).expect("Keystrokes valid"))
|
||||
.collect()
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
mod base_keymap_setting;
|
||||
mod editable_setting_control;
|
||||
mod key_equivalents;
|
||||
mod keymap_file;
|
||||
mod settings_file;
|
||||
mod settings_json;
|
||||
|
@ -13,6 +14,7 @@ use util::asset_str;
|
|||
|
||||
pub use base_keymap_setting::*;
|
||||
pub use editable_setting_control::*;
|
||||
pub use key_equivalents::*;
|
||||
pub use keymap_file::{
|
||||
KeyBindingValidator, KeyBindingValidatorRegistration, KeybindSource, KeybindUpdateOperation,
|
||||
KeybindUpdateTarget, KeymapFile, KeymapFileLoadResult,
|
||||
|
@ -87,10 +89,7 @@ pub fn default_settings() -> Cow<'static, str> {
|
|||
#[cfg(target_os = "macos")]
|
||||
pub const DEFAULT_KEYMAP_PATH: &str = "keymaps/default-macos.json";
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub const DEFAULT_KEYMAP_PATH: &str = "keymaps/default-windows.json";
|
||||
|
||||
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
pub const DEFAULT_KEYMAP_PATH: &str = "keymaps/default-linux.json";
|
||||
|
||||
pub fn default_keymap() -> Cow<'static, str> {
|
||||
|
|
|
@ -14,9 +14,9 @@ use gpui::{
|
|||
Action, AppContext as _, AsyncApp, Axis, ClickEvent, Context, DismissEvent, Entity,
|
||||
EventEmitter, FocusHandle, Focusable, Global, IsZero,
|
||||
KeyBindingContextPredicate::{And, Descendant, Equal, Identifier, Not, NotEqual, Or},
|
||||
KeyContext, KeybindingKeystroke, Keystroke, MouseButton, PlatformKeyboardMapper, Point,
|
||||
ScrollStrategy, ScrollWheelEvent, Stateful, StyledText, Subscription, Task,
|
||||
TextStyleRefinement, WeakEntity, actions, anchored, deferred, div,
|
||||
KeyContext, Keystroke, MouseButton, Point, ScrollStrategy, ScrollWheelEvent, Stateful,
|
||||
StyledText, Subscription, Task, TextStyleRefinement, WeakEntity, actions, anchored, deferred,
|
||||
div,
|
||||
};
|
||||
use language::{Language, LanguageConfig, ToOffset as _};
|
||||
use notifications::status_toast::{StatusToast, ToastIcon};
|
||||
|
@ -174,7 +174,7 @@ impl FilterState {
|
|||
|
||||
#[derive(Debug, Default, PartialEq, Eq, Clone, Hash)]
|
||||
struct ActionMapping {
|
||||
keystrokes: Vec<KeybindingKeystroke>,
|
||||
keystrokes: Vec<Keystroke>,
|
||||
context: Option<SharedString>,
|
||||
}
|
||||
|
||||
|
@ -236,7 +236,7 @@ struct ConflictState {
|
|||
}
|
||||
|
||||
type ConflictKeybindMapping = HashMap<
|
||||
Vec<KeybindingKeystroke>,
|
||||
Vec<Keystroke>,
|
||||
Vec<(
|
||||
Option<gpui::KeyBindingContextPredicate>,
|
||||
Vec<ConflictOrigin>,
|
||||
|
@ -414,14 +414,12 @@ impl Focusable for KeymapEditor {
|
|||
}
|
||||
}
|
||||
/// Helper function to check if two keystroke sequences match exactly
|
||||
fn keystrokes_match_exactly(
|
||||
keystrokes1: &[KeybindingKeystroke],
|
||||
keystrokes2: &[KeybindingKeystroke],
|
||||
) -> bool {
|
||||
fn keystrokes_match_exactly(keystrokes1: &[Keystroke], keystrokes2: &[Keystroke]) -> bool {
|
||||
keystrokes1.len() == keystrokes2.len()
|
||||
&& keystrokes1.iter().zip(keystrokes2).all(|(k1, k2)| {
|
||||
k1.inner.key == k2.inner.key && k1.inner.modifiers == k2.inner.modifiers
|
||||
})
|
||||
&& keystrokes1
|
||||
.iter()
|
||||
.zip(keystrokes2)
|
||||
.all(|(k1, k2)| k1.key == k2.key && k1.modifiers == k2.modifiers)
|
||||
}
|
||||
|
||||
impl KeymapEditor {
|
||||
|
@ -511,7 +509,7 @@ impl KeymapEditor {
|
|||
self.filter_editor.read(cx).text(cx)
|
||||
}
|
||||
|
||||
fn current_keystroke_query(&self, cx: &App) -> Vec<KeybindingKeystroke> {
|
||||
fn current_keystroke_query(&self, cx: &App) -> Vec<Keystroke> {
|
||||
match self.search_mode {
|
||||
SearchMode::KeyStroke { .. } => self.keystroke_editor.read(cx).keystrokes().to_vec(),
|
||||
SearchMode::Normal => Default::default(),
|
||||
|
@ -532,7 +530,7 @@ impl KeymapEditor {
|
|||
|
||||
let keystroke_query = keystroke_query
|
||||
.into_iter()
|
||||
.map(|keystroke| keystroke.inner.unparse())
|
||||
.map(|keystroke| keystroke.unparse())
|
||||
.collect::<Vec<String>>()
|
||||
.join(" ");
|
||||
|
||||
|
@ -556,7 +554,7 @@ impl KeymapEditor {
|
|||
async fn update_matches(
|
||||
this: WeakEntity<Self>,
|
||||
action_query: String,
|
||||
keystroke_query: Vec<KeybindingKeystroke>,
|
||||
keystroke_query: Vec<Keystroke>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> anyhow::Result<()> {
|
||||
let action_query = command_palette::normalize_action_query(&action_query);
|
||||
|
@ -605,15 +603,13 @@ impl KeymapEditor {
|
|||
{
|
||||
let query = &keystroke_query[query_cursor];
|
||||
let keystroke = &keystrokes[keystroke_cursor];
|
||||
let matches = query
|
||||
.inner
|
||||
.modifiers
|
||||
.is_subset_of(&keystroke.inner.modifiers)
|
||||
&& ((query.inner.key.is_empty()
|
||||
|| query.inner.key == keystroke.inner.key)
|
||||
&& query.inner.key_char.as_ref().is_none_or(
|
||||
|q_kc| q_kc == &keystroke.inner.key,
|
||||
));
|
||||
let matches =
|
||||
query.modifiers.is_subset_of(&keystroke.modifiers)
|
||||
&& ((query.key.is_empty()
|
||||
|| query.key == keystroke.key)
|
||||
&& query.key_char.as_ref().is_none_or(
|
||||
|q_kc| q_kc == &keystroke.key,
|
||||
));
|
||||
if matches {
|
||||
found_count += 1;
|
||||
query_cursor += 1;
|
||||
|
@ -682,7 +678,7 @@ impl KeymapEditor {
|
|||
.map(KeybindSource::from_meta)
|
||||
.unwrap_or(KeybindSource::Unknown);
|
||||
|
||||
let keystroke_text = ui::text_for_keybinding_keystrokes(key_binding.keystrokes(), cx);
|
||||
let keystroke_text = ui::text_for_keystrokes(key_binding.keystrokes(), cx);
|
||||
let ui_key_binding = ui::KeyBinding::new_from_gpui(key_binding.clone(), cx)
|
||||
.vim_mode(source == KeybindSource::Vim);
|
||||
|
||||
|
@ -1206,11 +1202,8 @@ impl KeymapEditor {
|
|||
.read(cx)
|
||||
.get_scrollbar_offset(Axis::Vertical),
|
||||
));
|
||||
let keyboard_mapper = cx.keyboard_mapper().clone();
|
||||
cx.spawn(async move |_, _| {
|
||||
remove_keybinding(to_remove, &fs, tab_size, keyboard_mapper.as_ref()).await
|
||||
})
|
||||
.detach_and_notify_err(window, cx);
|
||||
cx.spawn(async move |_, _| remove_keybinding(to_remove, &fs, tab_size).await)
|
||||
.detach_and_notify_err(window, cx);
|
||||
}
|
||||
|
||||
fn copy_context_to_clipboard(
|
||||
|
@ -1429,7 +1422,7 @@ impl ProcessedBinding {
|
|||
.map(|keybind| keybind.get_action_mapping())
|
||||
}
|
||||
|
||||
fn keystrokes(&self) -> Option<&[KeybindingKeystroke]> {
|
||||
fn keystrokes(&self) -> Option<&[Keystroke]> {
|
||||
self.ui_key_binding()
|
||||
.map(|binding| binding.keystrokes.as_slice())
|
||||
}
|
||||
|
@ -2227,7 +2220,7 @@ impl KeybindingEditorModal {
|
|||
Ok(action_arguments)
|
||||
}
|
||||
|
||||
fn validate_keystrokes(&self, cx: &App) -> anyhow::Result<Vec<KeybindingKeystroke>> {
|
||||
fn validate_keystrokes(&self, cx: &App) -> anyhow::Result<Vec<Keystroke>> {
|
||||
let new_keystrokes = self
|
||||
.keybind_editor
|
||||
.read_with(cx, |editor, _| editor.keystrokes().to_vec());
|
||||
|
@ -2323,7 +2316,6 @@ impl KeybindingEditorModal {
|
|||
}).unwrap_or(Ok(()))?;
|
||||
|
||||
let create = self.creating;
|
||||
let keyboard_mapper = cx.keyboard_mapper().clone();
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
let action_name = existing_keybind.action().name;
|
||||
|
@ -2336,7 +2328,6 @@ impl KeybindingEditorModal {
|
|||
new_action_args.as_deref(),
|
||||
&fs,
|
||||
tab_size,
|
||||
keyboard_mapper.as_ref(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
|
@ -2454,21 +2445,11 @@ impl KeybindingEditorModal {
|
|||
}
|
||||
}
|
||||
|
||||
fn remove_key_char(
|
||||
KeybindingKeystroke {
|
||||
inner,
|
||||
display_modifiers,
|
||||
display_key,
|
||||
}: KeybindingKeystroke,
|
||||
) -> KeybindingKeystroke {
|
||||
KeybindingKeystroke {
|
||||
inner: Keystroke {
|
||||
modifiers: inner.modifiers,
|
||||
key: inner.key,
|
||||
key_char: None,
|
||||
},
|
||||
display_modifiers,
|
||||
display_key,
|
||||
fn remove_key_char(Keystroke { modifiers, key, .. }: Keystroke) -> Keystroke {
|
||||
Keystroke {
|
||||
modifiers,
|
||||
key,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3011,7 +2992,6 @@ async fn save_keybinding_update(
|
|||
new_args: Option<&str>,
|
||||
fs: &Arc<dyn Fs>,
|
||||
tab_size: usize,
|
||||
keyboard_mapper: &dyn PlatformKeyboardMapper,
|
||||
) -> anyhow::Result<()> {
|
||||
let keymap_contents = settings::KeymapFile::load_keymap_file(fs)
|
||||
.await
|
||||
|
@ -3054,13 +3034,9 @@ async fn save_keybinding_update(
|
|||
|
||||
let (new_keybinding, removed_keybinding, source) = operation.generate_telemetry();
|
||||
|
||||
let updated_keymap_contents = settings::KeymapFile::update_keybinding(
|
||||
operation,
|
||||
keymap_contents,
|
||||
tab_size,
|
||||
keyboard_mapper,
|
||||
)
|
||||
.map_err(|err| anyhow::anyhow!("Could not save updated keybinding: {}", err))?;
|
||||
let updated_keymap_contents =
|
||||
settings::KeymapFile::update_keybinding(operation, keymap_contents, tab_size)
|
||||
.map_err(|err| anyhow::anyhow!("Could not save updated keybinding: {}", err))?;
|
||||
fs.write(
|
||||
paths::keymap_file().as_path(),
|
||||
updated_keymap_contents.as_bytes(),
|
||||
|
@ -3081,7 +3057,6 @@ async fn remove_keybinding(
|
|||
existing: ProcessedBinding,
|
||||
fs: &Arc<dyn Fs>,
|
||||
tab_size: usize,
|
||||
keyboard_mapper: &dyn PlatformKeyboardMapper,
|
||||
) -> anyhow::Result<()> {
|
||||
let Some(keystrokes) = existing.keystrokes() else {
|
||||
anyhow::bail!("Cannot remove a keybinding that does not exist");
|
||||
|
@ -3105,13 +3080,9 @@ async fn remove_keybinding(
|
|||
};
|
||||
|
||||
let (new_keybinding, removed_keybinding, source) = operation.generate_telemetry();
|
||||
let updated_keymap_contents = settings::KeymapFile::update_keybinding(
|
||||
operation,
|
||||
keymap_contents,
|
||||
tab_size,
|
||||
keyboard_mapper,
|
||||
)
|
||||
.context("Failed to update keybinding")?;
|
||||
let updated_keymap_contents =
|
||||
settings::KeymapFile::update_keybinding(operation, keymap_contents, tab_size)
|
||||
.context("Failed to update keybinding")?;
|
||||
fs.write(
|
||||
paths::keymap_file().as_path(),
|
||||
updated_keymap_contents.as_bytes(),
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use gpui::{
|
||||
Animation, AnimationExt, Context, EventEmitter, FocusHandle, Focusable, FontWeight, KeyContext,
|
||||
KeybindingKeystroke, Keystroke, Modifiers, ModifiersChangedEvent, Subscription, Task, actions,
|
||||
Keystroke, Modifiers, ModifiersChangedEvent, Subscription, Task, actions,
|
||||
};
|
||||
use ui::{
|
||||
ActiveTheme as _, Color, IconButton, IconButtonShape, IconName, IconSize, Label, LabelSize,
|
||||
|
@ -42,8 +42,8 @@ impl PartialEq for CloseKeystrokeResult {
|
|||
}
|
||||
|
||||
pub struct KeystrokeInput {
|
||||
keystrokes: Vec<KeybindingKeystroke>,
|
||||
placeholder_keystrokes: Option<Vec<KeybindingKeystroke>>,
|
||||
keystrokes: Vec<Keystroke>,
|
||||
placeholder_keystrokes: Option<Vec<Keystroke>>,
|
||||
outer_focus_handle: FocusHandle,
|
||||
inner_focus_handle: FocusHandle,
|
||||
intercept_subscription: Option<Subscription>,
|
||||
|
@ -70,7 +70,7 @@ impl KeystrokeInput {
|
|||
const KEYSTROKE_COUNT_MAX: usize = 3;
|
||||
|
||||
pub fn new(
|
||||
placeholder_keystrokes: Option<Vec<KeybindingKeystroke>>,
|
||||
placeholder_keystrokes: Option<Vec<Keystroke>>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
|
@ -97,7 +97,7 @@ impl KeystrokeInput {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn set_keystrokes(&mut self, keystrokes: Vec<KeybindingKeystroke>, cx: &mut Context<Self>) {
|
||||
pub fn set_keystrokes(&mut self, keystrokes: Vec<Keystroke>, cx: &mut Context<Self>) {
|
||||
self.keystrokes = keystrokes;
|
||||
self.keystrokes_changed(cx);
|
||||
}
|
||||
|
@ -106,7 +106,7 @@ impl KeystrokeInput {
|
|||
self.search = search;
|
||||
}
|
||||
|
||||
pub fn keystrokes(&self) -> &[KeybindingKeystroke] {
|
||||
pub fn keystrokes(&self) -> &[Keystroke] {
|
||||
if let Some(placeholders) = self.placeholder_keystrokes.as_ref()
|
||||
&& self.keystrokes.is_empty()
|
||||
{
|
||||
|
@ -116,22 +116,18 @@ impl KeystrokeInput {
|
|||
&& self
|
||||
.keystrokes
|
||||
.last()
|
||||
.is_some_and(|last| last.display_key.is_empty())
|
||||
.is_some_and(|last| last.key.is_empty())
|
||||
{
|
||||
return &self.keystrokes[..self.keystrokes.len() - 1];
|
||||
}
|
||||
&self.keystrokes
|
||||
}
|
||||
|
||||
fn dummy(modifiers: Modifiers) -> KeybindingKeystroke {
|
||||
KeybindingKeystroke {
|
||||
inner: Keystroke {
|
||||
modifiers,
|
||||
key: "".to_string(),
|
||||
key_char: None,
|
||||
},
|
||||
display_modifiers: modifiers,
|
||||
display_key: "".to_string(),
|
||||
fn dummy(modifiers: Modifiers) -> Keystroke {
|
||||
Keystroke {
|
||||
modifiers,
|
||||
key: "".to_string(),
|
||||
key_char: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -258,7 +254,7 @@ impl KeystrokeInput {
|
|||
self.keystrokes_changed(cx);
|
||||
|
||||
if let Some(last) = self.keystrokes.last_mut()
|
||||
&& last.display_key.is_empty()
|
||||
&& last.key.is_empty()
|
||||
&& keystrokes_len <= Self::KEYSTROKE_COUNT_MAX
|
||||
{
|
||||
if !self.search && !event.modifiers.modified() {
|
||||
|
@ -267,15 +263,13 @@ impl KeystrokeInput {
|
|||
}
|
||||
if self.search {
|
||||
if self.previous_modifiers.modified() {
|
||||
last.display_modifiers |= event.modifiers;
|
||||
last.inner.modifiers |= event.modifiers;
|
||||
last.modifiers |= event.modifiers;
|
||||
} else {
|
||||
self.keystrokes.push(Self::dummy(event.modifiers));
|
||||
}
|
||||
self.previous_modifiers |= event.modifiers;
|
||||
} else {
|
||||
last.display_modifiers = event.modifiers;
|
||||
last.inner.modifiers = event.modifiers;
|
||||
last.modifiers = event.modifiers;
|
||||
return;
|
||||
}
|
||||
} else if keystrokes_len < Self::KEYSTROKE_COUNT_MAX {
|
||||
|
@ -303,17 +297,14 @@ impl KeystrokeInput {
|
|||
return;
|
||||
}
|
||||
|
||||
let mut keystroke =
|
||||
KeybindingKeystroke::new(keystroke.clone(), false, cx.keyboard_mapper().as_ref());
|
||||
let mut keystroke = keystroke.clone();
|
||||
if let Some(last) = self.keystrokes.last()
|
||||
&& last.display_key.is_empty()
|
||||
&& last.key.is_empty()
|
||||
&& (!self.search || self.previous_modifiers.modified())
|
||||
{
|
||||
let display_key = keystroke.display_key.clone();
|
||||
let inner_key = keystroke.inner.key.clone();
|
||||
let key = keystroke.key.clone();
|
||||
keystroke = last.clone();
|
||||
keystroke.display_key = display_key;
|
||||
keystroke.inner.key = inner_key;
|
||||
keystroke.key = key;
|
||||
self.keystrokes.pop();
|
||||
}
|
||||
|
||||
|
@ -333,14 +324,11 @@ impl KeystrokeInput {
|
|||
self.keystrokes_changed(cx);
|
||||
|
||||
if self.search {
|
||||
self.previous_modifiers = keystroke.display_modifiers;
|
||||
self.previous_modifiers = keystroke.modifiers;
|
||||
return;
|
||||
}
|
||||
if self.keystrokes.len() < Self::KEYSTROKE_COUNT_MAX
|
||||
&& keystroke.display_modifiers.modified()
|
||||
{
|
||||
self.keystrokes
|
||||
.push(Self::dummy(keystroke.display_modifiers));
|
||||
if self.keystrokes.len() < Self::KEYSTROKE_COUNT_MAX && keystroke.modifiers.modified() {
|
||||
self.keystrokes.push(Self::dummy(keystroke.modifiers));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -376,7 +364,7 @@ impl KeystrokeInput {
|
|||
&self.keystrokes
|
||||
};
|
||||
keystrokes.iter().map(move |keystroke| {
|
||||
h_flex().children(ui::render_keybinding_keystroke(
|
||||
h_flex().children(ui::render_keystroke(
|
||||
keystroke,
|
||||
Some(Color::Default),
|
||||
Some(rems(0.875).into()),
|
||||
|
@ -821,13 +809,9 @@ mod tests {
|
|||
/// Verifies that the keystrokes match the expected strings
|
||||
#[track_caller]
|
||||
pub fn expect_keystrokes(&mut self, expected: &[&str]) -> &mut Self {
|
||||
let actual: Vec<Keystroke> = self.input.read_with(&self.cx, |input, _| {
|
||||
input
|
||||
.keystrokes
|
||||
.iter()
|
||||
.map(|keystroke| keystroke.inner.clone())
|
||||
.collect()
|
||||
});
|
||||
let actual = self
|
||||
.input
|
||||
.read_with(&self.cx, |input, _| input.keystrokes.clone());
|
||||
Self::expect_keystrokes_equal(&actual, expected);
|
||||
self
|
||||
}
|
||||
|
@ -955,7 +939,7 @@ mod tests {
|
|||
}
|
||||
|
||||
struct KeystrokeUpdateTracker {
|
||||
initial_keystrokes: Vec<KeybindingKeystroke>,
|
||||
initial_keystrokes: Vec<Keystroke>,
|
||||
_subscription: Subscription,
|
||||
input: Entity<KeystrokeInput>,
|
||||
received_keystrokes_updated: bool,
|
||||
|
@ -999,8 +983,8 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
fn keystrokes_str(ks: &[KeybindingKeystroke]) -> String {
|
||||
ks.iter().map(|ks| ks.inner.unparse()).join(" ")
|
||||
fn keystrokes_str(ks: &[Keystroke]) -> String {
|
||||
ks.iter().map(|ks| ks.unparse()).join(" ")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -119,7 +119,7 @@ impl Render for OnboardingBanner {
|
|||
h_flex()
|
||||
.h_full()
|
||||
.gap_1()
|
||||
.child(Icon::new(self.details.icon_name).size(IconSize::XSmall))
|
||||
.child(Icon::new(self.details.icon_name).size(IconSize::Small))
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_0p5()
|
||||
|
|
|
@ -275,11 +275,11 @@ impl TitleBar {
|
|||
|
||||
let banner = cx.new(|cx| {
|
||||
OnboardingBanner::new(
|
||||
"ACP Onboarding",
|
||||
IconName::Sparkle,
|
||||
"Bring Your Own Agent",
|
||||
Some("Introducing:".into()),
|
||||
zed_actions::agent::OpenAcpOnboardingModal.boxed_clone(),
|
||||
"Debugger Onboarding",
|
||||
IconName::Debug,
|
||||
"The Debugger",
|
||||
None,
|
||||
zed_actions::debugger::OpenOnboardingModal.boxed_clone(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
|
|
@ -13,9 +13,6 @@ use crate::prelude::*;
|
|||
)]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
pub enum VectorName {
|
||||
AcpGrid,
|
||||
AcpLogo,
|
||||
AcpLogoSerif,
|
||||
AiGrid,
|
||||
DebuggerGrid,
|
||||
Grid,
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
use crate::PlatformStyle;
|
||||
use crate::{Icon, IconName, IconSize, h_flex, prelude::*};
|
||||
use gpui::{
|
||||
Action, AnyElement, App, FocusHandle, Global, IntoElement, KeybindingKeystroke, Keystroke,
|
||||
Modifiers, Window, relative,
|
||||
Action, AnyElement, App, FocusHandle, Global, IntoElement, Keystroke, Modifiers, Window,
|
||||
relative,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
|
||||
|
@ -13,7 +13,7 @@ pub struct KeyBinding {
|
|||
/// More than one keystroke produces a chord.
|
||||
///
|
||||
/// This should always contain at least one keystroke.
|
||||
pub keystrokes: Vec<KeybindingKeystroke>,
|
||||
pub keystrokes: Vec<Keystroke>,
|
||||
|
||||
/// The [`PlatformStyle`] to use when displaying this keybinding.
|
||||
platform_style: PlatformStyle,
|
||||
|
@ -59,7 +59,7 @@ impl KeyBinding {
|
|||
cx.try_global::<VimStyle>().is_some_and(|g| g.0)
|
||||
}
|
||||
|
||||
pub fn new(keystrokes: Vec<KeybindingKeystroke>, cx: &App) -> Self {
|
||||
pub fn new(keystrokes: Vec<Keystroke>, cx: &App) -> Self {
|
||||
Self {
|
||||
keystrokes,
|
||||
platform_style: PlatformStyle::platform(),
|
||||
|
@ -99,16 +99,16 @@ impl KeyBinding {
|
|||
}
|
||||
|
||||
fn render_key(
|
||||
key: &str,
|
||||
keystroke: &Keystroke,
|
||||
color: Option<Color>,
|
||||
platform_style: PlatformStyle,
|
||||
size: impl Into<Option<AbsoluteLength>>,
|
||||
) -> AnyElement {
|
||||
let key_icon = icon_for_key(key, platform_style);
|
||||
let key_icon = icon_for_key(keystroke, platform_style);
|
||||
match key_icon {
|
||||
Some(icon) => KeyIcon::new(icon, color).size(size).into_any_element(),
|
||||
None => {
|
||||
let key = util::capitalize(key);
|
||||
let key = util::capitalize(&keystroke.key);
|
||||
Key::new(&key, color).size(size).into_any_element()
|
||||
}
|
||||
}
|
||||
|
@ -124,7 +124,7 @@ impl RenderOnce for KeyBinding {
|
|||
"KEY_BINDING-{}",
|
||||
self.keystrokes
|
||||
.iter()
|
||||
.map(|k| k.display_key.to_string())
|
||||
.map(|k| k.key.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ")
|
||||
)
|
||||
|
@ -137,7 +137,7 @@ impl RenderOnce for KeyBinding {
|
|||
.py_0p5()
|
||||
.rounded_xs()
|
||||
.text_color(cx.theme().colors().text_muted)
|
||||
.children(render_keybinding_keystroke(
|
||||
.children(render_keystroke(
|
||||
keystroke,
|
||||
color,
|
||||
self.size,
|
||||
|
@ -148,8 +148,8 @@ impl RenderOnce for KeyBinding {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn render_keybinding_keystroke(
|
||||
keystroke: &KeybindingKeystroke,
|
||||
pub fn render_keystroke(
|
||||
keystroke: &Keystroke,
|
||||
color: Option<Color>,
|
||||
size: impl Into<Option<AbsoluteLength>>,
|
||||
platform_style: PlatformStyle,
|
||||
|
@ -163,39 +163,26 @@ pub fn render_keybinding_keystroke(
|
|||
let size = size.into();
|
||||
|
||||
if use_text {
|
||||
let element = Key::new(
|
||||
keystroke_text(
|
||||
&keystroke.display_modifiers,
|
||||
&keystroke.display_key,
|
||||
platform_style,
|
||||
vim_mode,
|
||||
),
|
||||
color,
|
||||
)
|
||||
.size(size)
|
||||
.into_any_element();
|
||||
let element = Key::new(keystroke_text(keystroke, platform_style, vim_mode), color)
|
||||
.size(size)
|
||||
.into_any_element();
|
||||
vec![element]
|
||||
} else {
|
||||
let mut elements = Vec::new();
|
||||
elements.extend(render_modifiers(
|
||||
&keystroke.display_modifiers,
|
||||
&keystroke.modifiers,
|
||||
platform_style,
|
||||
color,
|
||||
size,
|
||||
true,
|
||||
));
|
||||
elements.push(render_key(
|
||||
&keystroke.display_key,
|
||||
color,
|
||||
platform_style,
|
||||
size,
|
||||
));
|
||||
elements.push(render_key(keystroke, color, platform_style, size));
|
||||
elements
|
||||
}
|
||||
}
|
||||
|
||||
fn icon_for_key(key: &str, platform_style: PlatformStyle) -> Option<IconName> {
|
||||
match key {
|
||||
fn icon_for_key(keystroke: &Keystroke, platform_style: PlatformStyle) -> Option<IconName> {
|
||||
match keystroke.key.as_str() {
|
||||
"left" => Some(IconName::ArrowLeft),
|
||||
"right" => Some(IconName::ArrowRight),
|
||||
"up" => Some(IconName::ArrowUp),
|
||||
|
@ -392,7 +379,7 @@ impl KeyIcon {
|
|||
/// Returns a textual representation of the key binding for the given [`Action`].
|
||||
pub fn text_for_action(action: &dyn Action, window: &Window, cx: &App) -> Option<String> {
|
||||
let key_binding = window.highest_precedence_binding_for_action(action)?;
|
||||
Some(text_for_keybinding_keystrokes(key_binding.keystrokes(), cx))
|
||||
Some(text_for_keystrokes(key_binding.keystrokes(), cx))
|
||||
}
|
||||
|
||||
pub fn text_for_keystrokes(keystrokes: &[Keystroke], cx: &App) -> String {
|
||||
|
@ -400,50 +387,22 @@ pub fn text_for_keystrokes(keystrokes: &[Keystroke], cx: &App) -> String {
|
|||
let vim_enabled = cx.try_global::<VimStyle>().is_some();
|
||||
keystrokes
|
||||
.iter()
|
||||
.map(|keystroke| {
|
||||
keystroke_text(
|
||||
&keystroke.modifiers,
|
||||
&keystroke.key,
|
||||
platform_style,
|
||||
vim_enabled,
|
||||
)
|
||||
})
|
||||
.map(|keystroke| keystroke_text(keystroke, platform_style, vim_enabled))
|
||||
.join(" ")
|
||||
}
|
||||
|
||||
pub fn text_for_keybinding_keystrokes(keystrokes: &[KeybindingKeystroke], cx: &App) -> String {
|
||||
pub fn text_for_keystroke(keystroke: &Keystroke, cx: &App) -> String {
|
||||
let platform_style = PlatformStyle::platform();
|
||||
let vim_enabled = cx.try_global::<VimStyle>().is_some();
|
||||
keystrokes
|
||||
.iter()
|
||||
.map(|keystroke| {
|
||||
keystroke_text(
|
||||
&keystroke.display_modifiers,
|
||||
&keystroke.display_key,
|
||||
platform_style,
|
||||
vim_enabled,
|
||||
)
|
||||
})
|
||||
.join(" ")
|
||||
}
|
||||
|
||||
pub fn text_for_keystroke(modifiers: &Modifiers, key: &str, cx: &App) -> String {
|
||||
let platform_style = PlatformStyle::platform();
|
||||
let vim_enabled = cx.try_global::<VimStyle>().is_some();
|
||||
keystroke_text(modifiers, key, platform_style, vim_enabled)
|
||||
keystroke_text(keystroke, platform_style, vim_enabled)
|
||||
}
|
||||
|
||||
/// Returns a textual representation of the given [`Keystroke`].
|
||||
fn keystroke_text(
|
||||
modifiers: &Modifiers,
|
||||
key: &str,
|
||||
platform_style: PlatformStyle,
|
||||
vim_mode: bool,
|
||||
) -> String {
|
||||
fn keystroke_text(keystroke: &Keystroke, platform_style: PlatformStyle, vim_mode: bool) -> String {
|
||||
let mut text = String::new();
|
||||
let delimiter = '-';
|
||||
|
||||
if modifiers.function {
|
||||
if keystroke.modifiers.function {
|
||||
match vim_mode {
|
||||
false => text.push_str("Fn"),
|
||||
true => text.push_str("fn"),
|
||||
|
@ -452,7 +411,7 @@ fn keystroke_text(
|
|||
text.push(delimiter);
|
||||
}
|
||||
|
||||
if modifiers.control {
|
||||
if keystroke.modifiers.control {
|
||||
match (platform_style, vim_mode) {
|
||||
(PlatformStyle::Mac, false) => text.push_str("Control"),
|
||||
(PlatformStyle::Linux | PlatformStyle::Windows, false) => text.push_str("Ctrl"),
|
||||
|
@ -462,7 +421,7 @@ fn keystroke_text(
|
|||
text.push(delimiter);
|
||||
}
|
||||
|
||||
if modifiers.platform {
|
||||
if keystroke.modifiers.platform {
|
||||
match (platform_style, vim_mode) {
|
||||
(PlatformStyle::Mac, false) => text.push_str("Command"),
|
||||
(PlatformStyle::Mac, true) => text.push_str("cmd"),
|
||||
|
@ -475,7 +434,7 @@ fn keystroke_text(
|
|||
text.push(delimiter);
|
||||
}
|
||||
|
||||
if modifiers.alt {
|
||||
if keystroke.modifiers.alt {
|
||||
match (platform_style, vim_mode) {
|
||||
(PlatformStyle::Mac, false) => text.push_str("Option"),
|
||||
(PlatformStyle::Linux | PlatformStyle::Windows, false) => text.push_str("Alt"),
|
||||
|
@ -485,7 +444,7 @@ fn keystroke_text(
|
|||
text.push(delimiter);
|
||||
}
|
||||
|
||||
if modifiers.shift {
|
||||
if keystroke.modifiers.shift {
|
||||
match (platform_style, vim_mode) {
|
||||
(_, false) => text.push_str("Shift"),
|
||||
(_, true) => text.push_str("shift"),
|
||||
|
@ -494,9 +453,9 @@ fn keystroke_text(
|
|||
}
|
||||
|
||||
if vim_mode {
|
||||
text.push_str(key)
|
||||
text.push_str(&keystroke.key)
|
||||
} else {
|
||||
let key = match key {
|
||||
let key = match keystroke.key.as_str() {
|
||||
"pageup" => "PageUp",
|
||||
"pagedown" => "PageDown",
|
||||
key => &util::capitalize(key),
|
||||
|
@ -603,11 +562,9 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_text_for_keystroke() {
|
||||
let keystroke = Keystroke::parse("cmd-c").unwrap();
|
||||
assert_eq!(
|
||||
keystroke_text(
|
||||
&keystroke.modifiers,
|
||||
&keystroke.key,
|
||||
&Keystroke::parse("cmd-c").unwrap(),
|
||||
PlatformStyle::Mac,
|
||||
false
|
||||
),
|
||||
|
@ -615,8 +572,7 @@ mod tests {
|
|||
);
|
||||
assert_eq!(
|
||||
keystroke_text(
|
||||
&keystroke.modifiers,
|
||||
&keystroke.key,
|
||||
&Keystroke::parse("cmd-c").unwrap(),
|
||||
PlatformStyle::Linux,
|
||||
false
|
||||
),
|
||||
|
@ -624,19 +580,16 @@ mod tests {
|
|||
);
|
||||
assert_eq!(
|
||||
keystroke_text(
|
||||
&keystroke.modifiers,
|
||||
&keystroke.key,
|
||||
&Keystroke::parse("cmd-c").unwrap(),
|
||||
PlatformStyle::Windows,
|
||||
false
|
||||
),
|
||||
"Win-C".to_string()
|
||||
);
|
||||
|
||||
let keystroke = Keystroke::parse("ctrl-alt-delete").unwrap();
|
||||
assert_eq!(
|
||||
keystroke_text(
|
||||
&keystroke.modifiers,
|
||||
&keystroke.key,
|
||||
&Keystroke::parse("ctrl-alt-delete").unwrap(),
|
||||
PlatformStyle::Mac,
|
||||
false
|
||||
),
|
||||
|
@ -644,8 +597,7 @@ mod tests {
|
|||
);
|
||||
assert_eq!(
|
||||
keystroke_text(
|
||||
&keystroke.modifiers,
|
||||
&keystroke.key,
|
||||
&Keystroke::parse("ctrl-alt-delete").unwrap(),
|
||||
PlatformStyle::Linux,
|
||||
false
|
||||
),
|
||||
|
@ -653,19 +605,16 @@ mod tests {
|
|||
);
|
||||
assert_eq!(
|
||||
keystroke_text(
|
||||
&keystroke.modifiers,
|
||||
&keystroke.key,
|
||||
&Keystroke::parse("ctrl-alt-delete").unwrap(),
|
||||
PlatformStyle::Windows,
|
||||
false
|
||||
),
|
||||
"Ctrl-Alt-Delete".to_string()
|
||||
);
|
||||
|
||||
let keystroke = Keystroke::parse("shift-pageup").unwrap();
|
||||
assert_eq!(
|
||||
keystroke_text(
|
||||
&keystroke.modifiers,
|
||||
&keystroke.key,
|
||||
&Keystroke::parse("shift-pageup").unwrap(),
|
||||
PlatformStyle::Mac,
|
||||
false
|
||||
),
|
||||
|
@ -673,8 +622,7 @@ mod tests {
|
|||
);
|
||||
assert_eq!(
|
||||
keystroke_text(
|
||||
&keystroke.modifiers,
|
||||
&keystroke.key,
|
||||
&Keystroke::parse("shift-pageup").unwrap(),
|
||||
PlatformStyle::Linux,
|
||||
false,
|
||||
),
|
||||
|
@ -682,8 +630,7 @@ mod tests {
|
|||
);
|
||||
assert_eq!(
|
||||
keystroke_text(
|
||||
&keystroke.modifiers,
|
||||
&keystroke.key,
|
||||
&Keystroke::parse("shift-pageup").unwrap(),
|
||||
PlatformStyle::Windows,
|
||||
false
|
||||
),
|
||||
|
|
|
@ -203,10 +203,7 @@ impl Vim {
|
|||
|
||||
// hook into the existing to clear out any vim search state on cmd+f or edit -> find.
|
||||
fn search_deploy(&mut self, _: &buffer_search::Deploy, _: &mut Window, cx: &mut Context<Self>) {
|
||||
// Preserve the current mode when resetting search state
|
||||
let current_mode = self.mode;
|
||||
self.search = Default::default();
|
||||
self.search.prior_mode = current_mode;
|
||||
cx.propagate();
|
||||
}
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@ path = "src/workspace.rs"
|
|||
doctest = false
|
||||
|
||||
[features]
|
||||
default = ["call"]
|
||||
test-support = [
|
||||
"call/test-support",
|
||||
"client/test-support",
|
||||
|
@ -29,7 +30,7 @@ test-support = [
|
|||
any_vec.workspace = true
|
||||
anyhow.workspace = true
|
||||
async-recursion.workspace = true
|
||||
call.workspace = true
|
||||
call = { workspace = true, optional = true }
|
||||
client.workspace = true
|
||||
clock.workspace = true
|
||||
collections.workspace = true
|
||||
|
|
|
@ -4,11 +4,14 @@ use crate::{
|
|||
workspace_settings::{PaneSplitDirectionHorizontal, PaneSplitDirectionVertical},
|
||||
};
|
||||
use anyhow::Result;
|
||||
|
||||
#[cfg(feature = "call")]
|
||||
use call::{ActiveCall, ParticipantLocation};
|
||||
|
||||
use collections::HashMap;
|
||||
use gpui::{
|
||||
Along, AnyView, AnyWeakView, Axis, Bounds, Entity, Hsla, IntoElement, MouseButton, Pixels,
|
||||
Point, StyleRefinement, WeakEntity, Window, point, size,
|
||||
Along, AnyView, AnyWeakView, Axis, Bounds, Entity, Hsla, IntoElement, Pixels, Point,
|
||||
StyleRefinement, WeakEntity, Window, point, size,
|
||||
};
|
||||
use parking_lot::Mutex;
|
||||
use project::Project;
|
||||
|
@ -197,6 +200,7 @@ pub enum Member {
|
|||
pub struct PaneRenderContext<'a> {
|
||||
pub project: &'a Entity<Project>,
|
||||
pub follower_states: &'a HashMap<CollaboratorId, FollowerState>,
|
||||
#[cfg(feature = "call")]
|
||||
pub active_call: Option<&'a Entity<ActiveCall>>,
|
||||
pub active_pane: &'a Entity<Pane>,
|
||||
pub app_state: &'a Arc<AppState>,
|
||||
|
@ -258,6 +262,11 @@ impl PaneLeaderDecorator for PaneRenderContext<'_> {
|
|||
let mut leader_color;
|
||||
let status_box;
|
||||
match leader_id {
|
||||
#[cfg(not(feature = "call"))]
|
||||
CollaboratorId::PeerId(_) => {
|
||||
return LeaderDecoration::default();
|
||||
}
|
||||
#[cfg(feature = "call")]
|
||||
CollaboratorId::PeerId(peer_id) => {
|
||||
let Some(leader) = self.active_call.as_ref().and_then(|call| {
|
||||
let room = call.read(cx).room()?.read(cx);
|
||||
|
@ -315,7 +324,7 @@ impl PaneLeaderDecorator for PaneRenderContext<'_> {
|
|||
|this, (leader_project_id, leader_user_id)| {
|
||||
let app_state = self.app_state.clone();
|
||||
this.cursor_pointer().on_mouse_down(
|
||||
MouseButton::Left,
|
||||
gpui::MouseButton::Left,
|
||||
move |_, _, cx| {
|
||||
crate::join_in_room_project(
|
||||
leader_project_id,
|
||||
|
|
|
@ -599,13 +599,6 @@ impl Domain for WorkspaceDb {
|
|||
ssh_projects ON
|
||||
workspaces.ssh_project_id = ssh_projects.id;
|
||||
|
||||
DELETE FROM workspaces_2
|
||||
WHERE workspace_id NOT IN (
|
||||
SELECT MAX(workspace_id)
|
||||
FROM workspaces_2
|
||||
GROUP BY ssh_connection_id, paths
|
||||
);
|
||||
|
||||
DROP TABLE ssh_projects;
|
||||
DROP TABLE workspaces;
|
||||
ALTER TABLE workspaces_2 RENAME TO workspaces;
|
||||
|
|
|
@ -9,6 +9,7 @@ pub mod pane_group;
|
|||
mod path_list;
|
||||
mod persistence;
|
||||
pub mod searchable;
|
||||
#[cfg(feature = "call")]
|
||||
pub mod shared_screen;
|
||||
mod status_bar;
|
||||
pub mod tasks;
|
||||
|
@ -22,11 +23,17 @@ pub use dock::Panel;
|
|||
pub use path_list::PathList;
|
||||
pub use toast_layer::{ToastAction, ToastLayer, ToastView};
|
||||
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
#[cfg(feature = "call")]
|
||||
use call::{ActiveCall, call_settings::CallSettings};
|
||||
#[cfg(feature = "call")]
|
||||
use client::{Status, proto::ErrorCode};
|
||||
#[cfg(feature = "call")]
|
||||
use shared_screen::SharedScreen;
|
||||
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use client::{
|
||||
ChannelId, Client, ErrorExt, Status, TypedEnvelope, UserStore,
|
||||
proto::{self, ErrorCode, PanelId, PeerId},
|
||||
ChannelId, Client, ErrorExt, TypedEnvelope, UserStore,
|
||||
proto::{self, PanelId, PeerId},
|
||||
};
|
||||
use collections::{HashMap, HashSet, hash_map};
|
||||
use dock::{Dock, DockPosition, PanelButtons, PanelHandle, RESIZE_HANDLE_SIZE};
|
||||
|
@ -79,7 +86,6 @@ use schemars::JsonSchema;
|
|||
use serde::Deserialize;
|
||||
use session::AppSession;
|
||||
use settings::{Settings, update_settings_file};
|
||||
use shared_screen::SharedScreen;
|
||||
use sqlez::{
|
||||
bindable::{Bind, Column, StaticColumnCount},
|
||||
statement::Statement,
|
||||
|
@ -886,6 +892,7 @@ impl Global for GlobalAppState {}
|
|||
|
||||
pub struct WorkspaceStore {
|
||||
workspaces: HashSet<WindowHandle<Workspace>>,
|
||||
#[cfg(feature = "call")]
|
||||
client: Arc<Client>,
|
||||
_subscriptions: Vec<client::Subscription>,
|
||||
}
|
||||
|
@ -1117,6 +1124,7 @@ pub struct Workspace {
|
|||
window_edited: bool,
|
||||
last_window_title: Option<String>,
|
||||
dirty_items: HashMap<EntityId, Subscription>,
|
||||
#[cfg(feature = "call")]
|
||||
active_call: Option<(Entity<ActiveCall>, Vec<Subscription>)>,
|
||||
leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>,
|
||||
database_id: Option<WorkspaceId>,
|
||||
|
@ -1158,6 +1166,7 @@ pub struct FollowerState {
|
|||
|
||||
struct FollowerView {
|
||||
view: Box<dyn FollowableItemHandle>,
|
||||
#[cfg(feature = "call")]
|
||||
location: Option<proto::PanelId>,
|
||||
}
|
||||
|
||||
|
@ -1357,10 +1366,15 @@ impl Workspace {
|
|||
|
||||
let session_id = app_state.session.read(cx).id().to_owned();
|
||||
|
||||
#[cfg(feature = "call")]
|
||||
let mut active_call = None;
|
||||
if let Some(call) = ActiveCall::try_global(cx) {
|
||||
let subscriptions = vec![cx.subscribe_in(&call, window, Self::on_active_call_event)];
|
||||
active_call = Some((call, subscriptions));
|
||||
#[cfg(feature = "call")]
|
||||
{
|
||||
if let Some(call) = ActiveCall::try_global(cx) {
|
||||
let subscriptions =
|
||||
vec![cx.subscribe_in(&call, window, Self::on_active_call_event)];
|
||||
active_call = Some((call, subscriptions));
|
||||
}
|
||||
}
|
||||
|
||||
let (serializable_items_tx, serializable_items_rx) =
|
||||
|
@ -1446,6 +1460,7 @@ impl Workspace {
|
|||
window_edited: false,
|
||||
last_window_title: None,
|
||||
dirty_items: Default::default(),
|
||||
#[cfg(feature = "call")]
|
||||
active_call,
|
||||
database_id: workspace_id,
|
||||
app_state,
|
||||
|
@ -2250,6 +2265,7 @@ impl Workspace {
|
|||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<bool>> {
|
||||
#[cfg(feature = "call")]
|
||||
let active_call = self.active_call().cloned();
|
||||
|
||||
// On Linux and Windows, closing the last window should restore the last workspace.
|
||||
|
@ -2258,51 +2274,58 @@ impl Workspace {
|
|||
&& cx.windows().len() == 1;
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let workspace_count = cx.update(|_window, cx| {
|
||||
cx.windows()
|
||||
.iter()
|
||||
.filter(|window| window.downcast::<Workspace>().is_some())
|
||||
.count()
|
||||
})?;
|
||||
|
||||
if let Some(active_call) = active_call
|
||||
&& workspace_count == 1
|
||||
&& active_call.read_with(cx, |call, _| call.room().is_some())?
|
||||
#[cfg(feature = "call")]
|
||||
{
|
||||
if close_intent == CloseIntent::CloseWindow {
|
||||
let answer = cx.update(|window, cx| {
|
||||
window.prompt(
|
||||
PromptLevel::Warning,
|
||||
"Do you want to leave the current call?",
|
||||
None,
|
||||
&["Close window and hang up", "Cancel"],
|
||||
cx,
|
||||
)
|
||||
})?;
|
||||
let workspace_count = cx.update(|_window, cx| {
|
||||
cx.windows()
|
||||
.iter()
|
||||
.filter(|window| window.downcast::<Workspace>().is_some())
|
||||
.count()
|
||||
})?;
|
||||
if let Some(active_call) = active_call
|
||||
&& workspace_count == 1
|
||||
&& active_call.read_with(cx, |call, _| call.room().is_some())?
|
||||
{
|
||||
if close_intent == CloseIntent::CloseWindow {
|
||||
let answer = cx.update(|window, cx| {
|
||||
window.prompt(
|
||||
PromptLevel::Warning,
|
||||
"Do you want to leave the current call?",
|
||||
None,
|
||||
&["Close window and hang up", "Cancel"],
|
||||
cx,
|
||||
)
|
||||
})?;
|
||||
|
||||
if answer.await.log_err() == Some(1) {
|
||||
return anyhow::Ok(false);
|
||||
} else {
|
||||
active_call
|
||||
.update(cx, |call, cx| call.hang_up(cx))?
|
||||
.await
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
if close_intent == CloseIntent::ReplaceWindow {
|
||||
_ = active_call.update(cx, |this, cx| {
|
||||
let workspace = cx
|
||||
.windows()
|
||||
.iter()
|
||||
.filter_map(|window| window.downcast::<Workspace>())
|
||||
.next()
|
||||
.unwrap();
|
||||
let project = workspace.read(cx)?.project.clone();
|
||||
if project.read(cx).is_shared() {
|
||||
this.unshare_project(project, cx)?;
|
||||
if answer.await.log_err() == Some(1) {
|
||||
return anyhow::Ok(false);
|
||||
} else {
|
||||
{
|
||||
active_call
|
||||
.update(cx, |call, cx| call.hang_up(cx))?
|
||||
.await
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
Ok::<_, anyhow::Error>(())
|
||||
})?;
|
||||
}
|
||||
if close_intent == CloseIntent::ReplaceWindow {
|
||||
#[cfg(feature = "call")]
|
||||
{
|
||||
_ = active_call.update(cx, |active_call, cx| {
|
||||
let workspace = cx
|
||||
.windows()
|
||||
.iter()
|
||||
.filter_map(|window| window.downcast::<Workspace>())
|
||||
.next()
|
||||
.unwrap();
|
||||
let project = workspace.read(cx)?.project.clone();
|
||||
if project.read(cx).is_shared() {
|
||||
active_call.unshare_project(project, cx)?;
|
||||
}
|
||||
Ok::<_, anyhow::Error>(())
|
||||
})?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3486,6 +3509,7 @@ impl Workspace {
|
|||
item
|
||||
}
|
||||
|
||||
#[cfg(feature = "call")]
|
||||
pub fn open_shared_screen(
|
||||
&mut self,
|
||||
peer_id: PeerId,
|
||||
|
@ -3907,8 +3931,11 @@ impl Workspace {
|
|||
pane.update(cx, |pane, _| {
|
||||
pane.track_alternate_file_items();
|
||||
});
|
||||
if *local {
|
||||
self.unfollow_in_pane(pane, window, cx);
|
||||
#[cfg(feature = "call")]
|
||||
{
|
||||
if *local {
|
||||
self.unfollow_in_pane(pane, window, cx);
|
||||
}
|
||||
}
|
||||
serialize_workspace = *focus_changed || pane != self.active_pane();
|
||||
if pane == self.active_pane() {
|
||||
|
@ -3973,6 +4000,17 @@ impl Workspace {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "call"))]
|
||||
pub fn unfollow_in_pane(
|
||||
&mut self,
|
||||
_pane: &Entity<Pane>,
|
||||
_window: &mut Window,
|
||||
_cx: &mut Context<Workspace>,
|
||||
) -> Option<CollaboratorId> {
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(feature = "call")]
|
||||
pub fn unfollow_in_pane(
|
||||
&mut self,
|
||||
pane: &Entity<Pane>,
|
||||
|
@ -4122,6 +4160,7 @@ impl Workspace {
|
|||
cx.notify();
|
||||
}
|
||||
|
||||
#[cfg(feature = "call")]
|
||||
pub fn start_following(
|
||||
&mut self,
|
||||
leader_id: impl Into<CollaboratorId>,
|
||||
|
@ -4185,6 +4224,16 @@ impl Workspace {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "call"))]
|
||||
pub fn follow_next_collaborator(
|
||||
&mut self,
|
||||
_: &FollowNextCollaborator,
|
||||
_window: &mut Window,
|
||||
_cx: &mut Context<Self>,
|
||||
) {
|
||||
}
|
||||
|
||||
#[cfg(feature = "call")]
|
||||
pub fn follow_next_collaborator(
|
||||
&mut self,
|
||||
_: &FollowNextCollaborator,
|
||||
|
@ -4233,6 +4282,16 @@ impl Workspace {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "call"))]
|
||||
pub fn follow(
|
||||
&mut self,
|
||||
_leader_id: impl Into<CollaboratorId>,
|
||||
_window: &mut Window,
|
||||
_cx: &mut Context<Self>,
|
||||
) {
|
||||
}
|
||||
|
||||
#[cfg(feature = "call")]
|
||||
pub fn follow(
|
||||
&mut self,
|
||||
leader_id: impl Into<CollaboratorId>,
|
||||
|
@ -4285,6 +4344,17 @@ impl Workspace {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "call"))]
|
||||
pub fn unfollow(
|
||||
&mut self,
|
||||
_leader_id: impl Into<CollaboratorId>,
|
||||
_window: &mut Window,
|
||||
_cx: &mut Context<Self>,
|
||||
) -> Option<()> {
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(feature = "call")]
|
||||
pub fn unfollow(
|
||||
&mut self,
|
||||
leader_id: impl Into<CollaboratorId>,
|
||||
|
@ -4595,6 +4665,7 @@ impl Workspace {
|
|||
anyhow::bail!("no id for view");
|
||||
};
|
||||
let id = ViewId::from_proto(id)?;
|
||||
#[cfg(feature = "call")]
|
||||
let panel_id = view.panel_id.and_then(proto::PanelId::from_i32);
|
||||
|
||||
let pane = this.update(cx, |this, _cx| {
|
||||
|
@ -4667,6 +4738,7 @@ impl Workspace {
|
|||
id,
|
||||
FollowerView {
|
||||
view: item,
|
||||
#[cfg(feature = "call")]
|
||||
location: panel_id,
|
||||
},
|
||||
);
|
||||
|
@ -4721,6 +4793,7 @@ impl Workspace {
|
|||
view.map(|view| {
|
||||
entry.insert(FollowerView {
|
||||
view,
|
||||
#[cfg(feature = "call")]
|
||||
location: None,
|
||||
})
|
||||
})
|
||||
|
@ -4911,6 +4984,17 @@ impl Workspace {
|
|||
)
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "call"))]
|
||||
fn active_item_for_peer(
|
||||
&self,
|
||||
_peer_id: PeerId,
|
||||
_window: &mut Window,
|
||||
_cx: &mut Context<Self>,
|
||||
) -> Option<(Option<PanelId>, Box<dyn ItemHandle>)> {
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(feature = "call")]
|
||||
fn active_item_for_peer(
|
||||
&self,
|
||||
peer_id: PeerId,
|
||||
|
@ -4952,6 +5036,7 @@ impl Workspace {
|
|||
item_to_activate
|
||||
}
|
||||
|
||||
#[cfg(feature = "call")]
|
||||
fn shared_screen_for_peer(
|
||||
&self,
|
||||
peer_id: PeerId,
|
||||
|
@ -5002,10 +5087,12 @@ impl Workspace {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "call")]
|
||||
pub fn active_call(&self) -> Option<&Entity<ActiveCall>> {
|
||||
self.active_call.as_ref().map(|(call, _)| call)
|
||||
}
|
||||
|
||||
#[cfg(feature = "call")]
|
||||
fn on_active_call_event(
|
||||
&mut self,
|
||||
_: &Entity<ActiveCall>,
|
||||
|
@ -5918,6 +6005,17 @@ impl Workspace {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "call"))]
|
||||
fn leader_border_for_pane(
|
||||
_follower_states: &HashMap<CollaboratorId, FollowerState>,
|
||||
_pane: &Entity<Pane>,
|
||||
_: &Window,
|
||||
_cx: &App,
|
||||
) -> Option<Div> {
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(feature = "call")]
|
||||
fn leader_border_for_pane(
|
||||
follower_states: &HashMap<CollaboratorId, FollowerState>,
|
||||
pane: &Entity<Pane>,
|
||||
|
@ -6384,6 +6482,7 @@ impl Render for Workspace {
|
|||
&PaneRenderContext {
|
||||
follower_states:
|
||||
&self.follower_states,
|
||||
#[cfg(feature = "call")]
|
||||
active_call: self.active_call(),
|
||||
active_pane: &self.active_pane,
|
||||
app_state: &self.app_state,
|
||||
|
@ -6448,6 +6547,7 @@ impl Render for Workspace {
|
|||
&PaneRenderContext {
|
||||
follower_states:
|
||||
&self.follower_states,
|
||||
#[cfg(feature = "call")]
|
||||
active_call: self.active_call(),
|
||||
active_pane: &self.active_pane,
|
||||
app_state: &self.app_state,
|
||||
|
@ -6510,6 +6610,7 @@ impl Render for Workspace {
|
|||
&PaneRenderContext {
|
||||
follower_states:
|
||||
&self.follower_states,
|
||||
#[cfg(feature = "call")]
|
||||
active_call: self.active_call(),
|
||||
active_pane: &self.active_pane,
|
||||
app_state: &self.app_state,
|
||||
|
@ -6558,6 +6659,7 @@ impl Render for Workspace {
|
|||
&PaneRenderContext {
|
||||
follower_states:
|
||||
&self.follower_states,
|
||||
#[cfg(feature = "call")]
|
||||
active_call: self.active_call(),
|
||||
active_pane: &self.active_pane,
|
||||
app_state: &self.app_state,
|
||||
|
@ -6631,10 +6733,22 @@ impl WorkspaceStore {
|
|||
client.add_request_handler(cx.weak_entity(), Self::handle_follow),
|
||||
client.add_message_handler(cx.weak_entity(), Self::handle_update_followers),
|
||||
],
|
||||
#[cfg(feature = "call")]
|
||||
client,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "call"))]
|
||||
pub fn update_followers(
|
||||
&self,
|
||||
_project_id: Option<u64>,
|
||||
_update: proto::update_followers::Variant,
|
||||
_cx: &App,
|
||||
) -> Option<()> {
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(feature = "call")]
|
||||
pub fn update_followers(
|
||||
&self,
|
||||
project_id: Option<u64>,
|
||||
|
@ -6800,6 +6914,7 @@ actions!(
|
|||
]
|
||||
);
|
||||
|
||||
#[cfg(feature = "call")]
|
||||
async fn join_channel_internal(
|
||||
channel_id: ChannelId,
|
||||
app_state: &Arc<AppState>,
|
||||
|
@ -6947,6 +7062,17 @@ async fn join_channel_internal(
|
|||
anyhow::Ok(false)
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "call"))]
|
||||
pub fn join_channel(
|
||||
_channel_id: ChannelId,
|
||||
_app_state: Arc<AppState>,
|
||||
_requesting_window: Option<WindowHandle<Workspace>>,
|
||||
_cx: &mut App,
|
||||
) -> Task<Result<()>> {
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
|
||||
#[cfg(feature = "call")]
|
||||
pub fn join_channel(
|
||||
channel_id: ChannelId,
|
||||
app_state: Arc<AppState>,
|
||||
|
@ -7299,6 +7425,7 @@ pub fn open_ssh_project_with_new_connection(
|
|||
cx,
|
||||
)
|
||||
})?;
|
||||
// TODO kb register here instead?
|
||||
|
||||
open_ssh_project_inner(
|
||||
project,
|
||||
|
@ -7454,6 +7581,17 @@ fn serialize_ssh_project(
|
|||
})
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "call"))]
|
||||
pub fn join_in_room_project(
|
||||
_project_id: u64,
|
||||
_follow_user_id: u64,
|
||||
_app_state: Arc<AppState>,
|
||||
_cx: &mut App,
|
||||
) -> Task<Result<()>> {
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
|
||||
#[cfg(feature = "call")]
|
||||
pub fn join_in_room_project(
|
||||
project_id: u64,
|
||||
follow_user_id: u64,
|
||||
|
|
|
@ -1308,11 +1308,11 @@ pub fn handle_keymap_file_changes(
|
|||
})
|
||||
.detach();
|
||||
|
||||
let mut current_layout_id = cx.keyboard_layout().id().to_string();
|
||||
let mut current_mapping = settings::get_key_equivalents(cx.keyboard_layout().id());
|
||||
cx.on_keyboard_layout_change(move |cx| {
|
||||
let next_layout_id = cx.keyboard_layout().id();
|
||||
if next_layout_id != current_layout_id {
|
||||
current_layout_id = next_layout_id.to_string();
|
||||
let next_mapping = settings::get_key_equivalents(cx.keyboard_layout().id());
|
||||
if next_mapping != current_mapping {
|
||||
current_mapping = next_mapping;
|
||||
keyboard_layout_tx.unbounded_send(()).ok();
|
||||
}
|
||||
})
|
||||
|
@ -4729,7 +4729,7 @@ mod tests {
|
|||
// and key strokes contain the given key
|
||||
bindings
|
||||
.into_iter()
|
||||
.any(|binding| binding.keystrokes().iter().any(|k| k.display_key == key)),
|
||||
.any(|binding| binding.keystrokes().iter().any(|k| k.key == key)),
|
||||
"On {} Failed to find {} with key binding {}",
|
||||
line,
|
||||
action.name(),
|
||||
|
|
|
@ -72,10 +72,7 @@ impl QuickActionBar {
|
|||
Tooltip::with_meta(
|
||||
tooltip_text,
|
||||
Some(open_action_for_tooltip),
|
||||
format!(
|
||||
"{} to open in a split",
|
||||
text_for_keystroke(&alt_click.modifiers, &alt_click.key, cx)
|
||||
),
|
||||
format!("{} to open in a split", text_for_keystroke(&alt_click, cx)),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
|
|
|
@ -284,8 +284,6 @@ pub mod agent {
|
|||
OpenSettings,
|
||||
/// Opens the agent onboarding modal.
|
||||
OpenOnboardingModal,
|
||||
/// Opens the ACP onboarding modal.
|
||||
OpenAcpOnboardingModal,
|
||||
/// Resets the agent onboarding state.
|
||||
ResetOnboarding,
|
||||
/// Starts a chat conversation with the agent.
|
||||
|
|
|
@ -3243,7 +3243,6 @@ Run the `theme selector: toggle` action in the command palette to see a current
|
|||
"indent_size": 20,
|
||||
"auto_reveal_entries": true,
|
||||
"auto_fold_dirs": true,
|
||||
"drag_and_drop": true,
|
||||
"scrollbar": {
|
||||
"show": null
|
||||
},
|
||||
|
|
|
@ -45,9 +45,9 @@ Zed supports ways to spawn (and rerun) commands using its integrated terminal to
|
|||
// Whether to show the task line in the output of the spawned task, defaults to `true`.
|
||||
"show_summary": true,
|
||||
// Whether to show the command line in the output of the spawned task, defaults to `true`.
|
||||
"show_output": true
|
||||
"show_output": true,
|
||||
// Represents the tags for inline runnable indicators, or spawning multiple tasks at once.
|
||||
// "tags": []
|
||||
"tags": []
|
||||
}
|
||||
]
|
||||
```
|
||||
|
|
|
@ -431,7 +431,6 @@ Project panel can be shown/hidden with {#action project_panel::ToggleFocus} ({#k
|
|||
"auto_reveal_entries": true, // Show file in panel when activating its buffer
|
||||
"auto_fold_dirs": true, // Fold dirs with single subdir
|
||||
"sticky_scroll": true, // Stick parent directories at top of the project panel.
|
||||
"drag_and_drop": true, // Whether drag and drop is enabled
|
||||
"scrollbar": { // Project panel scrollbar settings
|
||||
"show": null // Show/hide: (auto, system, always, never)
|
||||
},
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue