diff --git a/.github/workflows/autofix_pr.yml b/.github/workflows/autofix_pr.yml index 1591ba2a9a98b8587814d25858f4e0d78d9f7d34..d3688a722aa107efb3dfb95351404f43c9aece65 100644 --- a/.github/workflows/autofix_pr.yml +++ b/.github/workflows/autofix_pr.yml @@ -90,7 +90,7 @@ jobs: runs-on: namespace-profile-2x4-ubuntu-2404 steps: - id: get-app-token - name: autofix_pr::commit_changes::authenticate_as_zippy + name: steps::authenticate_as_zippy uses: actions/create-github-app-token@bef1eaf1c0ac2b148ee2a0a74c65fbe6db0631f1 with: app-id: ${{ secrets.ZED_ZIPPY_APP_ID }} diff --git a/.github/workflows/cherry_pick.yml b/.github/workflows/cherry_pick.yml index bc01aae17e7141a2359b162c3de94c1aec7b765c..d4dee5154f2209521f3e9d183c05c118e8861521 100644 --- a/.github/workflows/cherry_pick.yml +++ b/.github/workflows/cherry_pick.yml @@ -30,7 +30,7 @@ jobs: with: clean: false - id: get-app-token - name: cherry_pick::run_cherry_pick::authenticate_as_zippy + name: steps::authenticate_as_zippy uses: actions/create-github-app-token@bef1eaf1c0ac2b148ee2a0a74c65fbe6db0631f1 with: app-id: ${{ secrets.ZED_ZIPPY_APP_ID }} diff --git a/.github/workflows/extension_tests.yml b/.github/workflows/extension_tests.yml index 9f0917e388c74cffed8f342f7504bc111e6f5147..7a7fff9b97d694c1b02dd426f5d59301fe2be81e 100644 --- a/.github/workflows/extension_tests.yml +++ b/.github/workflows/extension_tests.yml @@ -61,7 +61,8 @@ jobs: uses: namespacelabs/nscloud-cache-action@v1 with: cache: rust - - name: steps::cargo_fmt + - id: cargo_fmt + name: steps::cargo_fmt run: cargo fmt --all -- --check shell: bash -euxo pipefail {0} - name: extension_tests::run_clippy diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 155b38666f4bd73e68e9ea326db9a6862288a1fe..317d5a8df37a62887ce4ddcdd67c8d77b48d56d6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -26,7 +26,8 @@ jobs: uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 with: node-version: '20' - - name: steps::clippy + - id: clippy + name: steps::clippy run: ./script/clippy shell: bash -euxo pipefail {0} - name: steps::clear_target_dir_if_large @@ -71,15 +72,15 @@ jobs: uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 with: node-version: '20' - - name: steps::clippy + - id: clippy + name: steps::clippy run: ./script/clippy shell: bash -euxo pipefail {0} - - name: steps::trigger_autofix - if: failure() && github.event_name == 'pull_request' && github.actor != 'zed-zippy[bot]' - run: gh workflow run autofix_pr.yml -f pr_number=${{ github.event.pull_request.number }} -f run_clippy=true + - id: record_clippy_failure + name: steps::record_clippy_failure + if: always() + run: echo "failed=${{ steps.clippy.outcome == 'failure' }}" >> "$GITHUB_OUTPUT" shell: bash -euxo pipefail {0} - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: steps::cargo_install_nextest uses: taiki-e/install-action@nextest - name: steps::clear_target_dir_if_large @@ -93,6 +94,8 @@ jobs: run: | rm -rf ./../.cargo shell: bash -euxo pipefail {0} + outputs: + clippy_failed: ${{ steps.record_clippy_failure.outputs.failed == 'true' }} timeout-minutes: 60 run_tests_windows: if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions') @@ -111,7 +114,8 @@ jobs: uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 with: node-version: '20' - - name: steps::clippy + - id: clippy + name: steps::clippy run: ./script/clippy.ps1 shell: pwsh - name: steps::clear_target_dir_if_large @@ -478,11 +482,17 @@ jobs: if: startsWith(github.ref, 'refs/tags/v') && endsWith(github.ref, '-pre') && !endsWith(github.ref, '.0-pre') runs-on: namespace-profile-2x4-ubuntu-2404 steps: + - id: get-app-token + name: steps::authenticate_as_zippy + uses: actions/create-github-app-token@bef1eaf1c0ac2b148ee2a0a74c65fbe6db0631f1 + with: + app-id: ${{ secrets.ZED_ZIPPY_APP_ID }} + private-key: ${{ secrets.ZED_ZIPPY_APP_PRIVATE_KEY }} - name: gh release edit "$GITHUB_REF_NAME" --repo=zed-industries/zed --draft=false run: gh release edit "$GITHUB_REF_NAME" --repo=zed-industries/zed --draft=false shell: bash -euxo pipefail {0} env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ steps.get-app-token.outputs.token }} notify_on_failure: needs: - upload_release_assets diff --git a/.github/workflows/release_nightly.yml b/.github/workflows/release_nightly.yml index d76244175accc3e816cbd7d5dc322d2529a0a236..b23e4b7518a672c0d586ea5ba437db5cf8f94bb6 100644 --- a/.github/workflows/release_nightly.yml +++ b/.github/workflows/release_nightly.yml @@ -20,7 +20,8 @@ jobs: with: clean: false fetch-depth: 0 - - name: steps::cargo_fmt + - id: cargo_fmt + name: steps::cargo_fmt run: cargo fmt --all -- --check shell: bash -euxo pipefail {0} - name: ./script/clippy @@ -44,7 +45,8 @@ jobs: uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 with: node-version: '20' - - name: steps::clippy + - id: clippy + name: steps::clippy run: ./script/clippy.ps1 shell: pwsh - name: steps::clear_target_dir_if_large diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml index a9a46b7a797faae793c87601d306a2aea80e6592..fac3221d63a080fa53b7ba1c5b7249e6a405c73c 100644 --- a/.github/workflows/run_tests.yml +++ b/.github/workflows/run_tests.yml @@ -74,18 +74,19 @@ jobs: uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 with: version: '9' - - name: ./script/prettier + - id: prettier + name: steps::prettier run: ./script/prettier shell: bash -euxo pipefail {0} - - name: steps::cargo_fmt + - id: cargo_fmt + name: steps::cargo_fmt run: cargo fmt --all -- --check shell: bash -euxo pipefail {0} - - name: steps::trigger_autofix - if: failure() && github.event_name == 'pull_request' && github.actor != 'zed-zippy[bot]' - run: gh workflow run autofix_pr.yml -f pr_number=${{ github.event.pull_request.number }} -f run_clippy=false + - id: record_style_failure + name: steps::record_style_failure + if: always() + run: echo "failed=${{ steps.prettier.outcome == 'failure' || steps.cargo_fmt.outcome == 'failure' }}" >> "$GITHUB_OUTPUT" shell: bash -euxo pipefail {0} - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: ./script/check-todos run: ./script/check-todos shell: bash -euxo pipefail {0} @@ -96,6 +97,8 @@ jobs: uses: crate-ci/typos@2d0ce569feab1f8752f1dde43cc2f2aa53236e06 with: config: ./typos.toml + outputs: + style_failed: ${{ steps.record_style_failure.outputs.failed == 'true' }} timeout-minutes: 60 run_tests_windows: needs: @@ -116,7 +119,8 @@ jobs: uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 with: node-version: '20' - - name: steps::clippy + - id: clippy + name: steps::clippy run: ./script/clippy.ps1 shell: pwsh - name: steps::clear_target_dir_if_large @@ -163,15 +167,15 @@ jobs: uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 with: node-version: '20' - - name: steps::clippy + - id: clippy + name: steps::clippy run: ./script/clippy shell: bash -euxo pipefail {0} - - name: steps::trigger_autofix - if: failure() && github.event_name == 'pull_request' && github.actor != 'zed-zippy[bot]' - run: gh workflow run autofix_pr.yml -f pr_number=${{ github.event.pull_request.number }} -f run_clippy=true + - id: record_clippy_failure + name: steps::record_clippy_failure + if: always() + run: echo "failed=${{ steps.clippy.outcome == 'failure' }}" >> "$GITHUB_OUTPUT" shell: bash -euxo pipefail {0} - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: steps::cargo_install_nextest uses: taiki-e/install-action@nextest - name: steps::clear_target_dir_if_large @@ -185,6 +189,8 @@ jobs: run: | rm -rf ./../.cargo shell: bash -euxo pipefail {0} + outputs: + clippy_failed: ${{ steps.record_clippy_failure.outputs.failed == 'true' }} timeout-minutes: 60 run_tests_mac: needs: @@ -205,7 +211,8 @@ jobs: uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 with: node-version: '20' - - name: steps::clippy + - id: clippy + name: steps::clippy run: ./script/clippy shell: bash -euxo pipefail {0} - name: steps::clear_target_dir_if_large @@ -585,6 +592,24 @@ jobs: exit $EXIT_CODE shell: bash -euxo pipefail {0} + call_autofix: + needs: + - check_style + - run_tests_linux + if: always() && (needs.check_style.outputs.style_failed == 'true' || needs.run_tests_linux.outputs.clippy_failed == 'true') && github.event_name == 'pull_request' && github.actor != 'zed-zippy[bot]' + runs-on: namespace-profile-2x4-ubuntu-2404 + steps: + - id: get-app-token + name: steps::authenticate_as_zippy + uses: actions/create-github-app-token@bef1eaf1c0ac2b148ee2a0a74c65fbe6db0631f1 + with: + app-id: ${{ secrets.ZED_ZIPPY_APP_ID }} + private-key: ${{ secrets.ZED_ZIPPY_APP_PRIVATE_KEY }} + - name: run_tests::call_autofix::dispatch_autofix + run: gh workflow run autofix_pr.yml -f pr_number=${{ github.event.pull_request.number }} -f run_clippy=${{ needs.run_tests_linux.outputs.clippy_failed == 'true' }} + shell: bash -euxo pipefail {0} + env: + GITHUB_TOKEN: ${{ steps.get-app-token.outputs.token }} concurrency: group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name == 'main' && github.sha || 'anysha' }} cancel-in-progress: true diff --git a/assets/prompts/content_prompt_v2.hbs b/assets/prompts/content_prompt_v2.hbs index 87376f49f12f0e27cc61e9f9747d9de6bfde43cb..826aada8c04863c21d756cf99beb64e582ed4906 100644 --- a/assets/prompts/content_prompt_v2.hbs +++ b/assets/prompts/content_prompt_v2.hbs @@ -14,7 +14,6 @@ The section you'll need to rewrite is marked with The context around the relevant section has been truncated (possibly in the middle of a line) for brevity. {{/if}} -{{#if rewrite_section}} And here's the section to rewrite based on that prompt again for reference: @@ -33,8 +32,6 @@ Below are the diagnostic errors visible to the user. If the user requests probl {{/each}} {{/if}} -{{/if}} - Only make changes that are necessary to fulfill the prompt, leave everything else as-is. All surrounding {{content_type}} will be preserved. Start at the indentation level in the original file in the rewritten {{content_type}}. diff --git a/assets/settings/default.json b/assets/settings/default.json index 46324130ecfefefab74c26c6b48683015f766d08..d59516f6883afd93abfac6777eb855d569db2ac2 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -1705,7 +1705,12 @@ // } // "file_types": { - "JSONC": ["**/.zed/**/*.json", "**/zed/**/*.json", "**/Zed/**/*.json", "**/.vscode/**/*.json", "tsconfig*.json"], + "JSONC": [ + "**/.zed/*.json", + "**/.vscode/**/*.json", + "**/{zed,Zed}/{settings,keymap,tasks,debug}.json", + "tsconfig*.json", + ], "Markdown": [".rules", ".cursorrules", ".windsurfrules", ".clinerules"], "Shell Script": [".env.*"], }, diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index 2ec6347fd4aa088d7ae2cc8f5a7b6cef37d3b202..a994cc8e57e4456ec57092b2257269b104af74c7 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -192,6 +192,7 @@ pub struct ToolCall { pub locations: Vec, pub resolved_locations: Vec>, pub raw_input: Option, + pub raw_input_markdown: Option>, pub raw_output: Option, } @@ -222,6 +223,11 @@ impl ToolCall { } } + let raw_input_markdown = tool_call + .raw_input + .as_ref() + .and_then(|input| markdown_for_raw_output(input, &language_registry, cx)); + let result = Self { id: tool_call.tool_call_id, label: cx @@ -232,6 +238,7 @@ impl ToolCall { resolved_locations: Vec::default(), status, raw_input: tool_call.raw_input, + raw_input_markdown, raw_output: tool_call.raw_output, }; Ok(result) @@ -307,6 +314,7 @@ impl ToolCall { } if let Some(raw_input) = raw_input { + self.raw_input_markdown = markdown_for_raw_output(&raw_input, &language_registry, cx); self.raw_input = Some(raw_input); } @@ -1355,6 +1363,7 @@ impl AcpThread { locations: Vec::new(), resolved_locations: Vec::new(), raw_input: None, + raw_input_markdown: None, raw_output: None, }; self.push_entry(AgentThreadEntry::ToolCall(failed_tool_call), cx); diff --git a/crates/agent_ui/src/acp/model_selector.rs b/crates/agent_ui/src/acp/model_selector.rs index 216604fa5574631d3ae5467806a15da93c2351ad..903d5fe425d99389aae0e2a8028d9a31b986fbb3 100644 --- a/crates/agent_ui/src/acp/model_selector.rs +++ b/crates/agent_ui/src/acp/model_selector.rs @@ -221,7 +221,7 @@ impl PickerDelegate for AcpModelPickerDelegate { cx: &mut Context>, ) -> Task<()> { let favorites = if self.selector.supports_favorites() { - Arc::new(AgentSettings::get_global(cx).favorite_model_ids()) + AgentSettings::get_global(cx).favorite_model_ids() } else { Default::default() }; @@ -242,7 +242,7 @@ impl PickerDelegate for AcpModelPickerDelegate { this.update_in(cx, |this, window, cx| { this.delegate.filtered_entries = - info_list_to_picker_entries(filtered_models, favorites); + info_list_to_picker_entries(filtered_models, &favorites); // Finds the currently selected model in the list let new_index = this .delegate @@ -410,7 +410,7 @@ impl PickerDelegate for AcpModelPickerDelegate { fn info_list_to_picker_entries( model_list: AgentModelList, - favorites: Arc>, + favorites: &HashSet, ) -> Vec { let mut entries = Vec::new(); @@ -576,13 +576,11 @@ mod tests { } } - fn create_favorites(models: Vec<&str>) -> Arc> { - Arc::new( - models - .into_iter() - .map(|m| ModelId::new(m.to_string())) - .collect(), - ) + fn create_favorites(models: Vec<&str>) -> HashSet { + models + .into_iter() + .map(|m| ModelId::new(m.to_string())) + .collect() } fn get_entry_model_ids(entries: &[AcpModelPickerEntry]) -> Vec<&str> { @@ -613,7 +611,7 @@ mod tests { ]); let favorites = create_favorites(vec!["zed/gemini"]); - let entries = info_list_to_picker_entries(models, favorites); + let entries = info_list_to_picker_entries(models, &favorites); assert!(matches!( entries.first(), @@ -629,7 +627,7 @@ mod tests { let models = create_model_list(vec![("zed", vec!["zed/claude", "zed/gemini"])]); let favorites = create_favorites(vec![]); - let entries = info_list_to_picker_entries(models, favorites); + let entries = info_list_to_picker_entries(models, &favorites); assert!(matches!( entries.first(), @@ -645,7 +643,7 @@ mod tests { ]); let favorites = create_favorites(vec!["zed/claude"]); - let entries = info_list_to_picker_entries(models, favorites); + let entries = info_list_to_picker_entries(models, &favorites); for entry in &entries { if let AcpModelPickerEntry::Model(info, is_favorite) = entry { @@ -666,7 +664,7 @@ mod tests { ]); let favorites = create_favorites(vec!["zed/gemini", "openai/gpt-5"]); - let entries = info_list_to_picker_entries(models, favorites); + let entries = info_list_to_picker_entries(models, &favorites); let model_ids = get_entry_model_ids(&entries); assert_eq!(model_ids[0], "zed/gemini"); @@ -687,7 +685,7 @@ mod tests { let favorites = create_favorites(vec!["zed/claude"]); - let entries = info_list_to_picker_entries(models, favorites); + let entries = info_list_to_picker_entries(models, &favorites); let labels = get_entry_labels(&entries); assert_eq!( @@ -727,7 +725,7 @@ mod tests { ]); let favorites = create_favorites(vec!["zed/gemini"]); - let entries = info_list_to_picker_entries(models, favorites); + let entries = info_list_to_picker_entries(models, &favorites); assert!(matches!( entries.first(), diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 9e9af499727ad8478fa5fc1d46dc3b3bf8e20a71..fe6a3a3087066946a2973067d4439b63de60bdf0 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -34,7 +34,7 @@ use language::Buffer; use language_model::LanguageModelRegistry; use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle}; -use project::{Project, ProjectEntryId}; +use project::{AgentServerStore, ExternalAgentServerName, Project, ProjectEntryId}; use prompt_store::{PromptId, PromptStore}; use rope::Point; use settings::{NotifyWhenAgentWaiting, Settings as _, SettingsStore}; @@ -260,6 +260,7 @@ impl ThreadFeedbackState { pub struct AcpThreadView { agent: Rc, + agent_server_store: Entity, workspace: WeakEntity, project: Entity, thread_state: ThreadState, @@ -406,6 +407,7 @@ impl AcpThreadView { Self { agent: agent.clone(), + agent_server_store, workspace: workspace.clone(), project: project.clone(), entry_view_state, @@ -737,7 +739,7 @@ impl AcpThreadView { cx: &mut App, ) { let agent_name = agent.name(); - let (configuration_view, subscription) = if let Some(provider_id) = err.provider_id { + let (configuration_view, subscription) = if let Some(provider_id) = &err.provider_id { let registry = LanguageModelRegistry::global(cx); let sub = window.subscribe(®istry, cx, { @@ -779,7 +781,6 @@ impl AcpThreadView { configuration_view, description: err .description - .clone() .map(|desc| cx.new(|cx| Markdown::new(desc.into(), None, None, cx))), _subscription: subscription, }; @@ -1088,10 +1089,7 @@ impl AcpThreadView { window.defer(cx, |window, cx| { Self::handle_auth_required( this, - AuthRequired { - description: None, - provider_id: None, - }, + AuthRequired::new(), agent, connection, window, @@ -1663,44 +1661,6 @@ impl AcpThreadView { }); return; } - } else if method.0.as_ref() == "anthropic-api-key" { - let registry = LanguageModelRegistry::global(cx); - let provider = registry - .read(cx) - .provider(&language_model::ANTHROPIC_PROVIDER_ID) - .unwrap(); - let this = cx.weak_entity(); - let agent = self.agent.clone(); - let connection = connection.clone(); - window.defer(cx, move |window, cx| { - if !provider.is_authenticated(cx) { - Self::handle_auth_required( - this, - AuthRequired { - description: Some("ANTHROPIC_API_KEY must be set".to_owned()), - provider_id: Some(language_model::ANTHROPIC_PROVIDER_ID), - }, - agent, - connection, - window, - cx, - ); - } else { - this.update(cx, |this, cx| { - this.thread_state = Self::initial_state( - agent, - None, - this.workspace.clone(), - this.project.clone(), - true, - window, - cx, - ) - }) - .ok(); - } - }); - return; } else if method.0.as_ref() == "vertex-ai" && std::env::var("GOOGLE_API_KEY").is_err() && (std::env::var("GOOGLE_CLOUD_PROJECT").is_err() @@ -2440,6 +2400,12 @@ impl AcpThreadView { let is_collapsible = !tool_call.content.is_empty() && !needs_confirmation; let is_open = needs_confirmation || self.expanded_tool_calls.contains(&tool_call.id); + let input_output_header = |label: SharedString| { + Label::new(label) + .size(LabelSize::XSmall) + .color(Color::Muted) + .buffer_font(cx) + }; let tool_output_display = if is_open { @@ -2481,7 +2447,25 @@ impl AcpThreadView { | ToolCallStatus::Completed | ToolCallStatus::Failed | ToolCallStatus::Canceled => v_flex() + .mt_1p5() .w_full() + .child( + v_flex() + .ml(rems(0.4)) + .px_3p5() + .pb_1() + .gap_1() + .border_l_1() + .border_color(self.tool_card_border_color(cx)) + .child(input_output_header("Raw Input".into())) + .children(tool_call.raw_input_markdown.clone().map(|input| { + self.render_markdown( + input, + default_markdown_style(false, false, window, cx), + ) + })) + .child(input_output_header("Output:".into())), + ) .children(tool_call.content.iter().enumerate().map( |(content_ix, content)| { div().child(self.render_tool_call_content( @@ -2580,7 +2564,7 @@ impl AcpThreadView { .gap_px() .when(is_collapsible, |this| { this.child( - Disclosure::new(("expand", entry_ix), is_open) + Disclosure::new(("expand-output", entry_ix), is_open) .opened_icon(IconName::ChevronUp) .closed_icon(IconName::ChevronDown) .visible_on_hover(&card_header_id) @@ -2766,7 +2750,6 @@ impl AcpThreadView { let button_id = SharedString::from(format!("tool_output-{:?}", tool_call_id)); v_flex() - .mt_1p5() .gap_2() .when(!card_layout, |this| { this.ml(rems(0.4)) @@ -3500,138 +3483,119 @@ impl AcpThreadView { pending_auth_method: Option<&acp::AuthMethodId>, window: &mut Window, cx: &Context, - ) -> Div { - let show_description = - configuration_view.is_none() && description.is_none() && pending_auth_method.is_none(); - + ) -> impl IntoElement { let auth_methods = connection.auth_methods(); - v_flex().flex_1().size_full().justify_end().child( - v_flex() - .p_2() - .pr_3() - .w_full() - .gap_1() - .border_t_1() - .border_color(cx.theme().colors().border) - .bg(cx.theme().status().warning.opacity(0.04)) - .child( - h_flex() - .gap_1p5() - .child( - Icon::new(IconName::Warning) - .color(Color::Warning) - .size(IconSize::Small), - ) - .child(Label::new("Authentication Required").size(LabelSize::Small)), - ) - .children(description.map(|desc| { - div().text_ui(cx).child(self.render_markdown( - desc.clone(), - default_markdown_style(false, false, window, cx), - )) - })) - .children( - configuration_view - .cloned() - .map(|view| div().w_full().child(view)), - ) - .when(show_description, |el| { - el.child( - Label::new(format!( - "You are not currently authenticated with {}.{}", - self.agent.name(), - if auth_methods.len() > 1 { - " Please choose one of the following options:" - } else { - "" - } - )) - .size(LabelSize::Small) - .color(Color::Muted) - .mb_1() - .ml_5(), - ) - }) - .when_some(pending_auth_method, |el, _| { - el.child( - h_flex() - .py_4() - .w_full() - .justify_center() - .gap_1() - .child( - Icon::new(IconName::ArrowCircle) - .size(IconSize::Small) - .color(Color::Muted) - .with_rotate_animation(2), - ) - .child(Label::new("Authenticating…").size(LabelSize::Small)), - ) - }) - .when(!auth_methods.is_empty(), |this| { - this.child( - h_flex() - .justify_end() - .flex_wrap() - .gap_1() - .when(!show_description, |this| { - this.border_t_1() - .mt_1() - .pt_2() - .border_color(cx.theme().colors().border.opacity(0.8)) + let agent_display_name = self + .agent_server_store + .read(cx) + .agent_display_name(&ExternalAgentServerName(self.agent.name())) + .unwrap_or_else(|| self.agent.name()); + + let show_fallback_description = auth_methods.len() > 1 + && configuration_view.is_none() + && description.is_none() + && pending_auth_method.is_none(); + + let auth_buttons = || { + h_flex().justify_end().flex_wrap().gap_1().children( + connection + .auth_methods() + .iter() + .enumerate() + .rev() + .map(|(ix, method)| { + let (method_id, name) = if self.project.read(cx).is_via_remote_server() + && method.id.0.as_ref() == "oauth-personal" + && method.name == "Log in with Google" + { + ("spawn-gemini-cli".into(), "Log in with Gemini CLI".into()) + } else { + (method.id.0.clone(), method.name.clone()) + }; + + let agent_telemetry_id = connection.telemetry_id(); + + Button::new(method_id.clone(), name) + .label_size(LabelSize::Small) + .map(|this| { + if ix == 0 { + this.style(ButtonStyle::Tinted(TintColor::Accent)) + } else { + this.style(ButtonStyle::Outlined) + } }) - .children(connection.auth_methods().iter().enumerate().rev().map( - |(ix, method)| { - let (method_id, name) = if self - .project - .read(cx) - .is_via_remote_server() - && method.id.0.as_ref() == "oauth-personal" - && method.name == "Log in with Google" - { - ("spawn-gemini-cli".into(), "Log in with Gemini CLI".into()) - } else { - (method.id.0.clone(), method.name.clone()) - }; + .when_some(method.description.clone(), |this, description| { + this.tooltip(Tooltip::text(description)) + }) + .on_click({ + cx.listener(move |this, _, window, cx| { + telemetry::event!( + "Authenticate Agent Started", + agent = agent_telemetry_id, + method = method_id + ); - let agent_telemetry_id = connection.telemetry_id(); + this.authenticate( + acp::AuthMethodId::new(method_id.clone()), + window, + cx, + ) + }) + }) + }), + ) + }; - Button::new(method_id.clone(), name) - .label_size(LabelSize::Small) - .map(|this| { - if ix == 0 { - this.style(ButtonStyle::Tinted(TintColor::Warning)) - } else { - this.style(ButtonStyle::Outlined) - } - }) - .when_some( - method.description.clone(), - |this, description| { - this.tooltip(Tooltip::text(description)) - }, - ) - .on_click({ - cx.listener(move |this, _, window, cx| { - telemetry::event!( - "Authenticate Agent Started", - agent = agent_telemetry_id, - method = method_id - ); - - this.authenticate( - acp::AuthMethodId::new(method_id.clone()), - window, - cx, - ) - }) - }) - }, - )), - ) - }), - ) + if pending_auth_method.is_some() { + return Callout::new() + .icon(IconName::Info) + .title(format!("Authenticating to {}…", agent_display_name)) + .actions_slot( + Icon::new(IconName::ArrowCircle) + .size(IconSize::Small) + .color(Color::Muted) + .with_rotate_animation(2) + .into_any_element(), + ) + .into_any_element(); + } + + Callout::new() + .icon(IconName::Info) + .title(format!("Authenticate to {}", agent_display_name)) + .when(auth_methods.len() == 1, |this| { + this.actions_slot(auth_buttons()) + }) + .description_slot( + v_flex() + .text_ui(cx) + .map(|this| { + if show_fallback_description { + this.child( + Label::new("Choose one of the following authentication options:") + .size(LabelSize::Small) + .color(Color::Muted), + ) + } else { + this.children( + configuration_view + .cloned() + .map(|view| div().w_full().child(view)), + ) + .children(description.map(|desc| { + self.render_markdown( + desc.clone(), + default_markdown_style(false, false, window, cx), + ) + })) + } + }) + .when(auth_methods.len() > 1, |this| { + this.gap_1().child(auth_buttons()) + }), + ) + .into_any_element() } fn render_load_error( @@ -5880,10 +5844,6 @@ impl AcpThreadView { }; let connection = thread.read(cx).connection().clone(); - let err = AuthRequired { - description: None, - provider_id: None, - }; this.clear_thread_error(cx); if let Some(message) = this.in_flight_prompt.take() { this.message_editor.update(cx, |editor, cx| { @@ -5892,7 +5852,14 @@ impl AcpThreadView { } let this = cx.weak_entity(); window.defer(cx, |window, cx| { - Self::handle_auth_required(this, err, agent, connection, window, cx); + Self::handle_auth_required( + this, + AuthRequired::new(), + agent, + connection, + window, + cx, + ); }) } })) @@ -5905,14 +5872,10 @@ impl AcpThreadView { }; let connection = thread.read(cx).connection().clone(); - let err = AuthRequired { - description: None, - provider_id: None, - }; self.clear_thread_error(cx); let this = cx.weak_entity(); window.defer(cx, |window, cx| { - Self::handle_auth_required(this, err, agent, connection, window, cx); + Self::handle_auth_required(this, AuthRequired::new(), agent, connection, window, cx); }) } @@ -6015,16 +5978,19 @@ impl Render for AcpThreadView { configuration_view, pending_auth_method, .. - } => self - .render_auth_required_state( + } => v_flex() + .flex_1() + .size_full() + .justify_end() + .child(self.render_auth_required_state( connection, description.as_ref(), configuration_view.as_ref(), pending_auth_method.as_ref(), window, cx, - ) - .into_any(), + )) + .into_any_element(), ThreadState::Loading { .. } => v_flex() .flex_1() .child(self.render_recent_history(cx)) diff --git a/crates/agent_ui/src/buffer_codegen.rs b/crates/agent_ui/src/buffer_codegen.rs index 87ce6d386b38f31a0d7b550aab00bb766ce75010..a296d4d20918fba6eb32bfcf7fcc657f9db2b3ac 100644 --- a/crates/agent_ui/src/buffer_codegen.rs +++ b/crates/agent_ui/src/buffer_codegen.rs @@ -75,6 +75,9 @@ pub struct BufferCodegen { session_id: Uuid, } +pub const REWRITE_SECTION_TOOL_NAME: &str = "rewrite_section"; +pub const FAILURE_MESSAGE_TOOL_NAME: &str = "failure_message"; + impl BufferCodegen { pub fn new( buffer: Entity, @@ -522,12 +525,12 @@ impl CodegenAlternative { let tools = vec![ LanguageModelRequestTool { - name: "rewrite_section".to_string(), + name: REWRITE_SECTION_TOOL_NAME.to_string(), description: "Replaces text in tags with your replacement_text.".to_string(), input_schema: language_model::tool_schema::root_schema_for::(tool_input_format).to_value(), }, LanguageModelRequestTool { - name: "failure_message".to_string(), + name: FAILURE_MESSAGE_TOOL_NAME.to_string(), description: "Use this tool to provide a message to the user when you're unable to complete a task.".to_string(), input_schema: language_model::tool_schema::root_schema_for::(tool_input_format).to_value(), }, @@ -1167,7 +1170,7 @@ impl CodegenAlternative { let process_tool_use = move |tool_use: LanguageModelToolUse| -> Option { let mut chars_read_so_far = chars_read_so_far.lock(); match tool_use.name.as_ref() { - "rewrite_section" => { + REWRITE_SECTION_TOOL_NAME => { let Ok(input) = serde_json::from_value::(tool_use.input) else { @@ -1180,7 +1183,7 @@ impl CodegenAlternative { description: None, }) } - "failure_message" => { + FAILURE_MESSAGE_TOOL_NAME => { let Ok(mut input) = serde_json::from_value::(tool_use.input) else { @@ -1493,7 +1496,10 @@ mod tests { use indoc::indoc; use language::{Buffer, Point}; use language_model::fake_provider::FakeLanguageModel; - use language_model::{LanguageModelRegistry, TokenUsage}; + use language_model::{ + LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelRegistry, + LanguageModelToolUse, StopReason, TokenUsage, + }; use languages::rust_lang; use rand::prelude::*; use settings::SettingsStore; @@ -1805,6 +1811,51 @@ mod tests { ); } + // When not streaming tool calls, we strip backticks as part of parsing the model's + // plain text response. This is a regression test for a bug where we stripped + // backticks incorrectly. + #[gpui::test] + async fn test_allows_model_to_output_backticks(cx: &mut TestAppContext) { + init_test(cx); + let text = "- Improved; `cmd+click` behavior. Now requires `cmd` to be pressed before the click starts or it doesn't run. ([#44579](https://github.com/zed-industries/zed/pull/44579); thanks [Zachiah](https://github.com/Zachiah))"; + let buffer = cx.new(|cx| Buffer::local("", cx)); + let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); + let range = buffer.read_with(cx, |buffer, cx| { + let snapshot = buffer.snapshot(cx); + snapshot.anchor_before(Point::new(0, 0))..snapshot.anchor_after(Point::new(0, 0)) + }); + let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap()); + let codegen = cx.new(|cx| { + CodegenAlternative::new( + buffer.clone(), + range.clone(), + true, + prompt_builder, + Uuid::new_v4(), + cx, + ) + }); + + let events_tx = simulate_tool_based_completion(&codegen, cx); + let chunk_len = text.find('`').unwrap(); + events_tx + .unbounded_send(rewrite_tool_use("tool_1", &text[..chunk_len], false)) + .unwrap(); + events_tx + .unbounded_send(rewrite_tool_use("tool_2", &text, true)) + .unwrap(); + events_tx + .unbounded_send(LanguageModelCompletionEvent::Stop(StopReason::EndTurn)) + .unwrap(); + drop(events_tx); + cx.run_until_parked(); + + assert_eq!( + buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx).text()), + text + ); + } + #[gpui::test] async fn test_strip_invalid_spans_from_codeblock() { assert_chunks("Lorem ipsum dolor", "Lorem ipsum dolor").await; @@ -1870,4 +1921,39 @@ mod tests { }); chunks_tx } + + fn simulate_tool_based_completion( + codegen: &Entity, + cx: &mut TestAppContext, + ) -> mpsc::UnboundedSender { + let (events_tx, events_rx) = mpsc::unbounded(); + let model = Arc::new(FakeLanguageModel::default()); + codegen.update(cx, |codegen, cx| { + let completion_stream = Task::ready(Ok(events_rx.map(Ok).boxed() + as BoxStream< + 'static, + Result, + >)); + codegen.generation = codegen.handle_completion(model, completion_stream, cx); + }); + events_tx + } + + fn rewrite_tool_use( + id: &str, + replacement_text: &str, + is_complete: bool, + ) -> LanguageModelCompletionEvent { + let input = RewriteSectionInput { + replacement_text: replacement_text.into(), + }; + LanguageModelCompletionEvent::ToolUse(LanguageModelToolUse { + id: id.into(), + name: REWRITE_SECTION_TOOL_NAME.into(), + raw_input: serde_json::to_string(&input).unwrap(), + input: serde_json::to_value(&input).unwrap(), + is_input_complete: is_complete, + thought_signature: None, + }) + } } diff --git a/crates/agent_ui/src/inline_assistant.rs b/crates/agent_ui/src/inline_assistant.rs index 052d8598a76d1044c6d97b5378041b5cd12e23b3..671579f9ef018b495b7993279a852595c78d3e02 100644 --- a/crates/agent_ui/src/inline_assistant.rs +++ b/crates/agent_ui/src/inline_assistant.rs @@ -2271,6 +2271,36 @@ pub mod evals { ); } + #[test] + #[cfg_attr(not(feature = "unit-eval"), ignore)] + fn eval_empty_buffer() { + run_eval( + 20, + 1.0, + "Write a Python hello, world program".to_string(), + "ˇ".to_string(), + |output| match output { + InlineAssistantOutput::Success { + full_buffer_text, .. + } => { + if full_buffer_text.is_empty() { + EvalOutput::failed("expected some output".to_string()) + } else { + EvalOutput::passed(format!("Produced {full_buffer_text}")) + } + } + o @ InlineAssistantOutput::Failure { .. } => EvalOutput::failed(format!( + "Assistant output does not match expected output: {:?}", + o + )), + o @ InlineAssistantOutput::Malformed { .. } => EvalOutput::failed(format!( + "Assistant output does not match expected output: {:?}", + o + )), + }, + ); + } + fn run_eval( iterations: usize, expected_pass_ratio: f32, diff --git a/crates/codestral/src/codestral.rs b/crates/codestral/src/codestral.rs index 9bf0296ac357937cd1ad1470dba9a98864911de9..9cf2fab80b78ba06c6a2523013e2f73934f50052 100644 --- a/crates/codestral/src/codestral.rs +++ b/crates/codestral/src/codestral.rs @@ -1,6 +1,6 @@ use anyhow::{Context as _, Result}; use edit_prediction_context::{EditPredictionExcerpt, EditPredictionExcerptOptions}; -use edit_prediction_types::{Direction, EditPrediction, EditPredictionDelegate}; +use edit_prediction_types::{EditPrediction, EditPredictionDelegate}; use futures::AsyncReadExt; use gpui::{App, Context, Entity, Task}; use http_client::HttpClient; @@ -300,16 +300,6 @@ impl EditPredictionDelegate for CodestralEditPredictionDelegate { })); } - fn cycle( - &mut self, - _buffer: Entity, - _cursor_position: Anchor, - _direction: Direction, - _cx: &mut Context, - ) { - // Codestral doesn't support multiple completions, so cycling does nothing - } - fn accept(&mut self, _cx: &mut Context) { log::debug!("Codestral: Completion accepted"); self.pending_request = None; diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index f248fbdb43ec37b19ca951992df6a7ddbc4f7313..a6963296f5c0ce0395698d2952618123c103ff55 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -4,6 +4,7 @@ pub mod copilot_responses; pub mod request; mod sign_in; +use crate::request::NextEditSuggestions; use crate::sign_in::initiate_sign_out; use ::fs::Fs; use anyhow::{Context as _, Result, anyhow}; @@ -18,7 +19,7 @@ use http_client::HttpClient; use language::language_settings::CopilotSettings; use language::{ Anchor, Bias, Buffer, BufferSnapshot, Language, PointUtf16, ToPointUtf16, - language_settings::{EditPredictionProvider, all_language_settings, language_settings}, + language_settings::{EditPredictionProvider, all_language_settings}, point_from_lsp, point_to_lsp, }; use lsp::{LanguageServer, LanguageServerBinary, LanguageServerId, LanguageServerName}; @@ -40,7 +41,7 @@ use std::{ sync::Arc, }; use sum_tree::Dimensions; -use util::{ResultExt, fs::remove_matching, rel_path::RelPath}; +use util::{ResultExt, fs::remove_matching}; use workspace::Workspace; pub use crate::copilot_edit_prediction_delegate::CopilotEditPredictionDelegate; @@ -315,6 +316,15 @@ struct GlobalCopilot(Entity); impl Global for GlobalCopilot {} +/// Copilot's NextEditSuggestion response, with coordinates converted to Anchors. +struct CopilotEditPrediction { + buffer: Entity, + range: Range, + text: String, + command: Option, + snapshot: BufferSnapshot, +} + impl Copilot { pub fn global(cx: &App) -> Option> { cx.try_global::() @@ -873,101 +883,19 @@ impl Copilot { } } - pub fn completions( - &mut self, - buffer: &Entity, - position: T, - cx: &mut Context, - ) -> Task>> - where - T: ToPointUtf16, - { - self.request_completions::(buffer, position, cx) - } - - pub fn completions_cycling( + pub(crate) fn completions( &mut self, buffer: &Entity, - position: T, + position: Anchor, cx: &mut Context, - ) -> Task>> - where - T: ToPointUtf16, - { - self.request_completions::(buffer, position, cx) - } - - pub fn accept_completion( - &mut self, - completion: &Completion, - cx: &mut Context, - ) -> Task> { - let server = match self.server.as_authenticated() { - Ok(server) => server, - Err(error) => return Task::ready(Err(error)), - }; - let request = - server - .lsp - .request::(request::NotifyAcceptedParams { - uuid: completion.uuid.clone(), - }); - cx.background_spawn(async move { - request - .await - .into_response() - .context("copilot: notify accepted")?; - Ok(()) - }) - } - - pub fn discard_completions( - &mut self, - completions: &[Completion], - cx: &mut Context, - ) -> Task> { - let server = match self.server.as_authenticated() { - Ok(server) => server, - Err(_) => return Task::ready(Ok(())), - }; - let request = - server - .lsp - .request::(request::NotifyRejectedParams { - uuids: completions - .iter() - .map(|completion| completion.uuid.clone()) - .collect(), - }); - cx.background_spawn(async move { - request - .await - .into_response() - .context("copilot: notify rejected")?; - Ok(()) - }) - } - - fn request_completions( - &mut self, - buffer: &Entity, - position: T, - cx: &mut Context, - ) -> Task>> - where - R: 'static - + lsp::request::Request< - Params = request::GetCompletionsParams, - Result = request::GetCompletionsResult, - >, - T: ToPointUtf16, - { + ) -> Task>> { self.register_buffer(buffer, cx); let server = match self.server.as_authenticated() { Ok(server) => server, Err(error) => return Task::ready(Err(error)), }; + let buffer_entity = buffer.clone(); let lsp = server.lsp.clone(); let registered_buffer = server .registered_buffers @@ -977,46 +905,31 @@ impl Copilot { let buffer = buffer.read(cx); let uri = registered_buffer.uri.clone(); let position = position.to_point_utf16(buffer); - let settings = language_settings( - buffer.language_at(position).map(|l| l.name()), - buffer.file(), - cx, - ); - let tab_size = settings.tab_size; - let hard_tabs = settings.hard_tabs; - let relative_path = buffer - .file() - .map_or(RelPath::empty().into(), |file| file.path().clone()); cx.background_spawn(async move { let (version, snapshot) = snapshot.await?; let result = lsp - .request::(request::GetCompletionsParams { - doc: request::GetCompletionsDocument { - uri, - tab_size: tab_size.into(), - indent_size: 1, - insert_spaces: !hard_tabs, - relative_path: relative_path.to_proto(), - position: point_to_lsp(position), - version: version.try_into().unwrap(), - }, + .request::(request::NextEditSuggestionsParams { + text_document: lsp::VersionedTextDocumentIdentifier { uri, version }, + position: point_to_lsp(position), }) .await .into_response() .context("copilot: get completions")?; let completions = result - .completions + .edits .into_iter() .map(|completion| { let start = snapshot .clip_point_utf16(point_from_lsp(completion.range.start), Bias::Left); let end = snapshot.clip_point_utf16(point_from_lsp(completion.range.end), Bias::Left); - Completion { - uuid: completion.uuid, + CopilotEditPrediction { + buffer: buffer_entity.clone(), range: snapshot.anchor_before(start)..snapshot.anchor_after(end), text: completion.text, + command: completion.command, + snapshot: snapshot.clone(), } }) .collect(); @@ -1024,6 +937,35 @@ impl Copilot { }) } + pub(crate) fn accept_completion( + &mut self, + completion: &CopilotEditPrediction, + cx: &mut Context, + ) -> Task> { + let server = match self.server.as_authenticated() { + Ok(server) => server, + Err(error) => return Task::ready(Err(error)), + }; + if let Some(command) = &completion.command { + let request = server + .lsp + .request::(lsp::ExecuteCommandParams { + command: command.command.clone(), + arguments: command.arguments.clone().unwrap_or_default(), + ..Default::default() + }); + cx.background_spawn(async move { + request + .await + .into_response() + .context("copilot: notify accepted")?; + Ok(()) + }) + } else { + Task::ready(Ok(())) + } + } + pub fn status(&self) -> Status { match &self.server { CopilotServer::Starting { task } => Status::Starting { task: task.clone() }, @@ -1260,7 +1202,11 @@ async fn get_copilot_lsp(fs: Arc, node_runtime: NodeRuntime) -> anyhow:: mod tests { use super::*; use gpui::TestAppContext; - use util::{path, paths::PathStyle, rel_path::rel_path}; + use util::{ + path, + paths::PathStyle, + rel_path::{RelPath, rel_path}, + }; #[gpui::test(iterations = 10)] async fn test_buffer_management(cx: &mut TestAppContext) { diff --git a/crates/copilot/src/copilot_edit_prediction_delegate.rs b/crates/copilot/src/copilot_edit_prediction_delegate.rs index bbda32e1102f096e96a41cbc59268f597b1629ba..514e135cb4c34f6a1f49687fcd413113f78f9eae 100644 --- a/crates/copilot/src/copilot_edit_prediction_delegate.rs +++ b/crates/copilot/src/copilot_edit_prediction_delegate.rs @@ -1,49 +1,29 @@ -use crate::{Completion, Copilot}; +use crate::{Copilot, CopilotEditPrediction}; use anyhow::Result; -use edit_prediction_types::{Direction, EditPrediction, EditPredictionDelegate}; -use gpui::{App, Context, Entity, EntityId, Task}; -use language::{Buffer, OffsetRangeExt, ToOffset, language_settings::AllLanguageSettings}; -use settings::Settings; -use std::{path::Path, time::Duration}; +use edit_prediction_types::{EditPrediction, EditPredictionDelegate, interpolate_edits}; +use gpui::{App, Context, Entity, Task}; +use language::{Anchor, Buffer, EditPreview, OffsetRangeExt}; +use std::{ops::Range, sync::Arc, time::Duration}; pub const COPILOT_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(75); pub struct CopilotEditPredictionDelegate { - cycled: bool, - buffer_id: Option, - completions: Vec, - active_completion_index: usize, - file_extension: Option, + completion: Option<(CopilotEditPrediction, EditPreview)>, pending_refresh: Option>>, - pending_cycling_refresh: Option>>, copilot: Entity, } impl CopilotEditPredictionDelegate { pub fn new(copilot: Entity) -> Self { Self { - cycled: false, - buffer_id: None, - completions: Vec::new(), - active_completion_index: 0, - file_extension: None, + completion: None, pending_refresh: None, - pending_cycling_refresh: None, copilot, } } - fn active_completion(&self) -> Option<&Completion> { - self.completions.get(self.active_completion_index) - } - - fn push_completion(&mut self, new_completion: Completion) { - for completion in &self.completions { - if completion.text == new_completion.text && completion.range == new_completion.range { - return; - } - } - self.completions.push(new_completion); + fn active_completion(&self) -> Option<&(CopilotEditPrediction, EditPreview)> { + self.completion.as_ref() } } @@ -64,12 +44,8 @@ impl EditPredictionDelegate for CopilotEditPredictionDelegate { true } - fn supports_jump_to_edit() -> bool { - false - } - fn is_refreshing(&self, _cx: &App) -> bool { - self.pending_refresh.is_some() && self.completions.is_empty() + self.pending_refresh.is_some() && self.completion.is_none() } fn is_enabled( @@ -102,160 +78,96 @@ impl EditPredictionDelegate for CopilotEditPredictionDelegate { })? .await?; - this.update(cx, |this, cx| { - if !completions.is_empty() { - this.cycled = false; + if let Some(mut completion) = completions.into_iter().next() + && let Some(trimmed_completion) = cx + .update(|cx| trim_completion(&completion, cx)) + .ok() + .flatten() + { + let preview = buffer + .update(cx, |this, cx| { + this.preview_edits(Arc::from(std::slice::from_ref(&trimmed_completion)), cx) + })? + .await; + this.update(cx, |this, cx| { this.pending_refresh = None; - this.pending_cycling_refresh = None; - this.completions.clear(); - this.active_completion_index = 0; - this.buffer_id = Some(buffer.entity_id()); - this.file_extension = buffer.read(cx).file().and_then(|file| { - Some( - Path::new(file.file_name(cx)) - .extension()? - .to_str()? - .to_string(), - ) - }); - - for completion in completions { - this.push_completion(completion); - } + completion.range = trimmed_completion.0; + completion.text = trimmed_completion.1.to_string(); + this.completion = Some((completion, preview)); + cx.notify(); - } - })?; + })?; + } Ok(()) })); } - fn cycle( - &mut self, - buffer: Entity, - cursor_position: language::Anchor, - direction: Direction, - cx: &mut Context, - ) { - if self.cycled { - match direction { - Direction::Prev => { - self.active_completion_index = if self.active_completion_index == 0 { - self.completions.len().saturating_sub(1) - } else { - self.active_completion_index - 1 - }; - } - Direction::Next => { - if self.completions.is_empty() { - self.active_completion_index = 0 - } else { - self.active_completion_index = - (self.active_completion_index + 1) % self.completions.len(); - } - } - } - - cx.notify(); - } else { - let copilot = self.copilot.clone(); - self.pending_cycling_refresh = Some(cx.spawn(async move |this, cx| { - let completions = copilot - .update(cx, |copilot, cx| { - copilot.completions_cycling(&buffer, cursor_position, cx) - })? - .await?; - - this.update(cx, |this, cx| { - this.cycled = true; - this.file_extension = buffer.read(cx).file().and_then(|file| { - Some( - Path::new(file.file_name(cx)) - .extension()? - .to_str()? - .to_string(), - ) - }); - for completion in completions { - this.push_completion(completion); - } - this.cycle(buffer, cursor_position, direction, cx); - })?; - - Ok(()) - })); - } - } - fn accept(&mut self, cx: &mut Context) { - if let Some(completion) = self.active_completion() { + if let Some((completion, _)) = self.active_completion() { self.copilot .update(cx, |copilot, cx| copilot.accept_completion(completion, cx)) .detach_and_log_err(cx); } } - fn discard(&mut self, cx: &mut Context) { - let settings = AllLanguageSettings::get_global(cx); - - let copilot_enabled = settings.show_edit_predictions(None, cx); - - if !copilot_enabled { - return; - } - - self.copilot - .update(cx, |copilot, cx| { - copilot.discard_completions(&self.completions, cx) - }) - .detach_and_log_err(cx); - } + fn discard(&mut self, _: &mut Context) {} fn suggest( &mut self, buffer: &Entity, - cursor_position: language::Anchor, + _: language::Anchor, cx: &mut Context, ) -> Option { let buffer_id = buffer.entity_id(); let buffer = buffer.read(cx); - let completion = self.active_completion()?; - if Some(buffer_id) != self.buffer_id + let (completion, edit_preview) = self.active_completion()?; + + if Some(buffer_id) != Some(completion.buffer.entity_id()) || !completion.range.start.is_valid(buffer) || !completion.range.end.is_valid(buffer) { return None; } + let edits = vec![( + completion.range.clone(), + Arc::from(completion.text.as_ref()), + )]; + let edits = interpolate_edits(&completion.snapshot, &buffer.snapshot(), &edits) + .filter(|edits| !edits.is_empty())?; + + Some(EditPrediction::Local { + id: None, + edits, + edit_preview: Some(edit_preview.clone()), + }) + } +} - let mut completion_range = completion.range.to_offset(buffer); - let prefix_len = common_prefix( - buffer.chars_for_range(completion_range.clone()), - completion.text.chars(), - ); - completion_range.start += prefix_len; - let suffix_len = common_prefix( - buffer.reversed_chars_for_range(completion_range.clone()), - completion.text[prefix_len..].chars().rev(), - ); - completion_range.end = completion_range.end.saturating_sub(suffix_len); - - if completion_range.is_empty() - && completion_range.start == cursor_position.to_offset(buffer) - { - let completion_text = &completion.text[prefix_len..completion.text.len() - suffix_len]; - if completion_text.trim().is_empty() { - None - } else { - let position = cursor_position.bias_right(buffer); - Some(EditPrediction::Local { - id: None, - edits: vec![(position..position, completion_text.into())], - edit_preview: None, - }) - } - } else { - None - } +fn trim_completion( + completion: &CopilotEditPrediction, + cx: &mut App, +) -> Option<(Range, Arc)> { + let buffer = completion.buffer.read(cx); + let mut completion_range = completion.range.to_offset(buffer); + let prefix_len = common_prefix( + buffer.chars_for_range(completion_range.clone()), + completion.text.chars(), + ); + completion_range.start += prefix_len; + let suffix_len = common_prefix( + buffer.reversed_chars_for_range(completion_range.clone()), + completion.text[prefix_len..].chars().rev(), + ); + completion_range.end = completion_range.end.saturating_sub(suffix_len); + let completion_text = &completion.text[prefix_len..completion.text.len() - suffix_len]; + if completion_text.trim().is_empty() { + None + } else { + let completion_range = + buffer.anchor_after(completion_range.start)..buffer.anchor_after(completion_range.end); + + Some((completion_range, Arc::from(completion_text))) } } @@ -282,6 +194,7 @@ mod tests { Point, language_settings::{CompletionSettingsContent, LspInsertMode, WordsCompletionMode}, }; + use lsp::Uri; use project::Project; use serde_json::json; use settings::{AllLanguageSettingsContent, SettingsStore}; @@ -337,12 +250,15 @@ mod tests { )); handle_copilot_completion_request( &copilot_lsp, - vec![crate::request::Completion { + vec![crate::request::NextEditSuggestion { text: "one.copilot1".into(), range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)), - ..Default::default() + command: None, + text_document: lsp::VersionedTextDocumentIdentifier { + uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(), + version: 0, + }, }], - vec![], ); executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); cx.update_editor(|editor, window, cx| { @@ -383,12 +299,15 @@ mod tests { )); handle_copilot_completion_request( &copilot_lsp, - vec![crate::request::Completion { + vec![crate::request::NextEditSuggestion { text: "one.copilot1".into(), range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)), - ..Default::default() + command: None, + text_document: lsp::VersionedTextDocumentIdentifier { + uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(), + version: 0, + }, }], - vec![], ); executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); cx.update_editor(|editor, _, cx| { @@ -412,12 +331,15 @@ mod tests { // After debouncing, new Copilot completions should be requested. handle_copilot_completion_request( &copilot_lsp, - vec![crate::request::Completion { + vec![crate::request::NextEditSuggestion { text: "one.copilot2".into(), range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 5)), - ..Default::default() + command: None, + text_document: lsp::VersionedTextDocumentIdentifier { + uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(), + version: 0, + }, }], - vec![], ); executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); cx.update_editor(|editor, window, cx| { @@ -479,45 +401,6 @@ mod tests { assert_eq!(editor.display_text(cx), "one.cop\ntwo\nthree\n"); assert_eq!(editor.text(cx), "one.cop\ntwo\nthree\n"); }); - - // Reset the editor to verify how suggestions behave when tabbing on leading indentation. - cx.update_editor(|editor, window, cx| { - editor.set_text("fn foo() {\n \n}", window, cx); - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.select_ranges([Point::new(1, 2)..Point::new(1, 2)]) - }); - }); - handle_copilot_completion_request( - &copilot_lsp, - vec![crate::request::Completion { - text: " let x = 4;".into(), - range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 2)), - ..Default::default() - }], - vec![], - ); - - cx.update_editor(|editor, window, cx| { - editor.next_edit_prediction(&Default::default(), window, cx) - }); - executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); - cx.update_editor(|editor, window, cx| { - assert!(editor.has_active_edit_prediction()); - assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}"); - assert_eq!(editor.text(cx), "fn foo() {\n \n}"); - - // Tabbing inside of leading whitespace inserts indentation without accepting the suggestion. - editor.tab(&Default::default(), window, cx); - assert!(editor.has_active_edit_prediction()); - assert_eq!(editor.text(cx), "fn foo() {\n \n}"); - assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}"); - - // Using AcceptEditPrediction again accepts the suggestion. - editor.accept_edit_prediction(&Default::default(), window, cx); - assert!(!editor.has_active_edit_prediction()); - assert_eq!(editor.text(cx), "fn foo() {\n let x = 4;\n}"); - assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}"); - }); } #[gpui::test(iterations = 10)] @@ -570,12 +453,15 @@ mod tests { )); handle_copilot_completion_request( &copilot_lsp, - vec![crate::request::Completion { + vec![crate::request::NextEditSuggestion { text: "one.copilot1".into(), range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)), - ..Default::default() + command: None, + text_document: lsp::VersionedTextDocumentIdentifier { + uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(), + version: 0, + }, }], - vec![], ); executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); cx.update_editor(|editor, window, cx| { @@ -614,12 +500,15 @@ mod tests { )); handle_copilot_completion_request( &copilot_lsp, - vec![crate::request::Completion { + vec![crate::request::NextEditSuggestion { text: "one.123. copilot\n 456".into(), range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)), - ..Default::default() + command: None, + text_document: lsp::VersionedTextDocumentIdentifier { + uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(), + version: 0, + }, }], - vec![], ); executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); cx.update_editor(|editor, window, cx| { @@ -686,15 +575,18 @@ mod tests { handle_copilot_completion_request( &copilot_lsp, - vec![crate::request::Completion { + vec![crate::request::NextEditSuggestion { text: "two.foo()".into(), range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 2)), - ..Default::default() + command: None, + text_document: lsp::VersionedTextDocumentIdentifier { + uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(), + version: 0, + }, }], - vec![], ); cx.update_editor(|editor, window, cx| { - editor.next_edit_prediction(&Default::default(), window, cx) + editor.show_edit_prediction(&Default::default(), window, cx) }); executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); cx.update_editor(|editor, window, cx| { @@ -703,15 +595,22 @@ mod tests { assert_eq!(editor.text(cx), "one\ntw\nthree\n"); editor.backspace(&Default::default(), window, cx); + }); + executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); + cx.run_until_parked(); + cx.update_editor(|editor, window, cx| { assert!(editor.has_active_edit_prediction()); assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n"); assert_eq!(editor.text(cx), "one\nt\nthree\n"); editor.backspace(&Default::default(), window, cx); + }); + executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); + cx.run_until_parked(); + cx.update_editor(|editor, window, cx| { assert!(editor.has_active_edit_prediction()); assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n"); assert_eq!(editor.text(cx), "one\n\nthree\n"); - // Deleting across the original suggestion range invalidates it. editor.backspace(&Default::default(), window, cx); assert!(!editor.has_active_edit_prediction()); @@ -765,19 +664,22 @@ mod tests { handle_copilot_completion_request( &copilot_lsp, - vec![crate::request::Completion { + vec![crate::request::NextEditSuggestion { text: "b = 2 + a".into(), range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 5)), - ..Default::default() + command: None, + text_document: lsp::VersionedTextDocumentIdentifier { + uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(), + version: 0, + }, }], - vec![], ); _ = editor.update(cx, |editor, window, cx| { // Ensure copilot suggestions are shown for the first excerpt. editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_ranges([Point::new(1, 5)..Point::new(1, 5)]) }); - editor.next_edit_prediction(&Default::default(), window, cx); + editor.show_edit_prediction(&Default::default(), window, cx); }); executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); _ = editor.update(cx, |editor, _, cx| { @@ -791,12 +693,15 @@ mod tests { handle_copilot_completion_request( &copilot_lsp, - vec![crate::request::Completion { + vec![crate::request::NextEditSuggestion { text: "d = 4 + c".into(), range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 6)), - ..Default::default() + command: None, + text_document: lsp::VersionedTextDocumentIdentifier { + uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(), + version: 0, + }, }], - vec![], ); _ = editor.update(cx, |editor, window, cx| { // Move to another excerpt, ensuring the suggestion gets cleared. @@ -873,15 +778,18 @@ mod tests { )); handle_copilot_completion_request( &copilot_lsp, - vec![crate::request::Completion { + vec![crate::request::NextEditSuggestion { text: "two.foo()".into(), range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 2)), - ..Default::default() + command: None, + text_document: lsp::VersionedTextDocumentIdentifier { + uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(), + version: 0, + }, }], - vec![], ); cx.update_editor(|editor, window, cx| { - editor.next_edit_prediction(&Default::default(), window, cx) + editor.show_edit_prediction(&Default::default(), window, cx) }); executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); cx.update_editor(|editor, _, cx| { @@ -903,12 +811,15 @@ mod tests { )); handle_copilot_completion_request( &copilot_lsp, - vec![crate::request::Completion { + vec![crate::request::NextEditSuggestion { text: "two.foo()".into(), range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 3)), - ..Default::default() + command: None, + text_document: lsp::VersionedTextDocumentIdentifier { + uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(), + version: 0, + }, }], - vec![], ); executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); cx.update_editor(|editor, _, cx| { @@ -930,12 +841,15 @@ mod tests { )); handle_copilot_completion_request( &copilot_lsp, - vec![crate::request::Completion { + vec![crate::request::NextEditSuggestion { text: "two.foo()".into(), range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 4)), - ..Default::default() + command: None, + text_document: lsp::VersionedTextDocumentIdentifier { + uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(), + version: 0, + }, }], - vec![], ); executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); cx.update_editor(|editor, _, cx| { @@ -1011,16 +925,20 @@ mod tests { .unwrap(); let mut copilot_requests = copilot_lsp - .set_request_handler::( + .set_request_handler::( move |_params, _cx| async move { - Ok(crate::request::GetCompletionsResult { - completions: vec![crate::request::Completion { + Ok(crate::request::NextEditSuggestionsResult { + edits: vec![crate::request::NextEditSuggestion { text: "next line".into(), range: lsp::Range::new( lsp::Position::new(1, 0), lsp::Position::new(1, 0), ), - ..Default::default() + command: None, + text_document: lsp::VersionedTextDocumentIdentifier { + uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(), + version: 0, + }, }], }) }, @@ -1049,23 +967,14 @@ mod tests { fn handle_copilot_completion_request( lsp: &lsp::FakeLanguageServer, - completions: Vec, - completions_cycling: Vec, + completions: Vec, ) { - lsp.set_request_handler::(move |_params, _cx| { - let completions = completions.clone(); - async move { - Ok(crate::request::GetCompletionsResult { - completions: completions.clone(), - }) - } - }); - lsp.set_request_handler::( + lsp.set_request_handler::( move |_params, _cx| { - let completions_cycling = completions_cycling.clone(); + let completions = completions.clone(); async move { - Ok(crate::request::GetCompletionsResult { - completions: completions_cycling.clone(), + Ok(crate::request::NextEditSuggestionsResult { + edits: completions.clone(), }) } }, diff --git a/crates/copilot/src/request.rs b/crates/copilot/src/request.rs index 85d6254dc060824a9b2686e8f53090fccb39980e..2f97fb72a42904b1fefdd3999f680fca12559ecd 100644 --- a/crates/copilot/src/request.rs +++ b/crates/copilot/src/request.rs @@ -1,3 +1,4 @@ +use lsp::VersionedTextDocumentIdentifier; use serde::{Deserialize, Serialize}; pub enum CheckStatus {} @@ -88,72 +89,6 @@ impl lsp::request::Request for SignOut { const METHOD: &'static str = "signOut"; } -pub enum GetCompletions {} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct GetCompletionsParams { - pub doc: GetCompletionsDocument, -} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct GetCompletionsDocument { - pub tab_size: u32, - pub indent_size: u32, - pub insert_spaces: bool, - pub uri: lsp::Uri, - pub relative_path: String, - pub position: lsp::Position, - pub version: usize, -} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct GetCompletionsResult { - pub completions: Vec, -} - -#[derive(Clone, Debug, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct Completion { - pub text: String, - pub position: lsp::Position, - pub uuid: String, - pub range: lsp::Range, - pub display_text: String, -} - -impl lsp::request::Request for GetCompletions { - type Params = GetCompletionsParams; - type Result = GetCompletionsResult; - const METHOD: &'static str = "getCompletions"; -} - -pub enum GetCompletionsCycling {} - -impl lsp::request::Request for GetCompletionsCycling { - type Params = GetCompletionsParams; - type Result = GetCompletionsResult; - const METHOD: &'static str = "getCompletionsCycling"; -} - -pub enum LogMessage {} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct LogMessageParams { - pub level: u8, - pub message: String, - pub metadata_str: String, - pub extra: Vec, -} - -impl lsp::notification::Notification for LogMessage { - type Params = LogMessageParams; - const METHOD: &'static str = "LogMessage"; -} - pub enum StatusNotification {} #[derive(Debug, Serialize, Deserialize)] @@ -223,3 +158,36 @@ impl lsp::request::Request for NotifyRejected { type Result = String; const METHOD: &'static str = "notifyRejected"; } + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct NextEditSuggestions; + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct NextEditSuggestionsParams { + pub(crate) text_document: VersionedTextDocumentIdentifier, + pub(crate) position: lsp::Position, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct NextEditSuggestion { + pub text: String, + pub text_document: VersionedTextDocumentIdentifier, + pub range: lsp::Range, + pub command: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct NextEditSuggestionsResult { + pub edits: Vec, +} + +impl lsp::request::Request for NextEditSuggestions { + type Params = NextEditSuggestionsParams; + type Result = NextEditSuggestionsResult; + + const METHOD: &'static str = "textDocument/copilotInlineEdit"; +} diff --git a/crates/deepseek/src/deepseek.rs b/crates/deepseek/src/deepseek.rs index e978aa08048bfa4c7b7b203ce6b405ba8a0a7d0c..636258a5a132ce79cb5d15b1aaa25d6e4d3af643 100644 --- a/crates/deepseek/src/deepseek.rs +++ b/crates/deepseek/src/deepseek.rs @@ -103,8 +103,9 @@ impl Model { pub fn max_output_tokens(&self) -> Option { match self { - Self::Chat => Some(8_192), - Self::Reasoner => Some(64_000), + // Their API treats this max against the context window, which means we hit the limit a lot + // Using the default value of None in the API instead + Self::Chat | Self::Reasoner => None, Self::Custom { max_output_tokens, .. } => *max_output_tokens, diff --git a/crates/edit_prediction/src/mercury.rs b/crates/edit_prediction/src/mercury.rs index b47bd2ad0374eba33e7b8db726c2fa13c0519465..8186fc5d8c609468be04c117eabac11c6c015efd 100644 --- a/crates/edit_prediction/src/mercury.rs +++ b/crates/edit_prediction/src/mercury.rs @@ -6,7 +6,7 @@ use crate::{ use anyhow::{Context as _, Result}; use futures::AsyncReadExt as _; use gpui::{ - App, AppContext as _, Entity, SharedString, Task, + App, AppContext as _, Entity, Global, SharedString, Task, http_client::{self, AsyncBody, Method}, }; use language::{OffsetRangeExt as _, ToOffset, ToPoint as _}; @@ -300,14 +300,19 @@ pub const MERCURY_CREDENTIALS_URL: SharedString = SharedString::new_static("https://api.inceptionlabs.ai/v1/edit/completions"); pub const MERCURY_CREDENTIALS_USERNAME: &str = "mercury-api-token"; pub static MERCURY_TOKEN_ENV_VAR: std::sync::LazyLock = env_var!("MERCURY_AI_TOKEN"); -pub static MERCURY_API_KEY: std::sync::OnceLock> = std::sync::OnceLock::new(); + +struct GlobalMercuryApiKey(Entity); + +impl Global for GlobalMercuryApiKey {} pub fn mercury_api_token(cx: &mut App) -> Entity { - MERCURY_API_KEY - .get_or_init(|| { - cx.new(|_| ApiKeyState::new(MERCURY_CREDENTIALS_URL, MERCURY_TOKEN_ENV_VAR.clone())) - }) - .clone() + if let Some(global) = cx.try_global::() { + return global.0.clone(); + } + let entity = + cx.new(|_| ApiKeyState::new(MERCURY_CREDENTIALS_URL, MERCURY_TOKEN_ENV_VAR.clone())); + cx.set_global(GlobalMercuryApiKey(entity.clone())); + entity } pub fn load_mercury_api_token(cx: &mut App) -> Task> { diff --git a/crates/edit_prediction/src/sweep_ai.rs b/crates/edit_prediction/src/sweep_ai.rs index 2ed24cd8ef728383ec800acbb2ab7c7b99f07c06..71f28c9213c3440a9267dab7d5a5416dc219f2f3 100644 --- a/crates/edit_prediction/src/sweep_ai.rs +++ b/crates/edit_prediction/src/sweep_ai.rs @@ -1,7 +1,7 @@ use anyhow::Result; use futures::AsyncReadExt as _; use gpui::{ - App, AppContext as _, Entity, SharedString, Task, + App, AppContext as _, Entity, Global, SharedString, Task, http_client::{self, AsyncBody, Method}, }; use language::{Point, ToOffset as _}; @@ -272,14 +272,19 @@ pub const SWEEP_CREDENTIALS_URL: SharedString = SharedString::new_static("https://autocomplete.sweep.dev"); pub const SWEEP_CREDENTIALS_USERNAME: &str = "sweep-api-token"; pub static SWEEP_AI_TOKEN_ENV_VAR: std::sync::LazyLock = env_var!("SWEEP_AI_TOKEN"); -pub static SWEEP_API_KEY: std::sync::OnceLock> = std::sync::OnceLock::new(); + +struct GlobalSweepApiKey(Entity); + +impl Global for GlobalSweepApiKey {} pub fn sweep_api_token(cx: &mut App) -> Entity { - SWEEP_API_KEY - .get_or_init(|| { - cx.new(|_| ApiKeyState::new(SWEEP_CREDENTIALS_URL, SWEEP_AI_TOKEN_ENV_VAR.clone())) - }) - .clone() + if let Some(global) = cx.try_global::() { + return global.0.clone(); + } + let entity = + cx.new(|_| ApiKeyState::new(SWEEP_CREDENTIALS_URL, SWEEP_AI_TOKEN_ENV_VAR.clone())); + cx.set_global(GlobalSweepApiKey(entity.clone())); + entity } pub fn load_sweep_api_token(cx: &mut App) -> Task> { diff --git a/crates/edit_prediction/src/zed_edit_prediction_delegate.rs b/crates/edit_prediction/src/zed_edit_prediction_delegate.rs index 0a87ca661435de4d22e6f258c30ff406f0deecc2..289bcd76daab2b9a4b82db88b86285e6c7aca00d 100644 --- a/crates/edit_prediction/src/zed_edit_prediction_delegate.rs +++ b/crates/edit_prediction/src/zed_edit_prediction_delegate.rs @@ -2,7 +2,7 @@ use std::{cmp, sync::Arc}; use client::{Client, UserStore}; use cloud_llm_client::EditPredictionRejectReason; -use edit_prediction_types::{DataCollectionState, Direction, EditPredictionDelegate}; +use edit_prediction_types::{DataCollectionState, EditPredictionDelegate}; use gpui::{App, Entity, prelude::*}; use language::{Buffer, ToPoint as _}; use project::Project; @@ -139,15 +139,6 @@ impl EditPredictionDelegate for ZedEditPredictionDelegate { }); } - fn cycle( - &mut self, - _buffer: Entity, - _cursor_position: language::Anchor, - _direction: Direction, - _cx: &mut Context, - ) { - } - fn accept(&mut self, cx: &mut Context) { self.store.update(cx, |store, cx| { store.accept_current_prediction(&self.project, cx); diff --git a/crates/edit_prediction_types/src/edit_prediction_types.rs b/crates/edit_prediction_types/src/edit_prediction_types.rs index 945cfea4a168af4470d98ca844f311a79de9800a..5a37aba59923598b20becd91f07633e409b2bdb7 100644 --- a/crates/edit_prediction_types/src/edit_prediction_types.rs +++ b/crates/edit_prediction_types/src/edit_prediction_types.rs @@ -95,13 +95,6 @@ pub trait EditPredictionDelegate: 'static + Sized { debounce: bool, cx: &mut Context, ); - fn cycle( - &mut self, - buffer: Entity, - cursor_position: language::Anchor, - direction: Direction, - cx: &mut Context, - ); fn accept(&mut self, cx: &mut Context); fn discard(&mut self, cx: &mut Context); fn did_show(&mut self, _cx: &mut Context) {} @@ -136,13 +129,6 @@ pub trait EditPredictionDelegateHandle { debounce: bool, cx: &mut App, ); - fn cycle( - &self, - buffer: Entity, - cursor_position: language::Anchor, - direction: Direction, - cx: &mut App, - ); fn did_show(&self, cx: &mut App); fn accept(&self, cx: &mut App); fn discard(&self, cx: &mut App); @@ -215,18 +201,6 @@ where }) } - fn cycle( - &self, - buffer: Entity, - cursor_position: language::Anchor, - direction: Direction, - cx: &mut App, - ) { - self.update(cx, |this, cx| { - this.cycle(buffer, cursor_position, direction, cx) - }) - } - fn accept(&self, cx: &mut App) { self.update(cx, |this, cx| this.accept(cx)) } diff --git a/crates/editor/src/bracket_colorization.rs b/crates/editor/src/bracket_colorization.rs index 4879c5e9ce703227d3c03f4d3373512769b1515c..ee7e785ed30a14bce53bb777b67bdf69a9cecd07 100644 --- a/crates/editor/src/bracket_colorization.rs +++ b/crates/editor/src/bracket_colorization.rs @@ -348,6 +348,61 @@ where ); } + #[gpui::test] + async fn test_bracket_colorization_after_language_swap(cx: &mut gpui::TestAppContext) { + init_test(cx, |language_settings| { + language_settings.defaults.colorize_brackets = Some(true); + }); + + let language_registry = Arc::new(language::LanguageRegistry::test(cx.executor())); + language_registry.add(markdown_lang()); + language_registry.add(rust_lang()); + + let mut cx = EditorTestContext::new(cx).await; + cx.update_buffer(|buffer, cx| { + buffer.set_language_registry(language_registry.clone()); + buffer.set_language(Some(markdown_lang()), cx); + }); + + cx.set_state(indoc! {r#" + fn main() { + let v: Vec = vec![]; + } + "#}); + cx.executor().advance_clock(Duration::from_millis(100)); + cx.executor().run_until_parked(); + + assert_eq!( + r#"fn main«1()1» «1{ + let v: Vec = vec!«2[]2»; +}1» + +1 hsla(207.80, 16.20%, 69.19%, 1.00) +2 hsla(29.00, 54.00%, 65.88%, 1.00) +"#, + &bracket_colors_markup(&mut cx), + "Markdown does not colorize <> brackets" + ); + + cx.update_buffer(|buffer, cx| { + buffer.set_language(Some(rust_lang()), cx); + }); + cx.executor().advance_clock(Duration::from_millis(100)); + cx.executor().run_until_parked(); + + assert_eq!( + r#"fn main«1()1» «1{ + let v: Vec«22» = vec!«2[]2»; +}1» + +1 hsla(207.80, 16.20%, 69.19%, 1.00) +2 hsla(29.00, 54.00%, 65.88%, 1.00) +"#, + &bracket_colors_markup(&mut cx), + "After switching to Rust, <> brackets are now colorized" + ); + } + #[gpui::test] async fn test_bracket_colorization_when_editing(cx: &mut gpui::TestAppContext) { init_test(cx, |language_settings| { diff --git a/crates/editor/src/edit_prediction_tests.rs b/crates/editor/src/edit_prediction_tests.rs index bfce1532ce78699e1fb524fd594df1ba83c864a5..b5931cde42a4e2c0e21b2d1f68558879de9750b4 100644 --- a/crates/editor/src/edit_prediction_tests.rs +++ b/crates/editor/src/edit_prediction_tests.rs @@ -485,15 +485,6 @@ impl EditPredictionDelegate for FakeEditPredictionDelegate { ) { } - fn cycle( - &mut self, - _buffer: gpui::Entity, - _cursor_position: language::Anchor, - _direction: edit_prediction_types::Direction, - _cx: &mut gpui::Context, - ) { - } - fn accept(&mut self, _cx: &mut gpui::Context) {} fn discard(&mut self, _cx: &mut gpui::Context) {} @@ -561,15 +552,6 @@ impl EditPredictionDelegate for FakeNonZedEditPredictionDelegate { ) { } - fn cycle( - &mut self, - _buffer: gpui::Entity, - _cursor_position: language::Anchor, - _direction: edit_prediction_types::Direction, - _cx: &mut gpui::Context, - ) { - } - fn accept(&mut self, _cx: &mut gpui::Context) {} fn discard(&mut self, _cx: &mut gpui::Context) {} diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 7da06c3d8de91709cdcea8cbc923918464021079..8560705802264dad55b87dbf21e1f9aa7625edf8 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -73,11 +73,7 @@ pub use multi_buffer::{ pub use split::SplittableEditor; pub use text::Bias; -use ::git::{ - Restore, - blame::{BlameEntry, ParsedCommitMessage}, - status::FileStatus, -}; +use ::git::{Restore, blame::BlameEntry, commit::ParsedCommitMessage, status::FileStatus}; use aho_corasick::{AhoCorasick, AhoCorasickBuilder, BuildError}; use anyhow::{Context as _, Result, anyhow, bail}; use blink_manager::BlinkManager; @@ -7468,26 +7464,6 @@ impl Editor { .unwrap_or(false) } - fn cycle_edit_prediction( - &mut self, - direction: Direction, - window: &mut Window, - cx: &mut Context, - ) -> Option<()> { - let provider = self.edit_prediction_provider()?; - let cursor = self.selections.newest_anchor().head(); - let (buffer, cursor_buffer_position) = - self.buffer.read(cx).text_anchor_for_position(cursor, cx)?; - if self.edit_predictions_hidden_for_vim_mode || !self.should_show_edit_predictions() { - return None; - } - - provider.cycle(buffer, cursor_buffer_position, direction, cx); - self.update_visible_edit_prediction(window, cx); - - Some(()) - } - pub fn show_edit_prediction( &mut self, _: &ShowEditPrediction, @@ -7525,42 +7501,6 @@ impl Editor { .detach(); } - pub fn next_edit_prediction( - &mut self, - _: &NextEditPrediction, - window: &mut Window, - cx: &mut Context, - ) { - if self.has_active_edit_prediction() { - self.cycle_edit_prediction(Direction::Next, window, cx); - } else { - let is_copilot_disabled = self - .refresh_edit_prediction(false, true, window, cx) - .is_none(); - if is_copilot_disabled { - cx.propagate(); - } - } - } - - pub fn previous_edit_prediction( - &mut self, - _: &PreviousEditPrediction, - window: &mut Window, - cx: &mut Context, - ) { - if self.has_active_edit_prediction() { - self.cycle_edit_prediction(Direction::Prev, window, cx); - } else { - let is_copilot_disabled = self - .refresh_edit_prediction(false, true, window, cx) - .is_none(); - if is_copilot_disabled { - cx.propagate(); - } - } - } - pub fn accept_partial_edit_prediction( &mut self, granularity: EditPredictionGranularity, diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 85b32324a1c1cc7fb84162fb120e8ef0e4e8b599..f7b6aa949e74dca9bee73419fa2b87899f9986fd 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -37,11 +37,7 @@ use crate::{ use buffer_diff::{DiffHunkStatus, DiffHunkStatusKind}; use collections::{BTreeMap, HashMap}; use file_icons::FileIcons; -use git::{ - Oid, - blame::{BlameEntry, ParsedCommitMessage}, - status::FileStatus, -}; +use git::{Oid, blame::BlameEntry, commit::ParsedCommitMessage, status::FileStatus}; use gpui::{ Action, Along, AnyElement, App, AppContext, AvailableSpace, Axis as ScrollbarAxis, BorderStyle, Bounds, ClickEvent, ClipboardItem, ContentMask, Context, Corner, Corners, CursorStyle, @@ -594,8 +590,6 @@ impl EditorElement { register_action(editor, window, Editor::show_signature_help); register_action(editor, window, Editor::signature_help_prev); register_action(editor, window, Editor::signature_help_next); - register_action(editor, window, Editor::next_edit_prediction); - register_action(editor, window, Editor::previous_edit_prediction); register_action(editor, window, Editor::show_edit_prediction); register_action(editor, window, Editor::context_menu_first); register_action(editor, window, Editor::context_menu_prev); diff --git a/crates/editor/src/git/blame.rs b/crates/editor/src/git/blame.rs index 031795ff2dbfceb96f950db18101b37fd3cdcf84..d1338c3cbd3540914b23a53410fd5c823e1285c8 100644 --- a/crates/editor/src/git/blame.rs +++ b/crates/editor/src/git/blame.rs @@ -3,9 +3,9 @@ use anyhow::{Context as _, Result}; use collections::HashMap; use git::{ - GitHostingProviderRegistry, GitRemote, Oid, - blame::{Blame, BlameEntry, ParsedCommitMessage}, - parse_git_remote_url, + GitHostingProviderRegistry, Oid, + blame::{Blame, BlameEntry}, + commit::ParsedCommitMessage, }; use gpui::{ AnyElement, App, AppContext as _, Context, Entity, Hsla, ScrollHandle, Subscription, Task, @@ -525,12 +525,7 @@ impl GitBlame { .git_store() .read(cx) .repository_and_path_for_buffer_id(buffer.read(cx).remote_id(), cx) - .and_then(|(repo, _)| { - repo.read(cx) - .remote_upstream_url - .clone() - .or(repo.read(cx).remote_origin_url.clone()) - }); + .and_then(|(repo, _)| repo.read(cx).default_remote_url()); let blame_buffer = project .update(cx, |project, cx| project.blame_buffer(&buffer, None, cx)); Ok(async move { @@ -554,13 +549,19 @@ impl GitBlame { entries, snapshot.max_point().row, ); - let commit_details = parse_commit_messages( - messages, - remote_url, - provider_registry.clone(), - ) - .await; - + let commit_details = messages + .into_iter() + .map(|(oid, message)| { + let parsed_commit_message = + ParsedCommitMessage::parse( + oid.to_string(), + message, + remote_url.as_deref(), + Some(provider_registry.clone()), + ); + (oid, parsed_commit_message) + }) + .collect(); res.push(( id, snapshot, @@ -680,55 +681,6 @@ fn build_blame_entry_sum_tree(entries: Vec, max_row: u32) -> SumTree entries } -async fn parse_commit_messages( - messages: impl IntoIterator, - remote_url: Option, - provider_registry: Arc, -) -> HashMap { - let mut commit_details = HashMap::default(); - - let parsed_remote_url = remote_url - .as_deref() - .and_then(|remote_url| parse_git_remote_url(provider_registry, remote_url)); - - for (oid, message) in messages { - let permalink = if let Some((provider, git_remote)) = parsed_remote_url.as_ref() { - Some(provider.build_commit_permalink( - git_remote, - git::BuildCommitPermalinkParams { - sha: oid.to_string().as_str(), - }, - )) - } else { - None - }; - - let remote = parsed_remote_url - .as_ref() - .map(|(provider, remote)| GitRemote { - host: provider.clone(), - owner: remote.owner.clone().into(), - repo: remote.repo.clone().into(), - }); - - let pull_request = parsed_remote_url - .as_ref() - .and_then(|(provider, remote)| provider.extract_pull_request(remote, &message)); - - commit_details.insert( - oid, - ParsedCommitMessage { - message: message.into(), - permalink, - remote, - pull_request, - }, - ); - } - - commit_details -} - #[cfg(test)] mod tests { use super::*; diff --git a/crates/extension_api/src/extension_api.rs b/crates/extension_api/src/extension_api.rs index 0a67f5965d569b7b531283b2c8f0ac385e4397f8..8b08335d24e140b78f4a5c027f2d8a861e80f846 100644 --- a/crates/extension_api/src/extension_api.rs +++ b/crates/extension_api/src/extension_api.rs @@ -438,7 +438,6 @@ static mut EXTENSION: Option> = None; pub static ZED_API_VERSION: [u8; 6] = *include_bytes!(concat!(env!("OUT_DIR"), "/version_bytes")); mod wit { - wit_bindgen::generate!({ skip: ["init-extension"], path: "./wit/since_v0.8.0", @@ -692,6 +691,12 @@ impl wit::Guest for Component { #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)] pub struct LanguageServerId(String); +impl LanguageServerId { + pub fn new(value: String) -> Self { + Self(value) + } +} + impl AsRef for LanguageServerId { fn as_ref(&self) -> &str { &self.0 @@ -708,6 +713,12 @@ impl fmt::Display for LanguageServerId { #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)] pub struct ContextServerId(String); +impl ContextServerId { + pub fn new(value: String) -> Self { + Self(value) + } +} + impl AsRef for ContextServerId { fn as_ref(&self) -> &str { &self.0 diff --git a/crates/git/src/blame.rs b/crates/git/src/blame.rs index c3bbeff3f7d15d84b779f2ab92cb89799f63c4e8..d6011de98b8c69837d16bf2a2211fc7632726230 100644 --- a/crates/git/src/blame.rs +++ b/crates/git/src/blame.rs @@ -1,10 +1,9 @@ +use crate::Oid; use crate::commit::get_messages; use crate::repository::RepoPath; -use crate::{GitRemote, Oid}; use anyhow::{Context as _, Result}; use collections::{HashMap, HashSet}; use futures::AsyncWriteExt; -use gpui::SharedString; use serde::{Deserialize, Serialize}; use std::process::Stdio; use std::{ops::Range, path::Path}; @@ -21,14 +20,6 @@ pub struct Blame { pub messages: HashMap, } -#[derive(Clone, Debug, Default)] -pub struct ParsedCommitMessage { - pub message: SharedString, - pub permalink: Option, - pub pull_request: Option, - pub remote: Option, -} - impl Blame { pub async fn for_path( git_binary: &Path, diff --git a/crates/git/src/commit.rs b/crates/git/src/commit.rs index ece1d76b8ae9c9f40f27178da1ef13fe1a78e659..1b450a3dffb9e9956e5b43aa2797ae02f90e731c 100644 --- a/crates/git/src/commit.rs +++ b/crates/git/src/commit.rs @@ -1,7 +1,52 @@ -use crate::{Oid, status::StatusCode}; +use crate::{ + BuildCommitPermalinkParams, GitHostingProviderRegistry, GitRemote, Oid, parse_git_remote_url, + status::StatusCode, +}; use anyhow::{Context as _, Result}; use collections::HashMap; -use std::path::Path; +use gpui::SharedString; +use std::{path::Path, sync::Arc}; + +#[derive(Clone, Debug, Default)] +pub struct ParsedCommitMessage { + pub message: SharedString, + pub permalink: Option, + pub pull_request: Option, + pub remote: Option, +} + +impl ParsedCommitMessage { + pub fn parse( + sha: String, + message: String, + remote_url: Option<&str>, + provider_registry: Option>, + ) -> Self { + if let Some((hosting_provider, remote)) = provider_registry + .and_then(|reg| remote_url.and_then(|url| parse_git_remote_url(reg, url))) + { + let pull_request = hosting_provider.extract_pull_request(&remote, &message); + Self { + message: message.into(), + permalink: Some( + hosting_provider + .build_commit_permalink(&remote, BuildCommitPermalinkParams { sha: &sha }), + ), + pull_request, + remote: Some(GitRemote { + host: hosting_provider, + owner: remote.owner.into(), + repo: remote.repo.into(), + }), + } + } else { + Self { + message: message.into(), + ..Default::default() + } + } + } +} pub async fn get_messages(working_directory: &Path, shas: &[Oid]) -> Result> { if shas.is_empty() { diff --git a/crates/git_ui/src/blame_ui.rs b/crates/git_ui/src/blame_ui.rs index 09ab3229bc5b2b7814b89bbb914472407793a52d..d4d8750a18ee6efbd90a38722043450c6ec61358 100644 --- a/crates/git_ui/src/blame_ui.rs +++ b/crates/git_ui/src/blame_ui.rs @@ -3,10 +3,7 @@ use crate::{ commit_view::CommitView, }; use editor::{BlameRenderer, Editor, hover_markdown_style}; -use git::{ - blame::{BlameEntry, ParsedCommitMessage}, - repository::CommitSummary, -}; +use git::{blame::BlameEntry, commit::ParsedCommitMessage, repository::CommitSummary}; use gpui::{ ClipboardItem, Entity, Hsla, MouseButton, ScrollHandle, Subscription, TextStyle, TextStyleRefinement, UnderlineStyle, WeakEntity, prelude::*, diff --git a/crates/git_ui/src/commit_tooltip.rs b/crates/git_ui/src/commit_tooltip.rs index cf6512b0763e128633cfa65f934d8ed18cd6d022..d18770a704ff31d6dffd705baf44defaaf6d8d4a 100644 --- a/crates/git_ui/src/commit_tooltip.rs +++ b/crates/git_ui/src/commit_tooltip.rs @@ -3,7 +3,7 @@ use editor::hover_markdown_style; use futures::Future; use git::blame::BlameEntry; use git::repository::CommitSummary; -use git::{GitRemote, blame::ParsedCommitMessage}; +use git::{GitRemote, commit::ParsedCommitMessage}; use gpui::{ App, Asset, ClipboardItem, Element, Entity, MouseButton, ParentElement, Render, ScrollHandle, StatefulInteractiveElement, WeakEntity, prelude::*, diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index 7216e1fc46e9d1240d23d8bd18202aa0963f846a..4e94a811510ee07707bf729040d41fc8b1eb922c 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -15,12 +15,13 @@ use askpass::AskPassDelegate; use cloud_llm_client::CompletionIntent; use collections::{BTreeMap, HashMap, HashSet}; use db::kvp::KEY_VALUE_STORE; +use editor::RewrapOptions; use editor::{ Direction, Editor, EditorElement, EditorMode, MultiBuffer, MultiBufferOffset, actions::ExpandAllDiffHunks, }; use futures::StreamExt as _; -use git::blame::ParsedCommitMessage; +use git::commit::ParsedCommitMessage; use git::repository::{ Branch, CommitDetails, CommitOptions, CommitSummary, DiffType, FetchOptions, GitCommitter, PushOptions, Remote, RemoteCommandOutput, ResetMode, Upstream, UpstreamTracking, @@ -30,15 +31,14 @@ use git::stash::GitStash; use git::status::StageStatus; use git::{Amend, Signoff, ToggleStaged, repository::RepoPath, status::FileStatus}; use git::{ - ExpandCommitEditor, RestoreTrackedFiles, StageAll, StashAll, StashApply, StashPop, - TrashUntrackedFiles, UnstageAll, + ExpandCommitEditor, GitHostingProviderRegistry, RestoreTrackedFiles, StageAll, StashAll, + StashApply, StashPop, TrashUntrackedFiles, UnstageAll, }; use gpui::{ Action, AsyncApp, AsyncWindowContext, Bounds, ClickEvent, Corner, DismissEvent, Entity, - EventEmitter, FocusHandle, Focusable, KeyContext, ListHorizontalSizingBehavior, - ListSizingBehavior, MouseButton, MouseDownEvent, Point, PromptLevel, ScrollStrategy, - Subscription, Task, UniformListScrollHandle, WeakEntity, actions, anchored, deferred, point, - size, uniform_list, + EventEmitter, FocusHandle, Focusable, KeyContext, MouseButton, MouseDownEvent, Point, + PromptLevel, ScrollStrategy, Subscription, Task, UniformListScrollHandle, WeakEntity, actions, + anchored, deferred, point, size, uniform_list, }; use itertools::Itertools; use language::{Buffer, File}; @@ -212,8 +212,7 @@ const GIT_PANEL_KEY: &str = "GitPanel"; const UPDATE_DEBOUNCE: Duration = Duration::from_millis(50); // TODO: We should revise this part. It seems the indentation width is not aligned with the one in project panel -const TREE_INDENT: f32 = 12.0; -const TREE_INDENT_GUIDE_OFFSET: f32 = 16.0; +const TREE_INDENT: f32 = 16.0; pub fn register(workspace: &mut Workspace) { workspace.register_action(|workspace, _: &ToggleFocus, window, cx| { @@ -2182,7 +2181,13 @@ impl GitPanel { let editor = cx.new(|cx| Editor::for_buffer(buffer, None, window, cx)); let wrapped_message = editor.update(cx, |editor, cx| { editor.select_all(&Default::default(), window, cx); - editor.rewrap(&Default::default(), window, cx); + editor.rewrap_impl( + RewrapOptions { + override_language_settings: false, + preserve_existing_whitespace: true, + }, + cx, + ); editor.text(cx) }); if wrapped_message.trim().is_empty() { @@ -4697,7 +4702,10 @@ impl GitPanel { }, ) .with_render_fn(cx.entity(), |_, params, _, _| { - let left_offset = px(TREE_INDENT_GUIDE_OFFSET); + // Magic number to align the tree item is 3 here + // because we're using 12px as the left-side padding + // and 3 makes the alignment work with the bounding box of the icon + let left_offset = px(TREE_INDENT + 3_f32); let indent_size = params.indent_size; let item_height = params.item_height; @@ -4725,10 +4733,6 @@ impl GitPanel { }) .size_full() .flex_grow() - .with_sizing_behavior(ListSizingBehavior::Auto) - .with_horizontal_sizing_behavior( - ListHorizontalSizingBehavior::Unconstrained, - ) .with_width_from_item(self.max_width_item_index) .track_scroll(&self.scroll_handle), ) @@ -4752,7 +4756,7 @@ impl GitPanel { } fn entry_label(&self, label: impl Into, color: Color) -> Label { - Label::new(label.into()).color(color).single_line() + Label::new(label.into()).color(color) } fn list_item_height(&self) -> Rems { @@ -4774,8 +4778,8 @@ impl GitPanel { .h(self.list_item_height()) .w_full() .items_end() - .px(rems(0.75)) // ~12px - .pb(rems(0.3125)) // ~ 5px + .px_3() + .pb_1() .child( Label::new(header.title()) .color(Color::Muted) @@ -4963,113 +4967,68 @@ impl GitPanel { let marked_bg_alpha = 0.12; let state_opacity_step = 0.04; + let info_color = cx.theme().status().info; + let base_bg = match (selected, marked) { - (true, true) => cx - .theme() - .status() - .info - .alpha(selected_bg_alpha + marked_bg_alpha), - (true, false) => cx.theme().status().info.alpha(selected_bg_alpha), - (false, true) => cx.theme().status().info.alpha(marked_bg_alpha), + (true, true) => info_color.alpha(selected_bg_alpha + marked_bg_alpha), + (true, false) => info_color.alpha(selected_bg_alpha), + (false, true) => info_color.alpha(marked_bg_alpha), _ => cx.theme().colors().ghost_element_background, }; - let hover_bg = if selected { - cx.theme() - .status() - .info - .alpha(selected_bg_alpha + state_opacity_step) - } else { - cx.theme().colors().ghost_element_hover - }; - - let active_bg = if selected { - cx.theme() - .status() - .info - .alpha(selected_bg_alpha + state_opacity_step * 2.0) + let (hover_bg, active_bg) = if selected { + ( + info_color.alpha(selected_bg_alpha + state_opacity_step), + info_color.alpha(selected_bg_alpha + state_opacity_step * 2.0), + ) } else { - cx.theme().colors().ghost_element_active + ( + cx.theme().colors().ghost_element_hover, + cx.theme().colors().ghost_element_active, + ) }; - let mut name_row = h_flex() - .items_center() - .gap_1() + let name_row = h_flex() + .min_w_0() .flex_1() - .pl(if tree_view { - px(depth as f32 * TREE_INDENT) - } else { - px(0.) - }) - .child(git_status_icon(status)); - - name_row = if tree_view { - name_row.child( - self.entry_label(display_name, label_color) - .when(status.is_deleted(), Label::strikethrough) - .truncate(), - ) - } else { - name_row.child(h_flex().items_center().flex_1().map(|this| { - self.path_formatted( - this, - entry.parent_dir(path_style), - path_color, - display_name, - label_color, - path_style, - git_path_style, - status.is_deleted(), - ) - })) - }; + .gap_1() + .child(git_status_icon(status)) + .map(|this| { + if tree_view { + this.pl(px(depth as f32 * TREE_INDENT)).child( + self.entry_label(display_name, label_color) + .when(status.is_deleted(), Label::strikethrough) + .truncate(), + ) + } else { + this.child(self.path_formatted( + entry.parent_dir(path_style), + path_color, + display_name, + label_color, + path_style, + git_path_style, + status.is_deleted(), + )) + } + }); h_flex() .id(id) .h(self.list_item_height()) .w_full() + .pl_3() + .pr_1() + .gap_1p5() .border_1() .border_r_2() .when(selected && self.focus_handle.is_focused(window), |el| { el.border_color(cx.theme().colors().panel_focused_border) }) - .px(rems(0.75)) // ~12px - .overflow_hidden() - .flex_none() - .gap_1p5() .bg(base_bg) - .hover(|this| this.bg(hover_bg)) - .active(|this| this.bg(active_bg)) - .on_click({ - cx.listener(move |this, event: &ClickEvent, window, cx| { - this.selected_entry = Some(ix); - cx.notify(); - if event.modifiers().secondary() { - this.open_file(&Default::default(), window, cx) - } else { - this.open_diff(&Default::default(), window, cx); - this.focus_handle.focus(window, cx); - } - }) - }) - .on_mouse_down( - MouseButton::Right, - move |event: &MouseDownEvent, window, cx| { - // why isn't this happening automatically? we are passing MouseButton::Right to `on_mouse_down`? - if event.button != MouseButton::Right { - return; - } - - let Some(this) = handle.upgrade() else { - return; - }; - this.update(cx, |this, cx| { - this.deploy_entry_context_menu(event.position, ix, window, cx); - }); - cx.stop_propagation(); - }, - ) - .child(name_row.overflow_x_hidden()) + .hover(|s| s.bg(hover_bg)) + .active(|s| s.bg(active_bg)) + .child(name_row) .child( div() .id(checkbox_wrapper_id) @@ -5119,6 +5078,35 @@ impl GitPanel { }), ), ) + .on_click({ + cx.listener(move |this, event: &ClickEvent, window, cx| { + this.selected_entry = Some(ix); + cx.notify(); + if event.modifiers().secondary() { + this.open_file(&Default::default(), window, cx) + } else { + this.open_diff(&Default::default(), window, cx); + this.focus_handle.focus(window, cx); + } + }) + }) + .on_mouse_down( + MouseButton::Right, + move |event: &MouseDownEvent, window, cx| { + // why isn't this happening automatically? we are passing MouseButton::Right to `on_mouse_down`? + if event.button != MouseButton::Right { + return; + } + + let Some(this) = handle.upgrade() else { + return; + }; + this.update(cx, |this, cx| { + this.deploy_entry_context_menu(event.position, ix, window, cx); + }); + cx.stop_propagation(); + }, + ) .into_any_element() } @@ -5143,29 +5131,23 @@ impl GitPanel { let selected_bg_alpha = 0.08; let state_opacity_step = 0.04; - let base_bg = if selected { - cx.theme().status().info.alpha(selected_bg_alpha) - } else { - cx.theme().colors().ghost_element_background - }; + let info_color = cx.theme().status().info; + let colors = cx.theme().colors(); - let hover_bg = if selected { - cx.theme() - .status() - .info - .alpha(selected_bg_alpha + state_opacity_step) + let (base_bg, hover_bg, active_bg) = if selected { + ( + info_color.alpha(selected_bg_alpha), + info_color.alpha(selected_bg_alpha + state_opacity_step), + info_color.alpha(selected_bg_alpha + state_opacity_step * 2.0), + ) } else { - cx.theme().colors().ghost_element_hover + ( + colors.ghost_element_background, + colors.ghost_element_hover, + colors.ghost_element_active, + ) }; - let active_bg = if selected { - cx.theme() - .status() - .info - .alpha(selected_bg_alpha + state_opacity_step * 2.0) - } else { - cx.theme().colors().ghost_element_active - }; let folder_icon = if entry.expanded { IconName::FolderOpen } else { @@ -5188,9 +5170,8 @@ impl GitPanel { }; let name_row = h_flex() - .items_center() + .min_w_0() .gap_1() - .flex_1() .pl(px(entry.depth as f32 * TREE_INDENT)) .child( Icon::new(folder_icon) @@ -5202,28 +5183,21 @@ impl GitPanel { h_flex() .id(id) .h(self.list_item_height()) + .min_w_0() .w_full() - .items_center() + .pl_3() + .pr_1() + .gap_1p5() + .justify_between() .border_1() .border_r_2() .when(selected && self.focus_handle.is_focused(window), |el| { el.border_color(cx.theme().colors().panel_focused_border) }) - .px(rems(0.75)) - .overflow_hidden() - .flex_none() - .gap_1p5() .bg(base_bg) - .hover(|this| this.bg(hover_bg)) - .active(|this| this.bg(active_bg)) - .on_click({ - let key = entry.key.clone(); - cx.listener(move |this, _event: &ClickEvent, window, cx| { - this.selected_entry = Some(ix); - this.toggle_directory(&key, window, cx); - }) - }) - .child(name_row.overflow_x_hidden()) + .hover(|s| s.bg(hover_bg)) + .active(|s| s.bg(active_bg)) + .child(name_row) .child( div() .id(checkbox_wrapper_id) @@ -5262,12 +5236,18 @@ impl GitPanel { }), ), ) + .on_click({ + let key = entry.key.clone(); + cx.listener(move |this, _event: &ClickEvent, window, cx| { + this.selected_entry = Some(ix); + this.toggle_directory(&key, window, cx); + }) + }) .into_any_element() } fn path_formatted( &self, - parent: Div, directory: Option, path_color: Color, file_name: String, @@ -5276,41 +5256,31 @@ impl GitPanel { git_path_style: GitPathStyle, strikethrough: bool, ) -> Div { - parent - .when(git_path_style == GitPathStyle::FileNameFirst, |this| { - this.child( - self.entry_label( - match directory.as_ref().is_none_or(|d| d.is_empty()) { - true => file_name.clone(), - false => format!("{file_name} "), - }, - label_color, - ) - .when(strikethrough, Label::strikethrough), - ) - }) - .when_some(directory, |this, dir| { - match ( - !dir.is_empty(), - git_path_style == GitPathStyle::FileNameFirst, - ) { - (true, true) => this.child( - self.entry_label(dir, path_color) - .when(strikethrough, Label::strikethrough), - ), - (true, false) => this.child( - self.entry_label( - format!("{dir}{}", path_style.primary_separator()), - path_color, - ) + let file_name_first = git_path_style == GitPathStyle::FileNameFirst; + let file_path_first = git_path_style == GitPathStyle::FilePathFirst; + + let file_name = format!("{} ", file_name); + + h_flex() + .min_w_0() + .overflow_hidden() + .when(file_path_first, |this| this.flex_row_reverse()) + .child( + div().flex_none().child( + self.entry_label(file_name, label_color) .when(strikethrough, Label::strikethrough), - ), - _ => this, - } - }) - .when(git_path_style == GitPathStyle::FilePathFirst, |this| { + ), + ) + .when_some(directory, |this, dir| { + let path_name = if file_name_first { + dir + } else { + format!("{dir}{}", path_style.primary_separator()) + }; + this.child( - self.entry_label(file_name, label_color) + self.entry_label(path_name, path_color) + .truncate() .when(strikethrough, Label::strikethrough), ) }) @@ -5650,6 +5620,7 @@ impl GitPanelMessageTooltip { window: &mut Window, cx: &mut App, ) -> Entity { + let remote_url = repository.read(cx).default_remote_url(); cx.new(|cx| { cx.spawn_in(window, async move |this, cx| { let (details, workspace) = git_panel.update(cx, |git_panel, cx| { @@ -5659,16 +5630,21 @@ impl GitPanelMessageTooltip { ) })?; let details = details.await?; + let provider_registry = cx + .update(|_, app| GitHostingProviderRegistry::default_global(app)) + .ok(); let commit_details = crate::commit_tooltip::CommitDetails { sha: details.sha.clone(), author_name: details.author_name.clone(), author_email: details.author_email.clone(), commit_time: OffsetDateTime::from_unix_timestamp(details.commit_timestamp)?, - message: Some(ParsedCommitMessage { - message: details.message, - ..Default::default() - }), + message: Some(ParsedCommitMessage::parse( + details.sha.to_string(), + details.message.to_string(), + remote_url.as_deref(), + provider_registry, + )), }; this.update(cx, |this: &mut GitPanelMessageTooltip, cx| { diff --git a/crates/google_ai/src/google_ai.rs b/crates/google_ai/src/google_ai.rs index b6bba48c4b04608b502932787cfcdcd429276b5b..a7d82c584b208cec33075d65a53a74c963ec05b5 100644 --- a/crates/google_ai/src/google_ai.rs +++ b/crates/google_ai/src/google_ai.rs @@ -566,22 +566,22 @@ impl Model { pub fn max_token_count(&self) -> u64 { match self { - Self::Gemini25FlashLite => 1_048_576, - Self::Gemini25Flash => 1_048_576, - Self::Gemini25Pro => 1_048_576, - Self::Gemini3Pro => 1_048_576, - Self::Gemini3Flash => 1_048_576, + Self::Gemini25FlashLite + | Self::Gemini25Flash + | Self::Gemini25Pro + | Self::Gemini3Pro + | Self::Gemini3Flash => 1_048_576, Self::Custom { max_tokens, .. } => *max_tokens, } } pub fn max_output_tokens(&self) -> Option { match self { - Model::Gemini25FlashLite => Some(65_536), - Model::Gemini25Flash => Some(65_536), - Model::Gemini25Pro => Some(65_536), - Model::Gemini3Pro => Some(65_536), - Model::Gemini3Flash => Some(65_536), + Model::Gemini25FlashLite + | Model::Gemini25Flash + | Model::Gemini25Pro + | Model::Gemini3Pro + | Model::Gemini3Flash => Some(65_536), Model::Custom { .. } => None, } } diff --git a/crates/gpui/src/platform/linux/wayland/window.rs b/crates/gpui/src/platform/linux/wayland/window.rs index 3334ae28a31927b2150e79fc513855fa699c55ba..8cc47c3c139708c3cc278c6146411a4383cc0004 100644 --- a/crates/gpui/src/platform/linux/wayland/window.rs +++ b/crates/gpui/src/platform/linux/wayland/window.rs @@ -1025,13 +1025,26 @@ impl PlatformWindow for WaylandWindow { fn resize(&mut self, size: Size) { let state = self.borrow(); let state_ptr = self.0.clone(); - let dp_size = size.to_device_pixels(self.scale_factor()); + + // Keep window geometry consistent with configure handling. On Wayland, window geometry is + // surface-local: resizing should not attempt to translate the window; the compositor + // controls placement. We also account for client-side decoration insets and tiling. + let window_geometry = inset_by_tiling( + Bounds { + origin: Point::default(), + size, + }, + state.inset(), + state.tiling, + ) + .map(|v| v.0 as i32) + .map_size(|v| if v <= 0 { 1 } else { v }); state.surface_state.set_geometry( - state.bounds.origin.x.0 as i32, - state.bounds.origin.y.0 as i32, - dp_size.width.0, - dp_size.height.0, + window_geometry.origin.x, + window_geometry.origin.y, + window_geometry.size.width, + window_geometry.size.height, ); state diff --git a/crates/gpui/src/platform/windows/events.rs b/crates/gpui/src/platform/windows/events.rs index e6fa6006eb95ec45f1634cb72ef63e2f622455a7..f224a1bf3c47dc1a61c5e0216f5d7825cfc72533 100644 --- a/crates/gpui/src/platform/windows/events.rs +++ b/crates/gpui/src/platform/windows/events.rs @@ -40,6 +40,11 @@ impl WindowsWindowInner { lparam: LPARAM, ) -> LRESULT { let handled = match msg { + // eagerly activate the window, so calls to `active_window` will work correctly + WM_MOUSEACTIVATE => { + unsafe { SetActiveWindow(handle).log_err() }; + None + } WM_ACTIVATE => self.handle_activate_msg(wparam), WM_CREATE => self.handle_create_msg(handle), WM_MOVE => self.handle_move_msg(handle, lparam), diff --git a/crates/gpui/src/platform/windows/platform.rs b/crates/gpui/src/platform/windows/platform.rs index af0cb89ecc94da70cc42c8d4c397aeb2a811d6fb..0e0fdd56c54d56587c09bca14f16dd8e5aef389d 100644 --- a/crates/gpui/src/platform/windows/platform.rs +++ b/crates/gpui/src/platform/windows/platform.rs @@ -659,7 +659,7 @@ impl Platform for WindowsPlatform { if let Err(err) = result { // ERROR_NOT_FOUND means the credential doesn't exist. // Return Ok(None) to match macOS and Linux behavior. - if err.code().0 == ERROR_NOT_FOUND.0 as i32 { + if err.code() == ERROR_NOT_FOUND.to_hresult() { return Ok(None); } return Err(err.into()); diff --git a/crates/gpui/src/text_system/line_wrapper.rs b/crates/gpui/src/text_system/line_wrapper.rs index 45159313b43c508029f2525234c80c6575d0f695..e4e18671a3d85c2f55abd8f8a61ec80833dabdf5 100644 --- a/crates/gpui/src/text_system/line_wrapper.rs +++ b/crates/gpui/src/text_system/line_wrapper.rs @@ -182,6 +182,11 @@ impl LineWrapper { // Cyrillic for Russian, Ukrainian, etc. // https://en.wikipedia.org/wiki/Cyrillic_script_in_Unicode matches!(c, '\u{0400}'..='\u{04FF}') || + + // Vietnamese (https://vietunicode.sourceforge.net/charset/) + matches!(c, '\u{1E00}'..='\u{1EFF}') || // Latin Extended Additional + matches!(c, '\u{0300}'..='\u{036F}') || // Combining Diacritical Marks + // Some other known special characters that should be treated as word characters, // e.g. `a-b`, `var_name`, `I'm`, '@mention`, `#hashtag`, `100%`, `3.1415`, // `2^3`, `a~b`, `a=1`, `Self::new`, etc. @@ -618,7 +623,12 @@ mod tests { #[track_caller] fn assert_word(word: &str) { for c in word.chars() { - assert!(LineWrapper::is_word_char(c), "assertion failed for '{}'", c); + assert!( + LineWrapper::is_word_char(c), + "assertion failed for '{}' (unicode 0x{:x})", + c, + c as u32 + ); } } @@ -661,6 +671,8 @@ mod tests { assert_word("ƀƁƂƃƄƅƆƇƈƉƊƋƌƍƎƏ"); // Cyrillic assert_word("АБВГДЕЖЗИЙКЛМНОП"); + // Vietnamese (https://github.com/zed-industries/zed/issues/23245) + assert_word("ThậmchíđếnkhithuachạychúngcònnhẫntâmgiếtnốtsốđôngtùchínhtrịởYênBáivàCaoBằng"); // non-word characters assert_not_word("你好"); diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 840f2223fcc4a62b6e522f38b967a3fe4ad3209e..2ccd7edac86bced89048cbe5dbf196d8fbcf95f3 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -4966,7 +4966,7 @@ impl From> for AnyWindowHandle { } /// A handle to a window with any root view type, which can be downcast to a window with a specific root view type. -#[derive(Copy, Clone, PartialEq, Eq, Hash)] +#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] pub struct AnyWindowHandle { pub(crate) id: WindowId, state_type: TypeId, diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index abf4d9b10a761b9c0247145e8ddb0664127756d2..99e0c8d4ebdad709eea0e9ab6dbdf9d889d54ec5 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -1801,9 +1801,7 @@ impl Buffer { self.syntax_map.lock().did_parse(syntax_snapshot); self.request_autoindent(cx); self.parse_status.0.send(ParseStatus::Idle).unwrap(); - if self.text.version() != *self.tree_sitter_data.version() { - self.invalidate_tree_sitter_data(self.text.snapshot()); - } + self.invalidate_tree_sitter_data(self.text.snapshot()); cx.emit(BufferEvent::Reparsed); cx.notify(); } diff --git a/crates/language_tools/src/lsp_log_view.rs b/crates/language_tools/src/lsp_log_view.rs index 6fc061cd07edd9e22609ba698f27860b1b905765..e34bbb46d35d5a524c08369fcc991dfe81865127 100644 --- a/crates/language_tools/src/lsp_log_view.rs +++ b/crates/language_tools/src/lsp_log_view.rs @@ -125,7 +125,7 @@ pub fn init(on_headless_host: bool, cx: &mut App) { let server_id = server.server_id(); let weak_lsp_store = cx.weak_entity(); log_store.copilot_log_subscription = - Some(server.on_notification::( + Some(server.on_notification::( move |params, cx| { weak_lsp_store .update(cx, |lsp_store, cx| { diff --git a/crates/languages/src/python.rs b/crates/languages/src/python.rs index 77d4be6f49a4928731d39d2154cbe4f0e38024ef..a06b1efe649b93ef56a35c40bd0d35cd1bc7ca9c 100644 --- a/crates/languages/src/python.rs +++ b/crates/languages/src/python.rs @@ -295,6 +295,23 @@ impl LspInstaller for TyLspAdapter { }) } + async fn check_if_user_installed( + &self, + delegate: &dyn LspAdapterDelegate, + _: Option, + _: &AsyncApp, + ) -> Option { + let Some(ty_bin) = delegate.which(Self::SERVER_NAME.as_ref()).await else { + return None; + }; + let env = delegate.shell_env().await; + Some(LanguageServerBinary { + path: ty_bin, + env: Some(env), + arguments: vec!["server".into()], + }) + } + async fn fetch_server_binary( &self, latest_version: Self::BinaryVersion, diff --git a/crates/languages/src/rust.rs b/crates/languages/src/rust.rs index c10f76b079bf093e71b5444934196940e7b26d6c..80bc48908b0894f251d6631b67cb4a19658454bd 100644 --- a/crates/languages/src/rust.rs +++ b/crates/languages/src/rust.rs @@ -355,7 +355,7 @@ impl LspAdapter for RustLspAdapter { | lsp::CompletionTextEdit::Edit(lsp::TextEdit { new_text, .. }), ) = completion.text_edit.as_ref() && let Ok(mut snippet) = snippet::Snippet::parse(new_text) - && !snippet.tabstops.is_empty() + && snippet.tabstops.len() > 1 { label = String::new(); @@ -421,7 +421,9 @@ impl LspAdapter for RustLspAdapter { 0..label.rfind('(').unwrap_or(completion.label.len()), highlight_id, )); - } else if detail_left.is_none() { + } else if detail_left.is_none() + && kind != Some(lsp::CompletionItemKind::SNIPPET) + { return None; } } @@ -1597,6 +1599,40 @@ mod tests { )) ); + // Postfix completion without actual tabstops (only implicit final $0) + // The label should use completion.label so it can be filtered by "ref" + let ref_completion = adapter + .label_for_completion( + &lsp::CompletionItem { + kind: Some(lsp::CompletionItemKind::SNIPPET), + label: "ref".to_string(), + filter_text: Some("ref".to_string()), + label_details: Some(CompletionItemLabelDetails { + detail: None, + description: Some("&expr".to_string()), + }), + detail: Some("&expr".to_string()), + insert_text_format: Some(lsp::InsertTextFormat::SNIPPET), + text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { + range: lsp::Range::default(), + new_text: "&String::new()".to_string(), + })), + ..Default::default() + }, + &language, + ) + .await; + assert!( + ref_completion.is_some(), + "ref postfix completion should have a label" + ); + let ref_label = ref_completion.unwrap(); + let filter_text = &ref_label.text[ref_label.filter_range.clone()]; + assert!( + filter_text.contains("ref"), + "filter range text '{filter_text}' should contain 'ref' for filtering to work", + ); + // Test for correct range calculation with mixed empty and non-empty tabstops.(See https://github.com/zed-industries/zed/issues/44825) let res = adapter .label_for_completion( diff --git a/crates/mistral/src/mistral.rs b/crates/mistral/src/mistral.rs index eca4743d0442b9ca169ac966f78af0112565fcbc..2fa8a2cedaee01daa1452ade35b20c440055b7fc 100644 --- a/crates/mistral/src/mistral.rs +++ b/crates/mistral/src/mistral.rs @@ -155,15 +155,15 @@ impl Model { pub fn max_token_count(&self) -> u64 { match self { Self::CodestralLatest => 256000, - Self::MistralLargeLatest => 131000, + Self::MistralLargeLatest => 256000, Self::MistralMediumLatest => 128000, Self::MistralSmallLatest => 32000, - Self::MagistralMediumLatest => 40000, - Self::MagistralSmallLatest => 40000, + Self::MagistralMediumLatest => 128000, + Self::MagistralSmallLatest => 128000, Self::OpenMistralNemo => 131000, Self::OpenCodestralMamba => 256000, - Self::DevstralMediumLatest => 128000, - Self::DevstralSmallLatest => 262144, + Self::DevstralMediumLatest => 256000, + Self::DevstralSmallLatest => 256000, Self::Pixtral12BLatest => 128000, Self::PixtralLargeLatest => 128000, Self::Custom { max_tokens, .. } => *max_tokens, diff --git a/crates/project/src/agent_server_store.rs b/crates/project/src/agent_server_store.rs index 287b25935676e2d5a09e92285a6cc94b81e52e13..1443e4d877d4e288fb379a02fee8a351075d8db8 100644 --- a/crates/project/src/agent_server_store.rs +++ b/crates/project/src/agent_server_store.rs @@ -460,7 +460,7 @@ impl AgentServerStore { .gemini .as_ref() .and_then(|settings| settings.ignore_system_version) - .unwrap_or(false), + .unwrap_or(true), }), ); self.external_agents.insert( diff --git a/crates/project/src/git_store.rs b/crates/project/src/git_store.rs index a414a03320a2defa4c9dbd4b6193a131e761d2c7..85ff38ab67f873d8197729de9577075951676597 100644 --- a/crates/project/src/git_store.rs +++ b/crates/project/src/git_store.rs @@ -1672,59 +1672,6 @@ impl GitStore { } } - fn mark_entries_pending_by_project_paths( - &mut self, - project_paths: &[ProjectPath], - stage: bool, - cx: &mut Context, - ) { - let buffer_store = &self.buffer_store; - - for project_path in project_paths { - let Some(buffer) = buffer_store.read(cx).get_by_path(project_path) else { - continue; - }; - - let buffer_id = buffer.read(cx).remote_id(); - let Some(diff_state) = self.diffs.get(&buffer_id) else { - continue; - }; - - diff_state.update(cx, |diff_state, cx| { - let Some(uncommitted_diff) = diff_state.uncommitted_diff() else { - return; - }; - - let buffer_snapshot = buffer.read(cx).text_snapshot(); - let file_exists = buffer - .read(cx) - .file() - .is_some_and(|file| file.disk_state().exists()); - - let all_hunks: Vec<_> = uncommitted_diff - .read(cx) - .hunks_intersecting_range( - text::Anchor::MIN..text::Anchor::MAX, - &buffer_snapshot, - cx, - ) - .collect(); - - if !all_hunks.is_empty() { - uncommitted_diff.update(cx, |diff, cx| { - diff.stage_or_unstage_hunks( - stage, - &all_hunks, - &buffer_snapshot, - file_exists, - cx, - ); - }); - } - }); - } - } - pub fn git_clone( &self, repo: String, @@ -4253,28 +4200,6 @@ impl Repository { save_futures } - fn mark_entries_pending_for_stage( - &self, - entries: &[RepoPath], - stage: bool, - cx: &mut Context, - ) { - let Some(git_store) = self.git_store() else { - return; - }; - - let mut project_paths = Vec::new(); - for repo_path in entries { - if let Some(project_path) = self.repo_path_to_project_path(repo_path, cx) { - project_paths.push(project_path); - } - } - - git_store.update(cx, move |git_store, cx| { - git_store.mark_entries_pending_by_project_paths(&project_paths, stage, cx); - }); - } - pub fn stage_entries( &mut self, entries: Vec, @@ -4283,9 +4208,6 @@ impl Repository { if entries.is_empty() { return Task::ready(Ok(())); } - - self.mark_entries_pending_for_stage(&entries, true, cx); - let id = self.id; let save_tasks = self.save_buffers(&entries, cx); let paths = entries @@ -4351,9 +4273,6 @@ impl Repository { if entries.is_empty() { return Task::ready(Ok(())); } - - self.mark_entries_pending_for_stage(&entries, false, cx); - let id = self.id; let save_tasks = self.save_buffers(&entries, cx); let paths = entries @@ -5948,6 +5867,11 @@ impl Repository { self.pending_ops.edit(edits, ()); ids } + pub fn default_remote_url(&self) -> Option { + self.remote_upstream_url + .clone() + .or(self.remote_origin_url.clone()) + } } fn get_permalink_in_rust_registry_src( diff --git a/crates/prompt_store/src/prompts.rs b/crates/prompt_store/src/prompts.rs index 674d4869e9825fd700dde3db510fbf68c6b4d5cc..6a845bb8dd394f8a1ff26a8a0e130156a2a158bd 100644 --- a/crates/prompt_store/src/prompts.rs +++ b/crates/prompt_store/src/prompts.rs @@ -112,7 +112,7 @@ pub struct ContentPromptContextV2 { pub language_name: Option, pub is_truncated: bool, pub document_content: String, - pub rewrite_section: Option, + pub rewrite_section: String, pub diagnostic_errors: Vec, } @@ -310,7 +310,6 @@ impl PromptBuilder { }; const MAX_CTX: usize = 50000; - let is_insert = range.is_empty(); let mut is_truncated = false; let before_range = 0..range.start; @@ -335,28 +334,19 @@ impl PromptBuilder { for chunk in buffer.text_for_range(truncated_before) { document_content.push_str(chunk); } - if is_insert { - document_content.push_str(""); - } else { - document_content.push_str("\n"); - for chunk in buffer.text_for_range(range.clone()) { - document_content.push_str(chunk); - } - document_content.push_str("\n"); + + document_content.push_str("\n"); + for chunk in buffer.text_for_range(range.clone()) { + document_content.push_str(chunk); } + document_content.push_str("\n"); + for chunk in buffer.text_for_range(truncated_after) { document_content.push_str(chunk); } - let rewrite_section = if !is_insert { - let mut section = String::new(); - for chunk in buffer.text_for_range(range.clone()) { - section.push_str(chunk); - } - Some(section) - } else { - None - }; + let rewrite_section: String = buffer.text_for_range(range.clone()).collect(); + let diagnostics = buffer.diagnostics_in_range::<_, Point>(range, false); let diagnostic_errors: Vec = diagnostics .map(|entry| { diff --git a/crates/supermaven/src/supermaven_edit_prediction_delegate.rs b/crates/supermaven/src/supermaven_edit_prediction_delegate.rs index 578bc894f223fd458f510694194aebe633d7a6db..9563a0aa99f1760b5af214be28f25dbf1734c371 100644 --- a/crates/supermaven/src/supermaven_edit_prediction_delegate.rs +++ b/crates/supermaven/src/supermaven_edit_prediction_delegate.rs @@ -1,6 +1,6 @@ use crate::{Supermaven, SupermavenCompletionStateId}; use anyhow::Result; -use edit_prediction_types::{Direction, EditPrediction, EditPredictionDelegate}; +use edit_prediction_types::{EditPrediction, EditPredictionDelegate}; use futures::StreamExt as _; use gpui::{App, Context, Entity, EntityId, Task}; use language::{Anchor, Buffer, BufferSnapshot}; @@ -189,15 +189,6 @@ impl EditPredictionDelegate for SupermavenEditPredictionDelegate { })); } - fn cycle( - &mut self, - _buffer: Entity, - _cursor_position: Anchor, - _direction: Direction, - _cx: &mut Context, - ) { - } - fn accept(&mut self, _cx: &mut Context) { reset_completion_cache(self, _cx); } diff --git a/crates/terminal/src/terminal_hyperlinks.rs b/crates/terminal/src/terminal_hyperlinks.rs index 8ff33895251f707c8bc9a7894bd74b0bb323ae6c..4fe2baa2dc27f3a589efd9b7739262a6fec3fcb4 100644 --- a/crates/terminal/src/terminal_hyperlinks.rs +++ b/crates/terminal/src/terminal_hyperlinks.rs @@ -11,6 +11,7 @@ use alacritty_terminal::{ use log::{info, warn}; use regex::Regex; use std::{ + iter::{once, once_with}, ops::{Index, Range}, time::{Duration, Instant}, }; @@ -232,14 +233,17 @@ fn path_match( (line_end.line.0 - line_start.line.0 + 1) as usize * term.grid().columns(), ); let first_cell = &term.grid()[line_start]; + let mut prev_len = 0; line.push(first_cell.c); - let mut start_offset = 0; + let mut prev_char_is_space = first_cell.c == ' '; let mut hovered_point_byte_offset = None; + let mut hovered_word_start_offset = None; + let mut hovered_word_end_offset = None; - if !first_cell.flags.intersects(WIDE_CHAR_SPACERS) { - start_offset += first_cell.c.len_utf8(); - if line_start == hovered { - hovered_point_byte_offset = Some(0); + if line_start == hovered { + hovered_point_byte_offset = Some(0); + if first_cell.c != ' ' { + hovered_word_start_offset = Some(0); } } @@ -247,27 +251,44 @@ fn path_match( if cell.point > line_end { break; } - let is_spacer = cell.flags.intersects(WIDE_CHAR_SPACERS); - if cell.point == hovered { - debug_assert!(hovered_point_byte_offset.is_none()); - if start_offset > 0 && cell.flags.contains(Flags::WIDE_CHAR_SPACER) { - // If we hovered on a trailing spacer, back up to the end of the previous char's bytes. - start_offset -= 1; + + if !cell.flags.intersects(WIDE_CHAR_SPACERS) { + prev_len = line.len(); + match cell.c { + ' ' | '\t' => { + if hovered_point_byte_offset.is_some() && !prev_char_is_space { + if hovered_word_end_offset.is_none() { + hovered_word_end_offset = Some(line.len()); + } + } + line.push(' '); + prev_char_is_space = true; + } + c @ _ => { + if hovered_point_byte_offset.is_none() && prev_char_is_space { + hovered_word_start_offset = Some(line.len()); + } + line.push(c); + prev_char_is_space = false; + } } - hovered_point_byte_offset = Some(start_offset); - } else if cell.point < hovered && !is_spacer { - start_offset += cell.c.len_utf8(); } - if !is_spacer { - line.push(match cell.c { - '\t' => ' ', - c @ _ => c, - }); + if cell.point == hovered { + debug_assert!(hovered_point_byte_offset.is_none()); + hovered_point_byte_offset = Some(prev_len); } } let line = line.trim_ascii_end(); let hovered_point_byte_offset = hovered_point_byte_offset?; + let hovered_word_range = { + let word_start_offset = hovered_word_start_offset.unwrap_or(0); + (word_start_offset != 0) + .then_some(word_start_offset..hovered_word_end_offset.unwrap_or(line.len())) + }; + if line.len() <= hovered_point_byte_offset { + return None; + } let found_from_range = |path_range: Range, link_range: Range, position: Option<(u32, Option)>| { @@ -313,10 +334,27 @@ fn path_match( for regex in path_hyperlink_regexes { let mut path_found = false; - for captures in regex.captures_iter(&line) { + for (line_start_offset, captures) in once( + regex + .captures_iter(&line) + .next() + .map(|captures| (0, captures)), + ) + .chain(once_with(|| { + if let Some(hovered_word_range) = &hovered_word_range { + regex + .captures_iter(&line[hovered_word_range.clone()]) + .next() + .map(|captures| (hovered_word_range.start, captures)) + } else { + None + } + })) + .flatten() + { path_found = true; let match_range = captures.get(0).unwrap().range(); - let (path_range, line_column) = if let Some(path) = captures.name("path") { + let (mut path_range, line_column) = if let Some(path) = captures.name("path") { let parse = |name: &str| { captures .name(name) @@ -330,10 +368,15 @@ fn path_match( } else { (match_range.clone(), None) }; - let link_range = captures + let mut link_range = captures .name("link") .map_or_else(|| match_range.clone(), |link| link.range()); + path_range.start += line_start_offset; + path_range.end += line_start_offset; + link_range.start += line_start_offset; + link_range.end += line_start_offset; + if !link_range.contains(&hovered_point_byte_offset) { // No match, just skip. continue; @@ -638,9 +681,6 @@ mod tests { test_path!( "‹«🦀 multiple_👉same_line 🦀» 🚣«4» 🏛️«2»›: 🦀 multiple_same_line 🦀 🚣4 🏛️2:" ); - test_path!( - "🦀 multiple_same_line 🦀 🚣4 🏛️2 ‹«🦀 multiple_👉same_line 🦀» 🚣«4» 🏛️«2»›:" - ); // ls output (tab separated) test_path!( @@ -977,7 +1017,7 @@ mod tests { use crate::TerminalSettings; use alacritty_terminal::{ event::VoidListener, - grid::Dimensions, + grid::Scroll, index::{Column, Point as AlacPoint}, term::test::mock_term, term::{Term, search::Match}, @@ -986,14 +1026,20 @@ mod tests { use std::{cell::RefCell, rc::Rc}; use util_macros::perf; - fn build_test_term(line: &str) -> (Term, AlacPoint) { - let content = line.repeat(500); - let term = mock_term(&content); - let point = AlacPoint::new( - term.grid().bottommost_line() - 1, - Column(term.grid().last_column().0 / 2), - ); - + fn build_test_term( + line: &str, + repeat: usize, + hover_offset_column: usize, + ) -> (Term, AlacPoint) { + let content = line.repeat(repeat); + let mut term = mock_term(&content); + term.resize(TermSize { + columns: 1024, + screen_lines: 10, + }); + term.scroll_display(Scroll::Top); + let point = + AlacPoint::new(Line(term.topmost_line().0 + 3), Column(hover_offset_column)); (term, point) } @@ -1002,11 +1048,14 @@ mod tests { const LINE: &str = " Compiling terminal v0.1.0 (/Hyperlinks/Bench/Source/zed-hyperlinks/crates/terminal)\r\n"; thread_local! { static TEST_TERM_AND_POINT: (Term, AlacPoint) = - build_test_term(LINE); + build_test_term(LINE, 500, 50); } TEST_TERM_AND_POINT.with(|(term, point)| { - assert!( - find_from_grid_point_bench(term, *point).is_some(), + assert_eq!( + find_from_grid_point_bench(term, *point) + .map(|(path, ..)| path) + .unwrap_or_default(), + "/Hyperlinks/Bench/Source/zed-hyperlinks/crates/terminal", "Hyperlink should have been found" ); }); @@ -1017,11 +1066,14 @@ mod tests { const LINE: &str = " --> /Hyperlinks/Bench/Source/zed-hyperlinks/crates/terminal/terminal.rs:1000:42\r\n"; thread_local! { static TEST_TERM_AND_POINT: (Term, AlacPoint) = - build_test_term(LINE); + build_test_term(LINE, 500, 50); } TEST_TERM_AND_POINT.with(|(term, point)| { - assert!( - find_from_grid_point_bench(term, *point).is_some(), + assert_eq!( + find_from_grid_point_bench(term, *point) + .map(|(path, ..)| path) + .unwrap_or_default(), + "/Hyperlinks/Bench/Source/zed-hyperlinks/crates/terminal/terminal.rs:1000:42", "Hyperlink should have been found" ); }); @@ -1032,11 +1084,111 @@ mod tests { const LINE: &str = "Cargo.toml experiments notebooks rust-toolchain.toml tooling\r\n"; thread_local! { static TEST_TERM_AND_POINT: (Term, AlacPoint) = - build_test_term(LINE); + build_test_term(LINE, 500, 60); } TEST_TERM_AND_POINT.with(|(term, point)| { - assert!( - find_from_grid_point_bench(term, *point).is_some(), + assert_eq!( + find_from_grid_point_bench(term, *point) + .map(|(path, ..)| path) + .unwrap_or_default(), + "rust-toolchain.toml", + "Hyperlink should have been found" + ); + }); + } + + #[perf] + // https://github.com/zed-industries/zed/pull/44407 + pub fn pr_44407_hyperlink_benchmark() { + const LINE: &str = "-748, 706, 163, 222, -980, 949, 381, -568, 199, 501, 760, -821, 90, -451, 183, 867, -351, -810, -762, -109, 423, 84, 14, -77, -820, -345, 74, -791, 930, -618, -900, 862, -959, 289, -19, 471, -757, 793, 155, -554, 249, 830, 402, 732, -731, -866, -720, -703, -257, -439, 731, 872, -489, 676, -167, 613, -698, 415, -80, -453, -896, 333, -511, 621, -450, 624, -309, -575, 177, 141, 891, -104, -97, -367, -599, -675, 607, -225, -760, 552, -465, 804, 55, 282, 104, -929, -252,\ +-311, 900, 550, 599, -80, 774, 553, 837, -395, 541, 953, 154, -396, -596, -111, -802, -221, -337, -633, -73, -527, -82, -658, -264, 222, 375, 434, 204, -756, -703, 303, 239, -257, -365, -351, 904, 364, -743, -484, 655, -542, 446, 888, 632, -167, -260, 716, 150, 806, 723, 513, -118, -323, -683, 983, -564, 358, -16, -287, 277, -607, 87, 365, -1, 164, 401, 257, 369, -893, 145, -969, 375, -53, 541, -408, -865, 753, 258, 337, -886, 593, -378, -528, 191, 204, 566, -61, -621, 769, 524, -628, 6,\ +249, 896, -785, -776, 321, -681, 604, -740, 886, 426, -480, -983, 23, -247, 125, -666, 913, 842, -460, -797, -483, -58, -565, -587, -206, 197, 715, 764, -97, 457, -149, -226, 261, 194, -390, 431, 180, -778, 829, -657, -668, 397, 859, 152, -178, 677, -18, 687, -247, 96, 466, -572, 478, 622, -143, -25, -471, 265, 335, 957, 152, -951, -647, 670, 57, 152, -115, 206, 87, 629, -798, -125, -725, -31, 844, 398, -876, 44, 963, -211, 518, -8, -103, -999, 948, 823, 149, -803, 769, -236, -683, 527,\ +-108, -36, 18, -437, 687, -305, -526, 972, -965, 276, 420, -259, -379, -142, -747, 600, -578, 197, 673, 890, 324, -931, 755, -765, -422, 785, -369, -110, -505, 532, -208, -438, 713, 110, 853, 996, -360, 823, 289, -699, 629, -661, 560, -329, -323, 439, 571, -537, 644, -84, 25, -536, -161, 112, 169, -922, -537, -734, -423, 37, 451, -149, 408, 18, -672, 206, -784, 444, 593, -241, 502, -259, -798, -352, -658, 712, -675, -734, 627, -620, 64, -554, 999, -537, -160, -641, 464, 894, 29, 322, 566,\ +-510, -749, 982, 204, 967, -261, -986, -136, 251, -598, 995, -831, 891, 22, 761, -783, -415, 125, 470, -919, -97, -668, 85, 205, -175, -550, 502, 652, -468, 798, 775, -216, 89, -433, -24, -621, 877, -126, 951, 809, 782, 156, -618, -841, -463, 19, -723, -904, 550, 263, 991, -758, -114, 446, -731, -623, -634, 462, 48, 851, 333, -846, 480, 892, -966, -910, -436, 317, -711, -341, -294, 124, 238, -214, -281, 467, -950, -342, 913, -90, -388, -573, 740, -883, -451, 493, -500, 863, 930, 127, 530,\ +-810, 540, 541, -664, -951, -227, -420, -476, -581, -534, 549, 253, 984, -985, -84, -521, 538, 484, -440, 371, 784, -306, -850, 530, -133, 251, -799, 446, -170, -243, -674, 769, 646, 778, -680, -714, -442, 804, 901, -774, 69, 307, -293, 755, 443, 224, -918, -771, 723, 40, 132, 568, -847, -47, 844, 69, 986, -293, -459, 313, 155, 331, 69, 280, -637, 569, 104, -119, -988, 252, 857, -590, 810, -891, 484, 566, -934, -587, -290, 566, 587, 489, 870, 280, 454, -252, 613, -701, -278, 195, -198,\ +683, 533, -372, 707, -152, 371, 866, 609, -5, -372, -30, -694, 552, 192, 452, -663, 350, -985, 10, 884, 813, -592, -331, -470, 711, -941, 928, 379, -339, 220, 999, 376, 507, 179, 916, 84, 104, 392, 192, 299, -860, 218, -698, -919, -452, 37, 850, 5, -874, 287, 123, -746, -575, 776, -909, 118, 903, -275, 450, -996, -591, -920, -850, 453, -896, 73, 83, -535, -20, 287, -765, 442, 808, 45, 445, 202, 917, -208, 783, 790, -534, 373, -129, 556, -757, -69, 459, -163, -59, 265, -563, -889, 635,\ +-583, -261, -790, 799, 826, 953, 85, 619, 334, 842, 672, -869, -4, -833, 315, 942, -524, 579, 926, 628, -404, 128, -629, 161, 568, -117, -526, 223, -876, 906, 176, -549, -317, 381, 375, -801, -416, 647, 335, 253, -386, -375, -254, 635, 352, 317, 398, -422, 111, 201, 220, 554, -972, 853, 378, 956, 942, -857, -289, -333, -180, 488, -814, -42, -595, 721, 39, 644, 721, -242, -44, 643, -457, -419, 560, -863, 974, 458, 222, -882, 526, -243, -318, -343, -707, -401, 117, 677, -489, 546, -903,\ +-960, -881, -684, 125, -928, -995, -692, -773, 647, -718, -862, -814, 671, 664, -130, -856, -674, 653, 711, 194, -685, -160, 138, -27, -128, -671, -242, 526, 494, -674, 424, -921, -778, 313, -237, 332, 913, 252, 808, -936, 289, 755, 52, -139, 57, -19, -827, -775, -561, -14, 107, -84, 622, -303, -747, 258, -942, 290, 211, -919, -207, 797, 95, 794, -830, -181, -788, 757, 75, -946, -949, -988, 152, 340, 732, 886, -891, -642, -666, 321, -910, 841, 632, 298, 55, -349, 498, 287, -711, 97, 305,\ +-974, -987, 790, -64, 605, -583, -821, 345, 887, -861, 548, 894, 288, 452, 556, -448, 813, 420, 545, 967, 127, -947, 19, -314, -607, -513, -851, 254, -290, -938, -783, -93, 474, 368, -485, -935, -539, 81, 404, -283, 779, 345, -164, 53, 563, -771, 911, -323, 522, -998, 315, 415, 460, 58, -541, -878, -152, -886, 201, -446, -810, 549, -142, -575, -632, 521, 549, 209, -681, 998, 798, -611, -919, -708, -4, 677, -172, 588, 750, -435, 508, 609, 498, -535, -691, -738, 85, 615, 705, 169, 425,\ +-669, -491, -783, 73, -847, 228, -981, -812, -229, 950, -904, 175, -438, 632, -556, 910, 173, 576, -751, -53, -169, 635, 607, -944, -13, -84, 105, -644, 984, 935, 259, -445, 620, -405, 832, 167, 114, 209, -181, -944, -496, 693, -473, 137, 38, -873, -334, -353, -57, 397, 944, 698, 811, -401, 712, -667, 905, 276, -653, 368, -543, -349, 414, 287, 894, 935, 461, 55, 741, -623, -660, -773, 617, 834, 278, -121, 52, 495, -855, -440, -210, -99, 279, -661, 540, 934, 540, 784, 895, 268, -503, 513,\ +-484, -352, 528, 341, -451, 885, -71, 799, -195, -885, -585, -233, 92, 453, 994, 464, 694, 190, -561, -116, 675, -775, -236, 556, -110, -465, 77, -781, 507, -960, -410, 229, -632, 717, 597, 429, 358, -430, -692, -825, 576, 571, 758, -891, 528, -267, 190, -869, 132, -811, 796, 750, -596, -681, 870, 360, 969, 860, -412, -567, 694, -86, -498, 38, -178, -583, -778, 412, 842, -586, 722, -192, 350, 363, 81, -677, -163, 564, 543, 671, 110, 314, 739, -552, -224, -644, 922, 685, 134, 613, 793,\ +-363, -244, -284, -257, -561, 418, 988, 333, 110, -966, 790, 927, 536, -620, -309, -358, 895, -867, -796, -357, 308, -740, 287, -732, -363, -969, 658, 711, 511, 256, 590, -574, 815, -845, -84, 546, -581, -71, -334, -890, 652, -959, 320, -236, 445, -851, 825, -756, -4, 877, 308, 573, -117, 293, 686, -483, 391, 342, -550, -982, 713, 886, 552, 474, -673, 283, -591, -383, 988, 435, -131, 708, -326, -884, 87, 680, -818, -408, -486, 813, -307, -799, 23, -497, 802, -146, -100, 541, 7, -493, 577,\ +50, -270, 672, 834, 111, -788, 247, 337, 628, -33, -964, -519, 683, 54, -703, 633, -127, -448, 759, -975, 696, 2, -870, -760, 67, 696, 306, 750, 615, 155, -933, -568, 399, 795, 164, -460, 205, 439, -526, -691, 35, -136, -481, -63, 73, -598, 748, 133, 874, -29, 4, -73, 472, 389, 962, 231, -328, 240, 149, 959, 46, -207, 72, -514, -608, 0, -14, 32, 374, -478, -806, 919, -729, -286, 652, 109, 509, -879, -979, -865, 584, -92, -346, -992, 781, 401, 575, 993, -746, -33, 684, -683, 750, -105,\ +-425, -508, -627, 27, 770, -45, 338, 921, -139, -392, -933, 634, 563, 224, -780, 921, 991, 737, 22, 64, 414, -249, -687, 869, 50, 759, -97, 515, 20, -775, -332, 957, 138, -542, -835, 591, -819, 363, -715, -146, -950, -641, -35, -435, -407, -548, -984, 383, -216, -559, 853, 4, -410, -319, -831, -459, -628, -819, -324, 755, 696, -192, 238, -234, -724, -445, 915, 302, -708, 484, 224, -641, 25, -771, 528, -106, -744, -588, 913, -554, -515, -239, -843, -812, -171, 721, 543, -269, 440, 151,\ +996, -723, -557, -522, -280, -514, -593, 208, 715, 404, 353, 270, -483, -785, 318, -313, 798, 638, 764, 748, -929, -827, -318, -56, 389, -546, -958, -398, 463, -700, 461, 311, -787, -488, 877, 456, 166, 535, -995, -189, -715, 244, 40, 484, 212, -329, -351, 638, -69, -446, -292, 801, -822, 490, -486, -185, 790, 370, -340, 401, -656, 584, 561, -749, 269, -19, -294, -111, 975, 874, -73, 851, 231, -331, -684, 460, 765, -654, -76, 10, 733, 520, 521, 416, -958, -202, -186, -167, 175, 343, -50,\ +673, -763, -854, -977, -17, -853, -122, -25, 180, 149, 268, 874, -816, -745, 747, -303, -959, 390, 509, 18, -66, 275, -277, 9, 837, -124, 989, -542, -649, -845, 894, 926, 997, -847, -809, -579, -96, -372, 766, 238, -251, 503, 559, 276, -281, -102, -735, 815, 109, 175, -10, 128, 543, -558, -707, 949, 996, -422, -506, 252, 702, -930, 552, -961, 584, -79, -177, 341, -275, 503, -21, 677, -545, 8, -956, -795, -870, -254, 170, -502, -880, 106, 174, 459, 603, -600, -963, 164, -136, -641, -309,\ +-380, -707, -727, -10, 727, 952, 997, -731, -133, 269, 287, 855, 716, -650, 479, 299, -839, -308, -782, 769, 545, 663, -536, -115, 904, -986, -258, -562, 582, 664, 408, -525, -889, 471, -370, -534, -220, 310, 766, 931, -193, -897, -192, -74, -365, -256, -359, -328, 658, -691, -431, 406, 699, 425, 713, -584, -45, -588, 289, 658, -290, -880, -987, -444, 371, 904, -155, 81, -278, -708, -189, -78, 655, 342, -998, -647, -734, -218, 726, 619, 663, 744, 518, 60, -409, 561, -727, -961, -306,\ +-147, -550, 240, -218, -393, 267, 724, 791, -548, 480, 180, -631, 825, -170, 107, 227, -691, 905, -909, 359, 227, 287, 909, 632, -89, -522, 80, -429, 37, 561, -732, -474, 565, -798, -460, 188, 507, -511, -654, 212, -314, -376, -997, -114, -708, 512, -848, 781, 126, -956, -298, 354, -400, -121, 510, 445, 926, 27, -708, 676, 248, 834, 542, 236, -105, -153, 102, 128, 96, -348, -626, 598, 8, 978, -589, -461, -38, 381, -232, -817, 467, 356, -151, -460, 429, -408, 425, 618, -611, -247, 819,\ +963, -160, 1000, 141, -647, -875, 108, 790, -127, 463, -37, -195, -542, 12, 845, -384, 770, -129, 315, 826, -942, 430, 146, -170, -583, -903, -489, 497, -559, -401, -29, -129, -411, 166, 942, -646, -862, -404, 785, 777, -111, -481, -738, 490, 741, -398, 846, -178, -509, -661, 748, 297, -658, -567, 531, 427, -201, -41, -808, -668, 782, -860, -324, 249, 835, -234, 116, 542, -201, 328, 675, 480, -906, 188, 445, 63, -525, 811, 277, 133, 779, -680, 950, -477, -306, -64, 552, -890, -956, 169,\ +442, 44, -169, -243, -242, 423, -884, -757, -403, 739, -350, 383, 429, 153, -702, -725, 51, 310, 857, -56, 538, 46, -311, 132, -620, -297, -124, 534, 884, -629, -117, 506, -837, -100, -27, -381, -735, 262, 843, 703, 260, -457, 834, 469, 9, 950, 59, 127, -820, 518, 64, -783, 659, -608, -676, 802, 30, 589, 246, -369, 361, 347, 534, -376, 68, 941, 709, 264, 384, 481, 628, 199, -568, -342, -337, 853, -804, -858, -169, -270, 641, -344, 112, 530, -773, -349, -135, -367, -350, -756, -911, 180,\ +-660, 116, -478, -265, -581, 510, 520, -986, 935, 219, 522, 744, 47, -145, 917, 638, 301, 296, 858, -721, 511, -816, 328, 473, 441, 697, -260, -673, -379, 893, 458, 154, 86, 905, 590, 231, -717, -179, 79, 272, -439, -192, 178, -200, 51, 717, -256, -358, -626, -518, -314, -825, -325, 588, 675, -892, -798, 448, -518, 603, -23, 668, -655, 845, -314, 783, -347, -496, 921, 893, -163, -748, -906, 11, -143, -64, 300, 336, 882, 646, 533, 676, -98, -148, -607, -952, -481, -959, -874, 764, 537,\ +736, -347, 646, -843, 966, -916, -718, -391, -648, 740, 755, 919, -608, 388, -655, 68, 201, 675, -855, 7, -503, 881, 760, 669, 831, 721, -564, -445, 217, 331, 970, 521, 486, -254, 25, -259, 336, -831, 252, -995, 908, -412, -240, 123, -478, 366, 264, -504, -843, 632, -288, 896, 301, 423, 185, 318, 380, 457, -450, -162, -313, 673, -963, 570, 433, -548, 107, -39, -142, -98, -884, -3, 599, -486, -926, 923, -82, 686, 290, 99, -382, -789, 16, 495, 570, 284, 474, -504, -201, -178, -1, 592, 52,\ +827, -540, -151, -991, 130, 353, -420, -467, -661, 417, -690, 942, 936, 814, -566, -251, -298, 341, -139, 786, 129, 525, -861, 680, 955, -245, -50, 331, 412, -38, -66, 611, -558, 392, -629, -471, -68, -535, 744, 495, 87, 558, 695, 260, -308, 215, -464, 239, -50, 193, -540, 184, -8, -194, 148, 898, -557, -21, 884, 644, -785, -689, -281, -737, 267, 50, 206, 292, 265, 380, -511, 310, 53, 375, -497, -40, 312, -606, -395, 142, 422, 662, -584, 72, 144, 40, -679, -593, 581, 689, -829, 442, 822,\ +977, -832, -134, -248, -207, 248, 29, 259, 189, 592, -834, -866, 102, 0, 340, 25, -354, -239, 420, -730, -992, -925, -314, 420, 914, 607, -296, -415, -30, 813, 866, 153, -90, 150, -81, 636, -392, -222, -835, 482, -631, -962, -413, -727, 280, 686, -382, 157, -404, -511, -432, 455, 58, 108, -408, 290, -829, -252, 113, 550, -935, 925, 422, 38, 789, 361, 487, -460, -769, -963, -285, 206, -799, -488, -233, 416, 143, -456, 753, 520, 599, 621, -168, 178, -841, 51, 952, 374, 166, -300, -576, 844,\ +-656, 90, 780, 371, 730, -896, -895, -386, -662, 467, -61, 130, -362, -675, -113, 135, -761, -55, 408, 822, 675, -347, 725, 114, 952, -510, -972, 390, -413, -277, -52, 315, -80, 401, -712, 147, -202, 84, 214, -178, 970, -571, -210, 525, -887, -863, 504, 192, 837, -594, 203, -876, -209, 305, -826, 377, 103, -928, -803, -956, 949, -868, -547, 824, -994, 516, 93, -524, -866, -890, -988, -501, 15, -6, 413, -825, 304, -818, -223, 525, 176, 610, 828, 391, 940, 540, -831, 650, 438, 589, 941, 57,\ +523, 126, 221, 860, -282, -262, -226, 764, 743, -640, 390, 384, -434, 608, -983, 566, -446, 618, 456, -176, -278, 215, 871, -180, 444, -931, -200, -781, 404, 881, 780, -782, 517, -739, -548, -811, 201, -95, -249, -228, 491, -299, 700, 964, -550, 108, 334, -653, 245, -293, -552, 350, -685, -415, -818, 216, -194, -255, 295, 249, 408, 351, 287, 379, 682, 231, -693, 902, -902, 574, 937, -708, -402, -460, 827, -268, 791, 343, -780, -150, -738, 920, -430, -88, -361, -588, -727, -47, -297, 662,\ +-840, -637, -635, 916, -857, 938, 132, -553, 391, -522, 640, 626, 690, 833, 867, -555, 577, 226, 686, -44, 0, -965, 651, -1, 909, 595, -646, 740, -821, -648, -962, 927, -193, 159, 490, 594, -189, 707, -884, 759, -278, -160, -566, -340, 19, 862, -440, 445, -598, 341, 664, -311, 309, -159, 19, -672, 705, -646, 976, 247, 686, -830, -27, -667, 81, 399, -423, -567, 945, 38, 51, 740, 621, 204, -199, -908, -593, 424, 250, -561, 695, 9, 520, 878, 120, -109, 42, -375, -635, -711, -687, 383, -278,\ +36, 970, 925, 864, 836, 309, 117, 89, 654, -387, 346, -53, 617, -164, -624, 184, -45, 852, 498, -513, 794, -682, -576, 13, -147, 285, -776, -886, -96, 483, 994, -188, 346, -629, -848, 738, 51, 128, -898, -753, -906, 270, -203, -577, 48, -243, -210, 666, 353, 636, -954, 862, 560, -944, -877, -137, 440, -945, -316, 274, -211, -435, 615, -635, -468, 744, 948, -589, 525, 757, -191, -431, 42, 451, -160, -827, -991, 324, 697, 342, -610, 894, -787, -384, 872, 734, 878, 70, -260, 57, 397, -518,\ +629, -510, -94, 207, 214, -625, 106, -882, -575, 908, -650, 723, -154, 45, 108, -69, -565, 927, -68, -351, 707, -282, 429, -889, -596, 848, 578, -492, 41, -822, -992, 168, -286, -780, 970, 597, -293, -12, 367, 708, -415, 194, -86, -390, 224, 69, -368, -674, 1000, -672, 356, -202, -169, 826, 476, -285, 29, -448, 545, 186, 319, 67, 705, 412, 225, -212, -351, -391, -783, -9, 875, -59, -159, -123, -151, -296, 871, -638, 359, 909, -945, 345, -16, -562, -363, -183, -625, -115, -571, -329, 514,\ +99, 263, 463, -39, 597, -652, -349, 246, 77, -127, -563, -879, -30, 756, 777, -865, 675, -813, -501, 871, -406, -627, 834, -609, -205, -812, 643, -204, 291, -251, -184, -584, -541, 410, -573, -600, 908, -871, -687, 296, -713, -139, -778, -790, 347, -52, -400, 407, -653, 670, 39, -856, 904, 433, 392, 590, -271, -144, -863, 443, 353, 468, -544, 486, -930, 458, -596, -890, 163, 822, 768, 980, -783, -792, 126, 386, 367, -264, 603, -61, 728, 160, -4, -837, 832, 591, 436, 518, 796, -622, -867,\ +-669, -947, 253, 100, -792, 841, 413, 833, -249, -550, 282, -825, 936, -348, 898, -451, -283, 818, -237, 630, 216, -499, -637, -511, 767, -396, 221, 958, -586, -920, 401, -313, -580, -145, -270, 118, 497, 426, -975, 480, -445, -150, -721, -929, 439, -893, 902, 960, -525, -793, 924, 563, 683, -727, -86, 309, 432, -762, -345, 371, -617, 149, -215, -228, 505, 593, -20, -292, 704, -999, 149, -104, 819, -414, -443, 517, -599, -5, 145, -24, -993, -283, 904, 174, -112, -276, -860, 44, -257,\ +-931, -821, -667, 540, 421, 485, 531, 407, 833, 431, -415, 878, 503, -901, 639, -608, 896, 860, 927, 424, 113, -808, -323, 729, 382, -922, 548, -791, -379, 207, 203, 559, 537, 137, 999, -913, -240, 942, 249, 616, 775, -4, 915, 855, -987, -234, -384, 948, -310, -542, 125, -289, -599, 967, -492, -349, -552, 562, -926, 632, -164, 217, -165, -496, 847, 684, -884, 457, -748, -745, -38, 93, 961, 934, 588, 366, -130, 851, -803, -811, -211, 428, 183, -469, 888, 596, -475, -899, -681, 508, 184,\ +921, 863, -610, -416, -119, -966, -686, 210, 733, 715, -889, -925, -434, -566, -455, 596, -514, 983, 755, -194, -802, -313, 91, -541, 808, -834, 243, -377, 256, 966, -402, -773, -308, -605, 266, 866, 118, -425, -531, 498, 666, 813, -267, 830, 69, -869, -496, 735, 28, 488, -645, -493, -689, 170, -940, 532, 844, -658, -617, 408, -200, 764, -665, 568, 342, 621, 908, 471, 280, 859, 709, 898, 81, -547, 406, 514, -595, 43, -824, -696, -746, -429, -59, -263, -813, 233, 279, -125, 687, -418,\ +-530, 409, 614, 803, -407, 78, -676, -39, -887, -141, -292, 270, -343, 400, 907, 588, 668, 899, 973, 103, -101, -11, 397, -16, 165, 705, -410, -585, 316, 391, -346, -336, 957, -118, -538, -441, -845, 121, 591, -359, -188, -362, -208, 27, -925, -157, -495, -177, -580, 9, 531, -752, 94, 107, 820, 769, -500, 852, 617, 145, 355, 34, -463, -265, -709, -111, -855, -405, 560, 470, 3, -177, -164, -249, 450, 662, 841, -689, -509, 987, -33, 769, 234, -2, 203, 780, 744, -895, 497, -432, -406, -264,\ +-71, 124, 778, -897, 495, 127, -76, 52, -768, 205, 464, -992, 801, -83, -806, 545, -316, 146, 772, 786, 289, -936, 145, -30, -722, -455, 270, 444, 427, -482, 383, -861, 36, 630, -404, 83, 864, 743, -351, -846, 315, -837, 357, -195, 450, -715, 227, -942, 740, -519, 476, 716, 713, 169, 492, -112, -49, -931, 866, 95, -725, 198, -50, -17, -660, 356, -142, -781, 53, 431, 720, 143, -416, 446, -497, 490, -96, 157, 239, 487, -337, -224, -445, 813, 92, -22, 603, 424, 952, -632, -367, 898, -927,\ +884, -277, -187, -777, 537, -575, -313, 347, -33, 800, 672, -919, -541, 5, -270, -94, -265, -793, -183, -761, -516, -608, -218, 57, -889, -912, 508, 93, -90, 34, 530, 201, 999, -37, -186, -62, -980, 239, 902, 983, -287, -634, 524, -772, 470, -961, 32, 162, 315, -411, 400, -235, -283, -787, -703, 869, 792, 543, -274, 239, 733, -439, 306, 349, 579, -200, -201, -824, 384, -246, 133, -508, 770, -102, 957, -825, 740, 748, -376, 183, -426, 46, 668, -886, -43, -174, 672, -419, 390, 927, 1000,\ +318, 886, 47, 908, -540, -825, -5, 314, -999, 354, -603, 966, -633, -689, 985, 534, -290, 167, -652, -797, -612, -79, 488, 622, -464, -950, 595, 897, 704, -238, -395, 125, 831, -180, 226, -379, 310, 564, 56, -978, 895, -61, 686, -251, 434, -417, 161, -512, 752, 528, -589, -425, 66, -925, -157, 1000, 96, 256, -239, -784, -882, -464, -909, 663, -177, -678, -441, 669, -564, -201, -121, -743, 187, -107, -768, -682, 355, 161, 411, 984, -954, 166, -842, -755, 267, -709, 372, -699, -272, -850,\ +403, -839, 949, 622, -62, 51, 917, 70, 528, -558, -632, 832, 276, 61, -445, -195, 960, 846, -474, 764, 879, -411, 948, -62, -592, -123, -96, -551, -555, -724, 849, 250, -808, -732, 797, -839, -554, 306, -919, 888, 484, -728, 152, -122, -287, 16, -345, -396, -268, -963, -500, 433, 343, 418, -480, 828, 594, 821, -9, 933, -230, 707, -847, -610, -748, -234, 688, 935, 713, 865, -743, 293, -143, -20, 928, -906, -762, 528, 722, 412, -70, 622, -245, 539, -686, 730, -866, -705, 28, -916, -623,\ +-768, -614, -915, -123, -183, 680, -223, 515, -37, -235, -5, 260, 347, -239, -322, -861, -848, -936, 945, 721, -580, -639, 780, -153, -26, 685, 177, 587, 307, -915, 435, 658, 539, -229, -719, -171, -858, 162, 734, -539, -437, 246, 639, 765, -477, -342, -209, -284, -779, -414, -452, 914, 338, -83, 759, 567, 266, -485, 14, 225, 347, -432, -242, 997, -365, -764, 119, -641, -416, -388, -436, -388, -54, -649, -571, -920, -477, 714, -363, 836, 369, 702, 869, 503, -287, -679, 46, -666, -202,\ +-602, 71, -259, 967, 601, -571, -830, -993, -271, 281, -494, 482, -180, 572, 587, -651, -566, -448, -228, 511, -924, 832, -52, -712, 402, -644, -533, -865, 269, 965, 56, 675, 179, -338, -272, 614, 602, -283, 303, -70, 909, -942, 117, 839, 468, 813, -765, 884, -697, -813, 352, 374, -705, -295, 633, 211, -754, 597, -941, -142, -393, -469, -653, 688, 996, 911, 214, 431, 453, -141, 874, -81, -258, -735, -3, -110, -338, -929, -182, -306, -104, -840, -588, -759, -157, -801, 848, -698, 627, 914,\ +-33, -353, 425, 150, -798, 553, 934, -778, -196, -132, 808, 745, -894, 144, 213, 662, 273, -79, 454, -60, -467, 48, -15, -807, 69, -930, 749, 559, -867, -103, 258, -677, 750, -303, 846, -227, -936, 744, -770, 770, -434, 594, -477, 589, -612, 535, 357, -623, 683, 369, 905, 980, -410, -663, 762, -888, -563, -845, 843, 353, -491, 996, -255, -336, -132, 695, -823, 289, -143, 365, 916, 877, 245, -530, -848, -804, -118, -108, 847, 620, -355, 499, 881, 92, -640, 542, 38, 626, -260, -34, -378,\ +598, 890, 305, -118, 711, -385, 600, -570, 27, -129, -893, 354, 459, 374, 816, 470, 356, 661, 877, 735, -286, -780, 620, 943, -169, -888, 978, 441, -667, -399, 662, 249, 137, 598, -863, -453, 722, -815, -251, -995, -294, -707, 901, 763, 977, 137, 431, -994, 905, 593, 694, 444, -626, -816, 252, 282, 616, 841, 360, -932, 817, -908, 50, 394, -120, -786, -338, 499, -982, -95, -454, 838, -312, 320, -127, -653, 53, 16, 988, -968, -151, -369, -836, 293, -271, 483, 18, 724, -204, -965, 245, 310,\ +987, 552, -835, -912, -861, 254, 560, 124, 145, 798, 178, 476, 138, -311, 151, -907, -886, -592, 728, -43, -489, 873, -422, -439, -489, 375, -703, -459, 338, 418, -25, 332, -454, 730, -604, -800, 37, -172, -197, -568, -563, -332, 228, -182, 994, -123, 444, -567, 98, 78, 0, -504, -150, 88, -936, 199, -651, -776, 192, 46, 526, -727, -991, 534, -659, -738, 256, -894, 965, -76, 816, 435, -418, 800, 838, 67, -733, 570, 112, -514, -416\r\ +"; + thread_local! { + static TEST_TERM_AND_POINT: (Term, AlacPoint) = + build_test_term(&LINE, 5, 50); + } + TEST_TERM_AND_POINT.with(|(term, point)| { + assert_eq!( + find_from_grid_point_bench(term, *point) + .map(|(path, ..)| path) + .unwrap_or_default(), + "392", + "Hyperlink should have been found" + ); + }); + } + + #[perf] + // https://github.com/zed-industries/zed/issues/44510 + pub fn issue_44510_hyperlink_benchmark() { + const LINE: &str = "..............................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................\ +..............................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................\ +..............................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................\ +..............................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................\ +..............................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................\ +..............................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................\ +..............................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................\ +..............................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................\ +..............................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................\ +..............................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................\ +..............................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................\ +..............................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................\ +..............................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................\ +..............................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................\ +...............................................E.\r\ +"; + thread_local! { + static TEST_TERM_AND_POINT: (Term, AlacPoint) = + build_test_term(&LINE, 5, 50); + } + TEST_TERM_AND_POINT.with(|(term, point)| { + assert_eq!( + find_from_grid_point_bench(term, *point) + .map(|(path, ..)| path) + .unwrap_or_default(), + LINE.trim_end_matches(['.', '\r', '\n']), "Hyperlink should have been found" ); }); diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index 85c6b81f406597e097cabc27408d3df70aad6395..a00e544f97836078ab8d96f2e90d36893cac27ca 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -790,8 +790,7 @@ impl TerminalPanel { } pane.update(cx, |pane, cx| { - let focus = pane.has_focus(window, cx) - || matches!(reveal_strategy, RevealStrategy::Always); + let focus = matches!(reveal_strategy, RevealStrategy::Always); pane.add_item(terminal_view, true, focus, None, window, cx); }); @@ -853,8 +852,7 @@ impl TerminalPanel { } pane.update(cx, |pane, cx| { - let focus = pane.has_focus(window, cx) - || matches!(reveal_strategy, RevealStrategy::Always); + let focus = matches!(reveal_strategy, RevealStrategy::Always); pane.add_item(terminal_view, true, focus, None, window, cx); }); diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 54808934ba7b098a695a8b104a048a379966e6f1..e7e60ff4b31dfbdd16b7de8841285d81fc311fc5 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -8,8 +8,8 @@ mod terminal_slash_command; use assistant_slash_command::SlashCommandRegistry; use editor::{EditorSettings, actions::SelectAll, blink_manager::BlinkManager}; use gpui::{ - Action, AnyElement, App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, - KeyContext, KeyDownEvent, Keystroke, MouseButton, MouseDownEvent, Pixels, Render, + Action, AnyElement, App, ClipboardEntry, DismissEvent, Entity, EventEmitter, FocusHandle, + Focusable, KeyContext, KeyDownEvent, Keystroke, MouseButton, MouseDownEvent, Pixels, Render, ScrollWheelEvent, Styled, Subscription, Task, WeakEntity, actions, anchored, deferred, div, }; use persistence::TERMINAL_DB; @@ -687,12 +687,32 @@ impl TerminalView { ///Attempt to paste the clipboard into the terminal fn paste(&mut self, _: &Paste, _: &mut Window, cx: &mut Context) { - if let Some(clipboard_string) = cx.read_from_clipboard().and_then(|item| item.text()) { + let Some(clipboard) = cx.read_from_clipboard() else { + return; + }; + + if clipboard.entries().iter().any(|entry| match entry { + ClipboardEntry::Image(image) => !image.bytes.is_empty(), + _ => false, + }) { + self.forward_ctrl_v(cx); + return; + } + + if let Some(text) = clipboard.text() { self.terminal - .update(cx, |terminal, _cx| terminal.paste(&clipboard_string)); + .update(cx, |terminal, _cx| terminal.paste(&text)); } } + /// Emits a raw Ctrl+V so TUI agents can read the OS clipboard directly + /// and attach images using their native workflows. + fn forward_ctrl_v(&self, cx: &mut Context) { + self.terminal.update(cx, |term, _| { + term.input(vec![0x16]); + }); + } + fn send_text(&mut self, text: &SendText, _: &mut Window, cx: &mut Context) { self.clear_bell(cx); self.terminal.update(cx, |term, _| { diff --git a/crates/ui/src/components/callout.rs b/crates/ui/src/components/callout.rs index 4eb849d7f640aca78b70645f5f93301281ca6627..de95e5db2bcee2e7acbadf5570de09d9cdedbf4d 100644 --- a/crates/ui/src/components/callout.rs +++ b/crates/ui/src/components/callout.rs @@ -121,7 +121,7 @@ impl RenderOnce for Callout { Severity::Info => ( IconName::Info, Color::Muted, - cx.theme().colors().panel_background.opacity(0.), + cx.theme().status().info_background.opacity(0.1), ), Severity::Success => ( IconName::Check, diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index 4bde632ce720dad792d19677c60ab62fd51d3637..1570c125fa33135631d8181359ad34bb7802ec5f 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -886,8 +886,12 @@ impl ItemHandle for Entity { // Only trigger autosave if focus has truly left the item. // If focus is still within the item's hierarchy (e.g., moved to a context menu), // don't trigger autosave to avoid unwanted formatting and cursor jumps. + // Also skip autosave if focus moved to a modal (e.g., command palette), + // since the user is still interacting with the workspace. let focus_handle = item.item_focus_handle(cx); - if !focus_handle.contains_focused(window, cx) { + if !focus_handle.contains_focused(window, cx) + && !workspace.has_active_modal(window, cx) + { Pane::autosave_item(&item, workspace.project.clone(), window, cx) .detach_and_log_err(cx); } diff --git a/crates/workspace/src/modal_layer.rs b/crates/workspace/src/modal_layer.rs index db4d85752835299117dba7fc2aeb1833383a390a..58667e7ffa8ad4fe5a22d293e4fc4aa71015a3bd 100644 --- a/crates/workspace/src/modal_layer.rs +++ b/crates/workspace/src/modal_layer.rs @@ -193,6 +193,12 @@ impl Render for ModalLayer { background.fade_out(0.2); this.bg(background) }) + .on_mouse_down( + MouseButton::Left, + cx.listener(|this, _, window, cx| { + this.hide_modal(window, cx); + }), + ) .child( v_flex() .h(px(0.0)) diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs index cf5bdf2ab0059f10f2fb44e2069c8c0baf24d72b..094d03494e726677dc43235d96fc62c076673bf5 100644 --- a/crates/workspace/src/persistence.rs +++ b/crates/workspace/src/persistence.rs @@ -3296,4 +3296,53 @@ mod tests { assert_eq!(workspace.center_group, new_workspace.center_group); } + + #[gpui::test] + async fn test_empty_workspace_window_bounds() { + zlog::init_test(); + + let db = WorkspaceDb::open_test_db("test_empty_workspace_window_bounds").await; + let id = db.next_id().await.unwrap(); + + // Create a workspace with empty paths (empty workspace) + let empty_paths: &[&str] = &[]; + let display_uuid = Uuid::new_v4(); + let window_bounds = SerializedWindowBounds(WindowBounds::Windowed(Bounds { + origin: point(px(100.0), px(200.0)), + size: size(px(800.0), px(600.0)), + })); + + let workspace = SerializedWorkspace { + id, + paths: PathList::new(empty_paths), + location: SerializedWorkspaceLocation::Local, + center_group: Default::default(), + window_bounds: None, + display: None, + docks: Default::default(), + breakpoints: Default::default(), + centered_layout: false, + session_id: None, + window_id: None, + user_toolchains: Default::default(), + }; + + // Save the workspace (this creates the record with empty paths) + db.save_workspace(workspace.clone()).await; + + // Save window bounds separately (as the actual code does via set_window_open_status) + db.set_window_open_status(id, window_bounds, display_uuid) + .await + .unwrap(); + + // Retrieve it using empty paths + let retrieved = db.workspace_for_roots(empty_paths).unwrap(); + + // Verify window bounds were persisted + assert_eq!(retrieved.id, id); + assert!(retrieved.window_bounds.is_some()); + assert_eq!(retrieved.window_bounds.unwrap().0, window_bounds.0); + assert!(retrieved.display.is_some()); + assert_eq!(retrieved.display.unwrap(), display_uuid); + } } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 031e22e93942b3ab8308b4fd0c045ea63441ea17..47fa44a6a8ff6e0d925d994101857342ec4589c2 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1749,26 +1749,18 @@ impl Workspace { window } else { let window_bounds_override = window_bounds_env_override(); - let is_empty_workspace = project_paths.is_empty(); let (window_bounds, display) = if let Some(bounds) = window_bounds_override { (Some(WindowBounds::Windowed(bounds)), None) - } else if let Some(workspace) = serialized_workspace.as_ref() { + } else if let Some(workspace) = serialized_workspace.as_ref() + && let Some(display) = workspace.display + && let Some(bounds) = workspace.window_bounds.as_ref() + { // Reopening an existing workspace - restore its saved bounds - if let (Some(display), Some(bounds)) = - (workspace.display, workspace.window_bounds.as_ref()) - { - (Some(bounds.0), Some(display)) - } else { - (None, None) - } - } else if is_empty_workspace { - // Empty workspace - try to restore the last known no-project window bounds - if let Some((display, bounds)) = persistence::read_default_window_bounds() { - (Some(bounds), Some(display)) - } else { - (None, None) - } + (Some(bounds.0), Some(display)) + } else if let Some((display, bounds)) = persistence::read_default_window_bounds() { + // New or empty workspace - use the last known window bounds + (Some(bounds), Some(display)) } else { // New window - let GPUI's default_bounds() handle cascading (None, None) @@ -5674,12 +5666,24 @@ impl Workspace { persistence::DB.save_workspace(serialized_workspace).await; }) } - WorkspaceLocation::DetachFromSession => window.spawn(cx, async move |_| { - persistence::DB - .set_session_id(database_id, None) - .await - .log_err(); - }), + WorkspaceLocation::DetachFromSession => { + let window_bounds = SerializedWindowBounds(window.window_bounds()); + let display = window.display(cx).and_then(|d| d.uuid().ok()); + window.spawn(cx, async move |_| { + persistence::DB + .set_window_open_status( + database_id, + window_bounds, + display.unwrap_or_default(), + ) + .await + .log_err(); + persistence::DB + .set_session_id(database_id, None) + .await + .log_err(); + }) + } WorkspaceLocation::None => Task::ready(()), } } diff --git a/crates/zed/resources/Document.icns b/crates/zed/resources/Document.icns new file mode 100644 index 0000000000000000000000000000000000000000..5d0185c81a32c214f213f12243aeab01e32830e1 Binary files /dev/null and b/crates/zed/resources/Document.icns differ diff --git a/crates/zed/src/zed/edit_prediction_registry.rs b/crates/zed/src/zed/edit_prediction_registry.rs index 77a1f71596f9cf1d2f4e32137580d0e3648359f5..51327bfc9ab715a1b11aa3c639ffd60b6b0a0ea8 100644 --- a/crates/zed/src/zed/edit_prediction_registry.rs +++ b/crates/zed/src/zed/edit_prediction_registry.rs @@ -145,23 +145,6 @@ fn register_backward_compatible_actions(editor: &mut Editor, cx: &mut Context| { - editor.next_edit_prediction(&Default::default(), window, cx); - }, - )) - .detach(); - editor - .register_action(cx.listener( - |editor, - _: &copilot::PreviousSuggestion, - window: &mut Window, - cx: &mut Context| { - editor.previous_edit_prediction(&Default::default(), window, cx); - }, - )) - .detach(); } fn assign_edit_prediction_provider( diff --git a/docs/src/completions.md b/docs/src/completions.md index ff96ede7503cd461bbd3d7b4afdedcaa2f36a2e5..7b35ec2d09d91a7ba7dc5ae4b968157e0184227f 100644 --- a/docs/src/completions.md +++ b/docs/src/completions.md @@ -15,6 +15,10 @@ When there is an appropriate language server available, Zed will provide complet You can manually trigger completions with `ctrl-space` or by triggering the `editor::ShowCompletions` action from the command palette. +> Note: Using `ctrl-space` in Zed requires disabling the macOS global shortcut. +> Open **System Settings** > **Keyboard** > **Keyboard Shortcut**s > +> **Input Sources** and uncheck **Select the previous input source**. + For more information, see: - [Configuring Supported Languages](./configuring-languages.md) diff --git a/script/bundle-mac b/script/bundle-mac index c6c925f073600336f4aa3114a732609481ade26e..93ea07b162612d27784dbd0eb54598b0aa2252c3 100755 --- a/script/bundle-mac +++ b/script/bundle-mac @@ -106,6 +106,17 @@ mv Cargo.toml.backup Cargo.toml popd echo "Bundled ${app_path}" +# DocumentTypes.plist references CFBundleTypeIconFile "Document", so the bundle must contain Document.icns. +# We use the app icon as a placeholder document icon for now. +document_icon_source="crates/zed/resources/Document.icns" +document_icon_target="${app_path}/Contents/Resources/Document.icns" +if [[ -f "${document_icon_source}" ]]; then + mkdir -p "$(dirname "${document_icon_target}")" + cp "${document_icon_source}" "${document_icon_target}" +else + echo "cargo::warning=Missing ${document_icon_source}; macOS document icons may not appear in Finder." +fi + if [[ -n "${MACOS_CERTIFICATE:-}" && -n "${MACOS_CERTIFICATE_PASSWORD:-}" && -n "${APPLE_NOTARIZATION_KEY:-}" && -n "${APPLE_NOTARIZATION_KEY_ID:-}" && -n "${APPLE_NOTARIZATION_ISSUER_ID:-}" ]]; then can_code_sign=true diff --git a/script/verify-macos-document-icon b/script/verify-macos-document-icon new file mode 100755 index 0000000000000000000000000000000000000000..de2581c9df764ee2019740048381d6d66dc3499d --- /dev/null +++ b/script/verify-macos-document-icon @@ -0,0 +1,81 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + cat <<'USAGE' +Usage: + script/verify-macos-document-icon /path/to/Zed.app + +Verifies that the given macOS app bundle's Info.plist references a document icon +named "Document" and that the corresponding icon file exists in the bundle. + +Specifically checks: + - CFBundleDocumentTypes[*].CFBundleTypeIconFile includes "Document" + - Contents/Resources/Document.icns exists + +Exit codes: + 0 - success + 1 - verification failed + 2 - invalid usage / missing prerequisites +USAGE +} + +fail() { + echo "error: $*" >&2 + exit 1 +} + +if [[ $# -ne 1 ]]; then + usage >&2 + exit 2 +fi + +app_path="$1" + +if [[ ! -d "${app_path}" ]]; then + fail "app bundle not found: ${app_path}" +fi + +info_plist="${app_path}/Contents/Info.plist" +if [[ ! -f "${info_plist}" ]]; then + fail "missing Info.plist: ${info_plist}" +fi + +if ! command -v plutil >/dev/null 2>&1; then + fail "plutil not found (required on macOS to read Info.plist)" +fi + +# Convert to JSON for robust parsing. plutil outputs JSON to stdout in this mode. +info_json="$(plutil -convert json -o - "${info_plist}")" + +# Check that CFBundleDocumentTypes exists and that at least one entry references "Document". +# We use Python for JSON parsing; macOS ships with Python 3 on many setups, but not all. +# If python3 isn't available, fall back to a simpler grep-based check. +has_document_icon_ref="false" +if command -v python3 >/dev/null 2>&1; then + has_document_icon_ref="$(python3 -c "import json,sys; d=json.load(sys.stdin); types=d.get('CFBundleDocumentTypes', []); vals=[t.get('CFBundleTypeIconFile') for t in types if isinstance(t, dict)]; print('true' if 'Document' in vals else 'false')" <<<"${info_json}")" +else + # This is a best-effort fallback. It may produce false negatives if the JSON formatting differs. + if echo "${info_json}" | grep -q '"CFBundleTypeIconFile"[[:space:]]*:[[:space:]]*"Document"'; then + has_document_icon_ref="true" + fi +fi + +if [[ "${has_document_icon_ref}" != "true" ]]; then + echo "Verification failed for: ${app_path}" >&2 + echo "Expected Info.plist to reference CFBundleTypeIconFile \"Document\" in CFBundleDocumentTypes." >&2 + echo "Tip: This bundle may be missing DocumentTypes.plist extensions or may have different icon naming." >&2 + exit 1 +fi + +document_icon_path="${app_path}/Contents/Resources/Document.icns" +if [[ ! -f "${document_icon_path}" ]]; then + echo "Verification failed for: ${app_path}" >&2 + echo "Expected document icon to exist: ${document_icon_path}" >&2 + echo "Tip: The bundle script should copy crates/zed/resources/Document.icns into Contents/Resources/Document.icns." >&2 + exit 1 +fi + +echo "OK: ${app_path}" +echo " - Info.plist references CFBundleTypeIconFile \"Document\"" +echo " - Found ${document_icon_path}" diff --git a/tooling/xtask/src/tasks/workflows/autofix_pr.rs b/tooling/xtask/src/tasks/workflows/autofix_pr.rs index f4a8e0e2b09df93cc430f0931c3db3f9e67b07df..ab59e735225dfb4f9658960a35a992553642b4c2 100644 --- a/tooling/xtask/src/tasks/workflows/autofix_pr.rs +++ b/tooling/xtask/src/tasks/workflows/autofix_pr.rs @@ -109,19 +109,6 @@ fn run_autofix(pr_number: &WorkflowInput, run_clippy: &WorkflowInput) -> NamedJo } fn commit_changes(pr_number: &WorkflowInput, autofix_job: &NamedJob) -> NamedJob { - fn authenticate_as_zippy() -> (Step, StepOutput) { - let step = named::uses( - "actions", - "create-github-app-token", - "bef1eaf1c0ac2b148ee2a0a74c65fbe6db0631f1", - ) - .add_with(("app-id", vars::ZED_ZIPPY_APP_ID)) - .add_with(("private-key", vars::ZED_ZIPPY_APP_PRIVATE_KEY)) - .id("get-app-token"); - let output = StepOutput::new(&step, "token"); - (step, output) - } - fn checkout_pr(pr_number: &WorkflowInput, token: &StepOutput) -> Step { named::bash(&format!("gh pr checkout {pr_number}")).add_env(("GITHUB_TOKEN", token)) } @@ -148,7 +135,7 @@ fn commit_changes(pr_number: &WorkflowInput, autofix_job: &NamedJob) -> NamedJob .add_env(("GITHUB_TOKEN", token)) } - let (authenticate, token) = authenticate_as_zippy(); + let (authenticate, token) = steps::authenticate_as_zippy(); named::job( Job::default() diff --git a/tooling/xtask/src/tasks/workflows/cherry_pick.rs b/tooling/xtask/src/tasks/workflows/cherry_pick.rs index 105bf74c4194a46ad4ca62991fae3a945eea150d..eaa786837f84ebf4d4f7e1a579db0c7b4dcc5040 100644 --- a/tooling/xtask/src/tasks/workflows/cherry_pick.rs +++ b/tooling/xtask/src/tasks/workflows/cherry_pick.rs @@ -3,7 +3,7 @@ use gh_workflow::*; use crate::tasks::workflows::{ runners, steps::{self, NamedJob, named}, - vars::{self, StepOutput, WorkflowInput}, + vars::{StepOutput, WorkflowInput}, }; pub fn cherry_pick() -> Workflow { @@ -29,19 +29,6 @@ fn run_cherry_pick( commit: &WorkflowInput, channel: &WorkflowInput, ) -> NamedJob { - fn authenticate_as_zippy() -> (Step, StepOutput) { - let step = named::uses( - "actions", - "create-github-app-token", - "bef1eaf1c0ac2b148ee2a0a74c65fbe6db0631f1", - ) // v2 - .add_with(("app-id", vars::ZED_ZIPPY_APP_ID)) - .add_with(("private-key", vars::ZED_ZIPPY_APP_PRIVATE_KEY)) - .id("get-app-token"); - let output = StepOutput::new(&step, "token"); - (step, output) - } - fn cherry_pick( branch: &WorkflowInput, commit: &WorkflowInput, @@ -54,7 +41,7 @@ fn run_cherry_pick( .add_env(("GITHUB_TOKEN", token)) } - let (authenticate, token) = authenticate_as_zippy(); + let (authenticate, token) = steps::authenticate_as_zippy(); named::job( Job::default() diff --git a/tooling/xtask/src/tasks/workflows/release.rs b/tooling/xtask/src/tasks/workflows/release.rs index e06a71340192c036d442d65d9572e52ed2983cae..80fb075f7f6445b1a6a078d9defba2018a406851 100644 --- a/tooling/xtask/src/tasks/workflows/release.rs +++ b/tooling/xtask/src/tasks/workflows/release.rs @@ -97,17 +97,20 @@ pub(crate) fn create_sentry_release() -> Step { } fn auto_release_preview(deps: &[&NamedJob; 1]) -> NamedJob { + let (authenticate, token) = steps::authenticate_as_zippy(); + named::job( dependant_job(deps) .runs_on(runners::LINUX_SMALL) .cond(Expression::new(indoc::indoc!( r#"startsWith(github.ref, 'refs/tags/v') && endsWith(github.ref, '-pre') && !endsWith(github.ref, '.0-pre')"# ))) + .add_step(authenticate) .add_step( steps::script( r#"gh release edit "$GITHUB_REF_NAME" --repo=zed-industries/zed --draft=false"#, ) - .add_env(("GITHUB_TOKEN", vars::GITHUB_TOKEN)), + .add_env(("GITHUB_TOKEN", &token)), ) ) } diff --git a/tooling/xtask/src/tasks/workflows/run_tests.rs b/tooling/xtask/src/tasks/workflows/run_tests.rs index 13639fd6c4bf33fe090dcb9d5f3cafdf45a36e76..d0caab82b057f21735b7f828c8917a358dd548b2 100644 --- a/tooling/xtask/src/tasks/workflows/run_tests.rs +++ b/tooling/xtask/src/tasks/workflows/run_tests.rs @@ -45,11 +45,15 @@ pub(crate) fn run_tests() -> Workflow { &should_run_tests, ]); + let check_style = check_style(); + let run_tests_linux = run_platform_tests(Platform::Linux); + let call_autofix = call_autofix(&check_style, &run_tests_linux); + let mut jobs = vec![ orchestrate, - check_style(), + check_style, should_run_tests.guard(run_platform_tests(Platform::Windows)), - should_run_tests.guard(run_platform_tests(Platform::Linux)), + should_run_tests.guard(run_tests_linux), should_run_tests.guard(run_platform_tests(Platform::Mac)), should_run_tests.guard(doctests()), should_run_tests.guard(check_workspace_binaries()), @@ -106,6 +110,7 @@ pub(crate) fn run_tests() -> Workflow { workflow }) .add_job(tests_pass.name, tests_pass.job) + .add_job(call_autofix.name, call_autofix.job) } // Generates a bash script that checks changed files against regex patterns @@ -221,6 +226,8 @@ pub fn tests_pass(jobs: &[NamedJob]) -> NamedJob { named::job(job) } +pub const STYLE_FAILED_OUTPUT: &str = "style_failed"; + fn check_style() -> NamedJob { fn check_for_typos() -> Step { named::uses( @@ -236,15 +243,58 @@ fn check_style() -> NamedJob { .add_step(steps::checkout_repo()) .add_step(steps::cache_rust_dependencies_namespace()) .add_step(steps::setup_pnpm()) - .add_step(steps::script("./script/prettier")) + .add_step(steps::prettier()) .add_step(steps::cargo_fmt()) - .add_step(steps::trigger_autofix(false)) + .add_step(steps::record_style_failure()) .add_step(steps::script("./script/check-todos")) .add_step(steps::script("./script/check-keymaps")) - .add_step(check_for_typos()), + .add_step(check_for_typos()) + .outputs([( + STYLE_FAILED_OUTPUT.to_owned(), + format!( + "${{{{ steps.{}.outputs.failed == 'true' }}}}", + steps::RECORD_STYLE_FAILURE_STEP_ID + ), + )]), ) } +fn call_autofix(check_style: &NamedJob, run_tests_linux: &NamedJob) -> NamedJob { + fn dispatch_autofix(run_tests_linux_name: &str) -> Step { + let clippy_failed_expr = format!( + "needs.{}.outputs.{} == 'true'", + run_tests_linux_name, CLIPPY_FAILED_OUTPUT + ); + named::bash(format!( + "gh workflow run autofix_pr.yml -f pr_number=${{{{ github.event.pull_request.number }}}} -f run_clippy=${{{{ {} }}}}", + clippy_failed_expr + )) + .add_env(("GITHUB_TOKEN", "${{ steps.get-app-token.outputs.token }}")) + } + + let style_failed_expr = format!( + "needs.{}.outputs.{} == 'true'", + check_style.name, STYLE_FAILED_OUTPUT + ); + let clippy_failed_expr = format!( + "needs.{}.outputs.{} == 'true'", + run_tests_linux.name, CLIPPY_FAILED_OUTPUT + ); + let (authenticate, _token) = steps::authenticate_as_zippy(); + + let job = Job::default() + .runs_on(runners::LINUX_SMALL) + .cond(Expression::new(format!( + "always() && ({} || {}) && github.event_name == 'pull_request' && github.actor != 'zed-zippy[bot]'", + style_failed_expr, clippy_failed_expr + ))) + .needs(vec![check_style.name.clone(), run_tests_linux.name.clone()]) + .add_step(authenticate) + .add_step(dispatch_autofix(&run_tests_linux.name)); + + named::job(job) +} + fn check_dependencies() -> NamedJob { fn install_cargo_machete() -> Step { named::uses( @@ -305,6 +355,8 @@ fn check_workspace_binaries() -> NamedJob { ) } +pub const CLIPPY_FAILED_OUTPUT: &str = "clippy_failed"; + pub(crate) fn run_platform_tests(platform: Platform) -> NamedJob { let runner = match platform { Platform::Windows => runners::WINDOWS_DEFAULT, @@ -327,12 +379,23 @@ pub(crate) fn run_platform_tests(platform: Platform) -> NamedJob { .add_step(steps::setup_node()) .add_step(steps::clippy(platform)) .when(platform == Platform::Linux, |job| { - job.add_step(steps::trigger_autofix(true)) - .add_step(steps::cargo_install_nextest()) + job.add_step(steps::record_clippy_failure()) + }) + .when(platform == Platform::Linux, |job| { + job.add_step(steps::cargo_install_nextest()) }) .add_step(steps::clear_target_dir_if_large(platform)) .add_step(steps::cargo_nextest(platform)) - .add_step(steps::cleanup_cargo_config(platform)), + .add_step(steps::cleanup_cargo_config(platform)) + .when(platform == Platform::Linux, |job| { + job.outputs([( + CLIPPY_FAILED_OUTPUT.to_owned(), + format!( + "${{{{ steps.{}.outputs.failed == 'true' }}}}", + steps::RECORD_CLIPPY_FAILURE_STEP_ID + ), + )]) + }), } } diff --git a/tooling/xtask/src/tasks/workflows/steps.rs b/tooling/xtask/src/tasks/workflows/steps.rs index 54873c011ce9d1fb7d4e7e0b734695c7c1a30fad..eaa51dc35205f51e7fe3a56668ed0679e92999f0 100644 --- a/tooling/xtask/src/tasks/workflows/steps.rs +++ b/tooling/xtask/src/tasks/workflows/steps.rs @@ -54,8 +54,25 @@ pub fn setup_sentry() -> Step { .add_with(("token", vars::SENTRY_AUTH_TOKEN)) } +pub const PRETTIER_STEP_ID: &str = "prettier"; +pub const CARGO_FMT_STEP_ID: &str = "cargo_fmt"; +pub const RECORD_STYLE_FAILURE_STEP_ID: &str = "record_style_failure"; + +pub fn prettier() -> Step { + named::bash("./script/prettier").id(PRETTIER_STEP_ID) +} + pub fn cargo_fmt() -> Step { - named::bash("cargo fmt --all -- --check") + named::bash("cargo fmt --all -- --check").id(CARGO_FMT_STEP_ID) +} + +pub fn record_style_failure() -> Step { + named::bash(format!( + "echo \"failed=${{{{ steps.{}.outcome == 'failure' || steps.{}.outcome == 'failure' }}}}\" >> \"$GITHUB_OUTPUT\"", + PRETTIER_STEP_ID, CARGO_FMT_STEP_ID + )) + .id(RECORD_STYLE_FAILURE_STEP_ID) + .if_condition(Expression::new("always()")) } pub fn cargo_install_nextest() -> Step { @@ -101,13 +118,25 @@ pub fn clear_target_dir_if_large(platform: Platform) -> Step { } } +pub const CLIPPY_STEP_ID: &str = "clippy"; +pub const RECORD_CLIPPY_FAILURE_STEP_ID: &str = "record_clippy_failure"; + pub fn clippy(platform: Platform) -> Step { match platform { - Platform::Windows => named::pwsh("./script/clippy.ps1"), - _ => named::bash("./script/clippy"), + Platform::Windows => named::pwsh("./script/clippy.ps1").id(CLIPPY_STEP_ID), + _ => named::bash("./script/clippy").id(CLIPPY_STEP_ID), } } +pub fn record_clippy_failure() -> Step { + named::bash(format!( + "echo \"failed=${{{{ steps.{}.outcome == 'failure' }}}}\" >> \"$GITHUB_OUTPUT\"", + CLIPPY_STEP_ID + )) + .id(RECORD_CLIPPY_FAILURE_STEP_ID) + .if_condition(Expression::new("always()")) +} + pub fn cache_rust_dependencies_namespace() -> Step { named::uses("namespacelabs", "nscloud-cache-action", "v1").add_with(("cache", "rust")) } @@ -345,12 +374,15 @@ pub fn git_checkout(ref_name: &dyn std::fmt::Display) -> Step { )) } -pub fn trigger_autofix(run_clippy: bool) -> Step { - named::bash(format!( - "gh workflow run autofix_pr.yml -f pr_number=${{{{ github.event.pull_request.number }}}} -f run_clippy={run_clippy}" - )) - .if_condition(Expression::new( - "failure() && github.event_name == 'pull_request' && github.actor != 'zed-zippy[bot]'", - )) - .add_env(("GITHUB_TOKEN", vars::GITHUB_TOKEN)) +pub fn authenticate_as_zippy() -> (Step, StepOutput) { + let step = named::uses( + "actions", + "create-github-app-token", + "bef1eaf1c0ac2b148ee2a0a74c65fbe6db0631f1", + ) + .add_with(("app-id", vars::ZED_ZIPPY_APP_ID)) + .add_with(("private-key", vars::ZED_ZIPPY_APP_PRIVATE_KEY)) + .id("get-app-token"); + let output = StepOutput::new(&step, "token"); + (step, output) }