Compare commits
1 Commits
use-defaul
...
cherry-pic
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5eb04c7b28 |
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.
|
||||
-->
|
||||
|
||||
|
||||
151
.github/workflows/cherry-pick.yml
vendored
Normal file
151
.github/workflows/cherry-pick.yml
vendored
Normal file
@@ -0,0 +1,151 @@
|
||||
name: Cherry Pick
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
pull_request_target:
|
||||
types: [closed]
|
||||
|
||||
jobs:
|
||||
cherry-pick:
|
||||
# This job will run when a PR is merged with a specific comment,
|
||||
# or when a comment is added to an already merged PR.
|
||||
runs-on: ubuntu-latest
|
||||
# Use pull_request_target so that we can add comments back to the PR
|
||||
# if the cherry-pick fails.
|
||||
permissions:
|
||||
pull-requests: write
|
||||
contents: write
|
||||
issues: write
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # Required to get all history for cherry-picking
|
||||
|
||||
- name: Extract info and determine trigger
|
||||
id: info
|
||||
run: |
|
||||
# Default to failure unless a valid trigger is found
|
||||
echo "valid=false" >> $GITHUB_OUTPUT
|
||||
|
||||
if [[ "${{ github.event_name }}" == "pull_request_target" && "${{ github.event.pull_request.merged }}" == "true" ]]; then
|
||||
echo "Triggered by PR merge"
|
||||
PR_NUMBER="${{ github.event.pull_request.number }}"
|
||||
|
||||
# Check PR body first, then fall back to comments
|
||||
TEXT_TO_SEARCH="${{ github.event.pull_request.body }}"
|
||||
if [[ ! "$TEXT_TO_SEARCH" =~ /cherry-pick[[:space:]]+(stable|preview) ]]; then
|
||||
echo "Command not found in PR body. Checking comments..."
|
||||
TEXT_TO_SEARCH=$(gh pr view $PR_NUMBER --json comments -q '.comments[].body' | tail -n 100)
|
||||
fi
|
||||
|
||||
if [[ "$TEXT_TO_SEARCH" =~ /cherry-pick[[:space:]]+(stable|preview) ]]; then
|
||||
echo "Found cherry-pick command."
|
||||
MERGE_SHA="${{ github.event.pull_request.merge_commit_sha }}"
|
||||
# Get the last matching command in the text
|
||||
CHANNEL=$(echo "$TEXT_TO_SEARCH" | grep -oP '/cherry-pick[[:space:]]+\K(stable|preview)' | tail -n1)
|
||||
|
||||
echo "valid=true" >> $GITHUB_OUTPUT
|
||||
echo "merge_sha=$MERGE_SHA" >> $GITHUB_OUTPUT
|
||||
echo "pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT
|
||||
echo "channel=$CHANNEL" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "No cherry-pick command found in PR body or recent comments. Exiting."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
elif [[ "${{ github.event_name }}" == "issue_comment" && "${{ github.event.issue.pull_request }}" != "" ]]; then
|
||||
echo "Triggered by issue comment"
|
||||
COMMENT_BODY="${{ github.event.comment.body }}"
|
||||
if [[ ! "$COMMENT_BODY" =~ /cherry-pick[[:space:]]+(stable|preview) ]]; then
|
||||
echo "Comment does not contain cherry-pick command. Exiting."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
PR_NUMBER="${{ github.event.issue.number }}"
|
||||
|
||||
# Check if the PR is merged
|
||||
MERGE_SHA=$(gh pr view $PR_NUMBER --json mergeCommit -q .mergeCommit.oid)
|
||||
if [[ -z "$MERGE_SHA" ]]; then
|
||||
echo "PR #$PR_NUMBER is not merged. Exiting."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
CHANNEL=$(echo "$COMMENT_BODY" | grep -oP '/cherry-pick[[:space:]]+\K(stable|preview)' | head -n1)
|
||||
|
||||
echo "valid=true" >> $GITHUB_OUTPUT
|
||||
echo "merge_sha=$MERGE_SHA" >> $GITHUB_OUTPUT
|
||||
echo "pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT
|
||||
echo "channel=$CHANNEL" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Cherry-pick
|
||||
if: steps.info.outputs.valid == 'true'
|
||||
run: |
|
||||
set -e
|
||||
|
||||
CHANNEL="${{ steps.info.outputs.channel }}"
|
||||
MERGE_SHA="${{ steps.info.outputs.merge_sha }}"
|
||||
PR_NUMBER="${{ steps.info.outputs.pr_number }}"
|
||||
|
||||
# Get the latest version for the channel
|
||||
echo "Fetching latest version for '$CHANNEL' channel..."
|
||||
query=""
|
||||
case $CHANNEL in
|
||||
stable)
|
||||
;;
|
||||
preview)
|
||||
query="&preview=1"
|
||||
;;
|
||||
*)
|
||||
echo "Invalid channel: $CHANNEL" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
LATEST_VERSION=$(curl -s "https://zed.dev/api/releases/latest?asset=zed&os=macos&arch=aarch64$query" | jq -r .version)
|
||||
|
||||
if [[ -z "$LATEST_VERSION" ]]; then
|
||||
echo "Could not fetch latest version for channel '$CHANNEL'"
|
||||
gh pr comment $PR_NUMBER --body "Could not fetch latest version for channel '$CHANNEL' from zed.dev API."
|
||||
exit 1
|
||||
fi
|
||||
echo "Latest version is $LATEST_VERSION"
|
||||
|
||||
# Construct target branch name (e.g., v0.85.4 -> v0.85.x)
|
||||
TARGET_BRANCH=$(echo "$LATEST_VERSION" | sed -E 's/v([0-9]+\.[0-9]+)\..*/v\1.x/')
|
||||
echo "Target branch is $TARGET_BRANCH"
|
||||
|
||||
# Configure git
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
# Create and push the cherry-pick branch
|
||||
NEW_BRANCH="cherry-pick/pr-${PR_NUMBER}-to-${TARGET_BRANCH}"
|
||||
|
||||
git fetch origin $TARGET_BRANCH
|
||||
git checkout -b $NEW_BRANCH "origin/$TARGET_BRANCH"
|
||||
|
||||
echo "Attempting to cherry-pick $MERGE_SHA..."
|
||||
if ! git cherry-pick $MERGE_SHA; then
|
||||
echo "Cherry-pick failed. Please resolve conflicts manually."
|
||||
gh pr comment $PR_NUMBER --body "Automated cherry-pick to \`$TARGET_BRANCH\` failed due to conflicts. Please resolve them manually."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Pushing new branch $NEW_BRANCH..."
|
||||
git push -u origin $NEW_BRANCH
|
||||
|
||||
# Create the pull request
|
||||
echo "Creating pull request..."
|
||||
gh pr create \
|
||||
--title "Cherry-pick PR #${PR_NUMBER} to ${TARGET_BRANCH}" \
|
||||
--body "This PR cherry-picks the changes from #${PR_NUMBER} to the \`$TARGET_BRANCH\` branch." \
|
||||
--base $TARGET_BRANCH \
|
||||
--head $NEW_BRANCH \
|
||||
--reviewer "${{ github.actor }}"
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
6
Cargo.lock
generated
6
Cargo.lock
generated
@@ -13521,7 +13521,6 @@ dependencies = [
|
||||
"smol",
|
||||
"sysinfo",
|
||||
"telemetry_events",
|
||||
"thiserror 2.0.12",
|
||||
"toml 0.8.20",
|
||||
"unindent",
|
||||
"util",
|
||||
@@ -17185,7 +17184,8 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "tree-sitter-cpp"
|
||||
version = "0.23.4"
|
||||
source = "git+https://github.com/tree-sitter/tree-sitter-cpp?rev=5cb9b693cfd7bfacab1d9ff4acac1a4150700609#5cb9b693cfd7bfacab1d9ff4acac1a4150700609"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df2196ea9d47b4ab4a31b9297eaa5a5d19a0b121dceb9f118f6790ad0ab94743"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"tree-sitter-language",
|
||||
@@ -20395,7 +20395,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.203.0"
|
||||
version = "0.202.0"
|
||||
dependencies = [
|
||||
"acp_tools",
|
||||
"activity_indicator",
|
||||
|
||||
@@ -624,7 +624,7 @@ tower-http = "0.4.4"
|
||||
tree-sitter = { version = "0.25.6", features = ["wasm"] }
|
||||
tree-sitter-bash = "0.25.0"
|
||||
tree-sitter-c = "0.23"
|
||||
tree-sitter-cpp = { git = "https://github.com/tree-sitter/tree-sitter-cpp", rev = "5cb9b693cfd7bfacab1d9ff4acac1a4150700609" }
|
||||
tree-sitter-cpp = "0.23"
|
||||
tree-sitter-css = "0.23"
|
||||
tree-sitter-diff = "0.1.0"
|
||||
tree-sitter-elixir = "0.3"
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -130,8 +130,8 @@
|
||||
"bindings": {
|
||||
"shift-enter": "editor::Newline",
|
||||
"enter": "editor::Newline",
|
||||
"ctrl-enter": "editor::NewlineBelow",
|
||||
"ctrl-shift-enter": "editor::NewlineAbove",
|
||||
"ctrl-enter": "editor::NewlineAbove",
|
||||
"ctrl-shift-enter": "editor::NewlineBelow",
|
||||
"ctrl-k ctrl-z": "editor::ToggleSoftWrap",
|
||||
"ctrl-k z": "editor::ToggleSoftWrap",
|
||||
"find": "buffer_search::Deploy",
|
||||
|
||||
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
|
||||
|
||||
@@ -428,13 +428,11 @@
|
||||
"g h": "vim::StartOfLine",
|
||||
"g s": "vim::FirstNonWhitespace", // "g s" default behavior is "space s"
|
||||
"g e": "vim::EndOfDocument",
|
||||
"g .": "vim::HelixGotoLastModification", // go to last modification
|
||||
"g r": "editor::FindAllReferences", // zed specific
|
||||
"g t": "vim::WindowTop",
|
||||
"g c": "vim::WindowMiddle",
|
||||
"g b": "vim::WindowBottom",
|
||||
|
||||
"shift-r": "editor::Paste",
|
||||
"x": "editor::SelectLine",
|
||||
"shift-x": "editor::SelectLine",
|
||||
"%": "editor::SelectAll",
|
||||
|
||||
@@ -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": []
|
||||
}
|
||||
]
|
||||
|
||||
@@ -183,15 +183,16 @@ impl ToolCall {
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
cx: &mut App,
|
||||
) -> Self {
|
||||
let title = if let Some((first_line, _)) = tool_call.title.split_once("\n") {
|
||||
first_line.to_owned() + "…"
|
||||
} else {
|
||||
tool_call.title
|
||||
};
|
||||
Self {
|
||||
id: tool_call.id,
|
||||
label: cx
|
||||
.new(|cx| Markdown::new(title.into(), Some(language_registry.clone()), None, cx)),
|
||||
label: cx.new(|cx| {
|
||||
Markdown::new(
|
||||
tool_call.title.into(),
|
||||
Some(language_registry.clone()),
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
kind: tool_call.kind,
|
||||
content: tool_call
|
||||
.content
|
||||
@@ -232,11 +233,7 @@ impl ToolCall {
|
||||
|
||||
if let Some(title) = title {
|
||||
self.label.update(cx, |label, cx| {
|
||||
if let Some((first_line, _)) = title.split_once("\n") {
|
||||
label.replace(first_line.to_owned() + "…", cx)
|
||||
} else {
|
||||
label.replace(title, cx);
|
||||
}
|
||||
label.replace(title, cx);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -789,10 +786,15 @@ pub enum ThreadStatus {
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum LoadError {
|
||||
NotInstalled,
|
||||
NotInstalled {
|
||||
error_message: SharedString,
|
||||
install_message: SharedString,
|
||||
install_command: String,
|
||||
},
|
||||
Unsupported {
|
||||
command: SharedString,
|
||||
current_version: SharedString,
|
||||
error_message: SharedString,
|
||||
upgrade_message: SharedString,
|
||||
upgrade_command: String,
|
||||
},
|
||||
Exited {
|
||||
status: ExitStatus,
|
||||
@@ -803,12 +805,9 @@ pub enum LoadError {
|
||||
impl Display for LoadError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
LoadError::NotInstalled => write!(f, "not installed"),
|
||||
LoadError::Unsupported {
|
||||
command: path,
|
||||
current_version,
|
||||
} => {
|
||||
write!(f, "version {current_version} from {path} is not supported")
|
||||
LoadError::NotInstalled { error_message, .. }
|
||||
| LoadError::Unsupported { error_message, .. } => {
|
||||
write!(f, "{error_message}")
|
||||
}
|
||||
LoadError::Exited { status } => write!(f, "Server exited with status {status}"),
|
||||
LoadError::Other(msg) => write!(f, "{}", msg),
|
||||
|
||||
@@ -21,12 +21,12 @@ use ui::prelude::*;
|
||||
use util::ResultExt as _;
|
||||
use workspace::{Item, Workspace};
|
||||
|
||||
actions!(dev, [OpenAcpLogs]);
|
||||
actions!(acp, [OpenDebugTools]);
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
cx.observe_new(
|
||||
|workspace: &mut Workspace, _window, _cx: &mut Context<Workspace>| {
|
||||
workspace.register_action(|workspace, _: &OpenAcpLogs, window, cx| {
|
||||
workspace.register_action(|workspace, _: &OpenDebugTools, window, cx| {
|
||||
let acp_tools =
|
||||
Box::new(cx.new(|cx| AcpTools::new(workspace.project().clone(), cx)));
|
||||
workspace.add_item_to_active_pane(acp_tools, None, true, window, cx);
|
||||
|
||||
@@ -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(),
|
||||
}));
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
@@ -61,19 +61,16 @@ pub struct LanguageModels {
|
||||
model_list: acp_thread::AgentModelList,
|
||||
refresh_models_rx: watch::Receiver<()>,
|
||||
refresh_models_tx: watch::Sender<()>,
|
||||
_authenticate_all_providers_task: Task<()>,
|
||||
}
|
||||
|
||||
impl LanguageModels {
|
||||
fn new(cx: &mut App) -> Self {
|
||||
fn new(cx: &App) -> Self {
|
||||
let (refresh_models_tx, refresh_models_rx) = watch::channel(());
|
||||
|
||||
let mut this = Self {
|
||||
models: HashMap::default(),
|
||||
model_list: acp_thread::AgentModelList::Grouped(IndexMap::default()),
|
||||
refresh_models_rx,
|
||||
refresh_models_tx,
|
||||
_authenticate_all_providers_task: Self::authenticate_all_language_model_providers(cx),
|
||||
};
|
||||
this.refresh_list(cx);
|
||||
this
|
||||
@@ -153,52 +150,6 @@ impl LanguageModels {
|
||||
fn model_id(model: &Arc<dyn LanguageModel>) -> acp_thread::AgentModelId {
|
||||
acp_thread::AgentModelId(format!("{}/{}", model.provider_id().0, model.id().0).into())
|
||||
}
|
||||
|
||||
fn authenticate_all_language_model_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.background_spawn(async move {
|
||||
for (provider_id, provider_name, authenticate_task) in authenticate_all_providers {
|
||||
if let Err(err) = authenticate_task.await {
|
||||
if matches!(err, language_model::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 struct NativeAgent {
|
||||
@@ -277,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);
|
||||
@@ -573,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| {
|
||||
|
||||
@@ -42,10 +42,6 @@ impl AgentServer for NativeAgentServer {
|
||||
ui::IconName::ZedAgent
|
||||
}
|
||||
|
||||
fn install_command(&self) -> Option<&'static str> {
|
||||
None
|
||||
}
|
||||
|
||||
fn connect(
|
||||
&self,
|
||||
_root_dir: &Path,
|
||||
|
||||
@@ -72,7 +72,6 @@ async fn test_echo(cx: &mut TestAppContext) {
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
#[cfg_attr(target_os = "windows", ignore)] // TODO: Fix this test on Windows
|
||||
async fn test_thinking(cx: &mut TestAppContext) {
|
||||
let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
|
||||
let fake_model = model.as_fake();
|
||||
@@ -472,7 +471,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
|
||||
})
|
||||
]
|
||||
);
|
||||
@@ -1348,7 +1347,6 @@ async fn test_cancellation(cx: &mut TestAppContext) {
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
#[cfg_attr(target_os = "windows", ignore)] // TODO: Fix this test on Windows
|
||||
async fn test_in_progress_send_canceled_by_next_send(cx: &mut TestAppContext) {
|
||||
let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
|
||||
let fake_model = model.as_fake();
|
||||
@@ -1822,11 +1820,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);
|
||||
|
||||
@@ -11,7 +11,6 @@ use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::Settings;
|
||||
use std::{path::Path, sync::Arc};
|
||||
use util::markdown::MarkdownCodeBlock;
|
||||
|
||||
use crate::{AgentTool, ToolCallEventStream};
|
||||
|
||||
@@ -244,19 +243,6 @@ impl AgentTool for ReadFileTool {
|
||||
}]),
|
||||
..Default::default()
|
||||
});
|
||||
if let Ok(LanguageModelToolResultContent::Text(text)) = &result {
|
||||
let markdown = MarkdownCodeBlock {
|
||||
tag: &input.path,
|
||||
text,
|
||||
}
|
||||
.to_string();
|
||||
event_stream.update_fields(ToolCallUpdateFields {
|
||||
content: Some(vec![acp::ToolCallContent::Content {
|
||||
content: markdown.into(),
|
||||
}]),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
}
|
||||
})?;
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ impl AcpConnection {
|
||||
root_dir: &Path,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<Self> {
|
||||
let mut child = util::command::new_smol_command(command.path)
|
||||
let mut child = util::command::new_smol_command(&command.path)
|
||||
.args(command.args.iter().map(|arg| arg.as_str()))
|
||||
.envs(command.env.iter().flatten())
|
||||
.current_dir(root_dir)
|
||||
@@ -150,10 +150,6 @@ impl AcpConnection {
|
||||
_io_task: io_task,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn prompt_capabilities(&self) -> &acp::PromptCapabilities {
|
||||
&self.prompt_capabilities
|
||||
}
|
||||
}
|
||||
|
||||
impl AgentConnection for AcpConnection {
|
||||
@@ -166,34 +162,12 @@ impl AgentConnection for AcpConnection {
|
||||
let conn = self.connection.clone();
|
||||
let sessions = self.sessions.clone();
|
||||
let cwd = cwd.to_path_buf();
|
||||
let context_server_store = project.read(cx).context_server_store().read(cx);
|
||||
let mcp_servers = context_server_store
|
||||
.configured_server_ids()
|
||||
.iter()
|
||||
.filter_map(|id| {
|
||||
let configuration = context_server_store.configuration_for_server(id)?;
|
||||
let command = configuration.command();
|
||||
Some(acp::McpServer {
|
||||
name: id.0.to_string(),
|
||||
command: command.path.clone(),
|
||||
args: command.args.clone(),
|
||||
env: if let Some(env) = command.env.as_ref() {
|
||||
env.iter()
|
||||
.map(|(name, value)| acp::EnvVariable {
|
||||
name: name.clone(),
|
||||
value: value.clone(),
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
vec![]
|
||||
},
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
let response = conn
|
||||
.new_session(acp::NewSessionRequest { mcp_servers, cwd })
|
||||
.new_session(acp::NewSessionRequest {
|
||||
mcp_servers: vec![],
|
||||
cwd,
|
||||
})
|
||||
.await
|
||||
.map_err(|err| {
|
||||
if err.code == acp::ErrorCode::AUTH_REQUIRED.code {
|
||||
@@ -292,9 +266,7 @@ impl AgentConnection for AcpConnection {
|
||||
|
||||
match serde_json::from_value(data.clone()) {
|
||||
Ok(ErrorDetails { details }) => {
|
||||
if suppress_abort_err
|
||||
&& (details.contains("This operation was aborted")
|
||||
|| details.contains("The user aborted a request"))
|
||||
if suppress_abort_err && details.contains("This operation was aborted")
|
||||
{
|
||||
Ok(acp::PromptResponse {
|
||||
stop_reason: acp::StopReason::Cancelled,
|
||||
|
||||
524
crates/agent_servers/src/acp/v0.rs
Normal file
524
crates/agent_servers/src/acp/v0.rs
Normal file
@@ -0,0 +1,524 @@
|
||||
// Translates old acp agents into the new schema
|
||||
use action_log::ActionLog;
|
||||
use agent_client_protocol as acp;
|
||||
use agentic_coding_protocol::{self as acp_old, AgentRequest as _};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use futures::channel::oneshot;
|
||||
use gpui::{AppContext as _, AsyncApp, Entity, Task, WeakEntity};
|
||||
use project::Project;
|
||||
use std::{any::Any, cell::RefCell, path::Path, rc::Rc};
|
||||
use ui::App;
|
||||
use util::ResultExt as _;
|
||||
|
||||
use crate::AgentServerCommand;
|
||||
use acp_thread::{AcpThread, AgentConnection, AuthRequired};
|
||||
|
||||
#[derive(Clone)]
|
||||
struct OldAcpClientDelegate {
|
||||
thread: Rc<RefCell<WeakEntity<AcpThread>>>,
|
||||
cx: AsyncApp,
|
||||
next_tool_call_id: Rc<RefCell<u64>>,
|
||||
// sent_buffer_versions: HashMap<Entity<Buffer>, HashMap<u64, BufferSnapshot>>,
|
||||
}
|
||||
|
||||
impl OldAcpClientDelegate {
|
||||
fn new(thread: Rc<RefCell<WeakEntity<AcpThread>>>, cx: AsyncApp) -> Self {
|
||||
Self {
|
||||
thread,
|
||||
cx,
|
||||
next_tool_call_id: Rc::new(RefCell::new(0)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl acp_old::Client for OldAcpClientDelegate {
|
||||
async fn stream_assistant_message_chunk(
|
||||
&self,
|
||||
params: acp_old::StreamAssistantMessageChunkParams,
|
||||
) -> Result<(), acp_old::Error> {
|
||||
let cx = &mut self.cx.clone();
|
||||
|
||||
cx.update(|cx| {
|
||||
self.thread
|
||||
.borrow()
|
||||
.update(cx, |thread, cx| match params.chunk {
|
||||
acp_old::AssistantMessageChunk::Text { text } => {
|
||||
thread.push_assistant_content_block(text.into(), false, cx)
|
||||
}
|
||||
acp_old::AssistantMessageChunk::Thought { thought } => {
|
||||
thread.push_assistant_content_block(thought.into(), true, cx)
|
||||
}
|
||||
})
|
||||
.log_err();
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn request_tool_call_confirmation(
|
||||
&self,
|
||||
request: acp_old::RequestToolCallConfirmationParams,
|
||||
) -> Result<acp_old::RequestToolCallConfirmationResponse, acp_old::Error> {
|
||||
let cx = &mut self.cx.clone();
|
||||
|
||||
let old_acp_id = *self.next_tool_call_id.borrow() + 1;
|
||||
self.next_tool_call_id.replace(old_acp_id);
|
||||
|
||||
let tool_call = into_new_tool_call(
|
||||
acp::ToolCallId(old_acp_id.to_string().into()),
|
||||
request.tool_call,
|
||||
);
|
||||
|
||||
let mut options = match request.confirmation {
|
||||
acp_old::ToolCallConfirmation::Edit { .. } => vec![(
|
||||
acp_old::ToolCallConfirmationOutcome::AlwaysAllow,
|
||||
acp::PermissionOptionKind::AllowAlways,
|
||||
"Always Allow Edits".to_string(),
|
||||
)],
|
||||
acp_old::ToolCallConfirmation::Execute { root_command, .. } => vec![(
|
||||
acp_old::ToolCallConfirmationOutcome::AlwaysAllow,
|
||||
acp::PermissionOptionKind::AllowAlways,
|
||||
format!("Always Allow {}", root_command),
|
||||
)],
|
||||
acp_old::ToolCallConfirmation::Mcp {
|
||||
server_name,
|
||||
tool_name,
|
||||
..
|
||||
} => vec![
|
||||
(
|
||||
acp_old::ToolCallConfirmationOutcome::AlwaysAllowMcpServer,
|
||||
acp::PermissionOptionKind::AllowAlways,
|
||||
format!("Always Allow {}", server_name),
|
||||
),
|
||||
(
|
||||
acp_old::ToolCallConfirmationOutcome::AlwaysAllowTool,
|
||||
acp::PermissionOptionKind::AllowAlways,
|
||||
format!("Always Allow {}", tool_name),
|
||||
),
|
||||
],
|
||||
acp_old::ToolCallConfirmation::Fetch { .. } => vec![(
|
||||
acp_old::ToolCallConfirmationOutcome::AlwaysAllow,
|
||||
acp::PermissionOptionKind::AllowAlways,
|
||||
"Always Allow".to_string(),
|
||||
)],
|
||||
acp_old::ToolCallConfirmation::Other { .. } => vec![(
|
||||
acp_old::ToolCallConfirmationOutcome::AlwaysAllow,
|
||||
acp::PermissionOptionKind::AllowAlways,
|
||||
"Always Allow".to_string(),
|
||||
)],
|
||||
};
|
||||
|
||||
options.extend([
|
||||
(
|
||||
acp_old::ToolCallConfirmationOutcome::Allow,
|
||||
acp::PermissionOptionKind::AllowOnce,
|
||||
"Allow".to_string(),
|
||||
),
|
||||
(
|
||||
acp_old::ToolCallConfirmationOutcome::Reject,
|
||||
acp::PermissionOptionKind::RejectOnce,
|
||||
"Reject".to_string(),
|
||||
),
|
||||
]);
|
||||
|
||||
let mut outcomes = Vec::with_capacity(options.len());
|
||||
let mut acp_options = Vec::with_capacity(options.len());
|
||||
|
||||
for (index, (outcome, kind, label)) in options.into_iter().enumerate() {
|
||||
outcomes.push(outcome);
|
||||
acp_options.push(acp::PermissionOption {
|
||||
id: acp::PermissionOptionId(index.to_string().into()),
|
||||
name: label,
|
||||
kind,
|
||||
})
|
||||
}
|
||||
|
||||
let response = cx
|
||||
.update(|cx| {
|
||||
self.thread.borrow().update(cx, |thread, cx| {
|
||||
thread.request_tool_call_authorization(tool_call.into(), acp_options, cx)
|
||||
})
|
||||
})??
|
||||
.context("Failed to update thread")?
|
||||
.await;
|
||||
|
||||
let outcome = match response {
|
||||
Ok(option_id) => outcomes[option_id.0.parse::<usize>().unwrap_or(0)],
|
||||
Err(oneshot::Canceled) => acp_old::ToolCallConfirmationOutcome::Cancel,
|
||||
};
|
||||
|
||||
Ok(acp_old::RequestToolCallConfirmationResponse {
|
||||
id: acp_old::ToolCallId(old_acp_id),
|
||||
outcome,
|
||||
})
|
||||
}
|
||||
|
||||
async fn push_tool_call(
|
||||
&self,
|
||||
request: acp_old::PushToolCallParams,
|
||||
) -> Result<acp_old::PushToolCallResponse, acp_old::Error> {
|
||||
let cx = &mut self.cx.clone();
|
||||
|
||||
let old_acp_id = *self.next_tool_call_id.borrow() + 1;
|
||||
self.next_tool_call_id.replace(old_acp_id);
|
||||
|
||||
cx.update(|cx| {
|
||||
self.thread.borrow().update(cx, |thread, cx| {
|
||||
thread.upsert_tool_call(
|
||||
into_new_tool_call(acp::ToolCallId(old_acp_id.to_string().into()), request),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})??
|
||||
.context("Failed to update thread")?;
|
||||
|
||||
Ok(acp_old::PushToolCallResponse {
|
||||
id: acp_old::ToolCallId(old_acp_id),
|
||||
})
|
||||
}
|
||||
|
||||
async fn update_tool_call(
|
||||
&self,
|
||||
request: acp_old::UpdateToolCallParams,
|
||||
) -> Result<(), acp_old::Error> {
|
||||
let cx = &mut self.cx.clone();
|
||||
|
||||
cx.update(|cx| {
|
||||
self.thread.borrow().update(cx, |thread, cx| {
|
||||
thread.update_tool_call(
|
||||
acp::ToolCallUpdate {
|
||||
id: acp::ToolCallId(request.tool_call_id.0.to_string().into()),
|
||||
fields: acp::ToolCallUpdateFields {
|
||||
status: Some(into_new_tool_call_status(request.status)),
|
||||
content: Some(
|
||||
request
|
||||
.content
|
||||
.into_iter()
|
||||
.map(into_new_tool_call_content)
|
||||
.collect::<Vec<_>>(),
|
||||
),
|
||||
..Default::default()
|
||||
},
|
||||
},
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})?
|
||||
.context("Failed to update thread")??;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn update_plan(&self, request: acp_old::UpdatePlanParams) -> Result<(), acp_old::Error> {
|
||||
let cx = &mut self.cx.clone();
|
||||
|
||||
cx.update(|cx| {
|
||||
self.thread.borrow().update(cx, |thread, cx| {
|
||||
thread.update_plan(
|
||||
acp::Plan {
|
||||
entries: request
|
||||
.entries
|
||||
.into_iter()
|
||||
.map(into_new_plan_entry)
|
||||
.collect(),
|
||||
},
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})?
|
||||
.context("Failed to update thread")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn read_text_file(
|
||||
&self,
|
||||
acp_old::ReadTextFileParams { path, line, limit }: acp_old::ReadTextFileParams,
|
||||
) -> Result<acp_old::ReadTextFileResponse, acp_old::Error> {
|
||||
let content = self
|
||||
.cx
|
||||
.update(|cx| {
|
||||
self.thread.borrow().update(cx, |thread, cx| {
|
||||
thread.read_text_file(path, line, limit, false, cx)
|
||||
})
|
||||
})?
|
||||
.context("Failed to update thread")?
|
||||
.await?;
|
||||
Ok(acp_old::ReadTextFileResponse { content })
|
||||
}
|
||||
|
||||
async fn write_text_file(
|
||||
&self,
|
||||
acp_old::WriteTextFileParams { path, content }: acp_old::WriteTextFileParams,
|
||||
) -> Result<(), acp_old::Error> {
|
||||
self.cx
|
||||
.update(|cx| {
|
||||
self.thread
|
||||
.borrow()
|
||||
.update(cx, |thread, cx| thread.write_text_file(path, content, cx))
|
||||
})?
|
||||
.context("Failed to update thread")?
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn into_new_tool_call(id: acp::ToolCallId, request: acp_old::PushToolCallParams) -> acp::ToolCall {
|
||||
acp::ToolCall {
|
||||
id,
|
||||
title: request.label,
|
||||
kind: acp_kind_from_old_icon(request.icon),
|
||||
status: acp::ToolCallStatus::InProgress,
|
||||
content: request
|
||||
.content
|
||||
.into_iter()
|
||||
.map(into_new_tool_call_content)
|
||||
.collect(),
|
||||
locations: request
|
||||
.locations
|
||||
.into_iter()
|
||||
.map(into_new_tool_call_location)
|
||||
.collect(),
|
||||
raw_input: None,
|
||||
raw_output: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn acp_kind_from_old_icon(icon: acp_old::Icon) -> acp::ToolKind {
|
||||
match icon {
|
||||
acp_old::Icon::FileSearch => acp::ToolKind::Search,
|
||||
acp_old::Icon::Folder => acp::ToolKind::Search,
|
||||
acp_old::Icon::Globe => acp::ToolKind::Search,
|
||||
acp_old::Icon::Hammer => acp::ToolKind::Other,
|
||||
acp_old::Icon::LightBulb => acp::ToolKind::Think,
|
||||
acp_old::Icon::Pencil => acp::ToolKind::Edit,
|
||||
acp_old::Icon::Regex => acp::ToolKind::Search,
|
||||
acp_old::Icon::Terminal => acp::ToolKind::Execute,
|
||||
}
|
||||
}
|
||||
|
||||
fn into_new_tool_call_status(status: acp_old::ToolCallStatus) -> acp::ToolCallStatus {
|
||||
match status {
|
||||
acp_old::ToolCallStatus::Running => acp::ToolCallStatus::InProgress,
|
||||
acp_old::ToolCallStatus::Finished => acp::ToolCallStatus::Completed,
|
||||
acp_old::ToolCallStatus::Error => acp::ToolCallStatus::Failed,
|
||||
}
|
||||
}
|
||||
|
||||
fn into_new_tool_call_content(content: acp_old::ToolCallContent) -> acp::ToolCallContent {
|
||||
match content {
|
||||
acp_old::ToolCallContent::Markdown { markdown } => markdown.into(),
|
||||
acp_old::ToolCallContent::Diff { diff } => acp::ToolCallContent::Diff {
|
||||
diff: into_new_diff(diff),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn into_new_diff(diff: acp_old::Diff) -> acp::Diff {
|
||||
acp::Diff {
|
||||
path: diff.path,
|
||||
old_text: diff.old_text,
|
||||
new_text: diff.new_text,
|
||||
}
|
||||
}
|
||||
|
||||
fn into_new_tool_call_location(location: acp_old::ToolCallLocation) -> acp::ToolCallLocation {
|
||||
acp::ToolCallLocation {
|
||||
path: location.path,
|
||||
line: location.line,
|
||||
}
|
||||
}
|
||||
|
||||
fn into_new_plan_entry(entry: acp_old::PlanEntry) -> acp::PlanEntry {
|
||||
acp::PlanEntry {
|
||||
content: entry.content,
|
||||
priority: into_new_plan_priority(entry.priority),
|
||||
status: into_new_plan_status(entry.status),
|
||||
}
|
||||
}
|
||||
|
||||
fn into_new_plan_priority(priority: acp_old::PlanEntryPriority) -> acp::PlanEntryPriority {
|
||||
match priority {
|
||||
acp_old::PlanEntryPriority::Low => acp::PlanEntryPriority::Low,
|
||||
acp_old::PlanEntryPriority::Medium => acp::PlanEntryPriority::Medium,
|
||||
acp_old::PlanEntryPriority::High => acp::PlanEntryPriority::High,
|
||||
}
|
||||
}
|
||||
|
||||
fn into_new_plan_status(status: acp_old::PlanEntryStatus) -> acp::PlanEntryStatus {
|
||||
match status {
|
||||
acp_old::PlanEntryStatus::Pending => acp::PlanEntryStatus::Pending,
|
||||
acp_old::PlanEntryStatus::InProgress => acp::PlanEntryStatus::InProgress,
|
||||
acp_old::PlanEntryStatus::Completed => acp::PlanEntryStatus::Completed,
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AcpConnection {
|
||||
pub name: &'static str,
|
||||
pub connection: acp_old::AgentConnection,
|
||||
pub _child_status: Task<Result<()>>,
|
||||
pub current_thread: Rc<RefCell<WeakEntity<AcpThread>>>,
|
||||
}
|
||||
|
||||
impl AcpConnection {
|
||||
pub fn stdio(
|
||||
name: &'static str,
|
||||
command: AgentServerCommand,
|
||||
root_dir: &Path,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Task<Result<Self>> {
|
||||
let root_dir = root_dir.to_path_buf();
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
let mut child = util::command::new_smol_command(&command.path)
|
||||
.args(command.args.iter())
|
||||
.current_dir(root_dir)
|
||||
.stdin(std::process::Stdio::piped())
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.stderr(std::process::Stdio::inherit())
|
||||
.kill_on_drop(true)
|
||||
.spawn()?;
|
||||
|
||||
let stdin = child.stdin.take().unwrap();
|
||||
let stdout = child.stdout.take().unwrap();
|
||||
log::trace!("Spawned (pid: {})", child.id());
|
||||
|
||||
let foreground_executor = cx.foreground_executor().clone();
|
||||
|
||||
let thread_rc = Rc::new(RefCell::new(WeakEntity::new_invalid()));
|
||||
|
||||
let (connection, io_fut) = acp_old::AgentConnection::connect_to_agent(
|
||||
OldAcpClientDelegate::new(thread_rc.clone(), cx.clone()),
|
||||
stdin,
|
||||
stdout,
|
||||
move |fut| foreground_executor.spawn(fut).detach(),
|
||||
);
|
||||
|
||||
let io_task = cx.background_spawn(async move {
|
||||
io_fut.await.log_err();
|
||||
});
|
||||
|
||||
let child_status = cx.background_spawn(async move {
|
||||
let result = match child.status().await {
|
||||
Err(e) => Err(anyhow!(e)),
|
||||
Ok(result) if result.success() => Ok(()),
|
||||
Ok(result) => Err(anyhow!(result)),
|
||||
};
|
||||
drop(io_task);
|
||||
result
|
||||
});
|
||||
|
||||
Ok(Self {
|
||||
name,
|
||||
connection,
|
||||
_child_status: child_status,
|
||||
current_thread: thread_rc,
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl AgentConnection for AcpConnection {
|
||||
fn new_thread(
|
||||
self: Rc<Self>,
|
||||
project: Entity<Project>,
|
||||
_cwd: &Path,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Entity<AcpThread>>> {
|
||||
let task = self.connection.request_any(
|
||||
acp_old::InitializeParams {
|
||||
protocol_version: acp_old::ProtocolVersion::latest(),
|
||||
}
|
||||
.into_any(),
|
||||
);
|
||||
let current_thread = self.current_thread.clone();
|
||||
cx.spawn(async move |cx| {
|
||||
let result = task.await?;
|
||||
let result = acp_old::InitializeParams::response_from_any(result)?;
|
||||
|
||||
if !result.is_authenticated {
|
||||
anyhow::bail!(AuthRequired::new())
|
||||
}
|
||||
|
||||
cx.update(|cx| {
|
||||
let thread = cx.new(|cx| {
|
||||
let session_id = acp::SessionId("acp-old-no-id".into());
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
AcpThread::new(self.name, self.clone(), project, action_log, session_id)
|
||||
});
|
||||
current_thread.replace(thread.downgrade());
|
||||
thread
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn auth_methods(&self) -> &[acp::AuthMethod] {
|
||||
&[]
|
||||
}
|
||||
|
||||
fn authenticate(&self, _method_id: acp::AuthMethodId, cx: &mut App) -> Task<Result<()>> {
|
||||
let task = self
|
||||
.connection
|
||||
.request_any(acp_old::AuthenticateParams.into_any());
|
||||
cx.foreground_executor().spawn(async move {
|
||||
task.await?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn prompt(
|
||||
&self,
|
||||
_id: Option<acp_thread::UserMessageId>,
|
||||
params: acp::PromptRequest,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<acp::PromptResponse>> {
|
||||
let chunks = params
|
||||
.prompt
|
||||
.into_iter()
|
||||
.filter_map(|block| match block {
|
||||
acp::ContentBlock::Text(text) => {
|
||||
Some(acp_old::UserMessageChunk::Text { text: text.text })
|
||||
}
|
||||
acp::ContentBlock::ResourceLink(link) => Some(acp_old::UserMessageChunk::Path {
|
||||
path: link.uri.into(),
|
||||
}),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
|
||||
let task = self
|
||||
.connection
|
||||
.request_any(acp_old::SendUserMessageParams { chunks }.into_any());
|
||||
cx.foreground_executor().spawn(async move {
|
||||
task.await?;
|
||||
anyhow::Ok(acp::PromptResponse {
|
||||
stop_reason: acp::StopReason::EndTurn,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn prompt_capabilities(&self) -> acp::PromptCapabilities {
|
||||
acp::PromptCapabilities {
|
||||
image: false,
|
||||
audio: false,
|
||||
embedded_context: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn cancel(&self, _session_id: &acp::SessionId, cx: &mut App) {
|
||||
let task = self
|
||||
.connection
|
||||
.request_any(acp_old::CancelSendMessageParams.into_any());
|
||||
cx.foreground_executor()
|
||||
.spawn(async move {
|
||||
task.await?;
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx)
|
||||
}
|
||||
|
||||
fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
|
||||
self
|
||||
}
|
||||
}
|
||||
376
crates/agent_servers/src/acp/v1.rs
Normal file
376
crates/agent_servers/src/acp/v1.rs
Normal file
@@ -0,0 +1,376 @@
|
||||
use acp_tools::AcpConnectionRegistry;
|
||||
use action_log::ActionLog;
|
||||
use agent_client_protocol::{self as acp, Agent as _, ErrorCode};
|
||||
use anyhow::anyhow;
|
||||
use collections::HashMap;
|
||||
use futures::AsyncBufReadExt as _;
|
||||
use futures::channel::oneshot;
|
||||
use futures::io::BufReader;
|
||||
use project::Project;
|
||||
use serde::Deserialize;
|
||||
use std::path::Path;
|
||||
use std::rc::Rc;
|
||||
use std::{any::Any, cell::RefCell};
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use gpui::{App, AppContext as _, AsyncApp, Entity, Task, WeakEntity};
|
||||
|
||||
use crate::{AgentServerCommand, acp::UnsupportedVersion};
|
||||
use acp_thread::{AcpThread, AgentConnection, AuthRequired, LoadError};
|
||||
|
||||
pub struct AcpConnection {
|
||||
server_name: &'static str,
|
||||
connection: Rc<acp::ClientSideConnection>,
|
||||
sessions: Rc<RefCell<HashMap<acp::SessionId, AcpSession>>>,
|
||||
auth_methods: Vec<acp::AuthMethod>,
|
||||
prompt_capabilities: acp::PromptCapabilities,
|
||||
_io_task: Task<Result<()>>,
|
||||
}
|
||||
|
||||
pub struct AcpSession {
|
||||
thread: WeakEntity<AcpThread>,
|
||||
suppress_abort_err: bool,
|
||||
}
|
||||
|
||||
const MINIMUM_SUPPORTED_VERSION: acp::ProtocolVersion = acp::V1;
|
||||
|
||||
impl AcpConnection {
|
||||
pub async fn stdio(
|
||||
server_name: &'static str,
|
||||
command: AgentServerCommand,
|
||||
root_dir: &Path,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<Self> {
|
||||
let mut child = util::command::new_smol_command(&command.path)
|
||||
.args(command.args.iter().map(|arg| arg.as_str()))
|
||||
.envs(command.env.iter().flatten())
|
||||
.current_dir(root_dir)
|
||||
.stdin(std::process::Stdio::piped())
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.stderr(std::process::Stdio::piped())
|
||||
.kill_on_drop(true)
|
||||
.spawn()?;
|
||||
|
||||
let stdout = child.stdout.take().context("Failed to take stdout")?;
|
||||
let stdin = child.stdin.take().context("Failed to take stdin")?;
|
||||
let stderr = child.stderr.take().context("Failed to take stderr")?;
|
||||
log::trace!("Spawned (pid: {})", child.id());
|
||||
|
||||
let sessions = Rc::new(RefCell::new(HashMap::default()));
|
||||
|
||||
let client = ClientDelegate {
|
||||
sessions: sessions.clone(),
|
||||
cx: cx.clone(),
|
||||
};
|
||||
let (connection, io_task) = acp::ClientSideConnection::new(client, stdin, stdout, {
|
||||
let foreground_executor = cx.foreground_executor().clone();
|
||||
move |fut| {
|
||||
foreground_executor.spawn(fut).detach();
|
||||
}
|
||||
});
|
||||
|
||||
let io_task = cx.background_spawn(io_task);
|
||||
|
||||
cx.background_spawn(async move {
|
||||
let mut stderr = BufReader::new(stderr);
|
||||
let mut line = String::new();
|
||||
while let Ok(n) = stderr.read_line(&mut line).await
|
||||
&& n > 0
|
||||
{
|
||||
log::warn!("agent stderr: {}", &line);
|
||||
line.clear();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
cx.spawn({
|
||||
let sessions = sessions.clone();
|
||||
async move |cx| {
|
||||
let status = child.status().await?;
|
||||
|
||||
for session in sessions.borrow().values() {
|
||||
session
|
||||
.thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.emit_load_error(LoadError::Exited { status }, cx)
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
let connection = Rc::new(connection);
|
||||
|
||||
cx.update(|cx| {
|
||||
AcpConnectionRegistry::default_global(cx).update(cx, |registry, cx| {
|
||||
registry.set_active_connection(server_name, &connection, cx)
|
||||
});
|
||||
})?;
|
||||
|
||||
let response = connection
|
||||
.initialize(acp::InitializeRequest {
|
||||
protocol_version: acp::VERSION,
|
||||
client_capabilities: acp::ClientCapabilities {
|
||||
fs: acp::FileSystemCapability {
|
||||
read_text_file: true,
|
||||
write_text_file: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
.await?;
|
||||
|
||||
if response.protocol_version < MINIMUM_SUPPORTED_VERSION {
|
||||
return Err(UnsupportedVersion.into());
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
auth_methods: response.auth_methods,
|
||||
connection,
|
||||
server_name,
|
||||
sessions,
|
||||
prompt_capabilities: response.agent_capabilities.prompt_capabilities,
|
||||
_io_task: io_task,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl AgentConnection for AcpConnection {
|
||||
fn new_thread(
|
||||
self: Rc<Self>,
|
||||
project: Entity<Project>,
|
||||
cwd: &Path,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Entity<AcpThread>>> {
|
||||
let conn = self.connection.clone();
|
||||
let sessions = self.sessions.clone();
|
||||
let cwd = cwd.to_path_buf();
|
||||
cx.spawn(async move |cx| {
|
||||
let response = conn
|
||||
.new_session(acp::NewSessionRequest {
|
||||
mcp_servers: vec![],
|
||||
cwd,
|
||||
})
|
||||
.await
|
||||
.map_err(|err| {
|
||||
if err.code == acp::ErrorCode::AUTH_REQUIRED.code {
|
||||
let mut error = AuthRequired::new();
|
||||
|
||||
if err.message != acp::ErrorCode::AUTH_REQUIRED.message {
|
||||
error = error.with_description(err.message);
|
||||
}
|
||||
|
||||
anyhow!(error)
|
||||
} else {
|
||||
anyhow!(err)
|
||||
}
|
||||
})?;
|
||||
|
||||
let session_id = response.session_id;
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()))?;
|
||||
let thread = cx.new(|_cx| {
|
||||
AcpThread::new(
|
||||
self.server_name,
|
||||
self.clone(),
|
||||
project,
|
||||
action_log,
|
||||
session_id.clone(),
|
||||
)
|
||||
})?;
|
||||
|
||||
let session = AcpSession {
|
||||
thread: thread.downgrade(),
|
||||
suppress_abort_err: false,
|
||||
};
|
||||
sessions.borrow_mut().insert(session_id, session);
|
||||
|
||||
Ok(thread)
|
||||
})
|
||||
}
|
||||
|
||||
fn auth_methods(&self) -> &[acp::AuthMethod] {
|
||||
&self.auth_methods
|
||||
}
|
||||
|
||||
fn authenticate(&self, method_id: acp::AuthMethodId, cx: &mut App) -> Task<Result<()>> {
|
||||
let conn = self.connection.clone();
|
||||
cx.foreground_executor().spawn(async move {
|
||||
let result = conn
|
||||
.authenticate(acp::AuthenticateRequest {
|
||||
method_id: method_id.clone(),
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(result)
|
||||
})
|
||||
}
|
||||
|
||||
fn prompt(
|
||||
&self,
|
||||
_id: Option<acp_thread::UserMessageId>,
|
||||
params: acp::PromptRequest,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<acp::PromptResponse>> {
|
||||
let conn = self.connection.clone();
|
||||
let sessions = self.sessions.clone();
|
||||
let session_id = params.session_id.clone();
|
||||
cx.foreground_executor().spawn(async move {
|
||||
let result = conn.prompt(params).await;
|
||||
|
||||
let mut suppress_abort_err = false;
|
||||
|
||||
if let Some(session) = sessions.borrow_mut().get_mut(&session_id) {
|
||||
suppress_abort_err = session.suppress_abort_err;
|
||||
session.suppress_abort_err = false;
|
||||
}
|
||||
|
||||
match result {
|
||||
Ok(response) => Ok(response),
|
||||
Err(err) => {
|
||||
if err.code != ErrorCode::INTERNAL_ERROR.code {
|
||||
anyhow::bail!(err)
|
||||
}
|
||||
|
||||
let Some(data) = &err.data else {
|
||||
anyhow::bail!(err)
|
||||
};
|
||||
|
||||
// Temporary workaround until the following PR is generally available:
|
||||
// https://github.com/google-gemini/gemini-cli/pull/6656
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct ErrorDetails {
|
||||
details: Box<str>,
|
||||
}
|
||||
|
||||
match serde_json::from_value(data.clone()) {
|
||||
Ok(ErrorDetails { details }) => {
|
||||
if suppress_abort_err && details.contains("This operation was aborted")
|
||||
{
|
||||
Ok(acp::PromptResponse {
|
||||
stop_reason: acp::StopReason::Cancelled,
|
||||
})
|
||||
} else {
|
||||
Err(anyhow!(details))
|
||||
}
|
||||
}
|
||||
Err(_) => Err(anyhow!(err)),
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn prompt_capabilities(&self) -> acp::PromptCapabilities {
|
||||
self.prompt_capabilities
|
||||
}
|
||||
|
||||
fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) {
|
||||
if let Some(session) = self.sessions.borrow_mut().get_mut(session_id) {
|
||||
session.suppress_abort_err = true;
|
||||
}
|
||||
let conn = self.connection.clone();
|
||||
let params = acp::CancelNotification {
|
||||
session_id: session_id.clone(),
|
||||
};
|
||||
cx.foreground_executor()
|
||||
.spawn(async move { conn.cancel(params).await })
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
struct ClientDelegate {
|
||||
sessions: Rc<RefCell<HashMap<acp::SessionId, AcpSession>>>,
|
||||
cx: AsyncApp,
|
||||
}
|
||||
|
||||
impl acp::Client for ClientDelegate {
|
||||
async fn request_permission(
|
||||
&self,
|
||||
arguments: acp::RequestPermissionRequest,
|
||||
) -> Result<acp::RequestPermissionResponse, acp::Error> {
|
||||
let cx = &mut self.cx.clone();
|
||||
let rx = self
|
||||
.sessions
|
||||
.borrow()
|
||||
.get(&arguments.session_id)
|
||||
.context("Failed to get session")?
|
||||
.thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.request_tool_call_authorization(arguments.tool_call, arguments.options, cx)
|
||||
})?;
|
||||
|
||||
let result = rx?.await;
|
||||
|
||||
let outcome = match result {
|
||||
Ok(option) => acp::RequestPermissionOutcome::Selected { option_id: option },
|
||||
Err(oneshot::Canceled) => acp::RequestPermissionOutcome::Cancelled,
|
||||
};
|
||||
|
||||
Ok(acp::RequestPermissionResponse { outcome })
|
||||
}
|
||||
|
||||
async fn write_text_file(
|
||||
&self,
|
||||
arguments: acp::WriteTextFileRequest,
|
||||
) -> Result<(), acp::Error> {
|
||||
let cx = &mut self.cx.clone();
|
||||
let task = self
|
||||
.sessions
|
||||
.borrow()
|
||||
.get(&arguments.session_id)
|
||||
.context("Failed to get session")?
|
||||
.thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.write_text_file(arguments.path, arguments.content, cx)
|
||||
})?;
|
||||
|
||||
task.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn read_text_file(
|
||||
&self,
|
||||
arguments: acp::ReadTextFileRequest,
|
||||
) -> Result<acp::ReadTextFileResponse, acp::Error> {
|
||||
let cx = &mut self.cx.clone();
|
||||
let task = self
|
||||
.sessions
|
||||
.borrow()
|
||||
.get(&arguments.session_id)
|
||||
.context("Failed to get session")?
|
||||
.thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.read_text_file(arguments.path, arguments.line, arguments.limit, false, cx)
|
||||
})?;
|
||||
|
||||
let content = task.await?;
|
||||
|
||||
Ok(acp::ReadTextFileResponse { content })
|
||||
}
|
||||
|
||||
async fn session_notification(
|
||||
&self,
|
||||
notification: acp::SessionNotification,
|
||||
) -> Result<(), acp::Error> {
|
||||
let cx = &mut self.cx.clone();
|
||||
let sessions = self.sessions.borrow();
|
||||
let session = sessions
|
||||
.get(¬ification.session_id)
|
||||
.context("Failed to get session")?;
|
||||
|
||||
session.thread.update(cx, |thread, cx| {
|
||||
thread.handle_session_update(notification.update, cx)
|
||||
})??;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -46,8 +46,6 @@ pub trait AgentServer: Send {
|
||||
) -> Task<Result<Rc<dyn AgentConnection>>>;
|
||||
|
||||
fn into_any(self: Rc<Self>) -> Rc<dyn Any>;
|
||||
|
||||
fn install_command(&self) -> Option<&'static str>;
|
||||
}
|
||||
|
||||
impl dyn AgentServer {
|
||||
|
||||
@@ -63,10 +63,6 @@ impl AgentServer for ClaudeCode {
|
||||
ui::IconName::AiClaude
|
||||
}
|
||||
|
||||
fn install_command(&self) -> Option<&'static str> {
|
||||
Some("npm install -g @anthropic-ai/claude-code@latest")
|
||||
}
|
||||
|
||||
fn connect(
|
||||
&self,
|
||||
_root_dir: &Path,
|
||||
@@ -112,7 +108,11 @@ impl AgentConnection for ClaudeAgentConnection {
|
||||
)
|
||||
.await
|
||||
else {
|
||||
return Err(LoadError::NotInstalled.into());
|
||||
return Err(LoadError::NotInstalled {
|
||||
error_message: "Failed to find Claude Code binary".into(),
|
||||
install_message: "Install Claude Code".into(),
|
||||
install_command: "npm install -g @anthropic-ai/claude-code@latest".into(),
|
||||
}.into());
|
||||
};
|
||||
|
||||
let api_key =
|
||||
@@ -230,8 +230,17 @@ impl AgentConnection for ClaudeAgentConnection {
|
||||
|| !help.contains("--session-id"))
|
||||
{
|
||||
LoadError::Unsupported {
|
||||
command: command.path.to_string_lossy().to_string().into(),
|
||||
current_version: version.to_string().into(),
|
||||
error_message: format!(
|
||||
"Your installed version of Claude Code ({}, version {}) does not have required features for use with Zed.",
|
||||
command.path.to_string_lossy(),
|
||||
version,
|
||||
)
|
||||
.into(),
|
||||
upgrade_message: "Upgrade Claude Code to latest".into(),
|
||||
upgrade_command: format!(
|
||||
"{} update",
|
||||
command.path.to_string_lossy()
|
||||
),
|
||||
}
|
||||
} else {
|
||||
LoadError::Exited { status }
|
||||
|
||||
@@ -57,10 +57,6 @@ impl crate::AgentServer for CustomAgentServer {
|
||||
})
|
||||
}
|
||||
|
||||
fn install_command(&self) -> Option<&'static str> {
|
||||
None
|
||||
}
|
||||
|
||||
fn into_any(self: Rc<Self>) -> Rc<dyn std::any::Any> {
|
||||
self
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use std::rc::Rc;
|
||||
use std::{any::Any, path::Path};
|
||||
|
||||
use crate::acp::AcpConnection;
|
||||
use crate::{AgentServer, AgentServerCommand};
|
||||
use acp_thread::{AgentConnection, LoadError};
|
||||
use anyhow::Result;
|
||||
@@ -38,10 +37,6 @@ impl AgentServer for Gemini {
|
||||
ui::IconName::AiGemini
|
||||
}
|
||||
|
||||
fn install_command(&self) -> Option<&'static str> {
|
||||
Some("npm install --engine-strict -g @google/gemini-cli@latest")
|
||||
}
|
||||
|
||||
fn connect(
|
||||
&self,
|
||||
root_dir: &Path,
|
||||
@@ -57,73 +52,48 @@ impl AgentServer for Gemini {
|
||||
})?;
|
||||
|
||||
let Some(mut command) =
|
||||
AgentServerCommand::resolve("gemini", &[ACP_ARG], None, settings, &project, cx)
|
||||
.await
|
||||
AgentServerCommand::resolve("gemini", &[ACP_ARG], None, settings, &project, cx).await
|
||||
else {
|
||||
return Err(LoadError::NotInstalled.into());
|
||||
return Err(LoadError::NotInstalled {
|
||||
error_message: "Failed to find Gemini CLI binary".into(),
|
||||
install_message: "Install Gemini CLI".into(),
|
||||
install_command: Self::install_command().into(),
|
||||
}.into());
|
||||
};
|
||||
|
||||
if let Some(api_key) = cx.update(GoogleLanguageModelProvider::api_key)?.await.ok() {
|
||||
command
|
||||
.env
|
||||
.get_or_insert_default()
|
||||
.insert("GEMINI_API_KEY".to_owned(), api_key.key);
|
||||
if let Some(api_key)= cx.update(GoogleLanguageModelProvider::api_key)?.await.ok() {
|
||||
command.env.get_or_insert_default().insert("GEMINI_API_KEY".to_owned(), api_key.key);
|
||||
}
|
||||
|
||||
let result = crate::acp::connect(server_name, command.clone(), &root_dir, cx).await;
|
||||
match &result {
|
||||
Ok(connection) => {
|
||||
if let Some(connection) = connection.clone().downcast::<AcpConnection>()
|
||||
&& !connection.prompt_capabilities().image
|
||||
{
|
||||
let version_output = util::command::new_smol_command(&command.path)
|
||||
.args(command.args.iter())
|
||||
.arg("--version")
|
||||
.kill_on_drop(true)
|
||||
.output()
|
||||
.await;
|
||||
let current_version =
|
||||
String::from_utf8(version_output?.stdout)?.trim().to_owned();
|
||||
if !connection.prompt_capabilities().image {
|
||||
return Err(LoadError::Unsupported {
|
||||
current_version: current_version.into(),
|
||||
command: format!(
|
||||
"{} {}",
|
||||
command.path.to_string_lossy(),
|
||||
command.args.join(" ")
|
||||
)
|
||||
.into(),
|
||||
}
|
||||
.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
let version_fut = util::command::new_smol_command(&command.path)
|
||||
.args(command.args.iter())
|
||||
.arg("--version")
|
||||
.kill_on_drop(true)
|
||||
.output();
|
||||
if result.is_err() {
|
||||
let version_fut = util::command::new_smol_command(&command.path)
|
||||
.args(command.args.iter())
|
||||
.arg("--version")
|
||||
.kill_on_drop(true)
|
||||
.output();
|
||||
|
||||
let help_fut = util::command::new_smol_command(&command.path)
|
||||
.args(command.args.iter())
|
||||
.arg("--help")
|
||||
.kill_on_drop(true)
|
||||
.output();
|
||||
let help_fut = util::command::new_smol_command(&command.path)
|
||||
.args(command.args.iter())
|
||||
.arg("--help")
|
||||
.kill_on_drop(true)
|
||||
.output();
|
||||
|
||||
let (version_output, help_output) =
|
||||
futures::future::join(version_fut, help_fut).await;
|
||||
let (version_output, help_output) = futures::future::join(version_fut, help_fut).await;
|
||||
|
||||
let current_version = String::from_utf8(version_output?.stdout)?;
|
||||
let supported = String::from_utf8(help_output?.stdout)?.contains(ACP_ARG);
|
||||
let current_version = String::from_utf8(version_output?.stdout)?;
|
||||
let supported = String::from_utf8(help_output?.stdout)?.contains(ACP_ARG);
|
||||
|
||||
if !supported {
|
||||
return Err(LoadError::Unsupported {
|
||||
current_version: current_version.into(),
|
||||
command: command.path.to_string_lossy().to_string().into(),
|
||||
}
|
||||
.into());
|
||||
}
|
||||
if !supported {
|
||||
return Err(LoadError::Unsupported {
|
||||
error_message: format!(
|
||||
"Your installed version of Gemini CLI ({}, version {}) doesn't support the Agentic Coding Protocol (ACP).",
|
||||
command.path.to_string_lossy(),
|
||||
current_version
|
||||
).into(),
|
||||
upgrade_message: "Upgrade Gemini CLI to latest".into(),
|
||||
upgrade_command: Self::upgrade_command().into(),
|
||||
}.into())
|
||||
}
|
||||
}
|
||||
result
|
||||
@@ -141,11 +111,11 @@ impl Gemini {
|
||||
}
|
||||
|
||||
pub fn install_command() -> &'static str {
|
||||
"npm install --engine-strict -g @google/gemini-cli@latest"
|
||||
"npm install -g @google/gemini-cli@preview"
|
||||
}
|
||||
|
||||
pub fn upgrade_command() -> &'static str {
|
||||
"npm install -g @google/gemini-cli@latest"
|
||||
"npm install -g @google/gemini-cli@preview"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,12 +9,10 @@ use agent_servers::AgentServerSettings;
|
||||
use agent2::{DbThreadMetadata, HistoryEntry};
|
||||
use db::kvp::{Dismissable, KEY_VALUE_STORE};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use zed_actions::OpenBrowser;
|
||||
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,
|
||||
@@ -29,6 +27,7 @@ use crate::{
|
||||
slash_command::SlashCommandCompletionProvider,
|
||||
text_thread_editor::{
|
||||
AgentPanelDelegate, TextThreadEditor, humanize_token_count, make_lsp_adapter_delegate,
|
||||
render_remaining_tokens,
|
||||
},
|
||||
thread_history::{HistoryEntryElement, ThreadHistory},
|
||||
ui::{AgentOnboardingModal, EndTrialUpsell},
|
||||
@@ -77,10 +76,7 @@ use workspace::{
|
||||
};
|
||||
use zed_actions::{
|
||||
DecreaseBufferFontSize, IncreaseBufferFontSize, ResetBufferFontSize,
|
||||
agent::{
|
||||
OpenAcpOnboardingModal, OpenOnboardingModal, OpenSettings, ResetOnboarding,
|
||||
ToggleModelSelector,
|
||||
},
|
||||
agent::{OpenOnboardingModal, OpenSettings, ResetOnboarding, ToggleModelSelector},
|
||||
assistant::{OpenRulesLibrary, ToggleFocus},
|
||||
};
|
||||
|
||||
@@ -204,9 +200,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();
|
||||
@@ -597,6 +590,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(
|
||||
@@ -617,10 +621,6 @@ impl AgentPanel {
|
||||
}
|
||||
cx.notify();
|
||||
});
|
||||
} else {
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.new_agent_thread(AgentType::NativeAgent, window, cx);
|
||||
});
|
||||
}
|
||||
panel
|
||||
})?;
|
||||
@@ -1851,6 +1851,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()
|
||||
}
|
||||
@@ -1861,11 +1874,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(
|
||||
@@ -2546,7 +2554,7 @@ impl AgentPanel {
|
||||
workspace.panel::<AgentPanel>(cx)
|
||||
{
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.new_agent_thread(
|
||||
panel.set_selected_agent(
|
||||
AgentType::NativeAgent,
|
||||
window,
|
||||
cx,
|
||||
@@ -2572,7 +2580,7 @@ impl AgentPanel {
|
||||
workspace.panel::<AgentPanel>(cx)
|
||||
{
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.new_agent_thread(
|
||||
panel.set_selected_agent(
|
||||
AgentType::TextThread,
|
||||
window,
|
||||
cx,
|
||||
@@ -2600,7 +2608,7 @@ impl AgentPanel {
|
||||
workspace.panel::<AgentPanel>(cx)
|
||||
{
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.new_agent_thread(
|
||||
panel.set_selected_agent(
|
||||
AgentType::Gemini,
|
||||
window,
|
||||
cx,
|
||||
@@ -2627,7 +2635,7 @@ impl AgentPanel {
|
||||
workspace.panel::<AgentPanel>(cx)
|
||||
{
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.new_agent_thread(
|
||||
panel.set_selected_agent(
|
||||
AgentType::ClaudeCode,
|
||||
window,
|
||||
cx,
|
||||
@@ -2660,7 +2668,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,15 +2689,6 @@ impl AgentPanel {
|
||||
}
|
||||
|
||||
menu
|
||||
})
|
||||
.when(cx.has_flag::<GeminiAndNativeFeatureFlag>(), |menu| {
|
||||
menu.separator().link(
|
||||
"Add Other Agents",
|
||||
OpenBrowser {
|
||||
url: zed_urls::external_agents_docs(cx),
|
||||
}
|
||||
.boxed_clone(),
|
||||
)
|
||||
});
|
||||
menu
|
||||
}))
|
||||
@@ -2874,8 +2873,12 @@ impl AgentPanel {
|
||||
|
||||
Some(token_count)
|
||||
}
|
||||
ActiveView::TextThread { context_editor, .. } => {
|
||||
let element = render_remaining_tokens(context_editor, cx)?;
|
||||
|
||||
Some(element.into_any_element())
|
||||
}
|
||||
ActiveView::ExternalAgentThread { .. }
|
||||
| ActiveView::TextThread { .. }
|
||||
| ActiveView::History
|
||||
| ActiveView::Configuration => None,
|
||||
}
|
||||
|
||||
@@ -334,7 +334,7 @@ impl<T: 'static> PromptEditor<T> {
|
||||
EditorEvent::Edited { .. } => {
|
||||
if let Some(workspace) = window.root::<Workspace>().flatten() {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
let is_via_ssh = workspace.project().read(cx).is_via_remote_server();
|
||||
let is_via_ssh = workspace.project().read(cx).is_via_ssh();
|
||||
|
||||
workspace
|
||||
.client()
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -1857,53 +1857,6 @@ impl TextThreadEditor {
|
||||
.update(cx, |context, cx| context.summarize(true, cx));
|
||||
}
|
||||
|
||||
fn render_remaining_tokens(&self, cx: &App) -> Option<impl IntoElement + use<>> {
|
||||
let (token_count_color, token_count, max_token_count, tooltip) =
|
||||
match token_state(&self.context, cx)? {
|
||||
TokenState::NoTokensLeft {
|
||||
max_token_count,
|
||||
token_count,
|
||||
} => (
|
||||
Color::Error,
|
||||
token_count,
|
||||
max_token_count,
|
||||
Some("Token Limit Reached"),
|
||||
),
|
||||
TokenState::HasMoreTokens {
|
||||
max_token_count,
|
||||
token_count,
|
||||
over_warn_threshold,
|
||||
} => {
|
||||
let (color, tooltip) = if over_warn_threshold {
|
||||
(Color::Warning, Some("Token Limit is Close to Exhaustion"))
|
||||
} else {
|
||||
(Color::Muted, None)
|
||||
};
|
||||
(color, token_count, max_token_count, tooltip)
|
||||
}
|
||||
};
|
||||
|
||||
Some(
|
||||
h_flex()
|
||||
.id("token-count")
|
||||
.gap_0p5()
|
||||
.child(
|
||||
Label::new(humanize_token_count(token_count))
|
||||
.size(LabelSize::Small)
|
||||
.color(token_count_color),
|
||||
)
|
||||
.child(Label::new("/").size(LabelSize::Small).color(Color::Muted))
|
||||
.child(
|
||||
Label::new(humanize_token_count(max_token_count))
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.when_some(tooltip, |element, tooltip| {
|
||||
element.tooltip(Tooltip::text(tooltip))
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_send_button(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let focus_handle = self.focus_handle(cx);
|
||||
|
||||
@@ -2467,14 +2420,9 @@ impl Render for TextThreadEditor {
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2p5()
|
||||
.children(self.render_remaining_tokens(cx))
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(self.render_language_model_selector(window, cx))
|
||||
.child(self.render_send_button(window, cx)),
|
||||
),
|
||||
.gap_1()
|
||||
.child(self.render_language_model_selector(window, cx))
|
||||
.child(self.render_send_button(window, cx)),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -2762,6 +2710,58 @@ impl FollowableItem for TextThreadEditor {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render_remaining_tokens(
|
||||
context_editor: &Entity<TextThreadEditor>,
|
||||
cx: &App,
|
||||
) -> Option<impl IntoElement + use<>> {
|
||||
let context = &context_editor.read(cx).context;
|
||||
|
||||
let (token_count_color, token_count, max_token_count, tooltip) = match token_state(context, cx)?
|
||||
{
|
||||
TokenState::NoTokensLeft {
|
||||
max_token_count,
|
||||
token_count,
|
||||
} => (
|
||||
Color::Error,
|
||||
token_count,
|
||||
max_token_count,
|
||||
Some("Token Limit Reached"),
|
||||
),
|
||||
TokenState::HasMoreTokens {
|
||||
max_token_count,
|
||||
token_count,
|
||||
over_warn_threshold,
|
||||
} => {
|
||||
let (color, tooltip) = if over_warn_threshold {
|
||||
(Color::Warning, Some("Token Limit is Close to Exhaustion"))
|
||||
} else {
|
||||
(Color::Muted, None)
|
||||
};
|
||||
(color, token_count, max_token_count, tooltip)
|
||||
}
|
||||
};
|
||||
|
||||
Some(
|
||||
h_flex()
|
||||
.id("token-count")
|
||||
.gap_0p5()
|
||||
.child(
|
||||
Label::new(humanize_token_count(token_count))
|
||||
.size(LabelSize::Small)
|
||||
.color(token_count_color),
|
||||
)
|
||||
.child(Label::new("/").size(LabelSize::Small).color(Color::Muted))
|
||||
.child(
|
||||
Label::new(humanize_token_count(max_token_count))
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.when_some(tooltip, |element, tooltip| {
|
||||
element.tooltip(Tooltip::text(tooltip))
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
enum PendingSlashCommand {}
|
||||
|
||||
fn invoked_slash_command_fold_placeholder(
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -1161,7 +1161,7 @@ impl Room {
|
||||
let request = self.client.request(proto::ShareProject {
|
||||
room_id: self.id(),
|
||||
worktrees: project.read(cx).worktree_metadata_protos(cx),
|
||||
is_ssh_project: project.read(cx).is_via_remote_server(),
|
||||
is_ssh_project: project.read(cx).is_via_ssh(),
|
||||
});
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ use project::{
|
||||
debugger::session::ThreadId,
|
||||
lsp_store::{FormatTrigger, LspFormatTarget},
|
||||
};
|
||||
use remote::RemoteClient;
|
||||
use remote::SshRemoteClient;
|
||||
use remote_server::{HeadlessAppState, HeadlessProject};
|
||||
use rpc::proto;
|
||||
use serde_json::json;
|
||||
@@ -59,7 +59,7 @@ async fn test_sharing_an_ssh_remote_project(
|
||||
.await;
|
||||
|
||||
// Set up project on remote FS
|
||||
let (opts, server_ssh) = RemoteClient::fake_server(cx_a, server_cx);
|
||||
let (opts, server_ssh) = SshRemoteClient::fake_server(cx_a, server_cx);
|
||||
let remote_fs = FakeFs::new(server_cx.executor());
|
||||
remote_fs
|
||||
.insert_tree(
|
||||
@@ -101,7 +101,7 @@ async fn test_sharing_an_ssh_remote_project(
|
||||
)
|
||||
});
|
||||
|
||||
let client_ssh = RemoteClient::fake_client(opts, cx_a).await;
|
||||
let client_ssh = SshRemoteClient::fake_client(opts, cx_a).await;
|
||||
let (project_a, worktree_id) = client_a
|
||||
.build_ssh_project(path!("/code/project1"), client_ssh, cx_a)
|
||||
.await;
|
||||
@@ -235,7 +235,7 @@ async fn test_ssh_collaboration_git_branches(
|
||||
.await;
|
||||
|
||||
// Set up project on remote FS
|
||||
let (opts, server_ssh) = RemoteClient::fake_server(cx_a, server_cx);
|
||||
let (opts, server_ssh) = SshRemoteClient::fake_server(cx_a, server_cx);
|
||||
let remote_fs = FakeFs::new(server_cx.executor());
|
||||
remote_fs
|
||||
.insert_tree("/project", serde_json::json!({ ".git":{} }))
|
||||
@@ -268,7 +268,7 @@ async fn test_ssh_collaboration_git_branches(
|
||||
)
|
||||
});
|
||||
|
||||
let client_ssh = RemoteClient::fake_client(opts, cx_a).await;
|
||||
let client_ssh = SshRemoteClient::fake_client(opts, cx_a).await;
|
||||
let (project_a, _) = client_a
|
||||
.build_ssh_project("/project", client_ssh, cx_a)
|
||||
.await;
|
||||
@@ -420,7 +420,7 @@ async fn test_ssh_collaboration_formatting_with_prettier(
|
||||
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
|
||||
.await;
|
||||
|
||||
let (opts, server_ssh) = RemoteClient::fake_server(cx_a, server_cx);
|
||||
let (opts, server_ssh) = SshRemoteClient::fake_server(cx_a, server_cx);
|
||||
let remote_fs = FakeFs::new(server_cx.executor());
|
||||
let buffer_text = "let one = \"two\"";
|
||||
let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
|
||||
@@ -473,7 +473,7 @@ async fn test_ssh_collaboration_formatting_with_prettier(
|
||||
)
|
||||
});
|
||||
|
||||
let client_ssh = RemoteClient::fake_client(opts, cx_a).await;
|
||||
let client_ssh = SshRemoteClient::fake_client(opts, cx_a).await;
|
||||
let (project_a, worktree_id) = client_a
|
||||
.build_ssh_project(path!("/project"), client_ssh, cx_a)
|
||||
.await;
|
||||
@@ -602,7 +602,7 @@ async fn test_remote_server_debugger(
|
||||
release_channel::init(SemanticVersion::default(), cx);
|
||||
dap_adapters::init(cx);
|
||||
});
|
||||
let (opts, server_ssh) = RemoteClient::fake_server(cx_a, server_cx);
|
||||
let (opts, server_ssh) = SshRemoteClient::fake_server(cx_a, server_cx);
|
||||
let remote_fs = FakeFs::new(server_cx.executor());
|
||||
remote_fs
|
||||
.insert_tree(
|
||||
@@ -633,7 +633,7 @@ async fn test_remote_server_debugger(
|
||||
)
|
||||
});
|
||||
|
||||
let client_ssh = RemoteClient::fake_client(opts, cx_a).await;
|
||||
let client_ssh = SshRemoteClient::fake_client(opts, cx_a).await;
|
||||
let mut server = TestServer::start(server_cx.executor()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
cx_a.update(|cx| {
|
||||
@@ -711,7 +711,7 @@ async fn test_slow_adapter_startup_retries(
|
||||
release_channel::init(SemanticVersion::default(), cx);
|
||||
dap_adapters::init(cx);
|
||||
});
|
||||
let (opts, server_ssh) = RemoteClient::fake_server(cx_a, server_cx);
|
||||
let (opts, server_ssh) = SshRemoteClient::fake_server(cx_a, server_cx);
|
||||
let remote_fs = FakeFs::new(server_cx.executor());
|
||||
remote_fs
|
||||
.insert_tree(
|
||||
@@ -742,7 +742,7 @@ async fn test_slow_adapter_startup_retries(
|
||||
)
|
||||
});
|
||||
|
||||
let client_ssh = RemoteClient::fake_client(opts, cx_a).await;
|
||||
let client_ssh = SshRemoteClient::fake_client(opts, cx_a).await;
|
||||
let mut server = TestServer::start(server_cx.executor()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
cx_a.update(|cx| {
|
||||
|
||||
@@ -26,7 +26,7 @@ use node_runtime::NodeRuntime;
|
||||
use notifications::NotificationStore;
|
||||
use parking_lot::Mutex;
|
||||
use project::{Project, WorktreeId};
|
||||
use remote::RemoteClient;
|
||||
use remote::SshRemoteClient;
|
||||
use rpc::{
|
||||
RECEIVE_TIMEOUT,
|
||||
proto::{self, ChannelRole},
|
||||
@@ -765,11 +765,11 @@ impl TestClient {
|
||||
pub async fn build_ssh_project(
|
||||
&self,
|
||||
root_path: impl AsRef<Path>,
|
||||
ssh: Entity<RemoteClient>,
|
||||
ssh: Entity<SshRemoteClient>,
|
||||
cx: &mut TestAppContext,
|
||||
) -> (Entity<Project>, WorktreeId) {
|
||||
let project = cx.update(|cx| {
|
||||
Project::remote(
|
||||
Project::ssh(
|
||||
ssh,
|
||||
self.client().clone(),
|
||||
self.app_state.node_runtime.clone(),
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
use anyhow::Result;
|
||||
use db::{
|
||||
query,
|
||||
sqlez::{
|
||||
bindable::Column, domain::Domain, statement::Statement,
|
||||
thread_safe_connection::ThreadSafeConnection,
|
||||
},
|
||||
define_connection, query,
|
||||
sqlez::{bindable::Column, statement::Statement},
|
||||
sqlez_macros::sql,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -53,11 +50,8 @@ impl Column for SerializedCommandInvocation {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CommandPaletteDB(ThreadSafeConnection);
|
||||
|
||||
impl Domain for CommandPaletteDB {
|
||||
const NAME: &str = stringify!(CommandPaletteDB);
|
||||
const MIGRATIONS: &[&str] = &[sql!(
|
||||
define_connection!(pub static ref COMMAND_PALETTE_HISTORY: CommandPaletteDB<()> =
|
||||
&[sql!(
|
||||
CREATE TABLE IF NOT EXISTS command_invocations(
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
command_name TEXT NOT NULL,
|
||||
@@ -65,9 +59,7 @@ impl Domain for CommandPaletteDB {
|
||||
last_invoked INTEGER DEFAULT (unixepoch()) NOT NULL
|
||||
) STRICT;
|
||||
)];
|
||||
}
|
||||
|
||||
db::static_connection!(COMMAND_PALETTE_HISTORY, CommandPaletteDB, []);
|
||||
);
|
||||
|
||||
impl CommandPaletteDB {
|
||||
pub async fn write_command_invocation(
|
||||
|
||||
@@ -110,14 +110,11 @@ pub async fn open_test_db<M: Migrator>(db_name: &str) -> ThreadSafeConnection {
|
||||
}
|
||||
|
||||
/// Implements a basic DB wrapper for a given domain
|
||||
///
|
||||
/// Arguments:
|
||||
/// - static variable name for connection
|
||||
/// - type of connection wrapper
|
||||
/// - dependencies, whose migrations should be run prior to this domain's migrations
|
||||
#[macro_export]
|
||||
macro_rules! static_connection {
|
||||
($id:ident, $t:ident, [ $($d:ty),* ] $(, $global:ident)?) => {
|
||||
macro_rules! define_connection {
|
||||
(pub static ref $id:ident: $t:ident<()> = $migrations:expr; $($global:ident)?) => {
|
||||
pub struct $t($crate::sqlez::thread_safe_connection::ThreadSafeConnection);
|
||||
|
||||
impl ::std::ops::Deref for $t {
|
||||
type Target = $crate::sqlez::thread_safe_connection::ThreadSafeConnection;
|
||||
|
||||
@@ -126,6 +123,16 @@ macro_rules! static_connection {
|
||||
}
|
||||
}
|
||||
|
||||
impl $crate::sqlez::domain::Domain for $t {
|
||||
fn name() -> &'static str {
|
||||
stringify!($t)
|
||||
}
|
||||
|
||||
fn migrations() -> &'static [&'static str] {
|
||||
$migrations
|
||||
}
|
||||
}
|
||||
|
||||
impl $t {
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub async fn open_test_db(name: &'static str) -> Self {
|
||||
@@ -135,8 +142,7 @@ macro_rules! static_connection {
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub static $id: std::sync::LazyLock<$t> = std::sync::LazyLock::new(|| {
|
||||
#[allow(unused_parens)]
|
||||
$t($crate::smol::block_on($crate::open_test_db::<($($d,)* $t)>(stringify!($id))))
|
||||
$t($crate::smol::block_on($crate::open_test_db::<$t>(stringify!($id))))
|
||||
});
|
||||
|
||||
#[cfg(not(any(test, feature = "test-support")))]
|
||||
@@ -147,10 +153,46 @@ macro_rules! static_connection {
|
||||
} else {
|
||||
$crate::RELEASE_CHANNEL.dev_name()
|
||||
};
|
||||
#[allow(unused_parens)]
|
||||
$t($crate::smol::block_on($crate::open_db::<($($d,)* $t)>(db_dir, scope)))
|
||||
$t($crate::smol::block_on($crate::open_db::<$t>(db_dir, scope)))
|
||||
});
|
||||
}
|
||||
};
|
||||
(pub static ref $id:ident: $t:ident<$($d:ty),+> = $migrations:expr; $($global:ident)?) => {
|
||||
pub struct $t($crate::sqlez::thread_safe_connection::ThreadSafeConnection);
|
||||
|
||||
impl ::std::ops::Deref for $t {
|
||||
type Target = $crate::sqlez::thread_safe_connection::ThreadSafeConnection;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl $crate::sqlez::domain::Domain for $t {
|
||||
fn name() -> &'static str {
|
||||
stringify!($t)
|
||||
}
|
||||
|
||||
fn migrations() -> &'static [&'static str] {
|
||||
$migrations
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub static $id: std::sync::LazyLock<$t> = std::sync::LazyLock::new(|| {
|
||||
$t($crate::smol::block_on($crate::open_test_db::<($($d),+, $t)>(stringify!($id))))
|
||||
});
|
||||
|
||||
#[cfg(not(any(test, feature = "test-support")))]
|
||||
pub static $id: std::sync::LazyLock<$t> = std::sync::LazyLock::new(|| {
|
||||
let db_dir = $crate::database_dir();
|
||||
let scope = if false $(|| stringify!($global) == "global")? {
|
||||
"global"
|
||||
} else {
|
||||
$crate::RELEASE_CHANNEL.dev_name()
|
||||
};
|
||||
$t($crate::smol::block_on($crate::open_db::<($($d),+, $t)>(db_dir, scope)))
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
pub fn write_and_log<F>(cx: &App, db_write: impl FnOnce() -> F + Send + 'static)
|
||||
@@ -177,12 +219,17 @@ mod tests {
|
||||
enum BadDB {}
|
||||
|
||||
impl Domain for BadDB {
|
||||
const NAME: &str = "db_tests";
|
||||
const MIGRATIONS: &[&str] = &[
|
||||
sql!(CREATE TABLE test(value);),
|
||||
// failure because test already exists
|
||||
sql!(CREATE TABLE test(value);),
|
||||
];
|
||||
fn name() -> &'static str {
|
||||
"db_tests"
|
||||
}
|
||||
|
||||
fn migrations() -> &'static [&'static str] {
|
||||
&[
|
||||
sql!(CREATE TABLE test(value);),
|
||||
// failure because test already exists
|
||||
sql!(CREATE TABLE test(value);),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
let tempdir = tempfile::Builder::new()
|
||||
@@ -204,15 +251,25 @@ mod tests {
|
||||
enum CorruptedDB {}
|
||||
|
||||
impl Domain for CorruptedDB {
|
||||
const NAME: &str = "db_tests";
|
||||
const MIGRATIONS: &[&str] = &[sql!(CREATE TABLE test(value);)];
|
||||
fn name() -> &'static str {
|
||||
"db_tests"
|
||||
}
|
||||
|
||||
fn migrations() -> &'static [&'static str] {
|
||||
&[sql!(CREATE TABLE test(value);)]
|
||||
}
|
||||
}
|
||||
|
||||
enum GoodDB {}
|
||||
|
||||
impl Domain for GoodDB {
|
||||
const NAME: &str = "db_tests"; //Notice same name
|
||||
const MIGRATIONS: &[&str] = &[sql!(CREATE TABLE test2(value);)];
|
||||
fn name() -> &'static str {
|
||||
"db_tests" //Notice same name
|
||||
}
|
||||
|
||||
fn migrations() -> &'static [&'static str] {
|
||||
&[sql!(CREATE TABLE test2(value);)] //But different migration
|
||||
}
|
||||
}
|
||||
|
||||
let tempdir = tempfile::Builder::new()
|
||||
@@ -248,16 +305,25 @@ mod tests {
|
||||
enum CorruptedDB {}
|
||||
|
||||
impl Domain for CorruptedDB {
|
||||
const NAME: &str = "db_tests";
|
||||
fn name() -> &'static str {
|
||||
"db_tests"
|
||||
}
|
||||
|
||||
const MIGRATIONS: &[&str] = &[sql!(CREATE TABLE test(value);)];
|
||||
fn migrations() -> &'static [&'static str] {
|
||||
&[sql!(CREATE TABLE test(value);)]
|
||||
}
|
||||
}
|
||||
|
||||
enum GoodDB {}
|
||||
|
||||
impl Domain for GoodDB {
|
||||
const NAME: &str = "db_tests"; //Notice same name
|
||||
const MIGRATIONS: &[&str] = &[sql!(CREATE TABLE test2(value);)]; // But different migration
|
||||
fn name() -> &'static str {
|
||||
"db_tests" //Notice same name
|
||||
}
|
||||
|
||||
fn migrations() -> &'static [&'static str] {
|
||||
&[sql!(CREATE TABLE test2(value);)] //But different migration
|
||||
}
|
||||
}
|
||||
|
||||
let tempdir = tempfile::Builder::new()
|
||||
|
||||
@@ -2,26 +2,16 @@ use gpui::App;
|
||||
use sqlez_macros::sql;
|
||||
use util::ResultExt as _;
|
||||
|
||||
use crate::{
|
||||
query,
|
||||
sqlez::{domain::Domain, thread_safe_connection::ThreadSafeConnection},
|
||||
write_and_log,
|
||||
};
|
||||
use crate::{define_connection, query, write_and_log};
|
||||
|
||||
pub struct KeyValueStore(crate::sqlez::thread_safe_connection::ThreadSafeConnection);
|
||||
|
||||
impl Domain for KeyValueStore {
|
||||
const NAME: &str = stringify!(KeyValueStore);
|
||||
|
||||
const MIGRATIONS: &[&str] = &[sql!(
|
||||
define_connection!(pub static ref KEY_VALUE_STORE: KeyValueStore<()> =
|
||||
&[sql!(
|
||||
CREATE TABLE IF NOT EXISTS kv_store(
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL
|
||||
) STRICT;
|
||||
)];
|
||||
}
|
||||
|
||||
crate::static_connection!(KEY_VALUE_STORE, KeyValueStore, []);
|
||||
);
|
||||
|
||||
pub trait Dismissable {
|
||||
const KEY: &'static str;
|
||||
@@ -101,19 +91,15 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct GlobalKeyValueStore(ThreadSafeConnection);
|
||||
|
||||
impl Domain for GlobalKeyValueStore {
|
||||
const NAME: &str = stringify!(GlobalKeyValueStore);
|
||||
const MIGRATIONS: &[&str] = &[sql!(
|
||||
define_connection!(pub static ref GLOBAL_KEY_VALUE_STORE: GlobalKeyValueStore<()> =
|
||||
&[sql!(
|
||||
CREATE TABLE IF NOT EXISTS kv_store(
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL
|
||||
) STRICT;
|
||||
)];
|
||||
}
|
||||
|
||||
crate::static_connection!(GLOBAL_KEY_VALUE_STORE, GlobalKeyValueStore, [], global);
|
||||
global
|
||||
);
|
||||
|
||||
impl GlobalKeyValueStore {
|
||||
query! {
|
||||
|
||||
@@ -916,11 +916,10 @@ impl RunningState {
|
||||
let task_store = project.read(cx).task_store().downgrade();
|
||||
let weak_project = project.downgrade();
|
||||
let weak_workspace = workspace.downgrade();
|
||||
let remote_shell = project
|
||||
let ssh_info = project
|
||||
.read(cx)
|
||||
.remote_client()
|
||||
.as_ref()
|
||||
.and_then(|remote| remote.read(cx).shell());
|
||||
.ssh_client()
|
||||
.and_then(|it| it.read(cx).ssh_info());
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let DebugScenario {
|
||||
@@ -1004,7 +1003,7 @@ impl RunningState {
|
||||
None
|
||||
};
|
||||
|
||||
let builder = ShellBuilder::new(remote_shell.as_deref(), &task.resolved.shell);
|
||||
let builder = ShellBuilder::new(ssh_info.as_ref().map(|info| &*info.shell), &task.resolved.shell);
|
||||
let command_label = builder.command_label(&task.resolved.command_label);
|
||||
let (command, args) =
|
||||
builder.build(task.resolved.command.clone(), &task.resolved.args);
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -20074,7 +20074,7 @@ impl Editor {
|
||||
let (telemetry, is_via_ssh) = {
|
||||
let project = project.read(cx);
|
||||
let telemetry = project.client().telemetry().clone();
|
||||
let is_via_ssh = project.is_via_remote_server();
|
||||
let is_via_ssh = project.is_via_ssh();
|
||||
(telemetry, is_via_ssh)
|
||||
};
|
||||
refresh_linked_ranges(self, window, cx);
|
||||
@@ -20642,7 +20642,7 @@ impl Editor {
|
||||
copilot_enabled,
|
||||
copilot_enabled_for_language,
|
||||
edit_predictions_provider,
|
||||
is_via_ssh = project.is_via_remote_server(),
|
||||
is_via_ssh = project.is_via_ssh(),
|
||||
);
|
||||
} else {
|
||||
telemetry::event!(
|
||||
@@ -20652,7 +20652,7 @@ impl Editor {
|
||||
copilot_enabled,
|
||||
copilot_enabled_for_language,
|
||||
edit_predictions_provider,
|
||||
is_via_ssh = project.is_via_remote_server(),
|
||||
is_via_ssh = project.is_via_ssh(),
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -1404,7 +1404,7 @@ impl ProjectItem for Editor {
|
||||
}
|
||||
|
||||
fn for_broken_project_item(
|
||||
abs_path: &Path,
|
||||
abs_path: PathBuf,
|
||||
is_local: bool,
|
||||
e: &anyhow::Error,
|
||||
window: &mut Window,
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
use anyhow::Result;
|
||||
use db::{
|
||||
query,
|
||||
sqlez::{
|
||||
bindable::{Bind, Column, StaticColumnCount},
|
||||
domain::Domain,
|
||||
statement::Statement,
|
||||
},
|
||||
sqlez_macros::sql,
|
||||
};
|
||||
use db::sqlez::bindable::{Bind, Column, StaticColumnCount};
|
||||
use db::sqlez::statement::Statement;
|
||||
use fs::MTime;
|
||||
use itertools::Itertools as _;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use db::sqlez_macros::sql;
|
||||
use db::{define_connection, query};
|
||||
|
||||
use workspace::{ItemId, WorkspaceDb, WorkspaceId};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Default)]
|
||||
@@ -87,11 +83,7 @@ impl Column for SerializedEditor {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EditorDb(db::sqlez::thread_safe_connection::ThreadSafeConnection);
|
||||
|
||||
impl Domain for EditorDb {
|
||||
const NAME: &str = stringify!(EditorDb);
|
||||
|
||||
define_connection!(
|
||||
// Current schema shape using pseudo-rust syntax:
|
||||
// editors(
|
||||
// item_id: usize,
|
||||
@@ -121,8 +113,7 @@ impl Domain for EditorDb {
|
||||
// start: usize,
|
||||
// end: usize,
|
||||
// )
|
||||
|
||||
const MIGRATIONS: &[&str] = &[
|
||||
pub static ref DB: EditorDb<WorkspaceDb> = &[
|
||||
sql! (
|
||||
CREATE TABLE editors(
|
||||
item_id INTEGER NOT NULL,
|
||||
@@ -198,9 +189,7 @@ impl Domain for EditorDb {
|
||||
) STRICT;
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
db::static_connection!(DB, EditorDb, [WorkspaceDb]);
|
||||
);
|
||||
|
||||
// https://www.sqlite.org/limits.html
|
||||
// > <..> the maximum value of a host parameter number is SQLITE_MAX_VARIABLE_NUMBER,
|
||||
|
||||
@@ -43,7 +43,7 @@ use language::{
|
||||
use node_runtime::NodeRuntime;
|
||||
use project::ContextProviderWithTasks;
|
||||
use release_channel::ReleaseChannel;
|
||||
use remote::RemoteClient;
|
||||
use remote::SshRemoteClient;
|
||||
use semantic_version::SemanticVersion;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::Settings;
|
||||
@@ -117,7 +117,7 @@ pub struct ExtensionStore {
|
||||
pub wasm_host: Arc<WasmHost>,
|
||||
pub wasm_extensions: Vec<(Arc<ExtensionManifest>, WasmExtension)>,
|
||||
pub tasks: Vec<Task<()>>,
|
||||
pub remote_clients: HashMap<String, WeakEntity<RemoteClient>>,
|
||||
pub ssh_clients: HashMap<String, WeakEntity<SshRemoteClient>>,
|
||||
pub ssh_registered_tx: UnboundedSender<()>,
|
||||
}
|
||||
|
||||
@@ -270,7 +270,7 @@ impl ExtensionStore {
|
||||
reload_tx,
|
||||
tasks: Vec::new(),
|
||||
|
||||
remote_clients: HashMap::default(),
|
||||
ssh_clients: HashMap::default(),
|
||||
ssh_registered_tx: connection_registered_tx,
|
||||
};
|
||||
|
||||
@@ -1693,7 +1693,7 @@ impl ExtensionStore {
|
||||
|
||||
async fn sync_extensions_over_ssh(
|
||||
this: &WeakEntity<Self>,
|
||||
client: WeakEntity<RemoteClient>,
|
||||
client: WeakEntity<SshRemoteClient>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<()> {
|
||||
let extensions = this.update(cx, |this, _cx| {
|
||||
@@ -1765,8 +1765,8 @@ impl ExtensionStore {
|
||||
|
||||
pub async fn update_ssh_clients(this: &WeakEntity<Self>, cx: &mut AsyncApp) -> Result<()> {
|
||||
let clients = this.update(cx, |this, _cx| {
|
||||
this.remote_clients.retain(|_k, v| v.upgrade().is_some());
|
||||
this.remote_clients.values().cloned().collect::<Vec<_>>()
|
||||
this.ssh_clients.retain(|_k, v| v.upgrade().is_some());
|
||||
this.ssh_clients.values().cloned().collect::<Vec<_>>()
|
||||
})?;
|
||||
|
||||
for client in clients {
|
||||
@@ -1778,17 +1778,17 @@ impl ExtensionStore {
|
||||
anyhow::Ok(())
|
||||
}
|
||||
|
||||
pub fn register_remote_client(&mut self, client: Entity<RemoteClient>, cx: &mut Context<Self>) {
|
||||
pub fn register_ssh_client(&mut self, client: Entity<SshRemoteClient>, cx: &mut Context<Self>) {
|
||||
let connection_options = client.read(cx).connection_options();
|
||||
let ssh_url = connection_options.ssh_url();
|
||||
|
||||
if let Some(existing_client) = self.remote_clients.get(&ssh_url)
|
||||
if let Some(existing_client) = self.ssh_clients.get(&ssh_url)
|
||||
&& existing_client.upgrade().is_some()
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
self.remote_clients.insert(ssh_url, client.downgrade());
|
||||
self.ssh_clients.insert(ssh_url, client.downgrade());
|
||||
self.ssh_registered_tx.unbounded_send(()).ok();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -1381,7 +1381,7 @@ impl PickerDelegate for FileFinderDelegate {
|
||||
project
|
||||
.worktree_for_id(history_item.project.worktree_id, cx)
|
||||
.is_some()
|
||||
|| ((project.is_local() || project.is_via_remote_server())
|
||||
|| ((project.is_local() || project.is_via_ssh())
|
||||
&& history_item.absolute.is_some())
|
||||
}),
|
||||
self.currently_opened_path.as_ref(),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -401,19 +401,12 @@ pub fn init(cx: &mut App) {
|
||||
mod persistence {
|
||||
use std::path::PathBuf;
|
||||
|
||||
use db::{
|
||||
query,
|
||||
sqlez::{domain::Domain, thread_safe_connection::ThreadSafeConnection},
|
||||
sqlez_macros::sql,
|
||||
};
|
||||
use db::{define_connection, query, sqlez_macros::sql};
|
||||
use workspace::{ItemId, WorkspaceDb, WorkspaceId};
|
||||
|
||||
pub struct ImageViewerDb(ThreadSafeConnection);
|
||||
|
||||
impl Domain for ImageViewerDb {
|
||||
const NAME: &str = stringify!(ImageViewerDb);
|
||||
|
||||
const MIGRATIONS: &[&str] = &[sql!(
|
||||
define_connection! {
|
||||
pub static ref IMAGE_VIEWER: ImageViewerDb<WorkspaceDb> =
|
||||
&[sql!(
|
||||
CREATE TABLE image_viewers (
|
||||
workspace_id INTEGER,
|
||||
item_id INTEGER UNIQUE,
|
||||
@@ -424,11 +417,9 @@ mod persistence {
|
||||
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
|
||||
ON DELETE CASCADE
|
||||
) STRICT;
|
||||
)];
|
||||
)];
|
||||
}
|
||||
|
||||
db::static_connection!(IMAGE_VIEWER, ImageViewerDb, [WorkspaceDb]);
|
||||
|
||||
impl ImageViewerDb {
|
||||
query! {
|
||||
pub async fn save_image_path(
|
||||
|
||||
@@ -1569,21 +1569,11 @@ impl Buffer {
|
||||
self.send_operation(op, true, cx);
|
||||
}
|
||||
|
||||
pub fn buffer_diagnostics(
|
||||
&self,
|
||||
for_server: Option<LanguageServerId>,
|
||||
) -> Vec<&DiagnosticEntry<Anchor>> {
|
||||
match for_server {
|
||||
Some(server_id) => match self.diagnostics.binary_search_by_key(&server_id, |v| v.0) {
|
||||
Ok(idx) => self.diagnostics[idx].1.iter().collect(),
|
||||
Err(_) => Vec::new(),
|
||||
},
|
||||
None => self
|
||||
.diagnostics
|
||||
.iter()
|
||||
.flat_map(|(_, diagnostic_set)| diagnostic_set.iter())
|
||||
.collect(),
|
||||
}
|
||||
pub fn get_diagnostics(&self, server_id: LanguageServerId) -> Option<&DiagnosticSet> {
|
||||
let Ok(idx) = self.diagnostics.binary_search_by_key(&server_id, |v| v.0) else {
|
||||
return None;
|
||||
};
|
||||
Some(&self.diagnostics[idx].1)
|
||||
}
|
||||
|
||||
fn request_autoindent(&mut self, cx: &mut Context<Self>) {
|
||||
|
||||
@@ -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()))?;
|
||||
|
||||
|
||||
@@ -319,7 +319,7 @@ impl LanguageModel for XAiLanguageModel {
|
||||
}
|
||||
fn tool_input_format(&self) -> LanguageModelToolSchemaFormat {
|
||||
let model_id = self.model.id().trim().to_lowercase();
|
||||
if model_id.eq(x_ai::Model::Grok4.id()) || model_id.eq(x_ai::Model::GrokCodeFast1.id()) {
|
||||
if model_id.eq(x_ai::Model::Grok4.id()) {
|
||||
LanguageModelToolSchemaFormat::JsonSchemaSubset
|
||||
} else {
|
||||
LanguageModelToolSchemaFormat::JsonSchema
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -222,7 +222,7 @@ pub fn init(cx: &mut App) {
|
||||
|
||||
cx.observe_new(move |workspace: &mut Workspace, _, cx| {
|
||||
let project = workspace.project();
|
||||
if project.read(cx).is_local() || project.read(cx).is_via_remote_server() {
|
||||
if project.read(cx).is_local() || project.read(cx).is_via_ssh() {
|
||||
log_store.update(cx, |store, cx| {
|
||||
store.add_project(project, cx);
|
||||
});
|
||||
@@ -231,7 +231,7 @@ pub fn init(cx: &mut App) {
|
||||
let log_store = log_store.clone();
|
||||
workspace.register_action(move |workspace, _: &OpenLanguageServerLogs, window, cx| {
|
||||
let project = workspace.project().read(cx);
|
||||
if project.is_local() || project.is_via_remote_server() {
|
||||
if project.is_local() || project.is_via_ssh() {
|
||||
let project = workspace.project().clone();
|
||||
let log_store = log_store.clone();
|
||||
get_or_create_tool(
|
||||
@@ -321,7 +321,7 @@ impl LogStore {
|
||||
.retain(|_, state| state.kind.project() != Some(&weak_project));
|
||||
}),
|
||||
cx.subscribe(project, |this, project, event, cx| {
|
||||
let server_kind = if project.read(cx).is_via_remote_server() {
|
||||
let server_kind = if project.read(cx).is_via_ssh() {
|
||||
LanguageServerKind::Remote {
|
||||
project: project.downgrade(),
|
||||
}
|
||||
@@ -1743,5 +1743,6 @@ pub enum Event {
|
||||
}
|
||||
|
||||
impl EventEmitter<Event> for LogStore {}
|
||||
impl EventEmitter<Event> for LspLogView {}
|
||||
impl EventEmitter<EditorEvent> for LspLogView {}
|
||||
impl EventEmitter<SearchEvent> for LspLogView {}
|
||||
|
||||
@@ -3,27 +3,8 @@
|
||||
(namespace_identifier) @namespace
|
||||
|
||||
(concept_definition
|
||||
name: (identifier) @concept)
|
||||
(identifier) @concept)
|
||||
|
||||
(requires_clause
|
||||
constraint: (template_type
|
||||
name: (type_identifier) @concept))
|
||||
|
||||
(module_name
|
||||
(identifier) @module)
|
||||
|
||||
(module_declaration
|
||||
name: (module_name
|
||||
(identifier) @module))
|
||||
|
||||
(import_declaration
|
||||
name: (module_name
|
||||
(identifier) @module))
|
||||
|
||||
(import_declaration
|
||||
partition: (module_partition
|
||||
(module_name
|
||||
(identifier) @module)))
|
||||
|
||||
(call_expression
|
||||
function: (qualified_identifier
|
||||
@@ -80,9 +61,6 @@
|
||||
(operator_name
|
||||
(identifier)? @operator) @function
|
||||
|
||||
(operator_name
|
||||
"<=>" @operator.spaceship)
|
||||
|
||||
(destructor_name (identifier) @function)
|
||||
|
||||
((namespace_identifier) @type
|
||||
@@ -90,17 +68,21 @@
|
||||
|
||||
(auto) @type
|
||||
(type_identifier) @type
|
||||
type: (primitive_type) @type.builtin
|
||||
(sized_type_specifier) @type.builtin
|
||||
type :(primitive_type) @type.primitive
|
||||
(sized_type_specifier) @type.primitive
|
||||
|
||||
(requires_clause
|
||||
constraint: (template_type
|
||||
name: (type_identifier) @concept))
|
||||
|
||||
(attribute
|
||||
name: (identifier) @attribute)
|
||||
name: (identifier) @keyword)
|
||||
|
||||
((identifier) @constant.builtin
|
||||
(#match? @constant.builtin "^_*[A-Z][A-Z\\d_]*$"))
|
||||
((identifier) @constant
|
||||
(#match? @constant "^_*[A-Z][A-Z\\d_]*$"))
|
||||
|
||||
(statement_identifier) @label
|
||||
(this) @variable.builtin
|
||||
(this) @variable.special
|
||||
("static_assert") @function.builtin
|
||||
|
||||
[
|
||||
@@ -114,9 +96,7 @@ type: (primitive_type) @type.builtin
|
||||
"co_return"
|
||||
"co_yield"
|
||||
"concept"
|
||||
"consteval"
|
||||
"constexpr"
|
||||
"constinit"
|
||||
"continue"
|
||||
"decltype"
|
||||
"default"
|
||||
@@ -125,20 +105,15 @@ type: (primitive_type) @type.builtin
|
||||
"else"
|
||||
"enum"
|
||||
"explicit"
|
||||
"export"
|
||||
"extern"
|
||||
"final"
|
||||
"for"
|
||||
"friend"
|
||||
"goto"
|
||||
"if"
|
||||
"import"
|
||||
"inline"
|
||||
"module"
|
||||
"namespace"
|
||||
"new"
|
||||
"noexcept"
|
||||
"operator"
|
||||
"override"
|
||||
"private"
|
||||
"protected"
|
||||
@@ -149,7 +124,6 @@ type: (primitive_type) @type.builtin
|
||||
"struct"
|
||||
"switch"
|
||||
"template"
|
||||
"thread_local"
|
||||
"throw"
|
||||
"try"
|
||||
"typedef"
|
||||
@@ -172,7 +146,7 @@ type: (primitive_type) @type.builtin
|
||||
"#ifndef"
|
||||
"#include"
|
||||
(preproc_directive)
|
||||
] @keyword.directive
|
||||
] @keyword
|
||||
|
||||
(comment) @comment
|
||||
|
||||
@@ -250,24 +224,10 @@ type: (primitive_type) @type.builtin
|
||||
">"
|
||||
"<="
|
||||
">="
|
||||
"<=>"
|
||||
"||"
|
||||
"?"
|
||||
"and"
|
||||
"and_eq"
|
||||
"bitand"
|
||||
"bitor"
|
||||
"compl"
|
||||
"not"
|
||||
"not_eq"
|
||||
"or"
|
||||
"or_eq"
|
||||
"xor"
|
||||
"xor_eq"
|
||||
] @operator
|
||||
|
||||
"<=>" @operator.spaceship
|
||||
|
||||
(binary_expression
|
||||
operator: "<=>" @operator.spaceship)
|
||||
|
||||
(conditional_expression ":" @operator)
|
||||
(user_defined_literal (literal_suffix) @operator)
|
||||
|
||||
@@ -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| {
|
||||
|
||||
@@ -850,19 +850,13 @@ impl workspace::SerializableItem for Onboarding {
|
||||
}
|
||||
|
||||
mod persistence {
|
||||
use db::{
|
||||
query,
|
||||
sqlez::{domain::Domain, thread_safe_connection::ThreadSafeConnection},
|
||||
sqlez_macros::sql,
|
||||
};
|
||||
use db::{define_connection, query, sqlez_macros::sql};
|
||||
use workspace::WorkspaceDb;
|
||||
|
||||
pub struct OnboardingPagesDb(ThreadSafeConnection);
|
||||
|
||||
impl Domain for OnboardingPagesDb {
|
||||
const NAME: &str = stringify!(OnboardingPagesDb);
|
||||
|
||||
const MIGRATIONS: &[&str] = &[sql!(
|
||||
define_connection! {
|
||||
pub static ref ONBOARDING_PAGES: OnboardingPagesDb<WorkspaceDb> =
|
||||
&[
|
||||
sql!(
|
||||
CREATE TABLE onboarding_pages (
|
||||
workspace_id INTEGER,
|
||||
item_id INTEGER UNIQUE,
|
||||
@@ -872,11 +866,10 @@ mod persistence {
|
||||
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
|
||||
ON DELETE CASCADE
|
||||
) STRICT;
|
||||
)];
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
db::static_connection!(ONBOARDING_PAGES, OnboardingPagesDb, [WorkspaceDb]);
|
||||
|
||||
impl OnboardingPagesDb {
|
||||
query! {
|
||||
pub async fn save_onboarding_page(
|
||||
|
||||
@@ -414,19 +414,13 @@ impl workspace::SerializableItem for WelcomePage {
|
||||
}
|
||||
|
||||
mod persistence {
|
||||
use db::{
|
||||
query,
|
||||
sqlez::{domain::Domain, thread_safe_connection::ThreadSafeConnection},
|
||||
sqlez_macros::sql,
|
||||
};
|
||||
use db::{define_connection, query, sqlez_macros::sql};
|
||||
use workspace::WorkspaceDb;
|
||||
|
||||
pub struct WelcomePagesDb(ThreadSafeConnection);
|
||||
|
||||
impl Domain for WelcomePagesDb {
|
||||
const NAME: &str = stringify!(WelcomePagesDb);
|
||||
|
||||
const MIGRATIONS: &[&str] = (&[sql!(
|
||||
define_connection! {
|
||||
pub static ref WELCOME_PAGES: WelcomePagesDb<WorkspaceDb> =
|
||||
&[
|
||||
sql!(
|
||||
CREATE TABLE welcome_pages (
|
||||
workspace_id INTEGER,
|
||||
item_id INTEGER UNIQUE,
|
||||
@@ -436,11 +430,10 @@ mod persistence {
|
||||
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
|
||||
ON DELETE CASCADE
|
||||
) STRICT;
|
||||
)]);
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
db::static_connection!(WELCOME_PAGES, WelcomePagesDb, [WorkspaceDb]);
|
||||
|
||||
impl WelcomePagesDb {
|
||||
query! {
|
||||
pub async fn save_welcome_page(
|
||||
|
||||
@@ -5102,9 +5102,9 @@ impl EventEmitter<PanelEvent> for OutlinePanel {}
|
||||
|
||||
impl Render for OutlinePanel {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let (is_local, is_via_ssh) = self.project.read_with(cx, |project, _| {
|
||||
(project.is_local(), project.is_via_remote_server())
|
||||
});
|
||||
let (is_local, is_via_ssh) = self
|
||||
.project
|
||||
.read_with(cx, |project, _| (project.is_local(), project.is_via_ssh()));
|
||||
let query = self.query(cx);
|
||||
let pinned = self.pinned;
|
||||
let settings = OutlinePanelSettings::get_global(cx);
|
||||
|
||||
@@ -5,8 +5,11 @@ use super::{
|
||||
session::{self, Session, SessionStateEvent},
|
||||
};
|
||||
use crate::{
|
||||
InlayHint, InlayHintLabel, ProjectEnvironment, ResolveState, debugger::session::SessionQuirks,
|
||||
project_settings::ProjectSettings, worktree_store::WorktreeStore,
|
||||
InlayHint, InlayHintLabel, ProjectEnvironment, ResolveState,
|
||||
debugger::session::SessionQuirks,
|
||||
project_settings::ProjectSettings,
|
||||
terminals::{SshCommand, wrap_for_ssh},
|
||||
worktree_store::WorktreeStore,
|
||||
};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use async_trait::async_trait;
|
||||
@@ -31,7 +34,7 @@ use http_client::HttpClient;
|
||||
use language::{Buffer, LanguageToolchainStore, language_settings::InlayHintKind};
|
||||
use node_runtime::NodeRuntime;
|
||||
|
||||
use remote::RemoteClient;
|
||||
use remote::{SshInfo, SshRemoteClient, ssh_session::SshArgs};
|
||||
use rpc::{
|
||||
AnyProtoClient, TypedEnvelope,
|
||||
proto::{self},
|
||||
@@ -65,7 +68,7 @@ pub enum DapStoreEvent {
|
||||
|
||||
enum DapStoreMode {
|
||||
Local(LocalDapStore),
|
||||
Remote(RemoteDapStore),
|
||||
Ssh(SshDapStore),
|
||||
Collab,
|
||||
}
|
||||
|
||||
@@ -77,8 +80,8 @@ pub struct LocalDapStore {
|
||||
toolchain_store: Arc<dyn LanguageToolchainStore>,
|
||||
}
|
||||
|
||||
pub struct RemoteDapStore {
|
||||
remote_client: Entity<RemoteClient>,
|
||||
pub struct SshDapStore {
|
||||
ssh_client: Entity<SshRemoteClient>,
|
||||
upstream_client: AnyProtoClient,
|
||||
upstream_project_id: u64,
|
||||
}
|
||||
@@ -144,16 +147,16 @@ impl DapStore {
|
||||
Self::new(mode, breakpoint_store, worktree_store, cx)
|
||||
}
|
||||
|
||||
pub fn new_remote(
|
||||
pub fn new_ssh(
|
||||
project_id: u64,
|
||||
remote_client: Entity<RemoteClient>,
|
||||
ssh_client: Entity<SshRemoteClient>,
|
||||
breakpoint_store: Entity<BreakpointStore>,
|
||||
worktree_store: Entity<WorktreeStore>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let mode = DapStoreMode::Remote(RemoteDapStore {
|
||||
upstream_client: remote_client.read(cx).proto_client(),
|
||||
remote_client,
|
||||
let mode = DapStoreMode::Ssh(SshDapStore {
|
||||
upstream_client: ssh_client.read(cx).proto_client(),
|
||||
ssh_client,
|
||||
upstream_project_id: project_id,
|
||||
});
|
||||
|
||||
@@ -239,51 +242,64 @@ impl DapStore {
|
||||
Ok(binary)
|
||||
})
|
||||
}
|
||||
DapStoreMode::Remote(remote) => {
|
||||
let request = remote
|
||||
.upstream_client
|
||||
.request(proto::GetDebugAdapterBinary {
|
||||
session_id: session_id.to_proto(),
|
||||
project_id: remote.upstream_project_id,
|
||||
worktree_id: worktree.read(cx).id().to_proto(),
|
||||
definition: Some(definition.to_proto()),
|
||||
});
|
||||
let remote = remote.remote_client.clone();
|
||||
DapStoreMode::Ssh(ssh) => {
|
||||
let request = ssh.upstream_client.request(proto::GetDebugAdapterBinary {
|
||||
session_id: session_id.to_proto(),
|
||||
project_id: ssh.upstream_project_id,
|
||||
worktree_id: worktree.read(cx).id().to_proto(),
|
||||
definition: Some(definition.to_proto()),
|
||||
});
|
||||
let ssh_client = ssh.ssh_client.clone();
|
||||
|
||||
cx.spawn(async move |_, cx| {
|
||||
let response = request.await?;
|
||||
let binary = DebugAdapterBinary::from_proto(response)?;
|
||||
let (mut ssh_command, envs, path_style, ssh_shell) =
|
||||
ssh_client.read_with(cx, |ssh, _| {
|
||||
let SshInfo {
|
||||
args: SshArgs { arguments, envs },
|
||||
path_style,
|
||||
shell,
|
||||
} = ssh.ssh_info().context("SSH arguments not found")?;
|
||||
anyhow::Ok((
|
||||
SshCommand { arguments },
|
||||
envs.unwrap_or_default(),
|
||||
path_style,
|
||||
shell,
|
||||
))
|
||||
})??;
|
||||
|
||||
let port_forwarding;
|
||||
let connection;
|
||||
let mut connection = None;
|
||||
if let Some(c) = binary.connection {
|
||||
let host = Ipv4Addr::LOCALHOST;
|
||||
let port = dap::transport::TcpTransport::unused_port(host).await?;
|
||||
port_forwarding = Some((port, c.host.to_string(), c.port));
|
||||
let local_bind_addr = Ipv4Addr::LOCALHOST;
|
||||
let port =
|
||||
dap::transport::TcpTransport::unused_port(local_bind_addr).await?;
|
||||
|
||||
ssh_command.add_port_forwarding(port, c.host.to_string(), c.port);
|
||||
connection = Some(TcpArguments {
|
||||
port,
|
||||
host,
|
||||
host: local_bind_addr,
|
||||
timeout: c.timeout,
|
||||
})
|
||||
} else {
|
||||
port_forwarding = None;
|
||||
connection = None;
|
||||
}
|
||||
|
||||
let command = remote.read_with(cx, |remote, _cx| {
|
||||
remote.build_command(
|
||||
binary.command,
|
||||
&binary.arguments,
|
||||
&binary.envs,
|
||||
binary.cwd.map(|path| path.display().to_string()),
|
||||
port_forwarding,
|
||||
)
|
||||
})??;
|
||||
let (program, args) = wrap_for_ssh(
|
||||
&ssh_shell,
|
||||
&ssh_command,
|
||||
binary
|
||||
.command
|
||||
.as_ref()
|
||||
.map(|command| (command, &binary.arguments)),
|
||||
binary.cwd.as_deref(),
|
||||
binary.envs,
|
||||
None,
|
||||
path_style,
|
||||
);
|
||||
|
||||
Ok(DebugAdapterBinary {
|
||||
command: Some(command.program),
|
||||
arguments: command.args,
|
||||
envs: command.env,
|
||||
command: Some(program),
|
||||
arguments: args,
|
||||
envs,
|
||||
cwd: None,
|
||||
connection,
|
||||
request_args: binary.request_args,
|
||||
@@ -349,9 +365,9 @@ impl DapStore {
|
||||
)))
|
||||
}
|
||||
}
|
||||
DapStoreMode::Remote(remote) => {
|
||||
let request = remote.upstream_client.request(proto::RunDebugLocators {
|
||||
project_id: remote.upstream_project_id,
|
||||
DapStoreMode::Ssh(ssh) => {
|
||||
let request = ssh.upstream_client.request(proto::RunDebugLocators {
|
||||
project_id: ssh.upstream_project_id,
|
||||
build_command: Some(build_command.to_proto()),
|
||||
locator: locator_name.to_owned(),
|
||||
});
|
||||
|
||||
@@ -44,7 +44,7 @@ use parking_lot::Mutex;
|
||||
use postage::stream::Stream as _;
|
||||
use rpc::{
|
||||
AnyProtoClient, TypedEnvelope,
|
||||
proto::{self, FromProto, ToProto, git_reset, split_repository_update},
|
||||
proto::{self, FromProto, SSH_PROJECT_ID, ToProto, git_reset, split_repository_update},
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use std::{
|
||||
@@ -141,10 +141,14 @@ enum GitStoreState {
|
||||
project_environment: Entity<ProjectEnvironment>,
|
||||
fs: Arc<dyn Fs>,
|
||||
},
|
||||
Ssh {
|
||||
upstream_client: AnyProtoClient,
|
||||
upstream_project_id: ProjectId,
|
||||
downstream: Option<(AnyProtoClient, ProjectId)>,
|
||||
},
|
||||
Remote {
|
||||
upstream_client: AnyProtoClient,
|
||||
upstream_project_id: u64,
|
||||
downstream: Option<(AnyProtoClient, ProjectId)>,
|
||||
upstream_project_id: ProjectId,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -351,7 +355,7 @@ impl GitStore {
|
||||
worktree_store: &Entity<WorktreeStore>,
|
||||
buffer_store: Entity<BufferStore>,
|
||||
upstream_client: AnyProtoClient,
|
||||
project_id: u64,
|
||||
project_id: ProjectId,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
Self::new(
|
||||
@@ -360,6 +364,23 @@ impl GitStore {
|
||||
GitStoreState::Remote {
|
||||
upstream_client,
|
||||
upstream_project_id: project_id,
|
||||
},
|
||||
cx,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn ssh(
|
||||
worktree_store: &Entity<WorktreeStore>,
|
||||
buffer_store: Entity<BufferStore>,
|
||||
upstream_client: AnyProtoClient,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
Self::new(
|
||||
worktree_store.clone(),
|
||||
buffer_store,
|
||||
GitStoreState::Ssh {
|
||||
upstream_client,
|
||||
upstream_project_id: ProjectId(SSH_PROJECT_ID),
|
||||
downstream: None,
|
||||
},
|
||||
cx,
|
||||
@@ -430,7 +451,7 @@ impl GitStore {
|
||||
|
||||
pub fn shared(&mut self, project_id: u64, client: AnyProtoClient, cx: &mut Context<Self>) {
|
||||
match &mut self.state {
|
||||
GitStoreState::Remote {
|
||||
GitStoreState::Ssh {
|
||||
downstream: downstream_client,
|
||||
..
|
||||
} => {
|
||||
@@ -506,6 +527,9 @@ impl GitStore {
|
||||
}),
|
||||
});
|
||||
}
|
||||
GitStoreState::Remote { .. } => {
|
||||
debug_panic!("shared called on remote store");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -517,12 +541,15 @@ impl GitStore {
|
||||
} => {
|
||||
downstream_client.take();
|
||||
}
|
||||
GitStoreState::Remote {
|
||||
GitStoreState::Ssh {
|
||||
downstream: downstream_client,
|
||||
..
|
||||
} => {
|
||||
downstream_client.take();
|
||||
}
|
||||
GitStoreState::Remote { .. } => {
|
||||
debug_panic!("unshared called on remote store");
|
||||
}
|
||||
}
|
||||
self.shared_diffs.clear();
|
||||
}
|
||||
@@ -1020,17 +1047,21 @@ impl GitStore {
|
||||
} => downstream_client
|
||||
.as_ref()
|
||||
.map(|state| (state.client.clone(), state.project_id)),
|
||||
GitStoreState::Remote {
|
||||
GitStoreState::Ssh {
|
||||
downstream: downstream_client,
|
||||
..
|
||||
} => downstream_client.clone(),
|
||||
GitStoreState::Remote { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn upstream_client(&self) -> Option<AnyProtoClient> {
|
||||
match &self.state {
|
||||
GitStoreState::Local { .. } => None,
|
||||
GitStoreState::Remote {
|
||||
GitStoreState::Ssh {
|
||||
upstream_client, ..
|
||||
}
|
||||
| GitStoreState::Remote {
|
||||
upstream_client, ..
|
||||
} => Some(upstream_client.clone()),
|
||||
}
|
||||
@@ -1401,7 +1432,12 @@ impl GitStore {
|
||||
cx.background_executor()
|
||||
.spawn(async move { fs.git_init(&path, fallback_branch_name) })
|
||||
}
|
||||
GitStoreState::Remote {
|
||||
GitStoreState::Ssh {
|
||||
upstream_client,
|
||||
upstream_project_id: project_id,
|
||||
..
|
||||
}
|
||||
| GitStoreState::Remote {
|
||||
upstream_client,
|
||||
upstream_project_id: project_id,
|
||||
..
|
||||
@@ -1411,7 +1447,7 @@ impl GitStore {
|
||||
cx.background_executor().spawn(async move {
|
||||
client
|
||||
.request(proto::GitInit {
|
||||
project_id: project_id,
|
||||
project_id: project_id.0,
|
||||
abs_path: path.to_string_lossy().to_string(),
|
||||
fallback_branch_name,
|
||||
})
|
||||
@@ -1435,18 +1471,13 @@ impl GitStore {
|
||||
cx.background_executor()
|
||||
.spawn(async move { fs.git_clone(&repo, &path).await })
|
||||
}
|
||||
GitStoreState::Remote {
|
||||
GitStoreState::Ssh {
|
||||
upstream_client,
|
||||
upstream_project_id,
|
||||
..
|
||||
} => {
|
||||
if upstream_client.is_via_collab() {
|
||||
return Task::ready(Err(anyhow!(
|
||||
"Git Clone isn't supported for project guests"
|
||||
)));
|
||||
}
|
||||
let request = upstream_client.request(proto::GitClone {
|
||||
project_id: *upstream_project_id,
|
||||
project_id: upstream_project_id.0,
|
||||
abs_path: path.to_string_lossy().to_string(),
|
||||
remote_repo: repo,
|
||||
});
|
||||
@@ -1460,6 +1491,9 @@ impl GitStore {
|
||||
}
|
||||
})
|
||||
}
|
||||
GitStoreState::Remote { .. } => {
|
||||
Task::ready(Err(anyhow!("Git Clone isn't supported for remote users")))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7588,16 +7588,19 @@ impl LspStore {
|
||||
let snapshot = buffer_handle.read(cx).snapshot();
|
||||
let buffer = buffer_handle.read(cx);
|
||||
let reused_diagnostics = buffer
|
||||
.buffer_diagnostics(Some(server_id))
|
||||
.iter()
|
||||
.filter(|v| merge(buffer, &v.diagnostic, cx))
|
||||
.map(|v| {
|
||||
let start = Unclipped(v.range.start.to_point_utf16(&snapshot));
|
||||
let end = Unclipped(v.range.end.to_point_utf16(&snapshot));
|
||||
DiagnosticEntry {
|
||||
range: start..end,
|
||||
diagnostic: v.diagnostic.clone(),
|
||||
}
|
||||
.get_diagnostics(server_id)
|
||||
.into_iter()
|
||||
.flat_map(|diag| {
|
||||
diag.iter()
|
||||
.filter(|v| merge(buffer, &v.diagnostic, cx))
|
||||
.map(|v| {
|
||||
let start = Unclipped(v.range.start.to_point_utf16(&snapshot));
|
||||
let end = Unclipped(v.range.end.to_point_utf16(&snapshot));
|
||||
DiagnosticEntry {
|
||||
range: start..end,
|
||||
diagnostic: v.diagnostic.clone(),
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
@@ -11703,108 +11706,135 @@ impl LspStore {
|
||||
// Ignore payload since we notify clients of setting changes unconditionally, relying on them pulling the latest settings.
|
||||
}
|
||||
"workspace/symbol" => {
|
||||
let options = parse_register_or(®, || OneOf::Left(true))?;
|
||||
let options = parse_register_capabilities(reg)?;
|
||||
server.update_capabilities(|capabilities| {
|
||||
capabilities.workspace_symbol_provider = Some(options);
|
||||
});
|
||||
notify_server_capabilities_updated(&server, cx);
|
||||
}
|
||||
"workspace/fileOperations" => {
|
||||
let caps = parse_register_or_default(®)?;
|
||||
server.update_capabilities(|capabilities| {
|
||||
capabilities
|
||||
.workspace
|
||||
.get_or_insert_default()
|
||||
.file_operations = Some(caps);
|
||||
});
|
||||
notify_server_capabilities_updated(&server, cx);
|
||||
if let Some(options) = reg.register_options {
|
||||
let caps = serde_json::from_value(options)?;
|
||||
server.update_capabilities(|capabilities| {
|
||||
capabilities
|
||||
.workspace
|
||||
.get_or_insert_default()
|
||||
.file_operations = Some(caps);
|
||||
});
|
||||
notify_server_capabilities_updated(&server, cx);
|
||||
}
|
||||
}
|
||||
"workspace/executeCommand" => {
|
||||
let options = parse_register_or_default(®)?;
|
||||
server.update_capabilities(|capabilities| {
|
||||
capabilities.execute_command_provider = Some(options);
|
||||
});
|
||||
notify_server_capabilities_updated(&server, cx);
|
||||
if let Some(options) = reg.register_options {
|
||||
let options = serde_json::from_value(options)?;
|
||||
server.update_capabilities(|capabilities| {
|
||||
capabilities.execute_command_provider = Some(options);
|
||||
});
|
||||
notify_server_capabilities_updated(&server, cx);
|
||||
}
|
||||
}
|
||||
"textDocument/rangeFormatting" => {
|
||||
let options = parse_register_or(®, || OneOf::Left(true))?;
|
||||
let options = parse_register_capabilities(reg)?;
|
||||
server.update_capabilities(|capabilities| {
|
||||
capabilities.document_range_formatting_provider = Some(options);
|
||||
});
|
||||
notify_server_capabilities_updated(&server, cx);
|
||||
}
|
||||
"textDocument/onTypeFormatting" => {
|
||||
let options = parse_register_or_default(®)?;
|
||||
server.update_capabilities(|capabilities| {
|
||||
capabilities.document_on_type_formatting_provider = Some(options);
|
||||
});
|
||||
notify_server_capabilities_updated(&server, cx);
|
||||
if let Some(options) = reg
|
||||
.register_options
|
||||
.map(serde_json::from_value)
|
||||
.transpose()?
|
||||
{
|
||||
server.update_capabilities(|capabilities| {
|
||||
capabilities.document_on_type_formatting_provider = Some(options);
|
||||
});
|
||||
notify_server_capabilities_updated(&server, cx);
|
||||
}
|
||||
}
|
||||
"textDocument/formatting" => {
|
||||
let options = parse_register_or(®, || OneOf::Left(true))?;
|
||||
let options = parse_register_capabilities(reg)?;
|
||||
server.update_capabilities(|capabilities| {
|
||||
capabilities.document_formatting_provider = Some(options);
|
||||
});
|
||||
notify_server_capabilities_updated(&server, cx);
|
||||
}
|
||||
"textDocument/rename" => {
|
||||
let options = parse_register_or(®, || OneOf::Left(true))?;
|
||||
let options = parse_register_capabilities(reg)?;
|
||||
server.update_capabilities(|capabilities| {
|
||||
capabilities.rename_provider = Some(options);
|
||||
});
|
||||
notify_server_capabilities_updated(&server, cx);
|
||||
}
|
||||
"textDocument/inlayHint" => {
|
||||
let options = parse_register_or(®, || OneOf::Left(true))?;
|
||||
let options = parse_register_capabilities(reg)?;
|
||||
server.update_capabilities(|capabilities| {
|
||||
capabilities.inlay_hint_provider = Some(options);
|
||||
});
|
||||
notify_server_capabilities_updated(&server, cx);
|
||||
}
|
||||
"textDocument/documentSymbol" => {
|
||||
let options = parse_register_or(®, || OneOf::Left(true))?;
|
||||
let options = parse_register_capabilities(reg)?;
|
||||
server.update_capabilities(|capabilities| {
|
||||
capabilities.document_symbol_provider = Some(options);
|
||||
});
|
||||
notify_server_capabilities_updated(&server, cx);
|
||||
}
|
||||
"textDocument/codeAction" => {
|
||||
let provider = parse_register_or(®, || {
|
||||
lsp::CodeActionProviderCapability::Simple(true)
|
||||
})?;
|
||||
server.update_capabilities(|capabilities| {
|
||||
capabilities.code_action_provider = Some(provider);
|
||||
});
|
||||
notify_server_capabilities_updated(&server, cx);
|
||||
if let Some(options) = reg
|
||||
.register_options
|
||||
.map(serde_json::from_value)
|
||||
.transpose()?
|
||||
{
|
||||
server.update_capabilities(|capabilities| {
|
||||
capabilities.code_action_provider =
|
||||
Some(lsp::CodeActionProviderCapability::Options(options));
|
||||
});
|
||||
notify_server_capabilities_updated(&server, cx);
|
||||
}
|
||||
}
|
||||
"textDocument/definition" => {
|
||||
let options = parse_register_or(®, || OneOf::Left(true))?;
|
||||
let options = parse_register_capabilities(reg)?;
|
||||
server.update_capabilities(|capabilities| {
|
||||
capabilities.definition_provider = Some(options);
|
||||
});
|
||||
notify_server_capabilities_updated(&server, cx);
|
||||
}
|
||||
"textDocument/completion" => {
|
||||
let caps = parse_register_or_default(®)?;
|
||||
server.update_capabilities(|capabilities| {
|
||||
capabilities.completion_provider = Some(caps);
|
||||
});
|
||||
notify_server_capabilities_updated(&server, cx);
|
||||
if let Some(caps) = reg
|
||||
.register_options
|
||||
.map(serde_json::from_value)
|
||||
.transpose()?
|
||||
{
|
||||
server.update_capabilities(|capabilities| {
|
||||
capabilities.completion_provider = Some(caps);
|
||||
});
|
||||
notify_server_capabilities_updated(&server, cx);
|
||||
}
|
||||
}
|
||||
"textDocument/hover" => {
|
||||
let provider =
|
||||
parse_register_or(®, || lsp::HoverProviderCapability::Simple(true))?;
|
||||
server.update_capabilities(|capabilities| {
|
||||
capabilities.hover_provider = Some(provider);
|
||||
});
|
||||
notify_server_capabilities_updated(&server, cx);
|
||||
if let Some(caps) = reg
|
||||
.register_options
|
||||
.map(serde_json::from_value)
|
||||
.transpose()?
|
||||
{
|
||||
server.update_capabilities(|capabilities| {
|
||||
capabilities.hover_provider = Some(caps);
|
||||
});
|
||||
notify_server_capabilities_updated(&server, cx);
|
||||
}
|
||||
}
|
||||
"textDocument/signatureHelp" => {
|
||||
let caps = parse_register_or_default(®)?;
|
||||
server.update_capabilities(|capabilities| {
|
||||
capabilities.signature_help_provider = Some(caps);
|
||||
});
|
||||
notify_server_capabilities_updated(&server, cx);
|
||||
if let Some(caps) = reg
|
||||
.register_options
|
||||
.map(serde_json::from_value)
|
||||
.transpose()?
|
||||
{
|
||||
server.update_capabilities(|capabilities| {
|
||||
capabilities.signature_help_provider = Some(caps);
|
||||
});
|
||||
notify_server_capabilities_updated(&server, cx);
|
||||
}
|
||||
}
|
||||
"textDocument/didChange" => {
|
||||
if let Some(sync_kind) = reg
|
||||
@@ -11853,30 +11883,40 @@ impl LspStore {
|
||||
}
|
||||
}
|
||||
"textDocument/codeLens" => {
|
||||
let caps = parse_register_or(®, || lsp::CodeLensOptions {
|
||||
resolve_provider: None,
|
||||
})?;
|
||||
server.update_capabilities(|capabilities| {
|
||||
capabilities.code_lens_provider = Some(caps);
|
||||
});
|
||||
notify_server_capabilities_updated(&server, cx);
|
||||
if let Some(caps) = reg
|
||||
.register_options
|
||||
.map(serde_json::from_value)
|
||||
.transpose()?
|
||||
{
|
||||
server.update_capabilities(|capabilities| {
|
||||
capabilities.code_lens_provider = Some(caps);
|
||||
});
|
||||
notify_server_capabilities_updated(&server, cx);
|
||||
}
|
||||
}
|
||||
"textDocument/diagnostic" => {
|
||||
let caps = parse_register_or(®, || {
|
||||
lsp::DiagnosticServerCapabilities::Options(lsp::DiagnosticOptions::default())
|
||||
})?;
|
||||
server.update_capabilities(|capabilities| {
|
||||
capabilities.diagnostic_provider = Some(caps);
|
||||
});
|
||||
notify_server_capabilities_updated(&server, cx);
|
||||
if let Some(caps) = reg
|
||||
.register_options
|
||||
.map(serde_json::from_value)
|
||||
.transpose()?
|
||||
{
|
||||
server.update_capabilities(|capabilities| {
|
||||
capabilities.diagnostic_provider = Some(caps);
|
||||
});
|
||||
notify_server_capabilities_updated(&server, cx);
|
||||
}
|
||||
}
|
||||
"textDocument/documentColor" => {
|
||||
let provider =
|
||||
parse_register_or(®, || lsp::ColorProviderCapability::Simple(true))?;
|
||||
server.update_capabilities(|capabilities| {
|
||||
capabilities.color_provider = Some(provider);
|
||||
});
|
||||
notify_server_capabilities_updated(&server, cx);
|
||||
if let Some(caps) = reg
|
||||
.register_options
|
||||
.map(serde_json::from_value)
|
||||
.transpose()?
|
||||
{
|
||||
server.update_capabilities(|capabilities| {
|
||||
capabilities.color_provider = Some(caps);
|
||||
});
|
||||
notify_server_capabilities_updated(&server, cx);
|
||||
}
|
||||
}
|
||||
_ => log::warn!("unhandled capability registration: {reg:?}"),
|
||||
}
|
||||
@@ -12133,27 +12173,17 @@ impl LspStore {
|
||||
}
|
||||
}
|
||||
|
||||
// Registration with registerOptions as null, should fallback to true.
|
||||
// https://github.com/microsoft/vscode-languageserver-node/blob/d90a87f9557a0df9142cfb33e251cfa6fe27d970/client/src/common/client.ts#L2133
|
||||
// Parse register_options into T or return a provided default if None.
|
||||
fn parse_register_or<T, F>(reg: &lsp::Registration, default: F) -> Result<T>
|
||||
where
|
||||
T: serde::de::DeserializeOwned,
|
||||
F: FnOnce() -> T,
|
||||
{
|
||||
Ok(match reg.register_options.as_ref() {
|
||||
Some(options) => serde_json::from_value::<T>(options.clone())?,
|
||||
None => default(),
|
||||
fn parse_register_capabilities<T: serde::de::DeserializeOwned>(
|
||||
reg: lsp::Registration,
|
||||
) -> Result<OneOf<bool, T>> {
|
||||
Ok(match reg.register_options {
|
||||
Some(options) => OneOf::Right(serde_json::from_value::<T>(options)?),
|
||||
None => OneOf::Left(true),
|
||||
})
|
||||
}
|
||||
|
||||
// Parse register_options into T or default() if None.
|
||||
fn parse_register_or_default<T>(reg: &lsp::Registration) -> Result<T>
|
||||
where
|
||||
T: serde::de::DeserializeOwned + Default,
|
||||
{
|
||||
parse_register_or(reg, T::default)
|
||||
}
|
||||
|
||||
fn subscribe_to_binary_statuses(
|
||||
languages: &Arc<LanguageRegistry>,
|
||||
cx: &mut Context<'_, LspStore>,
|
||||
|
||||
@@ -42,7 +42,9 @@ pub use manifest_tree::ManifestTree;
|
||||
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use buffer_store::{BufferStore, BufferStoreEvent};
|
||||
use client::{Client, Collaborator, PendingEntitySubscription, TypedEnvelope, UserStore, proto};
|
||||
use client::{
|
||||
Client, Collaborator, PendingEntitySubscription, ProjectId, TypedEnvelope, UserStore, proto,
|
||||
};
|
||||
use clock::ReplicaId;
|
||||
|
||||
use dap::client::DebugAdapterClient;
|
||||
@@ -87,10 +89,10 @@ use node_runtime::NodeRuntime;
|
||||
use parking_lot::Mutex;
|
||||
pub use prettier_store::PrettierStore;
|
||||
use project_settings::{ProjectSettings, SettingsObserver, SettingsObserverEvent};
|
||||
use remote::{RemoteClient, SshConnectionOptions};
|
||||
use remote::{SshConnectionOptions, SshRemoteClient};
|
||||
use rpc::{
|
||||
AnyProtoClient, ErrorCode,
|
||||
proto::{FromProto, LanguageServerPromptResponse, REMOTE_SERVER_PROJECT_ID, ToProto},
|
||||
proto::{FromProto, LanguageServerPromptResponse, SSH_PROJECT_ID, ToProto},
|
||||
};
|
||||
use search::{SearchInputKind, SearchQuery, SearchResult};
|
||||
use search_history::SearchHistory;
|
||||
@@ -175,12 +177,12 @@ pub struct Project {
|
||||
dap_store: Entity<DapStore>,
|
||||
|
||||
breakpoint_store: Entity<BreakpointStore>,
|
||||
collab_client: Arc<client::Client>,
|
||||
client: Arc<client::Client>,
|
||||
join_project_response_message_id: u32,
|
||||
task_store: Entity<TaskStore>,
|
||||
user_store: Entity<UserStore>,
|
||||
fs: Arc<dyn Fs>,
|
||||
remote_client: Option<Entity<RemoteClient>>,
|
||||
ssh_client: Option<Entity<SshRemoteClient>>,
|
||||
client_state: ProjectClientState,
|
||||
git_store: Entity<GitStore>,
|
||||
collaborators: HashMap<proto::PeerId, Collaborator>,
|
||||
@@ -1152,12 +1154,12 @@ impl Project {
|
||||
active_entry: None,
|
||||
snippets,
|
||||
languages,
|
||||
collab_client: client,
|
||||
client,
|
||||
task_store,
|
||||
user_store,
|
||||
settings_observer,
|
||||
fs,
|
||||
remote_client: None,
|
||||
ssh_client: None,
|
||||
breakpoint_store,
|
||||
dap_store,
|
||||
|
||||
@@ -1181,8 +1183,8 @@ impl Project {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn remote(
|
||||
remote: Entity<RemoteClient>,
|
||||
pub fn ssh(
|
||||
ssh: Entity<SshRemoteClient>,
|
||||
client: Arc<Client>,
|
||||
node: NodeRuntime,
|
||||
user_store: Entity<UserStore>,
|
||||
@@ -1198,15 +1200,10 @@ impl Project {
|
||||
let snippets =
|
||||
SnippetProvider::new(fs.clone(), BTreeSet::from_iter([global_snippets_dir]), cx);
|
||||
|
||||
let (remote_proto, path_style) =
|
||||
remote.read_with(cx, |remote, _| (remote.proto_client(), remote.path_style()));
|
||||
let (ssh_proto, path_style) =
|
||||
ssh.read_with(cx, |ssh, _| (ssh.proto_client(), ssh.path_style()));
|
||||
let worktree_store = cx.new(|_| {
|
||||
WorktreeStore::remote(
|
||||
false,
|
||||
remote_proto.clone(),
|
||||
REMOTE_SERVER_PROJECT_ID,
|
||||
path_style,
|
||||
)
|
||||
WorktreeStore::remote(false, ssh_proto.clone(), SSH_PROJECT_ID, path_style)
|
||||
});
|
||||
cx.subscribe(&worktree_store, Self::on_worktree_store_event)
|
||||
.detach();
|
||||
@@ -1218,32 +1215,31 @@ impl Project {
|
||||
let buffer_store = cx.new(|cx| {
|
||||
BufferStore::remote(
|
||||
worktree_store.clone(),
|
||||
remote.read(cx).proto_client(),
|
||||
REMOTE_SERVER_PROJECT_ID,
|
||||
ssh.read(cx).proto_client(),
|
||||
SSH_PROJECT_ID,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let image_store = cx.new(|cx| {
|
||||
ImageStore::remote(
|
||||
worktree_store.clone(),
|
||||
remote.read(cx).proto_client(),
|
||||
REMOTE_SERVER_PROJECT_ID,
|
||||
ssh.read(cx).proto_client(),
|
||||
SSH_PROJECT_ID,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
cx.subscribe(&buffer_store, Self::on_buffer_store_event)
|
||||
.detach();
|
||||
let toolchain_store = cx.new(|cx| {
|
||||
ToolchainStore::remote(REMOTE_SERVER_PROJECT_ID, remote.read(cx).proto_client(), cx)
|
||||
});
|
||||
let toolchain_store = cx
|
||||
.new(|cx| ToolchainStore::remote(SSH_PROJECT_ID, ssh.read(cx).proto_client(), cx));
|
||||
let task_store = cx.new(|cx| {
|
||||
TaskStore::remote(
|
||||
fs.clone(),
|
||||
buffer_store.downgrade(),
|
||||
worktree_store.clone(),
|
||||
toolchain_store.read(cx).as_language_toolchain_store(),
|
||||
remote.read(cx).proto_client(),
|
||||
REMOTE_SERVER_PROJECT_ID,
|
||||
ssh.read(cx).proto_client(),
|
||||
SSH_PROJECT_ID,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
@@ -1266,8 +1262,8 @@ impl Project {
|
||||
buffer_store.clone(),
|
||||
worktree_store.clone(),
|
||||
languages.clone(),
|
||||
remote_proto.clone(),
|
||||
REMOTE_SERVER_PROJECT_ID,
|
||||
ssh_proto.clone(),
|
||||
SSH_PROJECT_ID,
|
||||
fs.clone(),
|
||||
cx,
|
||||
)
|
||||
@@ -1275,12 +1271,12 @@ impl Project {
|
||||
cx.subscribe(&lsp_store, Self::on_lsp_store_event).detach();
|
||||
|
||||
let breakpoint_store =
|
||||
cx.new(|_| BreakpointStore::remote(REMOTE_SERVER_PROJECT_ID, remote_proto.clone()));
|
||||
cx.new(|_| BreakpointStore::remote(SSH_PROJECT_ID, ssh_proto.clone()));
|
||||
|
||||
let dap_store = cx.new(|cx| {
|
||||
DapStore::new_remote(
|
||||
REMOTE_SERVER_PROJECT_ID,
|
||||
remote.clone(),
|
||||
DapStore::new_ssh(
|
||||
SSH_PROJECT_ID,
|
||||
ssh.clone(),
|
||||
breakpoint_store.clone(),
|
||||
worktree_store.clone(),
|
||||
cx,
|
||||
@@ -1288,16 +1284,10 @@ impl Project {
|
||||
});
|
||||
|
||||
let git_store = cx.new(|cx| {
|
||||
GitStore::remote(
|
||||
&worktree_store,
|
||||
buffer_store.clone(),
|
||||
remote_proto.clone(),
|
||||
REMOTE_SERVER_PROJECT_ID,
|
||||
cx,
|
||||
)
|
||||
GitStore::ssh(&worktree_store, buffer_store.clone(), ssh_proto.clone(), cx)
|
||||
});
|
||||
|
||||
cx.subscribe(&remote, Self::on_remote_client_event).detach();
|
||||
cx.subscribe(&ssh, Self::on_ssh_event).detach();
|
||||
|
||||
let this = Self {
|
||||
buffer_ordered_messages_tx: tx,
|
||||
@@ -1316,13 +1306,11 @@ impl Project {
|
||||
_subscriptions: vec![
|
||||
cx.on_release(Self::release),
|
||||
cx.on_app_quit(|this, cx| {
|
||||
let shutdown = this.remote_client.take().and_then(|client| {
|
||||
client.update(cx, |client, cx| {
|
||||
client.shutdown_processes(
|
||||
Some(proto::ShutdownRemoteServer {}),
|
||||
cx.background_executor().clone(),
|
||||
)
|
||||
})
|
||||
let shutdown = this.ssh_client.take().and_then(|client| {
|
||||
client.read(cx).shutdown_processes(
|
||||
Some(proto::ShutdownRemoteServer {}),
|
||||
cx.background_executor().clone(),
|
||||
)
|
||||
});
|
||||
|
||||
cx.background_executor().spawn(async move {
|
||||
@@ -1335,12 +1323,12 @@ impl Project {
|
||||
active_entry: None,
|
||||
snippets,
|
||||
languages,
|
||||
collab_client: client,
|
||||
client,
|
||||
task_store,
|
||||
user_store,
|
||||
settings_observer,
|
||||
fs,
|
||||
remote_client: Some(remote.clone()),
|
||||
ssh_client: Some(ssh.clone()),
|
||||
buffers_needing_diff: Default::default(),
|
||||
git_diff_debouncer: DebouncedDelay::new(),
|
||||
terminals: Terminals {
|
||||
@@ -1358,34 +1346,52 @@ impl Project {
|
||||
agent_location: None,
|
||||
};
|
||||
|
||||
// remote server -> local machine handlers
|
||||
remote_proto.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &cx.entity());
|
||||
remote_proto.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &this.buffer_store);
|
||||
remote_proto.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &this.worktree_store);
|
||||
remote_proto.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &this.lsp_store);
|
||||
remote_proto.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &this.dap_store);
|
||||
remote_proto.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &this.settings_observer);
|
||||
remote_proto.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &this.git_store);
|
||||
// ssh -> local machine handlers
|
||||
ssh_proto.subscribe_to_entity(SSH_PROJECT_ID, &cx.entity());
|
||||
ssh_proto.subscribe_to_entity(SSH_PROJECT_ID, &this.buffer_store);
|
||||
ssh_proto.subscribe_to_entity(SSH_PROJECT_ID, &this.worktree_store);
|
||||
ssh_proto.subscribe_to_entity(SSH_PROJECT_ID, &this.lsp_store);
|
||||
ssh_proto.subscribe_to_entity(SSH_PROJECT_ID, &this.dap_store);
|
||||
ssh_proto.subscribe_to_entity(SSH_PROJECT_ID, &this.settings_observer);
|
||||
ssh_proto.subscribe_to_entity(SSH_PROJECT_ID, &this.git_store);
|
||||
|
||||
remote_proto.add_entity_message_handler(Self::handle_create_buffer_for_peer);
|
||||
remote_proto.add_entity_message_handler(Self::handle_update_worktree);
|
||||
remote_proto.add_entity_message_handler(Self::handle_update_project);
|
||||
remote_proto.add_entity_message_handler(Self::handle_toast);
|
||||
remote_proto.add_entity_request_handler(Self::handle_language_server_prompt_request);
|
||||
remote_proto.add_entity_message_handler(Self::handle_hide_toast);
|
||||
remote_proto.add_entity_request_handler(Self::handle_update_buffer_from_remote_server);
|
||||
BufferStore::init(&remote_proto);
|
||||
LspStore::init(&remote_proto);
|
||||
SettingsObserver::init(&remote_proto);
|
||||
TaskStore::init(Some(&remote_proto));
|
||||
ToolchainStore::init(&remote_proto);
|
||||
DapStore::init(&remote_proto, cx);
|
||||
GitStore::init(&remote_proto);
|
||||
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);
|
||||
ssh_proto.add_entity_message_handler(Self::handle_toast);
|
||||
ssh_proto.add_entity_request_handler(Self::handle_language_server_prompt_request);
|
||||
ssh_proto.add_entity_message_handler(Self::handle_hide_toast);
|
||||
ssh_proto.add_entity_request_handler(Self::handle_update_buffer_from_ssh);
|
||||
BufferStore::init(&ssh_proto);
|
||||
LspStore::init(&ssh_proto);
|
||||
SettingsObserver::init(&ssh_proto);
|
||||
TaskStore::init(Some(&ssh_proto));
|
||||
ToolchainStore::init(&ssh_proto);
|
||||
DapStore::init(&ssh_proto, cx);
|
||||
GitStore::init(&ssh_proto);
|
||||
|
||||
this
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn remote(
|
||||
remote_id: u64,
|
||||
client: Arc<Client>,
|
||||
user_store: Entity<UserStore>,
|
||||
languages: Arc<LanguageRegistry>,
|
||||
fs: Arc<dyn Fs>,
|
||||
cx: AsyncApp,
|
||||
) -> Result<Entity<Self>> {
|
||||
let project =
|
||||
Self::in_room(remote_id, client, user_store, languages, fs, cx.clone()).await?;
|
||||
cx.update(|cx| {
|
||||
connection_manager::Manager::global(cx).update(cx, |manager, cx| {
|
||||
manager.maintain_project_connection(&project, cx)
|
||||
})
|
||||
})?;
|
||||
Ok(project)
|
||||
}
|
||||
|
||||
pub async fn in_room(
|
||||
remote_id: u64,
|
||||
client: Arc<Client>,
|
||||
@@ -1517,7 +1523,7 @@ impl Project {
|
||||
&worktree_store,
|
||||
buffer_store.clone(),
|
||||
client.clone().into(),
|
||||
remote_id,
|
||||
ProjectId(remote_id),
|
||||
cx,
|
||||
)
|
||||
})?;
|
||||
@@ -1568,11 +1574,11 @@ impl Project {
|
||||
task_store,
|
||||
snippets,
|
||||
fs,
|
||||
remote_client: None,
|
||||
ssh_client: None,
|
||||
settings_observer: settings_observer.clone(),
|
||||
client_subscriptions: Default::default(),
|
||||
_subscriptions: vec![cx.on_release(Self::release)],
|
||||
collab_client: client.clone(),
|
||||
client: client.clone(),
|
||||
client_state: ProjectClientState::Remote {
|
||||
sharing_has_stopped: false,
|
||||
capability: Capability::ReadWrite,
|
||||
@@ -1655,13 +1661,11 @@ impl Project {
|
||||
}
|
||||
|
||||
fn release(&mut self, cx: &mut App) {
|
||||
if let Some(client) = self.remote_client.take() {
|
||||
let shutdown = client.update(cx, |client, cx| {
|
||||
client.shutdown_processes(
|
||||
Some(proto::ShutdownRemoteServer {}),
|
||||
cx.background_executor().clone(),
|
||||
)
|
||||
});
|
||||
if let Some(client) = self.ssh_client.take() {
|
||||
let shutdown = client.read(cx).shutdown_processes(
|
||||
Some(proto::ShutdownRemoteServer {}),
|
||||
cx.background_executor().clone(),
|
||||
);
|
||||
|
||||
cx.background_spawn(async move {
|
||||
if let Some(shutdown) = shutdown {
|
||||
@@ -1677,7 +1681,7 @@ impl Project {
|
||||
let _ = self.unshare_internal(cx);
|
||||
}
|
||||
ProjectClientState::Remote { remote_id, .. } => {
|
||||
let _ = self.collab_client.send(proto::LeaveProject {
|
||||
let _ = self.client.send(proto::LeaveProject {
|
||||
project_id: *remote_id,
|
||||
});
|
||||
self.disconnected_from_host_internal(cx);
|
||||
@@ -1804,11 +1808,11 @@ impl Project {
|
||||
}
|
||||
|
||||
pub fn client(&self) -> Arc<Client> {
|
||||
self.collab_client.clone()
|
||||
self.client.clone()
|
||||
}
|
||||
|
||||
pub fn remote_client(&self) -> Option<Entity<RemoteClient>> {
|
||||
self.remote_client.clone()
|
||||
pub fn ssh_client(&self) -> Option<Entity<SshRemoteClient>> {
|
||||
self.ssh_client.clone()
|
||||
}
|
||||
|
||||
pub fn user_store(&self) -> Entity<UserStore> {
|
||||
@@ -1889,30 +1893,30 @@ impl Project {
|
||||
if self.is_local() {
|
||||
return true;
|
||||
}
|
||||
if self.is_via_remote_server() {
|
||||
if self.is_via_ssh() {
|
||||
return true;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
pub fn remote_connection_state(&self, cx: &App) -> Option<remote::ConnectionState> {
|
||||
self.remote_client
|
||||
pub fn ssh_connection_state(&self, cx: &App) -> Option<remote::ConnectionState> {
|
||||
self.ssh_client
|
||||
.as_ref()
|
||||
.map(|remote| remote.read(cx).connection_state())
|
||||
.map(|ssh| ssh.read(cx).connection_state())
|
||||
}
|
||||
|
||||
pub fn remote_connection_options(&self, cx: &App) -> Option<SshConnectionOptions> {
|
||||
self.remote_client
|
||||
pub fn ssh_connection_options(&self, cx: &App) -> Option<SshConnectionOptions> {
|
||||
self.ssh_client
|
||||
.as_ref()
|
||||
.map(|remote| remote.read(cx).connection_options())
|
||||
.map(|ssh| ssh.read(cx).connection_options())
|
||||
}
|
||||
|
||||
pub fn replica_id(&self) -> ReplicaId {
|
||||
match self.client_state {
|
||||
ProjectClientState::Remote { replica_id, .. } => replica_id,
|
||||
_ => {
|
||||
if self.remote_client.is_some() {
|
||||
if self.ssh_client.is_some() {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
@@ -2216,55 +2220,55 @@ impl Project {
|
||||
);
|
||||
|
||||
self.client_subscriptions.extend([
|
||||
self.collab_client
|
||||
self.client
|
||||
.subscribe_to_entity(project_id)?
|
||||
.set_entity(&cx.entity(), &cx.to_async()),
|
||||
self.collab_client
|
||||
self.client
|
||||
.subscribe_to_entity(project_id)?
|
||||
.set_entity(&self.worktree_store, &cx.to_async()),
|
||||
self.collab_client
|
||||
self.client
|
||||
.subscribe_to_entity(project_id)?
|
||||
.set_entity(&self.buffer_store, &cx.to_async()),
|
||||
self.collab_client
|
||||
self.client
|
||||
.subscribe_to_entity(project_id)?
|
||||
.set_entity(&self.lsp_store, &cx.to_async()),
|
||||
self.collab_client
|
||||
self.client
|
||||
.subscribe_to_entity(project_id)?
|
||||
.set_entity(&self.settings_observer, &cx.to_async()),
|
||||
self.collab_client
|
||||
self.client
|
||||
.subscribe_to_entity(project_id)?
|
||||
.set_entity(&self.dap_store, &cx.to_async()),
|
||||
self.collab_client
|
||||
self.client
|
||||
.subscribe_to_entity(project_id)?
|
||||
.set_entity(&self.breakpoint_store, &cx.to_async()),
|
||||
self.collab_client
|
||||
self.client
|
||||
.subscribe_to_entity(project_id)?
|
||||
.set_entity(&self.git_store, &cx.to_async()),
|
||||
]);
|
||||
|
||||
self.buffer_store.update(cx, |buffer_store, cx| {
|
||||
buffer_store.shared(project_id, self.collab_client.clone().into(), cx)
|
||||
buffer_store.shared(project_id, self.client.clone().into(), cx)
|
||||
});
|
||||
self.worktree_store.update(cx, |worktree_store, cx| {
|
||||
worktree_store.shared(project_id, self.collab_client.clone().into(), cx);
|
||||
worktree_store.shared(project_id, self.client.clone().into(), cx);
|
||||
});
|
||||
self.lsp_store.update(cx, |lsp_store, cx| {
|
||||
lsp_store.shared(project_id, self.collab_client.clone().into(), cx)
|
||||
lsp_store.shared(project_id, self.client.clone().into(), cx)
|
||||
});
|
||||
self.breakpoint_store.update(cx, |breakpoint_store, _| {
|
||||
breakpoint_store.shared(project_id, self.collab_client.clone().into())
|
||||
breakpoint_store.shared(project_id, self.client.clone().into())
|
||||
});
|
||||
self.dap_store.update(cx, |dap_store, cx| {
|
||||
dap_store.shared(project_id, self.collab_client.clone().into(), cx);
|
||||
dap_store.shared(project_id, self.client.clone().into(), cx);
|
||||
});
|
||||
self.task_store.update(cx, |task_store, cx| {
|
||||
task_store.shared(project_id, self.collab_client.clone().into(), cx);
|
||||
task_store.shared(project_id, self.client.clone().into(), cx);
|
||||
});
|
||||
self.settings_observer.update(cx, |settings_observer, cx| {
|
||||
settings_observer.shared(project_id, self.collab_client.clone().into(), cx)
|
||||
settings_observer.shared(project_id, self.client.clone().into(), cx)
|
||||
});
|
||||
self.git_store.update(cx, |git_store, cx| {
|
||||
git_store.shared(project_id, self.collab_client.clone().into(), cx)
|
||||
git_store.shared(project_id, self.client.clone().into(), cx)
|
||||
});
|
||||
|
||||
self.client_state = ProjectClientState::Shared {
|
||||
@@ -2289,7 +2293,7 @@ impl Project {
|
||||
});
|
||||
if let Some(remote_id) = self.remote_id() {
|
||||
self.git_store.update(cx, |git_store, cx| {
|
||||
git_store.shared(remote_id, self.collab_client.clone().into(), cx)
|
||||
git_store.shared(remote_id, self.client.clone().into(), cx)
|
||||
});
|
||||
}
|
||||
cx.emit(Event::Reshared);
|
||||
@@ -2366,7 +2370,7 @@ impl Project {
|
||||
git_store.unshared(cx);
|
||||
});
|
||||
|
||||
self.collab_client
|
||||
self.client
|
||||
.send(proto::UnshareProject {
|
||||
project_id: remote_id,
|
||||
})
|
||||
@@ -2433,17 +2437,15 @@ impl Project {
|
||||
sharing_has_stopped,
|
||||
..
|
||||
} => *sharing_has_stopped,
|
||||
ProjectClientState::Local if self.is_via_remote_server() => {
|
||||
self.remote_client_is_disconnected(cx)
|
||||
}
|
||||
ProjectClientState::Local if self.is_via_ssh() => self.ssh_is_disconnected(cx),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn remote_client_is_disconnected(&self, cx: &App) -> bool {
|
||||
self.remote_client
|
||||
fn ssh_is_disconnected(&self, cx: &App) -> bool {
|
||||
self.ssh_client
|
||||
.as_ref()
|
||||
.map(|remote| remote.read(cx).is_disconnected())
|
||||
.map(|ssh| ssh.read(cx).is_disconnected())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
@@ -2461,16 +2463,16 @@ impl Project {
|
||||
pub fn is_local(&self) -> bool {
|
||||
match &self.client_state {
|
||||
ProjectClientState::Local | ProjectClientState::Shared { .. } => {
|
||||
self.remote_client.is_none()
|
||||
self.ssh_client.is_none()
|
||||
}
|
||||
ProjectClientState::Remote { .. } => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_via_remote_server(&self) -> bool {
|
||||
pub fn is_via_ssh(&self) -> bool {
|
||||
match &self.client_state {
|
||||
ProjectClientState::Local | ProjectClientState::Shared { .. } => {
|
||||
self.remote_client.is_some()
|
||||
self.ssh_client.is_some()
|
||||
}
|
||||
ProjectClientState::Remote { .. } => false,
|
||||
}
|
||||
@@ -2494,7 +2496,7 @@ impl Project {
|
||||
language: Option<Arc<Language>>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Entity<Buffer> {
|
||||
if self.is_via_collab() || self.is_via_remote_server() {
|
||||
if self.is_via_collab() || self.is_via_ssh() {
|
||||
panic!("called create_local_buffer on a remote project")
|
||||
}
|
||||
self.buffer_store.update(cx, |buffer_store, cx| {
|
||||
@@ -2618,10 +2620,10 @@ impl Project {
|
||||
) -> Task<Result<Entity<Buffer>>> {
|
||||
if let Some(buffer) = self.buffer_for_id(id, cx) {
|
||||
Task::ready(Ok(buffer))
|
||||
} else if self.is_local() || self.is_via_remote_server() {
|
||||
} else if self.is_local() || self.is_via_ssh() {
|
||||
Task::ready(Err(anyhow!("buffer {id} does not exist")))
|
||||
} else if let Some(project_id) = self.remote_id() {
|
||||
let request = self.collab_client.request(proto::OpenBufferById {
|
||||
let request = self.client.request(proto::OpenBufferById {
|
||||
project_id,
|
||||
id: id.into(),
|
||||
});
|
||||
@@ -2739,7 +2741,7 @@ impl Project {
|
||||
for (buffer_id, operations) in operations_by_buffer_id.drain() {
|
||||
let request = this.read_with(cx, |this, _| {
|
||||
let project_id = this.remote_id()?;
|
||||
Some(this.collab_client.request(proto::UpdateBuffer {
|
||||
Some(this.client.request(proto::UpdateBuffer {
|
||||
buffer_id: buffer_id.into(),
|
||||
project_id,
|
||||
operations,
|
||||
@@ -2806,7 +2808,7 @@ impl Project {
|
||||
project.read_with(cx, |project, _| {
|
||||
if let Some(project_id) = project.remote_id() {
|
||||
project
|
||||
.collab_client
|
||||
.client
|
||||
.send(proto::UpdateLanguageServer {
|
||||
project_id,
|
||||
server_name: name.map(|name| String::from(name.0)),
|
||||
@@ -2844,8 +2846,8 @@ impl Project {
|
||||
self.register_buffer(buffer, cx).log_err();
|
||||
}
|
||||
BufferStoreEvent::BufferDropped(buffer_id) => {
|
||||
if let Some(ref remote_client) = self.remote_client {
|
||||
remote_client
|
||||
if let Some(ref ssh_client) = self.ssh_client {
|
||||
ssh_client
|
||||
.read(cx)
|
||||
.proto_client()
|
||||
.send(proto::CloseBuffer {
|
||||
@@ -2993,14 +2995,16 @@ impl Project {
|
||||
}
|
||||
}
|
||||
|
||||
fn on_remote_client_event(
|
||||
fn on_ssh_event(
|
||||
&mut self,
|
||||
_: Entity<RemoteClient>,
|
||||
event: &remote::RemoteClientEvent,
|
||||
_: Entity<SshRemoteClient>,
|
||||
event: &remote::SshRemoteEvent,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
match event {
|
||||
remote::RemoteClientEvent::Disconnected => {
|
||||
remote::SshRemoteEvent::Disconnected => {
|
||||
// if self.is_via_ssh() {
|
||||
// self.collaborators.clear();
|
||||
self.worktree_store.update(cx, |store, cx| {
|
||||
store.disconnected_from_host(cx);
|
||||
});
|
||||
@@ -3106,9 +3110,8 @@ impl Project {
|
||||
}
|
||||
|
||||
fn on_worktree_released(&mut self, id_to_remove: WorktreeId, cx: &mut Context<Self>) {
|
||||
if let Some(remote) = &self.remote_client {
|
||||
remote
|
||||
.read(cx)
|
||||
if let Some(ssh) = &self.ssh_client {
|
||||
ssh.read(cx)
|
||||
.proto_client()
|
||||
.send(proto::RemoveWorktree {
|
||||
worktree_id: id_to_remove.to_proto(),
|
||||
@@ -3141,9 +3144,8 @@ impl Project {
|
||||
} => {
|
||||
let operation = language::proto::serialize_operation(operation);
|
||||
|
||||
if let Some(remote) = &self.remote_client {
|
||||
remote
|
||||
.read(cx)
|
||||
if let Some(ssh) = &self.ssh_client {
|
||||
ssh.read(cx)
|
||||
.proto_client()
|
||||
.send(proto::UpdateBuffer {
|
||||
project_id: 0,
|
||||
@@ -3550,16 +3552,16 @@ impl Project {
|
||||
|
||||
pub fn open_server_settings(&mut self, cx: &mut Context<Self>) -> Task<Result<Entity<Buffer>>> {
|
||||
let guard = self.retain_remotely_created_models(cx);
|
||||
let Some(remote) = self.remote_client.as_ref() else {
|
||||
let Some(ssh_client) = self.ssh_client.as_ref() else {
|
||||
return Task::ready(Err(anyhow!("not an ssh project")));
|
||||
};
|
||||
|
||||
let proto_client = remote.read(cx).proto_client();
|
||||
let proto_client = ssh_client.read(cx).proto_client();
|
||||
|
||||
cx.spawn(async move |project, cx| {
|
||||
let buffer = proto_client
|
||||
.request(proto::OpenServerSettings {
|
||||
project_id: REMOTE_SERVER_PROJECT_ID,
|
||||
project_id: SSH_PROJECT_ID,
|
||||
})
|
||||
.await?;
|
||||
|
||||
@@ -3946,11 +3948,10 @@ impl Project {
|
||||
) -> Receiver<Entity<Buffer>> {
|
||||
let (tx, rx) = smol::channel::unbounded();
|
||||
|
||||
let (client, remote_id): (AnyProtoClient, _) = if let Some(ssh_client) = &self.remote_client
|
||||
{
|
||||
let (client, remote_id): (AnyProtoClient, _) = if let Some(ssh_client) = &self.ssh_client {
|
||||
(ssh_client.read(cx).proto_client(), 0)
|
||||
} else if let Some(remote_id) = self.remote_id() {
|
||||
(self.collab_client.clone().into(), remote_id)
|
||||
(self.client.clone().into(), remote_id)
|
||||
} else {
|
||||
return rx;
|
||||
};
|
||||
@@ -4094,14 +4095,14 @@ impl Project {
|
||||
is_dir: metadata.is_dir,
|
||||
})
|
||||
})
|
||||
} else if let Some(ssh_client) = self.remote_client.as_ref() {
|
||||
} else if let Some(ssh_client) = self.ssh_client.as_ref() {
|
||||
let path_style = ssh_client.read(cx).path_style();
|
||||
let request_path = RemotePathBuf::from_str(path, path_style);
|
||||
let request = ssh_client
|
||||
.read(cx)
|
||||
.proto_client()
|
||||
.request(proto::GetPathMetadata {
|
||||
project_id: REMOTE_SERVER_PROJECT_ID,
|
||||
project_id: SSH_PROJECT_ID,
|
||||
path: request_path.to_proto(),
|
||||
});
|
||||
cx.background_spawn(async move {
|
||||
@@ -4201,10 +4202,10 @@ impl Project {
|
||||
) -> Task<Result<Vec<DirectoryItem>>> {
|
||||
if self.is_local() {
|
||||
DirectoryLister::Local(cx.entity(), self.fs.clone()).list_directory(query, cx)
|
||||
} else if let Some(session) = self.remote_client.as_ref() {
|
||||
} else if let Some(session) = self.ssh_client.as_ref() {
|
||||
let path_buf = PathBuf::from(query);
|
||||
let request = proto::ListRemoteDirectory {
|
||||
dev_server_id: REMOTE_SERVER_PROJECT_ID,
|
||||
dev_server_id: SSH_PROJECT_ID,
|
||||
path: path_buf.to_proto(),
|
||||
config: Some(proto::ListRemoteDirectoryConfig { is_dir: true }),
|
||||
};
|
||||
@@ -4419,7 +4420,7 @@ impl Project {
|
||||
mut cx: AsyncApp,
|
||||
) -> Result<()> {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
if this.is_local() || this.is_via_remote_server() {
|
||||
if this.is_local() || this.is_via_ssh() {
|
||||
this.unshare(cx)?;
|
||||
} else {
|
||||
this.disconnected_from_host(cx);
|
||||
@@ -4628,7 +4629,7 @@ impl Project {
|
||||
})?
|
||||
}
|
||||
|
||||
async fn handle_update_buffer_from_remote_server(
|
||||
async fn handle_update_buffer_from_ssh(
|
||||
this: Entity<Self>,
|
||||
envelope: TypedEnvelope<proto::UpdateBuffer>,
|
||||
cx: AsyncApp,
|
||||
@@ -4637,7 +4638,7 @@ impl Project {
|
||||
if let Some(remote_id) = this.remote_id() {
|
||||
let mut payload = envelope.payload.clone();
|
||||
payload.project_id = remote_id;
|
||||
cx.background_spawn(this.collab_client.request(payload))
|
||||
cx.background_spawn(this.client.request(payload))
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
this.buffer_store.clone()
|
||||
@@ -4651,9 +4652,9 @@ impl Project {
|
||||
cx: AsyncApp,
|
||||
) -> Result<proto::Ack> {
|
||||
let buffer_store = this.read_with(&cx, |this, cx| {
|
||||
if let Some(ssh) = &this.remote_client {
|
||||
if let Some(ssh) = &this.ssh_client {
|
||||
let mut payload = envelope.payload.clone();
|
||||
payload.project_id = REMOTE_SERVER_PROJECT_ID;
|
||||
payload.project_id = SSH_PROJECT_ID;
|
||||
cx.background_spawn(ssh.read(cx).proto_client().request(payload))
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
@@ -4703,7 +4704,7 @@ impl Project {
|
||||
mut cx: AsyncApp,
|
||||
) -> Result<proto::SynchronizeBuffersResponse> {
|
||||
let response = this.update(&mut cx, |this, cx| {
|
||||
let client = this.collab_client.clone();
|
||||
let client = this.client.clone();
|
||||
this.buffer_store.update(cx, |this, cx| {
|
||||
this.handle_synchronize_buffers(envelope, cx, client)
|
||||
})
|
||||
@@ -4840,7 +4841,7 @@ impl Project {
|
||||
}
|
||||
};
|
||||
|
||||
let client = self.collab_client.clone();
|
||||
let client = self.client.clone();
|
||||
cx.spawn(async move |this, cx| {
|
||||
let (buffers, incomplete_buffer_ids) = this.update(cx, |this, cx| {
|
||||
this.buffer_store.read(cx).buffer_version_info(cx)
|
||||
|
||||
@@ -2,11 +2,13 @@ use crate::{Project, ProjectPath};
|
||||
use anyhow::{Context as _, Result};
|
||||
use collections::HashMap;
|
||||
use gpui::{App, AppContext as _, Context, Entity, Task, WeakEntity};
|
||||
use itertools::Itertools;
|
||||
use language::LanguageName;
|
||||
use remote::RemoteClient;
|
||||
use remote::{SshInfo, ssh_session::SshArgs};
|
||||
use settings::{Settings, SettingsLocation};
|
||||
use smol::channel::bounded;
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
env::{self},
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
@@ -16,7 +18,10 @@ use terminal::{
|
||||
TaskState, TaskStatus, Terminal, TerminalBuilder,
|
||||
terminal_settings::{self, ActivateScript, TerminalSettings, VenvSettings},
|
||||
};
|
||||
use util::{ResultExt, paths::RemotePathBuf};
|
||||
use util::{
|
||||
ResultExt,
|
||||
paths::{PathStyle, RemotePathBuf},
|
||||
};
|
||||
|
||||
/// The directory inside a Python virtual environment that contains executables
|
||||
const PYTHON_VENV_BIN_DIR: &str = if cfg!(target_os = "windows") {
|
||||
@@ -39,6 +44,29 @@ pub enum TerminalKind {
|
||||
Task(SpawnInTerminal),
|
||||
}
|
||||
|
||||
/// SshCommand describes how to connect to a remote server
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct SshCommand {
|
||||
pub arguments: Vec<String>,
|
||||
}
|
||||
|
||||
impl SshCommand {
|
||||
pub fn add_port_forwarding(&mut self, local_port: u16, host: String, remote_port: u16) {
|
||||
self.arguments.push("-L".to_string());
|
||||
self.arguments
|
||||
.push(format!("{}:{}:{}", local_port, host, remote_port));
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SshDetails {
|
||||
pub host: String,
|
||||
pub ssh_command: SshCommand,
|
||||
pub envs: Option<HashMap<String, String>>,
|
||||
pub path_style: PathStyle,
|
||||
pub shell: String,
|
||||
}
|
||||
|
||||
impl Project {
|
||||
pub fn active_project_directory(&self, cx: &App) -> Option<Arc<Path>> {
|
||||
self.active_entry()
|
||||
@@ -58,6 +86,28 @@ impl Project {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ssh_details(&self, cx: &App) -> Option<SshDetails> {
|
||||
if let Some(ssh_client) = &self.ssh_client {
|
||||
let ssh_client = ssh_client.read(cx);
|
||||
if let Some(SshInfo {
|
||||
args: SshArgs { arguments, envs },
|
||||
path_style,
|
||||
shell,
|
||||
}) = ssh_client.ssh_info()
|
||||
{
|
||||
return Some(SshDetails {
|
||||
host: ssh_client.connection_options().host,
|
||||
ssh_command: SshCommand { arguments },
|
||||
envs,
|
||||
path_style,
|
||||
shell,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub fn create_terminal(
|
||||
&mut self,
|
||||
kind: TerminalKind,
|
||||
@@ -118,14 +168,14 @@ impl Project {
|
||||
TerminalSettings::get(settings_location, cx)
|
||||
}
|
||||
|
||||
pub fn exec_in_shell(&self, command: String, cx: &App) -> Result<std::process::Command> {
|
||||
pub fn exec_in_shell(&self, command: String, cx: &App) -> std::process::Command {
|
||||
let path = self.first_project_directory(cx);
|
||||
let remote_client = self.remote_client.as_ref();
|
||||
let ssh_details = self.ssh_details(cx);
|
||||
let settings = self.terminal_settings(&path, cx).clone();
|
||||
let remote_shell = remote_client
|
||||
.as_ref()
|
||||
.and_then(|remote_client| remote_client.read(cx).shell());
|
||||
let builder = ShellBuilder::new(remote_shell.as_deref(), &settings.shell).non_interactive();
|
||||
|
||||
let builder =
|
||||
ShellBuilder::new(ssh_details.as_ref().map(|ssh| &*ssh.shell), &settings.shell)
|
||||
.non_interactive();
|
||||
let (command, args) = builder.build(Some(command), &Vec::new());
|
||||
|
||||
let mut env = self
|
||||
@@ -135,16 +185,29 @@ impl Project {
|
||||
.unwrap_or_default();
|
||||
env.extend(settings.env);
|
||||
|
||||
match remote_client {
|
||||
Some(remote_client) => {
|
||||
let command_template =
|
||||
remote_client
|
||||
.read(cx)
|
||||
.build_command(Some(command), &args, &env, None, None)?;
|
||||
let mut command = std::process::Command::new(command_template.program);
|
||||
command.args(command_template.args);
|
||||
command.envs(command_template.env);
|
||||
Ok(command)
|
||||
match self.ssh_details(cx) {
|
||||
Some(SshDetails {
|
||||
ssh_command,
|
||||
envs,
|
||||
path_style,
|
||||
shell,
|
||||
..
|
||||
}) => {
|
||||
let (command, args) = wrap_for_ssh(
|
||||
&shell,
|
||||
&ssh_command,
|
||||
Some((&command, &args)),
|
||||
path.as_deref(),
|
||||
env,
|
||||
None,
|
||||
path_style,
|
||||
);
|
||||
let mut command = std::process::Command::new(command);
|
||||
command.args(args);
|
||||
if let Some(envs) = envs {
|
||||
command.envs(envs);
|
||||
}
|
||||
command
|
||||
}
|
||||
None => {
|
||||
let mut command = std::process::Command::new(command);
|
||||
@@ -153,7 +216,7 @@ impl Project {
|
||||
if let Some(path) = path {
|
||||
command.current_dir(path);
|
||||
}
|
||||
Ok(command)
|
||||
command
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -164,13 +227,13 @@ impl Project {
|
||||
python_venv_directory: Option<PathBuf>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Result<Entity<Terminal>> {
|
||||
let is_via_remote = self.remote_client.is_some();
|
||||
|
||||
let this = &mut *self;
|
||||
let ssh_details = this.ssh_details(cx);
|
||||
let path: Option<Arc<Path>> = match &kind {
|
||||
TerminalKind::Shell(path) => path.as_ref().map(|path| Arc::from(path.as_ref())),
|
||||
TerminalKind::Task(spawn_task) => {
|
||||
if let Some(cwd) = &spawn_task.cwd {
|
||||
if is_via_remote {
|
||||
if ssh_details.is_some() {
|
||||
Some(Arc::from(cwd.as_ref()))
|
||||
} else {
|
||||
let cwd = cwd.to_string_lossy();
|
||||
@@ -178,14 +241,16 @@ impl Project {
|
||||
Some(Arc::from(Path::new(tilde_substituted.as_ref())))
|
||||
}
|
||||
} else {
|
||||
self.active_project_directory(cx)
|
||||
this.active_project_directory(cx)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let is_ssh_terminal = ssh_details.is_some();
|
||||
|
||||
let mut settings_location = None;
|
||||
if let Some(path) = path.as_ref()
|
||||
&& let Some((worktree, _)) = self.find_worktree(path, cx)
|
||||
&& let Some((worktree, _)) = this.find_worktree(path, cx)
|
||||
{
|
||||
settings_location = Some(SettingsLocation {
|
||||
worktree_id: worktree.read(cx).id(),
|
||||
@@ -197,7 +262,7 @@ impl Project {
|
||||
let (completion_tx, completion_rx) = bounded(1);
|
||||
|
||||
// Start with the environment that we might have inherited from the Zed CLI.
|
||||
let mut env = self
|
||||
let mut env = this
|
||||
.environment
|
||||
.read(cx)
|
||||
.get_cli_environment()
|
||||
@@ -206,17 +271,14 @@ impl Project {
|
||||
// precedence.
|
||||
env.extend(settings.env);
|
||||
|
||||
let local_path = if is_via_remote { None } else { path.clone() };
|
||||
let local_path = if is_ssh_terminal { None } else { path.clone() };
|
||||
|
||||
let mut python_venv_activate_command = Task::ready(None);
|
||||
|
||||
let remote_client = self.remote_client.clone();
|
||||
let spawn_task;
|
||||
let shell;
|
||||
match kind {
|
||||
let (spawn_task, shell) = match kind {
|
||||
TerminalKind::Shell(_) => {
|
||||
if let Some(python_venv_directory) = &python_venv_directory {
|
||||
python_venv_activate_command = self.python_activate_command(
|
||||
python_venv_activate_command = this.python_activate_command(
|
||||
python_venv_directory,
|
||||
&settings.detect_venv,
|
||||
&settings.shell,
|
||||
@@ -224,16 +286,63 @@ impl Project {
|
||||
);
|
||||
}
|
||||
|
||||
spawn_task = None;
|
||||
shell = match remote_client {
|
||||
Some(remote_client) => {
|
||||
create_remote_shell(None, &mut env, path, remote_client, cx)?
|
||||
match ssh_details {
|
||||
Some(SshDetails {
|
||||
host,
|
||||
ssh_command,
|
||||
envs,
|
||||
path_style,
|
||||
shell,
|
||||
}) => {
|
||||
log::debug!("Connecting to a remote server: {ssh_command:?}");
|
||||
|
||||
// Alacritty sets its terminfo to `alacritty`, this requiring hosts to have it installed
|
||||
// to properly display colors.
|
||||
// We do not have the luxury of assuming the host has it installed,
|
||||
// so we set it to a default that does not break the highlighting via ssh.
|
||||
env.entry("TERM".to_string())
|
||||
.or_insert_with(|| "xterm-256color".to_string());
|
||||
|
||||
let (program, args) = wrap_for_ssh(
|
||||
&shell,
|
||||
&ssh_command,
|
||||
None,
|
||||
path.as_deref(),
|
||||
env,
|
||||
None,
|
||||
path_style,
|
||||
);
|
||||
env = HashMap::default();
|
||||
if let Some(envs) = envs {
|
||||
env.extend(envs);
|
||||
}
|
||||
(
|
||||
Option::<TaskState>::None,
|
||||
Shell::WithArguments {
|
||||
program,
|
||||
args,
|
||||
title_override: Some(format!("{} — Terminal", host).into()),
|
||||
},
|
||||
)
|
||||
}
|
||||
None => settings.shell,
|
||||
};
|
||||
None => (None, settings.shell),
|
||||
}
|
||||
}
|
||||
TerminalKind::Task(task) => {
|
||||
env.extend(task.env);
|
||||
TerminalKind::Task(spawn_task) => {
|
||||
let task_state = Some(TaskState {
|
||||
id: spawn_task.id,
|
||||
full_label: spawn_task.full_label,
|
||||
label: spawn_task.label,
|
||||
command_label: spawn_task.command_label,
|
||||
hide: spawn_task.hide,
|
||||
status: TaskStatus::Running,
|
||||
show_summary: spawn_task.show_summary,
|
||||
show_command: spawn_task.show_command,
|
||||
show_rerun: spawn_task.show_rerun,
|
||||
completion_rx,
|
||||
});
|
||||
|
||||
env.extend(spawn_task.env);
|
||||
|
||||
if let Some(venv_path) = &python_venv_directory {
|
||||
env.insert(
|
||||
@@ -242,38 +351,41 @@ impl Project {
|
||||
);
|
||||
}
|
||||
|
||||
spawn_task = Some(TaskState {
|
||||
id: task.id,
|
||||
full_label: task.full_label,
|
||||
label: task.label,
|
||||
command_label: task.command_label,
|
||||
hide: task.hide,
|
||||
status: TaskStatus::Running,
|
||||
show_summary: task.show_summary,
|
||||
show_command: task.show_command,
|
||||
show_rerun: task.show_rerun,
|
||||
completion_rx,
|
||||
});
|
||||
shell = match remote_client {
|
||||
Some(remote_client) => {
|
||||
let path_style = remote_client.read(cx).path_style();
|
||||
if let Some(venv_directory) = &python_venv_directory
|
||||
&& let Ok(str) =
|
||||
shlex::try_quote(venv_directory.to_string_lossy().as_ref())
|
||||
{
|
||||
let path =
|
||||
RemotePathBuf::new(PathBuf::from(str.to_string()), path_style)
|
||||
.to_string();
|
||||
env.insert("PATH".into(), format!("{}:$PATH ", path));
|
||||
match ssh_details {
|
||||
Some(SshDetails {
|
||||
host,
|
||||
ssh_command,
|
||||
envs,
|
||||
path_style,
|
||||
shell,
|
||||
}) => {
|
||||
log::debug!("Connecting to a remote server: {ssh_command:?}");
|
||||
env.entry("TERM".to_string())
|
||||
.or_insert_with(|| "xterm-256color".to_string());
|
||||
let (program, args) = wrap_for_ssh(
|
||||
&shell,
|
||||
&ssh_command,
|
||||
spawn_task
|
||||
.command
|
||||
.as_ref()
|
||||
.map(|command| (command, &spawn_task.args)),
|
||||
path.as_deref(),
|
||||
env,
|
||||
python_venv_directory.as_deref(),
|
||||
path_style,
|
||||
);
|
||||
env = HashMap::default();
|
||||
if let Some(envs) = envs {
|
||||
env.extend(envs);
|
||||
}
|
||||
|
||||
create_remote_shell(
|
||||
task.command.as_ref().map(|command| (command, &task.args)),
|
||||
&mut env,
|
||||
path,
|
||||
remote_client,
|
||||
cx,
|
||||
)?
|
||||
(
|
||||
task_state,
|
||||
Shell::WithArguments {
|
||||
program,
|
||||
args,
|
||||
title_override: Some(format!("{} — Terminal", host).into()),
|
||||
},
|
||||
)
|
||||
}
|
||||
None => {
|
||||
if let Some(venv_path) = &python_venv_directory {
|
||||
@@ -281,17 +393,18 @@ impl Project {
|
||||
.log_err();
|
||||
}
|
||||
|
||||
if let Some(program) = task.command {
|
||||
let shell = if let Some(program) = spawn_task.command {
|
||||
Shell::WithArguments {
|
||||
program,
|
||||
args: task.args,
|
||||
args: spawn_task.args,
|
||||
title_override: None,
|
||||
}
|
||||
} else {
|
||||
Shell::System
|
||||
}
|
||||
};
|
||||
(task_state, shell)
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
TerminalBuilder::new(
|
||||
@@ -303,7 +416,7 @@ impl Project {
|
||||
settings.cursor_shape.unwrap_or_default(),
|
||||
settings.alternate_scroll,
|
||||
settings.max_scroll_history_lines,
|
||||
is_via_remote,
|
||||
is_ssh_terminal,
|
||||
cx.entity_id().as_u64(),
|
||||
completion_tx,
|
||||
cx,
|
||||
@@ -311,7 +424,7 @@ impl Project {
|
||||
.map(|builder| {
|
||||
let terminal_handle = cx.new(|cx| builder.subscribe(cx));
|
||||
|
||||
self.terminals
|
||||
this.terminals
|
||||
.local_handles
|
||||
.push(terminal_handle.downgrade());
|
||||
|
||||
@@ -329,7 +442,7 @@ impl Project {
|
||||
})
|
||||
.detach();
|
||||
|
||||
self.activate_python_virtual_environment(
|
||||
this.activate_python_virtual_environment(
|
||||
python_venv_activate_command,
|
||||
&terminal_handle,
|
||||
cx,
|
||||
@@ -539,42 +652,62 @@ impl Project {
|
||||
}
|
||||
}
|
||||
|
||||
fn create_remote_shell(
|
||||
spawn_command: Option<(&String, &Vec<String>)>,
|
||||
env: &mut HashMap<String, String>,
|
||||
working_directory: Option<Arc<Path>>,
|
||||
remote_client: Entity<RemoteClient>,
|
||||
cx: &mut App,
|
||||
) -> Result<Shell> {
|
||||
// Alacritty sets its terminfo to `alacritty`, this requiring hosts to have it installed
|
||||
// to properly display colors.
|
||||
// We do not have the luxury of assuming the host has it installed,
|
||||
// so we set it to a default that does not break the highlighting via ssh.
|
||||
env.entry("TERM".to_string())
|
||||
.or_insert_with(|| "xterm-256color".to_string());
|
||||
|
||||
let (program, args) = match spawn_command {
|
||||
Some((program, args)) => (Some(program.clone()), args),
|
||||
None => (None, &Vec::new()),
|
||||
pub fn wrap_for_ssh(
|
||||
shell: &str,
|
||||
ssh_command: &SshCommand,
|
||||
command: Option<(&String, &Vec<String>)>,
|
||||
path: Option<&Path>,
|
||||
env: HashMap<String, String>,
|
||||
venv_directory: Option<&Path>,
|
||||
path_style: PathStyle,
|
||||
) -> (String, Vec<String>) {
|
||||
let to_run = if let Some((command, args)) = command {
|
||||
let command: Option<Cow<str>> = shlex::try_quote(command).ok();
|
||||
let args = args.iter().filter_map(|arg| shlex::try_quote(arg).ok());
|
||||
command.into_iter().chain(args).join(" ")
|
||||
} else {
|
||||
format!("exec {shell} -l")
|
||||
};
|
||||
|
||||
let command = remote_client.read(cx).build_command(
|
||||
program,
|
||||
args.as_slice(),
|
||||
env,
|
||||
working_directory.map(|path| path.display().to_string()),
|
||||
None,
|
||||
)?;
|
||||
*env = command.env;
|
||||
let mut env_changes = String::new();
|
||||
for (k, v) in env.iter() {
|
||||
if let Some((k, v)) = shlex::try_quote(k).ok().zip(shlex::try_quote(v).ok()) {
|
||||
env_changes.push_str(&format!("{}={} ", k, v));
|
||||
}
|
||||
}
|
||||
if let Some(venv_directory) = venv_directory
|
||||
&& let Ok(str) = shlex::try_quote(venv_directory.to_string_lossy().as_ref())
|
||||
{
|
||||
let path = RemotePathBuf::new(PathBuf::from(str.to_string()), path_style).to_string();
|
||||
env_changes.push_str(&format!("PATH={}:$PATH ", path));
|
||||
}
|
||||
|
||||
log::debug!("Connecting to a remote server: {:?}", command.program);
|
||||
let host = remote_client.read(cx).connection_options().host;
|
||||
let commands = if let Some(path) = path {
|
||||
let path = RemotePathBuf::new(path.to_path_buf(), path_style).to_string();
|
||||
// shlex will wrap the command in single quotes (''), disabling ~ expansion,
|
||||
// replace ith with something that works
|
||||
let tilde_prefix = "~/";
|
||||
if path.starts_with(tilde_prefix) {
|
||||
let trimmed_path = path
|
||||
.trim_start_matches("/")
|
||||
.trim_start_matches("~")
|
||||
.trim_start_matches("/");
|
||||
|
||||
Ok(Shell::WithArguments {
|
||||
program: command.program,
|
||||
args: command.args,
|
||||
title_override: Some(format!("{} — Terminal", host).into()),
|
||||
})
|
||||
format!("cd \"$HOME/{trimmed_path}\"; {env_changes} {to_run}")
|
||||
} else {
|
||||
format!("cd \"{path}\"; {env_changes} {to_run}")
|
||||
}
|
||||
} else {
|
||||
format!("cd; {env_changes} {to_run}")
|
||||
};
|
||||
let shell_invocation = format!("{shell} -c {}", shlex::try_quote(&commands).unwrap());
|
||||
|
||||
let program = "ssh".to_string();
|
||||
let mut args = ssh_command.arguments.clone();
|
||||
|
||||
args.push("-t".to_string());
|
||||
args.push(shell_invocation);
|
||||
(program, args)
|
||||
}
|
||||
|
||||
fn add_environment_path(env: &mut HashMap<String, String>, new_path: &Path) -> Result<()> {
|
||||
|
||||
@@ -18,7 +18,7 @@ use gpui::{
|
||||
use postage::oneshot;
|
||||
use rpc::{
|
||||
AnyProtoClient, ErrorExt, TypedEnvelope,
|
||||
proto::{self, FromProto, REMOTE_SERVER_PROJECT_ID, ToProto},
|
||||
proto::{self, FromProto, SSH_PROJECT_ID, ToProto},
|
||||
};
|
||||
use smol::{
|
||||
channel::{Receiver, Sender},
|
||||
@@ -278,7 +278,7 @@ impl WorktreeStore {
|
||||
let path = RemotePathBuf::new(abs_path.into(), path_style);
|
||||
let response = client
|
||||
.request(proto::AddWorktree {
|
||||
project_id: REMOTE_SERVER_PROJECT_ID,
|
||||
project_id: SSH_PROJECT_ID,
|
||||
path: path.to_proto(),
|
||||
visible,
|
||||
})
|
||||
@@ -298,7 +298,7 @@ impl WorktreeStore {
|
||||
|
||||
let worktree = cx.update(|cx| {
|
||||
Worktree::remote(
|
||||
REMOTE_SERVER_PROJECT_ID,
|
||||
SSH_PROJECT_ID,
|
||||
0,
|
||||
proto::WorktreeMetadata {
|
||||
id: response.worktree_id,
|
||||
|
||||
@@ -653,7 +653,7 @@ impl ProjectPanel {
|
||||
let file_path = entry.path.clone();
|
||||
let worktree_id = worktree.read(cx).id();
|
||||
let entry_id = entry.id;
|
||||
let is_via_ssh = project.read(cx).is_via_remote_server();
|
||||
let is_via_ssh = project.read(cx).is_via_ssh();
|
||||
|
||||
workspace
|
||||
.open_path_preview(
|
||||
@@ -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(
|
||||
@@ -5301,7 +5295,7 @@ impl Render for ProjectPanel {
|
||||
.on_action(cx.listener(Self::open_system))
|
||||
.on_action(cx.listener(Self::open_in_terminal))
|
||||
})
|
||||
.when(project.is_via_remote_server(), |el| {
|
||||
.when(project.is_via_ssh(), |el| {
|
||||
el.on_action(cx.listener(Self::open_in_terminal))
|
||||
})
|
||||
.on_mouse_down(
|
||||
@@ -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 {
|
||||
|
||||
@@ -16,8 +16,8 @@ pub use typed_envelope::*;
|
||||
|
||||
include!(concat!(env!("OUT_DIR"), "/zed.messages.rs"));
|
||||
|
||||
pub const REMOTE_SERVER_PEER_ID: PeerId = PeerId { owner_id: 0, id: 0 };
|
||||
pub const REMOTE_SERVER_PROJECT_ID: u64 = 0;
|
||||
pub const SSH_PEER_ID: PeerId = PeerId { owner_id: 0, id: 0 };
|
||||
pub const SSH_PROJECT_ID: u64 = 0;
|
||||
|
||||
messages!(
|
||||
(Ack, Foreground),
|
||||
|
||||
@@ -64,8 +64,8 @@ impl DisconnectedOverlay {
|
||||
}
|
||||
let handle = cx.entity().downgrade();
|
||||
|
||||
let remote_connection_options = project.read(cx).remote_connection_options(cx);
|
||||
let host = if let Some(ssh_connection_options) = remote_connection_options {
|
||||
let ssh_connection_options = project.read(cx).ssh_connection_options(cx);
|
||||
let host = if let Some(ssh_connection_options) = ssh_connection_options {
|
||||
Host::SshRemoteProject(ssh_connection_options)
|
||||
} else {
|
||||
Host::RemoteProject
|
||||
|
||||
@@ -28,8 +28,8 @@ use paths::user_ssh_config_file;
|
||||
use picker::Picker;
|
||||
use project::Fs;
|
||||
use project::Project;
|
||||
use remote::remote_client::ConnectionIdentifier;
|
||||
use remote::{RemoteClient, SshConnectionOptions};
|
||||
use remote::ssh_session::ConnectionIdentifier;
|
||||
use remote::{SshConnectionOptions, SshRemoteClient};
|
||||
use settings::Settings;
|
||||
use settings::SettingsStore;
|
||||
use settings::update_settings_file;
|
||||
@@ -69,7 +69,7 @@ pub struct RemoteServerProjects {
|
||||
mode: Mode,
|
||||
focus_handle: FocusHandle,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
retained_connections: Vec<Entity<RemoteClient>>,
|
||||
retained_connections: Vec<Entity<SshRemoteClient>>,
|
||||
ssh_config_updates: Task<()>,
|
||||
ssh_config_servers: BTreeSet<SharedString>,
|
||||
create_new_window: bool,
|
||||
@@ -597,7 +597,7 @@ impl RemoteServerProjects {
|
||||
let (path_style, project) = cx.update(|_, cx| {
|
||||
(
|
||||
session.read(cx).path_style(),
|
||||
project::Project::remote(
|
||||
project::Project::ssh(
|
||||
session,
|
||||
app_state.client.clone(),
|
||||
app_state.node_runtime.clone(),
|
||||
|
||||
@@ -15,9 +15,8 @@ use gpui::{
|
||||
use language::CursorShape;
|
||||
use markdown::{Markdown, MarkdownElement, MarkdownStyle};
|
||||
use release_channel::ReleaseChannel;
|
||||
use remote::{
|
||||
ConnectionIdentifier, RemoteClient, RemotePlatform, SshConnectionOptions, SshPortForwardOption,
|
||||
};
|
||||
use remote::ssh_session::{ConnectionIdentifier, SshPortForwardOption};
|
||||
use remote::{SshConnectionOptions, SshPlatform, SshRemoteClient};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsSources};
|
||||
@@ -452,7 +451,7 @@ pub struct SshClientDelegate {
|
||||
known_password: Option<String>,
|
||||
}
|
||||
|
||||
impl remote::RemoteClientDelegate for SshClientDelegate {
|
||||
impl remote::SshClientDelegate for SshClientDelegate {
|
||||
fn ask_password(&self, prompt: String, tx: oneshot::Sender<String>, cx: &mut AsyncApp) {
|
||||
let mut known_password = self.known_password.clone();
|
||||
if let Some(password) = known_password.take() {
|
||||
@@ -474,7 +473,7 @@ impl remote::RemoteClientDelegate for SshClientDelegate {
|
||||
|
||||
fn download_server_binary_locally(
|
||||
&self,
|
||||
platform: RemotePlatform,
|
||||
platform: SshPlatform,
|
||||
release_channel: ReleaseChannel,
|
||||
version: Option<SemanticVersion>,
|
||||
cx: &mut AsyncApp,
|
||||
@@ -504,7 +503,7 @@ impl remote::RemoteClientDelegate for SshClientDelegate {
|
||||
|
||||
fn get_download_params(
|
||||
&self,
|
||||
platform: RemotePlatform,
|
||||
platform: SshPlatform,
|
||||
release_channel: ReleaseChannel,
|
||||
version: Option<SemanticVersion>,
|
||||
cx: &mut AsyncApp,
|
||||
@@ -544,13 +543,13 @@ pub fn connect_over_ssh(
|
||||
ui: Entity<SshPrompt>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Option<Entity<RemoteClient>>>> {
|
||||
) -> Task<Result<Option<Entity<SshRemoteClient>>>> {
|
||||
let window = window.window_handle();
|
||||
let known_password = connection_options.password.clone();
|
||||
let (tx, rx) = oneshot::channel();
|
||||
ui.update(cx, |ui, _cx| ui.set_cancellation_tx(tx));
|
||||
|
||||
remote::RemoteClient::ssh(
|
||||
remote::SshRemoteClient::new(
|
||||
unique_identifier,
|
||||
connection_options,
|
||||
rx,
|
||||
@@ -682,9 +681,9 @@ pub async fn open_ssh_project(
|
||||
|
||||
window
|
||||
.update(cx, |workspace, _, cx| {
|
||||
if let Some(client) = workspace.project().read(cx).remote_client() {
|
||||
if let Some(client) = workspace.project().read(cx).ssh_client() {
|
||||
ExtensionStore::global(cx)
|
||||
.update(cx, |store, cx| store.register_remote_client(client, cx));
|
||||
.update(cx, |store, cx| store.register_ssh_client(client, cx));
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
|
||||
@@ -51,16 +51,6 @@ pub async fn write_message<S: AsyncWrite + Unpin>(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn write_size_prefixed_buffer<S: AsyncWrite + Unpin>(
|
||||
stream: &mut S,
|
||||
buffer: &mut Vec<u8>,
|
||||
) -> Result<()> {
|
||||
let len = buffer.len() as u32;
|
||||
stream.write_all(len.to_le_bytes().as_slice()).await?;
|
||||
stream.write_all(buffer).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn read_message_raw<S: AsyncRead + Unpin>(
|
||||
stream: &mut S,
|
||||
buffer: &mut Vec<u8>,
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
pub mod json_log;
|
||||
pub mod protocol;
|
||||
pub mod proxy;
|
||||
pub mod remote_client;
|
||||
mod transport;
|
||||
pub mod ssh_session;
|
||||
|
||||
pub use remote_client::{
|
||||
ConnectionIdentifier, ConnectionState, RemoteClient, RemoteClientDelegate, RemoteClientEvent,
|
||||
RemotePlatform,
|
||||
pub use ssh_session::{
|
||||
ConnectionState, SshClientDelegate, SshConnectionOptions, SshInfo, SshPlatform,
|
||||
SshRemoteClient, SshRemoteEvent,
|
||||
};
|
||||
pub use transport::ssh::{SshConnectionOptions, SshPortForwardOption};
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user