Compare commits
39 Commits
read-file-
...
git/panel-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
637296ce6e | ||
|
|
90710b91c5 | ||
|
|
53f1fd5a27 | ||
|
|
e98de8c7ca | ||
|
|
fbd98c1fdc | ||
|
|
50b491b360 | ||
|
|
9dab0e5f9b | ||
|
|
b0c8952b0c | ||
|
|
47bdf9f026 | ||
|
|
02cc7765f9 | ||
|
|
07ffb17991 | ||
|
|
b8c644a12e | ||
|
|
f7dac91680 | ||
|
|
5a8a80a956 | ||
|
|
0c64f5185f | ||
|
|
56bf1e09f7 | ||
|
|
1ce5974917 | ||
|
|
db7d11e879 | ||
|
|
c70617c69e | ||
|
|
17afd49adb | ||
|
|
6984c9b459 | ||
|
|
be8038eb81 | ||
|
|
577467bed8 | ||
|
|
18ddbc044f | ||
|
|
5e1da57f25 | ||
|
|
4bee58f70e | ||
|
|
7eda0cd996 | ||
|
|
c21c47e8ef | ||
|
|
92142fc2b2 | ||
|
|
ed0121dc3a | ||
|
|
3aeeed0b30 | ||
|
|
7e6cdabb26 | ||
|
|
f91f3f24ef | ||
|
|
4ebc20b30c | ||
|
|
ac6aa735e4 | ||
|
|
d2cb9a13b8 | ||
|
|
5956489a47 | ||
|
|
418d850375 | ||
|
|
7ad3cf4387 |
74
.github/workflows/after_release.yml
vendored
Normal file
74
.github/workflows/after_release.yml
vendored
Normal file
@@ -0,0 +1,74 @@
|
||||
# Generated from xtask::workflows::after_release
|
||||
# Rebuild with `cargo xtask workflows`.
|
||||
name: after_release
|
||||
on:
|
||||
release:
|
||||
types:
|
||||
- published
|
||||
jobs:
|
||||
post_to_discord:
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
runs-on: namespace-profile-2x4-ubuntu-2404
|
||||
steps:
|
||||
- id: get-release-url
|
||||
name: after_release::post_to_discord::get_release_url
|
||||
run: |
|
||||
if [ "${{ github.event.release.prerelease }}" == "true" ]; then
|
||||
URL="https://zed.dev/releases/preview"
|
||||
else
|
||||
URL="https://zed.dev/releases/stable"
|
||||
fi
|
||||
|
||||
echo "URL=$URL" >> "$GITHUB_OUTPUT"
|
||||
shell: bash -euxo pipefail {0}
|
||||
- id: get-content
|
||||
name: after_release::post_to_discord::get_content
|
||||
uses: 2428392/gh-truncate-string-action@b3ff790d21cf42af3ca7579146eedb93c8fb0757
|
||||
with:
|
||||
stringToTruncate: |
|
||||
📣 Zed [${{ github.event.release.tag_name }}](<${{ steps.get-release-url.outputs.URL }}>) was just released!
|
||||
|
||||
${{ github.event.release.body }}
|
||||
maxLength: 2000
|
||||
truncationSymbol: '...'
|
||||
- name: after_release::post_to_discord::discord_webhook_action
|
||||
uses: tsickert/discord-webhook@c840d45a03a323fbc3f7507ac7769dbd91bfb164
|
||||
with:
|
||||
webhook-url: ${{ secrets.DISCORD_WEBHOOK_RELEASE_NOTES }}
|
||||
content: ${{ steps.get-content.outputs.string }}
|
||||
publish_winget:
|
||||
runs-on: namespace-profile-2x4-ubuntu-2404
|
||||
steps:
|
||||
- id: set-package-name
|
||||
name: after_release::publish_winget::set_package_name
|
||||
run: |
|
||||
if [ "${{ github.event.release.prerelease }}" == "true" ]; then
|
||||
PACKAGE_NAME=ZedIndustries.Zed.Preview
|
||||
else
|
||||
PACKAGE_NAME=ZedIndustries.Zed
|
||||
fi
|
||||
|
||||
echo "PACKAGE_NAME=$PACKAGE_NAME" >> "$GITHUB_OUTPUT"
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: after_release::publish_winget::winget_releaser
|
||||
uses: vedantmgoyal9/winget-releaser@19e706d4c9121098010096f9c495a70a7518b30f
|
||||
with:
|
||||
identifier: ${{ steps.set-package-name.outputs.PACKAGE_NAME }}
|
||||
max-versions-to-keep: 5
|
||||
token: ${{ secrets.WINGET_TOKEN }}
|
||||
create_sentry_release:
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
runs-on: namespace-profile-2x4-ubuntu-2404
|
||||
steps:
|
||||
- name: steps::checkout_repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||
with:
|
||||
clean: false
|
||||
- name: release::create_sentry_release
|
||||
uses: getsentry/action-release@526942b68292201ac6bbb99b9a0747d4abee354c
|
||||
with:
|
||||
environment: production
|
||||
env:
|
||||
SENTRY_ORG: zed-dev
|
||||
SENTRY_PROJECT: zed
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
93
.github/workflows/community_release_actions.yml
vendored
93
.github/workflows/community_release_actions.yml
vendored
@@ -1,93 +0,0 @@
|
||||
# IF YOU UPDATE THE NAME OF ANY GITHUB SECRET, YOU MUST CHERRY PICK THE COMMIT
|
||||
# TO BOTH STABLE AND PREVIEW CHANNELS
|
||||
|
||||
name: Release Actions
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
discord_release:
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Get release URL
|
||||
id: get-release-url
|
||||
run: |
|
||||
if [ "${{ github.event.release.prerelease }}" == "true" ]; then
|
||||
URL="https://zed.dev/releases/preview"
|
||||
else
|
||||
URL="https://zed.dev/releases/stable"
|
||||
fi
|
||||
|
||||
echo "URL=$URL" >> "$GITHUB_OUTPUT"
|
||||
- name: Get content
|
||||
uses: 2428392/gh-truncate-string-action@b3ff790d21cf42af3ca7579146eedb93c8fb0757 # v1.4.1
|
||||
id: get-content
|
||||
with:
|
||||
stringToTruncate: |
|
||||
📣 Zed [${{ github.event.release.tag_name }}](<${{ steps.get-release-url.outputs.URL }}>) was just released!
|
||||
|
||||
${{ github.event.release.body }}
|
||||
maxLength: 2000
|
||||
truncationSymbol: "..."
|
||||
- name: Discord Webhook Action
|
||||
uses: tsickert/discord-webhook@c840d45a03a323fbc3f7507ac7769dbd91bfb164 # v5.3.0
|
||||
with:
|
||||
webhook-url: ${{ secrets.DISCORD_WEBHOOK_RELEASE_NOTES }}
|
||||
content: ${{ steps.get-content.outputs.string }}
|
||||
|
||||
publish-winget:
|
||||
runs-on:
|
||||
- ubuntu-latest
|
||||
steps:
|
||||
- name: Set Package Name
|
||||
id: set-package-name
|
||||
run: |
|
||||
if [ "${{ github.event.release.prerelease }}" == "true" ]; then
|
||||
PACKAGE_NAME=ZedIndustries.Zed.Preview
|
||||
else
|
||||
PACKAGE_NAME=ZedIndustries.Zed
|
||||
fi
|
||||
|
||||
echo "PACKAGE_NAME=$PACKAGE_NAME" >> "$GITHUB_OUTPUT"
|
||||
- uses: vedantmgoyal9/winget-releaser@19e706d4c9121098010096f9c495a70a7518b30f # v2
|
||||
with:
|
||||
identifier: ${{ steps.set-package-name.outputs.PACKAGE_NAME }}
|
||||
max-versions-to-keep: 5
|
||||
token: ${{ secrets.WINGET_TOKEN }}
|
||||
|
||||
send_release_notes_email:
|
||||
if: false && github.repository_owner == 'zed-industries' && !github.event.release.prerelease
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Check if release was promoted from preview
|
||||
id: check-promotion-from-preview
|
||||
run: |
|
||||
VERSION="${{ github.event.release.tag_name }}"
|
||||
PREVIEW_TAG="${VERSION}-pre"
|
||||
|
||||
if git rev-parse "$PREVIEW_TAG" > /dev/null 2>&1; then
|
||||
echo "was_promoted_from_preview=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "was_promoted_from_preview=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Send release notes email
|
||||
if: steps.check-promotion-from-preview.outputs.was_promoted_from_preview == 'true'
|
||||
run: |
|
||||
TAG="${{ github.event.release.tag_name }}"
|
||||
cat << 'EOF' > release_body.txt
|
||||
${{ github.event.release.body }}
|
||||
EOF
|
||||
jq -n --arg tag "$TAG" --rawfile body release_body.txt '{version: $tag, markdown_body: $body}' \
|
||||
> release_data.json
|
||||
curl -X POST "https://zed.dev/api/send_release_notes_email" \
|
||||
-H "Authorization: Bearer ${{ secrets.RELEASE_NOTES_API_TOKEN }}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d @release_data.json
|
||||
8
.github/workflows/release.yml
vendored
8
.github/workflows/release.yml
vendored
@@ -475,14 +475,6 @@ jobs:
|
||||
shell: bash -euxo pipefail {0}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: release::create_sentry_release
|
||||
uses: getsentry/action-release@526942b68292201ac6bbb99b9a0747d4abee354c
|
||||
with:
|
||||
environment: production
|
||||
env:
|
||||
SENTRY_ORG: zed-dev
|
||||
SENTRY_PROJECT: zed
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name == 'main' && github.sha || 'anysha' }}
|
||||
cancel-in-progress: true
|
||||
|
||||
3
Cargo.lock
generated
3
Cargo.lock
generated
@@ -20951,6 +20951,7 @@ dependencies = [
|
||||
"gh-workflow",
|
||||
"indexmap 2.11.4",
|
||||
"indoc",
|
||||
"serde",
|
||||
"toml 0.8.23",
|
||||
"toml_edit 0.22.27",
|
||||
]
|
||||
@@ -21135,7 +21136,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.212.0"
|
||||
version = "0.212.3"
|
||||
dependencies = [
|
||||
"acp_tools",
|
||||
"activity_indicator",
|
||||
|
||||
@@ -50,13 +50,14 @@ impl crate::AgentServer for CustomAgentServer {
|
||||
fn set_default_mode(&self, mode_id: Option<acp::SessionModeId>, fs: Arc<dyn Fs>, cx: &mut App) {
|
||||
let name = self.name();
|
||||
update_settings_file(fs, cx, move |settings, _| {
|
||||
settings
|
||||
if let Some(settings) = settings
|
||||
.agent_servers
|
||||
.get_or_insert_default()
|
||||
.custom
|
||||
.get_mut(&name)
|
||||
.unwrap()
|
||||
.default_mode = mode_id.map(|m| m.to_string())
|
||||
{
|
||||
settings.default_mode = mode_id.map(|m| m.to_string())
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -646,16 +646,14 @@ impl ContextPickerCompletionProvider {
|
||||
cx: &mut App,
|
||||
) -> Vec<ContextPickerEntry> {
|
||||
let embedded_context = self.prompt_capabilities.borrow().embedded_context;
|
||||
let mut entries = if embedded_context {
|
||||
vec![
|
||||
ContextPickerEntry::Mode(ContextPickerMode::File),
|
||||
ContextPickerEntry::Mode(ContextPickerMode::Symbol),
|
||||
ContextPickerEntry::Mode(ContextPickerMode::Thread),
|
||||
]
|
||||
} else {
|
||||
// File is always available, but we don't need a mode entry
|
||||
vec![]
|
||||
};
|
||||
let mut entries = vec![
|
||||
ContextPickerEntry::Mode(ContextPickerMode::File),
|
||||
ContextPickerEntry::Mode(ContextPickerMode::Symbol),
|
||||
];
|
||||
|
||||
if embedded_context {
|
||||
entries.push(ContextPickerEntry::Mode(ContextPickerMode::Thread));
|
||||
}
|
||||
|
||||
let has_selection = workspace
|
||||
.read(cx)
|
||||
|
||||
@@ -356,7 +356,7 @@ impl MessageEditor {
|
||||
|
||||
let task = match mention_uri.clone() {
|
||||
MentionUri::Fetch { url } => self.confirm_mention_for_fetch(url, cx),
|
||||
MentionUri::Directory { .. } => Task::ready(Ok(Mention::UriOnly)),
|
||||
MentionUri::Directory { .. } => Task::ready(Ok(Mention::Link)),
|
||||
MentionUri::Thread { id, .. } => self.confirm_mention_for_thread(id, cx),
|
||||
MentionUri::TextThread { path, .. } => self.confirm_mention_for_text_thread(path, cx),
|
||||
MentionUri::File { abs_path } => self.confirm_mention_for_file(abs_path, cx),
|
||||
@@ -373,7 +373,6 @@ impl MessageEditor {
|
||||
)))
|
||||
}
|
||||
MentionUri::Selection { .. } => {
|
||||
// Handled elsewhere
|
||||
debug_panic!("unexpected selection URI");
|
||||
Task::ready(Err(anyhow!("unexpected selection URI")))
|
||||
}
|
||||
@@ -704,13 +703,11 @@ impl MessageEditor {
|
||||
return Task::ready(Err(err));
|
||||
}
|
||||
|
||||
let contents = self.mention_set.contents(
|
||||
&self.prompt_capabilities.borrow(),
|
||||
full_mention_content,
|
||||
self.project.clone(),
|
||||
cx,
|
||||
);
|
||||
let contents = self
|
||||
.mention_set
|
||||
.contents(full_mention_content, self.project.clone(), cx);
|
||||
let editor = self.editor.clone();
|
||||
let supports_embedded_context = self.prompt_capabilities.borrow().embedded_context;
|
||||
|
||||
cx.spawn(async move |_, cx| {
|
||||
let contents = contents.await?;
|
||||
@@ -738,18 +735,32 @@ impl MessageEditor {
|
||||
tracked_buffers,
|
||||
} => {
|
||||
all_tracked_buffers.extend(tracked_buffers.iter().cloned());
|
||||
acp::ContentBlock::Resource(acp::EmbeddedResource {
|
||||
annotations: None,
|
||||
resource: acp::EmbeddedResourceResource::TextResourceContents(
|
||||
acp::TextResourceContents {
|
||||
mime_type: None,
|
||||
text: content.clone(),
|
||||
uri: uri.to_uri().to_string(),
|
||||
meta: None,
|
||||
},
|
||||
),
|
||||
meta: None,
|
||||
})
|
||||
if supports_embedded_context {
|
||||
acp::ContentBlock::Resource(acp::EmbeddedResource {
|
||||
annotations: None,
|
||||
resource:
|
||||
acp::EmbeddedResourceResource::TextResourceContents(
|
||||
acp::TextResourceContents {
|
||||
mime_type: None,
|
||||
text: content.clone(),
|
||||
uri: uri.to_uri().to_string(),
|
||||
meta: None,
|
||||
},
|
||||
),
|
||||
meta: None,
|
||||
})
|
||||
} else {
|
||||
acp::ContentBlock::ResourceLink(acp::ResourceLink {
|
||||
name: uri.name(),
|
||||
uri: uri.to_uri().to_string(),
|
||||
annotations: None,
|
||||
description: None,
|
||||
mime_type: None,
|
||||
size: None,
|
||||
title: None,
|
||||
meta: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
Mention::Image(mention_image) => {
|
||||
let uri = match uri {
|
||||
@@ -771,18 +782,16 @@ impl MessageEditor {
|
||||
meta: None,
|
||||
})
|
||||
}
|
||||
Mention::UriOnly => {
|
||||
acp::ContentBlock::ResourceLink(acp::ResourceLink {
|
||||
name: uri.name(),
|
||||
uri: uri.to_uri().to_string(),
|
||||
annotations: None,
|
||||
description: None,
|
||||
mime_type: None,
|
||||
size: None,
|
||||
title: None,
|
||||
meta: None,
|
||||
})
|
||||
}
|
||||
Mention::Link => acp::ContentBlock::ResourceLink(acp::ResourceLink {
|
||||
name: uri.name(),
|
||||
uri: uri.to_uri().to_string(),
|
||||
annotations: None,
|
||||
description: None,
|
||||
mime_type: None,
|
||||
size: None,
|
||||
title: None,
|
||||
meta: None,
|
||||
}),
|
||||
};
|
||||
chunks.push(chunk);
|
||||
ix = crease_range.end;
|
||||
@@ -1111,7 +1120,7 @@ impl MessageEditor {
|
||||
let start = text.len();
|
||||
write!(&mut text, "{}", mention_uri.as_link()).ok();
|
||||
let end = text.len();
|
||||
mentions.push((start..end, mention_uri, Mention::UriOnly));
|
||||
mentions.push((start..end, mention_uri, Mention::Link));
|
||||
}
|
||||
}
|
||||
acp::ContentBlock::Image(acp::ImageContent {
|
||||
@@ -1517,7 +1526,7 @@ pub enum Mention {
|
||||
tracked_buffers: Vec<Entity<Buffer>>,
|
||||
},
|
||||
Image(MentionImage),
|
||||
UriOnly,
|
||||
Link,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
@@ -1534,21 +1543,10 @@ pub struct MentionSet {
|
||||
impl MentionSet {
|
||||
fn contents(
|
||||
&self,
|
||||
prompt_capabilities: &acp::PromptCapabilities,
|
||||
full_mention_content: bool,
|
||||
project: Entity<Project>,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<HashMap<CreaseId, (MentionUri, Mention)>>> {
|
||||
if !prompt_capabilities.embedded_context {
|
||||
let mentions = self
|
||||
.mentions
|
||||
.iter()
|
||||
.map(|(crease_id, (uri, _))| (*crease_id, (uri.clone(), Mention::UriOnly)))
|
||||
.collect();
|
||||
|
||||
return Task::ready(Ok(mentions));
|
||||
}
|
||||
|
||||
let mentions = self.mentions.clone();
|
||||
cx.spawn(async move |cx| {
|
||||
let mut contents = HashMap::default();
|
||||
@@ -2202,6 +2200,8 @@ mod tests {
|
||||
format!("seven.txt b{slash}"),
|
||||
format!("six.txt b{slash}"),
|
||||
format!("five.txt b{slash}"),
|
||||
"Files & Directories".into(),
|
||||
"Symbols".into()
|
||||
]
|
||||
);
|
||||
editor.set_text("", window, cx);
|
||||
@@ -2286,21 +2286,11 @@ mod tests {
|
||||
assert_eq!(fold_ranges(editor, cx).len(), 1);
|
||||
});
|
||||
|
||||
let all_prompt_capabilities = acp::PromptCapabilities {
|
||||
image: true,
|
||||
audio: true,
|
||||
embedded_context: true,
|
||||
meta: None,
|
||||
};
|
||||
|
||||
let contents = message_editor
|
||||
.update(&mut cx, |message_editor, cx| {
|
||||
message_editor.mention_set().contents(
|
||||
&all_prompt_capabilities,
|
||||
false,
|
||||
project.clone(),
|
||||
cx,
|
||||
)
|
||||
message_editor
|
||||
.mention_set()
|
||||
.contents(false, project.clone(), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
@@ -2318,30 +2308,6 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
let contents = message_editor
|
||||
.update(&mut cx, |message_editor, cx| {
|
||||
message_editor.mention_set().contents(
|
||||
&acp::PromptCapabilities::default(),
|
||||
false,
|
||||
project.clone(),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.into_values()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
{
|
||||
let [(uri, Mention::UriOnly)] = contents.as_slice() else {
|
||||
panic!("Unexpected mentions");
|
||||
};
|
||||
pretty_assertions::assert_eq!(
|
||||
uri,
|
||||
&MentionUri::parse(&url_one, PathStyle::local()).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
cx.simulate_input(" ");
|
||||
|
||||
editor.update(&mut cx, |editor, cx| {
|
||||
@@ -2377,12 +2343,9 @@ mod tests {
|
||||
|
||||
let contents = message_editor
|
||||
.update(&mut cx, |message_editor, cx| {
|
||||
message_editor.mention_set().contents(
|
||||
&all_prompt_capabilities,
|
||||
false,
|
||||
project.clone(),
|
||||
cx,
|
||||
)
|
||||
message_editor
|
||||
.mention_set()
|
||||
.contents(false, project.clone(), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
@@ -2503,12 +2466,9 @@ mod tests {
|
||||
|
||||
let contents = message_editor
|
||||
.update(&mut cx, |message_editor, cx| {
|
||||
message_editor.mention_set().contents(
|
||||
&all_prompt_capabilities,
|
||||
false,
|
||||
project.clone(),
|
||||
cx,
|
||||
)
|
||||
message_editor
|
||||
.mention_set()
|
||||
.contents(false, project.clone(), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
@@ -2554,12 +2514,9 @@ mod tests {
|
||||
// Getting the message contents fails
|
||||
message_editor
|
||||
.update(&mut cx, |message_editor, cx| {
|
||||
message_editor.mention_set().contents(
|
||||
&all_prompt_capabilities,
|
||||
false,
|
||||
project.clone(),
|
||||
cx,
|
||||
)
|
||||
message_editor
|
||||
.mention_set()
|
||||
.contents(false, project.clone(), cx)
|
||||
})
|
||||
.await
|
||||
.expect_err("Should fail to load x.png");
|
||||
@@ -2610,12 +2567,9 @@ mod tests {
|
||||
// Now getting the contents succeeds, because the invalid mention was removed
|
||||
let contents = message_editor
|
||||
.update(&mut cx, |message_editor, cx| {
|
||||
message_editor.mention_set().contents(
|
||||
&all_prompt_capabilities,
|
||||
false,
|
||||
project.clone(),
|
||||
cx,
|
||||
)
|
||||
message_editor
|
||||
.mention_set()
|
||||
.contents(false, project.clone(), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -2897,6 +2851,147 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_editor_respects_embedded_context_capability(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
|
||||
let file_content = "fn main() { println!(\"Hello, world!\"); }\n";
|
||||
|
||||
fs.insert_tree(
|
||||
"/project",
|
||||
json!({
|
||||
"src": {
|
||||
"main.rs": file_content,
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
|
||||
|
||||
let (workspace, cx) =
|
||||
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
|
||||
|
||||
let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx));
|
||||
let history_store = cx.new(|cx| HistoryStore::new(text_thread_store, cx));
|
||||
|
||||
let (message_editor, editor) = workspace.update_in(cx, |workspace, window, cx| {
|
||||
let workspace_handle = cx.weak_entity();
|
||||
let message_editor = cx.new(|cx| {
|
||||
MessageEditor::new(
|
||||
workspace_handle,
|
||||
project.clone(),
|
||||
history_store.clone(),
|
||||
None,
|
||||
Default::default(),
|
||||
Default::default(),
|
||||
"Test Agent".into(),
|
||||
"Test",
|
||||
EditorMode::AutoHeight {
|
||||
max_lines: None,
|
||||
min_lines: 1,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
workspace.active_pane().update(cx, |pane, cx| {
|
||||
pane.add_item(
|
||||
Box::new(cx.new(|_| MessageEditorItem(message_editor.clone()))),
|
||||
true,
|
||||
true,
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
message_editor.read(cx).focus_handle(cx).focus(window);
|
||||
let editor = message_editor.read(cx).editor().clone();
|
||||
(message_editor, editor)
|
||||
});
|
||||
|
||||
cx.simulate_input("What is in @file main");
|
||||
|
||||
editor.update_in(cx, |editor, window, cx| {
|
||||
assert!(editor.has_visible_completions_menu());
|
||||
assert_eq!(editor.text(cx), "What is in @file main");
|
||||
editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
|
||||
});
|
||||
|
||||
let content = message_editor
|
||||
.update(cx, |editor, cx| editor.contents(false, cx))
|
||||
.await
|
||||
.unwrap()
|
||||
.0;
|
||||
|
||||
let main_rs_uri = if cfg!(windows) {
|
||||
"file:///C:/project/src/main.rs".to_string()
|
||||
} else {
|
||||
"file:///project/src/main.rs".to_string()
|
||||
};
|
||||
|
||||
// When embedded context is `false` we should get a resource link
|
||||
pretty_assertions::assert_eq!(
|
||||
content,
|
||||
vec![
|
||||
acp::ContentBlock::Text(acp::TextContent {
|
||||
text: "What is in ".to_string(),
|
||||
annotations: None,
|
||||
meta: None
|
||||
}),
|
||||
acp::ContentBlock::ResourceLink(acp::ResourceLink {
|
||||
uri: main_rs_uri.clone(),
|
||||
name: "main.rs".to_string(),
|
||||
annotations: None,
|
||||
meta: None,
|
||||
description: None,
|
||||
mime_type: None,
|
||||
size: None,
|
||||
title: None,
|
||||
})
|
||||
]
|
||||
);
|
||||
|
||||
message_editor.update(cx, |editor, _cx| {
|
||||
editor.prompt_capabilities.replace(acp::PromptCapabilities {
|
||||
embedded_context: true,
|
||||
..Default::default()
|
||||
})
|
||||
});
|
||||
|
||||
let content = message_editor
|
||||
.update(cx, |editor, cx| editor.contents(false, cx))
|
||||
.await
|
||||
.unwrap()
|
||||
.0;
|
||||
|
||||
// When embedded context is `true` we should get a resource
|
||||
pretty_assertions::assert_eq!(
|
||||
content,
|
||||
vec![
|
||||
acp::ContentBlock::Text(acp::TextContent {
|
||||
text: "What is in ".to_string(),
|
||||
annotations: None,
|
||||
meta: None
|
||||
}),
|
||||
acp::ContentBlock::Resource(acp::EmbeddedResource {
|
||||
resource: acp::EmbeddedResourceResource::TextResourceContents(
|
||||
acp::TextResourceContents {
|
||||
text: file_content.to_string(),
|
||||
uri: main_rs_uri,
|
||||
mime_type: None,
|
||||
meta: None
|
||||
}
|
||||
),
|
||||
annotations: None,
|
||||
meta: None
|
||||
})
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_autoscroll_after_insert_selections(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
@@ -294,7 +294,6 @@ pub struct AcpThreadView {
|
||||
resume_thread_metadata: Option<DbThreadMetadata>,
|
||||
_cancel_task: Option<Task<()>>,
|
||||
_subscriptions: [Subscription; 5],
|
||||
#[cfg(target_os = "windows")]
|
||||
show_codex_windows_warning: bool,
|
||||
}
|
||||
|
||||
@@ -401,7 +400,6 @@ impl AcpThreadView {
|
||||
),
|
||||
];
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
let show_codex_windows_warning = crate::ExternalAgent::parse_built_in(agent.as_ref())
|
||||
== Some(crate::ExternalAgent::Codex);
|
||||
|
||||
@@ -447,7 +445,6 @@ impl AcpThreadView {
|
||||
focus_handle: cx.focus_handle(),
|
||||
new_server_version_available: None,
|
||||
resume_thread_metadata: resume_thread,
|
||||
#[cfg(target_os = "windows")]
|
||||
show_codex_windows_warning,
|
||||
}
|
||||
}
|
||||
@@ -1506,6 +1503,12 @@ impl AcpThreadView {
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
// Run SpawnInTerminal in the same dir as the ACP server
|
||||
let cwd = connection
|
||||
.clone()
|
||||
.downcast::<agent_servers::AcpConnection>()
|
||||
.map(|acp_conn| acp_conn.root_dir().to_path_buf());
|
||||
|
||||
// Build SpawnInTerminal from _meta
|
||||
let login = task::SpawnInTerminal {
|
||||
id: task::TaskId(format!("external-agent-{}-login", label)),
|
||||
@@ -1514,6 +1517,7 @@ impl AcpThreadView {
|
||||
command: Some(command.to_string()),
|
||||
args,
|
||||
command_label: label.to_string(),
|
||||
cwd,
|
||||
env,
|
||||
use_new_terminal: true,
|
||||
allow_concurrent_runs: true,
|
||||
@@ -1526,8 +1530,9 @@ impl AcpThreadView {
|
||||
pending_auth_method.replace(method.clone());
|
||||
|
||||
if let Some(workspace) = self.workspace.upgrade() {
|
||||
let project = self.project.clone();
|
||||
let authenticate = Self::spawn_external_agent_login(
|
||||
login, workspace, false, window, cx,
|
||||
login, workspace, project, false, true, window, cx,
|
||||
);
|
||||
cx.notify();
|
||||
self.auth_task = Some(cx.spawn_in(window, {
|
||||
@@ -1671,7 +1676,10 @@ impl AcpThreadView {
|
||||
&& let Some(login) = self.login.clone()
|
||||
{
|
||||
if let Some(workspace) = self.workspace.upgrade() {
|
||||
Self::spawn_external_agent_login(login, workspace, false, window, cx)
|
||||
let project = self.project.clone();
|
||||
Self::spawn_external_agent_login(
|
||||
login, workspace, project, false, false, window, cx,
|
||||
)
|
||||
} else {
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
@@ -1721,17 +1729,40 @@ impl AcpThreadView {
|
||||
fn spawn_external_agent_login(
|
||||
login: task::SpawnInTerminal,
|
||||
workspace: Entity<Workspace>,
|
||||
project: Entity<Project>,
|
||||
previous_attempt: bool,
|
||||
check_exit_code: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<()>> {
|
||||
let Some(terminal_panel) = workspace.read(cx).panel::<TerminalPanel>(cx) else {
|
||||
return Task::ready(Ok(()));
|
||||
};
|
||||
let project = workspace.read(cx).project().clone();
|
||||
|
||||
window.spawn(cx, async move |cx| {
|
||||
let mut task = login.clone();
|
||||
if let Some(cmd) = &task.command {
|
||||
// Have "node" command use Zed's managed Node runtime by default
|
||||
if cmd == "node" {
|
||||
let resolved_node_runtime = project
|
||||
.update(cx, |project, cx| {
|
||||
let agent_server_store = project.agent_server_store().clone();
|
||||
agent_server_store.update(cx, |store, cx| {
|
||||
store.node_runtime().map(|node_runtime| {
|
||||
cx.background_spawn(async move {
|
||||
node_runtime.binary_path().await
|
||||
})
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
if let Ok(Some(resolve_task)) = resolved_node_runtime {
|
||||
if let Ok(node_path) = resolve_task.await {
|
||||
task.command = Some(node_path.to_string_lossy().to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
task.shell = task::Shell::WithArguments {
|
||||
program: task.command.take().expect("login command should be set"),
|
||||
args: std::mem::take(&mut task.args),
|
||||
@@ -1749,44 +1780,65 @@ impl AcpThreadView {
|
||||
})?;
|
||||
|
||||
let terminal = terminal.await?;
|
||||
let mut exit_status = terminal
|
||||
.read_with(cx, |terminal, cx| terminal.wait_for_completed_task(cx))?
|
||||
.fuse();
|
||||
|
||||
let logged_in = cx
|
||||
.spawn({
|
||||
let terminal = terminal.clone();
|
||||
async move |cx| {
|
||||
loop {
|
||||
cx.background_executor().timer(Duration::from_secs(1)).await;
|
||||
let content =
|
||||
terminal.update(cx, |terminal, _cx| terminal.get_content())?;
|
||||
if content.contains("Login successful")
|
||||
|| content.contains("Type your message")
|
||||
{
|
||||
return anyhow::Ok(());
|
||||
if check_exit_code {
|
||||
// For extension-based auth, wait for the process to exit and check exit code
|
||||
let exit_status = terminal
|
||||
.read_with(cx, |terminal, cx| terminal.wait_for_completed_task(cx))?
|
||||
.await;
|
||||
|
||||
match exit_status {
|
||||
Some(status) if status.success() => {
|
||||
Ok(())
|
||||
}
|
||||
Some(status) => {
|
||||
Err(anyhow!("Login command failed with exit code: {:?}", status.code()))
|
||||
}
|
||||
None => {
|
||||
Err(anyhow!("Login command terminated without exit status"))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// For hardcoded agents (claude-login, gemini-cli): look for specific output
|
||||
let mut exit_status = terminal
|
||||
.read_with(cx, |terminal, cx| terminal.wait_for_completed_task(cx))?
|
||||
.fuse();
|
||||
|
||||
let logged_in = cx
|
||||
.spawn({
|
||||
let terminal = terminal.clone();
|
||||
async move |cx| {
|
||||
loop {
|
||||
cx.background_executor().timer(Duration::from_secs(1)).await;
|
||||
let content =
|
||||
terminal.update(cx, |terminal, _cx| terminal.get_content())?;
|
||||
if content.contains("Login successful")
|
||||
|| content.contains("Type your message")
|
||||
{
|
||||
return anyhow::Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.fuse();
|
||||
futures::pin_mut!(logged_in);
|
||||
futures::select_biased! {
|
||||
result = logged_in => {
|
||||
if let Err(e) = result {
|
||||
log::error!("{e}");
|
||||
return Err(anyhow!("exited before logging in"));
|
||||
}
|
||||
}
|
||||
})
|
||||
.fuse();
|
||||
futures::pin_mut!(logged_in);
|
||||
futures::select_biased! {
|
||||
result = logged_in => {
|
||||
if let Err(e) = result {
|
||||
log::error!("{e}");
|
||||
_ = exit_status => {
|
||||
if !previous_attempt && project.read_with(cx, |project, _| project.is_via_remote_server())? && login.label.contains("gemini") {
|
||||
return cx.update(|window, cx| Self::spawn_external_agent_login(login, workspace, project.clone(), true, false, window, cx))?.await
|
||||
}
|
||||
return Err(anyhow!("exited before logging in"));
|
||||
}
|
||||
}
|
||||
_ = exit_status => {
|
||||
if !previous_attempt && project.read_with(cx, |project, _| project.is_via_remote_server())? && login.label.contains("gemini") {
|
||||
return cx.update(|window, cx| Self::spawn_external_agent_login(login, workspace, true, window, cx))?.await
|
||||
}
|
||||
return Err(anyhow!("exited before logging in"));
|
||||
}
|
||||
terminal.update(cx, |terminal, _| terminal.kill_active_task())?;
|
||||
Ok(())
|
||||
}
|
||||
terminal.update(cx, |terminal, _| terminal.kill_active_task())?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -5197,7 +5249,6 @@ impl AcpThreadView {
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn render_codex_windows_warning(&self, cx: &mut Context<Self>) -> Option<Callout> {
|
||||
if self.show_codex_windows_warning {
|
||||
Some(
|
||||
@@ -5213,8 +5264,9 @@ impl AcpThreadView {
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.on_click(cx.listener({
|
||||
move |_, _, window, cx| {
|
||||
window.dispatch_action(
|
||||
move |_, _, _window, cx| {
|
||||
#[cfg(windows)]
|
||||
_window.dispatch_action(
|
||||
zed_actions::wsl_actions::OpenWsl::default().boxed_clone(),
|
||||
cx,
|
||||
);
|
||||
@@ -5717,13 +5769,10 @@ impl Render for AcpThreadView {
|
||||
})
|
||||
.children(self.render_thread_retry_status_callout(window, cx))
|
||||
.children({
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
if cfg!(windows) && self.project.read(cx).is_local() {
|
||||
self.render_codex_windows_warning(cx)
|
||||
}
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
Vec::<Empty>::new()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.children(self.render_thread_error(cx))
|
||||
|
||||
@@ -2081,7 +2081,7 @@ impl AgentPanel {
|
||||
let mut entry =
|
||||
ContextMenuEntry::new(format!("New {}", agent_name));
|
||||
if let Some(icon_path) = icon_path {
|
||||
entry = entry.custom_icon_path(icon_path);
|
||||
entry = entry.custom_icon_svg(icon_path);
|
||||
} else {
|
||||
entry = entry.icon(IconName::Terminal);
|
||||
}
|
||||
@@ -2150,7 +2150,7 @@ impl AgentPanel {
|
||||
.when_some(selected_agent_custom_icon, |this, icon_path| {
|
||||
let label = selected_agent_label.clone();
|
||||
this.px(DynamicSpacing::Base02.rems(cx))
|
||||
.child(Icon::from_path(icon_path).color(Color::Muted))
|
||||
.child(Icon::from_external_svg(icon_path).color(Color::Muted))
|
||||
.tooltip(move |_window, cx| {
|
||||
Tooltip::with_meta(label.clone(), None, "Selected Agent", cx)
|
||||
})
|
||||
|
||||
@@ -23,7 +23,7 @@ use project::{
|
||||
use settings::Settings;
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
cmp::Ordering,
|
||||
cmp::{self, Ordering},
|
||||
sync::Arc,
|
||||
};
|
||||
use text::{Anchor, BufferSnapshot, OffsetRangeExt};
|
||||
@@ -410,7 +410,7 @@ impl BufferDiagnosticsEditor {
|
||||
// in the editor.
|
||||
// This is done by iterating over the list of diagnostic blocks and
|
||||
// determine what range does the diagnostic block span.
|
||||
let mut excerpt_ranges: Vec<ExcerptRange<Point>> = Vec::new();
|
||||
let mut excerpt_ranges: Vec<ExcerptRange<_>> = Vec::new();
|
||||
|
||||
for diagnostic_block in blocks.iter() {
|
||||
let excerpt_range = context_range_for_entry(
|
||||
@@ -420,30 +420,43 @@ impl BufferDiagnosticsEditor {
|
||||
&mut cx,
|
||||
)
|
||||
.await;
|
||||
let initial_range = buffer_snapshot
|
||||
.anchor_after(diagnostic_block.initial_range.start)
|
||||
..buffer_snapshot.anchor_before(diagnostic_block.initial_range.end);
|
||||
|
||||
let index = excerpt_ranges
|
||||
.binary_search_by(|probe| {
|
||||
let bin_search = |probe: &ExcerptRange<text::Anchor>| {
|
||||
let context_start = || {
|
||||
probe
|
||||
.context
|
||||
.start
|
||||
.cmp(&excerpt_range.start)
|
||||
.then(probe.context.end.cmp(&excerpt_range.end))
|
||||
.then(
|
||||
probe
|
||||
.primary
|
||||
.start
|
||||
.cmp(&diagnostic_block.initial_range.start),
|
||||
)
|
||||
.then(probe.primary.end.cmp(&diagnostic_block.initial_range.end))
|
||||
.then(Ordering::Greater)
|
||||
})
|
||||
.unwrap_or_else(|index| index);
|
||||
.cmp(&excerpt_range.start, &buffer_snapshot)
|
||||
};
|
||||
let context_end =
|
||||
|| probe.context.end.cmp(&excerpt_range.end, &buffer_snapshot);
|
||||
let primary_start = || {
|
||||
probe
|
||||
.primary
|
||||
.start
|
||||
.cmp(&initial_range.start, &buffer_snapshot)
|
||||
};
|
||||
let primary_end =
|
||||
|| probe.primary.end.cmp(&initial_range.end, &buffer_snapshot);
|
||||
context_start()
|
||||
.then_with(context_end)
|
||||
.then_with(primary_start)
|
||||
.then_with(primary_end)
|
||||
.then(cmp::Ordering::Greater)
|
||||
};
|
||||
|
||||
let index = excerpt_ranges
|
||||
.binary_search_by(bin_search)
|
||||
.unwrap_or_else(|i| i);
|
||||
|
||||
excerpt_ranges.insert(
|
||||
index,
|
||||
ExcerptRange {
|
||||
context: excerpt_range,
|
||||
primary: diagnostic_block.initial_range.clone(),
|
||||
primary: initial_range,
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -466,6 +479,13 @@ impl BufferDiagnosticsEditor {
|
||||
buffer_diagnostics_editor
|
||||
.multibuffer
|
||||
.update(cx, |multibuffer, cx| {
|
||||
let excerpt_ranges = excerpt_ranges
|
||||
.into_iter()
|
||||
.map(|range| ExcerptRange {
|
||||
context: range.context.to_point(&buffer_snapshot),
|
||||
primary: range.primary.to_point(&buffer_snapshot),
|
||||
})
|
||||
.collect();
|
||||
multibuffer.set_excerpt_ranges_for_path(
|
||||
PathKey::for_buffer(&buffer, cx),
|
||||
buffer.clone(),
|
||||
|
||||
@@ -39,8 +39,8 @@ impl DiagnosticRenderer {
|
||||
let group_id = primary.diagnostic.group_id;
|
||||
let mut results = vec![];
|
||||
for entry in diagnostic_group.iter() {
|
||||
let mut markdown = Self::markdown(&entry.diagnostic);
|
||||
if entry.diagnostic.is_primary {
|
||||
let mut markdown = Self::markdown(&entry.diagnostic);
|
||||
let diagnostic = &primary.diagnostic;
|
||||
if diagnostic.source.is_some() || diagnostic.code.is_some() {
|
||||
markdown.push_str(" (");
|
||||
@@ -81,21 +81,12 @@ impl DiagnosticRenderer {
|
||||
diagnostics_editor: diagnostics_editor.clone(),
|
||||
markdown: cx.new(|cx| Markdown::new(markdown.into(), None, None, cx)),
|
||||
});
|
||||
} else if entry.range.start.row.abs_diff(primary.range.start.row) < 5 {
|
||||
let markdown = Self::markdown(&entry.diagnostic);
|
||||
|
||||
results.push(DiagnosticBlock {
|
||||
initial_range: entry.range.clone(),
|
||||
severity: entry.diagnostic.severity,
|
||||
diagnostics_editor: diagnostics_editor.clone(),
|
||||
markdown: cx.new(|cx| Markdown::new(markdown.into(), None, None, cx)),
|
||||
});
|
||||
} else {
|
||||
let mut markdown = Self::markdown(&entry.diagnostic);
|
||||
markdown.push_str(&format!(
|
||||
" ([back](file://#diagnostic-{buffer_id}-{group_id}-{primary_ix}))"
|
||||
));
|
||||
|
||||
if entry.range.start.row.abs_diff(primary.range.start.row) >= 5 {
|
||||
markdown.push_str(&format!(
|
||||
" ([back](file://#diagnostic-{buffer_id}-{group_id}-{primary_ix}))"
|
||||
));
|
||||
}
|
||||
results.push(DiagnosticBlock {
|
||||
initial_range: entry.range.clone(),
|
||||
severity: entry.diagnostic.severity,
|
||||
|
||||
@@ -152,8 +152,8 @@ impl Render for ProjectDiagnosticsEditor {
|
||||
|
||||
#[derive(PartialEq, Eq, Copy, Clone, Debug)]
|
||||
enum RetainExcerpts {
|
||||
Yes,
|
||||
No,
|
||||
All,
|
||||
Dirty,
|
||||
}
|
||||
|
||||
impl ProjectDiagnosticsEditor {
|
||||
@@ -207,17 +207,7 @@ impl ProjectDiagnosticsEditor {
|
||||
"diagnostics updated for server {language_server_id}, \
|
||||
paths {paths:?}. updating excerpts"
|
||||
);
|
||||
let focused = this.editor.focus_handle(cx).contains_focused(window, cx)
|
||||
|| this.focus_handle.contains_focused(window, cx);
|
||||
this.update_stale_excerpts(
|
||||
if focused {
|
||||
RetainExcerpts::Yes
|
||||
} else {
|
||||
RetainExcerpts::No
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
this.update_stale_excerpts(window, cx);
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
@@ -280,8 +270,7 @@ impl ProjectDiagnosticsEditor {
|
||||
cx,
|
||||
)
|
||||
});
|
||||
this.diagnostics.clear();
|
||||
this.update_all_excerpts(window, cx);
|
||||
this.refresh(window, cx);
|
||||
})
|
||||
.detach();
|
||||
|
||||
@@ -301,14 +290,14 @@ impl ProjectDiagnosticsEditor {
|
||||
diagnostic_summary_update: Task::ready(()),
|
||||
_subscription: project_event_subscription,
|
||||
};
|
||||
this.update_all_excerpts(window, cx);
|
||||
this.refresh(window, cx);
|
||||
this
|
||||
}
|
||||
|
||||
/// Closes all excerpts of buffers that:
|
||||
/// - have no diagnostics anymore
|
||||
/// - are saved (not dirty)
|
||||
/// - and, if `reatin_selections` is true, do not have selections within them
|
||||
/// - and, if `retain_selections` is true, do not have selections within them
|
||||
fn close_diagnosticless_buffers(
|
||||
&mut self,
|
||||
_window: &mut Window,
|
||||
@@ -328,19 +317,19 @@ impl ProjectDiagnosticsEditor {
|
||||
if retain_selections && selected_buffers.contains(&buffer_id) {
|
||||
continue;
|
||||
}
|
||||
let has_blocks = self
|
||||
let has_no_blocks = self
|
||||
.blocks
|
||||
.get(&buffer_id)
|
||||
.is_none_or(|blocks| blocks.is_empty());
|
||||
if !has_blocks {
|
||||
if !has_no_blocks {
|
||||
continue;
|
||||
}
|
||||
let is_dirty = self
|
||||
.multibuffer
|
||||
.read(cx)
|
||||
.buffer(buffer_id)
|
||||
.is_some_and(|buffer| buffer.read(cx).is_dirty());
|
||||
if !is_dirty {
|
||||
.is_none_or(|buffer| buffer.read(cx).is_dirty());
|
||||
if is_dirty {
|
||||
continue;
|
||||
}
|
||||
self.multibuffer.update(cx, |b, cx| {
|
||||
@@ -349,18 +338,10 @@ impl ProjectDiagnosticsEditor {
|
||||
}
|
||||
}
|
||||
|
||||
fn update_stale_excerpts(
|
||||
&mut self,
|
||||
mut retain_excerpts: RetainExcerpts,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
fn update_stale_excerpts(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if self.update_excerpts_task.is_some() {
|
||||
return;
|
||||
}
|
||||
if self.multibuffer.read(cx).is_dirty(cx) {
|
||||
retain_excerpts = RetainExcerpts::Yes;
|
||||
}
|
||||
|
||||
let project_handle = self.project.clone();
|
||||
self.update_excerpts_task = Some(cx.spawn_in(window, async move |this, cx| {
|
||||
@@ -386,6 +367,13 @@ impl ProjectDiagnosticsEditor {
|
||||
.log_err()
|
||||
{
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
let focused = this.editor.focus_handle(cx).contains_focused(window, cx)
|
||||
|| this.focus_handle.contains_focused(window, cx);
|
||||
let retain_excerpts = if focused {
|
||||
RetainExcerpts::All
|
||||
} else {
|
||||
RetainExcerpts::Dirty
|
||||
};
|
||||
this.update_excerpts(buffer, retain_excerpts, window, cx)
|
||||
})?
|
||||
.await?;
|
||||
@@ -441,7 +429,7 @@ impl ProjectDiagnosticsEditor {
|
||||
if self.update_excerpts_task.is_some() {
|
||||
self.update_excerpts_task = None;
|
||||
} else {
|
||||
self.update_all_excerpts(window, cx);
|
||||
self.refresh(window, cx);
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
@@ -459,31 +447,26 @@ impl ProjectDiagnosticsEditor {
|
||||
}
|
||||
}
|
||||
|
||||
/// Enqueue an update of all excerpts. Updates all paths that either
|
||||
/// currently have diagnostics or are currently present in this view.
|
||||
fn update_all_excerpts(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
/// Clears all diagnostics in this view, and refetches them from the project.
|
||||
fn refresh(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.diagnostics.clear();
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
for (_, block_ids) in self.blocks.drain() {
|
||||
editor.display_map.update(cx, |display_map, cx| {
|
||||
display_map.remove_blocks(block_ids.into_iter().collect(), cx)
|
||||
});
|
||||
}
|
||||
});
|
||||
self.multibuffer
|
||||
.update(cx, |multibuffer, cx| multibuffer.clear(cx));
|
||||
self.project.update(cx, |project, cx| {
|
||||
let mut project_paths = project
|
||||
self.paths_to_update = project
|
||||
.diagnostic_summaries(false, cx)
|
||||
.map(|(project_path, _, _)| project_path)
|
||||
.collect::<BTreeSet<_>>();
|
||||
|
||||
self.multibuffer.update(cx, |multibuffer, cx| {
|
||||
for buffer in multibuffer.all_buffers() {
|
||||
if let Some(file) = buffer.read(cx).file() {
|
||||
project_paths.insert(ProjectPath {
|
||||
path: file.path().clone(),
|
||||
worktree_id: file.worktree_id(cx),
|
||||
});
|
||||
}
|
||||
}
|
||||
multibuffer.clear(cx);
|
||||
});
|
||||
|
||||
self.paths_to_update = project_paths;
|
||||
});
|
||||
|
||||
self.update_stale_excerpts(RetainExcerpts::No, window, cx);
|
||||
self.update_stale_excerpts(window, cx);
|
||||
}
|
||||
|
||||
fn diagnostics_are_unchanged(
|
||||
@@ -511,7 +494,7 @@ impl ProjectDiagnosticsEditor {
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let was_empty = self.multibuffer.read(cx).is_empty();
|
||||
let buffer_snapshot = buffer.read(cx).snapshot();
|
||||
let mut buffer_snapshot = buffer.read(cx).snapshot();
|
||||
let buffer_id = buffer_snapshot.remote_id();
|
||||
|
||||
let max_severity = if self.include_warnings {
|
||||
@@ -559,6 +542,7 @@ impl ProjectDiagnosticsEditor {
|
||||
}
|
||||
let mut blocks: Vec<DiagnosticBlock> = Vec::new();
|
||||
|
||||
let diagnostics_toolbar_editor = Arc::new(this.clone());
|
||||
for (_, group) in grouped {
|
||||
let group_severity = group.iter().map(|d| d.diagnostic.severity).min();
|
||||
if group_severity.is_none_or(|s| s > max_severity) {
|
||||
@@ -568,7 +552,7 @@ impl ProjectDiagnosticsEditor {
|
||||
crate::diagnostic_renderer::DiagnosticRenderer::diagnostic_blocks_for_group(
|
||||
group,
|
||||
buffer_snapshot.remote_id(),
|
||||
Some(Arc::new(this.clone())),
|
||||
Some(diagnostics_toolbar_editor.clone()),
|
||||
cx,
|
||||
)
|
||||
})?;
|
||||
@@ -576,21 +560,37 @@ impl ProjectDiagnosticsEditor {
|
||||
blocks.extend(more);
|
||||
}
|
||||
|
||||
let mut excerpt_ranges: Vec<ExcerptRange<Point>> = match retain_excerpts {
|
||||
RetainExcerpts::No => Vec::new(),
|
||||
RetainExcerpts::Yes => this.update(cx, |this, cx| {
|
||||
this.multibuffer.update(cx, |multi_buffer, cx| {
|
||||
multi_buffer
|
||||
let cmp_excerpts = |buffer_snapshot: &BufferSnapshot,
|
||||
a: &ExcerptRange<text::Anchor>,
|
||||
b: &ExcerptRange<text::Anchor>| {
|
||||
let context_start = || a.context.start.cmp(&b.context.start, buffer_snapshot);
|
||||
let context_end = || a.context.end.cmp(&b.context.end, buffer_snapshot);
|
||||
let primary_start = || a.primary.start.cmp(&b.primary.start, buffer_snapshot);
|
||||
let primary_end = || a.primary.end.cmp(&b.primary.end, buffer_snapshot);
|
||||
context_start()
|
||||
.then_with(context_end)
|
||||
.then_with(primary_start)
|
||||
.then_with(primary_end)
|
||||
.then(cmp::Ordering::Greater)
|
||||
};
|
||||
|
||||
let mut excerpt_ranges: Vec<ExcerptRange<_>> = this.update(cx, |this, cx| {
|
||||
this.multibuffer.update(cx, |multi_buffer, cx| {
|
||||
let is_dirty = multi_buffer
|
||||
.buffer(buffer_id)
|
||||
.is_none_or(|buffer| buffer.read(cx).is_dirty());
|
||||
match retain_excerpts {
|
||||
RetainExcerpts::Dirty if !is_dirty => Vec::new(),
|
||||
RetainExcerpts::All | RetainExcerpts::Dirty => multi_buffer
|
||||
.excerpts_for_buffer(buffer_id, cx)
|
||||
.into_iter()
|
||||
.map(|(_, range)| ExcerptRange {
|
||||
context: range.context.to_point(&buffer_snapshot),
|
||||
primary: range.primary.to_point(&buffer_snapshot),
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
})?,
|
||||
};
|
||||
.map(|(_, range)| range)
|
||||
.sorted_by(|a, b| cmp_excerpts(&buffer_snapshot, a, b))
|
||||
.collect(),
|
||||
}
|
||||
})
|
||||
})?;
|
||||
|
||||
let mut result_blocks = vec![None; excerpt_ranges.len()];
|
||||
let context_lines = cx.update(|_, cx| multibuffer_context_lines(cx))?;
|
||||
for b in blocks {
|
||||
@@ -601,26 +601,17 @@ impl ProjectDiagnosticsEditor {
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
|
||||
buffer_snapshot = cx.update(|_, cx| buffer.read(cx).snapshot())?;
|
||||
let initial_range = buffer_snapshot.anchor_after(b.initial_range.start)
|
||||
..buffer_snapshot.anchor_before(b.initial_range.end);
|
||||
let excerpt_range = ExcerptRange {
|
||||
context: excerpt_range,
|
||||
primary: initial_range,
|
||||
};
|
||||
let i = excerpt_ranges
|
||||
.binary_search_by(|probe| {
|
||||
probe
|
||||
.context
|
||||
.start
|
||||
.cmp(&excerpt_range.start)
|
||||
.then(probe.context.end.cmp(&excerpt_range.end))
|
||||
.then(probe.primary.start.cmp(&b.initial_range.start))
|
||||
.then(probe.primary.end.cmp(&b.initial_range.end))
|
||||
.then(cmp::Ordering::Greater)
|
||||
})
|
||||
.binary_search_by(|probe| cmp_excerpts(&buffer_snapshot, probe, &excerpt_range))
|
||||
.unwrap_or_else(|i| i);
|
||||
excerpt_ranges.insert(
|
||||
i,
|
||||
ExcerptRange {
|
||||
context: excerpt_range,
|
||||
primary: b.initial_range.clone(),
|
||||
},
|
||||
);
|
||||
excerpt_ranges.insert(i, excerpt_range);
|
||||
result_blocks.insert(i, Some(b));
|
||||
}
|
||||
|
||||
@@ -633,6 +624,13 @@ impl ProjectDiagnosticsEditor {
|
||||
})
|
||||
}
|
||||
let (anchor_ranges, _) = this.multibuffer.update(cx, |multi_buffer, cx| {
|
||||
let excerpt_ranges = excerpt_ranges
|
||||
.into_iter()
|
||||
.map(|range| ExcerptRange {
|
||||
context: range.context.to_point(&buffer_snapshot),
|
||||
primary: range.primary.to_point(&buffer_snapshot),
|
||||
})
|
||||
.collect();
|
||||
multi_buffer.set_excerpt_ranges_for_path(
|
||||
PathKey::for_buffer(&buffer, cx),
|
||||
buffer.clone(),
|
||||
@@ -946,7 +944,7 @@ impl DiagnosticsToolbarEditor for WeakEntity<ProjectDiagnosticsEditor> {
|
||||
|
||||
fn refresh_diagnostics(&self, window: &mut Window, cx: &mut App) {
|
||||
let _ = self.update(cx, |project_diagnostics_editor, cx| {
|
||||
project_diagnostics_editor.update_all_excerpts(window, cx);
|
||||
project_diagnostics_editor.refresh(window, cx);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -978,8 +976,8 @@ async fn context_range_for_entry(
|
||||
context: u32,
|
||||
snapshot: BufferSnapshot,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Range<Point> {
|
||||
if let Some(rows) = heuristic_syntactic_expand(
|
||||
) -> Range<text::Anchor> {
|
||||
let range = if let Some(rows) = heuristic_syntactic_expand(
|
||||
range.clone(),
|
||||
DIAGNOSTIC_EXPANSION_ROW_LIMIT,
|
||||
snapshot.clone(),
|
||||
@@ -987,15 +985,17 @@ async fn context_range_for_entry(
|
||||
)
|
||||
.await
|
||||
{
|
||||
return Range {
|
||||
Range {
|
||||
start: Point::new(*rows.start(), 0),
|
||||
end: snapshot.clip_point(Point::new(*rows.end(), u32::MAX), Bias::Left),
|
||||
};
|
||||
}
|
||||
Range {
|
||||
start: Point::new(range.start.row.saturating_sub(context), 0),
|
||||
end: snapshot.clip_point(Point::new(range.end.row + context, u32::MAX), Bias::Left),
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Range {
|
||||
start: Point::new(range.start.row.saturating_sub(context), 0),
|
||||
end: snapshot.clip_point(Point::new(range.end.row + context, u32::MAX), Bias::Left),
|
||||
}
|
||||
};
|
||||
snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end)
|
||||
}
|
||||
|
||||
/// Expands the input range using syntax information from TreeSitter. This expansion will be limited
|
||||
|
||||
@@ -769,7 +769,7 @@ async fn test_random_diagnostics_blocks(cx: &mut TestAppContext, mut rng: StdRng
|
||||
|
||||
log::info!("updating mutated diagnostics view");
|
||||
mutated_diagnostics.update_in(cx, |diagnostics, window, cx| {
|
||||
diagnostics.update_stale_excerpts(RetainExcerpts::No, window, cx)
|
||||
diagnostics.update_stale_excerpts(window, cx)
|
||||
});
|
||||
|
||||
log::info!("constructing reference diagnostics view");
|
||||
@@ -968,7 +968,7 @@ async fn test_random_diagnostics_with_inlays(cx: &mut TestAppContext, mut rng: S
|
||||
|
||||
log::info!("updating mutated diagnostics view");
|
||||
mutated_diagnostics.update_in(cx, |diagnostics, window, cx| {
|
||||
diagnostics.update_stale_excerpts(RetainExcerpts::No, window, cx)
|
||||
diagnostics.update_stale_excerpts(window, cx)
|
||||
});
|
||||
|
||||
cx.executor()
|
||||
|
||||
@@ -24755,7 +24755,7 @@ fn render_diff_hunk_controls(
|
||||
is_created_file: bool,
|
||||
line_height: Pixels,
|
||||
editor: &Entity<Editor>,
|
||||
_window: &mut Window,
|
||||
_win: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> AnyElement {
|
||||
h_flex()
|
||||
@@ -24777,7 +24777,7 @@ fn render_diff_hunk_controls(
|
||||
.alpha(if status.is_pending() { 0.66 } else { 1.0 })
|
||||
.tooltip({
|
||||
let focus_handle = editor.focus_handle(cx);
|
||||
move |_window, cx| {
|
||||
move |_win, cx| {
|
||||
Tooltip::for_action_in(
|
||||
"Stage Hunk",
|
||||
&::git::ToggleStaged,
|
||||
|
||||
@@ -173,6 +173,10 @@ pub struct AgentServerManifestEntry {
|
||||
/// cmd = "node"
|
||||
/// args = ["index.js", "--port", "3000"]
|
||||
/// ```
|
||||
///
|
||||
/// Note: All commands are executed with the archive extraction directory as the
|
||||
/// working directory, so relative paths in args (like "index.js") will resolve
|
||||
/// relative to the extracted archive contents.
|
||||
pub targets: HashMap<String, TargetConfig>,
|
||||
}
|
||||
|
||||
|
||||
@@ -216,7 +216,7 @@ struct GitHeaderEntry {
|
||||
impl GitHeaderEntry {
|
||||
pub fn contains(&self, status_entry: &GitStatusEntry, repo: &Repository) -> bool {
|
||||
let this = &self.header;
|
||||
let status = status_entry.status;
|
||||
let status = status_entry.file_status(repo);
|
||||
match this {
|
||||
Section::Conflict => {
|
||||
repo.had_conflict_on_last_merge_head_change(&status_entry.repo_path)
|
||||
@@ -252,8 +252,6 @@ impl GitListEntry {
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub struct GitStatusEntry {
|
||||
pub(crate) repo_path: RepoPath,
|
||||
pub(crate) status: FileStatus,
|
||||
pub(crate) staging: StageStatus,
|
||||
}
|
||||
|
||||
impl GitStatusEntry {
|
||||
@@ -269,6 +267,20 @@ impl GitStatusEntry {
|
||||
.parent()
|
||||
.map(|parent| parent.display(path_style).to_string())
|
||||
}
|
||||
|
||||
pub(crate) fn file_status(&self, repo: &Repository) -> FileStatus {
|
||||
repo.snapshot()
|
||||
.status_for_path(&self.repo_path)
|
||||
.unwrap()
|
||||
.status
|
||||
}
|
||||
|
||||
fn staging(&self, panel: &GitPanel, repo: &Repository) -> StageStatus {
|
||||
if let Some(staging) = panel.entry_staging(&self.repo_path) {
|
||||
return staging;
|
||||
}
|
||||
self.file_status(repo).staging()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
@@ -279,6 +291,7 @@ enum TargetStatus {
|
||||
Unchanged,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct PendingOperation {
|
||||
finished: bool,
|
||||
target_status: TargetStatus,
|
||||
@@ -436,6 +449,7 @@ impl GitPanel {
|
||||
| RepositoryEvent::MergeHeadsChanged,
|
||||
true,
|
||||
) => {
|
||||
println!("Event::RepositoryUpdated with full_scan");
|
||||
this.schedule_update(true, window, cx);
|
||||
}
|
||||
GitStoreEvent::RepositoryUpdated(
|
||||
@@ -445,6 +459,7 @@ impl GitPanel {
|
||||
)
|
||||
| GitStoreEvent::RepositoryAdded
|
||||
| GitStoreEvent::RepositoryRemoved(_) => {
|
||||
println!("Event::RepositoryUpdated with full_scan");
|
||||
this.schedule_update(false, window, cx);
|
||||
}
|
||||
GitStoreEvent::IndexWriteError(error) => {
|
||||
@@ -814,11 +829,9 @@ impl GitPanel {
|
||||
) {
|
||||
maybe!({
|
||||
let entry = self.entries.get(self.selected_entry?)?.status_entry()?;
|
||||
let active_repo = self.active_repository.as_ref()?;
|
||||
let path = active_repo
|
||||
.read(cx)
|
||||
.repo_path_to_project_path(&entry.repo_path, cx)?;
|
||||
if entry.status.is_deleted() {
|
||||
let active_repo = self.active_repository.as_ref()?.read(cx);
|
||||
let path = active_repo.repo_path_to_project_path(&entry.repo_path, cx)?;
|
||||
if entry.file_status(active_repo).is_deleted() {
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -844,7 +857,10 @@ impl GitPanel {
|
||||
maybe!({
|
||||
let list_entry = self.entries.get(self.selected_entry?)?.clone();
|
||||
let entry = list_entry.status_entry()?.to_owned();
|
||||
let skip_prompt = action.skip_prompt || entry.status.is_created();
|
||||
let skip_prompt = action.skip_prompt
|
||||
|| entry
|
||||
.file_status(self.active_repository.as_ref().unwrap().read(cx))
|
||||
.is_created();
|
||||
|
||||
let prompt = if skip_prompt {
|
||||
Task::ready(Ok(0))
|
||||
@@ -893,7 +909,10 @@ impl GitPanel {
|
||||
let list_entry = self.entries.get(self.selected_entry?)?.clone();
|
||||
let entry = list_entry.status_entry()?.to_owned();
|
||||
|
||||
if !entry.status.is_created() {
|
||||
if !entry
|
||||
.file_status(self.active_repository.as_ref().unwrap().read(cx))
|
||||
.is_created()
|
||||
{
|
||||
return Some(());
|
||||
}
|
||||
|
||||
@@ -962,17 +981,16 @@ impl GitPanel {
|
||||
) {
|
||||
maybe!({
|
||||
let active_repo = self.active_repository.clone()?;
|
||||
let path = active_repo
|
||||
.read(cx)
|
||||
.repo_path_to_project_path(&entry.repo_path, cx)?;
|
||||
let repo = active_repo.read(cx);
|
||||
let path = repo.repo_path_to_project_path(&entry.repo_path, cx)?;
|
||||
let workspace = self.workspace.clone();
|
||||
|
||||
if entry.status.staging().has_staged() {
|
||||
if entry.staging(self, repo).has_staged() {
|
||||
self.change_file_stage(false, vec![entry.clone()], cx);
|
||||
}
|
||||
let filename = path.path.file_name()?.to_string();
|
||||
|
||||
if !entry.status.is_created() {
|
||||
if !entry.file_status(active_repo.read(cx)).is_created() {
|
||||
self.perform_checkout(vec![entry.clone()], window, cx);
|
||||
} else {
|
||||
let prompt = prompt(&format!("Trash {}?", filename), None, window, cx);
|
||||
@@ -1102,7 +1120,11 @@ impl GitPanel {
|
||||
.entries
|
||||
.iter()
|
||||
.filter_map(|entry| entry.status_entry().cloned())
|
||||
.filter(|status_entry| !status_entry.status.is_created())
|
||||
.filter(|status_entry| {
|
||||
!status_entry
|
||||
.file_status(self.active_repository.as_ref().unwrap().read(cx))
|
||||
.is_created()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
match entries.len() {
|
||||
@@ -1152,7 +1174,11 @@ impl GitPanel {
|
||||
.entries
|
||||
.iter()
|
||||
.filter_map(|entry| entry.status_entry())
|
||||
.filter(|status_entry| status_entry.status.is_created())
|
||||
.filter(|status_entry| {
|
||||
status_entry
|
||||
.file_status(self.active_repository.as_ref().unwrap().read(cx))
|
||||
.is_created()
|
||||
})
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
@@ -1198,11 +1224,17 @@ impl GitPanel {
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})?;
|
||||
let to_unstage = to_delete
|
||||
.into_iter()
|
||||
.filter(|entry| !entry.status.staging().is_fully_unstaged())
|
||||
.collect();
|
||||
this.update(cx, |this, cx| this.change_file_stage(false, to_unstage, cx))?;
|
||||
this.update(cx, |this, cx| {
|
||||
let to_unstage = to_delete
|
||||
.into_iter()
|
||||
.filter(|entry| {
|
||||
!entry
|
||||
.staging(this, active_repo.read(cx))
|
||||
.is_fully_unstaged()
|
||||
})
|
||||
.collect();
|
||||
this.change_file_stage(false, to_unstage, cx)
|
||||
})?;
|
||||
for task in tasks {
|
||||
task.await?;
|
||||
}
|
||||
@@ -1218,7 +1250,11 @@ impl GitPanel {
|
||||
.entries
|
||||
.iter()
|
||||
.filter_map(|entry| entry.status_entry())
|
||||
.filter(|status_entry| status_entry.staging.has_unstaged())
|
||||
.filter(|status_entry| {
|
||||
status_entry
|
||||
.staging(self, self.active_repository.as_ref().unwrap().read(cx))
|
||||
.has_unstaged()
|
||||
})
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
self.change_file_stage(true, entries, cx);
|
||||
@@ -1229,7 +1265,11 @@ impl GitPanel {
|
||||
.entries
|
||||
.iter()
|
||||
.filter_map(|entry| entry.status_entry())
|
||||
.filter(|status_entry| status_entry.staging.has_staged())
|
||||
.filter(|status_entry| {
|
||||
status_entry
|
||||
.staging(self, self.active_repository.as_ref().unwrap().read(cx))
|
||||
.has_staged()
|
||||
})
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
self.change_file_stage(false, entries, cx);
|
||||
@@ -1241,15 +1281,18 @@ impl GitPanel {
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
println!(" toggle_staged_for_entry");
|
||||
let Some(active_repository) = self.active_repository.as_ref() else {
|
||||
return;
|
||||
};
|
||||
let repo = active_repository.read(cx);
|
||||
let (stage, repo_paths) = match entry {
|
||||
GitListEntry::Status(status_entry) => {
|
||||
let repo_paths = vec![status_entry.clone()];
|
||||
let stage = if let Some(status) = self.entry_staging(&status_entry) {
|
||||
!status.is_fully_staged()
|
||||
} else if status_entry.status.staging().is_fully_staged() {
|
||||
println!(" {}", status_entry.repo_path.as_unix_str());
|
||||
println!(" >>> pending_ops: {:?}", self.pending);
|
||||
println!(" >>> entry: {:?}", status_entry);
|
||||
let stage = if status_entry.staging(self, repo).is_fully_staged() {
|
||||
if let Some(op) = self.bulk_staging.clone()
|
||||
&& op.anchor == status_entry.repo_path
|
||||
{
|
||||
@@ -1260,18 +1303,18 @@ impl GitPanel {
|
||||
self.set_bulk_staging_anchor(status_entry.repo_path.clone(), cx);
|
||||
true
|
||||
};
|
||||
println!(" >>> stage={stage}");
|
||||
(stage, repo_paths)
|
||||
}
|
||||
GitListEntry::Header(section) => {
|
||||
let goal_staged_state = !self.header_state(section.header).selected();
|
||||
let repository = active_repository.read(cx);
|
||||
let entries = self
|
||||
.entries
|
||||
.iter()
|
||||
.filter_map(|entry| entry.status_entry())
|
||||
.filter(|status_entry| {
|
||||
section.contains(status_entry, repository)
|
||||
&& status_entry.staging.as_bool() != Some(goal_staged_state)
|
||||
section.contains(status_entry, repo)
|
||||
&& status_entry.staging(self, repo).as_bool() != Some(goal_staged_state)
|
||||
})
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
@@ -1455,7 +1498,9 @@ impl GitPanel {
|
||||
let Some(status_entry) = selected_entry.status_entry() else {
|
||||
return;
|
||||
};
|
||||
if status_entry.staging != StageStatus::Staged {
|
||||
if status_entry.staging(self, self.active_repository.as_ref().unwrap().read(cx))
|
||||
!= StageStatus::Staged
|
||||
{
|
||||
self.change_file_stage(true, vec![status_entry.clone()], cx);
|
||||
}
|
||||
}
|
||||
@@ -1472,7 +1517,9 @@ impl GitPanel {
|
||||
let Some(status_entry) = selected_entry.status_entry() else {
|
||||
return;
|
||||
};
|
||||
if status_entry.staging != StageStatus::Unstaged {
|
||||
if status_entry.staging(self, self.active_repository.as_ref().unwrap().read(cx))
|
||||
!= StageStatus::Unstaged
|
||||
{
|
||||
self.change_file_stage(false, vec![status_entry.clone()], cx);
|
||||
}
|
||||
}
|
||||
@@ -1649,7 +1696,11 @@ impl GitPanel {
|
||||
.entries
|
||||
.iter()
|
||||
.filter_map(|entry| entry.status_entry())
|
||||
.filter(|status_entry| !status_entry.status.is_created())
|
||||
.filter(|status_entry| {
|
||||
!status_entry
|
||||
.file_status(active_repository.read(cx))
|
||||
.is_created()
|
||||
})
|
||||
.map(|status_entry| status_entry.repo_path.clone())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
@@ -1794,11 +1845,20 @@ impl GitPanel {
|
||||
None
|
||||
}?;
|
||||
|
||||
let action_text = if git_status_entry.status.is_deleted() {
|
||||
let action_text = if git_status_entry
|
||||
.file_status(self.active_repository.as_ref().unwrap().read(cx))
|
||||
.is_deleted()
|
||||
{
|
||||
Some("Delete")
|
||||
} else if git_status_entry.status.is_created() {
|
||||
} else if git_status_entry
|
||||
.file_status(self.active_repository.as_ref().unwrap().read(cx))
|
||||
.is_created()
|
||||
{
|
||||
Some("Create")
|
||||
} else if git_status_entry.status.is_modified() {
|
||||
} else if git_status_entry
|
||||
.file_status(self.active_repository.as_ref().unwrap().read(cx))
|
||||
.is_modified()
|
||||
{
|
||||
Some("Update")
|
||||
} else {
|
||||
None
|
||||
@@ -2582,7 +2642,7 @@ impl GitPanel {
|
||||
let handle = cx.entity().downgrade();
|
||||
self.reopen_commit_buffer(window, cx);
|
||||
self.update_visible_entries_task = cx.spawn_in(window, async move |_, cx| {
|
||||
cx.background_executor().timer(UPDATE_DEBOUNCE).await;
|
||||
// cx.background_executor().timer(UPDATE_DEBOUNCE).await;
|
||||
if let Some(git_panel) = handle.upgrade() {
|
||||
git_panel
|
||||
.update_in(cx, |git_panel, window, cx| {
|
||||
@@ -2638,10 +2698,12 @@ impl GitPanel {
|
||||
}
|
||||
|
||||
fn clear_pending(&mut self) {
|
||||
println!("*** CLEAR ***");
|
||||
self.pending.retain(|v| !v.finished)
|
||||
}
|
||||
|
||||
fn update_visible_entries(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
println!("*** update_visible_entries ***");
|
||||
let path_style = self.project.read(cx).path_style(cx);
|
||||
let bulk_staging = self.bulk_staging.take();
|
||||
let last_staged_path_prev_index = bulk_staging
|
||||
@@ -2693,8 +2755,6 @@ impl GitPanel {
|
||||
|
||||
let entry = GitStatusEntry {
|
||||
repo_path: entry.repo_path.clone(),
|
||||
status: entry.status,
|
||||
staging,
|
||||
};
|
||||
|
||||
if staging.has_staged() {
|
||||
@@ -2800,7 +2860,7 @@ impl GitPanel {
|
||||
&& let Some(index) = bulk_staging_anchor_new_index
|
||||
&& let Some(entry) = self.entries.get(index)
|
||||
&& let Some(entry) = entry.status_entry()
|
||||
&& self.entry_staging(entry).unwrap_or(entry.staging) == StageStatus::Staged
|
||||
&& entry.staging(self, repo) == StageStatus::Staged
|
||||
{
|
||||
self.bulk_staging = bulk_staging;
|
||||
}
|
||||
@@ -2848,38 +2908,26 @@ impl GitPanel {
|
||||
self.entry_count += 1;
|
||||
if repo.had_conflict_on_last_merge_head_change(&status_entry.repo_path) {
|
||||
self.conflicted_count += 1;
|
||||
if self
|
||||
.entry_staging(status_entry)
|
||||
.unwrap_or(status_entry.staging)
|
||||
.has_staged()
|
||||
{
|
||||
if status_entry.staging(self, repo).has_staged() {
|
||||
self.conflicted_staged_count += 1;
|
||||
}
|
||||
} else if status_entry.status.is_created() {
|
||||
} else if status_entry.file_status(repo).is_created() {
|
||||
self.new_count += 1;
|
||||
if self
|
||||
.entry_staging(status_entry)
|
||||
.unwrap_or(status_entry.staging)
|
||||
.has_staged()
|
||||
{
|
||||
if status_entry.staging(self, repo).has_staged() {
|
||||
self.new_staged_count += 1;
|
||||
}
|
||||
} else {
|
||||
self.tracked_count += 1;
|
||||
if self
|
||||
.entry_staging(status_entry)
|
||||
.unwrap_or(status_entry.staging)
|
||||
.has_staged()
|
||||
{
|
||||
if status_entry.staging(self, repo).has_staged() {
|
||||
self.tracked_staged_count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn entry_staging(&self, entry: &GitStatusEntry) -> Option<StageStatus> {
|
||||
fn entry_staging(&self, repo_path: &RepoPath) -> Option<StageStatus> {
|
||||
for pending in self.pending.iter().rev() {
|
||||
if pending.contains_path(&entry.repo_path) {
|
||||
if pending.contains_path(repo_path) {
|
||||
match pending.target_status {
|
||||
TargetStatus::Staged => return Some(StageStatus::Staged),
|
||||
TargetStatus::Unstaged => return Some(StageStatus::Unstaged),
|
||||
@@ -3729,7 +3777,7 @@ impl GitPanel {
|
||||
let entry = self.entries.get(ix)?;
|
||||
|
||||
let status = entry.status_entry()?;
|
||||
let entry_staging = self.entry_staging(status).unwrap_or(status.staging);
|
||||
let entry_staging = status.staging(self, repo);
|
||||
|
||||
let checkbox = Checkbox::new("stage-file", entry_staging.as_bool().into())
|
||||
.disabled(!self.has_write_access(cx))
|
||||
@@ -3767,6 +3815,7 @@ impl GitPanel {
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
let entry_count = self.entries.len();
|
||||
println!("render_entries");
|
||||
|
||||
v_flex()
|
||||
.flex_1()
|
||||
@@ -3900,12 +3949,16 @@ impl GitPanel {
|
||||
let Some(entry) = self.entries.get(ix).and_then(|e| e.status_entry()) else {
|
||||
return;
|
||||
};
|
||||
let stage_title = if entry.status.staging().is_fully_staged() {
|
||||
let file_status = entry.file_status(self.active_repository.as_ref().unwrap().read(cx));
|
||||
let stage_title = if entry
|
||||
.staging(self, self.active_repository.as_ref().unwrap().read(cx))
|
||||
.is_fully_staged()
|
||||
{
|
||||
"Unstage File"
|
||||
} else {
|
||||
"Stage File"
|
||||
};
|
||||
let restore_title = if entry.status.is_created() {
|
||||
let restore_title = if file_status.is_created() {
|
||||
"Trash File"
|
||||
} else {
|
||||
"Restore File"
|
||||
@@ -3916,7 +3969,7 @@ impl GitPanel {
|
||||
.action(stage_title, ToggleStaged.boxed_clone())
|
||||
.action(restore_title, git::RestoreFile::default().boxed_clone());
|
||||
|
||||
if entry.status.is_created() {
|
||||
if file_status.is_created() {
|
||||
context_menu =
|
||||
context_menu.action("Add to .gitignore", git::AddToGitignore.boxed_clone());
|
||||
}
|
||||
@@ -3984,13 +4037,14 @@ impl GitPanel {
|
||||
window: &Window,
|
||||
cx: &Context<Self>,
|
||||
) -> AnyElement {
|
||||
println!(" render_entry");
|
||||
let path_style = self.project.read(cx).path_style(cx);
|
||||
let display_name = entry.display_name(path_style);
|
||||
|
||||
let selected = self.selected_entry == Some(ix);
|
||||
let marked = self.marked_entries.contains(&ix);
|
||||
let status_style = GitPanelSettings::get_global(cx).status_style;
|
||||
let status = entry.status;
|
||||
let status = entry.file_status(self.active_repository.as_ref().unwrap().read(cx));
|
||||
|
||||
let has_conflict = status.is_conflicted();
|
||||
let is_modified = status.is_modified();
|
||||
@@ -4023,11 +4077,17 @@ impl GitPanel {
|
||||
let checkbox_id: ElementId =
|
||||
ElementId::Name(format!("entry_{}_{}_checkbox", display_name, ix).into());
|
||||
|
||||
let entry_staging = self.entry_staging(entry).unwrap_or(entry.staging);
|
||||
let entry_staging = entry.staging(self, self.active_repository.as_ref().unwrap().read(cx));
|
||||
let mut is_staged: ToggleState = entry_staging.as_bool().into();
|
||||
if self.show_placeholders && !self.has_staged_changes() && !entry.status.is_created() {
|
||||
if self.show_placeholders
|
||||
&& !self.has_staged_changes()
|
||||
&& !entry
|
||||
.file_status(self.active_repository.as_ref().unwrap().read(cx))
|
||||
.is_created()
|
||||
{
|
||||
is_staged = ToggleState::Selected;
|
||||
}
|
||||
println!(" Checkbox status = {is_staged:?}, entry.staging = {entry_staging:?}");
|
||||
|
||||
let handle = cx.weak_entity();
|
||||
|
||||
@@ -4124,6 +4184,14 @@ impl GitPanel {
|
||||
let entry = entry.clone();
|
||||
let this = cx.weak_entity();
|
||||
move |_, click, window, cx| {
|
||||
println!(
|
||||
" {} - *** Clicked - checkbox {} ***",
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs(),
|
||||
entry_staging.is_fully_staged()
|
||||
);
|
||||
this.update(cx, |this, cx| {
|
||||
if !has_write_access {
|
||||
return;
|
||||
|
||||
@@ -335,7 +335,7 @@ impl ProjectDiff {
|
||||
return;
|
||||
};
|
||||
let repo = git_repo.read(cx);
|
||||
let sort_prefix = sort_prefix(repo, &entry.repo_path, entry.status, cx);
|
||||
let sort_prefix = sort_prefix(repo, &entry.repo_path, entry.file_status(repo), cx);
|
||||
let path_key = PathKey::with_sort_prefix(sort_prefix, entry.repo_path.0);
|
||||
|
||||
self.move_to_path(path_key, window, cx)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use std::{fs, path::Path, sync::Arc};
|
||||
|
||||
use crate::{
|
||||
App, Bounds, Element, GlobalElementId, Hitbox, InspectorElementId, InteractiveElement,
|
||||
App, Asset, Bounds, Element, GlobalElementId, Hitbox, InspectorElementId, InteractiveElement,
|
||||
Interactivity, IntoElement, LayoutId, Pixels, Point, Radians, SharedString, Size,
|
||||
StyleRefinement, Styled, TransformationMatrix, Window, geometry::Negate as _, point, px,
|
||||
radians, size,
|
||||
@@ -11,6 +13,7 @@ pub struct Svg {
|
||||
interactivity: Interactivity,
|
||||
transformation: Option<Transformation>,
|
||||
path: Option<SharedString>,
|
||||
external_path: Option<SharedString>,
|
||||
}
|
||||
|
||||
/// Create a new SVG element.
|
||||
@@ -20,6 +23,7 @@ pub fn svg() -> Svg {
|
||||
interactivity: Interactivity::new(),
|
||||
transformation: None,
|
||||
path: None,
|
||||
external_path: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +34,12 @@ impl Svg {
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the path to the SVG file for this element.
|
||||
pub fn external_path(mut self, path: impl Into<SharedString>) -> Self {
|
||||
self.external_path = Some(path.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Transform the SVG element with the given transformation.
|
||||
/// Note that this won't effect the hitbox or layout of the element, only the rendering.
|
||||
pub fn with_transformation(mut self, transformation: Transformation) -> Self {
|
||||
@@ -117,7 +127,35 @@ impl Element for Svg {
|
||||
.unwrap_or_default();
|
||||
|
||||
window
|
||||
.paint_svg(bounds, path.clone(), transformation, color, cx)
|
||||
.paint_svg(bounds, path.clone(), None, transformation, color, cx)
|
||||
.log_err();
|
||||
} else if let Some((path, color)) =
|
||||
self.external_path.as_ref().zip(style.text.color)
|
||||
{
|
||||
let Some(bytes) = window
|
||||
.use_asset::<SvgAsset>(path, cx)
|
||||
.and_then(|asset| asset.log_err())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let transformation = self
|
||||
.transformation
|
||||
.as_ref()
|
||||
.map(|transformation| {
|
||||
transformation.into_matrix(bounds.center(), window.scale_factor())
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
window
|
||||
.paint_svg(
|
||||
bounds,
|
||||
path.clone(),
|
||||
Some(&bytes),
|
||||
transformation,
|
||||
color,
|
||||
cx,
|
||||
)
|
||||
.log_err();
|
||||
}
|
||||
},
|
||||
@@ -219,3 +257,21 @@ impl Transformation {
|
||||
.translate(center.scale(scale_factor).negate())
|
||||
}
|
||||
}
|
||||
|
||||
enum SvgAsset {}
|
||||
|
||||
impl Asset for SvgAsset {
|
||||
type Source = SharedString;
|
||||
type Output = Result<Arc<[u8]>, Arc<std::io::Error>>;
|
||||
|
||||
fn load(
|
||||
source: Self::Source,
|
||||
_cx: &mut App,
|
||||
) -> impl Future<Output = Self::Output> + Send + 'static {
|
||||
async move {
|
||||
let bytes = fs::read(Path::new(source.as_ref())).map_err(|e| Arc::new(e))?;
|
||||
let bytes = Arc::from(bytes);
|
||||
Ok(bytes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -281,7 +281,7 @@ impl BackgroundExecutor {
|
||||
});
|
||||
let mut cx = std::task::Context::from_waker(&waker);
|
||||
|
||||
let duration = Duration::from_secs(500);
|
||||
let duration = Duration::from_secs(180);
|
||||
let mut test_should_end_by = Instant::now() + duration;
|
||||
|
||||
loop {
|
||||
|
||||
@@ -231,7 +231,9 @@ impl PlatformTextSystem for DirectWriteTextSystem {
|
||||
Ok(*font_id)
|
||||
} else {
|
||||
let mut lock = RwLockUpgradableReadGuard::upgrade(lock);
|
||||
let font_id = lock.select_font(font);
|
||||
let font_id = lock
|
||||
.select_font(font)
|
||||
.with_context(|| format!("Failed to select font: {:?}", font))?;
|
||||
lock.font_selections.insert(font.clone(), font_id);
|
||||
Ok(font_id)
|
||||
}
|
||||
@@ -457,7 +459,7 @@ impl DirectWriteState {
|
||||
}
|
||||
}
|
||||
|
||||
fn select_font(&mut self, target_font: &Font) -> FontId {
|
||||
fn select_font(&mut self, target_font: &Font) -> Option<FontId> {
|
||||
unsafe {
|
||||
if target_font.family == ".SystemUIFont" {
|
||||
let family = self.system_ui_font_name.clone();
|
||||
@@ -468,7 +470,6 @@ impl DirectWriteState {
|
||||
&target_font.features,
|
||||
target_font.fallbacks.as_ref(),
|
||||
)
|
||||
.unwrap()
|
||||
} else {
|
||||
let family = self.system_ui_font_name.clone();
|
||||
self.find_font_id(
|
||||
@@ -478,7 +479,7 @@ impl DirectWriteState {
|
||||
&target_font.features,
|
||||
target_font.fallbacks.as_ref(),
|
||||
)
|
||||
.unwrap_or_else(|| {
|
||||
.or_else(|| {
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
{
|
||||
panic!("ERROR: {} font not found!", target_font.family);
|
||||
@@ -494,7 +495,6 @@ impl DirectWriteState {
|
||||
target_font.fallbacks.as_ref(),
|
||||
true,
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1479,8 +1479,10 @@ impl IDWriteTextRenderer_Impl for TextRenderer_Impl {
|
||||
.get(&font_identifier)
|
||||
{
|
||||
*id
|
||||
} else if let Some(id) = context.text_system.select_font(&font_struct) {
|
||||
id
|
||||
} else {
|
||||
context.text_system.select_font(&font_struct)
|
||||
return Err(Error::new(DWRITE_E_NOFONT, "Failed to select font"));
|
||||
};
|
||||
|
||||
let glyph_ids = unsafe { std::slice::from_raw_parts(glyphrun.glyphIndices, glyph_count) };
|
||||
|
||||
@@ -1370,7 +1370,7 @@ fn should_prefer_character_input(vkey: VIRTUAL_KEY, scan_code: u16) -> bool {
|
||||
scan_code as u32,
|
||||
Some(&keyboard_state),
|
||||
&mut buffer_c,
|
||||
0x4,
|
||||
0x5,
|
||||
)
|
||||
};
|
||||
if result_c < 0 {
|
||||
@@ -1415,7 +1415,7 @@ fn should_prefer_character_input(vkey: VIRTUAL_KEY, scan_code: u16) -> bool {
|
||||
scan_code as u32,
|
||||
Some(&state_no_modifiers),
|
||||
&mut buffer_c_no_modifiers,
|
||||
0x4,
|
||||
0x5,
|
||||
)
|
||||
};
|
||||
if result_c_no_modifiers <= 0 {
|
||||
|
||||
@@ -452,8 +452,9 @@ impl WindowsWindow {
|
||||
|
||||
// Failure to create a `WindowsWindowState` can cause window creation to fail,
|
||||
// so check the inner result first.
|
||||
let this = context.inner.take().unwrap()?;
|
||||
let this = context.inner.take().transpose()?;
|
||||
let hwnd = creation_result?;
|
||||
let this = this.unwrap();
|
||||
|
||||
register_drag_drop(&this)?;
|
||||
configure_dwm_dark_mode(hwnd, appearance);
|
||||
@@ -900,9 +901,9 @@ impl IDropTarget_Impl for WindowsDragDropHandler_Impl {
|
||||
if idata.u.hGlobal.is_invalid() {
|
||||
return Ok(());
|
||||
}
|
||||
let hdrop = idata.u.hGlobal.0 as *mut HDROP;
|
||||
let hdrop = HDROP(idata.u.hGlobal.0);
|
||||
let mut paths = SmallVec::<[PathBuf; 2]>::new();
|
||||
with_file_names(*hdrop, |file_name| {
|
||||
with_file_names(hdrop, |file_name| {
|
||||
if let Some(path) = PathBuf::from_str(&file_name).log_err() {
|
||||
paths.push(path);
|
||||
}
|
||||
|
||||
@@ -95,27 +95,34 @@ impl SvgRenderer {
|
||||
pub(crate) fn render_alpha_mask(
|
||||
&self,
|
||||
params: &RenderSvgParams,
|
||||
bytes: Option<&[u8]>,
|
||||
) -> Result<Option<(Size<DevicePixels>, Vec<u8>)>> {
|
||||
anyhow::ensure!(!params.size.is_zero(), "can't render at a zero size");
|
||||
|
||||
// Load the tree.
|
||||
let Some(bytes) = self.asset_source.load(¶ms.path)? else {
|
||||
return Ok(None);
|
||||
let render_pixmap = |bytes| {
|
||||
let pixmap = self.render_pixmap(bytes, SvgSize::Size(params.size))?;
|
||||
|
||||
// Convert the pixmap's pixels into an alpha mask.
|
||||
let size = Size::new(
|
||||
DevicePixels(pixmap.width() as i32),
|
||||
DevicePixels(pixmap.height() as i32),
|
||||
);
|
||||
let alpha_mask = pixmap
|
||||
.pixels()
|
||||
.iter()
|
||||
.map(|p| p.alpha())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Ok(Some((size, alpha_mask)))
|
||||
};
|
||||
|
||||
let pixmap = self.render_pixmap(&bytes, SvgSize::Size(params.size))?;
|
||||
|
||||
// Convert the pixmap's pixels into an alpha mask.
|
||||
let size = Size::new(
|
||||
DevicePixels(pixmap.width() as i32),
|
||||
DevicePixels(pixmap.height() as i32),
|
||||
);
|
||||
let alpha_mask = pixmap
|
||||
.pixels()
|
||||
.iter()
|
||||
.map(|p| p.alpha())
|
||||
.collect::<Vec<_>>();
|
||||
Ok(Some((size, alpha_mask)))
|
||||
if let Some(bytes) = bytes {
|
||||
render_pixmap(bytes)
|
||||
} else if let Some(bytes) = self.asset_source.load(¶ms.path)? {
|
||||
render_pixmap(&bytes)
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
fn render_pixmap(&self, bytes: &[u8], size: SvgSize) -> Result<Pixmap, usvg::Error> {
|
||||
|
||||
@@ -3084,6 +3084,7 @@ impl Window {
|
||||
&mut self,
|
||||
bounds: Bounds<Pixels>,
|
||||
path: SharedString,
|
||||
mut data: Option<&[u8]>,
|
||||
transformation: TransformationMatrix,
|
||||
color: Hsla,
|
||||
cx: &App,
|
||||
@@ -3104,7 +3105,8 @@ impl Window {
|
||||
let Some(tile) =
|
||||
self.sprite_atlas
|
||||
.get_or_insert_with(¶ms.clone().into(), &mut || {
|
||||
let Some((size, bytes)) = cx.svg_renderer.render_alpha_mask(¶ms)? else {
|
||||
let Some((size, bytes)) = cx.svg_renderer.render_alpha_mask(¶ms, data)?
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
Ok(Some((size, Cow::Owned(bytes))))
|
||||
|
||||
@@ -2324,17 +2324,19 @@ impl CodeLabel {
|
||||
}
|
||||
|
||||
pub fn plain(text: String, filter_text: Option<&str>) -> Self {
|
||||
Self::filtered(text, filter_text, Vec::new())
|
||||
Self::filtered(text.clone(), text.len(), filter_text, Vec::new())
|
||||
}
|
||||
|
||||
pub fn filtered(
|
||||
text: String,
|
||||
label_len: usize,
|
||||
filter_text: Option<&str>,
|
||||
runs: Vec<(Range<usize>, HighlightId)>,
|
||||
) -> Self {
|
||||
assert!(label_len <= text.len());
|
||||
let filter_range = filter_text
|
||||
.and_then(|filter| text.find(filter).map(|ix| ix..ix + filter.len()))
|
||||
.unwrap_or(0..text.len());
|
||||
.unwrap_or(0..label_len);
|
||||
Self::new(text, filter_range, runs)
|
||||
}
|
||||
|
||||
|
||||
@@ -406,6 +406,7 @@ impl LspAdapter for PyrightLspAdapter {
|
||||
language: &Arc<language::Language>,
|
||||
) -> Option<language::CodeLabel> {
|
||||
let label = &item.label;
|
||||
let label_len = label.len();
|
||||
let grammar = language.grammar()?;
|
||||
let highlight_id = match item.kind? {
|
||||
lsp::CompletionItemKind::METHOD => grammar.highlight_id_for_name("function.method"),
|
||||
@@ -427,9 +428,10 @@ impl LspAdapter for PyrightLspAdapter {
|
||||
}
|
||||
Some(language::CodeLabel::filtered(
|
||||
text,
|
||||
label_len,
|
||||
item.filter_text.as_deref(),
|
||||
highlight_id
|
||||
.map(|id| (0..label.len(), id))
|
||||
.map(|id| (0..label_len, id))
|
||||
.into_iter()
|
||||
.collect(),
|
||||
))
|
||||
@@ -1467,6 +1469,7 @@ impl LspAdapter for PyLspAdapter {
|
||||
language: &Arc<language::Language>,
|
||||
) -> Option<language::CodeLabel> {
|
||||
let label = &item.label;
|
||||
let label_len = label.len();
|
||||
let grammar = language.grammar()?;
|
||||
let highlight_id = match item.kind? {
|
||||
lsp::CompletionItemKind::METHOD => grammar.highlight_id_for_name("function.method")?,
|
||||
@@ -1477,6 +1480,7 @@ impl LspAdapter for PyLspAdapter {
|
||||
};
|
||||
Some(language::CodeLabel::filtered(
|
||||
label.clone(),
|
||||
label_len,
|
||||
item.filter_text.as_deref(),
|
||||
vec![(0..label.len(), highlight_id)],
|
||||
))
|
||||
@@ -1742,6 +1746,7 @@ impl LspAdapter for BasedPyrightLspAdapter {
|
||||
language: &Arc<language::Language>,
|
||||
) -> Option<language::CodeLabel> {
|
||||
let label = &item.label;
|
||||
let label_len = label.len();
|
||||
let grammar = language.grammar()?;
|
||||
let highlight_id = match item.kind? {
|
||||
lsp::CompletionItemKind::METHOD => grammar.highlight_id_for_name("function.method"),
|
||||
@@ -1763,6 +1768,7 @@ impl LspAdapter for BasedPyrightLspAdapter {
|
||||
}
|
||||
Some(language::CodeLabel::filtered(
|
||||
text,
|
||||
label_len,
|
||||
item.filter_text.as_deref(),
|
||||
highlight_id
|
||||
.map(|id| (0..label.len(), id))
|
||||
|
||||
@@ -754,7 +754,7 @@ impl LspAdapter for TypeScriptLspAdapter {
|
||||
language: &Arc<language::Language>,
|
||||
) -> Option<language::CodeLabel> {
|
||||
use lsp::CompletionItemKind as Kind;
|
||||
let len = item.label.len();
|
||||
let label_len = item.label.len();
|
||||
let grammar = language.grammar()?;
|
||||
let highlight_id = match item.kind? {
|
||||
Kind::CLASS | Kind::INTERFACE | Kind::ENUM => grammar.highlight_id_for_name("type"),
|
||||
@@ -779,8 +779,9 @@ impl LspAdapter for TypeScriptLspAdapter {
|
||||
};
|
||||
Some(language::CodeLabel::filtered(
|
||||
text,
|
||||
label_len,
|
||||
item.filter_text.as_deref(),
|
||||
vec![(0..len, highlight_id)],
|
||||
vec![(0..label_len, highlight_id)],
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
@@ -178,7 +178,7 @@ impl LspAdapter for VtslsLspAdapter {
|
||||
language: &Arc<language::Language>,
|
||||
) -> Option<language::CodeLabel> {
|
||||
use lsp::CompletionItemKind as Kind;
|
||||
let len = item.label.len();
|
||||
let label_len = item.label.len();
|
||||
let grammar = language.grammar()?;
|
||||
let highlight_id = match item.kind? {
|
||||
Kind::CLASS | Kind::INTERFACE | Kind::ENUM => grammar.highlight_id_for_name("type"),
|
||||
@@ -203,8 +203,9 @@ impl LspAdapter for VtslsLspAdapter {
|
||||
};
|
||||
Some(language::CodeLabel::filtered(
|
||||
text,
|
||||
label_len,
|
||||
item.filter_text.as_deref(),
|
||||
vec![(0..len, highlight_id)],
|
||||
vec![(0..label_len, highlight_id)],
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
@@ -1148,9 +1148,9 @@ impl MultiBuffer {
|
||||
let mut counts: Vec<usize> = Vec::new();
|
||||
for range in expanded_ranges {
|
||||
if let Some(last_range) = merged_ranges.last_mut() {
|
||||
debug_assert!(
|
||||
assert!(
|
||||
last_range.context.start <= range.context.start,
|
||||
"Last range: {last_range:?} Range: {range:?}"
|
||||
"ranges must be sorted: {last_range:?} <= {range:?}"
|
||||
);
|
||||
if last_range.context.end >= range.context.start
|
||||
|| last_range.context.end.row + 1 == range.context.start.row
|
||||
|
||||
@@ -172,7 +172,7 @@ impl MultiBuffer {
|
||||
.into_iter()
|
||||
.chunk_by(|id| self.paths_by_excerpt.get(id).cloned())
|
||||
.into_iter()
|
||||
.flat_map(|(k, v)| Some((k?, v.into_iter().collect::<Vec<_>>())))
|
||||
.filter_map(|(k, v)| Some((k?, v.into_iter().collect::<Vec<_>>())))
|
||||
.collect::<Vec<_>>();
|
||||
let snapshot = self.snapshot(cx);
|
||||
|
||||
@@ -280,7 +280,7 @@ impl MultiBuffer {
|
||||
.excerpts_by_path
|
||||
.range(..path.clone())
|
||||
.next_back()
|
||||
.map(|(_, value)| *value.last().unwrap())
|
||||
.and_then(|(_, value)| value.last().copied())
|
||||
.unwrap_or(ExcerptId::min());
|
||||
|
||||
let existing = self
|
||||
@@ -299,6 +299,7 @@ impl MultiBuffer {
|
||||
let snapshot = self.snapshot(cx);
|
||||
|
||||
let mut next_excerpt_id =
|
||||
// is this right? What if we remove the last excerpt, then we might reallocate with a wrong mapping?
|
||||
if let Some(last_entry) = self.snapshot.borrow().excerpt_ids.last() {
|
||||
last_entry.id.0 + 1
|
||||
} else {
|
||||
@@ -311,20 +312,16 @@ impl MultiBuffer {
|
||||
excerpts_cursor.next();
|
||||
|
||||
loop {
|
||||
let new = new_iter.peek();
|
||||
let existing = if let Some(existing_id) = existing_iter.peek() {
|
||||
let locator = snapshot.excerpt_locator_for_id(*existing_id);
|
||||
let existing = if let Some(&existing_id) = existing_iter.peek() {
|
||||
let locator = snapshot.excerpt_locator_for_id(existing_id);
|
||||
excerpts_cursor.seek_forward(&Some(locator), Bias::Left);
|
||||
if let Some(excerpt) = excerpts_cursor.item() {
|
||||
if excerpt.buffer_id != buffer_snapshot.remote_id() {
|
||||
to_remove.push(*existing_id);
|
||||
to_remove.push(existing_id);
|
||||
existing_iter.next();
|
||||
continue;
|
||||
}
|
||||
Some((
|
||||
*existing_id,
|
||||
excerpt.range.context.to_point(buffer_snapshot),
|
||||
))
|
||||
Some((existing_id, excerpt.range.context.to_point(buffer_snapshot)))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -332,6 +329,7 @@ impl MultiBuffer {
|
||||
None
|
||||
};
|
||||
|
||||
let new = new_iter.peek();
|
||||
if let Some((last_id, last)) = to_insert.last_mut() {
|
||||
if let Some(new) = new
|
||||
&& last.context.end >= new.context.start
|
||||
|
||||
@@ -438,6 +438,13 @@ impl AgentServerStore {
|
||||
cx.emit(AgentServersUpdated);
|
||||
}
|
||||
|
||||
pub fn node_runtime(&self) -> Option<NodeRuntime> {
|
||||
match &self.state {
|
||||
AgentServerStoreState::Local { node_runtime, .. } => Some(node_runtime.clone()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn local(
|
||||
node_runtime: NodeRuntime,
|
||||
fs: Arc<dyn Fs>,
|
||||
@@ -1560,7 +1567,7 @@ impl ExternalAgentServer for LocalExtensionArchiveAgent {
|
||||
env: Some(env),
|
||||
};
|
||||
|
||||
Ok((command, root_dir.to_string_lossy().into_owned(), None))
|
||||
Ok((command, version_dir.to_string_lossy().into_owned(), None))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1946,6 +1953,51 @@ mod extension_agent_tests {
|
||||
assert_eq!(target.args, vec!["index.js"]);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_commands_run_in_extraction_directory(cx: &mut TestAppContext) {
|
||||
let fs = fs::FakeFs::new(cx.background_executor.clone());
|
||||
let http_client = http_client::FakeHttpClient::with_404_response();
|
||||
let node_runtime = NodeRuntime::unavailable();
|
||||
let worktree_store = cx.new(|_| WorktreeStore::local(false, fs.clone()));
|
||||
let project_environment = cx.new(|cx| {
|
||||
crate::ProjectEnvironment::new(None, worktree_store.downgrade(), None, false, cx)
|
||||
});
|
||||
|
||||
let agent = LocalExtensionArchiveAgent {
|
||||
fs: fs.clone(),
|
||||
http_client,
|
||||
node_runtime,
|
||||
project_environment,
|
||||
extension_id: Arc::from("test-ext"),
|
||||
agent_id: Arc::from("test-agent"),
|
||||
targets: {
|
||||
let mut map = HashMap::default();
|
||||
map.insert(
|
||||
"darwin-aarch64".to_string(),
|
||||
extension::TargetConfig {
|
||||
archive: "https://example.com/test.zip".into(),
|
||||
cmd: "node".into(),
|
||||
args: vec![
|
||||
"server.js".into(),
|
||||
"--config".into(),
|
||||
"./config.json".into(),
|
||||
],
|
||||
sha256: None,
|
||||
},
|
||||
);
|
||||
map
|
||||
},
|
||||
env: HashMap::default(),
|
||||
};
|
||||
|
||||
// Verify the agent is configured with relative paths in args
|
||||
let target = agent.targets.get("darwin-aarch64").unwrap();
|
||||
assert_eq!(target.args[0], "server.js");
|
||||
assert_eq!(target.args[2], "./config.json");
|
||||
// These relative paths will resolve relative to the extraction directory
|
||||
// when the command is executed
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tilde_expansion_in_settings() {
|
||||
let settings = settings::BuiltinAgentServerSettings {
|
||||
|
||||
@@ -333,60 +333,51 @@ async fn load_directory_shell_environment(
|
||||
.into()
|
||||
};
|
||||
|
||||
if cfg!(target_os = "windows") {
|
||||
let (shell, args) = shell.program_and_args();
|
||||
let mut envs = util::shell_env::capture(shell.clone(), args, abs_path)
|
||||
.await
|
||||
.with_context(|| {
|
||||
tx.unbounded_send("Failed to load environment variables".into())
|
||||
.ok();
|
||||
format!("capturing shell environment with {shell:?}")
|
||||
})?;
|
||||
|
||||
if cfg!(target_os = "windows")
|
||||
&& let Some(path) = envs.remove("Path")
|
||||
{
|
||||
// windows env vars are case-insensitive, so normalize the path var
|
||||
// so we can just assume `PATH` in other places
|
||||
envs.insert("PATH".into(), path);
|
||||
}
|
||||
// If the user selects `Direct` for direnv, it would set an environment
|
||||
// variable that later uses to know that it should not run the hook.
|
||||
// We would include in `.envs` call so it is okay to run the hook
|
||||
// even if direnv direct mode is enabled.
|
||||
let direnv_environment = match load_direnv {
|
||||
DirenvSettings::ShellHook => None,
|
||||
// Note: direnv is not available on Windows, so we skip direnv processing
|
||||
// and just return the shell environment
|
||||
let (shell, args) = shell.program_and_args();
|
||||
let mut envs = util::shell_env::capture(shell.clone(), args, abs_path)
|
||||
DirenvSettings::Direct if cfg!(target_os = "windows") => None,
|
||||
DirenvSettings::Direct => load_direnv_environment(&envs, &dir)
|
||||
.await
|
||||
.with_context(|| {
|
||||
tx.unbounded_send("Failed to load environment variables".into())
|
||||
tx.unbounded_send("Failed to load direnv environment".into())
|
||||
.ok();
|
||||
format!("capturing shell environment with {shell:?}")
|
||||
})?;
|
||||
if let Some(path) = envs.remove("Path") {
|
||||
// windows env vars are case-insensitive, so normalize the path var
|
||||
// so we can just assume `PATH` in other places
|
||||
envs.insert("PATH".into(), path);
|
||||
}
|
||||
Ok(envs)
|
||||
} else {
|
||||
let (shell, args) = shell.program_and_args();
|
||||
let mut envs = util::shell_env::capture(shell.clone(), args, abs_path)
|
||||
.await
|
||||
.with_context(|| {
|
||||
tx.unbounded_send("Failed to load environment variables".into())
|
||||
.ok();
|
||||
format!("capturing shell environment with {shell:?}")
|
||||
})?;
|
||||
|
||||
// If the user selects `Direct` for direnv, it would set an environment
|
||||
// variable that later uses to know that it should not run the hook.
|
||||
// We would include in `.envs` call so it is okay to run the hook
|
||||
// even if direnv direct mode is enabled.
|
||||
let direnv_environment = match load_direnv {
|
||||
DirenvSettings::ShellHook => None,
|
||||
DirenvSettings::Direct => load_direnv_environment(&envs, &dir)
|
||||
.await
|
||||
.with_context(|| {
|
||||
tx.unbounded_send("Failed to load direnv environment".into())
|
||||
.ok();
|
||||
"load direnv environment"
|
||||
})
|
||||
.log_err(),
|
||||
};
|
||||
if let Some(direnv_environment) = direnv_environment {
|
||||
for (key, value) in direnv_environment {
|
||||
if let Some(value) = value {
|
||||
envs.insert(key, value);
|
||||
} else {
|
||||
envs.remove(&key);
|
||||
}
|
||||
"load direnv environment"
|
||||
})
|
||||
.log_err(),
|
||||
};
|
||||
if let Some(direnv_environment) = direnv_environment {
|
||||
for (key, value) in direnv_environment {
|
||||
if let Some(value) = value {
|
||||
envs.insert(key, value);
|
||||
} else {
|
||||
envs.remove(&key);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(envs)
|
||||
}
|
||||
|
||||
Ok(envs)
|
||||
}
|
||||
|
||||
async fn load_direnv_environment(
|
||||
|
||||
@@ -4815,10 +4815,11 @@ impl Repository {
|
||||
}),
|
||||
)
|
||||
.collect::<Vec<_>>();
|
||||
if !edits.is_empty() {
|
||||
let full_scan = !edits.is_empty();
|
||||
self.snapshot.statuses_by_path.edit(edits, ());
|
||||
if full_scan {
|
||||
cx.emit(RepositoryEvent::StatusesChanged { full_scan: true });
|
||||
}
|
||||
self.snapshot.statuses_by_path.edit(edits, ());
|
||||
if update.is_last_update {
|
||||
self.snapshot.scan_id = update.scan_id;
|
||||
}
|
||||
|
||||
@@ -4193,7 +4193,7 @@ impl LspStore {
|
||||
})
|
||||
.detach();
|
||||
} else {
|
||||
panic!("oops!");
|
||||
// Our remote connection got closed
|
||||
}
|
||||
handle
|
||||
}
|
||||
@@ -7643,7 +7643,10 @@ impl LspStore {
|
||||
let buffer = buffer.read(cx);
|
||||
let file = File::from_dyn(buffer.file())?;
|
||||
let abs_path = file.as_local()?.abs_path(cx);
|
||||
let uri = lsp::Uri::from_file_path(abs_path).unwrap();
|
||||
let uri = lsp::Uri::from_file_path(&abs_path)
|
||||
.ok()
|
||||
.with_context(|| format!("Failed to convert path to URI: {}", abs_path.display()))
|
||||
.unwrap();
|
||||
let next_snapshot = buffer.text_snapshot();
|
||||
for language_server in language_servers {
|
||||
let language_server = language_server.clone();
|
||||
|
||||
@@ -306,7 +306,7 @@ impl RemoteConnection for SshRemoteConnection {
|
||||
use futures::AsyncWriteExt;
|
||||
let sftp_batch = format!("put -r {src_path_display} {dest_path_str}\n");
|
||||
stdin.write_all(sftp_batch.as_bytes()).await?;
|
||||
drop(stdin);
|
||||
stdin.flush().await?;
|
||||
}
|
||||
|
||||
let output = child.output().await?;
|
||||
@@ -850,7 +850,7 @@ impl SshRemoteConnection {
|
||||
if let Some(mut stdin) = child.stdin.take() {
|
||||
use futures::AsyncWriteExt;
|
||||
stdin.write_all(sftp_batch.as_bytes()).await?;
|
||||
drop(stdin);
|
||||
stdin.flush().await?;
|
||||
}
|
||||
|
||||
let output = child.output().await?;
|
||||
|
||||
@@ -19,6 +19,7 @@ use std::{
|
||||
time::Instant,
|
||||
};
|
||||
use util::{
|
||||
ResultExt as _,
|
||||
paths::{PathStyle, RemotePathBuf},
|
||||
rel_path::RelPath,
|
||||
shell::ShellKind,
|
||||
@@ -79,43 +80,42 @@ impl WslRemoteConnection {
|
||||
can_exec: true,
|
||||
};
|
||||
delegate.set_status(Some("Detecting WSL environment"), cx);
|
||||
this.shell = this.detect_shell().await?;
|
||||
this.shell = this
|
||||
.detect_shell()
|
||||
.await
|
||||
.context("failed detecting shell")?;
|
||||
this.shell_kind = ShellKind::new(&this.shell, false);
|
||||
this.can_exec = this.detect_can_exec().await?;
|
||||
this.platform = this.detect_platform().await?;
|
||||
this.can_exec = this.detect_can_exec().await;
|
||||
this.platform = this
|
||||
.detect_platform()
|
||||
.await
|
||||
.context("failed detecting platform")?;
|
||||
this.remote_binary_path = Some(
|
||||
this.ensure_server_binary(&delegate, release_channel, version, commit, cx)
|
||||
.await?,
|
||||
.await
|
||||
.context("failed ensuring server binary")?,
|
||||
);
|
||||
log::debug!("Detected WSL environment: {this:#?}");
|
||||
|
||||
Ok(this)
|
||||
}
|
||||
|
||||
async fn detect_can_exec(&self) -> Result<bool> {
|
||||
async fn detect_can_exec(&self) -> bool {
|
||||
let options = &self.connection_options;
|
||||
let program = self.shell_kind.prepend_command_prefix("uname");
|
||||
let args = &["-m"];
|
||||
let output = wsl_command_impl(options, &program, args, true)
|
||||
.output()
|
||||
.await?;
|
||||
.await;
|
||||
|
||||
if !output.status.success() {
|
||||
let output = wsl_command_impl(options, &program, args, false)
|
||||
.output()
|
||||
.await?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(anyhow!(
|
||||
"Command '{}' failed: {}",
|
||||
program,
|
||||
String::from_utf8_lossy(&output.stderr).trim()
|
||||
));
|
||||
}
|
||||
|
||||
Ok(false)
|
||||
if !output.is_ok_and(|output| output.status.success()) {
|
||||
run_wsl_command_impl(options, &program, args, false)
|
||||
.await
|
||||
.context("failed detecting exec status")
|
||||
.log_err();
|
||||
false
|
||||
} else {
|
||||
Ok(true)
|
||||
true
|
||||
}
|
||||
}
|
||||
async fn detect_platform(&self) -> Result<RemotePlatform> {
|
||||
@@ -517,7 +517,9 @@ impl RemoteConnection for WslRemoteConnection {
|
||||
/// `wslpath` is a executable available in WSL, it's a linux binary.
|
||||
/// So it doesn't support Windows style paths.
|
||||
async fn sanitize_path(path: &Path) -> Result<String> {
|
||||
let path = smol::fs::canonicalize(path).await?;
|
||||
let path = smol::fs::canonicalize(path)
|
||||
.await
|
||||
.with_context(|| format!("Failed to canonicalize path {}", path.display()))?;
|
||||
let path_str = path.to_string_lossy();
|
||||
|
||||
let sanitized = path_str.strip_prefix(r"\\?\").unwrap_or(&path_str);
|
||||
@@ -539,14 +541,16 @@ async fn run_wsl_command_impl(
|
||||
args: &[&str],
|
||||
exec: bool,
|
||||
) -> Result<String> {
|
||||
let output = wsl_command_impl(options, program, args, exec)
|
||||
let mut command = wsl_command_impl(options, program, args, exec);
|
||||
let output = command
|
||||
.output()
|
||||
.await?;
|
||||
.await
|
||||
.with_context(|| format!("Failed to run command '{:?}'", command))?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(anyhow!(
|
||||
"Command '{}' failed: {}",
|
||||
program,
|
||||
"Command '{:?}' failed: {}",
|
||||
command,
|
||||
String::from_utf8_lossy(&output.stderr).trim()
|
||||
));
|
||||
}
|
||||
|
||||
@@ -448,7 +448,7 @@ where
|
||||
aggregate: &mut dyn SeekAggregate<'a, T>,
|
||||
) -> bool {
|
||||
assert!(
|
||||
target.cmp(&self.position, self.cx) >= Ordering::Equal,
|
||||
target.cmp(&self.position, self.cx).is_ge(),
|
||||
"cannot seek backward",
|
||||
);
|
||||
|
||||
|
||||
@@ -387,7 +387,7 @@ impl TerminalBuilder {
|
||||
selection_phase: SelectionPhase::Ended,
|
||||
hyperlink_regex_searches: RegexSearches::new(),
|
||||
vi_mode_enabled: false,
|
||||
is_ssh_terminal: false,
|
||||
is_remote_terminal: false,
|
||||
last_mouse_move_time: Instant::now(),
|
||||
last_hyperlink_search_position: None,
|
||||
#[cfg(windows)]
|
||||
@@ -419,14 +419,14 @@ impl TerminalBuilder {
|
||||
cursor_shape: CursorShape,
|
||||
alternate_scroll: AlternateScroll,
|
||||
max_scroll_history_lines: Option<usize>,
|
||||
is_ssh_terminal: bool,
|
||||
is_remote_terminal: bool,
|
||||
window_id: u64,
|
||||
completion_tx: Option<Sender<Option<ExitStatus>>>,
|
||||
cx: &App,
|
||||
activation_script: Vec<String>,
|
||||
) -> Task<Result<TerminalBuilder>> {
|
||||
let version = release_channel::AppVersion::global(cx);
|
||||
cx.background_spawn(async move {
|
||||
let fut = async move {
|
||||
// If the parent environment doesn't have a locale set
|
||||
// (As is the case when launched from a .app on MacOS),
|
||||
// and the Project doesn't have a locale set, then
|
||||
@@ -605,7 +605,7 @@ impl TerminalBuilder {
|
||||
selection_phase: SelectionPhase::Ended,
|
||||
hyperlink_regex_searches: RegexSearches::new(),
|
||||
vi_mode_enabled: false,
|
||||
is_ssh_terminal,
|
||||
is_remote_terminal,
|
||||
last_mouse_move_time: Instant::now(),
|
||||
last_hyperlink_search_position: None,
|
||||
#[cfg(windows)]
|
||||
@@ -648,7 +648,13 @@ impl TerminalBuilder {
|
||||
terminal,
|
||||
events_rx,
|
||||
})
|
||||
})
|
||||
};
|
||||
// the thread we spawn things on has an effect on signal handling
|
||||
if !cfg!(target_os = "windows") {
|
||||
cx.spawn(async move |_| fut.await)
|
||||
} else {
|
||||
cx.background_spawn(fut)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn subscribe(mut self, cx: &Context<Terminal>) -> Terminal {
|
||||
@@ -826,7 +832,7 @@ pub struct Terminal {
|
||||
hyperlink_regex_searches: RegexSearches,
|
||||
task: Option<TaskState>,
|
||||
vi_mode_enabled: bool,
|
||||
is_ssh_terminal: bool,
|
||||
is_remote_terminal: bool,
|
||||
last_mouse_move_time: Instant,
|
||||
last_hyperlink_search_position: Option<Point<Pixels>>,
|
||||
#[cfg(windows)]
|
||||
@@ -1957,7 +1963,7 @@ impl Terminal {
|
||||
}
|
||||
|
||||
pub fn working_directory(&self) -> Option<PathBuf> {
|
||||
if self.is_ssh_terminal {
|
||||
if self.is_remote_terminal {
|
||||
// We can't yet reliably detect the working directory of a shell on the
|
||||
// SSH host. Until we can do that, it doesn't make sense to display
|
||||
// the working directory on the client and persist that.
|
||||
@@ -2156,7 +2162,7 @@ impl Terminal {
|
||||
self.template.cursor_shape,
|
||||
self.template.alternate_scroll,
|
||||
self.template.max_scroll_history_lines,
|
||||
self.is_ssh_terminal,
|
||||
self.is_remote_terminal,
|
||||
self.template.window_id,
|
||||
None,
|
||||
cx,
|
||||
|
||||
@@ -48,6 +48,7 @@ pub struct ContextMenuEntry {
|
||||
label: SharedString,
|
||||
icon: Option<IconName>,
|
||||
custom_icon_path: Option<SharedString>,
|
||||
custom_icon_svg: Option<SharedString>,
|
||||
icon_position: IconPosition,
|
||||
icon_size: IconSize,
|
||||
icon_color: Option<Color>,
|
||||
@@ -68,6 +69,7 @@ impl ContextMenuEntry {
|
||||
label: label.into(),
|
||||
icon: None,
|
||||
custom_icon_path: None,
|
||||
custom_icon_svg: None,
|
||||
icon_position: IconPosition::Start,
|
||||
icon_size: IconSize::Small,
|
||||
icon_color: None,
|
||||
@@ -94,7 +96,15 @@ impl ContextMenuEntry {
|
||||
|
||||
pub fn custom_icon_path(mut self, path: impl Into<SharedString>) -> Self {
|
||||
self.custom_icon_path = Some(path.into());
|
||||
self.icon = None; // Clear IconName if custom path is set
|
||||
self.custom_icon_svg = None; // Clear other icon sources if custom path is set
|
||||
self.icon = None;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn custom_icon_svg(mut self, svg: impl Into<SharedString>) -> Self {
|
||||
self.custom_icon_svg = Some(svg.into());
|
||||
self.custom_icon_path = None; // Clear other icon sources if custom path is set
|
||||
self.icon = None;
|
||||
self
|
||||
}
|
||||
|
||||
@@ -396,6 +406,7 @@ impl ContextMenu {
|
||||
handler: Rc::new(move |_, window, cx| handler(window, cx)),
|
||||
icon: None,
|
||||
custom_icon_path: None,
|
||||
custom_icon_svg: None,
|
||||
icon_position: IconPosition::End,
|
||||
icon_size: IconSize::Small,
|
||||
icon_color: None,
|
||||
@@ -425,6 +436,7 @@ impl ContextMenu {
|
||||
handler: Rc::new(move |_, window, cx| handler(window, cx)),
|
||||
icon: None,
|
||||
custom_icon_path: None,
|
||||
custom_icon_svg: None,
|
||||
icon_position: IconPosition::End,
|
||||
icon_size: IconSize::Small,
|
||||
icon_color: None,
|
||||
@@ -454,6 +466,7 @@ impl ContextMenu {
|
||||
handler: Rc::new(move |_, window, cx| handler(window, cx)),
|
||||
icon: None,
|
||||
custom_icon_path: None,
|
||||
custom_icon_svg: None,
|
||||
icon_position: IconPosition::End,
|
||||
icon_size: IconSize::Small,
|
||||
icon_color: None,
|
||||
@@ -482,6 +495,7 @@ impl ContextMenu {
|
||||
handler: Rc::new(move |_, window, cx| handler(window, cx)),
|
||||
icon: None,
|
||||
custom_icon_path: None,
|
||||
custom_icon_svg: None,
|
||||
icon_position: position,
|
||||
icon_size: IconSize::Small,
|
||||
icon_color: None,
|
||||
@@ -541,6 +555,7 @@ impl ContextMenu {
|
||||
}),
|
||||
icon: None,
|
||||
custom_icon_path: None,
|
||||
custom_icon_svg: None,
|
||||
icon_position: IconPosition::End,
|
||||
icon_size: IconSize::Small,
|
||||
icon_color: None,
|
||||
@@ -572,6 +587,7 @@ impl ContextMenu {
|
||||
}),
|
||||
icon: None,
|
||||
custom_icon_path: None,
|
||||
custom_icon_svg: None,
|
||||
icon_size: IconSize::Small,
|
||||
icon_position: IconPosition::End,
|
||||
icon_color: None,
|
||||
@@ -593,6 +609,7 @@ impl ContextMenu {
|
||||
handler: Rc::new(move |_, window, cx| window.dispatch_action(action.boxed_clone(), cx)),
|
||||
icon: Some(IconName::ArrowUpRight),
|
||||
custom_icon_path: None,
|
||||
custom_icon_svg: None,
|
||||
icon_size: IconSize::XSmall,
|
||||
icon_position: IconPosition::End,
|
||||
icon_color: None,
|
||||
@@ -913,6 +930,7 @@ impl ContextMenu {
|
||||
handler,
|
||||
icon,
|
||||
custom_icon_path,
|
||||
custom_icon_svg,
|
||||
icon_position,
|
||||
icon_size,
|
||||
icon_color,
|
||||
@@ -965,6 +983,28 @@ impl ContextMenu {
|
||||
)
|
||||
})
|
||||
.into_any_element()
|
||||
} else if let Some(custom_icon_svg) = custom_icon_svg {
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.when(
|
||||
*icon_position == IconPosition::Start && toggle.is_none(),
|
||||
|flex| {
|
||||
flex.child(
|
||||
Icon::from_external_svg(custom_icon_svg.clone())
|
||||
.size(*icon_size)
|
||||
.color(icon_color),
|
||||
)
|
||||
},
|
||||
)
|
||||
.child(Label::new(label.clone()).color(label_color).truncate())
|
||||
.when(*icon_position == IconPosition::End, |flex| {
|
||||
flex.child(
|
||||
Icon::from_external_svg(custom_icon_svg.clone())
|
||||
.size(*icon_size)
|
||||
.color(icon_color),
|
||||
)
|
||||
})
|
||||
.into_any_element()
|
||||
} else if let Some(icon_name) = icon {
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
|
||||
@@ -115,24 +115,24 @@ impl From<IconName> for Icon {
|
||||
/// The source of an icon.
|
||||
enum IconSource {
|
||||
/// An SVG embedded in the Zed binary.
|
||||
Svg(SharedString),
|
||||
Embedded(SharedString),
|
||||
/// An image file located at the specified path.
|
||||
///
|
||||
/// Currently our SVG renderer is missing support for the following features:
|
||||
/// 1. Loading SVGs from external files.
|
||||
/// 2. Rendering polychrome SVGs.
|
||||
/// Currently our SVG renderer is missing support for rendering polychrome SVGs.
|
||||
///
|
||||
/// In order to support icon themes, we render the icons as images instead.
|
||||
Image(Arc<Path>),
|
||||
External(Arc<Path>),
|
||||
/// An SVG not embedded in the Zed binary.
|
||||
ExternalSvg(SharedString),
|
||||
}
|
||||
|
||||
impl IconSource {
|
||||
fn from_path(path: impl Into<SharedString>) -> Self {
|
||||
let path = path.into();
|
||||
if path.starts_with("icons/") {
|
||||
Self::Svg(path)
|
||||
Self::Embedded(path)
|
||||
} else {
|
||||
Self::Image(Arc::from(PathBuf::from(path.as_ref())))
|
||||
Self::External(Arc::from(PathBuf::from(path.as_ref())))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -148,7 +148,7 @@ pub struct Icon {
|
||||
impl Icon {
|
||||
pub fn new(icon: IconName) -> Self {
|
||||
Self {
|
||||
source: IconSource::Svg(icon.path().into()),
|
||||
source: IconSource::Embedded(icon.path().into()),
|
||||
color: Color::default(),
|
||||
size: IconSize::default().rems(),
|
||||
transformation: Transformation::default(),
|
||||
@@ -164,6 +164,15 @@ impl Icon {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_external_svg(svg: SharedString) -> Self {
|
||||
Self {
|
||||
source: IconSource::ExternalSvg(svg),
|
||||
color: Color::default(),
|
||||
size: IconSize::default().rems(),
|
||||
transformation: Transformation::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn color(mut self, color: Color) -> Self {
|
||||
self.color = color;
|
||||
self
|
||||
@@ -193,14 +202,21 @@ impl Transformable for Icon {
|
||||
impl RenderOnce for Icon {
|
||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
match self.source {
|
||||
IconSource::Svg(path) => svg()
|
||||
IconSource::Embedded(path) => svg()
|
||||
.with_transformation(self.transformation)
|
||||
.size(self.size)
|
||||
.flex_none()
|
||||
.path(path)
|
||||
.text_color(self.color.color(cx))
|
||||
.into_any_element(),
|
||||
IconSource::Image(path) => img(path)
|
||||
IconSource::ExternalSvg(path) => svg()
|
||||
.external_path(path)
|
||||
.with_transformation(self.transformation)
|
||||
.size(self.size)
|
||||
.flex_none()
|
||||
.text_color(self.color.color(cx))
|
||||
.into_any_element(),
|
||||
IconSource::External(path) => img(path)
|
||||
.size(self.size)
|
||||
.flex_none()
|
||||
.text_color(self.color.color(cx))
|
||||
|
||||
@@ -136,124 +136,80 @@ async fn capture_windows(
|
||||
std::env::current_exe().context("Failed to determine current zed executable path.")?;
|
||||
|
||||
let shell_kind = ShellKind::new(shell_path, true);
|
||||
let env_output = match shell_kind {
|
||||
if let ShellKind::Posix
|
||||
| ShellKind::Csh
|
||||
| ShellKind::Tcsh
|
||||
| ShellKind::Rc
|
||||
| ShellKind::Fish
|
||||
| ShellKind::Xonsh = shell_kind
|
||||
{
|
||||
return Err(anyhow::anyhow!("unsupported shell kind"));
|
||||
}
|
||||
let mut cmd = crate::command::new_smol_command(shell_path);
|
||||
let cmd = match shell_kind {
|
||||
ShellKind::Posix
|
||||
| ShellKind::Csh
|
||||
| ShellKind::Tcsh
|
||||
| ShellKind::Rc
|
||||
| ShellKind::Fish
|
||||
| ShellKind::Xonsh => {
|
||||
return Err(anyhow::anyhow!("unsupported shell kind"));
|
||||
unreachable!()
|
||||
}
|
||||
ShellKind::PowerShell => {
|
||||
let output = crate::command::new_smol_command(shell_path)
|
||||
.args([
|
||||
"-NonInteractive",
|
||||
"-NoProfile",
|
||||
"-Command",
|
||||
&format!(
|
||||
"Set-Location '{}'; & '{}' --printenv",
|
||||
directory.display(),
|
||||
zed_path.display()
|
||||
),
|
||||
])
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.output()
|
||||
.await?;
|
||||
|
||||
anyhow::ensure!(
|
||||
output.status.success(),
|
||||
"PowerShell command failed with {}. stdout: {:?}, stderr: {:?}",
|
||||
output.status,
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr),
|
||||
);
|
||||
output
|
||||
}
|
||||
ShellKind::Elvish => {
|
||||
let output = crate::command::new_smol_command(shell_path)
|
||||
.args([
|
||||
"-c",
|
||||
&format!(
|
||||
"cd '{}'; {} --printenv",
|
||||
directory.display(),
|
||||
zed_path.display()
|
||||
),
|
||||
])
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.output()
|
||||
.await?;
|
||||
|
||||
anyhow::ensure!(
|
||||
output.status.success(),
|
||||
"Elvish command failed with {}. stdout: {:?}, stderr: {:?}",
|
||||
output.status,
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr),
|
||||
);
|
||||
output
|
||||
}
|
||||
ShellKind::Nushell => {
|
||||
let output = crate::command::new_smol_command(shell_path)
|
||||
.args([
|
||||
"-c",
|
||||
&format!(
|
||||
"cd '{}'; {}'{}' --printenv",
|
||||
directory.display(),
|
||||
shell_kind
|
||||
.command_prefix()
|
||||
.map(|prefix| prefix.to_string())
|
||||
.unwrap_or_default(),
|
||||
zed_path.display()
|
||||
),
|
||||
])
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.output()
|
||||
.await?;
|
||||
|
||||
anyhow::ensure!(
|
||||
output.status.success(),
|
||||
"Nushell command failed with {}. stdout: {:?}, stderr: {:?}",
|
||||
output.status,
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr),
|
||||
);
|
||||
output
|
||||
}
|
||||
ShellKind::Cmd => {
|
||||
let output = crate::command::new_smol_command(shell_path)
|
||||
.args([
|
||||
"/c",
|
||||
&format!(
|
||||
"cd '{}'; '{}' --printenv",
|
||||
directory.display(),
|
||||
zed_path.display()
|
||||
),
|
||||
])
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.output()
|
||||
.await?;
|
||||
|
||||
anyhow::ensure!(
|
||||
output.status.success(),
|
||||
"Cmd command failed with {}. stdout: {:?}, stderr: {:?}",
|
||||
output.status,
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr),
|
||||
);
|
||||
output
|
||||
}
|
||||
};
|
||||
|
||||
let env_output = String::from_utf8_lossy(&env_output.stdout);
|
||||
ShellKind::PowerShell => cmd.args([
|
||||
"-NonInteractive",
|
||||
"-NoProfile",
|
||||
"-Command",
|
||||
&format!(
|
||||
"Set-Location '{}'; & '{}' --printenv",
|
||||
directory.display(),
|
||||
zed_path.display()
|
||||
),
|
||||
]),
|
||||
ShellKind::Elvish => cmd.args([
|
||||
"-c",
|
||||
&format!(
|
||||
"cd '{}'; {} --printenv",
|
||||
directory.display(),
|
||||
zed_path.display()
|
||||
),
|
||||
]),
|
||||
ShellKind::Nushell => cmd.args([
|
||||
"-c",
|
||||
&format!(
|
||||
"cd '{}'; {}'{}' --printenv",
|
||||
directory.display(),
|
||||
shell_kind
|
||||
.command_prefix()
|
||||
.map(|prefix| prefix.to_string())
|
||||
.unwrap_or_default(),
|
||||
zed_path.display()
|
||||
),
|
||||
]),
|
||||
ShellKind::Cmd => cmd.args([
|
||||
"/c",
|
||||
"cd",
|
||||
&directory.display().to_string(),
|
||||
"&&",
|
||||
&zed_path.display().to_string(),
|
||||
"--printenv",
|
||||
]),
|
||||
}
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
let output = cmd
|
||||
.output()
|
||||
.await
|
||||
.with_context(|| format!("command {cmd:?}"))?;
|
||||
anyhow::ensure!(
|
||||
output.status.success(),
|
||||
"Command {cmd:?} failed with {}. stdout: {:?}, stderr: {:?}",
|
||||
output.status,
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr),
|
||||
);
|
||||
// "cmd" "/c" "cd \'C:\\Workspace\\salsa\\\'; \'C:\\Workspace\\zed\\zed\\target\\debug\\zed.exe\' --printenv"
|
||||
let env_output = String::from_utf8_lossy(&output.stdout);
|
||||
|
||||
// Parse the JSON output from zed --printenv
|
||||
serde_json::from_str(&env_output)
|
||||
|
||||
@@ -611,17 +611,21 @@ where
|
||||
let file = caller.file().replace('\\', "/");
|
||||
// In this codebase all crates reside in a `crates` directory,
|
||||
// so discard the prefix up to that segment to find the crate name
|
||||
let target = file
|
||||
.split_once("crates/")
|
||||
.and_then(|(_, s)| s.split_once("/src/"));
|
||||
let file = file.split_once("crates/");
|
||||
let target = file.as_ref().and_then(|(_, s)| s.split_once("/src/"));
|
||||
|
||||
let module_path = target.map(|(krate, module)| {
|
||||
krate.to_owned() + "::" + &module.trim_end_matches(".rs").replace('/', "::")
|
||||
if module.starts_with(krate) {
|
||||
module.trim_end_matches(".rs").replace('/', "::")
|
||||
} else {
|
||||
krate.to_owned() + "::" + &module.trim_end_matches(".rs").replace('/', "::")
|
||||
}
|
||||
});
|
||||
let file = file.map(|(_, file)| format!("crates/{file}"));
|
||||
log::logger().log(
|
||||
&log::Record::builder()
|
||||
.target(target.map_or("", |(krate, _)| krate))
|
||||
.module_path(module_path.as_deref())
|
||||
.target(module_path.as_deref().unwrap_or(""))
|
||||
.module_path(file.as_deref())
|
||||
.args(format_args!("{:?}", error))
|
||||
.file(Some(caller.file()))
|
||||
.line(Some(caller.line()))
|
||||
|
||||
@@ -791,12 +791,11 @@ impl WorkspaceDb {
|
||||
remote_connection_id IS ?
|
||||
LIMIT 1
|
||||
})
|
||||
.map(|mut prepared_statement| {
|
||||
.and_then(|mut prepared_statement| {
|
||||
(prepared_statement)((
|
||||
root_paths.serialize().paths,
|
||||
remote_connection_id.map(|id| id.0 as i32),
|
||||
))
|
||||
.unwrap()
|
||||
})
|
||||
.context("No workspaces found")
|
||||
.warn_on_err()
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
description = "The fast, collaborative code editor."
|
||||
edition.workspace = true
|
||||
name = "zed"
|
||||
version = "0.212.0"
|
||||
version = "0.212.3"
|
||||
publish.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
authors = ["Zed Team <hi@zed.dev>"]
|
||||
|
||||
@@ -1 +1 @@
|
||||
dev
|
||||
stable
|
||||
@@ -859,15 +859,19 @@ fn handle_open_request(request: OpenRequest, app_state: Arc<AppState>, cx: &mut
|
||||
// zed://settings/languages/Rust/tab_size - SUPPORT
|
||||
// languages.$(language).tab_size
|
||||
// [ languages $(language) tab_size]
|
||||
workspace::with_active_or_new_workspace(cx, |_workspace, window, cx| {
|
||||
match setting_path {
|
||||
cx.spawn(async move |cx| {
|
||||
let workspace =
|
||||
workspace::get_any_active_workspace(app_state, cx.clone()).await?;
|
||||
|
||||
workspace.update(cx, |_, window, cx| match setting_path {
|
||||
None => window.dispatch_action(Box::new(zed_actions::OpenSettings), cx),
|
||||
Some(setting_path) => window.dispatch_action(
|
||||
Box::new(zed_actions::OpenSettingsAt { path: setting_path }),
|
||||
cx,
|
||||
),
|
||||
}
|
||||
});
|
||||
})
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ pub struct Record<'a> {
|
||||
pub level: log::Level,
|
||||
pub message: &'a std::fmt::Arguments<'a>,
|
||||
pub module_path: Option<&'a str>,
|
||||
pub line: Option<u32>,
|
||||
}
|
||||
|
||||
pub fn init_output_stdout() {
|
||||
@@ -105,7 +106,11 @@ static LEVEL_ANSI_COLORS: [&str; 6] = [
|
||||
];
|
||||
|
||||
// PERF: batching
|
||||
pub fn submit(record: Record) {
|
||||
pub fn submit(mut record: Record) {
|
||||
if record.module_path.is_none_or(|p| !p.ends_with(".rs")) {
|
||||
// Only render line numbers for actual rust files emitted by `log_err` and friends
|
||||
record.line.take();
|
||||
}
|
||||
if ENABLED_SINKS_STDOUT.load(Ordering::Acquire) {
|
||||
let mut stdout = std::io::stdout().lock();
|
||||
_ = writeln!(
|
||||
@@ -117,6 +122,7 @@ pub fn submit(record: Record) {
|
||||
SourceFmt {
|
||||
scope: record.scope,
|
||||
module_path: record.module_path,
|
||||
line: record.line,
|
||||
ansi: true,
|
||||
},
|
||||
record.message
|
||||
@@ -132,6 +138,7 @@ pub fn submit(record: Record) {
|
||||
SourceFmt {
|
||||
scope: record.scope,
|
||||
module_path: record.module_path,
|
||||
line: record.line,
|
||||
ansi: true,
|
||||
},
|
||||
record.message
|
||||
@@ -167,6 +174,7 @@ pub fn submit(record: Record) {
|
||||
SourceFmt {
|
||||
scope: record.scope,
|
||||
module_path: record.module_path,
|
||||
line: record.line,
|
||||
ansi: false,
|
||||
},
|
||||
record.message
|
||||
@@ -202,6 +210,7 @@ pub fn flush() {
|
||||
struct SourceFmt<'a> {
|
||||
scope: Scope,
|
||||
module_path: Option<&'a str>,
|
||||
line: Option<u32>,
|
||||
ansi: bool,
|
||||
}
|
||||
|
||||
@@ -225,6 +234,10 @@ impl std::fmt::Display for SourceFmt<'_> {
|
||||
f.write_str(subscope)?;
|
||||
}
|
||||
}
|
||||
if let Some(line) = self.line {
|
||||
f.write_char(':')?;
|
||||
line.fmt(f)?;
|
||||
}
|
||||
if self.ansi {
|
||||
f.write_str(ANSI_RESET)?;
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ impl log::Log for Zlog {
|
||||
None => (private::scope_new(&[]), private::scope_new(&["*unknown*"])),
|
||||
};
|
||||
let level = record.metadata().level();
|
||||
if !filter::is_scope_enabled(&crate_name_scope, record.module_path(), level) {
|
||||
if !filter::is_scope_enabled(&crate_name_scope, Some(record.target()), level) {
|
||||
return;
|
||||
}
|
||||
sink::submit(sink::Record {
|
||||
@@ -89,6 +89,7 @@ impl log::Log for Zlog {
|
||||
message: record.args(),
|
||||
// PERF(batching): store non-static paths in a cache + leak them and pass static str here
|
||||
module_path: record.module_path().or(record.file()),
|
||||
line: record.line(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -109,6 +110,7 @@ macro_rules! log {
|
||||
level,
|
||||
message: &format_args!($($arg)+),
|
||||
module_path: Some(module_path!()),
|
||||
line: Some(line!()),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -291,7 +293,7 @@ impl log::Log for Logger {
|
||||
return;
|
||||
}
|
||||
let level = record.metadata().level();
|
||||
if !filter::is_scope_enabled(&self.scope, record.module_path(), level) {
|
||||
if !filter::is_scope_enabled(&self.scope, Some(record.target()), level) {
|
||||
return;
|
||||
}
|
||||
sink::submit(sink::Record {
|
||||
@@ -299,6 +301,7 @@ impl log::Log for Logger {
|
||||
level,
|
||||
message: record.args(),
|
||||
module_path: record.module_path(),
|
||||
line: record.line(),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ async function main() {
|
||||
}
|
||||
|
||||
// currently we can only draft notes for patch releases.
|
||||
if (parts[2] == 0) {
|
||||
if (parts[2] === 0) {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
@@ -41,20 +41,13 @@ async function main() {
|
||||
"--depth",
|
||||
100,
|
||||
]);
|
||||
execFileSync("git", [
|
||||
"-C",
|
||||
"target/shallow_clone",
|
||||
"rev-parse",
|
||||
"--verify",
|
||||
tag,
|
||||
]);
|
||||
execFileSync("git", [
|
||||
"-C",
|
||||
"target/shallow_clone",
|
||||
"rev-parse",
|
||||
"--verify",
|
||||
priorTag,
|
||||
]);
|
||||
execFileSync("git", ["-C", "target/shallow_clone", "rev-parse", "--verify", tag]);
|
||||
try {
|
||||
execFileSync("git", ["-C", "target/shallow_clone", "rev-parse", "--verify", priorTag]);
|
||||
} catch (e) {
|
||||
console.error(`Prior tag ${priorTag} not found`);
|
||||
process.exit(0);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e.stderr.toString());
|
||||
process.exit(1);
|
||||
@@ -90,13 +83,7 @@ async function main() {
|
||||
function getCommits(oldTag, newTag) {
|
||||
const pullRequestNumbers = execFileSync(
|
||||
"git",
|
||||
[
|
||||
"-C",
|
||||
"target/shallow_clone",
|
||||
"log",
|
||||
`${oldTag}..${newTag}`,
|
||||
"--format=DIVIDER\n%H|||%B",
|
||||
],
|
||||
["-C", "target/shallow_clone", "log", `${oldTag}..${newTag}`, "--format=DIVIDER\n%H|||%B"],
|
||||
{ encoding: "utf8" },
|
||||
)
|
||||
.replace(/\r\n/g, "\n")
|
||||
|
||||
@@ -17,5 +17,6 @@ clap = { workspace = true, features = ["derive"] }
|
||||
toml.workspace = true
|
||||
indoc.workspace = true
|
||||
indexmap.workspace = true
|
||||
serde.workspace = true
|
||||
toml_edit.workspace = true
|
||||
gh-workflow.workspace = true
|
||||
|
||||
@@ -3,6 +3,7 @@ use clap::Parser;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
mod after_release;
|
||||
mod cherry_pick;
|
||||
mod compare_perf;
|
||||
mod danger;
|
||||
@@ -33,6 +34,7 @@ pub fn run_workflows(_: GenerateWorkflowArgs) -> Result<()> {
|
||||
("compare_perf.yml", compare_perf::compare_perf()),
|
||||
("run_unit_evals.yml", run_agent_evals::run_unit_evals()),
|
||||
("run_agent_evals.yml", run_agent_evals::run_agent_evals()),
|
||||
("after_release.yml", after_release::after_release()),
|
||||
];
|
||||
fs::create_dir_all(dir)
|
||||
.with_context(|| format!("Failed to create directory: {}", dir.display()))?;
|
||||
|
||||
136
tooling/xtask/src/tasks/workflows/after_release.rs
Normal file
136
tooling/xtask/src/tasks/workflows/after_release.rs
Normal file
@@ -0,0 +1,136 @@
|
||||
use gh_workflow::*;
|
||||
|
||||
use crate::tasks::workflows::{
|
||||
release, runners,
|
||||
steps::{NamedJob, checkout_repo, dependant_job, named},
|
||||
vars::{self, StepOutput},
|
||||
};
|
||||
|
||||
pub fn after_release() -> Workflow {
|
||||
// let refresh_zed_dev = rebuild_releases_page();
|
||||
let post_to_discord = post_to_discord(&[]);
|
||||
let publish_winget = publish_winget();
|
||||
let create_sentry_release = create_sentry_release();
|
||||
|
||||
named::workflow()
|
||||
.on(Event::default().release(Release::default().types(vec![ReleaseType::Published])))
|
||||
.add_job(post_to_discord.name, post_to_discord.job)
|
||||
.add_job(publish_winget.name, publish_winget.job)
|
||||
.add_job(create_sentry_release.name, create_sentry_release.job)
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
fn rebuild_releases_page() -> NamedJob {
|
||||
named::job(
|
||||
Job::default()
|
||||
.runs_on(runners::LINUX_SMALL)
|
||||
.cond(Expression::new(
|
||||
"github.repository_owner == 'zed-industries'",
|
||||
))
|
||||
.add_step(named::bash(
|
||||
"curl https://zed.dev/api/revalidate-releases -H \"Authorization: Bearer ${RELEASE_NOTES_API_TOKEN}\"",
|
||||
).add_env(("RELEASE_NOTES_API_TOKEN", vars::RELEASE_NOTES_API_TOKEN))),
|
||||
)
|
||||
}
|
||||
|
||||
fn post_to_discord(deps: &[&NamedJob]) -> NamedJob {
|
||||
fn get_release_url() -> Step<Run> {
|
||||
named::bash(indoc::indoc! {r#"
|
||||
if [ "${{ github.event.release.prerelease }}" == "true" ]; then
|
||||
URL="https://zed.dev/releases/preview"
|
||||
else
|
||||
URL="https://zed.dev/releases/stable"
|
||||
fi
|
||||
|
||||
echo "URL=$URL" >> "$GITHUB_OUTPUT"
|
||||
"#})
|
||||
.id("get-release-url")
|
||||
}
|
||||
|
||||
fn get_content() -> Step<Use> {
|
||||
named::uses(
|
||||
"2428392",
|
||||
"gh-truncate-string-action",
|
||||
"b3ff790d21cf42af3ca7579146eedb93c8fb0757", // v1.4.1
|
||||
)
|
||||
.id("get-content")
|
||||
.add_with((
|
||||
"stringToTruncate",
|
||||
indoc::indoc! {r#"
|
||||
📣 Zed [${{ github.event.release.tag_name }}](<${{ steps.get-release-url.outputs.URL }}>) was just released!
|
||||
|
||||
${{ github.event.release.body }}
|
||||
"#},
|
||||
))
|
||||
.add_with(("maxLength", 2000))
|
||||
.add_with(("truncationSymbol", "..."))
|
||||
}
|
||||
|
||||
fn discord_webhook_action() -> Step<Use> {
|
||||
named::uses(
|
||||
"tsickert",
|
||||
"discord-webhook",
|
||||
"c840d45a03a323fbc3f7507ac7769dbd91bfb164", // v5.3.0
|
||||
)
|
||||
.add_with(("webhook-url", vars::DISCORD_WEBHOOK_RELEASE_NOTES))
|
||||
.add_with(("content", "${{ steps.get-content.outputs.string }}"))
|
||||
}
|
||||
let job = dependant_job(deps)
|
||||
.runs_on(runners::LINUX_SMALL)
|
||||
.cond(Expression::new(
|
||||
"github.repository_owner == 'zed-industries'",
|
||||
))
|
||||
.add_step(get_release_url())
|
||||
.add_step(get_content())
|
||||
.add_step(discord_webhook_action());
|
||||
named::job(job)
|
||||
}
|
||||
|
||||
fn publish_winget() -> NamedJob {
|
||||
fn set_package_name() -> (Step<Run>, StepOutput) {
|
||||
let step = named::bash(indoc::indoc! {r#"
|
||||
if [ "${{ github.event.release.prerelease }}" == "true" ]; then
|
||||
PACKAGE_NAME=ZedIndustries.Zed.Preview
|
||||
else
|
||||
PACKAGE_NAME=ZedIndustries.Zed
|
||||
fi
|
||||
|
||||
echo "PACKAGE_NAME=$PACKAGE_NAME" >> "$GITHUB_OUTPUT"
|
||||
"#})
|
||||
.id("set-package-name");
|
||||
|
||||
let output = StepOutput::new(&step, "PACKAGE_NAME");
|
||||
(step, output)
|
||||
}
|
||||
|
||||
fn winget_releaser(package_name: &StepOutput) -> Step<Use> {
|
||||
named::uses(
|
||||
"vedantmgoyal9",
|
||||
"winget-releaser",
|
||||
"19e706d4c9121098010096f9c495a70a7518b30f", // v2
|
||||
)
|
||||
.add_with(("identifier", package_name.to_string()))
|
||||
.add_with(("max-versions-to-keep", 5))
|
||||
.add_with(("token", vars::WINGET_TOKEN))
|
||||
}
|
||||
|
||||
let (set_package_name, package_name) = set_package_name();
|
||||
|
||||
named::job(
|
||||
Job::default()
|
||||
.runs_on(runners::LINUX_SMALL)
|
||||
.add_step(set_package_name)
|
||||
.add_step(winget_releaser(&package_name)),
|
||||
)
|
||||
}
|
||||
|
||||
fn create_sentry_release() -> NamedJob {
|
||||
let job = Job::default()
|
||||
.runs_on(runners::LINUX_SMALL)
|
||||
.cond(Expression::new(
|
||||
"github.repository_owner == 'zed-industries'",
|
||||
))
|
||||
.add_step(checkout_repo())
|
||||
.add_step(release::create_sentry_release());
|
||||
named::job(job)
|
||||
}
|
||||
@@ -107,7 +107,6 @@ fn auto_release_preview(deps: &[&NamedJob; 1]) -> NamedJob {
|
||||
)
|
||||
.add_env(("GITHUB_TOKEN", vars::GITHUB_TOKEN)),
|
||||
)
|
||||
.add_step(create_sentry_release()),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -36,6 +36,9 @@ secret!(ZED_SENTRY_MINIDUMP_ENDPOINT);
|
||||
secret!(SLACK_APP_ZED_UNIT_EVALS_BOT_TOKEN);
|
||||
secret!(ZED_ZIPPY_APP_ID);
|
||||
secret!(ZED_ZIPPY_APP_PRIVATE_KEY);
|
||||
secret!(DISCORD_WEBHOOK_RELEASE_NOTES);
|
||||
secret!(WINGET_TOKEN);
|
||||
secret!(RELEASE_NOTES_API_TOKEN);
|
||||
|
||||
// todo(ci) make these secrets too...
|
||||
var!(AZURE_SIGNING_ACCOUNT_NAME);
|
||||
@@ -136,6 +139,15 @@ impl StepOutput {
|
||||
}
|
||||
}
|
||||
|
||||
impl serde::Serialize for StepOutput {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_str(&self.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for StepOutput {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "${{{{ steps.{}.outputs.{} }}}}", self.step_id, self.name)
|
||||
@@ -173,6 +185,15 @@ impl std::fmt::Display for Input {
|
||||
}
|
||||
}
|
||||
|
||||
impl serde::Serialize for Input {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_str(&self.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub mod assets {
|
||||
// NOTE: these asset names also exist in the zed.dev codebase.
|
||||
pub const MAC_AARCH64: &str = "Zed-aarch64.dmg";
|
||||
|
||||
Reference in New Issue
Block a user