Compare commits

..

9 Commits

Author SHA1 Message Date
Anthony
2d77a2377d add element id in item render_page items instead 2025-10-09 12:43:24 -04:00
Anthony
9e1f9c74c0 upper case all lsp references in titles and descriptions 2025-10-09 12:33:41 -04:00
Anthony
e0be758308 Fix some settings ui elements having duplicate ids 2025-10-09 12:29:21 -04:00
Anthony
bb31a97e22 Deduplicate terminal settings and fix dropdown toggle bug 2025-10-09 12:02:56 -04:00
Anthony
08b73d0943 Merge remote-tracking branch 'origin/main' into settings-ui-elements 2025-10-09 11:16:43 -04:00
Anthony
f2b1dd262c Merge conflicts 2025-10-09 11:12:09 -04:00
Anthony
898af2e75d Merge remote-tracking branch 'origin/main' into settings-ui-elements 2025-10-07 22:08:14 -04:00
Anthony
e34526534f Add Cursor Shape 2025-10-07 22:07:53 -04:00
Anthony
e3f4fee4d5 Add more terminal settings 2025-10-07 17:37:37 -04:00
318 changed files with 8445 additions and 13302 deletions

View File

@@ -1,35 +0,0 @@
name: Bug Report (Windows)
description: Zed Windows Related Bugs
type: "Bug"
labels: ["windows"]
title: "Windows: <a short description of the Windows bug>"
body:
- type: textarea
attributes:
label: Summary
description: Describe the bug with a one-line summary, and provide detailed reproduction steps
value: |
<!-- Please insert a one-line summary of the issue below -->
SUMMARY_SENTENCE_HERE
### Description
<!-- Describe with sufficient detail to reproduce from a clean Zed install. -->
Steps to trigger the problem:
1.
2.
3.
**Expected Behavior**:
**Actual Behavior**:
validations:
required: true
- type: textarea
id: environment
attributes:
label: Zed Version and System Specs
description: 'Open Zed, and in the command palette select "zed: copy system specs into clipboard"'
placeholder: |
Output of "zed: copy system specs into clipboard"
validations:
required: true

View File

@@ -866,7 +866,7 @@ jobs:
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
if: contains(github.event.pull_request.labels.*.name, 'run-bundling')
with:
name: Zed_${{ github.event.pull_request.head.sha || github.sha }}-x86_64.exe
name: ZedEditorUserSetup-x64-${{ github.event.pull_request.head.sha || github.sha }}.exe
path: ${{ env.SETUP_PATH }}
- name: Upload Artifacts to release
@@ -882,8 +882,7 @@ jobs:
auto-release-preview:
name: Auto release preview
if: |
false
&& startsWith(github.ref, 'refs/tags/v')
startsWith(github.ref, 'refs/tags/v')
&& endsWith(github.ref, '-pre') && !endsWith(github.ref, '.0-pre')
needs: [bundle-mac, bundle-linux-x86_x64, bundle-linux-aarch64, bundle-windows-x64]
runs-on:

View File

@@ -38,26 +38,6 @@ jobs:
webhook-url: ${{ secrets.DISCORD_WEBHOOK_RELEASE_NOTES }}
content: ${{ steps.get-content.outputs.string }}
publish-winget:
runs-on:
- ubuntu-latest
steps:
- name: Set Package Name
id: set-package-name
run: |
if [ "${{ github.event.release.prerelease }}" == "true" ]; then
PACKAGE_NAME=ZedIndustries.Zed.Preview
else
PACKAGE_NAME=ZedIndustries.Zed
fi
echo "PACKAGE_NAME=$PACKAGE_NAME" >> "$GITHUB_OUTPUT"
- uses: vedantmgoyal9/winget-releaser@19e706d4c9121098010096f9c495a70a7518b30f # v2
with:
identifier: ${{ steps.set-package-name.outputs.PACKAGE_NAME }}
max-versions-to-keep: 5
token: ${{ secrets.WINGET_TOKEN }}
send_release_notes_email:
if: false && github.repository_owner == 'zed-industries' && !github.event.release.prerelease
runs-on: ubuntu-latest

2144
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -274,7 +274,7 @@ cloud_llm_client = { path = "crates/cloud_llm_client" }
cloud_zeta2_prompt = { path = "crates/cloud_zeta2_prompt" }
collab = { path = "crates/collab" }
collab_ui = { path = "crates/collab_ui" }
collections = { path = "crates/collections", version = "0.1.0" }
collections = { path = "crates/collections", package = "zed-collections", version = "0.1.0" }
command_palette = { path = "crates/command_palette" }
command_palette_hooks = { path = "crates/command_palette_hooks" }
component = { path = "crates/component" }
@@ -290,7 +290,7 @@ debug_adapter_extension = { path = "crates/debug_adapter_extension" }
debugger_tools = { path = "crates/debugger_tools" }
debugger_ui = { path = "crates/debugger_ui" }
deepseek = { path = "crates/deepseek" }
derive_refineable = { path = "crates/refineable/derive_refineable" }
derive_refineable = { path = "crates/refineable/derive_refineable", package = "zed-derive-refineable", version = "0.1.0" }
diagnostics = { path = "crates/diagnostics" }
editor = { path = "crates/editor" }
extension = { path = "crates/extension" }
@@ -309,10 +309,10 @@ git_ui = { path = "crates/git_ui" }
go_to_line = { path = "crates/go_to_line" }
google_ai = { path = "crates/google_ai" }
gpui = { path = "crates/gpui", default-features = false }
gpui_macros = { path = "crates/gpui_macros" }
gpui_macros = { path = "crates/gpui_macros", package = "gpui-macros", version = "0.1.0" }
gpui_tokio = { path = "crates/gpui_tokio" }
html_to_markdown = { path = "crates/html_to_markdown" }
http_client = { path = "crates/http_client" }
http_client = { path = "crates/http_client", package = "zed-http-client", version = "0.1.0" }
http_client_tls = { path = "crates/http_client_tls" }
icons = { path = "crates/icons" }
image_viewer = { path = "crates/image_viewer" }
@@ -341,7 +341,7 @@ lsp = { path = "crates/lsp" }
markdown = { path = "crates/markdown" }
markdown_preview = { path = "crates/markdown_preview" }
svg_preview = { path = "crates/svg_preview" }
media = { path = "crates/media" }
media = { path = "crates/media", package = "zed-media", version = "0.1.0" }
menu = { path = "crates/menu" }
migrator = { path = "crates/migrator" }
mistral = { path = "crates/mistral" }
@@ -358,7 +358,7 @@ outline = { path = "crates/outline" }
outline_panel = { path = "crates/outline_panel" }
panel = { path = "crates/panel" }
paths = { path = "crates/paths" }
perf = { path = "tooling/perf" }
perf = { path = "tooling/perf", package = "zed-perf", version = "0.1.0" }
picker = { path = "crates/picker" }
plugin = { path = "crates/plugin" }
plugin_macros = { path = "crates/plugin_macros" }
@@ -370,7 +370,7 @@ project_symbols = { path = "crates/project_symbols" }
prompt_store = { path = "crates/prompt_store" }
proto = { path = "crates/proto" }
recent_projects = { path = "crates/recent_projects" }
refineable = { path = "crates/refineable" }
refineable = { path = "crates/refineable", package = "zed-refineable", version = "0.1.0" }
release_channel = { path = "crates/release_channel" }
scheduler = { path = "crates/scheduler" }
remote = { path = "crates/remote" }
@@ -383,7 +383,7 @@ rope = { path = "crates/rope" }
rpc = { path = "crates/rpc" }
rules_library = { path = "crates/rules_library" }
search = { path = "crates/search" }
semantic_version = { path = "crates/semantic_version" }
semantic_version = { path = "crates/semantic_version", package = "zed-semantic-version", version = "0.1.0" }
session = { path = "crates/session" }
settings = { path = "crates/settings" }
settings_macros = { path = "crates/settings_macros" }
@@ -396,7 +396,7 @@ sqlez_macros = { path = "crates/sqlez_macros" }
story = { path = "crates/story" }
storybook = { path = "crates/storybook" }
streaming_diff = { path = "crates/streaming_diff" }
sum_tree = { path = "crates/sum_tree" }
sum_tree = { path = "crates/sum_tree", package = "zed-sum-tree", version = "0.1.0" }
supermaven = { path = "crates/supermaven" }
supermaven_api = { path = "crates/supermaven_api" }
codestral = { path = "crates/codestral" }
@@ -420,8 +420,8 @@ ui = { path = "crates/ui" }
ui_input = { path = "crates/ui_input" }
ui_macros = { path = "crates/ui_macros" }
ui_prompt = { path = "crates/ui_prompt" }
util = { path = "crates/util" }
util_macros = { path = "crates/util_macros" }
util = { path = "crates/util", package = "zed-util", version = "0.1.0" }
util_macros = { path = "crates/util_macros", package = "zed-util-macros", version = "0.1.0" }
vercel = { path = "crates/vercel" }
vim = { path = "crates/vim" }
vim_mode_setting = { path = "crates/vim_mode_setting" }
@@ -457,7 +457,7 @@ async-dispatcher = "0.1"
async-fs = "2.1"
async-pipe = { git = "https://github.com/zed-industries/async-pipe-rs", rev = "82d00a04211cf4e1236029aa03e6b6ce2a74c553" }
async-recursion = "1.0.0"
async-tar = { git = "https://github.com/zed-industries/async-tar", rev = "8af312477196311c9ea4097f2a22022f6d609bf6" }
async-tar = "0.5.0"
async-task = "4.7"
async-trait = "0.1"
async-tungstenite = "0.29.1"
@@ -654,7 +654,7 @@ strum = { version = "0.27.0", features = ["derive"] }
subtle = "2.5.0"
syn = { version = "2.0.101", features = ["full", "extra-traits", "visit-mut"] }
sys-locale = "0.3.1"
sysinfo = "0.37.0"
sysinfo = "0.31.0"
take-until = "0.2.0"
tempfile = "3.20.0"
thiserror = "2.0.12"
@@ -805,7 +805,7 @@ wasmtime = { opt-level = 3 }
activity_indicator = { codegen-units = 1 }
assets = { codegen-units = 1 }
breadcrumbs = { codegen-units = 1 }
collections = { codegen-units = 1 }
zed-collections = { codegen-units = 1 }
command_palette = { codegen-units = 1 }
command_palette_hooks = { codegen-units = 1 }
extension_cli = { codegen-units = 1 }
@@ -825,11 +825,11 @@ outline = { codegen-units = 1 }
paths = { codegen-units = 1 }
prettier = { codegen-units = 1 }
project_symbols = { codegen-units = 1 }
refineable = { codegen-units = 1 }
zed-refineable = { codegen-units = 1 }
release_channel = { codegen-units = 1 }
reqwest_client = { codegen-units = 1 }
rich_text = { codegen-units = 1 }
semantic_version = { codegen-units = 1 }
zed-semantic-version = { codegen-units = 1 }
session = { codegen-units = 1 }
snippet = { codegen-units = 1 }
snippets_ui = { codegen-units = 1 }

2
Cross.toml Normal file
View File

@@ -0,0 +1,2 @@
[build]
dockerfile = "Dockerfile-cross"

17
Dockerfile-cross Normal file
View File

@@ -0,0 +1,17 @@
# syntax=docker/dockerfile:1
ARG CROSS_BASE_IMAGE
FROM ${CROSS_BASE_IMAGE}
WORKDIR /app
ARG TZ=Etc/UTC \
LANG=C.UTF-8 \
LC_ALL=C.UTF-8 \
DEBIAN_FRONTEND=noninteractive
ENV CARGO_TERM_COLOR=always
COPY script/install-mold script/
RUN ./script/install-mold "2.34.0"
COPY script/remote-server script/
RUN ./script/remote-server
COPY . .

View File

@@ -1,3 +1,9 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.2806 4.66818L8.26042 1.76982C8.09921 1.67673 7.9003 1.67673 7.73909 1.76982L2.71918 4.66818C2.58367 4.74642 2.5 4.89112 2.5 5.04785V10.8924C2.5 11.0489 2.58367 11.1938 2.71918 11.2721L7.73934 14.1704C7.90054 14.2635 8.09946 14.2635 8.26066 14.1704L13.2808 11.2721C13.4163 11.1938 13.5 11.0491 13.5 10.8924V5.04785C13.5 4.89136 13.4163 4.74642 13.2808 4.66818H13.2806ZM12.9653 5.28212L8.11901 13.676C8.08626 13.7326 7.99977 13.7095 7.99977 13.6439V8.14771C7.99977 8.03788 7.94107 7.9363 7.84586 7.88115L3.08613 5.13317C3.02957 5.10041 3.05266 5.0139 3.11818 5.0139H12.8106C12.9483 5.0139 13.0343 5.1631 12.9655 5.28236H12.9653V5.28212Z" fill="#C4CAD4"/>
<path opacity="0.6" d="M3.5 11V5.5L8.5 8L3.5 11Z" fill="black"/>
<path opacity="0.4" d="M8.5 14L3.5 11L8.5 8V14Z" fill="black"/>
<path opacity="0.6" d="M8.5 5.5H3.5L8.5 2.5L8.5 5.5Z" fill="black"/>
<path opacity="0.8" d="M8.5 5.5V2.5L13.5 5.5H8.5Z" fill="black"/>
<path opacity="0.2" d="M13.5 11L8.5 14L11 9.5L13.5 11Z" fill="black"/>
<path opacity="0.5" d="M13.5 11L11 9.5L13.5 5V11Z" fill="black"/>
<path d="M3.5 11V5L8.5 2.11325L13.5 5V11L8.5 13.8868L3.5 11Z" stroke="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 769 B

After

Width:  |  Height:  |  Size: 583 B

View File

@@ -1,4 +1 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.125 9.25001L3 6.125L6.125 3" stroke="#C4CAD4" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3 6.125H9.56251C10.0139 6.125 10.4609 6.21391 10.878 6.38666C11.295 6.55942 11.674 6.81262 11.9932 7.13182C12.3124 7.45102 12.5656 7.82997 12.7383 8.24703C12.9111 8.66408 13 9.11108 13 9.5625C13 10.0139 12.9111 10.4609 12.7383 10.878C12.5656 11.295 12.3124 11.674 11.9932 11.9932C11.674 12.3124 11.295 12.5656 10.878 12.7383C10.4609 12.9111 10.0139 13 9.56251 13H7.375" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M2.6 5v3.6h3.6"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M13.4 11A5.4 5.4 0 0 0 8 5.6a5.4 5.4 0 0 0-3.6 1.38L2.6 8.6"/></svg>

Before

Width:  |  Height:  |  Size: 692 B

After

Width:  |  Height:  |  Size: 339 B

View File

@@ -30,8 +30,8 @@
"ctrl-+": ["zed::IncreaseBufferFontSize", { "persist": false }],
"ctrl--": ["zed::DecreaseBufferFontSize", { "persist": false }],
"ctrl-0": ["zed::ResetBufferFontSize", { "persist": false }],
"ctrl-,": "zed::OpenSettings",
"ctrl-alt-,": "zed::OpenSettingsFile",
"ctrl-,": "zed::OpenSettingsEditor",
"ctrl-alt-,": "zed::OpenSettings",
"ctrl-q": "zed::Quit",
"f4": "debugger::Start",
"shift-f5": "debugger::Stop",
@@ -621,7 +621,7 @@
"ctrl-shift-f": "pane::DeploySearch",
"ctrl-shift-h": ["pane::DeploySearch", { "replace_enabled": true }],
"ctrl-shift-t": "pane::ReopenClosedItem",
"ctrl-k ctrl-s": "zed::OpenKeymap",
"ctrl-k ctrl-s": "zed::OpenKeymapEditor",
"ctrl-k ctrl-t": "theme_selector::Toggle",
"ctrl-alt-super-p": "settings_profile_selector::Toggle",
"ctrl-t": "project_symbols::Toggle",
@@ -1249,7 +1249,6 @@
"escape": "workspace::CloseWindow",
"ctrl-m": "settings_editor::Minimize",
"ctrl-f": "search::FocusSearch",
"left": "settings_editor::ToggleFocusNav",
"ctrl-shift-e": "settings_editor::ToggleFocusNav",
// todo(settings_ui): cut this down based on the max files and overflow UI
"ctrl-1": ["settings_editor::FocusFile", 0],
@@ -1270,8 +1269,6 @@
"context": "SettingsWindow > NavigationMenu",
"use_key_equivalents": true,
"bindings": {
"up": "settings_editor::FocusPreviousNavEntry",
"down": "settings_editor::FocusNextNavEntry",
"right": "settings_editor::ExpandNavEntry",
"left": "settings_editor::CollapseNavEntry",
"pageup": "settings_editor::FocusPreviousRootNavEntry",

View File

@@ -39,8 +39,8 @@
"cmd-+": ["zed::IncreaseBufferFontSize", { "persist": false }],
"cmd--": ["zed::DecreaseBufferFontSize", { "persist": false }],
"cmd-0": ["zed::ResetBufferFontSize", { "persist": false }],
"cmd-,": "zed::OpenSettings",
"cmd-alt-,": "zed::OpenSettingsFile",
"cmd-,": "zed::OpenSettingsEditor",
"cmd-alt-,": "zed::OpenSettings",
"cmd-q": "zed::Quit",
"cmd-h": "zed::Hide",
"alt-cmd-h": "zed::HideOthers",
@@ -690,7 +690,7 @@
"cmd-shift-f": "pane::DeploySearch",
"cmd-shift-h": ["pane::DeploySearch", { "replace_enabled": true }],
"cmd-shift-t": "pane::ReopenClosedItem",
"cmd-k cmd-s": "zed::OpenKeymap",
"cmd-k cmd-s": "zed::OpenKeymapEditor",
"cmd-k cmd-t": "theme_selector::Toggle",
"ctrl-alt-cmd-p": "settings_profile_selector::Toggle",
"cmd-t": "project_symbols::Toggle",
@@ -1354,7 +1354,6 @@
"escape": "workspace::CloseWindow",
"cmd-m": "settings_editor::Minimize",
"cmd-f": "search::FocusSearch",
"left": "settings_editor::ToggleFocusNav",
"cmd-shift-e": "settings_editor::ToggleFocusNav",
// todo(settings_ui): cut this down based on the max files and overflow UI
"ctrl-1": ["settings_editor::FocusFile", 0],
@@ -1375,8 +1374,6 @@
"context": "SettingsWindow > NavigationMenu",
"use_key_equivalents": true,
"bindings": {
"up": "settings_editor::FocusPreviousNavEntry",
"down": "settings_editor::FocusNextNavEntry",
"right": "settings_editor::ExpandNavEntry",
"left": "settings_editor::CollapseNavEntry",
"pageup": "settings_editor::FocusPreviousRootNavEntry",

View File

@@ -29,8 +29,8 @@
"ctrl-shift-=": ["zed::IncreaseBufferFontSize", { "persist": false }],
"ctrl--": ["zed::DecreaseBufferFontSize", { "persist": false }],
"ctrl-0": ["zed::ResetBufferFontSize", { "persist": false }],
"ctrl-,": "zed::OpenSettings",
"ctrl-alt-,": "zed::OpenSettingsFile",
"ctrl-,": "zed::OpenSettingsEditor",
"ctrl-alt-,": "zed::OpenSettings",
"ctrl-q": "zed::Quit",
"f4": "debugger::Start",
"shift-f5": "debugger::Stop",
@@ -134,7 +134,7 @@
"ctrl-k z": "editor::ToggleSoftWrap",
"ctrl-f": "buffer_search::Deploy",
"ctrl-h": "buffer_search::DeployReplace",
"ctrl-shift-.": "agent::QuoteSelection",
"ctrl-shift-.": "assistant::QuoteSelection",
"ctrl-shift-,": "assistant::InsertIntoEditor",
"shift-alt-e": "editor::SelectEnclosingSymbol",
"ctrl-shift-backspace": "editor::GoToPreviousChange",
@@ -244,7 +244,7 @@
"ctrl-shift-i": "agent::ToggleOptionsMenu",
// "ctrl-shift-alt-n": "agent::ToggleNewThreadMenu",
"shift-alt-escape": "agent::ExpandMessageEditor",
"ctrl-shift-.": "agent::QuoteSelection",
"ctrl-shift-.": "assistant::QuoteSelection",
"shift-alt-e": "agent::RemoveAllContext",
"ctrl-shift-e": "project_panel::ToggleFocus",
"ctrl-shift-enter": "agent::ContinueThread",
@@ -623,7 +623,7 @@
"ctrl-shift-f": "pane::DeploySearch",
"ctrl-shift-h": ["pane::DeploySearch", { "replace_enabled": true }],
"ctrl-shift-t": "pane::ReopenClosedItem",
"ctrl-k ctrl-s": "zed::OpenKeymap",
"ctrl-k ctrl-s": "zed::OpenKeymapEditor",
"ctrl-k ctrl-t": "theme_selector::Toggle",
"ctrl-alt-super-p": "settings_profile_selector::Toggle",
"ctrl-t": "project_symbols::Toggle",
@@ -1270,7 +1270,6 @@
"escape": "workspace::CloseWindow",
"ctrl-m": "settings_editor::Minimize",
"ctrl-f": "search::FocusSearch",
"left": "settings_editor::ToggleFocusNav",
"ctrl-shift-e": "settings_editor::ToggleFocusNav",
// todo(settings_ui): cut this down based on the max files and overflow UI
"ctrl-1": ["settings_editor::FocusFile", 0],
@@ -1291,8 +1290,6 @@
"context": "SettingsWindow > NavigationMenu",
"use_key_equivalents": true,
"bindings": {
"up": "settings_editor::FocusPreviousNavEntry",
"down": "settings_editor::FocusNextNavEntry",
"right": "settings_editor::ExpandNavEntry",
"left": "settings_editor::CollapseNavEntry",
"pageup": "settings_editor::FocusPreviousRootNavEntry",

View File

@@ -1,7 +1,7 @@
[
{
"bindings": {
"ctrl-alt-s": "zed::OpenSettingsFile",
"ctrl-alt-s": "zed::OpenSettings",
"ctrl-{": "pane::ActivatePreviousItem",
"ctrl-}": "pane::ActivateNextItem",
"shift-escape": null, // Unmap workspace::zoom

View File

@@ -1,5 +1,4 @@
{
"$schema": "zed://schemas/settings",
/// The displayed name of this project. If not set or empty, the root directory name
/// will be displayed.
"project_name": "",
@@ -722,9 +721,7 @@
// Whether to enable drag-and-drop operations in the project panel.
"drag_and_drop": true,
// Whether to hide the root entry when only one folder is open in the window.
"hide_root": false,
// Whether to hide the hidden entries in the project panel.
"hide_hidden": false
"hide_root": false
},
"outline_panel": {
// Whether to show the outline panel button in the status bar
@@ -906,7 +903,6 @@
"now": true,
"find_path": true,
"read_file": true,
"open": true,
"grep": true,
"terminal": true,
"thinking": true,
@@ -918,6 +914,7 @@
// We don't know which of the context server tools are safe for the "Ask" profile, so we don't enable them by default.
// "enable_all_context_servers": true,
"tools": {
"contents": true,
"diagnostics": true,
"fetch": true,
"list_directory": true,
@@ -1104,31 +1101,25 @@
// Removes any lines containing only whitespace at the end of the file and
// ensures just one newline at the end.
"ensure_final_newline_on_save": true,
// Whether or not to perform a buffer format before saving: [on, off]
// Whether or not to perform a buffer format before saving: [on, off, prettier, language_server]
// Keep in mind, if the autosave with delay is enabled, format_on_save will be ignored
"format_on_save": "on",
// How to perform a buffer format. This setting can take multiple values:
// How to perform a buffer format. This setting can take 4 values:
//
// 1. Default. Format files using Zed's Prettier integration (if applicable),
// or falling back to formatting via language server:
// "formatter": "auto"
// 2. Format code using the current language server:
// 1. Format code using the current language server:
// "formatter": "language_server"
// 3. Format code using a specific language server:
// "formatter": {"language_server": {"name": "ruff"}}
// 4. Format code using an external command:
// 2. Format code using an external command:
// "formatter": {
// "external": {
// "command": "prettier",
// "arguments": ["--stdin-filepath", "{buffer_path}"]
// }
// }
// 5. Format code using Zed's Prettier integration:
// 3. Format code using Zed's Prettier integration:
// "formatter": "prettier"
// 6. Format code using a code action
// "formatter": {"code_action": "source.fixAll.eslint"}
// 7. An array of any format step specified above to apply in order
// "formatter": [{"code_action": "source.fixAll.eslint"}, "prettier"]
// 4. Default. Format files using Zed's Prettier integration (if applicable),
// or falling back to formatting via language server:
// "formatter": "auto"
"formatter": "auto",
// How to soft-wrap long lines of text.
// Possible values:
@@ -1738,7 +1729,7 @@
}
},
"Kotlin": {
"language_servers": ["!kotlin-language-server", "kotlin-lsp", "..."]
"language_servers": ["kotlin-language-server", "!kotlin-lsp", "..."]
},
"LaTeX": {
"formatter": "language_server",
@@ -1816,11 +1807,10 @@
},
"SystemVerilog": {
"format_on_save": "off",
"language_servers": ["!slang", "..."],
"use_on_type_format": false
},
"Vue.js": {
"language_servers": ["vue-language-server", "vtsls", "..."],
"language_servers": ["vue-language-server", "..."],
"prettier": {
"allowed": true
}
@@ -2064,7 +2054,7 @@
// }
// }
// }
"profiles": {},
"profiles": [],
// A map of log scopes to the desired log level.
// Useful for filtering out noisy logs or enabling more verbose logging.

View File

@@ -9,8 +9,6 @@ disallowed-methods = [
{ path = "std::process::Command::spawn", reason = "Spawning `std::process::Command` can block the current thread for an unknown duration", replacement = "smol::process::Command::spawn" },
{ path = "std::process::Command::output", reason = "Spawning `std::process::Command` can block the current thread for an unknown duration", replacement = "smol::process::Command::output" },
{ path = "std::process::Command::status", reason = "Spawning `std::process::Command` can block the current thread for an unknown duration", replacement = "smol::process::Command::status" },
{ path = "serde_json::from_reader", reason = "Parsing from a buffer is much slower than first reading the buffer into a Vec/String, see https://github.com/serde-rs/json/issues/160#issuecomment-253446892. Use `serde_json::from_slice` instead." },
{ path = "serde_json_lenient::from_reader", reason = "Parsing from a buffer is much slower than first reading the buffer into a Vec/String, see https://github.com/serde-rs/json/issues/160#issuecomment-253446892, Use `serde_json_lenient::from_slice` instead." },
]
disallowed-types = [
# { path = "std::collections::HashMap", replacement = "collections::HashMap" },

View File

@@ -328,7 +328,7 @@ impl ToolCall {
location: acp::ToolCallLocation,
project: WeakEntity<Project>,
cx: &mut AsyncApp,
) -> Option<ResolvedLocation> {
) -> Option<AgentLocation> {
let buffer = project
.update(cx, |project, cx| {
project
@@ -350,14 +350,17 @@ impl ToolCall {
})
.ok()?;
Some(ResolvedLocation { buffer, position })
Some(AgentLocation {
buffer: buffer.downgrade(),
position,
})
}
fn resolve_locations(
&self,
project: Entity<Project>,
cx: &mut App,
) -> Task<Vec<Option<ResolvedLocation>>> {
) -> Task<Vec<Option<AgentLocation>>> {
let locations = self.locations.clone();
project.update(cx, |_, cx| {
cx.spawn(async move |project, cx| {
@@ -371,23 +374,6 @@ impl ToolCall {
}
}
// Separate so we can hold a strong reference to the buffer
// for saving on the thread
#[derive(Clone, Debug, PartialEq, Eq)]
struct ResolvedLocation {
buffer: Entity<Buffer>,
position: Anchor,
}
impl From<&ResolvedLocation> for AgentLocation {
fn from(value: &ResolvedLocation) -> Self {
Self {
buffer: value.buffer.downgrade(),
position: value.position,
}
}
}
#[derive(Debug)]
pub enum ToolCallStatus {
/// The tool call hasn't started running yet, but we start showing it to
@@ -1407,46 +1393,35 @@ impl AcpThread {
let task = tool_call.resolve_locations(project, cx);
cx.spawn(async move |this, cx| {
let resolved_locations = task.await;
this.update(cx, |this, cx| {
let project = this.project.clone();
for location in resolved_locations.iter().flatten() {
this.shared_buffers
.insert(location.buffer.clone(), location.buffer.read(cx).snapshot());
}
let Some((ix, tool_call)) = this.tool_call_mut(&id) else {
return;
};
if let Some(Some(location)) = resolved_locations.last() {
project.update(cx, |project, cx| {
let should_ignore = if let Some(agent_location) = project
.agent_location()
.filter(|agent_location| agent_location.buffer == location.buffer)
{
let snapshot = location.buffer.read(cx).snapshot();
let old_position = agent_location.position.to_point(&snapshot);
let new_position = location.position.to_point(&snapshot);
// ignore this so that when we get updates from the edit tool
// the position doesn't reset to the startof line
old_position.row == new_position.row
&& old_position.column > new_position.column
} else {
false
};
if !should_ignore {
project.set_agent_location(Some(location.into()), cx);
if let Some(agent_location) = project.agent_location() {
let should_ignore = agent_location.buffer == location.buffer
&& location
.buffer
.update(cx, |buffer, _| {
let snapshot = buffer.snapshot();
let old_position =
agent_location.position.to_point(&snapshot);
let new_position = location.position.to_point(&snapshot);
// ignore this so that when we get updates from the edit tool
// the position doesn't reset to the startof line
old_position.row == new_position.row
&& old_position.column > new_position.column
})
.ok()
.unwrap_or_default();
if !should_ignore {
project.set_agent_location(Some(location.clone()), cx);
}
}
});
}
let resolved_locations = resolved_locations
.iter()
.map(|l| l.as_ref().map(|l| AgentLocation::from(l)))
.collect::<Vec<_>>();
if tool_call.resolved_locations != resolved_locations {
tool_call.resolved_locations = resolved_locations;
cx.emit(AcpThreadEvent::EntryUpdated(ix));
@@ -2137,7 +2112,6 @@ impl AcpThread {
let project = self.project.clone();
let language_registry = project.read(cx).languages().clone();
let is_windows = project.read(cx).path_style(cx).is_windows();
let terminal_id = acp::TerminalId(Uuid::new_v4().to_string().into());
let terminal_task = cx.spawn({
@@ -2151,10 +2125,9 @@ impl AcpThread {
.and_then(|r| r.read(cx).default_system_shell())
})?
.unwrap_or_else(|| get_default_system_shell_preferring_bash());
let (task_command, task_args) =
ShellBuilder::new(&Shell::Program(shell), is_windows)
.redirect_stdin_to_dev_null()
.build(Some(command.clone()), &args);
let (task_command, task_args) = ShellBuilder::new(&Shell::Program(shell))
.redirect_stdin_to_dev_null()
.build(Some(command.clone()), &args);
let terminal = project
.update(cx, |project, cx| {
project.create_terminal_task(

View File

@@ -4,26 +4,22 @@ use std::{
fmt::Display,
rc::{Rc, Weak},
sync::Arc,
time::Duration,
};
use agent_client_protocol as acp;
use collections::HashMap;
use gpui::{
App, ClipboardItem, Empty, Entity, EventEmitter, FocusHandle, Focusable, Global, ListAlignment,
ListState, StyleRefinement, Subscription, Task, TextStyleRefinement, Window, actions, list,
prelude::*,
App, Empty, Entity, EventEmitter, FocusHandle, Focusable, Global, ListAlignment, ListState,
StyleRefinement, Subscription, Task, TextStyleRefinement, Window, actions, list, prelude::*,
};
use language::LanguageRegistry;
use markdown::{CodeBlockRenderer, Markdown, MarkdownElement, MarkdownStyle};
use project::Project;
use settings::Settings;
use theme::ThemeSettings;
use ui::{Tooltip, prelude::*};
use ui::prelude::*;
use util::ResultExt as _;
use workspace::{
Item, ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
};
use workspace::{Item, Workspace};
actions!(dev, [OpenAcpLogs]);
@@ -231,34 +227,6 @@ impl AcpTools {
cx.notify();
}
fn serialize_observed_messages(&self) -> Option<String> {
let connection = self.watched_connection.as_ref()?;
let messages: Vec<serde_json::Value> = connection
.messages
.iter()
.filter_map(|message| {
let params = match &message.params {
Ok(Some(params)) => params.clone(),
Ok(None) => serde_json::Value::Null,
Err(err) => serde_json::to_value(err).ok()?,
};
Some(serde_json::json!({
"_direction": match message.direction {
acp::StreamMessageDirection::Incoming => "incoming",
acp::StreamMessageDirection::Outgoing => "outgoing",
},
"_type": message.message_type.to_string().to_lowercase(),
"id": message.request_id,
"method": message.name.to_string(),
"params": params,
}))
})
.collect();
serde_json::to_string_pretty(&messages).ok()
}
fn render_message(
&mut self,
index: usize,
@@ -524,92 +492,3 @@ impl Render for AcpTools {
})
}
}
pub struct AcpToolsToolbarItemView {
acp_tools: Option<Entity<AcpTools>>,
just_copied: bool,
}
impl AcpToolsToolbarItemView {
pub fn new() -> Self {
Self {
acp_tools: None,
just_copied: false,
}
}
}
impl Render for AcpToolsToolbarItemView {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let Some(acp_tools) = self.acp_tools.as_ref() else {
return Empty.into_any_element();
};
let acp_tools = acp_tools.clone();
h_flex()
.gap_2()
.child(
IconButton::new(
"copy_all_messages",
if self.just_copied {
IconName::Check
} else {
IconName::Copy
},
)
.icon_size(IconSize::Small)
.tooltip(Tooltip::text(if self.just_copied {
"Copied!"
} else {
"Copy All Messages"
}))
.disabled(
acp_tools
.read(cx)
.watched_connection
.as_ref()
.is_none_or(|connection| connection.messages.is_empty()),
)
.on_click(cx.listener(move |this, _, _window, cx| {
if let Some(content) = acp_tools.read(cx).serialize_observed_messages() {
cx.write_to_clipboard(ClipboardItem::new_string(content));
this.just_copied = true;
cx.spawn(async move |this, cx| {
cx.background_executor().timer(Duration::from_secs(2)).await;
this.update(cx, |this, cx| {
this.just_copied = false;
cx.notify();
})
})
.detach();
}
})),
)
.into_any()
}
}
impl EventEmitter<ToolbarItemEvent> for AcpToolsToolbarItemView {}
impl ToolbarItemView for AcpToolsToolbarItemView {
fn set_active_pane_item(
&mut self,
active_pane_item: Option<&dyn ItemHandle>,
_window: &mut Window,
cx: &mut Context<Self>,
) -> ToolbarItemLocation {
if let Some(item) = active_pane_item
&& let Some(acp_tools) = item.downcast::<AcpTools>()
{
self.acp_tools = Some(acp_tools);
cx.notify();
return ToolbarItemLocation::PrimaryRight;
}
if self.acp_tools.take().is_some() {
cx.notify();
}
ToolbarItemLocation::Hidden
}
}

View File

@@ -20,6 +20,7 @@ use std::{
cmp::Reverse,
collections::HashSet,
fmt::Write,
path::Path,
sync::Arc,
time::{Duration, Instant},
};
@@ -327,13 +328,17 @@ impl ActivityIndicator {
.flatten()
}
fn pending_environment_error<'a>(&'a self, cx: &'a App) -> Option<&'a EnvironmentErrorMessage> {
self.project.read(cx).peek_environment_error(cx)
fn pending_environment_errors<'a>(
&'a self,
cx: &'a App,
) -> impl Iterator<Item = (&'a Arc<Path>, &'a EnvironmentErrorMessage)> {
self.project.read(cx).shell_environment_errors(cx)
}
fn content_to_render(&mut self, cx: &mut Context<Self>) -> Option<Content> {
// Show if any direnv calls failed
if let Some(error) = self.pending_environment_error(cx) {
if let Some((abs_path, error)) = self.pending_environment_errors(cx).next() {
let abs_path = abs_path.clone();
return Some(Content {
icon: Some(
Icon::new(IconName::Warning)
@@ -343,7 +348,7 @@ impl ActivityIndicator {
message: error.0.clone(),
on_click: Some(Arc::new(move |this, window, cx| {
this.project.update(cx, |project, cx| {
project.pop_environment_error(cx);
project.remove_environment_error(&abs_path, cx);
});
window.dispatch_action(Box::new(workspace::OpenLog), cx);
})),

View File

@@ -39,6 +39,7 @@ heed.workspace = true
http_client.workspace = true
icons.workspace = true
indoc.workspace = true
itertools.workspace = true
language.workspace = true
language_model.workspace = true
log.workspace = true

View File

@@ -2,6 +2,7 @@ pub mod agent_profile;
pub mod context;
pub mod context_server_tool;
pub mod context_store;
pub mod history_store;
pub mod thread;
pub mod thread_store;
pub mod tool_use;

View File

@@ -0,0 +1,253 @@
use crate::{ThreadId, thread_store::SerializedThreadMetadata};
use anyhow::{Context as _, Result};
use assistant_context::SavedContextMetadata;
use chrono::{DateTime, Utc};
use gpui::{App, AsyncApp, Entity, SharedString, Task, prelude::*};
use itertools::Itertools;
use paths::contexts_dir;
use serde::{Deserialize, Serialize};
use std::{collections::VecDeque, path::Path, sync::Arc, time::Duration};
use util::ResultExt as _;
const MAX_RECENTLY_OPENED_ENTRIES: usize = 6;
const NAVIGATION_HISTORY_PATH: &str = "agent-navigation-history.json";
const SAVE_RECENTLY_OPENED_ENTRIES_DEBOUNCE: Duration = Duration::from_millis(50);
#[derive(Clone, Debug)]
pub enum HistoryEntry {
Thread(SerializedThreadMetadata),
Context(SavedContextMetadata),
}
impl HistoryEntry {
pub fn updated_at(&self) -> DateTime<Utc> {
match self {
HistoryEntry::Thread(thread) => thread.updated_at,
HistoryEntry::Context(context) => context.mtime.to_utc(),
}
}
pub fn id(&self) -> HistoryEntryId {
match self {
HistoryEntry::Thread(thread) => HistoryEntryId::Thread(thread.id.clone()),
HistoryEntry::Context(context) => HistoryEntryId::Context(context.path.clone()),
}
}
pub fn title(&self) -> &SharedString {
match self {
HistoryEntry::Thread(thread) => &thread.summary,
HistoryEntry::Context(context) => &context.title,
}
}
}
/// Generic identifier for a history entry.
#[derive(Clone, PartialEq, Eq, Debug)]
pub enum HistoryEntryId {
Thread(ThreadId),
Context(Arc<Path>),
}
#[derive(Serialize, Deserialize)]
enum SerializedRecentOpen {
Thread(String),
ContextName(String),
/// Old format which stores the full path
Context(String),
}
pub struct HistoryStore {
context_store: Entity<assistant_context::ContextStore>,
recently_opened_entries: VecDeque<HistoryEntryId>,
_subscriptions: Vec<gpui::Subscription>,
_save_recently_opened_entries_task: Task<()>,
}
impl HistoryStore {
pub fn new(
context_store: Entity<assistant_context::ContextStore>,
initial_recent_entries: impl IntoIterator<Item = HistoryEntryId>,
cx: &mut Context<Self>,
) -> Self {
let subscriptions = vec![cx.observe(&context_store, |_, _, cx| cx.notify())];
cx.spawn(async move |this, cx| {
let entries = Self::load_recently_opened_entries(cx).await.log_err()?;
this.update(cx, |this, _| {
this.recently_opened_entries
.extend(
entries.into_iter().take(
MAX_RECENTLY_OPENED_ENTRIES
.saturating_sub(this.recently_opened_entries.len()),
),
);
})
.ok()
})
.detach();
Self {
context_store,
recently_opened_entries: initial_recent_entries.into_iter().collect(),
_subscriptions: subscriptions,
_save_recently_opened_entries_task: Task::ready(()),
}
}
pub fn entries(&self, cx: &mut Context<Self>) -> Vec<HistoryEntry> {
let mut history_entries = Vec::new();
#[cfg(debug_assertions)]
if std::env::var("ZED_SIMULATE_NO_THREAD_HISTORY").is_ok() {
return history_entries;
}
history_entries.extend(
self.context_store
.read(cx)
.unordered_contexts()
.cloned()
.map(HistoryEntry::Context),
);
history_entries.sort_unstable_by_key(|entry| std::cmp::Reverse(entry.updated_at()));
history_entries
}
pub fn recent_entries(&self, limit: usize, cx: &mut Context<Self>) -> Vec<HistoryEntry> {
self.entries(cx).into_iter().take(limit).collect()
}
pub fn recently_opened_entries(&self, cx: &App) -> Vec<HistoryEntry> {
#[cfg(debug_assertions)]
if std::env::var("ZED_SIMULATE_NO_THREAD_HISTORY").is_ok() {
return Vec::new();
}
let context_entries =
self.context_store
.read(cx)
.unordered_contexts()
.flat_map(|context| {
self.recently_opened_entries
.iter()
.enumerate()
.flat_map(|(index, entry)| match entry {
HistoryEntryId::Context(path) if &context.path == path => {
Some((index, HistoryEntry::Context(context.clone())))
}
_ => None,
})
});
context_entries
// optimization to halt iteration early
.take(self.recently_opened_entries.len())
.sorted_unstable_by_key(|(index, _)| *index)
.map(|(_, entry)| entry)
.collect()
}
fn save_recently_opened_entries(&mut self, cx: &mut Context<Self>) {
let serialized_entries = self
.recently_opened_entries
.iter()
.filter_map(|entry| match entry {
HistoryEntryId::Context(path) => path.file_name().map(|file| {
SerializedRecentOpen::ContextName(file.to_string_lossy().into_owned())
}),
HistoryEntryId::Thread(id) => Some(SerializedRecentOpen::Thread(id.to_string())),
})
.collect::<Vec<_>>();
self._save_recently_opened_entries_task = cx.spawn(async move |_, cx| {
cx.background_executor()
.timer(SAVE_RECENTLY_OPENED_ENTRIES_DEBOUNCE)
.await;
cx.background_spawn(async move {
let path = paths::data_dir().join(NAVIGATION_HISTORY_PATH);
let content = serde_json::to_string(&serialized_entries)?;
std::fs::write(path, content)?;
anyhow::Ok(())
})
.await
.log_err();
});
}
fn load_recently_opened_entries(cx: &AsyncApp) -> Task<Result<Vec<HistoryEntryId>>> {
cx.background_spawn(async move {
let path = paths::data_dir().join(NAVIGATION_HISTORY_PATH);
let contents = match smol::fs::read_to_string(path).await {
Ok(it) => it,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
return Ok(Vec::new());
}
Err(e) => {
return Err(e)
.context("deserializing persisted agent panel navigation history");
}
};
let entries = serde_json::from_str::<Vec<SerializedRecentOpen>>(&contents)
.context("deserializing persisted agent panel navigation history")?
.into_iter()
.take(MAX_RECENTLY_OPENED_ENTRIES)
.flat_map(|entry| match entry {
SerializedRecentOpen::Thread(id) => {
Some(HistoryEntryId::Thread(id.as_str().into()))
}
SerializedRecentOpen::ContextName(file_name) => Some(HistoryEntryId::Context(
contexts_dir().join(file_name).into(),
)),
SerializedRecentOpen::Context(path) => {
Path::new(&path).file_name().map(|file_name| {
HistoryEntryId::Context(contexts_dir().join(file_name).into())
})
}
})
.collect::<Vec<_>>();
Ok(entries)
})
}
pub fn push_recently_opened_entry(&mut self, entry: HistoryEntryId, cx: &mut Context<Self>) {
self.recently_opened_entries
.retain(|old_entry| old_entry != &entry);
self.recently_opened_entries.push_front(entry);
self.recently_opened_entries
.truncate(MAX_RECENTLY_OPENED_ENTRIES);
self.save_recently_opened_entries(cx);
}
pub fn remove_recently_opened_thread(&mut self, id: ThreadId, cx: &mut Context<Self>) {
self.recently_opened_entries.retain(
|entry| !matches!(entry, HistoryEntryId::Thread(thread_id) if thread_id == &id),
);
self.save_recently_opened_entries(cx);
}
pub fn replace_recently_opened_text_thread(
&mut self,
old_path: &Path,
new_path: &Arc<Path>,
cx: &mut Context<Self>,
) {
for entry in &mut self.recently_opened_entries {
match entry {
HistoryEntryId::Context(path) if path.as_ref() == old_path => {
*entry = HistoryEntryId::Context(new_path.clone());
break;
}
_ => {}
}
}
self.save_recently_opened_entries(cx);
}
pub fn remove_recently_opened_entry(&mut self, entry: &HistoryEntryId, cx: &mut Context<Self>) {
self.recently_opened_entries
.retain(|old_entry| old_entry != entry);
self.save_recently_opened_entries(cx);
}
}

View File

@@ -790,7 +790,7 @@ mod tests {
store.update_user_settings(cx, |settings| {
settings.project.all_languages.defaults.format_on_save = Some(FormatOnSave::On);
settings.project.all_languages.defaults.formatter =
Some(language::language_settings::FormatterList::default());
Some(language::language_settings::SelectedFormatter::Auto);
});
});
});

View File

@@ -10,7 +10,7 @@ use project::Project;
use project::agent_server_store::AgentServerCommand;
use serde::Deserialize;
use task::Shell;
use util::{ResultExt as _, get_default_system_shell_preferring_bash};
use util::ResultExt as _;
use std::path::PathBuf;
use std::{any::Any, cell::RefCell};
@@ -168,10 +168,7 @@ impl AcpConnection {
meta: None,
},
terminal: true,
meta: Some(serde_json::json!({
// Experimental: Allow for rendering terminal output from the agents
"terminal_output": true,
})),
meta: None,
},
meta: None,
})
@@ -837,11 +834,8 @@ impl acp::Client for ClientDelegate {
.and_then(|r| r.read(cx).default_system_shell())
.map(Shell::Program)
})?
.unwrap_or_else(|| Shell::Program(get_default_system_shell_preferring_bash()));
let is_windows = project
.read_with(&self.cx, |project, cx| project.path_style(cx).is_windows())
.unwrap_or(cfg!(windows));
let (task_command, task_args) = task::ShellBuilder::new(&shell, is_windows)
.unwrap_or(task::Shell::System);
let (task_command, task_args) = task::ShellBuilder::new(&shell)
.redirect_stdin_to_dev_null()
.build(Some(args.command.clone()), &args.args);

View File

@@ -1,16 +1,11 @@
use std::rc::Rc;
use std::sync::Arc;
use std::{any::Any, path::Path};
use acp_thread::AgentConnection;
use agent_client_protocol as acp;
use anyhow::{Context as _, Result};
use fs::Fs;
use gpui::{App, AppContext as _, SharedString, Task};
use project::agent_server_store::{AllAgentServersSettings, CODEX_NAME};
use settings::{SettingsStore, update_settings_file};
use crate::{AgentServer, AgentServerDelegate, load_proxy_env};
use acp_thread::AgentConnection;
use anyhow::{Context as _, Result};
use gpui::{App, SharedString, Task};
use project::agent_server_store::CODEX_NAME;
#[derive(Clone)]
pub struct Codex;
@@ -35,27 +30,6 @@ impl AgentServer for Codex {
ui::IconName::AiOpenAi
}
fn default_mode(&self, cx: &mut App) -> Option<acp::SessionModeId> {
let settings = cx.read_global(|settings: &SettingsStore, _| {
settings.get::<AllAgentServersSettings>(None).codex.clone()
});
settings
.as_ref()
.and_then(|s| s.default_mode.clone().map(|m| acp::SessionModeId(m.into())))
}
fn set_default_mode(&self, mode_id: Option<acp::SessionModeId>, fs: Arc<dyn Fs>, cx: &mut App) {
update_settings_file(fs, cx, |settings, _| {
settings
.agent_servers
.get_or_insert_default()
.codex
.get_or_insert_default()
.default_mode = mode_id.map(|m| m.to_string())
});
}
fn connect(
&self,
root_dir: Option<&Path>,

View File

@@ -27,7 +27,7 @@ use util::rel_path::RelPath;
use workspace::Workspace;
use crate::AgentPanel;
use crate::acp::message_editor::MessageEditor;
use crate::acp::message_editor::{MessageEditor, MessageEditorEvent};
use crate::context_picker::file_context_picker::{FileMatch, search_files};
use crate::context_picker::rules_context_picker::{RulesContextEntry, search_rules};
use crate::context_picker::symbol_context_picker::SymbolMatch;
@@ -759,13 +759,13 @@ impl CompletionProvider for ContextPickerCompletionProvider {
let editor = editor.clone();
move |cx| {
editor
.update(cx, |editor, cx| {
.update(cx, |_editor, cx| {
match intent {
CompletionIntent::Complete
| CompletionIntent::CompleteWithInsert
| CompletionIntent::CompleteWithReplace => {
if !is_missing_argument {
editor.send(cx);
cx.emit(MessageEditorEvent::Send);
}
}
CompletionIntent::Compose => {}
@@ -775,7 +775,7 @@ impl CompletionProvider for ContextPickerCompletionProvider {
}
});
}
false
is_missing_argument
}
})),
}
@@ -910,17 +910,6 @@ impl CompletionProvider for ContextPickerCompletionProvider {
offset_to_line,
self.prompt_capabilities.borrow().embedded_context,
)
.filter(|completion| {
// Right now we don't support completing arguments of slash commands
let is_slash_command_with_argument = matches!(
completion,
ContextCompletion::SlashCommand(SlashCommandCompletion {
argument: Some(_),
..
})
);
!is_slash_command_with_argument
})
.map(|completion| {
completion.source_range().start <= offset_to_line + position.column as usize
&& completion.source_range().end >= offset_to_line + position.column as usize

View File

@@ -141,9 +141,7 @@ impl MessageEditor {
subscriptions.push(cx.subscribe_in(&editor, window, {
move |this, editor, event, window, cx| {
if let EditorEvent::Edited { .. } = event
&& !editor.read(cx).read_only(cx)
{
if let EditorEvent::Edited { .. } = event {
let snapshot = editor.update(cx, |editor, cx| {
let new_hints = this
.command_hint(editor.buffer(), cx)
@@ -825,20 +823,13 @@ impl MessageEditor {
});
}
pub fn send(&mut self, cx: &mut Context<Self>) {
fn send(&mut self, _: &Chat, _: &mut Window, cx: &mut Context<Self>) {
if self.is_empty(cx) {
return;
}
self.editor.update(cx, |editor, cx| {
editor.clear_inlay_hints(cx);
});
cx.emit(MessageEditorEvent::Send)
}
fn chat(&mut self, _: &Chat, _: &mut Window, cx: &mut Context<Self>) {
self.send(cx);
}
fn cancel(&mut self, _: &editor::actions::Cancel, _: &mut Window, cx: &mut Context<Self>) {
cx.emit(MessageEditorEvent::Cancel)
}
@@ -1039,7 +1030,6 @@ impl MessageEditor {
) else {
return;
};
self.editor.update(cx, |message_editor, cx| {
message_editor.edit([(cursor_anchor..cursor_anchor, completion.new_text)], cx);
});
@@ -1297,7 +1287,7 @@ impl Render for MessageEditor {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div()
.key_context("MessageEditor")
.on_action(cx.listener(Self::chat))
.on_action(cx.listener(Self::send))
.on_action(cx.listener(Self::cancel))
.capture_action(cx.listener(Self::paste))
.flex_1()
@@ -2021,11 +2011,21 @@ mod tests {
editor.update_in(&mut cx, |editor, _window, cx| {
assert_eq!(editor.text(cx), "/say-hello ");
assert_eq!(editor.display_text(cx), "/say-hello <name>");
assert!(!editor.has_visible_completions_menu());
assert!(editor.has_visible_completions_menu());
assert_eq!(
current_completion_labels_with_documentation(editor),
&[("say-hello".into(), "Say hello to whoever you want".into())]
);
});
cx.simulate_input("GPT5");
editor.update_in(&mut cx, |editor, window, cx| {
assert!(editor.has_visible_completions_menu());
editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
});
cx.run_until_parked();
editor.update_in(&mut cx, |editor, window, cx| {
@@ -2034,7 +2034,7 @@ mod tests {
assert!(!editor.has_visible_completions_menu());
// Delete argument
for _ in 0..5 {
for _ in 0..4 {
editor.backspace(&editor::actions::Backspace, window, cx);
}
});
@@ -2042,12 +2042,13 @@ mod tests {
cx.run_until_parked();
editor.update_in(&mut cx, |editor, window, cx| {
assert_eq!(editor.text(cx), "/say-hello");
assert_eq!(editor.text(cx), "/say-hello ");
// Hint is visible because argument was deleted
assert_eq!(editor.display_text(cx), "/say-hello <name>");
// Delete last command letter
editor.backspace(&editor::actions::Backspace, window, cx);
editor.backspace(&editor::actions::Backspace, window, cx);
});
cx.run_until_parked();

View File

@@ -292,8 +292,6 @@ pub struct AcpThreadView {
resume_thread_metadata: Option<DbThreadMetadata>,
_cancel_task: Option<Task<()>>,
_subscriptions: [Subscription; 5],
#[cfg(target_os = "windows")]
show_codex_windows_warning: bool,
}
enum ThreadState {
@@ -396,10 +394,6 @@ impl AcpThreadView {
),
];
#[cfg(target_os = "windows")]
let show_codex_windows_warning = crate::ExternalAgent::parse_built_in(agent.as_ref())
== Some(crate::ExternalAgent::Codex);
Self {
agent: agent.clone(),
workspace: workspace.clone(),
@@ -442,8 +436,6 @@ impl AcpThreadView {
focus_handle: cx.focus_handle(),
new_server_version_available: None,
resume_thread_metadata: resume_thread,
#[cfg(target_os = "windows")]
show_codex_windows_warning,
}
}
@@ -1063,9 +1055,6 @@ impl AcpThreadView {
.iter()
.any(|command| command.name == "logout");
if can_login && !logout_supported {
self.message_editor
.update(cx, |editor, cx| editor.clear(window, cx));
let this = cx.weak_entity();
let agent = self.agent.clone();
window.defer(cx, |window, cx| {
@@ -1262,6 +1251,12 @@ impl AcpThreadView {
.detach();
}
fn open_agent_diff(&mut self, _: &OpenAgentDiff, window: &mut Window, cx: &mut Context<Self>) {
if let Some(thread) = self.thread() {
AgentDiffPane::deploy(thread.clone(), self.workspace.clone(), window, cx).log_err();
}
}
fn open_edited_buffer(
&mut self,
buffer: &Entity<Buffer>,
@@ -3288,12 +3283,6 @@ impl AcpThreadView {
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!(
@@ -4983,12 +4972,10 @@ impl AcpThreadView {
})
}
/// Inserts the selected text into the message editor or the message being
/// edited, if any.
pub(crate) fn insert_selections(&self, window: &mut Window, cx: &mut Context<Self>) {
self.active_editor(cx).update(cx, |editor, cx| {
editor.insert_selections(window, cx);
});
self.message_editor.update(cx, |message_editor, cx| {
message_editor.insert_selections(window, cx);
})
}
fn render_thread_retry_status_callout(
@@ -5033,49 +5020,6 @@ impl AcpThreadView {
)
}
#[cfg(target_os = "windows")]
fn render_codex_windows_warning(&self, cx: &mut Context<Self>) -> Option<Callout> {
if self.show_codex_windows_warning {
Some(
Callout::new()
.icon(IconName::Warning)
.severity(Severity::Warning)
.title("Codex on Windows")
.description(
"For best performance, run Codex in Windows Subsystem for Linux (WSL2)",
)
.actions_slot(
Button::new("open-wsl-modal", "Open in WSL")
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.on_click(cx.listener({
move |_, _, window, cx| {
window.dispatch_action(
zed_actions::wsl_actions::OpenWsl::default().boxed_clone(),
cx,
);
cx.notify();
}
})),
)
.dismiss_action(
IconButton::new("dismiss", IconName::Close)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.tooltip(Tooltip::text("Dismiss Warning"))
.on_click(cx.listener({
move |this, _, _, cx| {
this.show_codex_windows_warning = false;
cx.notify();
}
})),
),
)
} else {
None
}
}
fn render_thread_error(&self, window: &mut Window, cx: &mut Context<Self>) -> Option<Div> {
let content = match self.thread_error.as_ref()? {
ThreadError::Other(error) => self.render_any_thread_error(error.clone(), cx),
@@ -5442,23 +5386,6 @@ impl AcpThreadView {
};
task.detach_and_log_err(cx);
}
/// Returns the currently active editor, either for a message that is being
/// edited or the editor for a new message.
fn active_editor(&self, cx: &App) -> Entity<MessageEditor> {
if let Some(index) = self.editing_message
&& let Some(editor) = self
.entry_view_state
.read(cx)
.entry(index)
.and_then(|e| e.message_editor())
.cloned()
{
editor
} else {
self.message_editor.clone()
}
}
}
fn loading_contents_spinner(size: IconSize) -> AnyElement {
@@ -5473,7 +5400,7 @@ impl Focusable for AcpThreadView {
fn focus_handle(&self, cx: &App) -> FocusHandle {
match self.thread_state {
ThreadState::Loading { .. } | ThreadState::Ready { .. } => {
self.active_editor(cx).focus_handle(cx)
self.message_editor.focus_handle(cx)
}
ThreadState::LoadError(_) | ThreadState::Unauthenticated { .. } => {
self.focus_handle.clone()
@@ -5490,6 +5417,7 @@ impl Render for AcpThreadView {
v_flex()
.size_full()
.key_context("AcpThread")
.on_action(cx.listener(Self::open_agent_diff))
.on_action(cx.listener(Self::toggle_burn_mode))
.on_action(cx.listener(Self::keep_all))
.on_action(cx.listener(Self::reject_all))
@@ -5563,16 +5491,6 @@ impl Render for AcpThreadView {
_ => this,
})
.children(self.render_thread_retry_status_callout(window, cx))
.children({
#[cfg(target_os = "windows")]
{
self.render_codex_windows_warning(cx)
}
#[cfg(not(target_os = "windows"))]
{
Vec::<Empty>::new()
}
})
.children(self.render_thread_error(window, cx))
.when_some(
self.new_server_version_available.as_ref().filter(|_| {
@@ -6743,146 +6661,4 @@ pub(crate) mod tests {
)
});
}
#[gpui::test]
async fn test_message_editing_insert_selections(cx: &mut TestAppContext) {
init_test(cx);
let connection = StubAgentConnection::new();
connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk {
content: acp::ContentBlock::Text(acp::TextContent {
text: "Response".into(),
annotations: None,
meta: None,
}),
}]);
let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
add_to_workspace(thread_view.clone(), cx);
let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
message_editor.update_in(cx, |editor, window, cx| {
editor.set_text("Original message to edit", window, cx)
});
thread_view.update_in(cx, |thread_view, window, cx| thread_view.send(window, cx));
cx.run_until_parked();
let user_message_editor = thread_view.read_with(cx, |thread_view, cx| {
thread_view
.entry_view_state
.read(cx)
.entry(0)
.expect("Should have at least one entry")
.message_editor()
.expect("Should have message editor")
.clone()
});
cx.focus(&user_message_editor);
thread_view.read_with(cx, |thread_view, _cx| {
assert_eq!(thread_view.editing_message, Some(0));
});
// Ensure to edit the focused message before proceeding otherwise, since
// its content is not different from what was sent, focus will be lost.
user_message_editor.update_in(cx, |editor, window, cx| {
editor.set_text("Original message to edit with ", window, cx)
});
// Create a simple buffer with some text so we can create a selection
// that will then be added to the message being edited.
let (workspace, project) = thread_view.read_with(cx, |thread_view, _cx| {
(thread_view.workspace.clone(), thread_view.project.clone())
});
let buffer = project.update(cx, |project, cx| {
project.create_local_buffer("let a = 10 + 10;", None, false, cx)
});
workspace
.update_in(cx, |workspace, window, cx| {
let editor = cx.new(|cx| {
let mut editor =
Editor::for_buffer(buffer.clone(), Some(project.clone()), window, cx);
editor.change_selections(Default::default(), window, cx, |selections| {
selections.select_ranges([8..15]);
});
editor
});
workspace.add_item_to_active_pane(Box::new(editor), None, false, window, cx);
})
.unwrap();
thread_view.update_in(cx, |thread_view, window, cx| {
assert_eq!(thread_view.editing_message, Some(0));
thread_view.insert_selections(window, cx);
});
user_message_editor.read_with(cx, |editor, cx| {
let text = editor.editor().read(cx).text(cx);
let expected_text = String::from("Original message to edit with selection ");
assert_eq!(text, expected_text);
});
}
#[gpui::test]
async fn test_insert_selections(cx: &mut TestAppContext) {
init_test(cx);
let connection = StubAgentConnection::new();
connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk {
content: acp::ContentBlock::Text(acp::TextContent {
text: "Response".into(),
annotations: None,
meta: None,
}),
}]);
let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
add_to_workspace(thread_view.clone(), cx);
let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
message_editor.update_in(cx, |editor, window, cx| {
editor.set_text("Can you review this snippet ", window, cx)
});
// Create a simple buffer with some text so we can create a selection
// that will then be added to the message being edited.
let (workspace, project) = thread_view.read_with(cx, |thread_view, _cx| {
(thread_view.workspace.clone(), thread_view.project.clone())
});
let buffer = project.update(cx, |project, cx| {
project.create_local_buffer("let a = 10 + 10;", None, false, cx)
});
workspace
.update_in(cx, |workspace, window, cx| {
let editor = cx.new(|cx| {
let mut editor =
Editor::for_buffer(buffer.clone(), Some(project.clone()), window, cx);
editor.change_selections(Default::default(), window, cx, |selections| {
selections.select_ranges([8..15]);
});
editor
});
workspace.add_item_to_active_pane(Box::new(editor), None, false, window, cx);
})
.unwrap();
thread_view.update_in(cx, |thread_view, window, cx| {
assert_eq!(thread_view.editing_message, None);
thread_view.insert_selections(window, cx);
});
thread_view.read_with(cx, |thread_view, cx| {
let text = thread_view.message_editor.read(cx).text(cx);
let expected_txt = String::from("Can you review this snippet selection ");
assert_eq!(text, expected_txt);
})
}
}

View File

@@ -6,6 +6,7 @@ mod tool_picker;
use std::{ops::Range, sync::Arc};
use agent_settings::AgentSettings;
use anyhow::Result;
use assistant_tool::{ToolSource, ToolWorkingSet};
use cloud_llm_client::{Plan, PlanV1, PlanV2};
@@ -14,6 +15,7 @@ use context_server::ContextServerId;
use editor::{Editor, SelectionEffects, scroll::Autoscroll};
use extension::ExtensionManifest;
use extension_host::ExtensionStore;
use feature_flags::{CodexAcpFeatureFlag, FeatureFlagAppExt as _};
use fs::Fs;
use gpui::{
Action, AnyView, App, AsyncWindowContext, Corner, Entity, EventEmitter, FocusHandle, Focusable,
@@ -28,10 +30,10 @@ use project::{
agent_server_store::{AgentServerStore, CLAUDE_CODE_NAME, CODEX_NAME, GEMINI_NAME},
context_server_store::{ContextServerConfiguration, ContextServerStatus, ContextServerStore},
};
use settings::{SettingsStore, update_settings_file};
use settings::{Settings, SettingsStore, update_settings_file};
use ui::{
Chip, CommonAnimationExt, ContextMenu, Disclosure, Divider, DividerColor, ElevationIndex,
Indicator, PopoverMenu, Switch, SwitchColor, Tooltip, WithScrollbar, prelude::*,
Indicator, PopoverMenu, Switch, SwitchColor, SwitchField, Tooltip, WithScrollbar, prelude::*,
};
use util::ResultExt as _;
use workspace::{Workspace, create_and_open_local_file};
@@ -401,6 +403,101 @@ impl AgentConfiguration {
)
}
fn render_command_permission(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
let always_allow_tool_actions = AgentSettings::get_global(cx).always_allow_tool_actions;
let fs = self.fs.clone();
SwitchField::new(
"always-allow-tool-actions-switch",
Some("Allow running commands without asking for confirmation"),
Some(
"The agent can perform potentially destructive actions without asking for your confirmation.".into(),
),
always_allow_tool_actions,
move |state, _window, cx| {
let allow = state == &ToggleState::Selected;
update_settings_file(fs.clone(), cx, move |settings, _| {
settings.agent.get_or_insert_default().set_always_allow_tool_actions(allow);
});
},
)
}
fn render_single_file_review(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
let single_file_review = AgentSettings::get_global(cx).single_file_review;
let fs = self.fs.clone();
SwitchField::new(
"single-file-review",
Some("Enable single-file agent reviews"),
Some("Agent edits are also displayed in single-file editors for review.".into()),
single_file_review,
move |state, _window, cx| {
let allow = state == &ToggleState::Selected;
update_settings_file(fs.clone(), cx, move |settings, _| {
settings
.agent
.get_or_insert_default()
.set_single_file_review(allow);
});
},
)
}
fn render_sound_notification(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
let play_sound_when_agent_done = AgentSettings::get_global(cx).play_sound_when_agent_done;
let fs = self.fs.clone();
SwitchField::new(
"sound-notification",
Some("Play sound when finished generating"),
Some(
"Hear a notification sound when the agent is done generating changes or needs your input.".into(),
),
play_sound_when_agent_done,
move |state, _window, cx| {
let allow = state == &ToggleState::Selected;
update_settings_file(fs.clone(), cx, move |settings, _| {
settings.agent.get_or_insert_default().set_play_sound_when_agent_done(allow);
});
},
)
}
fn render_modifier_to_send(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
let use_modifier_to_send = AgentSettings::get_global(cx).use_modifier_to_send;
let fs = self.fs.clone();
SwitchField::new(
"modifier-send",
Some("Use modifier to submit a message"),
Some(
"Make a modifier (cmd-enter on macOS, ctrl-enter on Linux or Windows) required to send messages.".into(),
),
use_modifier_to_send,
move |state, _window, cx| {
let allow = state == &ToggleState::Selected;
update_settings_file(fs.clone(), cx, move |settings, _| {
settings.agent.get_or_insert_default().set_use_modifier_to_send(allow);
});
},
)
}
fn render_general_settings_section(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
v_flex()
.p(DynamicSpacing::Base16.rems(cx))
.pr(DynamicSpacing::Base20.rems(cx))
.gap_2p5()
.border_b_1()
.border_color(cx.theme().colors().border)
.child(Headline::new("General Settings"))
.child(self.render_command_permission(cx))
.child(self.render_single_file_review(cx))
.child(self.render_sound_notification(cx))
.child(self.render_modifier_to_send(cx))
}
fn render_zed_plan_info(&self, plan: Option<Plan>, cx: &mut Context<Self>) -> impl IntoElement {
if let Some(plan) = plan {
let free_chip_bg = cx
@@ -988,11 +1085,14 @@ impl AgentConfiguration {
"Claude Code",
))
.child(Divider::horizontal().color(DividerColor::BorderFaded))
.child(self.render_agent_server(
IconName::AiOpenAi,
"Codex",
))
.child(Divider::horizontal().color(DividerColor::BorderFaded))
.when(cx.has_flag::<CodexAcpFeatureFlag>(), |this| {
this
.child(self.render_agent_server(
IconName::AiOpenAi,
"Codex",
))
.child(Divider::horizontal().color(DividerColor::BorderFaded))
})
.child(self.render_agent_server(
IconName::AiGemini,
"Gemini CLI",
@@ -1045,6 +1145,7 @@ impl Render for AgentConfiguration {
.track_scroll(&self.scroll_handle)
.size_full()
.overflow_y_scroll()
.child(self.render_general_settings_section(cx))
.child(self.render_agent_servers_section(cx))
.child(self.render_context_servers_section(window, cx))
.child(self.render_provider_configuration_section(cx)),

View File

@@ -19,10 +19,9 @@ use zed_actions::agent::{OpenClaudeCodeOnboardingModal, ReauthenticateAgent};
use crate::acp::{AcpThreadHistory, ThreadHistoryEvent};
use crate::ui::{AcpOnboardingModal, ClaudeCodeOnboardingModal};
use crate::{
AddContextServer, AgentDiffPane, DeleteRecentlyOpenThread, Follow, InlineAssistant,
NewTextThread, NewThread, OpenActiveThreadAsMarkdown, OpenAgentDiff, OpenHistory,
ResetTrialEndUpsell, ResetTrialUpsell, ToggleNavigationMenu, ToggleNewThreadMenu,
ToggleOptionsMenu,
AddContextServer, DeleteRecentlyOpenThread, Follow, InlineAssistant, NewTextThread, NewThread,
OpenActiveThreadAsMarkdown, OpenHistory, ResetTrialEndUpsell, ResetTrialUpsell,
ToggleNavigationMenu, ToggleNewThreadMenu, ToggleOptionsMenu,
acp::AcpThreadView,
agent_configuration::{AgentConfiguration, AssistantConfigurationEvent},
slash_command::SlashCommandCompletionProvider,
@@ -34,6 +33,7 @@ use crate::{
};
use agent::{
context_store::ContextStore,
history_store::{HistoryEntryId, HistoryStore},
thread_store::{TextThreadStore, ThreadStore},
};
use agent_settings::AgentSettings;
@@ -75,6 +75,7 @@ use zed_actions::{
assistant::{OpenRulesLibrary, ToggleFocus},
};
use feature_flags::{CodexAcpFeatureFlag, FeatureFlagAppExt as _};
const AGENT_PANEL_KEY: &str = "agent_panel";
#[derive(Serialize, Deserialize, Debug)]
@@ -140,16 +141,6 @@ pub fn init(cx: &mut App) {
.register_action(|workspace, _: &Follow, window, cx| {
workspace.follow(CollaboratorId::Agent, window, cx);
})
.register_action(|workspace, _: &OpenAgentDiff, window, cx| {
let thread = workspace
.panel::<AgentPanel>(cx)
.and_then(|panel| panel.read(cx).active_thread_view().cloned())
.and_then(|thread_view| thread_view.read(cx).thread().cloned());
if let Some(thread) = thread {
AgentDiffPane::deploy_in_workspace(thread, workspace, window, cx);
}
})
.register_action(|workspace, _: &ToggleNavigationMenu, window, cx| {
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
workspace.focus_panel::<AgentPanel>(window, cx);
@@ -222,11 +213,12 @@ enum WhichFontSize {
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
pub enum AgentType {
#[default]
NativeAgent,
Zed,
TextThread,
Gemini,
ClaudeCode,
Codex,
NativeAgent,
Custom {
name: SharedString,
command: AgentServerCommand,
@@ -236,7 +228,8 @@ pub enum AgentType {
impl AgentType {
fn label(&self) -> SharedString {
match self {
Self::NativeAgent | Self::TextThread => "Zed Agent".into(),
Self::Zed | Self::TextThread => "Zed Agent".into(),
Self::NativeAgent => "Agent 2".into(),
Self::Gemini => "Gemini CLI".into(),
Self::ClaudeCode => "Claude Code".into(),
Self::Codex => "Codex".into(),
@@ -246,7 +239,7 @@ impl AgentType {
fn icon(&self) -> Option<IconName> {
match self {
Self::NativeAgent | Self::TextThread => None,
Self::Zed | Self::NativeAgent | Self::TextThread => None,
Self::Gemini => Some(IconName::AiGemini),
Self::ClaudeCode => Some(IconName::AiClaude),
Self::Codex => Some(IconName::AiOpenAi),
@@ -306,6 +299,7 @@ impl ActiveView {
pub fn prompt_editor(
context_editor: Entity<TextThreadEditor>,
history_store: Entity<HistoryStore>,
acp_history_store: Entity<agent2::HistoryStore>,
language_registry: Arc<LanguageRegistry>,
window: &mut Window,
@@ -373,6 +367,18 @@ impl ActiveView {
})
}
ContextEvent::PathChanged { old_path, new_path } => {
history_store.update(cx, |history_store, cx| {
if let Some(old_path) = old_path {
history_store
.replace_recently_opened_text_thread(old_path, new_path, cx);
} else {
history_store.push_recently_opened_entry(
HistoryEntryId::Context(new_path.clone()),
cx,
);
}
});
acp_history_store.update(cx, |history_store, cx| {
if let Some(old_path) = old_path {
history_store
@@ -414,7 +420,7 @@ pub struct AgentPanel {
language_registry: Arc<LanguageRegistry>,
thread_store: Entity<ThreadStore>,
acp_history: Entity<AcpThreadHistory>,
history_store: Entity<agent2::HistoryStore>,
acp_history_store: Entity<agent2::HistoryStore>,
context_store: Entity<TextThreadStore>,
prompt_store: Option<Entity<PromptStore>>,
inline_assist_context_store: Entity<ContextStore>,
@@ -422,6 +428,7 @@ pub struct AgentPanel {
configuration_subscription: Option<Subscription>,
active_view: ActiveView,
previous_view: Option<ActiveView>,
history_store: Entity<HistoryStore>,
new_thread_menu_handle: PopoverMenuHandle<ContextMenu>,
agent_panel_menu_handle: PopoverMenuHandle<ContextMenu>,
assistant_navigation_menu_handle: PopoverMenuHandle<ContextMenu>,
@@ -554,8 +561,10 @@ impl AgentPanel {
let inline_assist_context_store =
cx.new(|_cx| ContextStore::new(project.downgrade(), Some(thread_store.downgrade())));
let history_store = cx.new(|cx| agent2::HistoryStore::new(context_store.clone(), cx));
let acp_history = cx.new(|cx| AcpThreadHistory::new(history_store.clone(), window, cx));
let history_store = cx.new(|cx| HistoryStore::new(context_store.clone(), [], cx));
let acp_history_store = cx.new(|cx| agent2::HistoryStore::new(context_store.clone(), cx));
let acp_history = cx.new(|cx| AcpThreadHistory::new(acp_history_store.clone(), window, cx));
cx.subscribe_in(
&acp_history,
window,
@@ -577,12 +586,14 @@ impl AgentPanel {
)
.detach();
cx.observe(&history_store, |_, _, cx| cx.notify()).detach();
let panel_type = AgentSettings::get_global(cx).default_view;
let active_view = match panel_type {
DefaultView::Thread => ActiveView::native_agent(
fs.clone(),
prompt_store.clone(),
history_store.clone(),
acp_history_store.clone(),
project.clone(),
workspace.clone(),
window,
@@ -608,6 +619,7 @@ impl AgentPanel {
ActiveView::prompt_editor(
context_editor,
history_store.clone(),
acp_history_store.clone(),
language_registry.clone(),
window,
cx,
@@ -673,6 +685,7 @@ impl AgentPanel {
configuration_subscription: None,
inline_assist_context_store,
previous_view: None,
history_store: history_store.clone(),
new_thread_menu_handle: PopoverMenuHandle::default(),
agent_panel_menu_handle: PopoverMenuHandle::default(),
assistant_navigation_menu_handle: PopoverMenuHandle::default(),
@@ -683,7 +696,7 @@ impl AgentPanel {
pending_serialization: None,
onboarding,
acp_history,
history_store,
acp_history_store,
selected_agent: AgentType::default(),
loading: false,
}
@@ -737,7 +750,7 @@ impl AgentPanel {
cx: &mut Context<Self>,
) {
let Some(thread) = self
.history_store
.acp_history_store
.read(cx)
.thread_from_session_id(&action.from_session_id)
else {
@@ -786,6 +799,7 @@ impl AgentPanel {
ActiveView::prompt_editor(
context_editor.clone(),
self.history_store.clone(),
self.acp_history_store.clone(),
self.language_registry.clone(),
window,
cx,
@@ -811,13 +825,13 @@ impl AgentPanel {
const LAST_USED_EXTERNAL_AGENT_KEY: &str = "agent_panel__last_used_external_agent";
#[derive(Serialize, Deserialize)]
#[derive(Default, Serialize, Deserialize)]
struct LastUsedExternalAgent {
agent: crate::ExternalAgent,
}
let loading = self.loading;
let history = self.history_store.clone();
let history = self.acp_history_store.clone();
cx.spawn_in(window, async move |this, cx| {
let ext_agent = match agent_choice {
@@ -852,18 +866,18 @@ impl AgentPanel {
.and_then(|value| {
serde_json::from_str::<LastUsedExternalAgent>(&value).log_err()
})
.map(|agent| agent.agent)
.unwrap_or(ExternalAgent::NativeAgent)
.unwrap_or_default()
.agent
}
}
};
let server = ext_agent.server(fs, history);
if !loading {
telemetry::event!("Agent Thread Started", agent = server.telemetry_id());
telemetry::event!("Agent Thread Started", agent = ext_agent.name());
}
let server = ext_agent.server(fs, history);
this.update_in(cx, |this, window, cx| {
let selected_agent = ext_agent.into();
if this.selected_agent != selected_agent {
@@ -878,7 +892,7 @@ impl AgentPanel {
summarize_thread,
workspace.clone(),
project,
this.history_store.clone(),
this.acp_history_store.clone(),
this.prompt_store.clone(),
window,
cx,
@@ -976,6 +990,7 @@ impl AgentPanel {
ActiveView::prompt_editor(
editor,
self.history_store.clone(),
self.acp_history_store.clone(),
self.language_registry.clone(),
window,
cx,
@@ -1239,6 +1254,11 @@ impl AgentPanel {
match &new_view {
ActiveView::TextThread { context_editor, .. } => {
self.history_store.update(cx, |store, cx| {
if let Some(path) = context_editor.read(cx).context().read(cx).path() {
store.push_recently_opened_entry(HistoryEntryId::Context(path.clone()), cx)
}
});
self.acp_history_store.update(cx, |store, cx| {
if let Some(path) = context_editor.read(cx).context().read(cx).path() {
store.push_recently_opened_entry(
agent2::HistoryEntryId::TextThread(path.clone()),
@@ -1272,7 +1292,7 @@ impl AgentPanel {
) -> ContextMenu {
let entries = panel
.read(cx)
.history_store
.acp_history_store
.read(cx)
.recently_opened_entries(cx);
@@ -1317,7 +1337,7 @@ impl AgentPanel {
move |_window, cx| {
panel
.update(cx, |this, cx| {
this.history_store.update(cx, |history_store, cx| {
this.acp_history_store.update(cx, |history_store, cx| {
history_store.remove_recently_opened_entry(&id, cx);
});
})
@@ -1343,6 +1363,15 @@ impl AgentPanel {
cx: &mut Context<Self>,
) {
match agent {
AgentType::Zed => {
window.dispatch_action(
NewThread {
from_thread_id: None,
}
.boxed_clone(),
cx,
);
}
AgentType::TextThread => {
window.dispatch_action(NewTextThread.boxed_clone(), cx);
}
@@ -1910,32 +1939,34 @@ impl AgentPanel {
}
}),
)
.item(
ContextMenuEntry::new("New Codex Thread")
.icon(IconName::AiOpenAi)
.disabled(is_via_collab)
.icon_color(Color::Muted)
.handler({
let workspace = workspace.clone();
move |window, cx| {
if let Some(workspace) = workspace.upgrade() {
workspace.update(cx, |workspace, cx| {
if let Some(panel) =
workspace.panel::<AgentPanel>(cx)
{
panel.update(cx, |panel, cx| {
panel.new_agent_thread(
AgentType::Codex,
window,
cx,
);
});
}
});
.when(cx.has_flag::<CodexAcpFeatureFlag>(), |this| {
this.item(
ContextMenuEntry::new("New Codex Thread")
.icon(IconName::AiOpenAi)
.disabled(is_via_collab)
.icon_color(Color::Muted)
.handler({
let workspace = workspace.clone();
move |window, cx| {
if let Some(workspace) = workspace.upgrade() {
workspace.update(cx, |workspace, cx| {
if let Some(panel) =
workspace.panel::<AgentPanel>(cx)
{
panel.update(cx, |panel, cx| {
panel.new_agent_thread(
AgentType::Codex,
window,
cx,
);
});
}
});
}
}
}
}),
)
}),
)
})
.item(
ContextMenuEntry::new("New Gemini CLI Thread")
.icon(IconName::AiGemini)
@@ -2139,7 +2170,10 @@ impl AgentPanel {
false
}
_ => {
let history_is_empty = self.history_store.read(cx).is_empty(cx);
let history_is_empty = self.acp_history_store.read(cx).is_empty(cx)
&& self
.history_store
.update(cx, |store, cx| store.recent_entries(1, cx).is_empty());
let has_configured_non_zed_providers = LanguageModelRegistry::read_global(cx)
.providers()

View File

@@ -161,9 +161,10 @@ pub struct NewNativeAgentThreadFromSummary {
}
// TODO unify this with AgentType
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ExternalAgent {
enum ExternalAgent {
#[default]
Gemini,
ClaudeCode,
Codex,
@@ -183,13 +184,13 @@ fn placeholder_command() -> AgentServerCommand {
}
impl ExternalAgent {
pub fn parse_built_in(server: &dyn agent_servers::AgentServer) -> Option<Self> {
match server.telemetry_id() {
"gemini-cli" => Some(Self::Gemini),
"claude-code" => Some(Self::ClaudeCode),
"codex" => Some(Self::Codex),
"zed" => Some(Self::NativeAgent),
_ => None,
fn name(&self) -> &'static str {
match self {
Self::NativeAgent => "zed",
Self::Gemini => "gemini-cli",
Self::ClaudeCode => "claude-code",
Self::Codex => "codex",
Self::Custom { .. } => "custom",
}
}

View File

@@ -84,32 +84,10 @@ impl ZedAiOnboarding {
self
}
fn render_dismiss_button(&self) -> Option<AnyElement> {
self.dismiss_onboarding.as_ref().map(|dismiss_callback| {
let callback = dismiss_callback.clone();
h_flex()
.absolute()
.top_0()
.right_0()
.child(
IconButton::new("dismiss_onboarding", IconName::Close)
.icon_size(IconSize::Small)
.tooltip(Tooltip::text("Dismiss"))
.on_click(move |_, window, cx| {
telemetry::event!("Banner Dismissed", source = "AI Onboarding",);
callback(window, cx)
}),
)
.into_any_element()
})
}
fn render_sign_in_disclaimer(&self, _cx: &mut App) -> AnyElement {
let signing_in = matches!(self.sign_in_status, SignInStatus::SigningIn);
v_flex()
.relative()
.gap_1()
.child(Headline::new("Welcome to Zed AI"))
.child(
@@ -131,7 +109,6 @@ impl ZedAiOnboarding {
}
}),
)
.children(self.render_dismiss_button())
.into_any_element()
}
@@ -203,7 +180,27 @@ impl ZedAiOnboarding {
)
.child(PlanDefinitions.free_plan(is_v2)),
)
.children(self.render_dismiss_button())
.when_some(
self.dismiss_onboarding.as_ref(),
|this, dismiss_callback| {
let callback = dismiss_callback.clone();
this.child(
h_flex().absolute().top_0().right_0().child(
IconButton::new("dismiss_onboarding", IconName::Close)
.icon_size(IconSize::Small)
.tooltip(Tooltip::text("Dismiss"))
.on_click(move |_, window, cx| {
telemetry::event!(
"Banner Dismissed",
source = "AI Onboarding",
);
callback(window, cx)
}),
),
)
},
)
.child(
v_flex()
.mt_2()
@@ -248,7 +245,26 @@ impl ZedAiOnboarding {
.mb_2(),
)
.child(PlanDefinitions.pro_trial(is_v2, false))
.children(self.render_dismiss_button())
.when_some(
self.dismiss_onboarding.as_ref(),
|this, dismiss_callback| {
let callback = dismiss_callback.clone();
this.child(
h_flex().absolute().top_0().right_0().child(
IconButton::new("dismiss_onboarding", IconName::Close)
.icon_size(IconSize::Small)
.tooltip(Tooltip::text("Dismiss"))
.on_click(move |_, window, cx| {
telemetry::event!(
"Banner Dismissed",
source = "AI Onboarding",
);
callback(window, cx)
}),
),
)
},
)
.into_any_element()
}
@@ -262,7 +278,26 @@ impl ZedAiOnboarding {
.mb_2(),
)
.child(PlanDefinitions.pro_plan(is_v2, false))
.children(self.render_dismiss_button())
.when_some(
self.dismiss_onboarding.as_ref(),
|this, dismiss_callback| {
let callback = dismiss_callback.clone();
this.child(
h_flex().absolute().top_0().right_0().child(
IconButton::new("dismiss_onboarding", IconName::Close)
.icon_size(IconSize::Small)
.tooltip(Tooltip::text("Dismiss"))
.on_click(move |_, window, cx| {
telemetry::event!(
"Banner Dismissed",
source = "AI Onboarding",
);
callback(window, cx)
}),
),
)
},
)
.into_any_element()
}
}

View File

@@ -1538,7 +1538,7 @@ mod tests {
store.update_user_settings(cx, |settings| {
settings.project.all_languages.defaults.format_on_save = Some(FormatOnSave::On);
settings.project.all_languages.defaults.formatter =
Some(language::language_settings::FormatterList::default());
Some(language::language_settings::SelectedFormatter::Auto);
});
});
});

View File

@@ -136,7 +136,6 @@ impl Tool for TerminalTool {
}),
None => Task::ready(None).shared(),
};
let is_windows = project.read(cx).path_style(cx).is_windows();
let shell = project
.update(cx, |project, cx| {
project
@@ -156,7 +155,7 @@ impl Tool for TerminalTool {
let build_cmd = {
let input_command = input.command.clone();
move || {
ShellBuilder::new(&Shell::Program(shell), is_windows)
ShellBuilder::new(&Shell::Program(shell))
.redirect_stdin_to_dev_null()
.build(Some(input_command), &[])
}

View File

@@ -649,7 +649,7 @@ impl AutoUpdater {
#[cfg(not(target_os = "windows"))]
anyhow::ensure!(
which::which("rsync").is_ok(),
"Could not auto-update because the required rsync utility was not found."
"Aborting. Could not find rsync which is required for auto-updates."
);
Ok(())
}
@@ -658,7 +658,7 @@ impl AutoUpdater {
let filename = match OS {
"macos" => anyhow::Ok("Zed.dmg"),
"linux" => Ok("zed.tar.gz"),
"windows" => Ok("Zed.exe"),
"windows" => Ok("zed_editor_installer.exe"),
unsupported_os => anyhow::bail!("not supported: {unsupported_os}"),
}?;

View File

@@ -142,8 +142,6 @@ pub struct DeclarationScoreComponents {
pub normalized_import_similarity: f32,
pub wildcard_import_similarity: f32,
pub normalized_wildcard_import_similarity: f32,
pub included_by_others: usize,
pub includes_others: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]

View File

@@ -97,7 +97,6 @@ CREATE TABLE "worktree_entries" (
"is_external" BOOL NOT NULL,
"is_ignored" BOOL NOT NULL,
"is_deleted" BOOL NOT NULL,
"is_hidden" BOOL NOT NULL,
"git_status" INTEGER,
"is_fifo" BOOL NOT NULL,
PRIMARY KEY (project_id, worktree_id, id),

View File

@@ -1,2 +0,0 @@
ALTER TABLE "worktree_entries"
ADD "is_hidden" BOOL NOT NULL DEFAULT FALSE;

View File

@@ -282,7 +282,6 @@ impl Database {
git_status: ActiveValue::set(None),
is_external: ActiveValue::set(entry.is_external),
is_deleted: ActiveValue::set(false),
is_hidden: ActiveValue::set(entry.is_hidden),
scan_id: ActiveValue::set(update.scan_id as i64),
is_fifo: ActiveValue::set(entry.is_fifo),
}
@@ -301,7 +300,6 @@ impl Database {
worktree_entry::Column::MtimeNanos,
worktree_entry::Column::CanonicalPath,
worktree_entry::Column::IsIgnored,
worktree_entry::Column::IsHidden,
worktree_entry::Column::ScanId,
])
.to_owned(),
@@ -907,7 +905,6 @@ impl Database {
canonical_path: db_entry.canonical_path,
is_ignored: db_entry.is_ignored,
is_external: db_entry.is_external,
is_hidden: db_entry.is_hidden,
// This is only used in the summarization backlog, so if it's None,
// that just means we won't be able to detect when to resummarize
// based on total number of backlogged bytes - instead, we'd go

View File

@@ -671,7 +671,6 @@ impl Database {
canonical_path: db_entry.canonical_path,
is_ignored: db_entry.is_ignored,
is_external: db_entry.is_external,
is_hidden: db_entry.is_hidden,
// This is only used in the summarization backlog, so if it's None,
// that just means we won't be able to detect when to resummarize
// based on total number of backlogged bytes - instead, we'd go

View File

@@ -19,7 +19,6 @@ pub struct Model {
pub is_ignored: bool,
pub is_external: bool,
pub is_deleted: bool,
pub is_hidden: bool,
pub scan_id: i64,
pub is_fifo: bool,
pub canonical_path: Option<String>,

View File

@@ -4,7 +4,7 @@ use crate::{
};
use call::ActiveCall;
use editor::{
DocumentColorsRenderMode, Editor, FETCH_COLORS_DEBOUNCE_TIMEOUT, RowInfo, SelectionEffects,
DocumentColorsRenderMode, Editor, RowInfo, SelectionEffects,
actions::{
ConfirmCodeAction, ConfirmCompletion, ConfirmRename, ContextMenuFirst,
ExpandMacroRecursively, MoveToEnd, Redo, Rename, SelectAll, ToggleCodeActions, Undo,
@@ -2409,7 +2409,6 @@ async fn test_lsp_document_color(cx_a: &mut TestAppContext, cx_b: &mut TestAppCo
.unwrap();
color_request_handle.next().await.unwrap();
executor.advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
executor.run_until_parked();
assert_eq!(

View File

@@ -25,7 +25,7 @@ use gpui::{
use language::{
Diagnostic, DiagnosticEntry, DiagnosticSourceKind, FakeLspAdapter, Language, LanguageConfig,
LanguageMatcher, LineEnding, OffsetRangeExt, Point, Rope,
language_settings::{Formatter, FormatterList},
language_settings::{Formatter, FormatterList, SelectedFormatter},
tree_sitter_rust, tree_sitter_typescript,
};
use lsp::{LanguageServerId, OneOf};
@@ -39,7 +39,7 @@ use project::{
use prompt_store::PromptBuilder;
use rand::prelude::*;
use serde_json::json;
use settings::{LanguageServerFormatterSpecifier, PrettierSettingsContent, SettingsStore};
use settings::{PrettierSettingsContent, SettingsStore};
use std::{
cell::{Cell, RefCell},
env, future, mem,
@@ -4610,13 +4610,14 @@ async fn test_formatting_buffer(
cx_a.update(|cx| {
SettingsStore::update_global(cx, |store, cx| {
store.update_user_settings(cx, |file| {
file.project.all_languages.defaults.formatter =
Some(FormatterList::Single(Formatter::External {
file.project.all_languages.defaults.formatter = Some(SelectedFormatter::List(
FormatterList::Single(Formatter::External {
command: "awk".into(),
arguments: Some(
vec!["{sub(/two/,\"{buffer_path}\")}1".to_string()].into(),
),
}));
}),
));
});
});
});
@@ -4707,7 +4708,7 @@ async fn test_prettier_formatting_buffer(
cx_a.update(|cx| {
SettingsStore::update_global(cx, |store, cx| {
store.update_user_settings(cx, |file| {
file.project.all_languages.defaults.formatter = Some(FormatterList::default());
file.project.all_languages.defaults.formatter = Some(SelectedFormatter::Auto);
file.project.all_languages.defaults.prettier = Some(PrettierSettingsContent {
allowed: Some(true),
..Default::default()
@@ -4718,8 +4719,8 @@ async fn test_prettier_formatting_buffer(
cx_b.update(|cx| {
SettingsStore::update_global(cx, |store, cx| {
store.update_user_settings(cx, |file| {
file.project.all_languages.defaults.formatter = Some(FormatterList::Single(
Formatter::LanguageServer(LanguageServerFormatterSpecifier::Current),
file.project.all_languages.defaults.formatter = Some(SelectedFormatter::List(
FormatterList::Single(Formatter::LanguageServer { name: None }),
));
file.project.all_languages.defaults.prettier = Some(PrettierSettingsContent {
allowed: Some(true),

View File

@@ -14,7 +14,7 @@ use gpui::{
use http_client::BlockedHttpClient;
use language::{
FakeLspAdapter, Language, LanguageConfig, LanguageMatcher, LanguageRegistry,
language_settings::{Formatter, FormatterList, language_settings},
language_settings::{Formatter, FormatterList, SelectedFormatter, language_settings},
tree_sitter_typescript,
};
use node_runtime::NodeRuntime;
@@ -27,7 +27,7 @@ use remote::RemoteClient;
use remote_server::{HeadlessAppState, HeadlessProject};
use rpc::proto;
use serde_json::json;
use settings::{LanguageServerFormatterSpecifier, PrettierSettingsContent, SettingsStore};
use settings::{PrettierSettingsContent, SettingsStore};
use std::{
path::Path,
sync::{Arc, atomic::AtomicUsize},
@@ -491,7 +491,7 @@ async fn test_ssh_collaboration_formatting_with_prettier(
cx_a.update(|cx| {
SettingsStore::update_global(cx, |store, cx| {
store.update_user_settings(cx, |file| {
file.project.all_languages.defaults.formatter = Some(FormatterList::default());
file.project.all_languages.defaults.formatter = Some(SelectedFormatter::Auto);
file.project.all_languages.defaults.prettier = Some(PrettierSettingsContent {
allowed: Some(true),
..Default::default()
@@ -502,8 +502,8 @@ async fn test_ssh_collaboration_formatting_with_prettier(
cx_b.update(|cx| {
SettingsStore::update_global(cx, |store, cx| {
store.update_user_settings(cx, |file| {
file.project.all_languages.defaults.formatter = Some(FormatterList::Single(
Formatter::LanguageServer(LanguageServerFormatterSpecifier::Current),
file.project.all_languages.defaults.formatter = Some(SelectedFormatter::List(
FormatterList::Single(Formatter::LanguageServer { name: None }),
));
file.project.all_languages.defaults.prettier = Some(PrettierSettingsContent {
allowed: Some(true),
@@ -550,7 +550,7 @@ async fn test_ssh_collaboration_formatting_with_prettier(
cx_a.update(|cx| {
SettingsStore::update_global(cx, |store, cx| {
store.update_user_settings(cx, |file| {
file.project.all_languages.defaults.formatter = Some(FormatterList::default());
file.project.all_languages.defaults.formatter = Some(SelectedFormatter::Auto);
file.project.all_languages.defaults.prettier = Some(PrettierSettingsContent {
allowed: Some(true),
..Default::default()

View File

@@ -2250,7 +2250,7 @@ impl CollabPanel {
})),
)
.child(
v_flex().w_full().items_center().child(
div().flex().w_full().items_center().child(
Label::new("Sign in to enable collaboration.")
.color(Color::Muted)
.size(LabelSize::Small),

View File

@@ -1,8 +1,8 @@
[package]
name = "collections"
name = "zed-collections"
version = "0.1.0"
edition.workspace = true
publish = false
publish = true
license = "Apache-2.0"
description = "Standard collection type re-exports used by Zed and GPUI"

View File

@@ -92,10 +92,7 @@ pub async fn init(crash_init: InitCrashHandler) {
#[cfg(target_os = "macos")]
suspend_all_other_threads();
// on macos this "ping" is needed to ensure that all our
// `client.send_message` calls have been processed before we trigger the
// minidump request.
client.ping().ok();
client.ping().unwrap();
client.request_dump(crash_context).is_ok()
} else {
true

View File

@@ -1,12 +1,12 @@
use crate::*;
use anyhow::{Context as _, bail};
use anyhow::Context as _;
use dap::{DebugRequest, StartDebuggingRequestArguments, adapters::DebugTaskDefinition};
use fs::RemoveOptions;
use futures::{StreamExt, TryStreamExt};
use gpui::http_client::AsyncBody;
use gpui::{AsyncApp, SharedString};
use json_dotpath::DotPaths;
use language::{LanguageName, Toolchain};
use language::LanguageName;
use paths::debug_adapters_dir;
use serde_json::Value;
use smol::fs::File;
@@ -20,8 +20,7 @@ use std::{
ffi::OsStr,
path::{Path, PathBuf},
};
use util::command::new_smol_command;
use util::{ResultExt, paths::PathStyle, rel_path::RelPath};
use util::{ResultExt, maybe, paths::PathStyle, rel_path::RelPath};
#[derive(Default)]
pub(crate) struct PythonDebugAdapter {
@@ -93,16 +92,12 @@ impl PythonDebugAdapter {
})
}
async fn fetch_wheel(
&self,
toolchain: Option<Toolchain>,
delegate: &Arc<dyn DapDelegate>,
) -> Result<Arc<Path>> {
async fn fetch_wheel(&self, delegate: &Arc<dyn DapDelegate>) -> Result<Arc<Path>, String> {
let download_dir = debug_adapters_dir().join(Self::ADAPTER_NAME).join("wheels");
std::fs::create_dir_all(&download_dir)?;
let venv_python = self.base_venv_path(toolchain, delegate).await?;
std::fs::create_dir_all(&download_dir).map_err(|e| e.to_string())?;
let system_python = self.base_venv_path(delegate).await?;
let installation_succeeded = util::command::new_smol_command(venv_python.as_ref())
let installation_succeeded = util::command::new_smol_command(system_python.as_ref())
.args([
"-m",
"pip",
@@ -114,36 +109,36 @@ impl PythonDebugAdapter {
])
.output()
.await
.context("spawn system python")?
.map_err(|e| format!("{e}"))?
.status
.success();
if !installation_succeeded {
bail!("debugpy installation failed (could not fetch Debugpy's wheel)");
return Err("debugpy installation failed (could not fetch Debugpy's wheel)".into());
}
let wheel_path = std::fs::read_dir(&download_dir)?
let wheel_path = std::fs::read_dir(&download_dir)
.map_err(|e| e.to_string())?
.find_map(|entry| {
entry.ok().filter(|e| {
e.file_type().is_ok_and(|typ| typ.is_file())
&& Path::new(&e.file_name()).extension() == Some("whl".as_ref())
})
})
.with_context(|| format!("Did not find a .whl in {download_dir:?}"))?;
.ok_or_else(|| String::from("Did not find a .whl in {download_dir}"))?;
util::archive::extract_zip(
&debug_adapters_dir().join(Self::ADAPTER_NAME),
File::open(&wheel_path.path()).await?,
File::open(&wheel_path.path())
.await
.map_err(|e| e.to_string())?,
)
.await?;
.await
.map_err(|e| e.to_string())?;
Ok(Arc::from(wheel_path.path()))
}
async fn maybe_fetch_new_wheel(
&self,
toolchain: Option<Toolchain>,
delegate: &Arc<dyn DapDelegate>,
) -> Result<()> {
async fn maybe_fetch_new_wheel(&self, delegate: &Arc<dyn DapDelegate>) {
let latest_release = delegate
.http_client()
.get(
@@ -153,61 +148,62 @@ impl PythonDebugAdapter {
)
.await
.log_err();
let response = latest_release
.filter(|response| response.status().is_success())
.context("getting latest release")?;
maybe!(async move {
let response = latest_release.filter(|response| response.status().is_success())?;
let download_dir = debug_adapters_dir().join(Self::ADAPTER_NAME);
std::fs::create_dir_all(&download_dir)?;
let download_dir = debug_adapters_dir().join(Self::ADAPTER_NAME);
std::fs::create_dir_all(&download_dir).ok()?;
let mut output = String::new();
response.into_body().read_to_string(&mut output).await?;
let as_json = serde_json::Value::from_str(&output)?;
let latest_version = as_json
.get("info")
.and_then(|info| {
let mut output = String::new();
response
.into_body()
.read_to_string(&mut output)
.await
.ok()?;
let as_json = serde_json::Value::from_str(&output).ok()?;
let latest_version = as_json.get("info").and_then(|info| {
info.get("version")
.and_then(|version| version.as_str())
.map(ToOwned::to_owned)
})
.context("parsing latest release information")?;
let dist_info_dirname: OsString = format!("debugpy-{latest_version}.dist-info").into();
let is_up_to_date = delegate
.fs()
.read_dir(&debug_adapters_dir().join(Self::ADAPTER_NAME))
.await?
.into_stream()
.any(async |entry| {
entry.is_ok_and(|e| e.file_name().is_some_and(|name| name == dist_info_dirname))
})
.await;
if !is_up_to_date {
delegate
})?;
let dist_info_dirname: OsString = format!("debugpy-{latest_version}.dist-info").into();
let is_up_to_date = delegate
.fs()
.remove_dir(
&debug_adapters_dir().join(Self::ADAPTER_NAME),
RemoveOptions {
recursive: true,
ignore_if_not_exists: true,
},
)
.await?;
self.fetch_wheel(toolchain, delegate).await?;
}
anyhow::Ok(())
.read_dir(&debug_adapters_dir().join(Self::ADAPTER_NAME))
.await
.ok()?
.into_stream()
.any(async |entry| {
entry.is_ok_and(|e| e.file_name().is_some_and(|name| name == dist_info_dirname))
})
.await;
if !is_up_to_date {
delegate
.fs()
.remove_dir(
&debug_adapters_dir().join(Self::ADAPTER_NAME),
RemoveOptions {
recursive: true,
ignore_if_not_exists: true,
},
)
.await
.ok()?;
self.fetch_wheel(delegate).await.ok()?;
}
Some(())
})
.await;
}
async fn fetch_debugpy_whl(
&self,
toolchain: Option<Toolchain>,
delegate: &Arc<dyn DapDelegate>,
) -> Result<Arc<Path>, String> {
self.debugpy_whl_base_path
.get_or_init(|| async move {
self.maybe_fetch_new_wheel(toolchain, delegate)
.await
.map_err(|e| format!("{e}"))?;
self.maybe_fetch_new_wheel(delegate).await;
Ok(Arc::from(
debug_adapters_dir()
.join(Self::ADAPTER_NAME)
@@ -220,24 +216,12 @@ impl PythonDebugAdapter {
.clone()
}
async fn base_venv_path(
&self,
toolchain: Option<Toolchain>,
delegate: &Arc<dyn DapDelegate>,
) -> Result<Arc<Path>> {
let result = self.base_venv_path
async fn base_venv_path(&self, delegate: &Arc<dyn DapDelegate>) -> Result<Arc<Path>, String> {
self.base_venv_path
.get_or_init(|| async {
let base_python = if let Some(toolchain) = toolchain {
toolchain.path.to_string()
} else {
Self::system_python_name(delegate).await.ok_or_else(|| {
let mut message = "Could not find a Python installation".to_owned();
if cfg!(windows){
message.push_str(". Install Python from the Microsoft Store, or manually from https://www.python.org/downloads/windows.")
}
message
})?
};
let base_python = Self::system_python_name(delegate)
.await
.ok_or_else(|| String::from("Could not find a Python installation"))?;
let did_succeed = util::command::new_smol_command(base_python)
.args(["-m", "venv", "zed_base_venv"])
@@ -255,50 +239,35 @@ impl PythonDebugAdapter {
return Err("Failed to create base virtual environment".into());
}
const PYTHON_PATH: &str = if cfg!(target_os = "windows") {
"Scripts/python.exe"
const DIR: &str = if cfg!(target_os = "windows") {
"Scripts"
} else {
"bin/python3"
"bin"
};
Ok(Arc::from(
paths::debug_adapters_dir()
.join(Self::DEBUG_ADAPTER_NAME.as_ref())
.join("zed_base_venv")
.join(PYTHON_PATH)
.join(DIR)
.join("python3")
.as_ref(),
))
})
.await
.clone();
match result {
Ok(path) => Ok(path),
Err(e) => Err(anyhow::anyhow!("{e}")),
}
.clone()
}
async fn system_python_name(delegate: &Arc<dyn DapDelegate>) -> Option<String> {
const BINARY_NAMES: [&str; 3] = ["python3", "python", "py"];
let mut name = None;
for cmd in BINARY_NAMES {
let Some(path) = delegate.which(OsStr::new(cmd)).await else {
continue;
};
// Try to detect situations where `python3` exists but is not a real Python interpreter.
// Notably, on fresh Windows installs, `python3` is a shim that opens the Microsoft Store app
// when run with no arguments, and just fails otherwise.
let Some(output) = new_smol_command(&path)
.args(["-c", "print(1 + 2)"])
.output()
name = delegate
.which(OsStr::new(cmd))
.await
.ok()
else {
continue;
};
if output.stdout.trim_ascii() != b"3" {
continue;
.map(|path| path.to_string_lossy().into_owned());
if name.is_some() {
break;
}
name = Some(path.to_string_lossy().into_owned());
break;
}
name
}
@@ -777,10 +746,15 @@ impl DebugAdapter for PythonDebugAdapter {
)
.await;
self.fetch_debugpy_whl(toolchain.clone(), delegate)
let debugpy_path = self
.fetch_debugpy_whl(delegate)
.await
.map_err(|e| anyhow::anyhow!("{e}"))?;
if let Some(toolchain) = &toolchain {
log::debug!(
"Found debugpy in toolchain environment: {}",
debugpy_path.display()
);
return self
.get_installed_binary(
delegate,

View File

@@ -963,21 +963,26 @@ pub fn init(cx: &mut App) {
};
let project = workspace.project();
log_store.update(cx, |store, cx| {
store.add_project(project, cx);
});
if project.read(cx).is_local() {
log_store.update(cx, |store, cx| {
store.add_project(project, cx);
});
}
let log_store = log_store.clone();
workspace.register_action(move |workspace, _: &OpenDebugAdapterLogs, window, cx| {
workspace.add_item_to_active_pane(
Box::new(cx.new(|cx| {
DapLogView::new(workspace.project().clone(), log_store.clone(), window, cx)
})),
None,
true,
window,
cx,
);
let project = workspace.project().read(cx);
if project.is_local() {
workspace.add_item_to_active_pane(
Box::new(cx.new(|cx| {
DapLogView::new(workspace.project().clone(), log_store.clone(), window, cx)
})),
None,
true,
window,
cx,
);
}
});
})
.detach();

View File

@@ -268,12 +268,12 @@ impl DebugPanel {
async move |_, cx| {
if let Err(error) = task.await {
log::error!("{error:#}");
log::error!("{error}");
session
.update(cx, |session, cx| {
session
.console_output(cx)
.unbounded_send(format!("error: {:#}", error))
.unbounded_send(format!("error: {}", error))
.ok();
session.shutdown(cx)
})?

View File

@@ -1,7 +1,7 @@
use std::rc::Rc;
use collections::HashMap;
use gpui::{Corner, Entity, WeakEntity};
use gpui::{Entity, WeakEntity};
use project::debugger::session::{ThreadId, ThreadStatus};
use ui::{CommonAnimationExt, ContextMenu, DropdownMenu, DropdownStyle, Indicator, prelude::*};
use util::{maybe, truncate_and_trailoff};
@@ -211,7 +211,6 @@ impl DebugPanel {
this
}),
)
.attach(Corner::BottomLeft)
.style(DropdownStyle::Ghost)
.handle(self.session_picker_menu_handle.clone());
@@ -323,7 +322,6 @@ impl DebugPanel {
this
}),
)
.attach(Corner::BottomLeft)
.disabled(session_terminated)
.style(DropdownStyle::Ghost)
.handle(self.thread_picker_menu_handle.clone()),

View File

@@ -937,7 +937,6 @@ impl RunningState {
let task_store = project.read(cx).task_store().downgrade();
let weak_project = project.downgrade();
let weak_workspace = workspace.downgrade();
let is_windows = project.read(cx).path_style(cx).is_windows();
let remote_shell = project
.read(cx)
.remote_client()
@@ -1030,7 +1029,7 @@ impl RunningState {
task.resolved.shell = Shell::Program(remote_shell);
}
let builder = ShellBuilder::new(&task.resolved.shell, is_windows);
let builder = ShellBuilder::new(&task.resolved.shell);
let command_label = builder.command_label(task.resolved.command.as_deref().unwrap_or(""));
let (command, args) =
builder.build(task.resolved.command.clone(), &task.resolved.args);

View File

@@ -965,11 +965,10 @@ async fn heuristic_syntactic_expand(
let row_count = node_end.row - node_start.row + 1;
let mut ancestor_range = None;
let reached_outline_node = cx.background_executor().scoped({
let node_range = node_range.clone();
let outline_range = outline_range.clone();
let ancestor_range = &mut ancestor_range;
|scope| {
scope.spawn(async move {
let node_range = node_range.clone();
let outline_range = outline_range.clone();
let ancestor_range = &mut ancestor_range;
|scope| {scope.spawn(async move {
// Stop if we've exceeded the row count or reached an outline node. Then, find the interval
// of node children which contains the query range. For example, this allows just returning
// the header of a declaration rather than the entire declaration.
@@ -981,11 +980,8 @@ async fn heuristic_syntactic_expand(
if cursor.goto_first_child() {
loop {
let child_node = cursor.node();
let child_range =
previous_end..Point::from_ts_point(child_node.end_position());
if included_child_start.is_none()
&& child_range.contains(&input_range.start)
{
let child_range = previous_end..Point::from_ts_point(child_node.end_position());
if included_child_start.is_none() && child_range.contains(&input_range.start) {
included_child_start = Some(child_range.start);
}
if child_range.contains(&input_range.end) {
@@ -1001,22 +997,19 @@ async fn heuristic_syntactic_expand(
if let Some(start) = included_child_start {
let row_count = end.row - start.row;
if row_count < max_row_count {
*ancestor_range =
Some(Some(RangeInclusive::new(start.row, end.row)));
*ancestor_range = Some(Some(RangeInclusive::new(start.row, end.row)));
return;
}
}
log::info!(
"Expanding to ancestor started on {} node\
exceeding row limit of {max_row_count}.",
"Expanding to ancestor started on {} node exceeding row limit of {max_row_count}.",
node.grammar_name()
);
*ancestor_range = Some(None);
}
})
}
});
}});
reached_outline_node.await;
if let Some(node) = ancestor_range {
return node;

View File

@@ -20,8 +20,6 @@ util.workspace = true
workspace-hack.workspace = true
zed.workspace = true
zlog.workspace = true
task.workspace = true
theme.workspace = true
[lints]
workspace = true

View File

@@ -53,20 +53,9 @@ fn main() -> Result<()> {
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
enum PreprocessorError {
ActionNotFound {
action_name: String,
},
DeprecatedActionUsed {
used: String,
should_be: String,
},
ActionNotFound { action_name: String },
DeprecatedActionUsed { used: String, should_be: String },
InvalidFrontmatterLine(String),
InvalidSettingsJson {
file: std::path::PathBuf,
line: usize,
snippet: String,
error: String,
},
}
impl PreprocessorError {
@@ -83,20 +72,6 @@ impl PreprocessorError {
}
PreprocessorError::ActionNotFound { action_name }
}
fn new_for_invalid_settings_json(
chapter: &Chapter,
location: usize,
snippet: String,
error: String,
) -> Self {
PreprocessorError::InvalidSettingsJson {
file: chapter.path.clone().expect("chapter has path"),
line: chapter.content[..location].lines().count() + 1,
snippet,
error,
}
}
}
impl std::fmt::Display for PreprocessorError {
@@ -113,21 +88,6 @@ impl std::fmt::Display for PreprocessorError {
"Deprecated action used: {} should be {}",
used, should_be
),
PreprocessorError::InvalidSettingsJson {
file,
line,
snippet,
error,
} => {
write!(
f,
"Invalid settings JSON at {}:{}\nError: {}\n\n{}",
file.display(),
line,
error,
snippet
)
}
}
}
}
@@ -140,11 +100,11 @@ fn handle_preprocessing() -> Result<()> {
let (_ctx, mut book) = CmdPreprocessor::parse_input(input.as_bytes())?;
let mut errors = HashSet::<PreprocessorError>::new();
handle_frontmatter(&mut book, &mut errors);
template_big_table_of_actions(&mut book);
template_and_validate_keybindings(&mut book, &mut errors);
template_and_validate_actions(&mut book, &mut errors);
template_and_validate_json_snippets(&mut book, &mut errors);
if !errors.is_empty() {
const ANSI_RED: &str = "\x1b[31m";
@@ -275,161 +235,6 @@ fn find_binding(os: &str, action: &str) -> Option<String> {
})
}
fn template_and_validate_json_snippets(book: &mut Book, errors: &mut HashSet<PreprocessorError>) {
fn for_each_labeled_code_block_mut(
book: &mut Book,
errors: &mut HashSet<PreprocessorError>,
f: impl Fn(&str, &str) -> anyhow::Result<()>,
) {
const TAGGED_JSON_BLOCK_START: &'static str = "```json [";
const JSON_BLOCK_END: &'static str = "```";
for_each_chapter_mut(book, |chapter| {
let mut offset = 0;
while let Some(loc) = chapter.content[offset..].find(TAGGED_JSON_BLOCK_START) {
let loc = loc + offset;
let tag_start = loc + TAGGED_JSON_BLOCK_START.len();
offset = tag_start;
let Some(tag_end) = chapter.content[tag_start..].find(']') else {
errors.insert(PreprocessorError::new_for_invalid_settings_json(
chapter,
loc,
chapter.content[loc..tag_start].to_string(),
"Unclosed JSON block tag".to_string(),
));
continue;
};
let tag_end = tag_end + tag_start;
let tag = &chapter.content[tag_start..tag_end];
if tag.contains('\n') {
errors.insert(PreprocessorError::new_for_invalid_settings_json(
chapter,
loc,
chapter.content[loc..tag_start].to_string(),
"Unclosed JSON block tag".to_string(),
));
continue;
}
let snippet_start = tag_end + 1;
offset = snippet_start;
let Some(snippet_end) = chapter.content[snippet_start..].find(JSON_BLOCK_END)
else {
errors.insert(PreprocessorError::new_for_invalid_settings_json(
chapter,
loc,
chapter.content[loc..tag_end + 1].to_string(),
"Missing closing code block".to_string(),
));
continue;
};
let snippet_end = snippet_start + snippet_end;
let snippet_json = &chapter.content[snippet_start..snippet_end];
offset = snippet_end + 3;
if let Err(err) = f(tag, snippet_json) {
errors.insert(PreprocessorError::new_for_invalid_settings_json(
chapter,
loc,
chapter.content[loc..snippet_end + 3].to_string(),
err.to_string(),
));
continue;
};
let tag_range_complete = tag_start - 1..tag_end + 1;
offset -= tag_range_complete.len();
chapter.content.replace_range(tag_range_complete, "");
}
});
}
for_each_labeled_code_block_mut(book, errors, |label, snippet_json| {
let mut snippet_json_fixed = snippet_json
.to_string()
.replace("\n>", "\n")
.trim()
.to_string();
while snippet_json_fixed.starts_with("//") {
if let Some(line_end) = snippet_json_fixed.find('\n') {
snippet_json_fixed.replace_range(0..line_end, "");
snippet_json_fixed = snippet_json_fixed.trim().to_string();
}
}
match label {
"settings" => {
if !snippet_json_fixed.starts_with('{') || !snippet_json_fixed.ends_with('}') {
snippet_json_fixed.insert(0, '{');
snippet_json_fixed.push_str("\n}");
}
settings::parse_json_with_comments::<settings::SettingsContent>(
&snippet_json_fixed,
)?;
}
"keymap" => {
if !snippet_json_fixed.starts_with('[') || !snippet_json_fixed.ends_with(']') {
snippet_json_fixed.insert(0, '[');
snippet_json_fixed.push_str("\n]");
}
let keymap = settings::KeymapFile::parse(&snippet_json_fixed)
.context("Failed to parse keymap JSON")?;
for section in keymap.sections() {
for (keystrokes, action) in section.bindings() {
keystrokes
.split_whitespace()
.map(|source| gpui::Keystroke::parse(source))
.collect::<std::result::Result<Vec<_>, _>>()
.context("Failed to parse keystroke")?;
if let Some((action_name, _)) = settings::KeymapFile::parse_action(action)
.map_err(|err| anyhow::format_err!(err))
.context("Failed to parse action")?
{
anyhow::ensure!(
find_action_by_name(action_name).is_some(),
"Action not found: {}",
action_name
);
}
}
}
}
"debug" => {
if !snippet_json_fixed.starts_with('[') || !snippet_json_fixed.ends_with(']') {
snippet_json_fixed.insert(0, '[');
snippet_json_fixed.push_str("\n]");
}
settings::parse_json_with_comments::<task::DebugTaskFile>(&snippet_json_fixed)?;
}
"tasks" => {
if !snippet_json_fixed.starts_with('[') || !snippet_json_fixed.ends_with(']') {
snippet_json_fixed.insert(0, '[');
snippet_json_fixed.push_str("\n]");
}
settings::parse_json_with_comments::<task::TaskTemplates>(&snippet_json_fixed)?;
}
"icon-theme" => {
if !snippet_json_fixed.starts_with('{') || !snippet_json_fixed.ends_with('}') {
snippet_json_fixed.insert(0, '{');
snippet_json_fixed.push_str("\n}");
}
settings::parse_json_with_comments::<theme::IconThemeFamilyContent>(
&snippet_json_fixed,
)?;
}
label => {
anyhow::bail!("Unexpected JSON code block tag: {}", label)
}
};
Ok(())
});
}
/// Removes any configurable options from the stringified action if existing,
/// ensuring that only the actual action name is returned. If the action consists
/// only of a string and nothing else, the string is returned as-is.

View File

@@ -2,12 +2,10 @@ use cloud_llm_client::predict_edits_v3::DeclarationScoreComponents;
use collections::HashMap;
use language::BufferSnapshot;
use ordered_float::OrderedFloat;
use project::ProjectEntryId;
use serde::Serialize;
use std::{cmp::Reverse, ops::Range, path::Path, sync::Arc};
use strum::EnumIter;
use text::{Point, ToPoint};
use util::RangeExt as _;
use crate::{
CachedDeclarationPath, Declaration, EditPredictionExcerpt, Identifier,
@@ -75,7 +73,7 @@ impl ScoredDeclaration {
}
pub fn retrieval_score(&self) -> f32 {
let mut score = if self.components.is_same_file {
if self.components.is_same_file {
10.0 / self.components.same_file_declaration_count as f32
} else if self.components.path_import_match_count > 0 {
3.0
@@ -87,10 +85,7 @@ impl ScoredDeclaration {
0.5 * self.components.normalized_wildcard_import_similarity
} else {
1.0 / self.components.declaration_count as f32
};
score *= 1. + self.components.included_by_others as f32 / 2.;
score *= 1. + self.components.includes_others as f32 / 4.;
score
}
}
pub fn size(&self, style: DeclarationStyle) -> usize {
@@ -138,210 +133,194 @@ pub fn scored_declarations(
}
}
let mut scored_declarations = Vec::new();
let mut project_entry_id_to_outline_ranges: HashMap<ProjectEntryId, Vec<Range<usize>>> =
HashMap::default();
for (identifier, references) in identifier_to_references {
let mut import_occurrences = Vec::new();
let mut import_paths = Vec::new();
let mut found_external_identifier: Option<&Identifier> = None;
let mut declarations = identifier_to_references
.into_iter()
.flat_map(|(identifier, references)| {
let mut import_occurrences = Vec::new();
let mut import_paths = Vec::new();
let mut found_external_identifier: Option<&Identifier> = None;
if let Some(imports) = imports.identifier_to_imports.get(&identifier) {
// only use alias when it's the only import, could be generalized if some language
// has overlapping aliases
//
// TODO: when an aliased declaration is included in the prompt, should include the
// aliasing in the prompt.
//
// TODO: For SourceFuzzy consider having componentwise comparison that pays
// attention to ordering.
if let [
Import::Alias {
module,
external_identifier,
},
] = imports.as_slice()
{
match module {
Module::Namespace(namespace) => {
import_occurrences.push(namespace.occurrences())
}
Module::SourceExact(path) => import_paths.push(path),
Module::SourceFuzzy(path) => {
import_occurrences.push(Occurrences::from_path(&path))
}
}
found_external_identifier = Some(&external_identifier);
} else {
for import in imports {
match import {
Import::Direct { module } => match module {
Module::Namespace(namespace) => {
import_occurrences.push(namespace.occurrences())
}
Module::SourceExact(path) => import_paths.push(path),
Module::SourceFuzzy(path) => {
import_occurrences.push(Occurrences::from_path(&path))
}
},
Import::Alias { .. } => {}
}
}
}
}
let identifier_to_lookup = found_external_identifier.unwrap_or(&identifier);
// TODO: update this to be able to return more declarations? Especially if there is the
// ability to quickly filter a large list (based on imports)
let identifier_declarations = index
.declarations_for_identifier::<MAX_IDENTIFIER_DECLARATION_COUNT>(&identifier_to_lookup);
let declaration_count = identifier_declarations.len();
if declaration_count == 0 {
continue;
}
// TODO: option to filter out other candidates when same file / import match
let mut checked_declarations = Vec::with_capacity(declaration_count);
for (declaration_id, declaration) in identifier_declarations {
match declaration {
Declaration::Buffer {
buffer_id,
declaration: buffer_declaration,
..
} => {
if buffer_id == &current_buffer.remote_id() {
let already_included_in_prompt =
range_intersection(&buffer_declaration.item_range, &excerpt.range)
.is_some()
|| excerpt
.parent_declarations
.iter()
.any(|(excerpt_parent, _)| excerpt_parent == &declaration_id);
if !options.omit_excerpt_overlaps || !already_included_in_prompt {
let declaration_line = buffer_declaration
.item_range
.start
.to_point(current_buffer)
.row;
let declaration_line_distance =
(cursor_point.row as i32 - declaration_line as i32).unsigned_abs();
checked_declarations.push(CheckedDeclaration {
declaration,
same_file_line_distance: Some(declaration_line_distance),
path_import_match_count: 0,
wildcard_path_import_match_count: 0,
});
if let Some(imports) = imports.identifier_to_imports.get(&identifier) {
// only use alias when it's the only import, could be generalized if some language
// has overlapping aliases
//
// TODO: when an aliased declaration is included in the prompt, should include the
// aliasing in the prompt.
//
// TODO: For SourceFuzzy consider having componentwise comparison that pays
// attention to ordering.
if let [
Import::Alias {
module,
external_identifier,
},
] = imports.as_slice()
{
match module {
Module::Namespace(namespace) => {
import_occurrences.push(namespace.occurrences())
}
Module::SourceExact(path) => import_paths.push(path),
Module::SourceFuzzy(path) => {
import_occurrences.push(Occurrences::from_path(&path))
}
}
found_external_identifier = Some(&external_identifier);
} else {
for import in imports {
match import {
Import::Direct { module } => match module {
Module::Namespace(namespace) => {
import_occurrences.push(namespace.occurrences())
}
Module::SourceExact(path) => import_paths.push(path),
Module::SourceFuzzy(path) => {
import_occurrences.push(Occurrences::from_path(&path))
}
},
Import::Alias { .. } => {}
}
continue;
} else {
}
}
Declaration::File { .. } => {}
}
let declaration_path = declaration.cached_path();
let path_import_match_count = import_paths
.iter()
.filter(|import_path| {
declaration_path_matches_import(&declaration_path, import_path)
})
.count();
let wildcard_path_import_match_count = wildcard_import_paths
.iter()
.filter(|import_path| {
declaration_path_matches_import(&declaration_path, import_path)
})
.count();
checked_declarations.push(CheckedDeclaration {
declaration,
same_file_line_distance: None,
path_import_match_count,
wildcard_path_import_match_count,
});
}
let mut max_import_similarity = 0.0;
let mut max_wildcard_import_similarity = 0.0;
let mut scored_declarations_for_identifier = Vec::with_capacity(checked_declarations.len());
for checked_declaration in checked_declarations {
let same_file_declaration_count =
index.file_declaration_count(checked_declaration.declaration);
let declaration = score_declaration(
&identifier,
&references,
checked_declaration,
same_file_declaration_count,
declaration_count,
&excerpt_occurrences,
&adjacent_occurrences,
&import_occurrences,
&wildcard_import_occurrences,
cursor_point,
current_buffer,
);
if declaration.components.import_similarity > max_import_similarity {
max_import_similarity = declaration.components.import_similarity;
}
if declaration.components.wildcard_import_similarity > max_wildcard_import_similarity {
max_wildcard_import_similarity = declaration.components.wildcard_import_similarity;
let identifier_to_lookup = found_external_identifier.unwrap_or(&identifier);
// TODO: update this to be able to return more declarations? Especially if there is the
// ability to quickly filter a large list (based on imports)
let declarations = index
.declarations_for_identifier::<MAX_IDENTIFIER_DECLARATION_COUNT>(
&identifier_to_lookup,
);
let declaration_count = declarations.len();
if declaration_count == 0 {
return Vec::new();
}
project_entry_id_to_outline_ranges
.entry(declaration.declaration.project_entry_id())
.or_default()
.push(declaration.declaration.item_range());
scored_declarations_for_identifier.push(declaration);
}
if max_import_similarity > 0.0 || max_wildcard_import_similarity > 0.0 {
for declaration in scored_declarations_for_identifier.iter_mut() {
if max_import_similarity > 0.0 {
declaration.components.max_import_similarity = max_import_similarity;
declaration.components.normalized_import_similarity =
declaration.components.import_similarity / max_import_similarity;
// TODO: option to filter out other candidates when same file / import match
let mut checked_declarations = Vec::new();
for (declaration_id, declaration) in declarations {
match declaration {
Declaration::Buffer {
buffer_id,
declaration: buffer_declaration,
..
} => {
if buffer_id == &current_buffer.remote_id() {
let already_included_in_prompt =
range_intersection(&buffer_declaration.item_range, &excerpt.range)
.is_some()
|| excerpt.parent_declarations.iter().any(
|(excerpt_parent, _)| excerpt_parent == &declaration_id,
);
if !options.omit_excerpt_overlaps || !already_included_in_prompt {
let declaration_line = buffer_declaration
.item_range
.start
.to_point(current_buffer)
.row;
let declaration_line_distance = (cursor_point.row as i32
- declaration_line as i32)
.unsigned_abs();
checked_declarations.push(CheckedDeclaration {
declaration,
same_file_line_distance: Some(declaration_line_distance),
path_import_match_count: 0,
wildcard_path_import_match_count: 0,
});
}
continue;
} else {
}
}
Declaration::File { .. } => {}
}
if max_wildcard_import_similarity > 0.0 {
declaration.components.normalized_wildcard_import_similarity =
declaration.components.wildcard_import_similarity
/ max_wildcard_import_similarity;
let declaration_path = declaration.cached_path();
let path_import_match_count = import_paths
.iter()
.filter(|import_path| {
declaration_path_matches_import(&declaration_path, import_path)
})
.count();
let wildcard_path_import_match_count = wildcard_import_paths
.iter()
.filter(|import_path| {
declaration_path_matches_import(&declaration_path, import_path)
})
.count();
checked_declarations.push(CheckedDeclaration {
declaration,
same_file_line_distance: None,
path_import_match_count,
wildcard_path_import_match_count,
});
}
let mut max_import_similarity = 0.0;
let mut max_wildcard_import_similarity = 0.0;
let mut scored_declarations_for_identifier = checked_declarations
.into_iter()
.map(|checked_declaration| {
let same_file_declaration_count =
index.file_declaration_count(checked_declaration.declaration);
let declaration = score_declaration(
&identifier,
&references,
checked_declaration,
same_file_declaration_count,
declaration_count,
&excerpt_occurrences,
&adjacent_occurrences,
&import_occurrences,
&wildcard_import_occurrences,
cursor_point,
current_buffer,
);
if declaration.components.import_similarity > max_import_similarity {
max_import_similarity = declaration.components.import_similarity;
}
if declaration.components.wildcard_import_similarity
> max_wildcard_import_similarity
{
max_wildcard_import_similarity =
declaration.components.wildcard_import_similarity;
}
declaration
})
.collect::<Vec<_>>();
if max_import_similarity > 0.0 || max_wildcard_import_similarity > 0.0 {
for declaration in scored_declarations_for_identifier.iter_mut() {
if max_import_similarity > 0.0 {
declaration.components.max_import_similarity = max_import_similarity;
declaration.components.normalized_import_similarity =
declaration.components.import_similarity / max_import_similarity;
}
if max_wildcard_import_similarity > 0.0 {
declaration.components.normalized_wildcard_import_similarity =
declaration.components.wildcard_import_similarity
/ max_wildcard_import_similarity;
}
}
}
}
scored_declarations.extend(scored_declarations_for_identifier);
}
scored_declarations_for_identifier
})
.collect::<Vec<_>>();
// TODO: Inform this via import / retrieval scores of outline items
// TODO: Consider using a sweepline
for scored_declaration in scored_declarations.iter_mut() {
let project_entry_id = scored_declaration.declaration.project_entry_id();
let Some(ranges) = project_entry_id_to_outline_ranges.get(&project_entry_id) else {
continue;
};
for range in ranges {
if range.contains_inclusive(&scored_declaration.declaration.item_range()) {
scored_declaration.components.included_by_others += 1
} else if scored_declaration
.declaration
.item_range()
.contains_inclusive(range)
{
scored_declaration.components.includes_others += 1
}
}
}
scored_declarations.sort_unstable_by_key(|declaration| {
Reverse(OrderedFloat(
declaration.score(DeclarationStyle::Declaration),
))
declarations.sort_unstable_by_key(|declaration| {
let score_density = declaration
.score_density(DeclarationStyle::Declaration)
.max(declaration.score_density(DeclarationStyle::Signature));
Reverse(OrderedFloat(score_density))
});
scored_declarations
declarations
}
struct CheckedDeclaration<'a> {
@@ -486,8 +465,6 @@ fn score_declaration(
normalized_import_similarity: 0.0,
wildcard_import_similarity,
normalized_wildcard_import_similarity: 0.0,
included_by_others: 0,
includes_others: 0,
};
ScoredDeclaration {

View File

@@ -1518,7 +1518,6 @@ impl CodeActionsMenu {
this.child(
h_flex()
.overflow_hidden()
.when(is_quick_action_bar, |this| this.text_ui(cx))
.child(task.resolved_label.replace("\n", ""))
.when(selected, |this| {
this.text_color(colors.text_accent)
@@ -1529,7 +1528,6 @@ impl CodeActionsMenu {
this.child(
h_flex()
.overflow_hidden()
.when(is_quick_action_bar, |this| this.text_ui(cx))
.child("debug: ")
.child(scenario.label.clone())
.when(selected, |this| {

View File

@@ -594,11 +594,7 @@ impl DisplayMap {
self.block_map.read(snapshot, edits);
}
pub fn remove_inlays_for_excerpts(
&mut self,
excerpts_removed: &[ExcerptId],
cx: &mut Context<Self>,
) {
pub fn remove_inlays_for_excerpts(&mut self, excerpts_removed: &[ExcerptId]) {
let to_remove = self
.inlay_map
.current_inlays()
@@ -610,7 +606,7 @@ impl DisplayMap {
}
})
.collect::<Vec<_>>();
self.splice_inlays(&to_remove, Vec::new(), cx);
self.inlay_map.splice(&to_remove, Vec::new());
}
fn tab_size(buffer: &Entity<MultiBuffer>, cx: &App) -> NonZeroU32 {

View File

@@ -226,7 +226,6 @@ pub const SELECTION_HIGHLIGHT_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis
pub(crate) const CODE_ACTION_TIMEOUT: Duration = Duration::from_secs(5);
pub(crate) const FORMAT_TIMEOUT: Duration = Duration::from_secs(5);
pub(crate) const SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT: Duration = Duration::from_secs(1);
pub const FETCH_COLORS_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(150);
pub(crate) const EDIT_PREDICTION_KEY_CONTEXT: &str = "edit_prediction";
pub(crate) const EDIT_PREDICTION_CONFLICT_KEY_CONTEXT: &str = "edit_prediction_conflict";
@@ -1190,7 +1189,6 @@ pub struct Editor {
inline_value_cache: InlineValueCache,
selection_drag_state: SelectionDragState,
colors: Option<LspColorData>,
refresh_colors_task: Task<()>,
folding_newlines: Task<()>,
pub lookup_key: Option<Box<dyn Any + Send + Sync>>,
}
@@ -2246,7 +2244,6 @@ impl Editor {
tasks_update_task: None,
pull_diagnostics_task: Task::ready(()),
colors: None,
refresh_colors_task: Task::ready(()),
next_color_inlay_id: 0,
linked_edit_ranges: Default::default(),
in_project_search: false,
@@ -3514,46 +3511,26 @@ impl Editor {
) {
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
let tail = self.selections.newest::<usize>(cx).tail();
let click_count = click_count.max(match self.selections.select_mode() {
SelectMode::Character => 1,
SelectMode::Word(_) => 2,
SelectMode::Line(_) => 3,
SelectMode::All => 4,
});
self.begin_selection(position, false, click_count, window, cx);
let position = position.to_offset(&display_map, Bias::Left);
let tail_anchor = display_map.buffer_snapshot().anchor_before(tail);
let current_selection = match self.selections.select_mode() {
SelectMode::Character | SelectMode::All => tail_anchor..tail_anchor,
SelectMode::Word(range) | SelectMode::Line(range) => range.clone(),
};
let mut pending_selection = self
.selections
.pending_anchor()
.cloned()
.expect("extend_selection not called with pending selection");
if pending_selection
.start
.cmp(&current_selection.start, display_map.buffer_snapshot())
== Ordering::Greater
{
pending_selection.start = current_selection.start;
}
if pending_selection
.end
.cmp(&current_selection.end, display_map.buffer_snapshot())
== Ordering::Less
{
pending_selection.end = current_selection.end;
if position >= tail {
pending_selection.start = tail_anchor;
} else {
pending_selection.end = tail_anchor;
pending_selection.reversed = true;
}
let mut pending_mode = self.selections.pending_mode().unwrap();
match &mut pending_mode {
SelectMode::Word(range) | SelectMode::Line(range) => *range = current_selection,
SelectMode::Word(range) | SelectMode::Line(range) => *range = tail_anchor..tail_anchor,
_ => {}
}
@@ -3564,8 +3541,7 @@ impl Editor {
};
self.change_selections(effects, window, cx, |s| {
s.set_pending(pending_selection.clone(), pending_mode);
s.set_is_extending(true);
s.set_pending(pending_selection.clone(), pending_mode)
});
}
@@ -3834,16 +3810,11 @@ impl Editor {
fn end_selection(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.columnar_selection_state.take();
if let Some(pending_mode) = self.selections.pending_mode() {
if self.selections.pending_anchor().is_some() {
let selections = self.selections.all::<usize>(cx);
self.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select(selections);
s.clear_pending();
if s.is_extending() {
s.set_is_extending(false);
} else {
s.set_select_mode(pending_mode);
}
});
}
}
@@ -3899,9 +3870,6 @@ impl Editor {
}
})
.collect::<Vec<_>>();
if selection_ranges.is_empty() {
return;
}
let ranges = match columnar_state {
ColumnarSelectionState::FromMouse { .. } => {
@@ -5246,7 +5214,15 @@ impl Editor {
if enabled {
(InvalidationStrategy::RefreshRequested, None)
} else {
self.clear_inlay_hints(cx);
self.splice_inlays(
&self
.visible_inlay_hints(cx)
.iter()
.map(|inlay| inlay.id)
.collect::<Vec<InlayId>>(),
Vec::new(),
cx,
);
return;
}
}
@@ -5258,7 +5234,15 @@ impl Editor {
if enabled {
(InvalidationStrategy::RefreshRequested, None)
} else {
self.clear_inlay_hints(cx);
self.splice_inlays(
&self
.visible_inlay_hints(cx)
.iter()
.map(|inlay| inlay.id)
.collect::<Vec<InlayId>>(),
Vec::new(),
cx,
);
return;
}
} else {
@@ -5269,7 +5253,7 @@ impl Editor {
match self.inlay_hint_cache.update_settings(
&self.buffer,
new_settings,
self.visible_inlay_hints(cx).cloned().collect::<Vec<_>>(),
self.visible_inlay_hints(cx),
cx,
) {
ControlFlow::Break(Some(InlaySplice {
@@ -5291,8 +5275,8 @@ impl Editor {
{
self.splice_inlays(&to_remove, to_insert, cx);
}
self.display_map.update(cx, |display_map, cx| {
display_map.remove_inlays_for_excerpts(&excerpts_removed, cx)
self.display_map.update(cx, |display_map, _| {
display_map.remove_inlays_for_excerpts(&excerpts_removed)
});
return;
}
@@ -5319,25 +5303,13 @@ impl Editor {
}
}
pub fn clear_inlay_hints(&self, cx: &mut Context<Editor>) {
self.splice_inlays(
&self
.visible_inlay_hints(cx)
.map(|inlay| inlay.id)
.collect::<Vec<_>>(),
Vec::new(),
cx,
);
}
fn visible_inlay_hints<'a>(
&'a self,
cx: &'a Context<Editor>,
) -> impl Iterator<Item = &'a Inlay> {
fn visible_inlay_hints(&self, cx: &Context<Editor>) -> Vec<Inlay> {
self.display_map
.read(cx)
.current_inlays()
.filter(move |inlay| matches!(inlay.id, InlayId::Hint(_)))
.cloned()
.collect()
}
pub fn visible_excerpts(
@@ -7004,7 +6976,6 @@ impl Editor {
) else {
return Vec::default();
};
let query_range = query_range.to_anchors(&multi_buffer_snapshot);
for (buffer_snapshot, search_range, excerpt_id) in buffer_ranges {
match_ranges.extend(
regex
@@ -10499,33 +10470,29 @@ impl Editor {
let buffer = display_map.buffer_snapshot();
let mut edit_start = ToOffset::to_offset(&Point::new(rows.start.0, 0), buffer);
let (edit_end, target_row) = if buffer.max_point().row >= rows.end.0 {
let edit_end = if buffer.max_point().row >= rows.end.0 {
// If there's a line after the range, delete the \n from the end of the row range
(
ToOffset::to_offset(&Point::new(rows.end.0, 0), buffer),
rows.end,
)
ToOffset::to_offset(&Point::new(rows.end.0, 0), buffer)
} else {
// If there isn't a line after the range, delete the \n from the line before the
// start of the row range
edit_start = edit_start.saturating_sub(1);
(buffer.len(), rows.start.previous_row())
buffer.len()
};
let text_layout_details = self.text_layout_details(window);
let x = display_map.x_for_display_point(
let (cursor, goal) = movement::down_by_rows(
&display_map,
selection.head().to_display_point(&display_map),
&text_layout_details,
rows.len() as u32,
selection.goal,
false,
&self.text_layout_details(window),
);
let row = Point::new(target_row.0, 0)
.to_display_point(&display_map)
.row();
let column = display_map.display_column_for_x(row, x, &text_layout_details);
new_cursors.push((
selection.id,
buffer.anchor_after(DisplayPoint::new(row, column).to_point(&display_map)),
SelectionGoal::None,
buffer.anchor_after(cursor.to_point(&display_map)),
goal,
));
edit_ranges.push(edit_start..edit_end);
}
@@ -14452,10 +14419,6 @@ impl Editor {
let last_selection = selections.iter().max_by_key(|s| s.id).unwrap();
let mut next_selected_range = None;
// Collect and sort selection ranges for efficient overlap checking
let mut selection_ranges: Vec<_> = selections.iter().map(|s| s.range()).collect();
selection_ranges.sort_by_key(|r| r.start);
let bytes_after_last_selection =
buffer.bytes_in_range(last_selection.end..buffer.len());
let bytes_before_first_selection = buffer.bytes_in_range(0..first_selection.start);
@@ -14477,20 +14440,11 @@ impl Editor {
|| (!buffer.is_inside_word(offset_range.start, None)
&& !buffer.is_inside_word(offset_range.end, None))
{
// Use binary search to check for overlap (O(log n))
let overlaps = selection_ranges
.binary_search_by(|range| {
if range.end <= offset_range.start {
std::cmp::Ordering::Less
} else if range.start >= offset_range.end {
std::cmp::Ordering::Greater
} else {
std::cmp::Ordering::Equal
}
})
.is_ok();
if !overlaps {
// TODO: This is n^2, because we might check all the selections
if !selections
.iter()
.any(|selection| selection.range().overlaps(&offset_range))
{
next_selected_range = Some(offset_range);
break;
}
@@ -24691,7 +24645,7 @@ impl Render for MissingEditPredictionKeybindingTooltip {
.items_end()
.w_full()
.child(Button::new("open-keymap", "Assign Keybinding").size(ButtonSize::Compact).on_click(|_ev, window, cx| {
window.dispatch_action(zed_actions::OpenKeymapFile.boxed_clone(), cx)
window.dispatch_action(zed_actions::OpenKeymap.boxed_clone(), cx)
}))
.child(Button::new("see-docs", "See Docs").size(ButtonSize::Compact).on_click(|_ev, _window, cx| {
cx.open_url("https://zed.dev/docs/completions#edit-predictions-missing-keybinding");

View File

@@ -27,6 +27,7 @@ use language::{
LanguageConfigOverride, LanguageMatcher, LanguageName, Override, Point,
language_settings::{
CompletionSettingsContent, FormatterList, LanguageSettingsContent, LspInsertMode,
SelectedFormatter,
},
tree_sitter_python,
};
@@ -618,93 +619,6 @@ fn test_movement_actions_with_pending_selection(cx: &mut TestAppContext) {
});
}
#[gpui::test]
fn test_extending_selection(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let editor = cx.add_window(|window, cx| {
let buffer = MultiBuffer::build_simple("aaa bbb ccc ddd eee", cx);
build_editor(buffer, window, cx)
});
_ = editor.update(cx, |editor, window, cx| {
editor.begin_selection(DisplayPoint::new(DisplayRow(0), 5), false, 1, window, cx);
editor.end_selection(window, cx);
assert_eq!(
editor.selections.display_ranges(cx),
[DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)]
);
editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 1, window, cx);
editor.end_selection(window, cx);
assert_eq!(
editor.selections.display_ranges(cx),
[DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 10)]
);
editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 1, window, cx);
editor.end_selection(window, cx);
editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 2, window, cx);
assert_eq!(
editor.selections.display_ranges(cx),
[DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 11)]
);
editor.update_selection(
DisplayPoint::new(DisplayRow(0), 1),
0,
gpui::Point::<f32>::default(),
window,
cx,
);
editor.end_selection(window, cx);
assert_eq!(
editor.selections.display_ranges(cx),
[DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 0)]
);
editor.begin_selection(DisplayPoint::new(DisplayRow(0), 5), true, 1, window, cx);
editor.end_selection(window, cx);
editor.begin_selection(DisplayPoint::new(DisplayRow(0), 5), true, 2, window, cx);
editor.end_selection(window, cx);
assert_eq!(
editor.selections.display_ranges(cx),
[DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 7)]
);
editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 1, window, cx);
assert_eq!(
editor.selections.display_ranges(cx),
[DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 11)]
);
editor.update_selection(
DisplayPoint::new(DisplayRow(0), 6),
0,
gpui::Point::<f32>::default(),
window,
cx,
);
assert_eq!(
editor.selections.display_ranges(cx),
[DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 7)]
);
editor.update_selection(
DisplayPoint::new(DisplayRow(0), 1),
0,
gpui::Point::<f32>::default(),
window,
cx,
);
editor.end_selection(window, cx);
assert_eq!(
editor.selections.display_ranges(cx),
[DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 0)]
);
});
}
#[gpui::test]
fn test_clone(cx: &mut TestAppContext) {
init_test(cx, |_| {});
@@ -4386,8 +4300,8 @@ fn test_delete_line(cx: &mut TestAppContext) {
assert_eq!(
editor.selections.display_ranges(cx),
vec![
DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
]
);
});
@@ -4409,24 +4323,6 @@ fn test_delete_line(cx: &mut TestAppContext) {
vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
);
});
let editor = cx.add_window(|window, cx| {
let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n\njkl\nmno", cx);
build_editor(buffer, window, cx)
});
_ = editor.update(cx, |editor, window, cx| {
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_display_ranges([
DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(2), 1)
])
});
editor.delete_line(&DeleteLine, window, cx);
assert_eq!(editor.display_text(cx), "\njkl\nmno");
assert_eq!(
editor.selections.display_ranges(cx),
vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
);
});
}
#[gpui::test]
@@ -11907,8 +11803,8 @@ async fn test_range_format_respects_language_tab_size_override(cx: &mut TestAppC
#[gpui::test]
async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
init_test(cx, |settings| {
settings.defaults.formatter = Some(FormatterList::Single(Formatter::LanguageServer(
settings::LanguageServerFormatterSpecifier::Current,
settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
Formatter::LanguageServer { name: None },
)))
});
@@ -12033,11 +11929,11 @@ async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
async fn test_multiple_formatters(cx: &mut TestAppContext) {
init_test(cx, |settings| {
settings.defaults.remove_trailing_whitespace_on_save = Some(true);
settings.defaults.formatter = Some(FormatterList::Vec(vec![
Formatter::LanguageServer(settings::LanguageServerFormatterSpecifier::Current),
settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Vec(vec![
Formatter::LanguageServer { name: None },
Formatter::CodeAction("code-action-1".into()),
Formatter::CodeAction("code-action-2".into()),
]))
])))
});
let fs = FakeFs::new(cx.executor());
@@ -12292,9 +12188,9 @@ async fn test_multiple_formatters(cx: &mut TestAppContext) {
#[gpui::test]
async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
init_test(cx, |settings| {
settings.defaults.formatter = Some(FormatterList::Vec(vec![Formatter::LanguageServer(
settings::LanguageServerFormatterSpecifier::Current,
)]))
settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Vec(vec![
Formatter::LanguageServer { name: None },
])))
});
let fs = FakeFs::new(cx.executor());
@@ -12497,7 +12393,7 @@ async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
#[gpui::test]
async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
init_test(cx, |settings| {
settings.defaults.formatter = Some(FormatterList::default())
settings.defaults.formatter = Some(SelectedFormatter::Auto)
});
let mut cx = EditorLspTestContext::new_rust(
@@ -18186,7 +18082,9 @@ fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
#[gpui::test]
async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
init_test(cx, |settings| {
settings.defaults.formatter = Some(FormatterList::Single(Formatter::Prettier))
settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
Formatter::Prettier,
)))
});
let fs = FakeFs::new(cx.executor());
@@ -18253,7 +18151,7 @@ async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
);
update_test_language_settings(cx, |settings| {
settings.defaults.formatter = Some(FormatterList::default())
settings.defaults.formatter = Some(SelectedFormatter::Auto)
});
let format = editor.update_in(cx, |editor, window, cx| {
editor.perform_format(
@@ -25808,7 +25706,7 @@ async fn test_document_colors(cx: &mut TestAppContext) {
.set_request_handler::<lsp::request::DocumentColor, _, _>(move |_, _| async move {
panic!("Should not be called");
});
cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
cx.executor().advance_clock(Duration::from_millis(100));
color_request_handle.next().await.unwrap();
cx.run_until_parked();
assert_eq!(
@@ -25892,9 +25790,9 @@ async fn test_document_colors(cx: &mut TestAppContext) {
color_request_handle.next().await.unwrap();
cx.run_until_parked();
assert_eq!(
2,
3,
requests_made.load(atomic::Ordering::Acquire),
"Should query for colors once per save (deduplicated) and once per formatting after save"
"Should query for colors once per save and once per formatting after save"
);
drop(editor);
@@ -25915,7 +25813,7 @@ async fn test_document_colors(cx: &mut TestAppContext) {
.unwrap();
close.await.unwrap();
assert_eq!(
2,
3,
requests_made.load(atomic::Ordering::Acquire),
"After saving and closing all editors, no extra requests should be made"
);
@@ -25935,7 +25833,7 @@ async fn test_document_colors(cx: &mut TestAppContext) {
})
})
.unwrap();
cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
cx.executor().advance_clock(Duration::from_millis(100));
cx.run_until_parked();
let editor = workspace
.update(cx, |workspace, _, cx| {
@@ -25946,9 +25844,9 @@ async fn test_document_colors(cx: &mut TestAppContext) {
.expect("Should be an editor")
})
.unwrap();
color_request_handle.next().await.unwrap();
assert_eq!(
2,
3,
requests_made.load(atomic::Ordering::Acquire),
"Cache should be reused on buffer close and reopen"
);
@@ -25989,11 +25887,10 @@ async fn test_document_colors(cx: &mut TestAppContext) {
});
save.await.unwrap();
cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
empty_color_request_handle.next().await.unwrap();
cx.run_until_parked();
assert_eq!(
3,
4,
requests_made.load(atomic::Ordering::Acquire),
"Should query for colors once per save only, as formatting was not requested"
);

View File

@@ -651,6 +651,7 @@ impl EditorElement {
fn mouse_left_down(
editor: &mut Editor,
event: &MouseDownEvent,
hovered_hunk: Option<Range<Anchor>>,
position_map: &PositionMap,
line_numbers: &HashMap<MultiBufferRow, LineNumberLayout>,
window: &mut Window,
@@ -666,20 +667,7 @@ impl EditorElement {
let mut click_count = event.click_count;
let mut modifiers = event.modifiers;
if let Some(hovered_hunk) =
position_map
.display_hunks
.iter()
.find_map(|(hunk, hunk_hitbox)| match hunk {
DisplayDiffHunk::Folded { .. } => None,
DisplayDiffHunk::Unfolded {
multi_buffer_range, ..
} => hunk_hitbox
.as_ref()
.is_some_and(|hitbox| hitbox.is_hovered(window))
.then(|| multi_buffer_range.clone()),
})
{
if let Some(hovered_hunk) = hovered_hunk {
editor.toggle_single_diff_hunk(hovered_hunk, cx);
cx.notify();
return;
@@ -693,7 +681,6 @@ impl EditorElement {
.drag_and_drop_selection
.enabled
&& click_count == 1
&& !modifiers.shift
{
let newest_anchor = editor.selections.newest_anchor();
let snapshot = editor.snapshot(window, cx);
@@ -752,35 +739,6 @@ impl EditorElement {
}
}
if !is_singleton {
let display_row = (ScrollPixelOffset::from(
(event.position - gutter_hitbox.bounds.origin).y / position_map.line_height,
) + position_map.scroll_position.y) as u32;
let multi_buffer_row = position_map
.snapshot
.display_point_to_point(DisplayPoint::new(DisplayRow(display_row), 0), Bias::Right)
.row;
if line_numbers
.get(&MultiBufferRow(multi_buffer_row))
.and_then(|line_number| line_number.hitbox.as_ref())
.is_some_and(|hitbox| hitbox.contains(&event.position))
{
let line_offset_from_top = display_row - position_map.scroll_position.y as u32;
editor.open_excerpts_common(
Some(JumpData::MultiBufferRow {
row: MultiBufferRow(multi_buffer_row),
line_offset_from_top,
}),
modifiers.alt,
window,
cx,
);
cx.stop_propagation();
return;
}
}
let position = point_for_position.previous_valid;
if let Some(mode) = Editor::columnar_selection_mode(&modifiers, cx) {
editor.select(
@@ -818,6 +776,34 @@ impl EditorElement {
);
}
cx.stop_propagation();
if !is_singleton {
let display_row = (ScrollPixelOffset::from(
(event.position - gutter_hitbox.bounds.origin).y / position_map.line_height,
) + position_map.scroll_position.y) as u32;
let multi_buffer_row = position_map
.snapshot
.display_point_to_point(DisplayPoint::new(DisplayRow(display_row), 0), Bias::Right)
.row;
if line_numbers
.get(&MultiBufferRow(multi_buffer_row))
.and_then(|line_number| line_number.hitbox.as_ref())
.is_some_and(|hitbox| hitbox.contains(&event.position))
{
let line_offset_from_top = display_row - position_map.scroll_position.y as u32;
editor.open_excerpts_common(
Some(JumpData::MultiBufferRow {
row: MultiBufferRow(multi_buffer_row),
line_offset_from_top,
}),
modifiers.alt,
window,
cx,
);
cx.stop_propagation();
}
}
}
fn mouse_right_down(
@@ -7259,6 +7245,26 @@ impl EditorElement {
window.on_mouse_event({
let position_map = layout.position_map.clone();
let editor = self.editor.clone();
let diff_hunk_range =
layout
.display_hunks
.iter()
.find_map(|(hunk, hunk_hitbox)| match hunk {
DisplayDiffHunk::Folded { .. } => None,
DisplayDiffHunk::Unfolded {
multi_buffer_range, ..
} => {
if hunk_hitbox
.as_ref()
.map(|hitbox| hitbox.is_hovered(window))
.unwrap_or(false)
{
Some(multi_buffer_range.clone())
} else {
None
}
}
});
let line_numbers = layout.line_numbers.clone();
move |event: &MouseDownEvent, phase, window, cx| {
@@ -7275,6 +7281,7 @@ impl EditorElement {
Self::mouse_left_down(
editor,
event,
diff_hunk_range.clone(),
&position_map,
line_numbers.as_ref(),
window,

View File

@@ -307,6 +307,7 @@ pub fn update_inlay_link_and_hover_points(
buffer_snapshot.anchor_after(point_for_position.next_valid.to_point(snapshot));
if let Some(hovered_hint) = editor
.visible_inlay_hints(cx)
.into_iter()
.skip_while(|hint| {
hint.position
.cmp(&previous_valid_anchor, &buffer_snapshot)
@@ -493,15 +494,22 @@ pub fn show_link_definition(
}
let trigger_anchor = trigger_point.anchor();
let anchor = snapshot.buffer_snapshot().anchor_before(*trigger_anchor);
let Some(buffer) = editor.buffer().read(cx).buffer_for_anchor(anchor, cx) else {
let Some((buffer, buffer_position)) = editor
.buffer
.read(cx)
.text_anchor_for_position(*trigger_anchor, cx)
else {
return;
};
let Anchor {
excerpt_id,
text_anchor,
..
} = anchor;
let Some((excerpt_id, _, _)) = editor
.buffer()
.read(cx)
.excerpt_containing(*trigger_anchor, cx)
else {
return;
};
let same_kind = hovered_link_state.preferred_kind == preferred_kind
|| hovered_link_state
.links
@@ -531,7 +539,7 @@ pub fn show_link_definition(
async move {
let result = match &trigger_point {
TriggerPoint::Text(_) => {
if let Some((url_range, url)) = find_url(&buffer, text_anchor, cx.clone()) {
if let Some((url_range, url)) = find_url(&buffer, buffer_position, cx.clone()) {
this.read_with(cx, |_, _| {
let range = maybe!({
let start =
@@ -543,7 +551,7 @@ pub fn show_link_definition(
})
.ok()
} else if let Some((filename_range, filename)) =
find_file(&buffer, project.clone(), text_anchor, cx).await
find_file(&buffer, project.clone(), buffer_position, cx).await
{
let range = maybe!({
let start =
@@ -555,7 +563,7 @@ pub fn show_link_definition(
Some((range, vec![HoverLink::File(filename)]))
} else if let Some(provider) = provider {
let task = cx.update(|_, cx| {
provider.definitions(&buffer, text_anchor, preferred_kind, cx)
provider.definitions(&buffer, buffer_position, preferred_kind, cx)
})?;
if let Some(task) = task {
task.await.ok().flatten().map(|definition_result| {

View File

@@ -1013,7 +1013,7 @@ fn fetch_and_update_hints(
.cloned()
})?;
let visible_hints = editor.update(cx, |editor, cx| editor.visible_inlay_hints(cx).cloned().collect::<Vec<_>>())?;
let visible_hints = editor.update(cx, |editor, cx| editor.visible_inlay_hints(cx))?;
let new_hints = match inlay_hints_fetch_task {
Some(fetch_task) => {
log::debug!(
@@ -3570,6 +3570,7 @@ pub mod tests {
pub fn visible_hint_labels(editor: &Editor, cx: &Context<Editor>) -> Vec<String> {
editor
.visible_inlay_hints(cx)
.into_iter()
.map(|hint| hint.text().to_string())
.collect()
}

View File

@@ -13,8 +13,8 @@ use ui::{App, Context, Window};
use util::post_inc;
use crate::{
DisplayPoint, Editor, EditorSettings, EditorSnapshot, FETCH_COLORS_DEBOUNCE_TIMEOUT, InlayId,
InlaySplice, RangeToAnchorExt, display_map::Inlay, editor_settings::DocumentColorsRenderMode,
DisplayPoint, Editor, EditorSettings, EditorSnapshot, InlayId, InlaySplice, RangeToAnchorExt,
display_map::Inlay, editor_settings::DocumentColorsRenderMode,
};
#[derive(Debug)]
@@ -193,12 +193,7 @@ impl Editor {
})
.collect::<Vec<_>>()
});
self.refresh_colors_task = cx.spawn(async move |editor, cx| {
cx.background_executor()
.timer(FETCH_COLORS_DEBOUNCE_TIMEOUT)
.await;
cx.spawn(async move |editor, cx| {
let all_colors = join_all(all_colors_task).await;
if all_colors.is_empty() {
return;
@@ -425,6 +420,7 @@ impl Editor {
}
})
.ok();
});
})
.detach();
}
}

View File

@@ -35,8 +35,6 @@ pub struct SelectionsCollection {
disjoint: Arc<[Selection<Anchor>]>,
/// A pending selection, such as when the mouse is being dragged
pending: Option<PendingSelection>,
select_mode: SelectMode,
is_extending: bool,
}
impl SelectionsCollection {
@@ -57,8 +55,6 @@ impl SelectionsCollection {
},
mode: SelectMode::Character,
}),
select_mode: SelectMode::Character,
is_extending: false,
}
}
@@ -460,22 +456,6 @@ impl SelectionsCollection {
pub fn set_line_mode(&mut self, line_mode: bool) {
self.line_mode = line_mode;
}
pub fn select_mode(&self) -> &SelectMode {
&self.select_mode
}
pub fn set_select_mode(&mut self, select_mode: SelectMode) {
self.select_mode = select_mode;
}
pub fn is_extending(&self) -> bool {
self.is_extending
}
pub fn set_is_extending(&mut self, is_extending: bool) {
self.is_extending = is_extending;
}
}
pub struct MutableSelectionsCollection<'a> {
@@ -610,32 +590,21 @@ impl<'a> MutableSelectionsCollection<'a> {
self.select(selections);
}
pub fn select<T>(&mut self, selections: Vec<Selection<T>>)
pub fn select<T>(&mut self, mut selections: Vec<Selection<T>>)
where
T: ToOffset + std::marker::Copy + std::fmt::Debug,
T: ToOffset + ToPoint + Ord + std::marker::Copy + std::fmt::Debug,
{
let buffer = self.buffer.read(self.cx).snapshot(self.cx);
let mut selections = selections
.into_iter()
.map(|selection| selection.map(|it| it.to_offset(&buffer)))
.map(|mut selection| {
if selection.start > selection.end {
mem::swap(&mut selection.start, &mut selection.end);
selection.reversed = true
}
selection
})
.collect::<Vec<_>>();
selections.sort_unstable_by_key(|s| s.start);
// Merge overlapping selections.
let mut i = 1;
while i < selections.len() {
if selections[i].start <= selections[i - 1].end {
if selections[i - 1].end >= selections[i].start {
let removed = selections.remove(i);
if removed.start < selections[i - 1].start {
selections[i - 1].start = removed.start;
}
if selections[i - 1].end < removed.end {
if removed.end > selections[i - 1].end {
selections[i - 1].end = removed.end;
}
} else {
@@ -979,10 +948,13 @@ impl DerefMut for MutableSelectionsCollection<'_> {
}
}
fn selection_to_anchor_selection(
selection: Selection<usize>,
fn selection_to_anchor_selection<T>(
selection: Selection<T>,
buffer: &MultiBufferSnapshot,
) -> Selection<Anchor> {
) -> Selection<Anchor>
where
T: ToOffset + Ord,
{
let end_bias = if selection.start == selection.end {
Bias::Right
} else {
@@ -1020,7 +992,7 @@ fn resolve_selections_point<'a>(
})
}
/// Panics if passed selections are not in order
// Panics if passed selections are not in order
fn resolve_selections_display<'a>(
selections: impl 'a + IntoIterator<Item = &'a Selection<Anchor>>,
map: &'a DisplaySnapshot,
@@ -1052,7 +1024,7 @@ fn resolve_selections_display<'a>(
coalesce_selections(selections)
}
/// Panics if passed selections are not in order
// Panics if passed selections are not in order
pub(crate) fn resolve_selections<'a, D, I>(
selections: I,
map: &'a DisplaySnapshot,

View File

@@ -1,5 +1,5 @@
use crate::{
AnchorRangeExt, DisplayPoint, Editor, ExcerptId, MultiBuffer, MultiBufferSnapshot, RowExt,
AnchorRangeExt, DisplayPoint, Editor, MultiBuffer, RowExt,
display_map::{HighlightKey, ToDisplayPoint},
};
use buffer_diff::DiffHunkStatusKind;
@@ -24,7 +24,6 @@ use std::{
atomic::{AtomicUsize, Ordering},
},
};
use text::Selection;
use util::{
assert_set_eq,
test::{generate_marked_text, marked_text_ranges},
@@ -389,23 +388,6 @@ impl EditorTestContext {
#[track_caller]
pub fn assert_excerpts_with_selections(&mut self, marked_text: &str) {
let actual_text = self.to_format_multibuffer_as_marked_text();
let fmt_additional_notes = || {
struct Format<'a, T: std::fmt::Display>(&'a str, &'a T);
impl<T: std::fmt::Display> std::fmt::Display for Format<'_, T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"\n\n----- EXPECTED: -----\n\n{}\n\n----- ACTUAL: -----\n\n{}\n\n",
self.0, self.1
)
}
}
Format(marked_text, &actual_text)
};
let expected_excerpts = marked_text
.strip_prefix("[EXCERPT]\n")
.unwrap()
@@ -426,10 +408,9 @@ impl EditorTestContext {
assert!(
excerpts.len() == expected_excerpts.len(),
"should have {} excerpts, got {}{}",
"should have {} excerpts, got {}",
expected_excerpts.len(),
excerpts.len(),
fmt_additional_notes(),
excerpts.len()
);
for (ix, (excerpt_id, snapshot, range)) in excerpts.into_iter().enumerate() {
@@ -443,25 +424,18 @@ impl EditorTestContext {
if !expected_selections.is_empty() {
assert!(
is_selected,
"excerpt {ix} should contain selections. got {:?}{}",
"excerpt {ix} should be selected. got {:?}",
self.editor_state(),
fmt_additional_notes(),
);
} else {
assert!(
!is_selected,
"excerpt {ix} should not contain selections, got: {selections:?}{}",
fmt_additional_notes(),
"excerpt {ix} should not be selected, got: {selections:?}",
);
}
continue;
}
assert!(
!is_folded,
"excerpt {} should not be folded{}",
ix,
fmt_additional_notes()
);
assert!(!is_folded, "excerpt {} should not be folded", ix);
assert_eq!(
multibuffer_snapshot
.text_for_range(Anchor::range_in_buffer(
@@ -470,9 +444,7 @@ impl EditorTestContext {
range.context.clone()
))
.collect::<String>(),
expected_text,
"{}",
fmt_additional_notes(),
expected_text
);
let selections = selections
@@ -488,38 +460,13 @@ impl EditorTestContext {
.collect::<Vec<_>>();
// todo: selections that cross excerpt boundaries..
assert_eq!(
selections,
expected_selections,
"excerpt {} has incorrect selections{}",
selections, expected_selections,
"excerpt {} has incorrect selections",
ix,
fmt_additional_notes()
);
}
}
fn to_format_multibuffer_as_marked_text(&mut self) -> FormatMultiBufferAsMarkedText {
let (multibuffer_snapshot, selections, excerpts) = self.update_editor(|editor, _, cx| {
let multibuffer_snapshot = editor.buffer.read(cx).snapshot(cx);
let selections = editor.selections.disjoint_anchors_arc().to_vec();
let excerpts = multibuffer_snapshot
.excerpts()
.map(|(e_id, snapshot, range)| {
let is_folded = editor.is_buffer_folded(snapshot.remote_id(), cx);
(e_id, snapshot.clone(), range, is_folded)
})
.collect::<Vec<_>>();
(multibuffer_snapshot, selections, excerpts)
});
FormatMultiBufferAsMarkedText {
multibuffer_snapshot,
selections,
excerpts,
}
}
/// Make an assertion about the editor's text and the ranges and directions
/// of its selections using a string containing embedded range markers.
///
@@ -624,63 +571,6 @@ impl EditorTestContext {
}
}
struct FormatMultiBufferAsMarkedText {
multibuffer_snapshot: MultiBufferSnapshot,
selections: Vec<Selection<Anchor>>,
excerpts: Vec<(ExcerptId, BufferSnapshot, ExcerptRange<text::Anchor>, bool)>,
}
impl std::fmt::Display for FormatMultiBufferAsMarkedText {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let Self {
multibuffer_snapshot,
selections,
excerpts,
} = self;
for (excerpt_id, snapshot, range, is_folded) in excerpts.into_iter() {
write!(f, "[EXCERPT]\n")?;
if *is_folded {
write!(f, "[FOLDED]\n")?;
}
let mut text = multibuffer_snapshot
.text_for_range(Anchor::range_in_buffer(
*excerpt_id,
snapshot.remote_id(),
range.context.clone(),
))
.collect::<String>();
let selections = selections
.iter()
.filter(|&s| s.head().excerpt_id == *excerpt_id)
.map(|s| {
let head = text::ToOffset::to_offset(&s.head().text_anchor, &snapshot)
- text::ToOffset::to_offset(&range.context.start, &snapshot);
let tail = text::ToOffset::to_offset(&s.head().text_anchor, &snapshot)
- text::ToOffset::to_offset(&range.context.start, &snapshot);
tail..head
})
.rev()
.collect::<Vec<_>>();
for selection in selections {
if selection.is_empty() {
text.insert(selection.start, 'ˇ');
continue;
}
text.insert(selection.end, '»');
text.insert(selection.start, '«');
}
write!(f, "{text}")?;
}
Ok(())
}
}
#[track_caller]
pub fn assert_state_with_diff(
editor: &Entity<Editor>,

View File

@@ -1,4 +1,4 @@
use anyhow::{Context as _, Result, anyhow, bail};
use anyhow::{Context as _, Result, bail};
use collections::{BTreeMap, HashMap};
use fs::Fs;
use language::LanguageName;
@@ -226,9 +226,8 @@ impl ExtensionManifest {
.load(&extension_manifest_path)
.await
.with_context(|| format!("failed to load {extension_name} extension.toml"))?;
toml::from_str(&manifest_content).map_err(|err| {
anyhow!("Invalid extension.toml for extension {extension_name}:\n{err}")
})
toml::from_str(&manifest_content)
.with_context(|| format!("invalid extension.toml for extension {extension_name}"))
}
}
}

View File

@@ -17,3 +17,9 @@ pub struct PanicFeatureFlag;
impl FeatureFlag for PanicFeatureFlag {
const NAME: &'static str = "panic";
}
pub struct CodexAcpFeatureFlag;
impl FeatureFlag for CodexAcpFeatureFlag {
const NAME: &'static str = "codex-acp";
}

View File

@@ -16,12 +16,14 @@ test-support = []
[dependencies]
gpui.workspace = true
menu.workspace = true
system_specs.workspace = true
ui.workspace = true
urlencoding.workspace = true
util.workspace = true
workspace-hack.workspace = true
workspace.workspace = true
zed_actions.workspace = true
workspace-hack.workspace = true
[dev-dependencies]
editor = { workspace = true, features = ["test-support"] }

View File

@@ -2,13 +2,19 @@ use gpui::{App, ClipboardItem, PromptLevel, actions};
use system_specs::{CopySystemSpecsIntoClipboard, SystemSpecs};
use util::ResultExt;
use workspace::Workspace;
use zed_actions::feedback::{EmailZed, FileBugReport, RequestFeature};
use zed_actions::feedback::FileBugReport;
pub mod feedback_modal;
actions!(
zed,
[
/// Opens email client to send feedback to Zed support.
EmailZed,
/// Opens the Zed repository on GitHub.
OpenZedRepo,
/// Opens the feature request form.
RequestFeature,
]
);
@@ -42,7 +48,11 @@ fn email_body(specs: &SystemSpecs) -> String {
}
pub fn init(cx: &mut App) {
cx.observe_new(|workspace: &mut Workspace, _, _| {
cx.observe_new(|workspace: &mut Workspace, window, cx| {
let Some(window) = window else {
return;
};
feedback_modal::FeedbackModal::register(workspace, window, cx);
workspace
.register_action(|_, _: &CopySystemSpecsIntoClipboard, window, cx| {
let specs = SystemSpecs::new(window, cx);

View File

@@ -0,0 +1,113 @@
use gpui::{App, Context, DismissEvent, EventEmitter, FocusHandle, Focusable, Render, Window};
use ui::{IconPosition, prelude::*};
use workspace::{ModalView, Workspace};
use zed_actions::feedback::GiveFeedback;
use crate::{EmailZed, FileBugReport, OpenZedRepo, RequestFeature};
pub struct FeedbackModal {
focus_handle: FocusHandle,
}
impl Focusable for FeedbackModal {
fn focus_handle(&self, _: &App) -> FocusHandle {
self.focus_handle.clone()
}
}
impl EventEmitter<DismissEvent> for FeedbackModal {}
impl ModalView for FeedbackModal {}
impl FeedbackModal {
pub fn register(workspace: &mut Workspace, _: &mut Window, cx: &mut Context<Workspace>) {
let _handle = cx.entity().downgrade();
workspace.register_action(move |workspace, _: &GiveFeedback, window, cx| {
workspace.toggle_modal(window, cx, move |_, cx| FeedbackModal::new(cx));
});
}
pub fn new(cx: &mut Context<Self>) -> Self {
Self {
focus_handle: cx.focus_handle(),
}
}
fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
cx.emit(DismissEvent)
}
}
impl Render for FeedbackModal {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let open_zed_repo =
cx.listener(|_, _, window, cx| window.dispatch_action(Box::new(OpenZedRepo), cx));
v_flex()
.key_context("GiveFeedback")
.on_action(cx.listener(Self::cancel))
.elevation_3(cx)
.w_96()
.h_auto()
.p_4()
.gap_2()
.child(
h_flex()
.w_full()
.justify_between()
.child(Headline::new("Give Feedback"))
.child(
IconButton::new("close-btn", IconName::Close)
.icon_color(Color::Muted)
.on_click(cx.listener(move |_, _, window, cx| {
cx.spawn_in(window, async move |this, cx| {
this.update(cx, |_, cx| cx.emit(DismissEvent)).ok();
})
.detach();
})),
),
)
.child(Label::new("Thanks for using Zed! To share your experience with us, reach for the channel that's the most appropriate:"))
.child(
Button::new("file-a-bug-report", "File a Bug Report")
.full_width()
.icon(IconName::Debug)
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
.icon_position(IconPosition::Start)
.on_click(cx.listener(|_, _, window, cx| {
window.dispatch_action(Box::new(FileBugReport), cx);
})),
)
.child(
Button::new("request-a-feature", "Request a Feature")
.full_width()
.icon(IconName::Sparkle)
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
.icon_position(IconPosition::Start)
.on_click(cx.listener(|_, _, window, cx| {
window.dispatch_action(Box::new(RequestFeature), cx);
})),
)
.child(
Button::new("send-us_an-email", "Send an Email")
.full_width()
.icon(IconName::Envelope)
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
.icon_position(IconPosition::Start)
.on_click(cx.listener(|_, _, window, cx| {
window.dispatch_action(Box::new(EmailZed), cx);
})),
)
.child(
Button::new("zed_repository", "GitHub Repository")
.full_width()
.icon(IconName::Github)
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
.icon_position(IconPosition::Start)
.on_click(open_zed_repo),
)
}
}

View File

@@ -669,7 +669,7 @@ impl PickerDelegate for OpenPathDelegate {
) -> Option<Self::ListItem> {
let settings = FileFinderSettings::get_global(cx);
let candidate = self.get_entry(ix)?;
let mut match_positions = match &self.directory_state {
let match_positions = match &self.directory_state {
DirectoryState::List { .. } => self.string_matches.get(ix)?.positions.clone(),
DirectoryState::Create { user_input, .. } => {
if let Some(user_input) = user_input {
@@ -710,38 +710,29 @@ impl PickerDelegate for OpenPathDelegate {
});
match &self.directory_state {
DirectoryState::List { parent_path, .. } => {
let (label, indices) = if *parent_path == self.prompt_root {
match_positions.iter_mut().for_each(|position| {
*position += self.prompt_root.len();
});
(
format!("{}{}", self.prompt_root, candidate.path.string),
DirectoryState::List { parent_path, .. } => Some(
ListItem::new(ix)
.spacing(ListItemSpacing::Sparse)
.start_slot::<Icon>(file_icon)
.inset(true)
.toggle_state(selected)
.child(HighlightedLabel::new(
if parent_path == &self.prompt_root {
format!("{}{}", self.prompt_root, candidate.path.string)
} else if is_current_dir_candidate {
"open this directory".to_string()
} else {
candidate.path.string
},
match_positions,
)
} else if is_current_dir_candidate {
("open this directory".to_string(), vec![])
} else {
(candidate.path.string, match_positions)
};
Some(
ListItem::new(ix)
.spacing(ListItemSpacing::Sparse)
.start_slot::<Icon>(file_icon)
.inset(true)
.toggle_state(selected)
.child(HighlightedLabel::new(label, indices)),
)
}
)),
),
DirectoryState::Create {
parent_path,
user_input,
..
} => {
let (label, delta) = if *parent_path == self.prompt_root {
match_positions.iter_mut().for_each(|position| {
*position += self.prompt_root.len();
});
let (label, delta) = if parent_path == &self.prompt_root {
(
format!("{}{}", self.prompt_root, candidate.path.string),
self.prompt_root.len(),
@@ -749,10 +740,10 @@ impl PickerDelegate for OpenPathDelegate {
} else {
(candidate.path.string.clone(), 0)
};
let label_len = label.len();
let label_with_highlights = match user_input {
Some(user_input) => {
let label_len = label.len();
if user_input.file.string == candidate.path.string {
if user_input.exists {
let label = if user_input.is_dir {
@@ -781,10 +772,20 @@ impl PickerDelegate for OpenPathDelegate {
.into_any_element()
}
} else {
HighlightedLabel::new(label, match_positions).into_any_element()
let mut highlight_positions = match_positions;
highlight_positions.iter_mut().for_each(|position| {
*position += delta;
});
HighlightedLabel::new(label, highlight_positions).into_any_element()
}
}
None => HighlightedLabel::new(label, match_positions).into_any_element(),
None => {
let mut highlight_positions = match_positions;
highlight_positions.iter_mut().for_each(|position| {
*position += delta;
});
HighlightedLabel::new(label, highlight_positions).into_any_element()
}
};
Some(

View File

@@ -749,7 +749,6 @@ impl Fs for RealFs {
events
.into_iter()
.map(|event| {
log::trace!("fs path event: {event:?}");
let kind = if event.flags.contains(StreamFlags::ITEM_REMOVED) {
Some(PathEventKind::Removed)
} else if event.flags.contains(StreamFlags::ITEM_CREATED) {
@@ -807,7 +806,6 @@ impl Fs for RealFs {
// Check if path is a symlink and follow the target parent
if let Some(mut target) = self.read_link(path).await.ok() {
log::trace!("watch symlink {path:?} -> {target:?}");
// Check if symlink target is relative path, if so make it absolute
if target.is_relative()
&& let Some(parent) = path.parent()

View File

@@ -46,7 +46,6 @@ impl Drop for FsWatcher {
impl Watcher for FsWatcher {
fn add(&self, path: &std::path::Path) -> anyhow::Result<()> {
log::trace!("watcher add: {path:?}");
let tx = self.tx.clone();
let pending_paths = self.pending_path_events.clone();
@@ -64,15 +63,11 @@ impl Watcher for FsWatcher {
.next_back()
&& path.starts_with(watched_path.as_ref())
{
log::trace!(
"path to watch is covered by existing registration: {path:?}, {watched_path:?}"
);
return Ok(());
}
}
#[cfg(target_os = "linux")]
{
log::trace!("path to watch is already watched: {path:?}");
if self.registrations.lock().contains_key(path) {
return Ok(());
}
@@ -90,7 +85,6 @@ impl Watcher for FsWatcher {
let path = path.clone();
|g| {
g.add(path, mode, move |event: &notify::Event| {
log::trace!("watcher received event: {event:?}");
let kind = match event.kind {
EventKind::Create(_) => Some(PathEventKind::Created),
EventKind::Modify(_) => Some(PathEventKind::Changed),
@@ -132,7 +126,6 @@ impl Watcher for FsWatcher {
}
fn remove(&self, path: &std::path::Path) -> anyhow::Result<()> {
log::trace!("remove watched path: {path:?}");
let Some(registration) = self.registrations.lock().remove(path) else {
return Ok(());
};
@@ -222,7 +215,6 @@ static FS_WATCHER_INSTANCE: OnceLock<anyhow::Result<GlobalWatcher, notify::Error
OnceLock::new();
fn handle_event(event: Result<notify::Event, notify::Error>) {
log::trace!("global handle event: {event:?}");
// Filter out access events, which could lead to a weird bug on Linux after upgrading notify
// https://github.com/zed-industries/zed/actions/runs/14085230504/job/39449448832
let Some(event) = event

View File

@@ -32,7 +32,6 @@ impl MacWatcher {
impl Watcher for MacWatcher {
fn add(&self, path: &Path) -> Result<()> {
log::trace!("mac watcher add: {:?}", path);
let handles = self
.handles
.upgrade()
@@ -45,9 +44,6 @@ impl Watcher for MacWatcher {
.next_back()
&& path.starts_with(watched_path)
{
log::trace!(
"mac watched path starts with existing watched path: {watched_path:?}, {path:?}"
);
return Ok(());
}

View File

@@ -23,7 +23,6 @@ derive_more.workspace = true
git2.workspace = true
gpui.workspace = true
http_client.workspace = true
itertools.workspace = true
log.workspace = true
parking_lot.workspace = true
regex.workspace = true
@@ -37,7 +36,6 @@ text.workspace = true
thiserror.workspace = true
time.workspace = true
url.workspace = true
urlencoding.workspace = true
util.workspace = true
uuid.workspace = true
futures.workspace = true

View File

@@ -5,12 +5,9 @@ use async_trait::async_trait;
use derive_more::{Deref, DerefMut};
use gpui::{App, Global, SharedString};
use http_client::HttpClient;
use itertools::Itertools;
use parking_lot::RwLock;
use url::Url;
use crate::repository::RepoPath;
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct PullRequest {
pub number: u32,
@@ -58,21 +55,10 @@ pub struct BuildCommitPermalinkParams<'a> {
pub struct BuildPermalinkParams<'a> {
pub sha: &'a str,
/// URL-escaped path using unescaped `/` as the directory separator.
pub path: String,
pub path: &'a str,
pub selection: Option<Range<u32>>,
}
impl<'a> BuildPermalinkParams<'a> {
pub fn new(sha: &'a str, path: &RepoPath, selection: Option<Range<u32>>) -> Self {
Self {
sha,
path: path.components().map(urlencoding::encode).join("/"),
selection,
}
}
}
/// A Git hosting provider.
#[async_trait]
pub trait GitHostingProvider {

View File

@@ -30,4 +30,3 @@ workspace-hack.workspace = true
indoc.workspace = true
serde_json.workspace = true
pretty_assertions.workspace = true
git = { workspace = true, features = ["test-support"] }

View File

@@ -126,7 +126,6 @@ impl GitHostingProvider for Bitbucket {
#[cfg(test)]
mod tests {
use git::repository::repo_path;
use pretty_assertions::assert_eq;
use super::*;
@@ -183,7 +182,11 @@ mod tests {
owner: "zed-industries".into(),
repo: "zed".into(),
},
BuildPermalinkParams::new("f00b4r", &repo_path("main.rs"), None),
BuildPermalinkParams {
sha: "f00b4r",
path: "main.rs",
selection: None,
},
);
let expected_url = "https://bitbucket.org/zed-industries/zed/src/f00b4r/main.rs";
@@ -197,7 +200,11 @@ mod tests {
owner: "zed-industries".into(),
repo: "zed".into(),
},
BuildPermalinkParams::new("f00b4r", &repo_path("main.rs"), Some(6..6)),
BuildPermalinkParams {
sha: "f00b4r",
path: "main.rs",
selection: Some(6..6),
},
);
let expected_url = "https://bitbucket.org/zed-industries/zed/src/f00b4r/main.rs#lines-7";
@@ -211,7 +218,11 @@ mod tests {
owner: "zed-industries".into(),
repo: "zed".into(),
},
BuildPermalinkParams::new("f00b4r", &repo_path("main.rs"), Some(23..47)),
BuildPermalinkParams {
sha: "f00b4r",
path: "main.rs",
selection: Some(23..47),
},
);
let expected_url =

View File

@@ -191,7 +191,6 @@ impl GitHostingProvider for Chromium {
#[cfg(test)]
mod tests {
use git::repository::repo_path;
use indoc::indoc;
use pretty_assertions::assert_eq;
@@ -219,11 +218,11 @@ mod tests {
owner: Arc::from(""),
repo: "chromium/src".into(),
},
BuildPermalinkParams::new(
"fea5080b182fc92e3be0c01c5dece602fe70b588",
&repo_path("ui/base/cursor/cursor.h"),
None,
),
BuildPermalinkParams {
sha: "fea5080b182fc92e3be0c01c5dece602fe70b588",
path: "ui/base/cursor/cursor.h",
selection: None,
},
);
let expected_url = "https://chromium.googlesource.com/chromium/src/+/fea5080b182fc92e3be0c01c5dece602fe70b588/ui/base/cursor/cursor.h";
@@ -237,11 +236,11 @@ mod tests {
owner: Arc::from(""),
repo: "chromium/src".into(),
},
BuildPermalinkParams::new(
"fea5080b182fc92e3be0c01c5dece602fe70b588",
&repo_path("ui/base/cursor/cursor.h"),
Some(18..18),
),
BuildPermalinkParams {
sha: "fea5080b182fc92e3be0c01c5dece602fe70b588",
path: "ui/base/cursor/cursor.h",
selection: Some(18..18),
},
);
let expected_url = "https://chromium.googlesource.com/chromium/src/+/fea5080b182fc92e3be0c01c5dece602fe70b588/ui/base/cursor/cursor.h#19";
@@ -255,11 +254,11 @@ mod tests {
owner: Arc::from(""),
repo: "chromium/src".into(),
},
BuildPermalinkParams::new(
"fea5080b182fc92e3be0c01c5dece602fe70b588",
&repo_path("ui/base/cursor/cursor.h"),
Some(18..30),
),
BuildPermalinkParams {
sha: "fea5080b182fc92e3be0c01c5dece602fe70b588",
path: "ui/base/cursor/cursor.h",
selection: Some(18..30),
},
);
let expected_url = "https://chromium.googlesource.com/chromium/src/+/fea5080b182fc92e3be0c01c5dece602fe70b588/ui/base/cursor/cursor.h#19";

View File

@@ -204,7 +204,6 @@ impl GitHostingProvider for Codeberg {
#[cfg(test)]
mod tests {
use git::repository::repo_path;
use pretty_assertions::assert_eq;
use super::*;
@@ -246,11 +245,11 @@ mod tests {
owner: "zed-industries".into(),
repo: "zed".into(),
},
BuildPermalinkParams::new(
"faa6f979be417239b2e070dbbf6392b909224e0b",
&repo_path("crates/editor/src/git/permalink.rs"),
None,
),
BuildPermalinkParams {
sha: "faa6f979be417239b2e070dbbf6392b909224e0b",
path: "crates/editor/src/git/permalink.rs",
selection: None,
},
);
let expected_url = "https://codeberg.org/zed-industries/zed/src/commit/faa6f979be417239b2e070dbbf6392b909224e0b/crates/editor/src/git/permalink.rs";
@@ -264,11 +263,11 @@ mod tests {
owner: "zed-industries".into(),
repo: "zed".into(),
},
BuildPermalinkParams::new(
"faa6f979be417239b2e070dbbf6392b909224e0b",
&repo_path("crates/editor/src/git/permalink.rs"),
Some(6..6),
),
BuildPermalinkParams {
sha: "faa6f979be417239b2e070dbbf6392b909224e0b",
path: "crates/editor/src/git/permalink.rs",
selection: Some(6..6),
},
);
let expected_url = "https://codeberg.org/zed-industries/zed/src/commit/faa6f979be417239b2e070dbbf6392b909224e0b/crates/editor/src/git/permalink.rs#L7";
@@ -282,11 +281,11 @@ mod tests {
owner: "zed-industries".into(),
repo: "zed".into(),
},
BuildPermalinkParams::new(
"faa6f979be417239b2e070dbbf6392b909224e0b",
&repo_path("crates/editor/src/git/permalink.rs"),
Some(23..47),
),
BuildPermalinkParams {
sha: "faa6f979be417239b2e070dbbf6392b909224e0b",
path: "crates/editor/src/git/permalink.rs",
selection: Some(23..47),
},
);
let expected_url = "https://codeberg.org/zed-industries/zed/src/commit/faa6f979be417239b2e070dbbf6392b909224e0b/crates/editor/src/git/permalink.rs#L24-L48";

View File

@@ -84,7 +84,6 @@ impl GitHostingProvider for Gitee {
#[cfg(test)]
mod tests {
use git::repository::repo_path;
use pretty_assertions::assert_eq;
use super::*;
@@ -126,11 +125,11 @@ mod tests {
owner: "zed-industries".into(),
repo: "zed".into(),
},
BuildPermalinkParams::new(
"e5fe811d7ad0fc26934edd76f891d20bdc3bb194",
&repo_path("crates/editor/src/git/permalink.rs"),
None,
),
BuildPermalinkParams {
sha: "e5fe811d7ad0fc26934edd76f891d20bdc3bb194",
path: "crates/editor/src/git/permalink.rs",
selection: None,
},
);
let expected_url = "https://gitee.com/zed-industries/zed/blob/e5fe811d7ad0fc26934edd76f891d20bdc3bb194/crates/editor/src/git/permalink.rs";
@@ -144,11 +143,11 @@ mod tests {
owner: "zed-industries".into(),
repo: "zed".into(),
},
BuildPermalinkParams::new(
"e5fe811d7ad0fc26934edd76f891d20bdc3bb194",
&repo_path("crates/editor/src/git/permalink.rs"),
Some(6..6),
),
BuildPermalinkParams {
sha: "e5fe811d7ad0fc26934edd76f891d20bdc3bb194",
path: "crates/editor/src/git/permalink.rs",
selection: Some(6..6),
},
);
let expected_url = "https://gitee.com/zed-industries/zed/blob/e5fe811d7ad0fc26934edd76f891d20bdc3bb194/crates/editor/src/git/permalink.rs#L7";
@@ -162,11 +161,11 @@ mod tests {
owner: "zed-industries".into(),
repo: "zed".into(),
},
BuildPermalinkParams::new(
"e5fe811d7ad0fc26934edd76f891d20bdc3bb194",
&repo_path("crates/editor/src/git/permalink.rs"),
Some(23..47),
),
BuildPermalinkParams {
sha: "e5fe811d7ad0fc26934edd76f891d20bdc3bb194",
path: "crates/editor/src/git/permalink.rs",
selection: Some(23..47),
},
);
let expected_url = "https://gitee.com/zed-industries/zed/blob/e5fe811d7ad0fc26934edd76f891d20bdc3bb194/crates/editor/src/git/permalink.rs#L24-48";

View File

@@ -259,7 +259,6 @@ impl GitHostingProvider for Github {
#[cfg(test)]
mod tests {
use git::repository::repo_path;
use indoc::indoc;
use pretty_assertions::assert_eq;
@@ -401,11 +400,11 @@ mod tests {
};
let permalink = Github::public_instance().build_permalink(
remote,
BuildPermalinkParams::new(
"e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
&repo_path("crates/editor/src/git/permalink.rs"),
None,
),
BuildPermalinkParams {
sha: "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
path: "crates/editor/src/git/permalink.rs",
selection: None,
},
);
let expected_url = "https://github.com/zed-industries/zed/blob/e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7/crates/editor/src/git/permalink.rs";
@@ -419,11 +418,11 @@ mod tests {
owner: "zed-industries".into(),
repo: "zed".into(),
},
BuildPermalinkParams::new(
"b2efec9824c45fcc90c9a7eb107a50d1772a60aa",
&repo_path("crates/zed/src/main.rs"),
None,
),
BuildPermalinkParams {
sha: "b2efec9824c45fcc90c9a7eb107a50d1772a60aa",
path: "crates/zed/src/main.rs",
selection: None,
},
);
let expected_url = "https://github.com/zed-industries/zed/blob/b2efec9824c45fcc90c9a7eb107a50d1772a60aa/crates/zed/src/main.rs";
@@ -437,11 +436,11 @@ mod tests {
owner: "zed-industries".into(),
repo: "zed".into(),
},
BuildPermalinkParams::new(
"e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
&repo_path("crates/editor/src/git/permalink.rs"),
Some(6..6),
),
BuildPermalinkParams {
sha: "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
path: "crates/editor/src/git/permalink.rs",
selection: Some(6..6),
},
);
let expected_url = "https://github.com/zed-industries/zed/blob/e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7/crates/editor/src/git/permalink.rs#L7";
@@ -455,11 +454,11 @@ mod tests {
owner: "zed-industries".into(),
repo: "zed".into(),
},
BuildPermalinkParams::new(
"e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
&repo_path("crates/editor/src/git/permalink.rs"),
Some(23..47),
),
BuildPermalinkParams {
sha: "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
path: "crates/editor/src/git/permalink.rs",
selection: Some(23..47),
},
);
let expected_url = "https://github.com/zed-industries/zed/blob/e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7/crates/editor/src/git/permalink.rs#L24-L48";
@@ -507,23 +506,4 @@ mod tests {
};
assert_eq!(github.extract_pull_request(&remote, message), None);
}
/// Regression test for issue #39875
#[test]
fn test_git_permalink_url_escaping() {
let permalink = Github::public_instance().build_permalink(
ParsedGitRemote {
owner: "zed-industries".into(),
repo: "nonexistent".into(),
},
BuildPermalinkParams::new(
"3ef1539900037dd3601be7149b2b39ed6d0ce3db",
&repo_path("app/blog/[slug]/page.tsx"),
Some(7..7),
),
);
let expected_url = "https://github.com/zed-industries/nonexistent/blob/3ef1539900037dd3601be7149b2b39ed6d0ce3db/app/blog/%5Bslug%5D/page.tsx#L8";
assert_eq!(permalink.to_string(), expected_url.to_string())
}
}

View File

@@ -126,7 +126,6 @@ impl GitHostingProvider for Gitlab {
#[cfg(test)]
mod tests {
use git::repository::repo_path;
use pretty_assertions::assert_eq;
use super::*;
@@ -210,11 +209,11 @@ mod tests {
owner: "zed-industries".into(),
repo: "zed".into(),
},
BuildPermalinkParams::new(
"e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
&repo_path("crates/editor/src/git/permalink.rs"),
None,
),
BuildPermalinkParams {
sha: "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
path: "crates/editor/src/git/permalink.rs",
selection: None,
},
);
let expected_url = "https://gitlab.com/zed-industries/zed/-/blob/e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7/crates/editor/src/git/permalink.rs";
@@ -228,11 +227,11 @@ mod tests {
owner: "zed-industries".into(),
repo: "zed".into(),
},
BuildPermalinkParams::new(
"e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
&repo_path("crates/editor/src/git/permalink.rs"),
Some(6..6),
),
BuildPermalinkParams {
sha: "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
path: "crates/editor/src/git/permalink.rs",
selection: Some(6..6),
},
);
let expected_url = "https://gitlab.com/zed-industries/zed/-/blob/e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7/crates/editor/src/git/permalink.rs#L7";
@@ -246,11 +245,11 @@ mod tests {
owner: "zed-industries".into(),
repo: "zed".into(),
},
BuildPermalinkParams::new(
"e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
&repo_path("crates/editor/src/git/permalink.rs"),
Some(23..47),
),
BuildPermalinkParams {
sha: "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
path: "crates/editor/src/git/permalink.rs",
selection: Some(23..47),
},
);
let expected_url = "https://gitlab.com/zed-industries/zed/-/blob/e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7/crates/editor/src/git/permalink.rs#L24-48";
@@ -267,11 +266,11 @@ mod tests {
owner: "zed-industries".into(),
repo: "zed".into(),
},
BuildPermalinkParams::new(
"e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
&repo_path("crates/editor/src/git/permalink.rs"),
None,
),
BuildPermalinkParams {
sha: "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
path: "crates/editor/src/git/permalink.rs",
selection: None,
},
);
let expected_url = "https://gitlab.some-enterprise.com/zed-industries/zed/-/blob/e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7/crates/editor/src/git/permalink.rs";
@@ -288,11 +287,11 @@ mod tests {
owner: "zed-industries".into(),
repo: "zed".into(),
},
BuildPermalinkParams::new(
"b2efec9824c45fcc90c9a7eb107a50d1772a60aa",
&repo_path("crates/zed/src/main.rs"),
None,
),
BuildPermalinkParams {
sha: "b2efec9824c45fcc90c9a7eb107a50d1772a60aa",
path: "crates/zed/src/main.rs",
selection: None,
},
);
let expected_url = "https://gitlab-instance.big-co.com/zed-industries/zed/-/blob/b2efec9824c45fcc90c9a7eb107a50d1772a60aa/crates/zed/src/main.rs";

View File

@@ -89,7 +89,6 @@ impl GitHostingProvider for Sourcehut {
#[cfg(test)]
mod tests {
use git::repository::repo_path;
use pretty_assertions::assert_eq;
use super::*;
@@ -146,11 +145,11 @@ mod tests {
owner: "zed-industries".into(),
repo: "zed".into(),
},
BuildPermalinkParams::new(
"faa6f979be417239b2e070dbbf6392b909224e0b",
&repo_path("crates/editor/src/git/permalink.rs"),
None,
),
BuildPermalinkParams {
sha: "faa6f979be417239b2e070dbbf6392b909224e0b",
path: "crates/editor/src/git/permalink.rs",
selection: None,
},
);
let expected_url = "https://git.sr.ht/~zed-industries/zed/tree/faa6f979be417239b2e070dbbf6392b909224e0b/item/crates/editor/src/git/permalink.rs";
@@ -164,11 +163,11 @@ mod tests {
owner: "zed-industries".into(),
repo: "zed.git".into(),
},
BuildPermalinkParams::new(
"faa6f979be417239b2e070dbbf6392b909224e0b",
&repo_path("crates/editor/src/git/permalink.rs"),
None,
),
BuildPermalinkParams {
sha: "faa6f979be417239b2e070dbbf6392b909224e0b",
path: "crates/editor/src/git/permalink.rs",
selection: None,
},
);
let expected_url = "https://git.sr.ht/~zed-industries/zed.git/tree/faa6f979be417239b2e070dbbf6392b909224e0b/item/crates/editor/src/git/permalink.rs";
@@ -182,11 +181,11 @@ mod tests {
owner: "zed-industries".into(),
repo: "zed".into(),
},
BuildPermalinkParams::new(
"faa6f979be417239b2e070dbbf6392b909224e0b",
&repo_path("crates/editor/src/git/permalink.rs"),
Some(6..6),
),
BuildPermalinkParams {
sha: "faa6f979be417239b2e070dbbf6392b909224e0b",
path: "crates/editor/src/git/permalink.rs",
selection: Some(6..6),
},
);
let expected_url = "https://git.sr.ht/~zed-industries/zed/tree/faa6f979be417239b2e070dbbf6392b909224e0b/item/crates/editor/src/git/permalink.rs#L7";
@@ -200,11 +199,11 @@ mod tests {
owner: "zed-industries".into(),
repo: "zed".into(),
},
BuildPermalinkParams::new(
"faa6f979be417239b2e070dbbf6392b909224e0b",
&repo_path("crates/editor/src/git/permalink.rs"),
Some(23..47),
),
BuildPermalinkParams {
sha: "faa6f979be417239b2e070dbbf6392b909224e0b",
path: "crates/editor/src/git/permalink.rs",
selection: Some(23..47),
},
);
let expected_url = "https://git.sr.ht/~zed-industries/zed/tree/faa6f979be417239b2e070dbbf6392b909224e0b/item/crates/editor/src/git/permalink.rs#L24-48";

View File

@@ -8,8 +8,8 @@ use git::{
repository::CommitSummary,
};
use gpui::{
ClipboardItem, Entity, Hsla, MouseButton, ScrollHandle, Subscription, TextStyle,
TextStyleRefinement, UnderlineStyle, WeakEntity, prelude::*,
ClipboardItem, Entity, Hsla, MouseButton, ScrollHandle, Subscription, TextStyle, WeakEntity,
prelude::*,
};
use markdown::{Markdown, MarkdownElement};
use project::{git_store::Repository, project_settings::ProjectSettings};
@@ -17,7 +17,7 @@ use settings::Settings as _;
use theme::ThemeSettings;
use time::OffsetDateTime;
use time_format::format_local_timestamp;
use ui::{ContextMenu, Divider, prelude::*, tooltip_container};
use ui::{ContextMenu, Divider, IconButtonShape, prelude::*, tooltip_container};
use workspace::Workspace;
const GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED: usize = 20;
@@ -61,15 +61,16 @@ impl BlameRenderer for GitBlameRenderer {
.mr_2()
.child(
h_flex()
.id(("blame", ix))
.w_full()
.gap_2()
.justify_between()
.font_family(style.font().family)
.line_height(style.line_height)
.id(("blame", ix))
.text_color(cx.theme().status().hint)
.gap_2()
.child(
h_flex()
.items_center()
.gap_2()
.child(div().text_color(sha_color).child(short_commit_id))
.children(avatar)
@@ -208,21 +209,11 @@ impl BlameRenderer for GitBlameRenderer {
OffsetDateTime::now_utc(),
time_format::TimestampFormat::MediumAbsolute,
);
let link_color = cx.theme().colors().text_accent;
let markdown_style = {
let mut style = hover_markdown_style(window, cx);
if let Some(code_block) = &style.code_block.text {
style.base_text_style.refine(code_block);
}
style.link.refine(&TextStyleRefinement {
color: Some(link_color),
underline: Some(UnderlineStyle {
color: Some(link_color.opacity(0.4)),
thickness: px(1.0),
..Default::default()
}),
..Default::default()
});
style
};
@@ -259,21 +250,20 @@ impl BlameRenderer for GitBlameRenderer {
};
Some(
tooltip_container(cx, |this, cx| {
this.occlude()
tooltip_container(cx, |d, cx| {
d.occlude()
.on_mouse_move(|_, _, cx| cx.stop_propagation())
.on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
.child(
v_flex()
.w(gpui::rems(30.))
.gap_4()
.child(
h_flex()
.pb_1()
.gap_2()
.pb_1p5()
.gap_x_2()
.overflow_x_hidden()
.flex_wrap()
.border_b_1()
.border_color(cx.theme().colors().border_variant)
.children(avatar)
.child(author)
.when(!author_email.is_empty(), |this| {
@@ -282,29 +272,30 @@ impl BlameRenderer for GitBlameRenderer {
.text_color(cx.theme().colors().text_muted)
.child(author_email.to_owned()),
)
}),
})
.border_b_1()
.border_color(cx.theme().colors().border_variant),
)
.child(
div()
.id("inline-blame-commit-message")
.track_scroll(&scroll_handle)
.py_1p5()
.child(message)
.max_h(message_max_height)
.overflow_y_scroll()
.child(message),
.track_scroll(&scroll_handle),
)
.child(
h_flex()
.text_color(cx.theme().colors().text_muted)
.w_full()
.justify_between()
.pt_1()
.pt_1p5()
.border_t_1()
.border_color(cx.theme().colors().border_variant)
.child(absolute_timestamp)
.child(
h_flex()
.gap_1()
.gap_1p5()
.when_some(pull_request, |this, pr| {
this.child(
Button::new(
@@ -315,24 +306,24 @@ impl BlameRenderer for GitBlameRenderer {
.icon(IconName::PullRequest)
.icon_color(Color::Muted)
.icon_position(IconPosition::Start)
.icon_size(IconSize::Small)
.style(ButtonStyle::Subtle)
.on_click(move |_, _, cx| {
cx.stop_propagation();
cx.open_url(pr.url.as_str())
}),
)
.child(Divider::vertical())
})
.child(Divider::vertical())
.child(
Button::new(
"commit-sha-button",
short_commit_id.clone(),
)
.style(ButtonStyle::Subtle)
.color(Color::Muted)
.icon(IconName::FileGit)
.icon_color(Color::Muted)
.icon_position(IconPosition::Start)
.icon_size(IconSize::Small)
.on_click(move |_, window, cx| {
CommitView::open(
commit_summary.clone(),
@@ -346,6 +337,7 @@ impl BlameRenderer for GitBlameRenderer {
)
.child(
IconButton::new("copy-sha-button", IconName::Copy)
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.on_click(move |_, _, cx| {

View File

@@ -228,7 +228,7 @@ impl PickerDelegate for PickerPromptDelegate {
let highlights: Vec<_> = hit
.positions
.iter()
.filter(|&&index| index < self.max_match_length)
.filter(|index| index < &&self.max_match_length)
.copied()
.collect();

View File

@@ -1619,13 +1619,14 @@ mod tests {
project_diff::{self, ProjectDiff},
};
#[cfg_attr(windows, ignore = "currently fails on windows")]
#[gpui::test]
async fn test_go_to_prev_hunk_multibuffer(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
path!("/a"),
"/a",
json!({
".git": {},
"a.txt": "created\n",
@@ -1636,7 +1637,7 @@ mod tests {
.await;
fs.set_head_and_index_for_repo(
Path::new(path!("/a/.git")),
Path::new("/a/.git"),
&[
("b.txt", "before\n".to_string()),
("c.txt", "unchanged\n".to_string()),
@@ -1644,7 +1645,7 @@ mod tests {
],
);
let project = Project::test(fs, [Path::new(path!("/a"))], cx).await;
let project = Project::test(fs, [Path::new("/a")], cx).await;
let (workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
@@ -1706,6 +1707,7 @@ mod tests {
));
}
#[cfg_attr(windows, ignore = "currently fails on windows")]
#[gpui::test]
async fn test_excerpts_splitting_after_restoring_the_middle_excerpt(cx: &mut TestAppContext) {
init_test(cx);
@@ -1745,7 +1747,7 @@ mod tests {
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
path!("/a"),
"/a",
json!({
".git": {},
"main.rs": buffer_contents,
@@ -1754,11 +1756,11 @@ mod tests {
.await;
fs.set_head_and_index_for_repo(
Path::new(path!("/a/.git")),
Path::new("/a/.git"),
&[("main.rs", git_contents.to_owned())],
);
let project = Project::test(fs, [Path::new(path!("/a"))], cx).await;
let project = Project::test(fs, [Path::new("/a")], cx).await;
let (workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
@@ -1923,7 +1925,6 @@ mod tests {
cx.run_until_parked();
let editor = diff.read_with(cx, |diff, _| diff.editor.clone());
assert_state_with_diff(
&editor,
cx,

View File

@@ -1,6 +1,6 @@
[package]
name = "gpui"
version = "0.2.1"
version = "0.2.0"
edition.workspace = true
authors = ["Nathan Sobo <nathan@zed.dev>"]
description = "Zed's GPU-accelerated UI framework"

View File

@@ -11,8 +11,6 @@ GPUI is still in active development as we work on the Zed code editor, and is st
gpui = { version = "*" }
```
- [Ownership and data flow](_ownership_and_data_flow)
Everything in GPUI starts with an `Application`. You can create one with `Application::new()`, and kick off your application by passing a callback to `Application::run()`. Inside this callback, you can create a new window with `App::open_window()`, and register your first root view. See [gpui.rs](https://www.gpui.rs/) for a complete example.
### Dependencies

View File

@@ -1,9 +1,10 @@
use std::{fs, path::PathBuf};
use std::{fs, path::PathBuf, time::Duration};
use anyhow::Result;
use gpui::{
App, Application, AssetSource, Bounds, BoxShadow, ClickEvent, Context, SharedString, Task,
Window, WindowBounds, WindowOptions, div, hsla, img, point, prelude::*, px, rgb, size, svg,
Timer, Window, WindowBounds, WindowOptions, div, hsla, img, point, prelude::*, px, rgb, size,
svg,
};
struct Assets {
@@ -36,7 +37,6 @@ impl AssetSource for Assets {
struct HelloWorld {
_task: Option<Task<()>>,
opacity: f32,
animating: bool,
}
impl HelloWorld {
@@ -44,29 +44,39 @@ impl HelloWorld {
Self {
_task: None,
opacity: 0.5,
animating: false,
}
}
fn start_animation(&mut self, _: &ClickEvent, _: &mut Window, cx: &mut Context<Self>) {
fn change_opacity(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
self.opacity = 0.0;
self.animating = true;
cx.notify();
self._task = Some(cx.spawn_in(window, async move |view, cx| {
loop {
Timer::after(Duration::from_secs_f32(0.05)).await;
let mut stop = false;
let _ = cx.update(|_, cx| {
view.update(cx, |view, cx| {
if view.opacity >= 1.0 {
stop = true;
return;
}
view.opacity += 0.1;
cx.notify();
})
});
if stop {
break;
}
}
}));
}
}
impl Render for HelloWorld {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
if self.animating {
self.opacity += 0.005;
if self.opacity >= 1.0 {
self.animating = false;
self.opacity = 1.0;
} else {
window.request_animation_frame();
}
}
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div()
.flex()
.flex_row()
@@ -86,7 +96,7 @@ impl Render for HelloWorld {
.child(
div()
.id("panel")
.on_click(cx.listener(Self::start_animation))
.on_click(cx.listener(Self::change_opacity))
.absolute()
.top_8()
.left_8()
@@ -140,15 +150,7 @@ impl Render for HelloWorld {
.text_2xl()
.size_8(),
)
.child(
div()
.flex()
.children(["🎊", "✈️", "🎉", "🎈", "🎁", "🎂"].map(|emoji| {
div()
.child(emoji.to_string())
.hover(|style| style.opacity(0.5))
})),
)
.child("🎊✈️🎉🎈🎁🎂")
.child(img("image/black-cat-typing.gif").size_12()),
),
)

View File

@@ -1,6 +1,6 @@
use gpui::{
App, Application, Bounds, Context, FontStyle, FontWeight, StyledText, Window, WindowBounds,
WindowOptions, div, prelude::*, px, size,
App, Application, Bounds, Context, Window, WindowBounds, WindowOptions, div, prelude::*, px,
size,
};
struct HelloWorld {}
@@ -71,12 +71,6 @@ impl Render for HelloWorld {
.child("100%"),
),
)
.child(div().flex().gap_2().justify_between().child(
StyledText::new("ABCD").with_highlights([
(0..1, FontWeight::EXTRA_BOLD.into()),
(2..3, FontStyle::Italic.into()),
]),
))
}
}

View File

@@ -1,9 +1,9 @@
use crate::{
AnyElement, AnyImageCache, App, Asset, AssetLogger, Bounds, DefiniteLength, Element, ElementId,
Entity, GlobalElementId, Hitbox, Image, ImageCache, InspectorElementId, InteractiveElement,
Interactivity, IntoElement, LayoutId, Length, ObjectFit, Pixels, RenderImage, Resource,
SMOOTH_SVG_SCALE_FACTOR, SharedString, SharedUri, StyleRefinement, Styled, SvgSize, Task,
Window, px, swap_rgba_pa_to_bgra,
AbsoluteLength, AnyElement, AnyImageCache, App, Asset, AssetLogger, Bounds, DefiniteLength,
Element, ElementId, Entity, GlobalElementId, Hitbox, Image, ImageCache, InspectorElementId,
InteractiveElement, Interactivity, IntoElement, LayoutId, Length, ObjectFit, Pixels,
RenderImage, Resource, SMOOTH_SVG_SCALE_FACTOR, SharedString, SharedUri, StyleRefinement,
Styled, SvgSize, Task, Window, px, swap_rgba_pa_to_bgra,
};
use anyhow::{Context as _, Result};
@@ -337,28 +337,24 @@ impl Element for Img {
if let Length::Auto = style.size.width {
style.size.width = match style.size.height {
Length::Definite(DefiniteLength::Absolute(abs_length)) => {
let height_px = abs_length.to_pixels(window.rem_size());
Length::Definite(
px(image_size.width.0 * height_px.0
/ image_size.height.0)
Length::Definite(DefiniteLength::Absolute(
AbsoluteLength::Pixels(height),
)) => Length::Definite(
px(image_size.width.0 * height.0 / image_size.height.0)
.into(),
)
}
),
_ => Length::Definite(image_size.width.into()),
};
}
if let Length::Auto = style.size.height {
style.size.height = match style.size.width {
Length::Definite(DefiniteLength::Absolute(abs_length)) => {
let width_px = abs_length.to_pixels(window.rem_size());
Length::Definite(
px(image_size.height.0 * width_px.0
/ image_size.width.0)
Length::Definite(DefiniteLength::Absolute(
AbsoluteLength::Pixels(width),
)) => Length::Definite(
px(image_size.height * f32::from(width) / image_size.width)
.into(),
)
}
),
_ => Length::Definite(image_size.height.into()),
};
}

View File

@@ -138,11 +138,7 @@ impl UniformListScrollHandle {
})))
}
/// Scroll the list so that the given item index is visible.
///
/// This uses non-strict scrolling: if the item is already fully visible, no scrolling occurs.
/// If the item is out of view, it scrolls the minimum amount to bring it into view according
/// to the strategy.
/// Scroll the list so that the given item index is onscreen.
pub fn scroll_to_item(&self, ix: usize, strategy: ScrollStrategy) {
self.0.borrow_mut().deferred_scroll_to_item = Some(DeferredScrollToItem {
item_index: ix,
@@ -153,9 +149,6 @@ impl UniformListScrollHandle {
}
/// Scroll the list so that the given item index is at scroll strategy position.
///
/// This uses strict scrolling: the item will always be scrolled to match the strategy position,
/// even if it's already visible. Use this when you need precise positioning.
pub fn scroll_to_item_strict(&self, ix: usize, strategy: ScrollStrategy) {
self.0.borrow_mut().deferred_scroll_to_item = Some(DeferredScrollToItem {
item_index: ix,
@@ -165,16 +158,11 @@ impl UniformListScrollHandle {
});
}
/// Scroll the list to the given item index with an offset in number of items.
/// Scroll the list to the given item index with an offset.
///
/// This uses non-strict scrolling: if the item is already visible within the offset region,
/// no scrolling occurs.
/// For ScrollStrategy::Top, the item will be placed at the offset position from the top.
///
/// The offset parameter shrinks the effective viewport by the specified number of items
/// from the corresponding edge, then applies the scroll strategy within that reduced viewport:
/// - `ScrollStrategy::Top`: Shrinks from top, positions item at the new top
/// - `ScrollStrategy::Center`: Shrinks from top, centers item in the reduced viewport
/// - `ScrollStrategy::Bottom`: Shrinks from bottom, positions item at the new bottom
/// For ScrollStrategy::Center, the item will be centered between offset and the last visible item.
pub fn scroll_to_item_with_offset(&self, ix: usize, strategy: ScrollStrategy, offset: usize) {
self.0.borrow_mut().deferred_scroll_to_item = Some(DeferredScrollToItem {
item_index: ix,
@@ -184,30 +172,6 @@ impl UniformListScrollHandle {
});
}
/// Scroll the list so that the given item index is at the exact scroll strategy position with an offset.
///
/// This uses strict scrolling: the item will always be scrolled to match the strategy position,
/// even if it's already visible.
///
/// The offset parameter shrinks the effective viewport by the specified number of items
/// from the corresponding edge, then applies the scroll strategy within that reduced viewport:
/// - `ScrollStrategy::Top`: Shrinks from top, positions item at the new top
/// - `ScrollStrategy::Center`: Shrinks from top, centers item in the reduced viewport
/// - `ScrollStrategy::Bottom`: Shrinks from bottom, positions item at the new bottom
pub fn scroll_to_item_strict_with_offset(
&self,
ix: usize,
strategy: ScrollStrategy,
offset: usize,
) {
self.0.borrow_mut().deferred_scroll_to_item = Some(DeferredScrollToItem {
item_index: ix,
strategy,
offset,
scroll_strict: true,
});
}
/// Check if the list is flipped vertically.
pub fn y_flipped(&self) -> bool {
self.0.borrow().y_flipped
@@ -428,7 +392,7 @@ impl Element for UniformList {
{
match deferred_scroll.strategy {
ScrollStrategy::Top => {
updated_scroll_offset.y = -(item_top - offset_pixels)
updated_scroll_offset.y = -item_top
.max(Pixels::ZERO)
.min(content_height - list_height)
.max(Pixels::ZERO);
@@ -446,8 +410,7 @@ impl Element for UniformList {
.max(Pixels::ZERO);
}
ScrollStrategy::Bottom => {
updated_scroll_offset.y = -(item_bottom - list_height
+ offset_pixels)
updated_scroll_offset.y = -(item_bottom - list_height)
.max(Pixels::ZERO)
.min(content_height - list_height)
.max(Pixels::ZERO);

View File

@@ -2968,15 +2968,6 @@ impl ScaledPixels {
/// # Returns
///
/// Returns a new `ScaledPixels` instance with the rounded value.
pub fn round(&self) -> Self {
Self(self.0.round())
}
/// Ceils the `ScaledPixels` value to the nearest whole number.
///
/// # Returns
///
/// Returns a new `ScaledPixels` instance with the ceiled value.
pub fn ceil(&self) -> Self {
Self(self.0.ceil())
}

View File

@@ -1,4 +1,67 @@
#![doc = include_str!("../README.md")]
//! # Welcome to GPUI!
//!
//! GPUI is a hybrid immediate and retained mode, GPU accelerated, UI framework
//! for Rust, designed to support a wide variety of applications.
//!
//! ## Getting Started
//!
//! GPUI is still in active development as we work on the Zed code editor and isn't yet on crates.io.
//! You'll also need to use the latest version of stable rust. Add the following to your Cargo.toml:
//!
//! ```toml
//! [dependencies]
//! gpui = { git = "https://github.com/zed-industries/zed" }
//! ```
//!
//! - [Ownership and data flow](_ownership_and_data_flow)
//!
//! Everything in GPUI starts with an [`Application`]. You can create one with [`Application::new`], and
//! kick off your application by passing a callback to [`Application::run`]. Inside this callback,
//! you can create a new window with [`App::open_window`], and register your first root
//! view. See [gpui.rs](https://www.gpui.rs/) for a complete example.
//!
//! ## The Big Picture
//!
//! GPUI offers three different [registers](https://en.wikipedia.org/wiki/Register_(sociolinguistics)) depending on your needs:
//!
//! - State management and communication with [`Entity`]'s. Whenever you need to store application state
//! that communicates between different parts of your application, you'll want to use GPUI's
//! entities. Entities are owned by GPUI and are only accessible through an owned smart pointer
//! similar to an [`std::rc::Rc`]. See [`app::Context`] for more information.
//!
//! - High level, declarative UI with views. All UI in GPUI starts with a view. A view is simply
//! a [`Entity`] that can be rendered, by implementing the [`Render`] trait. At the start of each frame, GPUI
//! will call this render method on the root view of a given window. Views build a tree of
//! [`Element`]s, lay them out and style them with a tailwind-style API, and then give them to
//! GPUI to turn into pixels. See the [`elements::Div`] element for an all purpose swiss-army
//! knife for UI.
//!
//! - Low level, imperative UI with Elements. Elements are the building blocks of UI in GPUI, and they
//! provide a nice wrapper around an imperative API that provides as much flexibility and control as
//! you need. Elements have total control over how they and their child elements are rendered and
//! can be used for making efficient views into large lists, implement custom layouting for a code editor,
//! and anything else you can think of. See the [`elements`] module for more information.
//!
//! Each of these registers has one or more corresponding contexts that can be accessed from all GPUI services.
//! This context is your main interface to GPUI, and is used extensively throughout the framework.
//!
//! ## Other Resources
//!
//! In addition to the systems above, GPUI provides a range of smaller services that are useful for building
//! complex applications:
//!
//! - Actions are user-defined structs that are used for converting keystrokes into logical operations in your UI.
//! Use this for implementing keyboard shortcuts, such as cmd-q (See `action` module for more information).
//! - Platform services, such as `quit the app` or `open a URL` are available as methods on the [`app::App`].
//! - An async executor that is integrated with the platform's event loop. See the [`executor`] module for more information.,
//! - The [`gpui::test`](macro@test) macro provides a convenient way to write tests for your GPUI applications. Tests also have their
//! own kind of context, a [`TestAppContext`] which provides ways of simulating common platform input. See [`TestAppContext`]
//! and [`mod@test`] modules for more details.
//!
//! Currently, the best way to learn about these APIs is to read the Zed source code, ask us about it at a fireside hack, or drop
//! a question in the [Zed Discord](https://zed.dev/community-links). We're working on improving the documentation, creating more examples,
//! and will be publishing more guides to GPUI on our [blog](https://zed.dev/blog).
#![deny(missing_docs)]
#![allow(clippy::type_complexity)] // Not useful, GPUI makes heavy use of callbacks
#![allow(clippy::collapsible_else_if)] // False positives in platform specific code

View File

@@ -683,24 +683,7 @@ fn fs_quad(input: QuadVarying) -> @location(0) vec4<f32> {
let is_horizontal =
corner_center_to_point.x <
corner_center_to_point.y;
// When applying dashed borders to just some, not all, the sides.
// The way we chose border widths above sometimes comes with a 0 width value.
// So we choose again to avoid division by zero.
// TODO: A better solution exists taking a look at the whole file.
// this does not fix single dashed borders at the corners
let dashed_border = vec2<f32>(
max(
quad.border_widths.bottom,
quad.border_widths.top,
),
max(
quad.border_widths.right,
quad.border_widths.left,
)
);
let border_width = select(dashed_border.y, dashed_border.x, is_horizontal);
let border_width = select(border.y, border.x, is_horizontal);
dash_velocity = dv_numerator / border_width;
t = select(point.y, point.x, is_horizontal) * dash_velocity;
max_t = select(size.y, size.x, is_horizontal) * dash_velocity;

View File

@@ -245,15 +245,7 @@ fragment float4 quad_fragment(QuadFragmentInput input [[stage_in]],
// out on each straight line, rather than around the whole
// perimeter. This way each line starts and ends with a dash.
bool is_horizontal = corner_center_to_point.x < corner_center_to_point.y;
// Choosing the right border width for dashed borders.
// TODO: A better solution exists taking a look at the whole file.
// this does not fix single dashed borders at the corners
float2 dashed_border = float2(
fmax(quad.border_widths.bottom, quad.border_widths.top),
fmax(quad.border_widths.right, quad.border_widths.left));
float border_width = is_horizontal ? dashed_border.x : dashed_border.y;
float border_width = is_horizontal ? border.x : border.y;
dash_velocity = dv_numerator / border_width;
t = is_horizontal ? point.x : point.y;
t *= dash_velocity;

View File

@@ -43,7 +43,7 @@ use pathfinder_geometry::{
vector::{Vector2F, Vector2I},
};
use smallvec::SmallVec;
use std::{borrow::Cow, char, convert::TryFrom, sync::Arc};
use std::{borrow::Cow, char, cmp, convert::TryFrom, sync::Arc};
use super::open_type::apply_features_and_fallbacks;
@@ -67,7 +67,6 @@ struct MacTextSystemState {
font_ids_by_postscript_name: HashMap<String, FontId>,
font_ids_by_font_key: HashMap<FontKey, SmallVec<[FontId; 4]>>,
postscript_names_by_font_id: HashMap<FontId, String>,
zwnjs_scratch_space: Vec<(usize, usize)>,
}
impl MacTextSystem {
@@ -80,7 +79,6 @@ impl MacTextSystem {
font_ids_by_postscript_name: HashMap::default(),
font_ids_by_font_key: HashMap::default(),
postscript_names_by_font_id: HashMap::default(),
zwnjs_scratch_space: Vec::new(),
}))
}
}
@@ -430,48 +428,28 @@ impl MacTextSystemState {
}
fn layout_line(&mut self, text: &str, font_size: Pixels, font_runs: &[FontRun]) -> LineLayout {
const ZWNJ: char = '\u{200C}';
const ZWNJ_STR: &str = "\u{200C}";
const ZWNJ_SIZE_16: usize = ZWNJ.len_utf16();
self.zwnjs_scratch_space.clear();
// Construct the attributed string, converting UTF8 ranges to UTF16 ranges.
let mut string = CFMutableAttributedString::new();
let mut max_ascent = 0.0f32;
let mut max_descent = 0.0f32;
{
let mut ix_converter = StringIndexConverter::new(&text);
let mut last_font_run = None;
string.replace_str(&CFString::new(text), CFRange::init(0, 0));
let utf16_line_len = string.char_len() as usize;
let mut ix_converter = StringIndexConverter::new(text);
for run in font_runs {
let text = &text[ix_converter.utf8_ix..][..run.len];
// if the fonts are the same, we need to disconnect the text with a ZWNJ
// to prevent core text from forming ligatures between them
let needs_zwnj = last_font_run.replace(run.font_id) == Some(run.font_id);
let utf8_end = ix_converter.utf8_ix + run.len;
let utf16_start = ix_converter.utf16_ix;
let n_zwnjs = self.zwnjs_scratch_space.len(); // from previous loop
let utf16_start = string.char_len(); // insert at end of string
ix_converter.advance_to_utf8_ix(ix_converter.utf8_ix + run.len);
// note: replace_str may silently ignore codepoints it dislikes (e.g., BOM at start of string)
string.replace_str(&CFString::new(text), CFRange::init(utf16_start, 0));
if needs_zwnj {
let zwnjs_pos = string.char_len();
self.zwnjs_scratch_space.push((n_zwnjs, zwnjs_pos as usize));
string.replace_str(
&CFString::from_static_string(ZWNJ_STR),
CFRange::init(zwnjs_pos, 0),
);
if utf16_start >= utf16_line_len {
break;
}
let utf16_end = string.char_len();
let cf_range = CFRange::init(utf16_start, utf16_end - utf16_start);
let font = &self.fonts[run.font_id.0];
ix_converter.advance_to_utf8_ix(utf8_end);
let utf16_end = cmp::min(ix_converter.utf16_ix, utf16_line_len);
let font_metrics = font.metrics();
let font_scale = font_size.0 / font_metrics.units_per_em as f32;
max_ascent = max_ascent.max(font_metrics.ascent * font_scale);
max_descent = max_descent.max(-font_metrics.descent * font_scale);
let cf_range =
CFRange::init(utf16_start as isize, (utf16_end - utf16_start) as isize);
let font: &FontKitFont = &self.fonts[run.font_id.0];
unsafe {
string.set_attribute(
@@ -480,12 +458,17 @@ impl MacTextSystemState {
&font.native_font().clone_with_font_size(font_size.into()),
);
}
if utf16_end == utf16_line_len {
break;
}
}
}
// Retrieve the glyphs from the shaped line, converting UTF16 offsets to UTF8 offsets.
let line = CTLine::new_with_attributed_string(string.as_concrete_TypeRef());
let glyph_runs = line.glyph_runs();
let mut runs = <Vec<ShapedRun>>::with_capacity(glyph_runs.len() as usize);
let mut runs = Vec::with_capacity(glyph_runs.len() as usize);
let mut ix_converter = StringIndexConverter::new(text);
for run in glyph_runs.into_iter() {
let attributes = run.attributes().unwrap();
@@ -497,63 +480,45 @@ impl MacTextSystemState {
};
let font_id = self.id_for_native_font(font);
let mut glyphs = match runs.last_mut() {
Some(run) if run.font_id == font_id => &mut run.glyphs,
_ => {
runs.push(ShapedRun {
font_id,
glyphs: Vec::with_capacity(run.glyph_count().try_into().unwrap_or(0)),
});
&mut runs.last_mut().unwrap().glyphs
}
};
for ((&glyph_id, position), &glyph_utf16_ix) in run
let mut glyphs = Vec::with_capacity(run.glyph_count().try_into().unwrap_or(0));
for ((glyph_id, position), glyph_utf16_ix) in run
.glyphs()
.iter()
.zip(run.positions().iter())
.zip(run.string_indices().iter())
{
let mut glyph_utf16_ix = usize::try_from(glyph_utf16_ix).unwrap();
let r = self
.zwnjs_scratch_space
.binary_search_by(|&(_, it)| it.cmp(&glyph_utf16_ix));
match r {
// this glyph is a ZWNJ, skip it
Ok(_) => continue,
// adjust the index to account for the ZWNJs we've inserted
Err(idx) => glyph_utf16_ix -= idx * ZWNJ_SIZE_16,
}
let glyph_utf16_ix = usize::try_from(*glyph_utf16_ix).unwrap();
if ix_converter.utf16_ix > glyph_utf16_ix {
// We cannot reuse current index converter, as it can only seek forward. Restart the search.
ix_converter = StringIndexConverter::new(text);
}
ix_converter.advance_to_utf16_ix(glyph_utf16_ix);
glyphs.push(ShapedGlyph {
id: GlyphId(glyph_id as u32),
id: GlyphId(*glyph_id as u32),
position: point(position.x as f32, position.y as f32).map(px),
index: ix_converter.utf8_ix,
is_emoji: self.is_emoji(font_id),
});
}
runs.push(ShapedRun { font_id, glyphs });
}
let typographic_bounds = line.get_typographic_bounds();
LineLayout {
runs,
font_size,
width: typographic_bounds.width.into(),
ascent: max_ascent.into(),
descent: max_descent.into(),
ascent: typographic_bounds.ascent.into(),
descent: typographic_bounds.descent.into(),
len: text.len(),
}
}
}
#[derive(Debug, Clone)]
#[derive(Clone)]
struct StringIndexConverter<'a> {
text: &'a str,
/// Index in UTF-8 bytes
utf8_ix: usize,
/// Index in UTF-16 code units
utf16_ix: usize,
}
@@ -734,113 +699,5 @@ mod tests {
assert_eq!(layout.runs[0].glyphs[0].id, GlyphId(68u32)); // a
// There's no glyph for \u{feff}
assert_eq!(layout.runs[0].glyphs[1].id, GlyphId(69u32)); // b
let line = "\u{feff}ab";
let font_runs = &[
FontRun {
len: "\u{feff}".len(),
font_id,
},
FontRun {
len: "ab".len(),
font_id,
},
];
let layout = fonts.layout_line(line, px(16.), font_runs);
assert_eq!(layout.len, line.len());
assert_eq!(layout.runs.len(), 1);
assert_eq!(layout.runs[0].glyphs.len(), 2);
// There's no glyph for \u{feff}
assert_eq!(layout.runs[0].glyphs[0].id, GlyphId(68u32)); // a
assert_eq!(layout.runs[0].glyphs[1].id, GlyphId(69u32)); // b
}
#[test]
fn test_layout_line_zwnj_insertion() {
let fonts = MacTextSystem::new();
let font_id = fonts.font_id(&font("Helvetica")).unwrap();
let text = "hello world";
let font_runs = &[
FontRun { font_id, len: 5 }, // "hello"
FontRun { font_id, len: 6 }, // " world"
];
let layout = fonts.layout_line(text, px(16.), font_runs);
assert_eq!(layout.len, text.len());
for run in &layout.runs {
for glyph in &run.glyphs {
assert!(
glyph.index < text.len(),
"Glyph index {} is out of bounds for text length {}",
glyph.index,
text.len()
);
}
}
// Test with different font runs - should not insert ZWNJ
let font_id2 = fonts.font_id(&font("Times")).unwrap_or(font_id);
let font_runs_different = &[
FontRun { font_id, len: 5 }, // "hello"
// " world"
FontRun {
font_id: font_id2,
len: 6,
},
];
let layout2 = fonts.layout_line(text, px(16.), font_runs_different);
assert_eq!(layout2.len, text.len());
for run in &layout2.runs {
for glyph in &run.glyphs {
assert!(
glyph.index < text.len(),
"Glyph index {} is out of bounds for text length {}",
glyph.index,
text.len()
);
}
}
}
#[test]
fn test_layout_line_zwnj_edge_cases() {
let fonts = MacTextSystem::new();
let font_id = fonts.font_id(&font("Helvetica")).unwrap();
let text = "hello";
let font_runs = &[FontRun { font_id, len: 5 }];
let layout = fonts.layout_line(text, px(16.), font_runs);
assert_eq!(layout.len, text.len());
let text = "abc";
let font_runs = &[
FontRun { font_id, len: 1 }, // "a"
FontRun { font_id, len: 1 }, // "b"
FontRun { font_id, len: 1 }, // "c"
];
let layout = fonts.layout_line(text, px(16.), font_runs);
assert_eq!(layout.len, text.len());
for run in &layout.runs {
for glyph in &run.glyphs {
assert!(
glyph.index < text.len(),
"Glyph index {} is out of bounds for text length {}",
glyph.index,
text.len()
);
}
}
// Test with empty text
let text = "";
let font_runs = &[];
let layout = fonts.layout_line(text, px(16.), font_runs);
assert_eq!(layout.len, 0);
assert!(layout.runs.is_empty());
}
}

View File

@@ -14,11 +14,10 @@ use windows::Win32::{
},
Dxgi::{
CreateDXGIFactory2, DXGI_CREATE_FACTORY_DEBUG, DXGI_CREATE_FACTORY_FLAGS,
IDXGIAdapter1, IDXGIFactory6,
DXGI_GPU_PREFERENCE_MINIMUM_POWER, IDXGIAdapter1, IDXGIFactory6,
},
},
};
use windows::core::Interface;
pub(crate) fn try_to_recover_from_device_lost<T>(
mut f: impl FnMut() -> Result<T>,
@@ -122,7 +121,10 @@ fn get_dxgi_factory(debug_layer_available: bool) -> Result<IDXGIFactory6> {
#[inline]
fn get_adapter(dxgi_factory: &IDXGIFactory6, debug_layer_available: bool) -> Result<IDXGIAdapter1> {
for adapter_index in 0.. {
let adapter: IDXGIAdapter1 = unsafe { dxgi_factory.EnumAdapters(adapter_index)?.cast()? };
let adapter: IDXGIAdapter1 = unsafe {
dxgi_factory
.EnumAdapterByGpuPreference(adapter_index, DXGI_GPU_PREFERENCE_MINIMUM_POWER)
}?;
if let Ok(desc) = unsafe { adapter.GetDesc1() } {
let gpu_name = String::from_utf16_lossy(&desc.Description)
.trim_matches(char::from(0))

Some files were not shown because too many files have changed in this diff Show More