Compare commits

...

28 Commits

Author SHA1 Message Date
Conrad Irwin
b3da4995b0 clipity 2024-06-04 23:16:07 -06:00
Conrad Irwin
23007f304a Toggle back to previous selection if you use this with no selection 2024-06-04 20:32:31 -06:00
Conrad Irwin
e1846f4dd3 More clippy 2024-06-04 20:32:27 -06:00
Kirill Shokhin
fb6cbbe7e0 Fix clippy warnings 2024-06-04 07:42:04 +03:00
Kirill Shokhin
6dba6860ea Add buffer_search tests for search in selections 2024-05-30 20:11:04 +03:00
Kirill Shokhin
f990bc050b Add tests for MultiBufferSnapshot::excerpts_in_ranges 2024-05-30 20:11:04 +03:00
Kirill Shokhin
9c7972d5db Fix clippy warnings 2024-05-30 20:11:04 +03:00
Kirill Shokhin
af2bbb190d Add default keybindings to toggle selection search
* cmd-alt-l for macos
* ctrl-alt-l for linux
* Toggle search ranges on BufferSearchBar deploy and update matches
2024-05-30 20:11:04 +03:00
Kirill Shokhin
8c450ae78c Search in selection for multibuffers 2024-05-30 20:11:04 +03:00
Kirill Shokhin
03f5c1b969 Remove concurrency from singleton buffer search
Likely the overhead from the tasks joining will be higher than the
profit from the concurrency
2024-05-30 20:11:04 +03:00
Kirill Shokhin
fea45a2d8b Disable search modes when search panel dismissed
I.e. set replace_enabled and selection_search_enabled flags to false
2024-05-30 20:11:04 +03:00
Kirill Shokhin
41f1f7f907 Expand selections to start and end of the selected lines 2024-05-30 20:11:04 +03:00
Kirill Shokhin
457c4c8fe0 Use search within ranges for search in selections
* Remove SearchOptions::SELECTION
* Make search in selection SearchableItem property instead of query
  property
* Move search in selections button out of the query text box
2024-05-30 20:11:04 +03:00
Kirill Shokhin
b09b74e32b Do not autofill search query with selected multiline text 2024-05-30 20:11:04 +03:00
Kirill Shokhin
85710c99f0 Add selection flag to supported search options list 2024-05-30 20:11:04 +03:00
Kirill Shokhin
ba2a4d789f Implement search in selections 2024-05-30 20:11:04 +03:00
Kirill Shokhin
c4cca7a527 Add toggle selection button to search bar 2024-05-30 20:11:04 +03:00
Marshall Bowers
39a2cdb13f rustdoc_to_markdown: Strip "Copy item path to clipboard" button (#12490)
This PR strips the "Copy item path to clipboard" button from the rustdoc
output.

Release Notes:

- N/A
2024-05-30 12:55:37 -04:00
Max Brunsfeld
8f942bf647 Use repository mutex more sparingly. Don't hold it while running git status. (#12489)
Previously, each git `Repository` object was held inside of a mutex.
This was needed because libgit2's Repository object is (as one would
expect) not thread safe. But now, the two longest-running git operations
that Zed performs, (`status` and `blame`) do not use libgit2 - they
invoke the `git` executable. For these operations, it's not necessary to
hold a lock on the repository.

In this PR, I've moved our mutex usage so that it only wraps the libgit2
calls, not our `git` subprocess spawns. The main user-facing impact of
this is that the UI is much more responsive when initially opening a
project with a very large git repository (e.g. `chromium`, `webkit`,
`linux`).

Release Notes:

- Improved Zed's responsiveness when initially opening a project
containing a very large git repository.
2024-05-30 09:37:11 -07:00
Bennet Bo Fenner
1ecd13ba50 Support copying permalink in multibuffer (#12435)
Closes #11392 

Release Notes:

- Added support for copying permalinks inside multi-buffers
([#11392](https://github.com/zed-industries/zed/issues/11392))
2024-05-30 18:36:24 +02:00
Marshall Bowers
c118012223 rustdoc_to_markdown: Add table support (#12488)
This PR extends `rustdoc_to_markdown` with support for tables:

<img width="1007" alt="Screenshot 2024-05-30 at 12 05 35 PM"
src="https://github.com/zed-industries/zed/assets/1486634/4e9a2a65-8aaa-4df1-98c4-4dd4e7874514">


Release Notes:

- N/A
2024-05-30 12:17:10 -04:00
Marshall Bowers
7a30937e21 Sort file_types.json (#12487)
This PR sorts the `file_types.json` file alphabetically.

This is the command I used to sort it:

```
pnpm --package=json-sort-cli dlx jsonsort assets/icons/file_icons/file_types.json
```

Release Notes:

- N/A
2024-05-30 11:26:52 -04:00
Kirill Bulatov
3c5d141a04 Force 60 minutes timeout for all regular CI jobs (#12486)
After gazing at
https://github.com/zed-industries/zed/actions/runs/9296132630/job/25596939148
for some time, I've decided to add a hard limit on every test-related CI
job.

Release Notes:

- N/A
2024-05-30 18:17:03 +03:00
Marshall Bowers
bf7c6a676a rustdoc_to_markdown: Recognize code blocks in other languages (#12484)
This PR updates `rustdoc_to_markdown` to be able to recognize code
blocks using non-Rust languages.

Release Notes:

- N/A
2024-05-30 10:50:27 -04:00
Antonio Scandurra
a259042f92 Make slash commands more discoverable (#12480)
<img width="648" alt="image"
src="https://github.com/zed-industries/zed/assets/482957/a63df904-fbbe-4e0a-80b2-c98ebee90690">

Release Notes:

- N/A

Co-authored-by: Nathan <nathan@zed.dev>
2024-05-30 16:45:05 +02:00
Sean Washington
436a8fa0ce php: Update Pest tree-sitter queries to capture single quotes (#12467)
Improved PHP Pest outline and runnables to support single quoted
arguments
([#12461](https://github.com/zed-industries/zed/issues/12461)).

Release Notes:

- N/A

| Before | After |
|--------|--------|
|
![image](https://github.com/zed-industries/zed/assets/428033/e0966510-da11-4a80-8901-7dba541ab721)
| ![CleanShot 2024-05-29 at 20 13
00@2x](https://github.com/zed-industries/zed/assets/428033/5f7ab492-2791-4a04-9ec3-f0adfa9b2986)
|
| ![CleanShot 2024-05-29 at 20 18
11@2x](https://github.com/zed-industries/zed/assets/428033/ac6bf58b-4e7d-410d-af51-328c41a76ba0)
| ![CleanShot 2024-05-29 at 20 14
35@2x](https://github.com/zed-industries/zed/assets/428033/1d226bb8-f102-4171-906d-e122ab8299cf)
|
2024-05-30 16:37:41 +02:00
Antonio Scandurra
55c47305c8 Align the inline assistant correctly (#12478)
Release Notes:

- Fixed the the alignment for the inline assistant.
2024-05-30 14:29:17 +02:00
Antonio Scandurra
6ff01b17ca Improve model selection in the assistant (#12472)
https://github.com/zed-industries/zed/assets/482957/3b017850-b7b6-457a-9b2f-324d5533442e


Release Notes:

- Improved the UX for selecting a model in the assistant panel. You can
now switch model using just the keyboard by pressing `alt-m`. Also, when
switching models via the UI, settings will now be updated automatically.
2024-05-30 12:36:07 +02:00
54 changed files with 1847 additions and 612 deletions

View File

@@ -23,6 +23,7 @@ env:
jobs:
style:
timeout-minutes: 60
name: Check formatting and spelling
runs-on:
- self-hosted
@@ -77,6 +78,7 @@ jobs:
against: "https://github.com/${GITHUB_REPOSITORY}.git#branch=${BUF_BASE_BRANCH},subdir=crates/rpc/proto/"
macos_tests:
timeout-minutes: 60
name: (macOS) Run Clippy and tests
runs-on:
- self-hosted
@@ -101,6 +103,7 @@ jobs:
# todo(linux): Actually run the tests
linux_tests:
timeout-minutes: 60
name: (Linux) Run Clippy and tests
runs-on:
- self-hosted
@@ -122,6 +125,7 @@ jobs:
# todo(windows): Actually run the tests
windows_tests:
timeout-minutes: 60
name: (Windows) Run Clippy and tests
runs-on: hosted-windows-1
steps:
@@ -142,6 +146,7 @@ jobs:
run: cargo build -p zed
bundle-mac:
timeout-minutes: 60
name: Create a macOS bundle
runs-on:
- self-hosted
@@ -252,6 +257,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
bundle-linux:
timeout-minutes: 60
name: Create a Linux bundle
runs-on:
- self-hosted

View File

@@ -15,6 +15,7 @@ env:
jobs:
style:
timeout-minutes: 60
name: Check formatting and Clippy lints
if: github.repository_owner == 'zed-industries'
runs-on:
@@ -33,6 +34,7 @@ jobs:
- name: Run clippy
run: cargo xtask clippy
tests:
timeout-minutes: 60
name: Run tests
if: github.repository_owner == 'zed-industries'
runs-on:
@@ -49,6 +51,7 @@ jobs:
uses: ./.github/actions/run_tests
bundle-mac:
timeout-minutes: 60
name: Create a macOS bundle
if: github.repository_owner == 'zed-industries'
runs-on:
@@ -91,6 +94,7 @@ jobs:
run: script/upload-nightly macos
bundle-deb:
timeout-minutes: 60
name: Create a Linux *.tar.gz bundle
if: github.repository_owner == 'zed-industries'
runs-on:

3
Cargo.lock generated
View File

@@ -230,6 +230,7 @@ dependencies = [
"schemars",
"serde",
"serde_json",
"strum",
"tokio",
]
@@ -376,6 +377,7 @@ dependencies = [
"settings",
"smol",
"strsim 0.11.1",
"strum",
"telemetry_events",
"theme",
"tiktoken-rs",
@@ -6983,6 +6985,7 @@ dependencies = [
"schemars",
"serde",
"serde_json",
"strum",
]
[[package]]

View File

@@ -1,15 +1,15 @@
{
"stems": {
"Dockerfile": "docker",
"Podfile": "ruby",
"Procfile": "heroku",
"Dockerfile": "docker"
"Procfile": "heroku"
},
"suffixes": {
"astro": "astro",
"Emakefile": "erlang",
"aac": "audio",
"accdb": "storage",
"app.src": "erlang",
"astro": "astro",
"avi": "video",
"avif": "image",
"bak": "backup",
@@ -22,12 +22,12 @@
"c": "c",
"cc": "cpp",
"cjs": "javascript",
"coffee": "coffeescript",
"conf": "settings",
"cpp": "cpp",
"css": "css",
"csv": "storage",
"cts": "typescript",
"coffee": "coffeescript",
"dart": "dart",
"dat": "storage",
"db": "storage",
@@ -61,12 +61,12 @@
"graphql": "graphql",
"graphqls": "graphql",
"h": "c",
"hpp": "cpp",
"handlebars": "code",
"hbs": "template",
"heex": "elixir",
"heif": "image",
"heic": "image",
"heif": "image",
"hpp": "cpp",
"hrl": "erlang",
"hs": "haskell",
"htm": "template",
@@ -81,9 +81,9 @@
"jpeg": "image",
"jpg": "image",
"js": "javascript",
"jsx": "react",
"json": "storage",
"jsonc": "storage",
"jsx": "react",
"jxl": "image",
"kt": "kotlin",
"ldf": "storage",
@@ -98,9 +98,9 @@
"mdf": "storage",
"mdx": "document",
"metadata": "code",
"mkv": "video",
"mjs": "javascript",
"mka": "audio",
"mkv": "video",
"ml": "ocaml",
"mli": "ocaml",
"mov": "video",
@@ -109,8 +109,8 @@
"mts": "typescript",
"myd": "storage",
"myi": "storage",
"nu": "terminal",
"nim": "nim",
"nu": "terminal",
"odp": "document",
"ods": "document",
"odt": "document",
@@ -132,33 +132,33 @@
"psd": "image",
"py": "python",
"qoi": "image",
"r": "r",
"rb": "ruby",
"rebar.config": "erlang",
"rkt": "code",
"rs": "rust",
"r": "r",
"rtf": "document",
"sav": "storage",
"sc": "scala",
"scala": "scala",
"scm": "code",
"sdf": "storage",
"sh": "terminal",
"sql": "storage",
"sqlite": "storage",
"svelte": "template",
"svg": "image",
"sc": "scala",
"scala": "scala",
"sql": "storage",
"swift": "swift",
"tcl": "tcl",
"tf": "terraform",
"tfvars": "terraform",
"tiff": "image",
"toml": "toml",
"ts": "typescript",
"tsv": "storage",
"ttf": "font",
"tsx": "react",
"ttf": "font",
"txt": "document",
"tcl": "tcl",
"vue": "vue",
"wav": "audio",
"webm": "video",
@@ -190,27 +190,30 @@
"audio": {
"icon": "icons/file_icons/audio.svg"
},
"bun": {
"icon": "icons/file_icons/bun.svg"
},
"c": {
"icon": "icons/file_icons/c.svg"
},
"code": {
"icon": "icons/file_icons/code.svg"
},
"coffeescript": {
"icon": "icons/file_icons/coffeescript.svg"
},
"collapsed_chevron": {
"icon": "icons/file_icons/chevron_right.svg"
},
"collapsed_folder": {
"icon": "icons/file_icons/folder.svg"
},
"c": {
"icon": "icons/file_icons/c.svg"
},
"cpp": {
"icon": "icons/file_icons/cpp.svg"
},
"css": {
"icon": "icons/file_icons/css.svg"
},
"coffeescript": {
"icon": "icons/file_icons/coffeescript.svg"
},
"dart": {
"icon": "icons/file_icons/dart.svg"
},
@@ -247,18 +250,18 @@
"fsharp": {
"icon": "icons/file_icons/fsharp.svg"
},
"haskell": {
"icon": "icons/file_icons/haskell.svg"
},
"heroku": {
"icon": "icons/file_icons/heroku.svg"
},
"go": {
"icon": "icons/file_icons/go.svg"
},
"graphql": {
"icon": "icons/file_icons/graphql.svg"
},
"haskell": {
"icon": "icons/file_icons/haskell.svg"
},
"heroku": {
"icon": "icons/file_icons/heroku.svg"
},
"image": {
"icon": "icons/file_icons/image.svg"
},
@@ -274,21 +277,18 @@
"lock": {
"icon": "icons/file_icons/lock.svg"
},
"bun": {
"icon": "icons/file_icons/bun.svg"
},
"log": {
"icon": "icons/file_icons/info.svg"
},
"lua": {
"icon": "icons/file_icons/lua.svg"
},
"ocaml": {
"icon": "icons/file_icons/ocaml.svg"
},
"nim": {
"icon": "icons/file_icons/nim.svg"
},
"ocaml": {
"icon": "icons/file_icons/ocaml.svg"
},
"phoenix": {
"icon": "icons/file_icons/phoenix.svg"
},
@@ -316,36 +316,36 @@
"rust": {
"icon": "icons/file_icons/rust.svg"
},
"scala": {
"icon": "icons/file_icons/scala.svg"
},
"settings": {
"icon": "icons/file_icons/settings.svg"
},
"storage": {
"icon": "icons/file_icons/database.svg"
},
"scala": {
"icon": "icons/file_icons/scala.svg"
},
"swift": {
"icon": "icons/file_icons/swift.svg"
},
"tcl": {
"icon": "icons/file_icons/tcl.svg"
},
"template": {
"icon": "icons/file_icons/html.svg"
},
"terraform": {
"icon": "icons/file_icons/terraform.svg"
},
"terminal": {
"icon": "icons/file_icons/terminal.svg"
},
"terraform": {
"icon": "icons/file_icons/terraform.svg"
},
"toml": {
"icon": "icons/file_icons/toml.svg"
},
"typescript": {
"icon": "icons/file_icons/typescript.svg"
},
"tcl": {
"icon": "icons/file_icons/tcl.svg"
},
"vcs": {
"icon": "icons/file_icons/git.svg"
},

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-text-quote"><path d="M17 6H3"/><path d="M21 12H8"/><path d="M21 18H8"/><path d="M3 12v6"/></svg>

After

Width:  |  Height:  |  Size: 299 B

View File

@@ -201,7 +201,8 @@
"context": "AssistantPanel",
"bindings": {
"ctrl-g": "search::SelectNextMatch",
"ctrl-shift-g": "search::SelectPrevMatch"
"ctrl-shift-g": "search::SelectPrevMatch",
"alt-m": "assistant::ToggleModelSelector"
}
},
{
@@ -225,7 +226,8 @@
"shift-enter": "search::SelectPrevMatch",
"alt-enter": "search::SelectAllMatches",
"ctrl-f": "search::FocusSearch",
"ctrl-h": "search::ToggleReplace"
"ctrl-h": "search::ToggleReplace",
"ctrl-l": "search::ToggleSelection"
}
},
{
@@ -289,6 +291,7 @@
"ctrl-alt-g": "search::SelectNextMatch",
"ctrl-alt-shift-g": "search::SelectPrevMatch",
"ctrl-alt-shift-h": "search::ToggleReplace",
"ctrl-alt-shift-l": "search::ToggleSelection",
"alt-enter": "search::SelectAllMatches",
"alt-c": "search::ToggleCaseSensitive",
"alt-w": "search::ToggleWholeWord",

View File

@@ -176,6 +176,12 @@
"replace_enabled": true
}
],
"cmd-alt-l": [
"buffer_search::Deploy",
{
"selection_search_enabled": true
}
],
"cmd-e": [
"buffer_search::Deploy",
{
@@ -214,10 +220,11 @@
}
},
{
"context": "AssistantPanel", // Used in the assistant crate, which we're replacing
"context": "AssistantPanel",
"bindings": {
"cmd-g": "search::SelectNextMatch",
"cmd-shift-g": "search::SelectPrevMatch"
"cmd-shift-g": "search::SelectPrevMatch",
"alt-m": "assistant::ToggleModelSelector"
}
},
{
@@ -241,7 +248,8 @@
"shift-enter": "search::SelectPrevMatch",
"alt-enter": "search::SelectAllMatches",
"cmd-f": "search::FocusSearch",
"cmd-alt-f": "search::ToggleReplace"
"cmd-alt-f": "search::ToggleReplace",
"cmd-alt-l": "search::ToggleSelection"
}
},
{
@@ -307,6 +315,7 @@
"cmd-g": "search::SelectNextMatch",
"cmd-shift-g": "search::SelectPrevMatch",
"cmd-shift-h": "search::ToggleReplace",
"cmd-alt-l": "search::ToggleSelection",
"alt-enter": "search::SelectAllMatches",
"alt-cmd-c": "search::ToggleCaseSensitive",
"alt-cmd-w": "search::ToggleWholeWord",

View File

@@ -23,6 +23,7 @@ isahc.workspace = true
schemars = { workspace = true, optional = true }
serde.workspace = true
serde_json.workspace = true
strum.workspace = true
[dev-dependencies]
tokio.workspace = true

View File

@@ -4,11 +4,12 @@ use http::{AsyncBody, HttpClient, Method, Request as HttpRequest};
use isahc::config::Configurable;
use serde::{Deserialize, Serialize};
use std::{convert::TryFrom, time::Duration};
use strum::EnumIter;
pub const ANTHROPIC_API_URL: &'static str = "https://api.anthropic.com";
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, EnumIter)]
pub enum Model {
#[default]
#[serde(alias = "claude-3-opus", rename = "claude-3-opus-20240229")]

View File

@@ -49,6 +49,7 @@ serde_json.workspace = true
settings.workspace = true
smol.workspace = true
strsim = "0.11"
strum.workspace = true
telemetry_events.workspace = true
theme.workspace = true
tiktoken-rs.workspace = true

View File

@@ -2,6 +2,7 @@ pub mod assistant_panel;
pub mod assistant_settings;
mod codegen;
mod completion_provider;
mod model_selector;
mod prompts;
mod saved_conversation;
mod search;
@@ -15,6 +16,7 @@ use client::{proto, Client};
use command_palette_hooks::CommandPaletteFilter;
pub(crate) use completion_provider::*;
use gpui::{actions, AppContext, Global, SharedString, UpdateGlobal};
pub(crate) use model_selector::*;
pub(crate) use saved_conversation::*;
use semantic_index::{CloudEmbeddingProvider, SemanticIndex};
use serde::{Deserialize, Serialize};
@@ -38,7 +40,8 @@ actions!(
InsertActivePrompt,
ToggleHistory,
ApplyEdit,
ConfirmCommand
ConfirmCommand,
ToggleModelSelector
]
);

View File

@@ -1,7 +1,7 @@
use crate::prompts::{generate_content_prompt, PromptLibrary, PromptManager};
use crate::slash_command::{rustdoc_command, search_command, tabs_command};
use crate::{
assistant_settings::{AssistantDockPosition, AssistantSettings, ZedDotDevModel},
assistant_settings::{AssistantDockPosition, AssistantSettings},
codegen::{self, Codegen, CodegenKind},
search::*,
slash_command::{
@@ -9,17 +9,18 @@ use crate::{
SlashCommandCompletionProvider, SlashCommandLine, SlashCommandRegistry,
},
ApplyEdit, Assist, CompletionProvider, ConfirmCommand, CycleMessageRole, InlineAssist,
LanguageModel, LanguageModelRequest, LanguageModelRequestMessage, MessageId, MessageMetadata,
MessageStatus, QuoteSelection, ResetKey, Role, SavedConversation, SavedConversationMetadata,
SavedMessage, Split, ToggleFocus, ToggleHistory,
LanguageModelRequest, LanguageModelRequestMessage, MessageId, MessageMetadata, MessageStatus,
QuoteSelection, ResetKey, Role, SavedConversation, SavedConversationMetadata, SavedMessage,
Split, ToggleFocus, ToggleHistory,
};
use crate::{ModelSelector, ToggleModelSelector};
use anyhow::{anyhow, Result};
use assistant_slash_command::{SlashCommandOutput, SlashCommandOutputSection};
use client::telemetry::Telemetry;
use collections::{hash_map, BTreeSet, HashMap, HashSet, VecDeque};
use editor::actions::UnfoldAt;
use editor::actions::ShowCompletions;
use editor::{
actions::{FoldAt, MoveDown, MoveUp},
actions::{FoldAt, MoveDown, MoveToEndOfLine, MoveUp, Newline, UnfoldAt},
display_map::{
BlockContext, BlockDisposition, BlockId, BlockProperties, BlockStyle, Flap, ToDisplayPoint,
},
@@ -64,8 +65,8 @@ use std::{
use telemetry_events::AssistantKind;
use theme::ThemeSettings;
use ui::{
popover_menu, prelude::*, ButtonLike, ContextMenu, ElevationIndex, KeyBinding, Tab, TabBar,
Tooltip,
popover_menu, prelude::*, ButtonLike, ContextMenu, ElevationIndex, KeyBinding,
PopoverMenuHandle, Tab, TabBar, Tooltip,
};
use util::{paths::CONVERSATIONS_DIR, post_inc, ResultExt, TryFutureExt};
use uuid::Uuid;
@@ -119,8 +120,8 @@ pub struct AssistantPanel {
pending_inline_assist_ids_by_editor: HashMap<WeakView<Editor>, Vec<usize>>,
inline_prompt_history: VecDeque<String>,
_watch_saved_conversations: Task<Result<()>>,
model: LanguageModel,
authentication_prompt: Option<AnyView>,
model_menu_handle: PopoverMenuHandle<ContextMenu>,
}
struct ActiveConversationEditor {
@@ -203,7 +204,6 @@ impl AssistantPanel {
}
}),
];
let model = CompletionProvider::global(cx).default_model();
cx.observe_global::<FileIcons>(|_, cx| {
cx.notify();
@@ -212,15 +212,20 @@ impl AssistantPanel {
let slash_command_registry = SlashCommandRegistry::global(cx);
slash_command_registry.register_command(file_command::FileSlashCommand);
slash_command_registry.register_command(file_command::FileSlashCommand, true);
slash_command_registry.register_command(
prompt_command::PromptSlashCommand::new(prompt_library.clone()),
true,
);
slash_command_registry.register_command(active_command::ActiveSlashCommand);
slash_command_registry.register_command(tabs_command::TabsSlashCommand);
slash_command_registry.register_command(project_command::ProjectSlashCommand);
slash_command_registry.register_command(search_command::SearchSlashCommand);
slash_command_registry.register_command(rustdoc_command::RustdocSlashCommand);
slash_command_registry
.register_command(active_command::ActiveSlashCommand, true);
slash_command_registry.register_command(tabs_command::TabsSlashCommand, true);
slash_command_registry
.register_command(project_command::ProjectSlashCommand, true);
slash_command_registry
.register_command(search_command::SearchSlashCommand, true);
slash_command_registry
.register_command(rustdoc_command::RustdocSlashCommand, false);
Self {
workspace: workspace_handle,
@@ -244,8 +249,8 @@ impl AssistantPanel {
pending_inline_assist_ids_by_editor: Default::default(),
inline_prompt_history: Default::default(),
_watch_saved_conversations,
model,
authentication_prompt: None,
model_menu_handle: PopoverMenuHandle::default(),
}
})
})
@@ -277,12 +282,20 @@ impl AssistantPanel {
if self.is_authenticated(cx) {
self.authentication_prompt = None;
let model = CompletionProvider::global(cx).default_model();
self.set_model(model, cx);
if let Some(editor) = self.active_conversation_editor() {
editor.update(cx, |active_conversation, cx| {
active_conversation
.conversation
.update(cx, |conversation, cx| {
conversation.completion_provider_changed(cx)
})
})
}
if self.active_conversation_editor().is_none() {
self.new_conversation(cx);
}
cx.notify();
} else if self.authentication_prompt.is_none()
|| prev_settings_version != CompletionProvider::global(cx).settings_version()
{
@@ -290,6 +303,7 @@ impl AssistantPanel {
Some(cx.update_global::<CompletionProvider, _>(|provider, cx| {
provider.authentication_prompt(cx)
}));
cx.notify();
}
}
@@ -438,8 +452,8 @@ impl AssistantPanel {
let inline_assistant = inline_assistant.clone();
move |cx: &mut BlockContext| {
*measurements.lock() = BlockMeasurements {
anchor_x: cx.anchor_x,
gutter_width: cx.gutter_dimensions.width,
gutter_margin: cx.gutter_dimensions.margin,
};
inline_assistant.clone().into_any_element()
}
@@ -734,7 +748,7 @@ impl AssistantPanel {
.map(|message| message.to_request_message(buffer)),
);
}
let model = self.model.clone();
let model = CompletionProvider::global(cx).model();
cx.spawn(|_, mut cx| async move {
// I Don't know if we want to return a ? here.
@@ -809,7 +823,6 @@ impl AssistantPanel {
let editor = cx.new_view(|cx| {
ConversationEditor::new(
self.model.clone(),
self.languages.clone(),
self.slash_commands.clone(),
self.fs.clone(),
@@ -850,53 +863,6 @@ impl AssistantPanel {
cx.notify();
}
fn cycle_model(&mut self, cx: &mut ViewContext<Self>) {
let next_model = match &self.model {
LanguageModel::OpenAi(model) => LanguageModel::OpenAi(match &model {
open_ai::Model::ThreePointFiveTurbo => open_ai::Model::Four,
open_ai::Model::Four => open_ai::Model::FourTurbo,
open_ai::Model::FourTurbo => open_ai::Model::FourOmni,
open_ai::Model::FourOmni => open_ai::Model::ThreePointFiveTurbo,
}),
LanguageModel::Anthropic(model) => LanguageModel::Anthropic(match &model {
anthropic::Model::Claude3Opus => anthropic::Model::Claude3Sonnet,
anthropic::Model::Claude3Sonnet => anthropic::Model::Claude3Haiku,
anthropic::Model::Claude3Haiku => anthropic::Model::Claude3Opus,
}),
LanguageModel::ZedDotDev(model) => LanguageModel::ZedDotDev(match &model {
ZedDotDevModel::Gpt3Point5Turbo => ZedDotDevModel::Gpt4,
ZedDotDevModel::Gpt4 => ZedDotDevModel::Gpt4Turbo,
ZedDotDevModel::Gpt4Turbo => ZedDotDevModel::Gpt4Omni,
ZedDotDevModel::Gpt4Omni => ZedDotDevModel::Claude3Opus,
ZedDotDevModel::Claude3Opus => ZedDotDevModel::Claude3Sonnet,
ZedDotDevModel::Claude3Sonnet => ZedDotDevModel::Claude3Haiku,
ZedDotDevModel::Claude3Haiku => {
match CompletionProvider::global(cx).default_model() {
LanguageModel::ZedDotDev(custom @ ZedDotDevModel::Custom(_)) => custom,
_ => ZedDotDevModel::Gpt3Point5Turbo,
}
}
ZedDotDevModel::Custom(_) => ZedDotDevModel::Gpt3Point5Turbo,
}),
};
self.set_model(next_model, cx);
}
fn set_model(&mut self, model: LanguageModel, cx: &mut ViewContext<Self>) {
self.model = model.clone();
if let Some(editor) = self.active_conversation_editor() {
editor.update(cx, |active_conversation, cx| {
active_conversation
.conversation
.update(cx, |conversation, cx| {
conversation.set_model(model, cx);
})
})
}
cx.notify();
}
fn handle_conversation_editor_event(
&mut self,
_: View<ConversationEditor>,
@@ -978,6 +944,18 @@ impl AssistantPanel {
.detach_and_log_err(cx);
}
fn toggle_model_selector(&mut self, _: &ToggleModelSelector, cx: &mut ViewContext<Self>) {
self.model_menu_handle.toggle(cx);
}
fn insert_command(&mut self, name: &str, cx: &mut ViewContext<Self>) {
if let Some(conversation_editor) = self.active_conversation_editor() {
conversation_editor.update(cx, |conversation_editor, cx| {
conversation_editor.insert_command(name, cx)
});
}
}
fn active_conversation_editor(&self) -> Option<&View<ConversationEditor>> {
Some(&self.active_conversation_editor.as_ref()?.editor)
}
@@ -1015,52 +993,65 @@ impl AssistantPanel {
})
}
fn render_inject_context_menu(&self, _cx: &mut ViewContext<Self>) -> impl Element {
let workspace = self.workspace.clone();
fn render_inject_context_menu(&self, cx: &mut ViewContext<Self>) -> impl Element {
let commands = self.slash_commands.clone();
let assistant_panel = cx.view().downgrade();
let active_editor_focus_handle = self.workspace.upgrade().and_then(|workspace| {
Some(
workspace
.read(cx)
.active_item_as::<Editor>(cx)?
.focus_handle(cx),
)
});
popover_menu("inject-context-menu")
.trigger(IconButton::new("trigger", IconName::Quote).tooltip(|cx| {
// Tooltip::with_meta("Insert Context", None, "Type # to insert via keyboard", cx)
Tooltip::text("Insert Context", cx)
Tooltip::with_meta("Insert Context", None, "Type / to insert via keyboard", cx)
}))
.menu(move |cx| {
ContextMenu::build(cx, |menu, _cx| {
// menu.entry("Insert Search", None, {
// let assistant = assistant.clone();
// move |_cx| {}
// })
// .entry("Insert Docs", None, {
// let assistant = assistant.clone();
// move |cx| {}
// })
menu.entry("Quote Selection", None, {
let workspace = workspace.clone();
move |cx| {
workspace
.update(cx, |workspace, cx| {
ConversationEditor::quote_selection(
workspace,
&Default::default(),
cx,
)
})
.ok();
ContextMenu::build(cx, |mut menu, _cx| {
for command_name in commands.featured_command_names() {
if let Some(command) = commands.command(&command_name) {
let menu_text = SharedString::from(Arc::from(command.menu_text()));
menu = menu.custom_entry(
{
let command_name = command_name.clone();
move |_cx| {
h_flex()
.w_full()
.justify_between()
.child(Label::new(menu_text.clone()))
.child(
div().ml_4().child(
Label::new(format!("/{command_name}"))
.color(Color::Muted),
),
)
.into_any()
}
},
{
let assistant_panel = assistant_panel.clone();
move |cx| {
assistant_panel
.update(cx, |assistant_panel, cx| {
assistant_panel.insert_command(&command_name, cx)
})
.ok();
}
},
)
}
})
// .entry("Insert Active Prompt", None, {
// let workspace = workspace.clone();
// move |cx| {
// workspace
// .update(cx, |workspace, cx| {
// ConversationEditor::insert_active_prompt(
// workspace,
// &Default::default(),
// cx,
// )
// })
// .ok();
// }
// })
}
if let Some(active_editor_focus_handle) = active_editor_focus_handle.clone() {
menu = menu
.context(active_editor_focus_handle)
.action("Quote Selection", Box::new(QuoteSelection));
}
menu
})
.into()
})
@@ -1133,10 +1124,8 @@ impl AssistantPanel {
cx.spawn(|this, mut cx| async move {
let saved_conversation = SavedConversation::load(&path, fs.as_ref()).await?;
let model = this.update(&mut cx, |this, _| this.model.clone())?;
let conversation = Conversation::deserialize(
saved_conversation,
model,
path.clone(),
languages,
slash_commands,
@@ -1206,7 +1195,10 @@ impl AssistantPanel {
this.child(
h_flex()
.gap_1()
.child(self.render_model(&conversation, cx))
.child(ModelSelector::new(
self.model_menu_handle.clone(),
self.fs.clone(),
))
.children(self.render_remaining_tokens(&conversation, cx)),
)
.child(
@@ -1256,6 +1248,7 @@ impl AssistantPanel {
.on_action(cx.listener(AssistantPanel::select_prev_match))
.on_action(cx.listener(AssistantPanel::handle_editor_cancel))
.on_action(cx.listener(AssistantPanel::reset_credentials))
.on_action(cx.listener(AssistantPanel::toggle_model_selector))
.track_focus(&self.focus_handle)
.child(header)
.children(if self.toolbar.read(cx).hidden() {
@@ -1314,23 +1307,12 @@ impl AssistantPanel {
))
}
fn render_model(
&self,
conversation: &Model<Conversation>,
cx: &mut ViewContext<Self>,
) -> impl IntoElement {
Button::new("current_model", conversation.read(cx).model.display_name())
.style(ButtonStyle::Filled)
.tooltip(move |cx| Tooltip::text("Change Model", cx))
.on_click(cx.listener(|this, _, cx| this.cycle_model(cx)))
}
fn render_remaining_tokens(
&self,
conversation: &Model<Conversation>,
cx: &mut ViewContext<Self>,
) -> Option<impl IntoElement> {
let remaining_tokens = conversation.read(cx).remaining_tokens()?;
let remaining_tokens = conversation.read(cx).remaining_tokens(cx)?;
let remaining_tokens_color = if remaining_tokens <= 0 {
Color::Error
} else if remaining_tokens <= 500 {
@@ -1486,7 +1468,6 @@ pub struct Conversation {
pending_summary: Task<Option<()>>,
completion_count: usize,
pending_completions: Vec<PendingCompletion>,
model: LanguageModel,
token_count: Option<usize>,
pending_token_count: Task<Option<()>>,
pending_edit_suggestion_parse: Option<Task<()>>,
@@ -1502,7 +1483,6 @@ impl EventEmitter<ConversationEvent> for Conversation {}
impl Conversation {
fn new(
model: LanguageModel,
language_registry: Arc<LanguageRegistry>,
slash_command_registry: Arc<SlashCommandRegistry>,
telemetry: Option<Arc<Telemetry>>,
@@ -1530,7 +1510,6 @@ impl Conversation {
token_count: None,
pending_token_count: Task::ready(None),
pending_edit_suggestion_parse: None,
model,
_subscriptions: vec![cx.subscribe(&buffer, Self::handle_buffer_event)],
pending_save: Task::ready(Ok(())),
path: None,
@@ -1583,7 +1562,6 @@ impl Conversation {
#[allow(clippy::too_many_arguments)]
async fn deserialize(
saved_conversation: SavedConversation,
model: LanguageModel,
path: PathBuf,
language_registry: Arc<LanguageRegistry>,
slash_command_registry: Arc<SlashCommandRegistry>,
@@ -1640,7 +1618,6 @@ impl Conversation {
token_count: None,
pending_edit_suggestion_parse: None,
pending_token_count: Task::ready(None),
model,
_subscriptions: vec![cx.subscribe(&buffer, Self::handle_buffer_event)],
pending_save: Task::ready(Ok(())),
path: Some(path),
@@ -1769,7 +1746,6 @@ impl Conversation {
let pending_command = PendingSlashCommand {
name: name.to_string(),
argument: argument.map(ToString::to_string),
tooltip_text: command.tooltip_text().into(),
source_range,
status: PendingSlashCommandStatus::Idle,
};
@@ -1938,12 +1914,12 @@ impl Conversation {
}
}
fn remaining_tokens(&self) -> Option<isize> {
Some(self.model.max_token_count() as isize - self.token_count? as isize)
fn remaining_tokens(&self, cx: &AppContext) -> Option<isize> {
let model = CompletionProvider::global(cx).model();
Some(model.max_token_count() as isize - self.token_count? as isize)
}
fn set_model(&mut self, model: LanguageModel, cx: &mut ModelContext<Self>) {
self.model = model;
fn completion_provider_changed(&mut self, cx: &mut ModelContext<Self>) {
self.count_remaining_tokens(cx);
}
@@ -2079,10 +2055,11 @@ impl Conversation {
}
if let Some(telemetry) = this.telemetry.as_ref() {
let model = CompletionProvider::global(cx).model();
telemetry.report_assistant_event(
this.id.clone(),
AssistantKind::Panel,
this.model.telemetry_id(),
model.telemetry_id(),
response_latency,
error_message,
);
@@ -2111,7 +2088,7 @@ impl Conversation {
.map(|message| message.to_request_message(self.buffer.read(cx)));
LanguageModelRequest {
model: self.model.clone(),
model: CompletionProvider::global(cx).model(),
messages: messages.collect(),
stop: vec![],
temperature: 1.0,
@@ -2300,7 +2277,7 @@ impl Conversation {
.into(),
}));
let request = LanguageModelRequest {
model: self.model.clone(),
model: CompletionProvider::global(cx).model(),
messages: messages.collect(),
stop: vec![],
temperature: 1.0,
@@ -2565,7 +2542,6 @@ struct PendingSlashCommand {
argument: Option<String>,
status: PendingSlashCommandStatus,
source_range: Range<language::Anchor>,
tooltip_text: SharedString,
}
#[derive(Clone)]
@@ -2605,7 +2581,6 @@ pub struct ConversationEditor {
impl ConversationEditor {
fn new(
model: LanguageModel,
language_registry: Arc<LanguageRegistry>,
slash_command_registry: Arc<SlashCommandRegistry>,
fs: Arc<dyn Fs>,
@@ -2618,7 +2593,6 @@ impl ConversationEditor {
let conversation = cx.new_model(|cx| {
Conversation::new(
model,
language_registry,
slash_command_registry,
Some(telemetry),
@@ -2740,11 +2714,47 @@ impl ConversationEditor {
.collect()
}
fn insert_command(&mut self, name: &str, cx: &mut ViewContext<Self>) {
if let Some(command) = self.slash_command_registry.command(name) {
self.editor.update(cx, |editor, cx| {
editor.transact(cx, |editor, cx| {
editor.change_selections(Some(Autoscroll::fit()), cx, |s| s.try_cancel());
let snapshot = editor.buffer().read(cx).snapshot(cx);
let newest_cursor = editor.selections.newest::<Point>(cx).head();
if newest_cursor.column > 0
|| snapshot
.chars_at(newest_cursor)
.next()
.map_or(false, |ch| ch != '\n')
{
editor.move_to_end_of_line(
&MoveToEndOfLine {
stop_at_soft_wraps: false,
},
cx,
);
editor.newline(&Newline, cx);
}
editor.insert(&format!("/{name}"), cx);
if command.requires_argument() {
editor.insert(" ", cx);
editor.show_completions(&ShowCompletions, cx);
}
});
});
if !command.requires_argument() {
self.confirm_command(&ConfirmCommand, cx);
}
}
}
pub fn confirm_command(&mut self, _: &ConfirmCommand, cx: &mut ViewContext<Self>) {
let selections = self.editor.read(cx).selections.disjoint_anchors();
let mut commands_by_range = HashMap::default();
let workspace = self.workspace.clone();
self.conversation.update(cx, |conversation, cx| {
conversation.reparse_slash_commands(cx);
for selection in selections.iter() {
if let Some(command) =
conversation.pending_command_for_position(selection.head().text_anchor, cx)
@@ -2901,9 +2911,8 @@ impl ConversationEditor {
let confirm_command = confirm_command.clone();
let command = command.clone();
move |row, _, _, _cx: &mut WindowContext| {
render_pending_slash_command_toggle(
render_pending_slash_command_gutter_decoration(
row,
command.tooltip_text.clone(),
command.status.clone(),
confirm_command.clone(),
)
@@ -3515,8 +3524,7 @@ impl Render for InlineAssistant {
.on_action(cx.listener(Self::move_down))
.child(
h_flex()
.justify_center()
.w(measurements.gutter_width)
.w(measurements.gutter_width + measurements.gutter_margin)
.children(if let Some(error) = self.codegen.read(cx).error() {
let error_message = SharedString::from(error.to_string());
Some(
@@ -3529,12 +3537,7 @@ impl Render for InlineAssistant {
None
}),
)
.child(
h_flex()
.w_full()
.ml(measurements.anchor_x - measurements.gutter_width)
.child(self.render_prompt_editor(cx)),
)
.child(h_flex().flex_1().child(self.render_prompt_editor(cx)))
}
}
@@ -3703,8 +3706,8 @@ impl InlineAssistant {
// This wouldn't need to exist if we could pass parameters when rendering child views.
#[derive(Copy, Clone, Default)]
struct BlockMeasurements {
anchor_x: Pixels,
gutter_width: Pixels,
gutter_margin: Pixels,
}
struct PendingInlineAssist {
@@ -3736,14 +3739,13 @@ fn render_slash_command_output_toggle(
.into_any_element()
}
fn render_pending_slash_command_toggle(
fn render_pending_slash_command_gutter_decoration(
row: MultiBufferRow,
tooltip_text: SharedString,
status: PendingSlashCommandStatus,
confirm_command: Arc<dyn Fn(&mut WindowContext)>,
) -> AnyElement {
let mut icon = IconButton::new(
("slash-command-output-fold-indicator", row.0),
("slash-command-gutter-decoration", row.0),
ui::IconName::TriangleRight,
)
.on_click(move |_e, cx| confirm_command(cx))
@@ -3752,14 +3754,10 @@ fn render_pending_slash_command_toggle(
match status {
PendingSlashCommandStatus::Idle => {
icon = icon
.icon_color(Color::Muted)
.tooltip(move |cx| Tooltip::text(tooltip_text.clone(), cx));
icon = icon.icon_color(Color::Muted);
}
PendingSlashCommandStatus::Running { .. } => {
icon = icon
.selected(true)
.tooltip(move |cx| Tooltip::text(tooltip_text.clone(), cx));
icon = icon.selected(true);
}
PendingSlashCommandStatus::Error(error) => {
icon = icon
@@ -3847,15 +3845,8 @@ mod tests {
init(cx);
let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
let conversation = cx.new_model(|cx| {
Conversation::new(
LanguageModel::default(),
registry,
Default::default(),
None,
cx,
)
});
let conversation =
cx.new_model(|cx| Conversation::new(registry, Default::default(), None, cx));
let buffer = conversation.read(cx).buffer.clone();
let message_1 = conversation.read(cx).message_anchors[0].clone();
@@ -3986,15 +3977,8 @@ mod tests {
init(cx);
let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
let conversation = cx.new_model(|cx| {
Conversation::new(
LanguageModel::default(),
registry,
Default::default(),
None,
cx,
)
});
let conversation =
cx.new_model(|cx| Conversation::new(registry, Default::default(), None, cx));
let buffer = conversation.read(cx).buffer.clone();
let message_1 = conversation.read(cx).message_anchors[0].clone();
@@ -4092,15 +4076,8 @@ mod tests {
cx.set_global(settings_store);
init(cx);
let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
let conversation = cx.new_model(|cx| {
Conversation::new(
LanguageModel::default(),
registry,
Default::default(),
None,
cx,
)
});
let conversation =
cx.new_model(|cx| Conversation::new(registry, Default::default(), None, cx));
let buffer = conversation.read(cx).buffer.clone();
let message_1 = conversation.read(cx).message_anchors[0].clone();
@@ -4203,21 +4180,15 @@ mod tests {
let prompt_library = Arc::new(PromptLibrary::default());
let slash_command_registry = SlashCommandRegistry::new();
slash_command_registry.register_command(file_command::FileSlashCommand);
slash_command_registry.register_command(prompt_command::PromptSlashCommand::new(
prompt_library.clone(),
));
slash_command_registry.register_command(file_command::FileSlashCommand, false);
slash_command_registry.register_command(
prompt_command::PromptSlashCommand::new(prompt_library.clone()),
false,
);
let registry = Arc::new(LanguageRegistry::test(cx.executor()));
let conversation = cx.new_model(|cx| {
Conversation::new(
LanguageModel::default(),
registry.clone(),
slash_command_registry,
None,
cx,
)
});
let conversation = cx
.new_model(|cx| Conversation::new(registry.clone(), slash_command_registry, None, cx));
let output_ranges = Rc::new(RefCell::new(HashSet::default()));
conversation.update(cx, |_, cx| {
@@ -4390,15 +4361,8 @@ mod tests {
cx.set_global(CompletionProvider::Fake(FakeCompletionProvider::default()));
cx.update(init);
let registry = Arc::new(LanguageRegistry::test(cx.executor()));
let conversation = cx.new_model(|cx| {
Conversation::new(
LanguageModel::default(),
registry.clone(),
Default::default(),
None,
cx,
)
});
let conversation =
cx.new_model(|cx| Conversation::new(registry.clone(), Default::default(), None, cx));
let buffer = conversation.read_with(cx, |conversation, _| conversation.buffer.clone());
let message_0 =
conversation.read_with(cx, |conversation, _| conversation.message_anchors[0].id);
@@ -4434,7 +4398,6 @@ mod tests {
let deserialized_conversation = Conversation::deserialize(
conversation.read_with(cx, |conversation, cx| conversation.serialize(cx)),
LanguageModel::default(),
Default::default(),
registry.clone(),
Default::default(),

View File

@@ -12,8 +12,11 @@ use serde::{
Deserialize, Deserializer, Serialize, Serializer,
};
use settings::{Settings, SettingsSources};
use strum::{EnumIter, IntoEnumIterator};
#[derive(Clone, Debug, Default, PartialEq)]
use crate::LanguageModel;
#[derive(Clone, Debug, Default, PartialEq, EnumIter)]
pub enum ZedDotDevModel {
Gpt3Point5Turbo,
Gpt4,
@@ -53,13 +56,10 @@ impl<'de> Deserialize<'de> for ZedDotDevModel {
where
E: de::Error,
{
match value {
"gpt-3.5-turbo" => Ok(ZedDotDevModel::Gpt3Point5Turbo),
"gpt-4" => Ok(ZedDotDevModel::Gpt4),
"gpt-4-turbo-preview" => Ok(ZedDotDevModel::Gpt4Turbo),
"gpt-4o" => Ok(ZedDotDevModel::Gpt4Omni),
_ => Ok(ZedDotDevModel::Custom(value.to_owned())),
}
let model = ZedDotDevModel::iter()
.find(|model| model.id() == value)
.unwrap_or_else(|| ZedDotDevModel::Custom(value.to_string()));
Ok(model)
}
}
@@ -73,24 +73,23 @@ impl JsonSchema for ZedDotDevModel {
}
fn json_schema(_generator: &mut schemars::gen::SchemaGenerator) -> Schema {
let variants = vec![
"gpt-3.5-turbo".to_owned(),
"gpt-4".to_owned(),
"gpt-4-turbo-preview".to_owned(),
"gpt-4o".to_owned(),
];
let variants = ZedDotDevModel::iter()
.filter_map(|model| {
let id = model.id();
if id.is_empty() {
None
} else {
Some(id.to_string())
}
})
.collect::<Vec<_>>();
Schema::Object(SchemaObject {
instance_type: Some(InstanceType::String.into()),
enum_values: Some(variants.into_iter().map(|s| s.into()).collect()),
enum_values: Some(variants.iter().map(|s| s.clone().into()).collect()),
metadata: Some(Box::new(Metadata {
title: Some("ZedDotDevModel".to_owned()),
default: Some(serde_json::json!("gpt-4-turbo-preview")),
examples: vec![
serde_json::json!("gpt-3.5-turbo"),
serde_json::json!("gpt-4"),
serde_json::json!("gpt-4-turbo-preview"),
serde_json::json!("custom-model-name"),
],
default: Some(ZedDotDevModel::default().id().into()),
examples: variants.into_iter().map(Into::into).collect(),
..Default::default()
})),
..Default::default()
@@ -145,51 +144,55 @@ pub enum AssistantDockPosition {
Bottom,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
#[serde(tag = "name", rename_all = "snake_case")]
#[derive(Debug, PartialEq)]
pub enum AssistantProvider {
#[serde(rename = "zed.dev")]
ZedDotDev {
#[serde(default)]
default_model: ZedDotDevModel,
model: ZedDotDevModel,
},
#[serde(rename = "openai")]
OpenAi {
#[serde(default)]
default_model: OpenAiModel,
#[serde(default = "open_ai_url")]
model: OpenAiModel,
api_url: String,
#[serde(default)]
low_speed_timeout_in_seconds: Option<u64>,
},
#[serde(rename = "anthropic")]
Anthropic {
#[serde(default)]
default_model: AnthropicModel,
#[serde(default = "anthropic_api_url")]
model: AnthropicModel,
api_url: String,
#[serde(default)]
low_speed_timeout_in_seconds: Option<u64>,
},
}
impl Default for AssistantProvider {
fn default() -> Self {
Self::ZedDotDev {
default_model: ZedDotDevModel::default(),
Self::OpenAi {
model: OpenAiModel::default(),
api_url: open_ai::OPEN_AI_API_URL.into(),
low_speed_timeout_in_seconds: None,
}
}
}
fn open_ai_url() -> String {
open_ai::OPEN_AI_API_URL.to_string()
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
#[serde(tag = "name", rename_all = "snake_case")]
pub enum AssistantProviderContent {
#[serde(rename = "zed.dev")]
ZedDotDev {
default_model: Option<ZedDotDevModel>,
},
#[serde(rename = "openai")]
OpenAi {
default_model: Option<OpenAiModel>,
api_url: Option<String>,
low_speed_timeout_in_seconds: Option<u64>,
},
#[serde(rename = "anthropic")]
Anthropic {
default_model: Option<AnthropicModel>,
api_url: Option<String>,
low_speed_timeout_in_seconds: Option<u64>,
},
}
fn anthropic_api_url() -> String {
anthropic::ANTHROPIC_API_URL.to_string()
}
#[derive(Default, Debug, Deserialize, Serialize)]
#[derive(Debug, Default)]
pub struct AssistantSettings {
pub enabled: bool,
pub button: bool,
@@ -240,16 +243,16 @@ impl AssistantSettingsContent {
default_width: settings.default_width,
default_height: settings.default_height,
provider: if let Some(open_ai_api_url) = settings.openai_api_url.as_ref() {
Some(AssistantProvider::OpenAi {
default_model: settings.default_open_ai_model.clone().unwrap_or_default(),
api_url: open_ai_api_url.clone(),
Some(AssistantProviderContent::OpenAi {
default_model: settings.default_open_ai_model.clone(),
api_url: Some(open_ai_api_url.clone()),
low_speed_timeout_in_seconds: None,
})
} else {
settings.default_open_ai_model.clone().map(|open_ai_model| {
AssistantProvider::OpenAi {
default_model: open_ai_model,
api_url: open_ai_url(),
AssistantProviderContent::OpenAi {
default_model: Some(open_ai_model),
api_url: None,
low_speed_timeout_in_seconds: None,
}
})
@@ -270,6 +273,64 @@ impl AssistantSettingsContent {
}
}
}
pub fn set_model(&mut self, new_model: LanguageModel) {
match self {
AssistantSettingsContent::Versioned(settings) => match settings {
VersionedAssistantSettingsContent::V1(settings) => match &mut settings.provider {
Some(AssistantProviderContent::ZedDotDev {
default_model: model,
}) => {
if let LanguageModel::ZedDotDev(new_model) = new_model {
*model = Some(new_model);
}
}
Some(AssistantProviderContent::OpenAi {
default_model: model,
..
}) => {
if let LanguageModel::OpenAi(new_model) = new_model {
*model = Some(new_model);
}
}
Some(AssistantProviderContent::Anthropic {
default_model: model,
..
}) => {
if let LanguageModel::Anthropic(new_model) = new_model {
*model = Some(new_model);
}
}
provider => match new_model {
LanguageModel::ZedDotDev(model) => {
*provider = Some(AssistantProviderContent::ZedDotDev {
default_model: Some(model),
})
}
LanguageModel::OpenAi(model) => {
*provider = Some(AssistantProviderContent::OpenAi {
default_model: Some(model),
api_url: None,
low_speed_timeout_in_seconds: None,
})
}
LanguageModel::Anthropic(model) => {
*provider = Some(AssistantProviderContent::Anthropic {
default_model: Some(model),
api_url: None,
low_speed_timeout_in_seconds: None,
})
}
},
},
},
AssistantSettingsContent::Legacy(settings) => {
if let LanguageModel::OpenAi(model) = new_model {
settings.default_open_ai_model = Some(model);
}
}
}
}
}
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
@@ -318,7 +379,7 @@ pub struct AssistantSettingsContentV1 {
///
/// This can either be the internal `zed.dev` service or an external `openai` service,
/// each with their respective default models and configurations.
provider: Option<AssistantProvider>,
provider: Option<AssistantProviderContent>,
}
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
@@ -376,31 +437,82 @@ impl Settings for AssistantSettings {
if let Some(provider) = value.provider.clone() {
match (&mut settings.provider, provider) {
(
AssistantProvider::ZedDotDev { default_model },
AssistantProvider::ZedDotDev {
default_model: default_model_override,
AssistantProvider::ZedDotDev { model },
AssistantProviderContent::ZedDotDev {
default_model: model_override,
},
) => {
*default_model = default_model_override;
merge(model, model_override);
}
(
AssistantProvider::OpenAi {
default_model,
model,
api_url,
low_speed_timeout_in_seconds,
},
AssistantProvider::OpenAi {
default_model: default_model_override,
AssistantProviderContent::OpenAi {
default_model: model_override,
api_url: api_url_override,
low_speed_timeout_in_seconds: low_speed_timeout_in_seconds_override,
},
) => {
*default_model = default_model_override;
*api_url = api_url_override;
*low_speed_timeout_in_seconds = low_speed_timeout_in_seconds_override;
merge(model, model_override);
merge(api_url, api_url_override);
if let Some(low_speed_timeout_in_seconds_override) =
low_speed_timeout_in_seconds_override
{
*low_speed_timeout_in_seconds =
Some(low_speed_timeout_in_seconds_override);
}
}
(merged, provider_override) => {
*merged = provider_override;
(
AssistantProvider::Anthropic {
model,
api_url,
low_speed_timeout_in_seconds,
},
AssistantProviderContent::Anthropic {
default_model: model_override,
api_url: api_url_override,
low_speed_timeout_in_seconds: low_speed_timeout_in_seconds_override,
},
) => {
merge(model, model_override);
merge(api_url, api_url_override);
if let Some(low_speed_timeout_in_seconds_override) =
low_speed_timeout_in_seconds_override
{
*low_speed_timeout_in_seconds =
Some(low_speed_timeout_in_seconds_override);
}
}
(provider, provider_override) => {
*provider = match provider_override {
AssistantProviderContent::ZedDotDev {
default_model: model,
} => AssistantProvider::ZedDotDev {
model: model.unwrap_or_default(),
},
AssistantProviderContent::OpenAi {
default_model: model,
api_url,
low_speed_timeout_in_seconds,
} => AssistantProvider::OpenAi {
model: model.unwrap_or_default(),
api_url: api_url.unwrap_or_else(|| open_ai::OPEN_AI_API_URL.into()),
low_speed_timeout_in_seconds,
},
AssistantProviderContent::Anthropic {
default_model: model,
api_url,
low_speed_timeout_in_seconds,
} => AssistantProvider::Anthropic {
model: model.unwrap_or_default(),
api_url: api_url
.unwrap_or_else(|| anthropic::ANTHROPIC_API_URL.into()),
low_speed_timeout_in_seconds,
},
};
}
}
}
@@ -410,7 +522,7 @@ impl Settings for AssistantSettings {
}
}
fn merge<T: Copy>(target: &mut T, value: Option<T>) {
fn merge<T>(target: &mut T, value: Option<T>) {
if let Some(value) = value {
*target = value;
}
@@ -433,8 +545,8 @@ mod tests {
assert_eq!(
AssistantSettings::get_global(cx).provider,
AssistantProvider::OpenAi {
default_model: OpenAiModel::FourOmni,
api_url: open_ai_url(),
model: OpenAiModel::FourOmni,
api_url: open_ai::OPEN_AI_API_URL.into(),
low_speed_timeout_in_seconds: None,
}
);
@@ -455,7 +567,7 @@ mod tests {
assert_eq!(
AssistantSettings::get_global(cx).provider,
AssistantProvider::OpenAi {
default_model: OpenAiModel::FourOmni,
model: OpenAiModel::FourOmni,
api_url: "test-url".into(),
low_speed_timeout_in_seconds: None,
}
@@ -475,8 +587,8 @@ mod tests {
assert_eq!(
AssistantSettings::get_global(cx).provider,
AssistantProvider::OpenAi {
default_model: OpenAiModel::Four,
api_url: open_ai_url(),
model: OpenAiModel::Four,
api_url: open_ai::OPEN_AI_API_URL.into(),
low_speed_timeout_in_seconds: None,
}
);
@@ -501,7 +613,7 @@ mod tests {
assert_eq!(
AssistantSettings::get_global(cx).provider,
AssistantProvider::ZedDotDev {
default_model: ZedDotDevModel::Custom("custom".into())
model: ZedDotDevModel::Custom("custom".into())
}
);
}

View File

@@ -25,31 +25,26 @@ use std::time::Duration;
pub fn init(client: Arc<Client>, cx: &mut AppContext) {
let mut settings_version = 0;
let provider = match &AssistantSettings::get_global(cx).provider {
AssistantProvider::ZedDotDev { default_model } => {
CompletionProvider::ZedDotDev(ZedDotDevCompletionProvider::new(
default_model.clone(),
client.clone(),
settings_version,
cx,
))
}
AssistantProvider::ZedDotDev { model } => CompletionProvider::ZedDotDev(
ZedDotDevCompletionProvider::new(model.clone(), client.clone(), settings_version, cx),
),
AssistantProvider::OpenAi {
default_model,
model,
api_url,
low_speed_timeout_in_seconds,
} => CompletionProvider::OpenAi(OpenAiCompletionProvider::new(
default_model.clone(),
model.clone(),
api_url.clone(),
client.http_client(),
low_speed_timeout_in_seconds.map(Duration::from_secs),
settings_version,
)),
AssistantProvider::Anthropic {
default_model,
model,
api_url,
low_speed_timeout_in_seconds,
} => CompletionProvider::Anthropic(AnthropicCompletionProvider::new(
default_model.clone(),
model.clone(),
api_url.clone(),
client.http_client(),
low_speed_timeout_in_seconds.map(Duration::from_secs),
@@ -65,13 +60,13 @@ pub fn init(client: Arc<Client>, cx: &mut AppContext) {
(
CompletionProvider::OpenAi(provider),
AssistantProvider::OpenAi {
default_model,
model,
api_url,
low_speed_timeout_in_seconds,
},
) => {
provider.update(
default_model.clone(),
model.clone(),
api_url.clone(),
low_speed_timeout_in_seconds.map(Duration::from_secs),
settings_version,
@@ -80,13 +75,13 @@ pub fn init(client: Arc<Client>, cx: &mut AppContext) {
(
CompletionProvider::Anthropic(provider),
AssistantProvider::Anthropic {
default_model,
model,
api_url,
low_speed_timeout_in_seconds,
},
) => {
provider.update(
default_model.clone(),
model.clone(),
api_url.clone(),
low_speed_timeout_in_seconds.map(Duration::from_secs),
settings_version,
@@ -94,13 +89,13 @@ pub fn init(client: Arc<Client>, cx: &mut AppContext) {
}
(
CompletionProvider::ZedDotDev(provider),
AssistantProvider::ZedDotDev { default_model },
AssistantProvider::ZedDotDev { model },
) => {
provider.update(default_model.clone(), settings_version);
provider.update(model.clone(), settings_version);
}
(_, AssistantProvider::ZedDotDev { default_model }) => {
(_, AssistantProvider::ZedDotDev { model }) => {
*provider = CompletionProvider::ZedDotDev(ZedDotDevCompletionProvider::new(
default_model.clone(),
model.clone(),
client.clone(),
settings_version,
cx,
@@ -109,13 +104,13 @@ pub fn init(client: Arc<Client>, cx: &mut AppContext) {
(
_,
AssistantProvider::OpenAi {
default_model,
model,
api_url,
low_speed_timeout_in_seconds,
},
) => {
*provider = CompletionProvider::OpenAi(OpenAiCompletionProvider::new(
default_model.clone(),
model.clone(),
api_url.clone(),
client.http_client(),
low_speed_timeout_in_seconds.map(Duration::from_secs),
@@ -125,13 +120,13 @@ pub fn init(client: Arc<Client>, cx: &mut AppContext) {
(
_,
AssistantProvider::Anthropic {
default_model,
model,
api_url,
low_speed_timeout_in_seconds,
},
) => {
*provider = CompletionProvider::Anthropic(AnthropicCompletionProvider::new(
default_model.clone(),
model.clone(),
api_url.clone(),
client.http_client(),
low_speed_timeout_in_seconds.map(Duration::from_secs),
@@ -159,6 +154,25 @@ impl CompletionProvider {
cx.global::<Self>()
}
pub fn available_models(&self) -> Vec<LanguageModel> {
match self {
CompletionProvider::OpenAi(provider) => provider
.available_models()
.map(LanguageModel::OpenAi)
.collect(),
CompletionProvider::Anthropic(provider) => provider
.available_models()
.map(LanguageModel::Anthropic)
.collect(),
CompletionProvider::ZedDotDev(provider) => provider
.available_models()
.map(LanguageModel::ZedDotDev)
.collect(),
#[cfg(test)]
CompletionProvider::Fake(_) => unimplemented!(),
}
}
pub fn settings_version(&self) -> usize {
match self {
CompletionProvider::OpenAi(provider) => provider.settings_version(),
@@ -209,17 +223,13 @@ impl CompletionProvider {
}
}
pub fn default_model(&self) -> LanguageModel {
pub fn model(&self) -> LanguageModel {
match self {
CompletionProvider::OpenAi(provider) => LanguageModel::OpenAi(provider.default_model()),
CompletionProvider::Anthropic(provider) => {
LanguageModel::Anthropic(provider.default_model())
}
CompletionProvider::ZedDotDev(provider) => {
LanguageModel::ZedDotDev(provider.default_model())
}
CompletionProvider::OpenAi(provider) => LanguageModel::OpenAi(provider.model()),
CompletionProvider::Anthropic(provider) => LanguageModel::Anthropic(provider.model()),
CompletionProvider::ZedDotDev(provider) => LanguageModel::ZedDotDev(provider.model()),
#[cfg(test)]
CompletionProvider::Fake(_) => unimplemented!(),
CompletionProvider::Fake(_) => LanguageModel::default(),
}
}

View File

@@ -12,6 +12,7 @@ use http::HttpClient;
use settings::Settings;
use std::time::Duration;
use std::{env, sync::Arc};
use strum::IntoEnumIterator;
use theme::ThemeSettings;
use ui::prelude::*;
use util::ResultExt;
@@ -19,7 +20,7 @@ use util::ResultExt;
pub struct AnthropicCompletionProvider {
api_key: Option<String>,
api_url: String,
default_model: AnthropicModel,
model: AnthropicModel,
http_client: Arc<dyn HttpClient>,
low_speed_timeout: Option<Duration>,
settings_version: usize,
@@ -27,7 +28,7 @@ pub struct AnthropicCompletionProvider {
impl AnthropicCompletionProvider {
pub fn new(
default_model: AnthropicModel,
model: AnthropicModel,
api_url: String,
http_client: Arc<dyn HttpClient>,
low_speed_timeout: Option<Duration>,
@@ -36,7 +37,7 @@ impl AnthropicCompletionProvider {
Self {
api_key: None,
api_url,
default_model,
model,
http_client,
low_speed_timeout,
settings_version,
@@ -45,17 +46,21 @@ impl AnthropicCompletionProvider {
pub fn update(
&mut self,
default_model: AnthropicModel,
model: AnthropicModel,
api_url: String,
low_speed_timeout: Option<Duration>,
settings_version: usize,
) {
self.default_model = default_model;
self.model = model;
self.api_url = api_url;
self.low_speed_timeout = low_speed_timeout;
self.settings_version = settings_version;
}
pub fn available_models(&self) -> impl Iterator<Item = AnthropicModel> {
AnthropicModel::iter()
}
pub fn settings_version(&self) -> usize {
self.settings_version
}
@@ -105,8 +110,8 @@ impl AnthropicCompletionProvider {
.into()
}
pub fn default_model(&self) -> AnthropicModel {
self.default_model.clone()
pub fn model(&self) -> AnthropicModel {
self.model.clone()
}
pub fn count_tokens(
@@ -165,7 +170,7 @@ impl AnthropicCompletionProvider {
fn to_anthropic_request(&self, request: LanguageModelRequest) -> Request {
let model = match request.model {
LanguageModel::Anthropic(model) => model,
_ => self.default_model(),
_ => self.model(),
};
let mut system_message = String::new();

View File

@@ -11,6 +11,7 @@ use open_ai::{stream_completion, Request, RequestMessage, Role as OpenAiRole};
use settings::Settings;
use std::time::Duration;
use std::{env, sync::Arc};
use strum::IntoEnumIterator;
use theme::ThemeSettings;
use ui::prelude::*;
use util::ResultExt;
@@ -18,7 +19,7 @@ use util::ResultExt;
pub struct OpenAiCompletionProvider {
api_key: Option<String>,
api_url: String,
default_model: OpenAiModel,
model: OpenAiModel,
http_client: Arc<dyn HttpClient>,
low_speed_timeout: Option<Duration>,
settings_version: usize,
@@ -26,7 +27,7 @@ pub struct OpenAiCompletionProvider {
impl OpenAiCompletionProvider {
pub fn new(
default_model: OpenAiModel,
model: OpenAiModel,
api_url: String,
http_client: Arc<dyn HttpClient>,
low_speed_timeout: Option<Duration>,
@@ -35,7 +36,7 @@ impl OpenAiCompletionProvider {
Self {
api_key: None,
api_url,
default_model,
model,
http_client,
low_speed_timeout,
settings_version,
@@ -44,17 +45,21 @@ impl OpenAiCompletionProvider {
pub fn update(
&mut self,
default_model: OpenAiModel,
model: OpenAiModel,
api_url: String,
low_speed_timeout: Option<Duration>,
settings_version: usize,
) {
self.default_model = default_model;
self.model = model;
self.api_url = api_url;
self.low_speed_timeout = low_speed_timeout;
self.settings_version = settings_version;
}
pub fn available_models(&self) -> impl Iterator<Item = OpenAiModel> {
OpenAiModel::iter()
}
pub fn settings_version(&self) -> usize {
self.settings_version
}
@@ -104,8 +109,8 @@ impl OpenAiCompletionProvider {
.into()
}
pub fn default_model(&self) -> OpenAiModel {
self.default_model.clone()
pub fn model(&self) -> OpenAiModel {
self.model.clone()
}
pub fn count_tokens(
@@ -152,7 +157,7 @@ impl OpenAiCompletionProvider {
fn to_open_ai_request(&self, request: LanguageModelRequest) -> Request {
let model = match request.model {
LanguageModel::OpenAi(model) => model,
_ => self.default_model(),
_ => self.model(),
};
Request {

View File

@@ -7,11 +7,12 @@ use client::{proto, Client};
use futures::{future::BoxFuture, stream::BoxStream, FutureExt, StreamExt, TryFutureExt};
use gpui::{AnyView, AppContext, Task};
use std::{future, sync::Arc};
use strum::IntoEnumIterator;
use ui::prelude::*;
pub struct ZedDotDevCompletionProvider {
client: Arc<Client>,
default_model: ZedDotDevModel,
model: ZedDotDevModel,
settings_version: usize,
status: client::Status,
_maintain_client_status: Task<()>,
@@ -19,7 +20,7 @@ pub struct ZedDotDevCompletionProvider {
impl ZedDotDevCompletionProvider {
pub fn new(
default_model: ZedDotDevModel,
model: ZedDotDevModel,
client: Arc<Client>,
settings_version: usize,
cx: &mut AppContext,
@@ -39,24 +40,39 @@ impl ZedDotDevCompletionProvider {
});
Self {
client,
default_model,
model,
settings_version,
status,
_maintain_client_status: maintain_client_status,
}
}
pub fn update(&mut self, default_model: ZedDotDevModel, settings_version: usize) {
self.default_model = default_model;
pub fn update(&mut self, model: ZedDotDevModel, settings_version: usize) {
self.model = model;
self.settings_version = settings_version;
}
pub fn available_models(&self) -> impl Iterator<Item = ZedDotDevModel> {
let mut custom_model = if let ZedDotDevModel::Custom(custom_model) = self.model.clone() {
Some(custom_model)
} else {
None
};
ZedDotDevModel::iter().filter_map(move |model| {
if let ZedDotDevModel::Custom(_) = model {
Some(ZedDotDevModel::Custom(custom_model.take()?))
} else {
Some(model)
}
})
}
pub fn settings_version(&self) -> usize {
self.settings_version
}
pub fn default_model(&self) -> ZedDotDevModel {
self.default_model.clone()
pub fn model(&self) -> ZedDotDevModel {
self.model.clone()
}
pub fn is_authenticated(&self) -> bool {

View File

@@ -0,0 +1,84 @@
use std::sync::Arc;
use crate::{assistant_settings::AssistantSettings, CompletionProvider, ToggleModelSelector};
use fs::Fs;
use settings::update_settings_file;
use ui::{popover_menu, prelude::*, ButtonLike, ContextMenu, PopoverMenuHandle, Tooltip};
#[derive(IntoElement)]
pub struct ModelSelector {
handle: PopoverMenuHandle<ContextMenu>,
fs: Arc<dyn Fs>,
}
impl ModelSelector {
pub fn new(handle: PopoverMenuHandle<ContextMenu>, fs: Arc<dyn Fs>) -> Self {
ModelSelector { handle, fs }
}
}
impl RenderOnce for ModelSelector {
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
popover_menu("model-switcher")
.with_handle(self.handle)
.menu(move |cx| {
ContextMenu::build(cx, |mut menu, cx| {
for model in CompletionProvider::global(cx).available_models() {
menu = menu.custom_entry(
{
let model = model.clone();
move |_| Label::new(model.display_name()).into_any_element()
},
{
let fs = self.fs.clone();
let model = model.clone();
move |cx| {
let model = model.clone();
update_settings_file::<AssistantSettings>(
fs.clone(),
cx,
move |settings| settings.set_model(model),
);
}
},
);
}
menu
})
.into()
})
.trigger(
ButtonLike::new("active-model")
.child(
h_flex()
.w_full()
.gap_0p5()
.child(
div()
.overflow_x_hidden()
.flex_grow()
.whitespace_nowrap()
.child(
Label::new(
CompletionProvider::global(cx).model().display_name(),
)
.size(LabelSize::Small)
.color(Color::Muted),
),
)
.child(
div().child(
Icon::new(IconName::ChevronDown)
.color(Color::Muted)
.size(IconSize::XSmall),
),
),
)
.style(ButtonStyle::Subtle)
.tooltip(move |cx| {
Tooltip::for_action("Change Model", &ToggleModelSelector, cx)
}),
)
.anchor(gpui::AnchorCorner::BottomRight)
}
}

View File

@@ -19,8 +19,8 @@ impl SlashCommand for ActiveSlashCommand {
"insert active tab".into()
}
fn tooltip_text(&self) -> String {
"insert active tab".into()
fn menu_text(&self) -> String {
"Insert Active Tab".into()
}
fn complete_argument(

View File

@@ -86,11 +86,11 @@ impl SlashCommand for FileSlashCommand {
}
fn description(&self) -> String {
"insert a file".into()
"insert file".into()
}
fn tooltip_text(&self) -> String {
"insert file".into()
fn menu_text(&self) -> String {
"Insert File".into()
}
fn requires_argument(&self) -> bool {

View File

@@ -94,11 +94,11 @@ impl SlashCommand for ProjectSlashCommand {
}
fn description(&self) -> String {
"insert current project context".into()
"insert project metadata".into()
}
fn tooltip_text(&self) -> String {
"insert current project context".into()
fn menu_text(&self) -> String {
"Insert Project Metadata".into()
}
fn complete_argument(

View File

@@ -25,11 +25,11 @@ impl SlashCommand for PromptSlashCommand {
}
fn description(&self) -> String {
"insert a prompt from the library".into()
"insert prompt from library".into()
}
fn tooltip_text(&self) -> String {
"insert prompt".into()
fn menu_text(&self) -> String {
"Insert Prompt from Library".into()
}
fn requires_argument(&self) -> bool {

View File

@@ -51,11 +51,11 @@ impl SlashCommand for RustdocSlashCommand {
}
fn description(&self) -> String {
"insert the docs for a Rust crate".into()
"insert Rust docs".into()
}
fn tooltip_text(&self) -> String {
"insert rustdoc".into()
fn menu_text(&self) -> String {
"Insert Rust Documentation".into()
}
fn requires_argument(&self) -> bool {

View File

@@ -32,11 +32,11 @@ impl SlashCommand for SearchSlashCommand {
}
fn description(&self) -> String {
"semantically search files".into()
"semantic search".into()
}
fn tooltip_text(&self) -> String {
"search".into()
fn menu_text(&self) -> String {
"Semantic Search".into()
}
fn requires_argument(&self) -> bool {

View File

@@ -17,11 +17,11 @@ impl SlashCommand for TabsSlashCommand {
}
fn description(&self) -> String {
"insert content from open tabs".into()
"insert open tabs".into()
}
fn tooltip_text(&self) -> String {
"insert open tabs".into()
fn menu_text(&self) -> String {
"Insert Open Tabs".into()
}
fn requires_argument(&self) -> bool {

View File

@@ -20,7 +20,7 @@ pub trait SlashCommand: 'static + Send + Sync {
CodeLabel::plain(self.name(), None)
}
fn description(&self) -> String;
fn tooltip_text(&self) -> String;
fn menu_text(&self) -> String;
fn complete_argument(
&self,
query: String,

View File

@@ -1,6 +1,6 @@
use std::sync::Arc;
use collections::HashMap;
use collections::{BTreeSet, HashMap};
use derive_more::{Deref, DerefMut};
use gpui::Global;
use gpui::{AppContext, ReadGlobal};
@@ -16,6 +16,7 @@ impl Global for GlobalSlashCommandRegistry {}
#[derive(Default)]
struct SlashCommandRegistryState {
commands: HashMap<Arc<str>, Arc<dyn SlashCommand>>,
featured_commands: BTreeSet<Arc<str>>,
}
#[derive(Default)]
@@ -40,16 +41,19 @@ impl SlashCommandRegistry {
Arc::new(Self {
state: RwLock::new(SlashCommandRegistryState {
commands: HashMap::default(),
featured_commands: BTreeSet::default(),
}),
})
}
/// Registers the provided [`SlashCommand`].
pub fn register_command(&self, command: impl SlashCommand) {
self.state
.write()
.commands
.insert(command.name().into(), Arc::new(command));
pub fn register_command(&self, command: impl SlashCommand, is_featured: bool) {
let mut state = self.state.write();
let command_name: Arc<str> = command.name().into();
if is_featured {
state.featured_commands.insert(command_name.clone());
}
state.commands.insert(command_name, Arc::new(command));
}
/// Returns the names of registered [`SlashCommand`]s.
@@ -57,6 +61,16 @@ impl SlashCommandRegistry {
self.state.read().commands.keys().cloned().collect()
}
/// Returns the names of registered, featured [`SlashCommand`]s.
pub fn featured_command_names(&self) -> Vec<Arc<str>> {
self.state
.read()
.featured_commands
.iter()
.cloned()
.collect()
}
/// Returns the [`SlashCommand`] with the given name.
pub fn command(&self, name: &str) -> Option<Arc<dyn SlashCommand>> {
self.state.read().commands.get(name).cloned()

View File

@@ -41,7 +41,7 @@ pub struct MovePageDown {
#[derive(PartialEq, Clone, Deserialize, Default)]
pub struct MoveToEndOfLine {
#[serde(default = "default_true")]
pub(super) stop_at_soft_wraps: bool,
pub stop_at_soft_wraps: bool,
}
#[derive(PartialEq, Clone, Deserialize, Default)]

View File

@@ -524,6 +524,7 @@ pub struct Editor {
expect_bounds_change: Option<Bounds<Pixels>>,
tasks: BTreeMap<(BufferId, BufferRow), RunnableTasks>,
tasks_update_task: Option<Task<()>>,
previous_search_ranges: Option<Arc<[Range<Anchor>]>>,
}
#[derive(Clone)]
@@ -1804,6 +1805,7 @@ impl Editor {
}),
],
tasks_update_task: None,
previous_search_ranges: None,
};
this.tasks_update_task = Some(this.refresh_runnables(cx));
this._subscriptions.extend(project_subscriptions);
@@ -3761,7 +3763,7 @@ impl Editor {
}))
}
fn show_completions(&mut self, _: &ShowCompletions, cx: &mut ViewContext<Self>) {
pub fn show_completions(&mut self, _: &ShowCompletions, cx: &mut ViewContext<Self>) {
if self.pending_rename.is_some() {
return;
}
@@ -9957,10 +9959,33 @@ impl Editor {
}
fn get_permalink_to_line(&mut self, cx: &mut ViewContext<Self>) -> Result<url::Url> {
let (path, repo) = maybe!({
let (path, selection, repo) = maybe!({
let project_handle = self.project.as_ref()?.clone();
let project = project_handle.read(cx);
let buffer = self.buffer().read(cx).as_singleton()?;
let selection = self.selections.newest::<Point>(cx);
let selection_range = selection.range();
let (buffer, selection) = if let Some(buffer) = self.buffer().read(cx).as_singleton() {
(buffer, selection_range.start.row..selection_range.end.row)
} else {
let buffer_ranges = self
.buffer()
.read(cx)
.range_to_buffer_ranges(selection_range, cx);
let (buffer, range, _) = if selection.reversed {
buffer_ranges.first()
} else {
buffer_ranges.last()
}?;
let snapshot = buffer.read(cx).snapshot();
let selection = text::ToPoint::to_point(&range.start, &snapshot).row
..text::ToPoint::to_point(&range.end, &snapshot).row;
(buffer.clone(), selection)
};
let path = buffer
.read(cx)
.file()?
@@ -9969,21 +9994,17 @@ impl Editor {
.to_str()?
.to_string();
let repo = project.get_repo(&buffer.read(cx).project_path(cx)?, cx)?;
Some((path, repo))
Some((path, selection, repo))
})
.ok_or_else(|| anyhow!("unable to open git repository"))?;
const REMOTE_NAME: &str = "origin";
let origin_url = repo
.lock()
.remote_url(REMOTE_NAME)
.ok_or_else(|| anyhow!("remote \"{REMOTE_NAME}\" not found"))?;
let sha = repo
.lock()
.head_sha()
.ok_or_else(|| anyhow!("failed to read HEAD SHA"))?;
let selections = self.selections.all::<Point>(cx);
let selection = selections.iter().peekable().next();
let (provider, remote) =
parse_git_remote_url(GitHostingProviderRegistry::default_global(cx), &origin_url)
@@ -9994,12 +10015,7 @@ impl Editor {
BuildPermalinkParams {
sha: &sha,
path: &path,
selection: selection.map(|selection| {
let range = selection.range();
let start = range.start.row;
let end = range.end.row;
start..end
}),
selection: Some(selection),
},
))
}
@@ -10221,6 +10237,27 @@ impl Editor {
self.background_highlights_in_range(start..end, &snapshot, theme)
}
#[cfg(feature = "test-support")]
pub fn search_background_highlights(
&mut self,
cx: &mut ViewContext<Self>,
) -> Vec<Range<Point>> {
let snapshot = self.buffer().read(cx).snapshot(cx);
let highlights = self
.background_highlights
.get(&TypeId::of::<items::BufferSearchHighlights>());
if let Some((_color, ranges)) = highlights {
ranges
.iter()
.map(|range| range.start.to_point(&snapshot)..range.end.to_point(&snapshot))
.collect_vec()
} else {
vec![]
}
}
fn document_highlights_for_position<'a>(
&'a self,
position: Anchor,

View File

@@ -13,8 +13,7 @@ use gpui::{
VisualContext, WeakView, WindowContext,
};
use language::{
proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, CharKind, OffsetRangeExt,
Point, SelectionGoal,
proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, CharKind, Point, SelectionGoal,
};
use multi_buffer::AnchorRangeExt;
use project::{search::SearchQuery, FormatTrigger, Item as _, Project, ProjectPath};
@@ -1008,6 +1007,25 @@ impl SearchableItem for Editor {
self.has_background_highlights::<SearchWithinRange>()
}
fn toggle_filtered_search_ranges(&mut self, enabled: bool, cx: &mut ViewContext<Self>) {
if self.has_filtered_search_ranges() {
self.previous_search_ranges = self
.clear_background_highlights::<SearchWithinRange>(cx)
.map(|(_, ranges)| ranges)
}
if !enabled {
return;
}
let ranges = self.selections.disjoint_anchor_ranges();
if ranges.iter().any(|range| range.start != range.end) {
self.set_search_within_ranges(&ranges, cx);
} else if let Some(previous_search_ranges) = self.previous_search_ranges.take() {
self.set_search_within_ranges(&previous_search_ranges, cx)
}
}
fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> String {
let setting = EditorSettings::get_global(cx).seed_search_query_from_cursor;
let snapshot = &self.snapshot(cx).buffer_snapshot;
@@ -1016,9 +1034,14 @@ impl SearchableItem for Editor {
match setting {
SeedQuerySetting::Never => String::new(),
SeedQuerySetting::Selection | SeedQuerySetting::Always if !selection.is_empty() => {
snapshot
let text: String = snapshot
.text_for_range(selection.start..selection.end)
.collect()
.collect();
if text.contains('\n') {
String::new()
} else {
text
}
}
SeedQuerySetting::Selection => String::new(),
SeedQuerySetting::Always => {
@@ -1135,58 +1158,64 @@ impl SearchableItem for Editor {
let search_within_ranges = self
.background_highlights
.get(&TypeId::of::<SearchWithinRange>())
.map(|(_color, ranges)| {
ranges
.iter()
.map(|range| range.to_offset(&buffer))
.collect::<Vec<_>>()
.map_or(vec![], |(_color, ranges)| {
ranges.iter().map(|range| range.clone()).collect::<Vec<_>>()
});
cx.background_executor().spawn(async move {
let mut ranges = Vec::new();
if let Some((_, _, excerpt_buffer)) = buffer.as_singleton() {
if let Some(search_within_ranges) = search_within_ranges {
for range in search_within_ranges {
let offset = range.start;
ranges.extend(
query
.search(excerpt_buffer, Some(range))
.await
.into_iter()
.map(|range| {
buffer.anchor_after(range.start + offset)
..buffer.anchor_before(range.end + offset)
}),
);
}
let search_within_ranges = if search_within_ranges.is_empty() {
vec![None]
} else {
ranges.extend(query.search(excerpt_buffer, None).await.into_iter().map(
|range| buffer.anchor_after(range.start)..buffer.anchor_before(range.end),
));
search_within_ranges
.into_iter()
.map(|range| Some(range.to_offset(&buffer)))
.collect::<Vec<_>>()
};
for range in search_within_ranges {
let buffer = &buffer;
ranges.extend(
query
.search(excerpt_buffer, range.clone())
.await
.into_iter()
.map(|matched_range| {
let offset = range.clone().map(|r| r.start).unwrap_or(0);
buffer.anchor_after(matched_range.start + offset)
..buffer.anchor_before(matched_range.end + offset)
}),
);
}
} else {
for excerpt in buffer.excerpt_boundaries_in_range(0..buffer.len()) {
if let Some(next_excerpt) = excerpt.next {
let excerpt_range =
next_excerpt.range.context.to_offset(&next_excerpt.buffer);
ranges.extend(
query
.search(&next_excerpt.buffer, Some(excerpt_range.clone()))
.await
.into_iter()
.map(|range| {
let start = next_excerpt
.buffer
.anchor_after(excerpt_range.start + range.start);
let end = next_excerpt
.buffer
.anchor_before(excerpt_range.start + range.end);
buffer.anchor_in_excerpt(next_excerpt.id, start).unwrap()
..buffer.anchor_in_excerpt(next_excerpt.id, end).unwrap()
}),
);
}
let search_within_ranges = if search_within_ranges.is_empty() {
vec![buffer.anchor_before(0)..buffer.anchor_after(buffer.len())]
} else {
search_within_ranges
};
for (excerpt_id, search_buffer, search_range) in
buffer.excerpts_in_ranges(search_within_ranges)
{
ranges.extend(
query
.search(&search_buffer, Some(search_range.clone()))
.await
.into_iter()
.map(|match_range| {
let start = search_buffer
.anchor_after(search_range.start + match_range.start);
let end = search_buffer
.anchor_before(search_range.start + match_range.end);
buffer.anchor_in_excerpt(excerpt_id, start).unwrap()
..buffer.anchor_in_excerpt(excerpt_id, end).unwrap()
}),
);
}
}
};
ranges
})
}

View File

@@ -273,6 +273,13 @@ impl SelectionsCollection {
self.all(cx).last().unwrap().clone()
}
pub fn disjoint_anchor_ranges(&self) -> Vec<Range<Anchor>> {
self.disjoint_anchors()
.iter()
.map(|s| s.start..s.end)
.collect()
}
#[cfg(any(test, feature = "test-support"))]
pub fn ranges<D: TextDimension + Ord + Sub<D, Output = D> + std::fmt::Debug>(
&self,

View File

@@ -27,7 +27,7 @@ impl SlashCommand for ExtensionSlashCommand {
self.command.description.clone()
}
fn tooltip_text(&self) -> String {
fn menu_text(&self) -> String {
self.command.tooltip_text.clone()
}

View File

@@ -1178,8 +1178,8 @@ impl ExtensionStore {
}
for (slash_command_name, slash_command) in &manifest.slash_commands {
this.slash_command_registry
.register_command(ExtensionSlashCommand {
this.slash_command_registry.register_command(
ExtensionSlashCommand {
command: crate::wit::SlashCommand {
name: slash_command_name.to_string(),
description: slash_command.description.to_string(),
@@ -1188,7 +1188,9 @@ impl ExtensionStore {
},
extension: wasm_extension.clone(),
host: this.wasm_host.clone(),
});
},
false,
);
}
}
this.wasm_extensions.extend(wasm_extensions);

View File

@@ -12,19 +12,13 @@ use std::os::unix::fs::MetadataExt;
use async_tar::Archive;
use futures::{future::BoxFuture, AsyncRead, Stream, StreamExt};
use git::repository::{GitRepository, RealGitRepository};
use git2::Repository as LibGitRepository;
use parking_lot::Mutex;
use rope::Rope;
#[cfg(any(test, feature = "test-support"))]
use smol::io::AsyncReadExt;
use smol::io::AsyncWriteExt;
use std::io::Write;
use std::sync::Arc;
use std::{
io,
io::{self, Write},
path::{Component, Path, PathBuf},
pin::Pin,
sync::Arc,
time::{Duration, SystemTime},
};
use tempfile::{NamedTempFile, TempDir};
@@ -36,6 +30,10 @@ use collections::{btree_map, BTreeMap};
#[cfg(any(test, feature = "test-support"))]
use git::repository::{FakeGitRepositoryState, GitFileStatus};
#[cfg(any(test, feature = "test-support"))]
use parking_lot::Mutex;
#[cfg(any(test, feature = "test-support"))]
use smol::io::AsyncReadExt;
#[cfg(any(test, feature = "test-support"))]
use std::ffi::OsStr;
#[async_trait::async_trait]
@@ -83,7 +81,7 @@ pub trait Fs: Send + Sync {
latency: Duration,
) -> Pin<Box<dyn Send + Stream<Item = Vec<PathBuf>>>>;
fn open_repo(&self, abs_dot_git: &Path) -> Option<Arc<Mutex<dyn GitRepository>>>;
fn open_repo(&self, abs_dot_git: &Path) -> Option<Arc<dyn GitRepository>>;
fn is_fake(&self) -> bool;
async fn is_case_sensitive(&self) -> Result<bool>;
#[cfg(any(test, feature = "test-support"))]
@@ -506,16 +504,13 @@ impl Fs for RealFs {
})))
}
fn open_repo(&self, dotgit_path: &Path) -> Option<Arc<Mutex<dyn GitRepository>>> {
LibGitRepository::open(dotgit_path)
.log_err()
.map::<Arc<Mutex<dyn GitRepository>>, _>(|libgit_repository| {
Arc::new(Mutex::new(RealGitRepository::new(
libgit_repository,
self.git_binary_path.clone(),
self.git_hosting_provider_registry.clone(),
)))
})
fn open_repo(&self, dotgit_path: &Path) -> Option<Arc<dyn GitRepository>> {
let repo = git2::Repository::open(dotgit_path).log_err()?;
Some(Arc::new(RealGitRepository::new(
repo,
self.git_binary_path.clone(),
self.git_hosting_provider_registry.clone(),
)))
}
fn is_fake(&self) -> bool {
@@ -1489,7 +1484,7 @@ impl Fs for FakeFs {
}))
}
fn open_repo(&self, abs_dot_git: &Path) -> Option<Arc<Mutex<dyn GitRepository>>> {
fn open_repo(&self, abs_dot_git: &Path) -> Option<Arc<dyn GitRepository>> {
let state = self.state.lock();
let entry = state.read_path(abs_dot_git).unwrap();
let mut entry = entry.lock();

View File

@@ -14,8 +14,6 @@ use std::{
use sum_tree::MapSeekTarget;
use util::ResultExt;
pub use git2::Repository as LibGitRepository;
#[derive(Clone, Debug, Hash, PartialEq)]
pub struct Branch {
pub is_head: bool,
@@ -24,7 +22,7 @@ pub struct Branch {
pub unix_timestamp: Option<i64>,
}
pub trait GitRepository: Send {
pub trait GitRepository: Send + Sync {
fn reload_index(&self);
/// Loads a git repository entry's contents.
@@ -58,19 +56,19 @@ impl std::fmt::Debug for dyn GitRepository {
}
pub struct RealGitRepository {
pub repository: LibGitRepository,
pub repository: Mutex<git2::Repository>,
pub git_binary_path: PathBuf,
hosting_provider_registry: Arc<GitHostingProviderRegistry>,
}
impl RealGitRepository {
pub fn new(
repository: LibGitRepository,
repository: git2::Repository,
git_binary_path: Option<PathBuf>,
hosting_provider_registry: Arc<GitHostingProviderRegistry>,
) -> Self {
Self {
repository,
repository: Mutex::new(repository),
git_binary_path: git_binary_path.unwrap_or_else(|| PathBuf::from("git")),
hosting_provider_registry,
}
@@ -79,13 +77,13 @@ impl RealGitRepository {
impl GitRepository for RealGitRepository {
fn reload_index(&self) {
if let Ok(mut index) = self.repository.index() {
if let Ok(mut index) = self.repository.lock().index() {
_ = index.read(false);
}
}
fn load_index_text(&self, relative_file_path: &Path) -> Option<String> {
fn logic(repo: &LibGitRepository, relative_file_path: &Path) -> Result<Option<String>> {
fn logic(repo: &git2::Repository, relative_file_path: &Path) -> Result<Option<String>> {
const STAGE_NORMAL: i32 = 0;
let index = repo.index()?;
@@ -101,7 +99,7 @@ impl GitRepository for RealGitRepository {
Ok(Some(String::from_utf8(content)?))
}
match logic(&self.repository, relative_file_path) {
match logic(&self.repository.lock(), relative_file_path) {
Ok(value) => return value,
Err(err) => log::error!("Error loading head text: {:?}", err),
}
@@ -109,31 +107,35 @@ impl GitRepository for RealGitRepository {
}
fn remote_url(&self, name: &str) -> Option<String> {
let remote = self.repository.find_remote(name).ok()?;
let repo = self.repository.lock();
let remote = repo.find_remote(name).ok()?;
remote.url().map(|url| url.to_string())
}
fn branch_name(&self) -> Option<String> {
let head = self.repository.head().log_err()?;
let repo = self.repository.lock();
let head = repo.head().log_err()?;
let branch = String::from_utf8_lossy(head.shorthand_bytes());
Some(branch.to_string())
}
fn head_sha(&self) -> Option<String> {
let head = self.repository.head().ok()?;
head.target().map(|oid| oid.to_string())
Some(self.repository.lock().head().ok()?.target()?.to_string())
}
fn statuses(&self, path_prefix: &Path) -> Result<GitStatus> {
let working_directory = self
.repository
.lock()
.workdir()
.context("failed to read git work directory")?;
GitStatus::new(&self.git_binary_path, working_directory, path_prefix)
.context("failed to read git work directory")?
.to_path_buf();
GitStatus::new(&self.git_binary_path, &working_directory, path_prefix)
}
fn branches(&self) -> Result<Vec<Branch>> {
let local_branches = self.repository.branches(Some(BranchType::Local))?;
let repo = self.repository.lock();
let local_branches = repo.branches(Some(BranchType::Local))?;
let valid_branches = local_branches
.filter_map(|branch| {
branch.ok().and_then(|(branch, _)| {
@@ -158,36 +160,40 @@ impl GitRepository for RealGitRepository {
}
fn change_branch(&self, name: &str) -> Result<()> {
let revision = self.repository.find_branch(name, BranchType::Local)?;
let repo = self.repository.lock();
let revision = repo.find_branch(name, BranchType::Local)?;
let revision = revision.get();
let as_tree = revision.peel_to_tree()?;
self.repository.checkout_tree(as_tree.as_object(), None)?;
self.repository.set_head(
repo.checkout_tree(as_tree.as_object(), None)?;
repo.set_head(
revision
.name()
.ok_or_else(|| anyhow::anyhow!("Branch name could not be retrieved"))?,
)?;
Ok(())
}
fn create_branch(&self, name: &str) -> Result<()> {
let current_commit = self.repository.head()?.peel_to_commit()?;
self.repository.branch(name, &current_commit, false)?;
fn create_branch(&self, name: &str) -> Result<()> {
let repo = self.repository.lock();
let current_commit = repo.head()?.peel_to_commit()?;
repo.branch(name, &current_commit, false)?;
Ok(())
}
fn blame(&self, path: &Path, content: Rope) -> Result<crate::blame::Blame> {
let working_directory = self
.repository
.lock()
.workdir()
.with_context(|| format!("failed to get git working directory for file {:?}", path))?;
.with_context(|| format!("failed to get git working directory for file {:?}", path))?
.to_path_buf();
const REMOTE_NAME: &str = "origin";
let remote_url = self.remote_url(REMOTE_NAME);
crate::blame::Blame::for_path(
&self.git_binary_path,
working_directory,
&working_directory,
path,
&content,
remote_url,
@@ -210,8 +216,8 @@ pub struct FakeGitRepositoryState {
}
impl FakeGitRepository {
pub fn open(state: Arc<Mutex<FakeGitRepositoryState>>) -> Arc<Mutex<dyn GitRepository>> {
Arc::new(Mutex::new(FakeGitRepository { state }))
pub fn open(state: Arc<Mutex<FakeGitRepositoryState>>) -> Arc<dyn GitRepository> {
Arc::new(FakeGitRepository { state })
}
}

View File

@@ -765,6 +765,7 @@ impl SearchableItem for LspLogView {
regex: true,
// LSP log is read-only.
replacement: false,
selection: false,
}
}
fn active_match_index(

View File

@@ -3731,6 +3731,62 @@ impl MultiBufferSnapshot {
}
}
/// Returns excerpts overlapping the given ranges. If range spans multiple excerpts returns one range for each excerpt
pub fn excerpts_in_ranges(
&self,
ranges: impl IntoIterator<Item = Range<Anchor>>,
) -> impl Iterator<Item = (ExcerptId, &BufferSnapshot, Range<usize>)> {
let mut ranges = ranges.into_iter().map(|range| range.to_offset(self));
let mut cursor = self.excerpts.cursor::<usize>();
let mut next_range = move |cursor: &mut Cursor<Excerpt, usize>| {
let range = ranges.next();
if let Some(range) = range.as_ref() {
cursor.seek_forward(&range.start, Bias::Right, &());
}
range
};
let mut range = next_range(&mut cursor);
iter::from_fn(move || {
if range.is_none() {
return None;
}
if range.as_ref().unwrap().is_empty() || *cursor.start() >= range.as_ref().unwrap().end
{
range = next_range(&mut cursor);
if range.is_none() {
return None;
}
}
cursor.item().map(|excerpt| {
let multibuffer_excerpt = MultiBufferExcerpt::new(&excerpt, *cursor.start());
let multibuffer_excerpt_range = multibuffer_excerpt
.map_range_from_buffer(excerpt.range.context.to_offset(&excerpt.buffer));
let overlap_range = cmp::max(
range.as_ref().unwrap().start,
multibuffer_excerpt_range.start,
)
..cmp::min(range.as_ref().unwrap().end, multibuffer_excerpt_range.end);
let overlap_range = multibuffer_excerpt.map_range_to_buffer(overlap_range);
if multibuffer_excerpt_range.end <= range.as_ref().unwrap().end {
cursor.next(&());
} else {
range = next_range(&mut cursor);
}
(excerpt.id, &excerpt.buffer, overlap_range)
})
})
}
pub fn remote_selections_in_range<'a>(
&'a self,
range: &'a Range<Anchor>,
@@ -6067,4 +6123,415 @@ mod tests {
assert_eq!(multibuffer.read(cx).text(), "XABCD1234\nAB5678");
});
}
#[gpui::test]
fn test_excerpts_in_ranges_no_ranges(cx: &mut AppContext) {
let buffer_1 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'a'), cx));
let buffer_2 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'g'), cx));
let multibuffer = cx.new_model(|_| MultiBuffer::new(0, Capability::ReadWrite));
multibuffer.update(cx, |multibuffer, cx| {
multibuffer.push_excerpts(
buffer_1.clone(),
[ExcerptRange {
context: 0..buffer_1.read(cx).len(),
primary: None,
}],
cx,
);
multibuffer.push_excerpts(
buffer_2.clone(),
[ExcerptRange {
context: 0..buffer_2.read(cx).len(),
primary: None,
}],
cx,
);
});
let snapshot = multibuffer.update(cx, |multibuffer, cx| multibuffer.snapshot(cx));
let mut excerpts = snapshot.excerpts_in_ranges(iter::from_fn(|| None));
assert!(excerpts.next().is_none());
}
fn validate_excerpts(
actual: &Vec<(ExcerptId, BufferId, Range<Anchor>)>,
expected: &Vec<(ExcerptId, BufferId, Range<Anchor>)>,
) {
assert_eq!(actual.len(), expected.len());
actual
.into_iter()
.zip(expected)
.map(|(actual, expected)| {
assert_eq!(actual.0, expected.0);
assert_eq!(actual.1, expected.1);
assert_eq!(actual.2.start, expected.2.start);
assert_eq!(actual.2.end, expected.2.end);
})
.collect_vec();
}
fn map_range_from_excerpt(
snapshot: &MultiBufferSnapshot,
excerpt_id: ExcerptId,
excerpt_buffer: &BufferSnapshot,
range: Range<usize>,
) -> Range<Anchor> {
snapshot
.anchor_in_excerpt(excerpt_id, excerpt_buffer.anchor_before(range.start))
.unwrap()
..snapshot
.anchor_in_excerpt(excerpt_id, excerpt_buffer.anchor_after(range.end))
.unwrap()
}
fn make_expected_excerpt_info(
snapshot: &MultiBufferSnapshot,
cx: &mut AppContext,
excerpt_id: ExcerptId,
buffer: &Model<Buffer>,
range: Range<usize>,
) -> (ExcerptId, BufferId, Range<Anchor>) {
(
excerpt_id,
buffer.read(cx).remote_id(),
map_range_from_excerpt(&snapshot, excerpt_id, &buffer.read(cx).snapshot(), range),
)
}
#[gpui::test]
fn test_excerpts_in_ranges_range_inside_the_excerpt(cx: &mut AppContext) {
let buffer_1 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'a'), cx));
let buffer_2 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'g'), cx));
let buffer_len = buffer_1.read(cx).len();
let multibuffer = cx.new_model(|_| MultiBuffer::new(0, Capability::ReadWrite));
let mut expected_excerpt_id = ExcerptId(0);
multibuffer.update(cx, |multibuffer, cx| {
expected_excerpt_id = multibuffer.push_excerpts(
buffer_1.clone(),
[ExcerptRange {
context: 0..buffer_1.read(cx).len(),
primary: None,
}],
cx,
)[0];
multibuffer.push_excerpts(
buffer_2.clone(),
[ExcerptRange {
context: 0..buffer_2.read(cx).len(),
primary: None,
}],
cx,
);
});
let snapshot = multibuffer.update(cx, |multibuffer, cx| multibuffer.snapshot(cx));
let range = snapshot
.anchor_in_excerpt(expected_excerpt_id, buffer_1.read(cx).anchor_before(1))
.unwrap()
..snapshot
.anchor_in_excerpt(
expected_excerpt_id,
buffer_1.read(cx).anchor_after(buffer_len / 2),
)
.unwrap();
let expected_excerpts = vec![make_expected_excerpt_info(
&snapshot,
cx,
expected_excerpt_id,
&buffer_1,
1..(buffer_len / 2),
)];
let excerpts = snapshot
.excerpts_in_ranges(vec![range.clone()].into_iter())
.map(|(excerpt_id, buffer, actual_range)| {
(
excerpt_id,
buffer.remote_id(),
map_range_from_excerpt(&snapshot, excerpt_id, buffer, actual_range),
)
})
.collect_vec();
validate_excerpts(&excerpts, &expected_excerpts);
}
#[gpui::test]
fn test_excerpts_in_ranges_range_crosses_excerpts_boundary(cx: &mut AppContext) {
let buffer_1 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'a'), cx));
let buffer_2 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'g'), cx));
let buffer_len = buffer_1.read(cx).len();
let multibuffer = cx.new_model(|_| MultiBuffer::new(0, Capability::ReadWrite));
let mut excerpt_1_id = ExcerptId(0);
let mut excerpt_2_id = ExcerptId(0);
multibuffer.update(cx, |multibuffer, cx| {
excerpt_1_id = multibuffer.push_excerpts(
buffer_1.clone(),
[ExcerptRange {
context: 0..buffer_1.read(cx).len(),
primary: None,
}],
cx,
)[0];
excerpt_2_id = multibuffer.push_excerpts(
buffer_2.clone(),
[ExcerptRange {
context: 0..buffer_2.read(cx).len(),
primary: None,
}],
cx,
)[0];
});
let snapshot = multibuffer.read(cx).snapshot(cx);
let expected_range = snapshot
.anchor_in_excerpt(
excerpt_1_id,
buffer_1.read(cx).anchor_before(buffer_len / 2),
)
.unwrap()
..snapshot
.anchor_in_excerpt(excerpt_2_id, buffer_2.read(cx).anchor_after(buffer_len / 2))
.unwrap();
let expected_excerpts = vec![
make_expected_excerpt_info(
&snapshot,
cx,
excerpt_1_id,
&buffer_1,
(buffer_len / 2)..buffer_len,
),
make_expected_excerpt_info(&snapshot, cx, excerpt_2_id, &buffer_2, 0..buffer_len / 2),
];
let excerpts = snapshot
.excerpts_in_ranges(vec![expected_range.clone()].into_iter())
.map(|(excerpt_id, buffer, actual_range)| {
(
excerpt_id,
buffer.remote_id(),
map_range_from_excerpt(&snapshot, excerpt_id, buffer, actual_range),
)
})
.collect_vec();
validate_excerpts(&excerpts, &expected_excerpts);
}
#[gpui::test]
fn test_excerpts_in_ranges_range_encloses_excerpt(cx: &mut AppContext) {
let buffer_1 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'a'), cx));
let buffer_2 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'g'), cx));
let buffer_3 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'r'), cx));
let buffer_len = buffer_1.read(cx).len();
let multibuffer = cx.new_model(|_| MultiBuffer::new(0, Capability::ReadWrite));
let mut excerpt_1_id = ExcerptId(0);
let mut excerpt_2_id = ExcerptId(0);
let mut excerpt_3_id = ExcerptId(0);
multibuffer.update(cx, |multibuffer, cx| {
excerpt_1_id = multibuffer.push_excerpts(
buffer_1.clone(),
[ExcerptRange {
context: 0..buffer_1.read(cx).len(),
primary: None,
}],
cx,
)[0];
excerpt_2_id = multibuffer.push_excerpts(
buffer_2.clone(),
[ExcerptRange {
context: 0..buffer_2.read(cx).len(),
primary: None,
}],
cx,
)[0];
excerpt_3_id = multibuffer.push_excerpts(
buffer_3.clone(),
[ExcerptRange {
context: 0..buffer_3.read(cx).len(),
primary: None,
}],
cx,
)[0];
});
let snapshot = multibuffer.read(cx).snapshot(cx);
let expected_range = snapshot
.anchor_in_excerpt(
excerpt_1_id,
buffer_1.read(cx).anchor_before(buffer_len / 2),
)
.unwrap()
..snapshot
.anchor_in_excerpt(excerpt_3_id, buffer_3.read(cx).anchor_after(buffer_len / 2))
.unwrap();
let expected_excerpts = vec![
make_expected_excerpt_info(
&snapshot,
cx,
excerpt_1_id,
&buffer_1,
(buffer_len / 2)..buffer_len,
),
make_expected_excerpt_info(&snapshot, cx, excerpt_2_id, &buffer_2, 0..buffer_len),
make_expected_excerpt_info(&snapshot, cx, excerpt_3_id, &buffer_3, 0..buffer_len / 2),
];
let excerpts = snapshot
.excerpts_in_ranges(vec![expected_range.clone()].into_iter())
.map(|(excerpt_id, buffer, actual_range)| {
(
excerpt_id,
buffer.remote_id(),
map_range_from_excerpt(&snapshot, excerpt_id, buffer, actual_range),
)
})
.collect_vec();
validate_excerpts(&excerpts, &expected_excerpts);
}
#[gpui::test]
fn test_excerpts_in_ranges_multiple_ranges(cx: &mut AppContext) {
let buffer_1 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'a'), cx));
let buffer_2 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'g'), cx));
let buffer_len = buffer_1.read(cx).len();
let multibuffer = cx.new_model(|_| MultiBuffer::new(0, Capability::ReadWrite));
let mut excerpt_1_id = ExcerptId(0);
let mut excerpt_2_id = ExcerptId(0);
multibuffer.update(cx, |multibuffer, cx| {
excerpt_1_id = multibuffer.push_excerpts(
buffer_1.clone(),
[ExcerptRange {
context: 0..buffer_1.read(cx).len(),
primary: None,
}],
cx,
)[0];
excerpt_2_id = multibuffer.push_excerpts(
buffer_2.clone(),
[ExcerptRange {
context: 0..buffer_2.read(cx).len(),
primary: None,
}],
cx,
)[0];
});
let snapshot = multibuffer.read(cx).snapshot(cx);
let ranges = vec![
1..(buffer_len / 4),
(buffer_len / 3)..(buffer_len / 2),
(buffer_len / 4 * 3)..(buffer_len),
];
let expected_excerpts = ranges
.iter()
.map(|range| {
make_expected_excerpt_info(&snapshot, cx, excerpt_1_id, &buffer_1, range.clone())
})
.collect_vec();
let ranges = ranges.into_iter().map(|range| {
map_range_from_excerpt(
&snapshot,
excerpt_1_id,
&buffer_1.read(cx).snapshot(),
range,
)
});
let excerpts = snapshot
.excerpts_in_ranges(ranges)
.map(|(excerpt_id, buffer, actual_range)| {
(
excerpt_id,
buffer.remote_id(),
map_range_from_excerpt(&snapshot, excerpt_id, buffer, actual_range),
)
})
.collect_vec();
validate_excerpts(&excerpts, &expected_excerpts);
}
#[gpui::test]
fn test_excerpts_in_ranges_range_ends_at_excerpt_end(cx: &mut AppContext) {
let buffer_1 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'a'), cx));
let buffer_2 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'g'), cx));
let buffer_len = buffer_1.read(cx).len();
let multibuffer = cx.new_model(|_| MultiBuffer::new(0, Capability::ReadWrite));
let mut excerpt_1_id = ExcerptId(0);
let mut excerpt_2_id = ExcerptId(0);
multibuffer.update(cx, |multibuffer, cx| {
excerpt_1_id = multibuffer.push_excerpts(
buffer_1.clone(),
[ExcerptRange {
context: 0..buffer_1.read(cx).len(),
primary: None,
}],
cx,
)[0];
excerpt_2_id = multibuffer.push_excerpts(
buffer_2.clone(),
[ExcerptRange {
context: 0..buffer_2.read(cx).len(),
primary: None,
}],
cx,
)[0];
});
let snapshot = multibuffer.read(cx).snapshot(cx);
let ranges = [0..buffer_len, (buffer_len / 3)..(buffer_len / 2)];
let expected_excerpts = vec![
make_expected_excerpt_info(&snapshot, cx, excerpt_1_id, &buffer_1, ranges[0].clone()),
make_expected_excerpt_info(&snapshot, cx, excerpt_2_id, &buffer_2, ranges[1].clone()),
];
let ranges = [
map_range_from_excerpt(
&snapshot,
excerpt_1_id,
&buffer_1.read(cx).snapshot(),
ranges[0].clone(),
),
map_range_from_excerpt(
&snapshot,
excerpt_2_id,
&buffer_2.read(cx).snapshot(),
ranges[1].clone(),
),
];
let excerpts = snapshot
.excerpts_in_ranges(ranges.into_iter())
.map(|(excerpt_id, buffer, actual_range)| {
(
excerpt_id,
buffer.remote_id(),
map_range_from_excerpt(&snapshot, excerpt_id, buffer, actual_range),
)
})
.collect_vec();
validate_excerpts(&excerpts, &expected_excerpts);
}
}

View File

@@ -20,3 +20,4 @@ isahc.workspace = true
schemars = { workspace = true, optional = true }
serde.workspace = true
serde_json.workspace = true
strum.workspace = true

View File

@@ -4,8 +4,8 @@ use http::{AsyncBody, HttpClient, Method, Request as HttpRequest};
use isahc::config::Configurable;
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use std::time::Duration;
use std::{convert::TryFrom, future::Future};
use std::{convert::TryFrom, future::Future, time::Duration};
use strum::EnumIter;
pub const OPEN_AI_API_URL: &str = "https://api.openai.com/v1";
@@ -44,7 +44,7 @@ impl From<Role> for String {
}
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, EnumIter)]
pub enum Model {
#[serde(rename = "gpt-3.5-turbo", alias = "gpt-3.5-turbo-0613")]
ThreePointFiveTurbo,

View File

@@ -7856,10 +7856,7 @@ impl Project {
None
} else {
let relative_path = repo.relativize(&snapshot, &path).ok()?;
local_repo_entry
.repo()
.lock()
.load_index_text(&relative_path)
local_repo_entry.repo().load_index_text(&relative_path)
};
Some((buffer, base_text))
}
@@ -8194,7 +8191,7 @@ impl Project {
&self,
project_path: &ProjectPath,
cx: &AppContext,
) -> Option<Arc<Mutex<dyn GitRepository>>> {
) -> Option<Arc<dyn GitRepository>> {
self.worktree_for_id(project_path.worktree_id, cx)?
.read(cx)
.as_local()?
@@ -8202,10 +8199,7 @@ impl Project {
.local_git_repo(&project_path.path)
}
pub fn get_first_worktree_root_repo(
&self,
cx: &AppContext,
) -> Option<Arc<Mutex<dyn GitRepository>>> {
pub fn get_first_worktree_root_repo(&self, cx: &AppContext) -> Option<Arc<dyn GitRepository>> {
let worktree = self.visible_worktrees(cx).next()?.read(cx).as_local()?;
let root_entry = worktree.root_git_entry()?;
@@ -8255,8 +8249,7 @@ impl Project {
cx.background_executor().spawn(async move {
let (repo, relative_path, content) = blame_params?;
let lock = repo.lock();
lock.blame(&relative_path, content)
repo.blame(&relative_path, content)
.with_context(|| format!("Failed to blame {:?}", relative_path.0))
})
} else {

View File

@@ -30,6 +30,10 @@ enum StartTagOutcome {
pub struct MarkdownWriter {
current_element_stack: VecDeque<HtmlElement>,
/// The number of columns in the current `<table>`.
current_table_columns: usize,
is_first_th: bool,
is_first_td: bool,
/// The Markdown output.
markdown: String,
}
@@ -38,6 +42,9 @@ impl MarkdownWriter {
pub fn new() -> Self {
Self {
current_element_stack: VecDeque::new(),
current_table_columns: 0,
is_first_th: true,
is_first_td: true,
markdown: String::new(),
}
}
@@ -58,6 +65,11 @@ impl MarkdownWriter {
self.push_str("\n");
}
/// Appends a blank line to the end of the Markdown output.
fn push_blank_line(&mut self) {
self.push_str("\n\n");
}
pub fn run(mut self, root_node: &Handle) -> Result<String> {
self.visit_node(&root_node)?;
Ok(Self::prettify_markdown(self.markdown))
@@ -148,13 +160,43 @@ impl MarkdownWriter {
.collect::<Vec<_>>()
})
.unwrap_or_default();
let is_rust = classes.into_iter().any(|class| class == "rust");
let language = if is_rust { "rs" } else { "" };
let is_rust = classes.iter().any(|class| class == &"rust");
let language = is_rust
.then(|| "rs")
.or_else(|| {
classes.iter().find_map(|class| {
if let Some((_, language)) = class.split_once("language-") {
Some(language.trim())
} else {
None
}
})
})
.unwrap_or("");
self.push_str(&format!("\n```{language}\n"))
self.push_str(&format!("\n\n```{language}\n"))
}
"ul" | "ol" => self.push_newline(),
"li" => self.push_str("- "),
"thead" => self.push_blank_line(),
"tr" => self.push_newline(),
"th" => {
self.current_table_columns += 1;
if self.is_first_th {
self.is_first_th = false;
} else {
self.push_str(" ");
}
self.push_str("| ");
}
"td" => {
if self.is_first_td {
self.is_first_td = false;
} else {
self.push_str(" ");
}
self.push_str("| ");
}
"summary" => {
if tag.attrs.borrow().iter().any(|attr| {
attr.name.local.to_string() == "class" && attr.value.to_string() == "hideme"
@@ -162,6 +204,13 @@ impl MarkdownWriter {
return StartTagOutcome::Skip;
}
}
"button" => {
if tag.attrs.borrow().iter().any(|attr| {
attr.name.local.to_string() == "id" && attr.value.to_string() == "copy-path"
}) {
return StartTagOutcome::Skip;
}
}
"div" | "span" => {
let classes_to_skip = ["nav-container", "sidebar-elems", "out-of-band"];
@@ -198,6 +247,24 @@ impl MarkdownWriter {
"pre" => self.push_str("\n```\n"),
"ul" | "ol" => self.push_newline(),
"li" => self.push_newline(),
"thead" => {
self.push_newline();
for ix in 0..self.current_table_columns {
if ix > 0 {
self.push_str(" ");
}
self.push_str("| ---");
}
self.push_str(" |");
self.is_first_th = true;
}
"tr" => {
self.push_str(" |");
self.is_first_td = true;
}
"table" => {
self.current_table_columns = 0;
}
"div" => {
if tag.attrs.borrow().iter().any(|attr| {
attr.name.local.to_string() == "class" && attr.value.to_string() == "item-name"

View File

@@ -45,7 +45,28 @@ mod tests {
use super::*;
#[test]
fn test_code_blocks() {
fn test_main_heading_buttons_get_removed() {
let html = indoc! {r##"
<div class="main-heading">
<h1>Crate <a class="mod" href="#">serde</a><button id="copy-path" title="Copy item path to clipboard">Copy item path</button></h1>
<span class="out-of-band">
<a class="src" href="../src/serde/lib.rs.html#1-340">source</a> · <button id="toggle-all-docs" title="collapse all docs">[<span></span>]</button>
</span>
</div>
"##};
let expected = indoc! {"
# Crate serde
"}
.trim();
assert_eq!(
convert_rustdoc_to_markdown(html.as_bytes()).unwrap(),
expected
)
}
#[test]
fn test_rust_code_block() {
let html = indoc! {r#"
<pre class="rust rust-example-rendered"><code><span class="kw">use </span>axum::extract::{Path, Query, Json};
<span class="kw">use </span>std::collections::HashMap;
@@ -85,4 +106,89 @@ mod tests {
expected
)
}
#[test]
fn test_toml_code_block() {
let html = indoc! {r##"
<h2 id="required-dependencies"><a class="doc-anchor" href="#required-dependencies">§</a>Required dependencies</h2>
<p>To use axum there are a few dependencies you have to pull in as well:</p>
<div class="example-wrap"><pre class="language-toml"><code>[dependencies]
axum = &quot;&lt;latest-version&gt;&quot;
tokio = { version = &quot;&lt;latest-version&gt;&quot;, features = [&quot;full&quot;] }
tower = &quot;&lt;latest-version&gt;&quot;
</code></pre></div>
"##};
let expected = indoc! {r#"
## Required dependencies
To use axum there are a few dependencies you have to pull in as well:
```toml
[dependencies]
axum = "<latest-version>"
tokio = { version = "<latest-version>", features = ["full"] }
tower = "<latest-version>"
```
"#}
.trim();
assert_eq!(
convert_rustdoc_to_markdown(html.as_bytes()).unwrap(),
expected
)
}
#[test]
fn test_table() {
let html = indoc! {r##"
<h2 id="feature-flags"><a class="doc-anchor" href="#feature-flags">§</a>Feature flags</h2>
<p>axum uses a set of <a href="https://doc.rust-lang.org/cargo/reference/features.html#the-features-section">feature flags</a> to reduce the amount of compiled and
optional dependencies.</p>
<p>The following optional features are available:</p>
<div><table><thead><tr><th>Name</th><th>Description</th><th>Default?</th></tr></thead><tbody>
<tr><td><code>http1</code></td><td>Enables hypers <code>http1</code> feature</td><td>Yes</td></tr>
<tr><td><code>http2</code></td><td>Enables hypers <code>http2</code> feature</td><td>No</td></tr>
<tr><td><code>json</code></td><td>Enables the <a href="struct.Json.html" title="struct axum::Json"><code>Json</code></a> type and some similar convenience functionality</td><td>Yes</td></tr>
<tr><td><code>macros</code></td><td>Enables optional utility macros</td><td>No</td></tr>
<tr><td><code>matched-path</code></td><td>Enables capturing of every requests router path and the <a href="extract/struct.MatchedPath.html" title="struct axum::extract::MatchedPath"><code>MatchedPath</code></a> extractor</td><td>Yes</td></tr>
<tr><td><code>multipart</code></td><td>Enables parsing <code>multipart/form-data</code> requests with <a href="extract/struct.Multipart.html" title="struct axum::extract::Multipart"><code>Multipart</code></a></td><td>No</td></tr>
<tr><td><code>original-uri</code></td><td>Enables capturing of every requests original URI and the <a href="extract/struct.OriginalUri.html" title="struct axum::extract::OriginalUri"><code>OriginalUri</code></a> extractor</td><td>Yes</td></tr>
<tr><td><code>tokio</code></td><td>Enables <code>tokio</code> as a dependency and <code>axum::serve</code>, <code>SSE</code> and <code>extract::connect_info</code> types.</td><td>Yes</td></tr>
<tr><td><code>tower-log</code></td><td>Enables <code>tower</code>s <code>log</code> feature</td><td>Yes</td></tr>
<tr><td><code>tracing</code></td><td>Log rejections from built-in extractors</td><td>Yes</td></tr>
<tr><td><code>ws</code></td><td>Enables WebSockets support via <a href="extract/ws/index.html" title="mod axum::extract::ws"><code>extract::ws</code></a></td><td>No</td></tr>
<tr><td><code>form</code></td><td>Enables the <code>Form</code> extractor</td><td>Yes</td></tr>
<tr><td><code>query</code></td><td>Enables the <code>Query</code> extractor</td><td>Yes</td></tr>
</tbody></table>
"##};
let expected = indoc! {r#"
## Feature flags
axum uses a set of feature flags to reduce the amount of compiled and
optional dependencies.The following optional features are available:
| Name | Description | Default? |
| --- | --- | --- |
| `http1` | Enables hypers `http1` feature | Yes |
| `http2` | Enables hypers `http2` feature | No |
| `json` | Enables the `Json` type and some similar convenience functionality | Yes |
| `macros` | Enables optional utility macros | No |
| `matched-path` | Enables capturing of every requests router path and the `MatchedPath` extractor | Yes |
| `multipart` | Enables parsing `multipart/form-data` requests with `Multipart` | No |
| `original-uri` | Enables capturing of every requests original URI and the `OriginalUri` extractor | Yes |
| `tokio` | Enables `tokio` as a dependency and `axum::serve`, `SSE` and `extract::connect_info` types. | Yes |
| `tower-log` | Enables `tower`s `log` feature | Yes |
| `tracing` | Log rejections from built-in extractors | Yes |
| `ws` | Enables WebSockets support via `extract::ws` | No |
| `form` | Enables the `Form` extractor | Yes |
| `query` | Enables the `Query` extractor | Yes |
"#}
.trim();
assert_eq!(
convert_rustdoc_to_markdown(html.as_bytes()).unwrap(),
expected
)
}
}

View File

@@ -3,7 +3,7 @@ mod registrar;
use crate::{
search_bar::render_nav_button, FocusSearch, NextHistoryQuery, PreviousHistoryQuery, ReplaceAll,
ReplaceNext, SearchOptions, SelectAllMatches, SelectNextMatch, SelectPrevMatch,
ToggleCaseSensitive, ToggleRegex, ToggleReplace, ToggleWholeWord,
ToggleCaseSensitive, ToggleRegex, ToggleReplace, ToggleSelection, ToggleWholeWord,
};
use any_vec::AnyVec;
use collections::HashMap;
@@ -48,6 +48,8 @@ pub struct Deploy {
pub focus: bool,
#[serde(default)]
pub replace_enabled: bool,
#[serde(default)]
pub selection_search_enabled: bool,
}
impl_actions!(buffer_search, [Deploy]);
@@ -59,6 +61,7 @@ impl Deploy {
Self {
focus: true,
replace_enabled: false,
selection_search_enabled: false,
}
}
}
@@ -90,6 +93,7 @@ pub struct BufferSearchBar {
search_history: SearchHistory,
search_history_cursor: SearchHistoryCursor,
replace_enabled: bool,
selection_search_enabled: bool,
scroll_handle: ScrollHandle,
editor_scroll_handle: ScrollHandle,
editor_needed_width: Pixels,
@@ -228,7 +232,7 @@ impl Render for BufferSearchBar {
}),
)
}))
.children(supported_options.word.then(|| {
.children(supported_options.regex.then(|| {
self.render_search_option_button(
SearchOptions::REGEX,
cx.listener(|this, _, cx| this.toggle_regex(&ToggleRegex, cx)),
@@ -251,6 +255,26 @@ impl Render for BufferSearchBar {
.tooltip(|cx| Tooltip::for_action("Toggle replace", &ToggleReplace, cx)),
)
})
.when(supported_options.selection, |this| {
this.child(
IconButton::new(
"buffer-search-bar-toggle-search-selection-button",
IconName::SearchSelection,
)
.style(ButtonStyle::Subtle)
.when(self.selection_search_enabled, |button| {
button.style(ButtonStyle::Filled)
})
.on_click(cx.listener(|this, _: &ClickEvent, cx| {
this.toggle_selection(&ToggleSelection, cx);
}))
.selected(self.selection_search_enabled)
.size(ButtonSize::Compact)
.tooltip(|cx| {
Tooltip::for_action("Toggle search selection", &ToggleSelection, cx)
}),
)
})
.child(
h_flex()
.flex_none()
@@ -359,6 +383,9 @@ impl Render for BufferSearchBar {
.when(self.supported_options().regex, |this| {
this.on_action(cx.listener(Self::toggle_regex))
})
.when(self.supported_options().selection, |this| {
this.on_action(cx.listener(Self::toggle_selection))
})
.gap_2()
.child(
h_flex()
@@ -440,6 +467,11 @@ impl BufferSearchBar {
this.toggle_whole_word(action, cx);
}
}));
registrar.register_handler(ForDeployed(|this, action: &ToggleSelection, cx| {
if this.supported_options().selection {
this.toggle_selection(action, cx);
}
}));
registrar.register_handler(ForDeployed(|this, action: &ToggleReplace, cx| {
if this.supported_options().replacement {
this.toggle_replace(action, cx);
@@ -497,6 +529,7 @@ impl BufferSearchBar {
search_history_cursor: Default::default(),
active_search: None,
replace_enabled: false,
selection_search_enabled: false,
scroll_handle: ScrollHandle::new(),
editor_scroll_handle: ScrollHandle::new(),
editor_needed_width: px(0.),
@@ -516,8 +549,11 @@ impl BufferSearchBar {
searchable_item.clear_matches(cx);
}
}
if let Some(active_editor) = self.active_searchable_item.as_ref() {
if let Some(active_editor) = self.active_searchable_item.as_mut() {
self.selection_search_enabled = false;
self.replace_enabled = false;
active_editor.search_bar_visibility_changed(false, cx);
active_editor.toggle_filtered_search_ranges(false, cx);
let handle = active_editor.focus_handle(cx);
cx.focus(&handle);
}
@@ -532,6 +568,7 @@ impl BufferSearchBar {
if self.show(cx) {
self.search_suggested(cx);
self.replace_enabled = deploy.replace_enabled;
self.selection_search_enabled = deploy.selection_search_enabled;
if deploy.focus {
let mut handle = self.query_editor.focus_handle(cx).clone();
let mut select_query = true;
@@ -539,10 +576,18 @@ impl BufferSearchBar {
handle = self.replacement_editor.focus_handle(cx).clone();
select_query = false;
};
if select_query {
self.select_query(cx);
}
cx.focus(&handle);
if let Some(active_item) = self.active_searchable_item.as_mut() {
active_item.toggle_filtered_search_ranges(self.selection_search_enabled, cx);
let _ = self.update_matches(cx);
cx.notify();
}
}
return true;
}
@@ -823,6 +868,15 @@ impl BufferSearchBar {
self.toggle_search_option(SearchOptions::WHOLE_WORD, cx)
}
fn toggle_selection(&mut self, _: &ToggleSelection, cx: &mut ViewContext<Self>) {
if let Some(active_item) = self.active_searchable_item.as_mut() {
self.selection_search_enabled = !self.selection_search_enabled;
active_item.toggle_filtered_search_ranges(self.selection_search_enabled, cx);
let _ = self.update_matches(cx);
cx.notify();
}
}
fn toggle_regex(&mut self, _: &ToggleRegex, cx: &mut ViewContext<Self>) {
self.toggle_search_option(SearchOptions::REGEX, cx)
}
@@ -1090,9 +1144,9 @@ mod tests {
use std::ops::Range;
use super::*;
use editor::{display_map::DisplayRow, DisplayPoint, Editor};
use editor::{display_map::DisplayRow, DisplayPoint, Editor, MultiBuffer};
use gpui::{Context, Hsla, TestAppContext, VisualTestContext};
use language::Buffer;
use language::{Buffer, Point};
use project::Project;
use smol::stream::StreamExt as _;
use unindent::Unindent as _;
@@ -1405,6 +1459,15 @@ mod tests {
});
}
fn display_points_of(
background_highlights: Vec<(Range<DisplayPoint>, Hsla)>,
) -> Vec<Range<DisplayPoint>> {
background_highlights
.into_iter()
.map(|(range, _)| range)
.collect::<Vec<_>>()
}
#[gpui::test]
async fn test_search_option_handling(cx: &mut TestAppContext) {
let (editor, search_bar, cx) = init_test(cx);
@@ -1417,12 +1480,6 @@ mod tests {
})
.await
.unwrap();
let display_points_of = |background_highlights: Vec<(Range<DisplayPoint>, Hsla)>| {
background_highlights
.into_iter()
.map(|(range, _)| range)
.collect::<Vec<_>>()
};
editor.update(cx, |editor, cx| {
assert_eq!(
display_points_of(editor.all_text_background_highlights(cx)),
@@ -2032,15 +2089,156 @@ mod tests {
.await;
}
#[gpui::test]
async fn test_find_matches_in_selections_singleton_buffer_multiple_selections(
cx: &mut TestAppContext,
) {
init_globals(cx);
let buffer = cx.new_model(|cx| {
Buffer::local(
r#"
aaa bbb aaa ccc
aaa bbb aaa ccc
aaa bbb aaa ccc
aaa bbb aaa ccc
aaa bbb aaa ccc
aaa bbb aaa ccc
"#
.unindent(),
cx,
)
});
let cx = cx.add_empty_window();
let editor = cx.new_view(|cx| Editor::for_buffer(buffer.clone(), None, cx));
let search_bar = cx.new_view(|cx| {
let mut search_bar = BufferSearchBar::new(cx);
search_bar.set_active_pane_item(Some(&editor), cx);
search_bar.show(cx);
search_bar
});
editor.update(cx, |editor, cx| {
editor.change_selections(None, cx, |s| {
s.select_ranges(vec![Point::new(1, 0)..Point::new(2, 4)])
})
});
search_bar.update(cx, |search_bar, cx| {
let deploy = Deploy {
focus: true,
replace_enabled: false,
selection_search_enabled: true,
};
search_bar.deploy(&deploy, cx);
});
cx.run_until_parked();
search_bar
.update(cx, |search_bar, cx| search_bar.search("aaa", None, cx))
.await
.unwrap();
editor.update(cx, |editor, cx| {
assert_eq!(
editor.search_background_highlights(cx),
&[
Point::new(1, 0)..Point::new(1, 3),
Point::new(1, 8)..Point::new(1, 11),
Point::new(2, 0)..Point::new(2, 3),
]
);
});
}
#[gpui::test]
async fn test_find_matches_in_selections_multiple_excerpts_buffer_multiple_selections(
cx: &mut TestAppContext,
) {
init_globals(cx);
let text = r#"
aaa bbb aaa ccc
aaa bbb aaa ccc
aaa bbb aaa ccc
aaa bbb aaa ccc
aaa bbb aaa ccc
aaa bbb aaa ccc
aaa bbb aaa ccc
aaa bbb aaa ccc
aaa bbb aaa ccc
aaa bbb aaa ccc
aaa bbb aaa ccc
aaa bbb aaa ccc
"#
.unindent();
let cx = cx.add_empty_window();
let editor = cx.new_view(|cx| {
let multibuffer = MultiBuffer::build_multi(
[
(
&text,
vec![
Point::new(0, 0)..Point::new(2, 0),
Point::new(4, 0)..Point::new(5, 0),
],
),
(&text, vec![Point::new(9, 0)..Point::new(11, 0)]),
],
cx,
);
Editor::for_multibuffer(multibuffer, None, false, cx)
});
let search_bar = cx.new_view(|cx| {
let mut search_bar = BufferSearchBar::new(cx);
search_bar.set_active_pane_item(Some(&editor), cx);
search_bar.show(cx);
search_bar
});
editor.update(cx, |editor, cx| {
editor.change_selections(None, cx, |s| {
s.select_ranges(vec![
Point::new(1, 0)..Point::new(1, 4),
Point::new(5, 3)..Point::new(6, 4),
])
})
});
search_bar.update(cx, |search_bar, cx| {
let deploy = Deploy {
focus: true,
replace_enabled: false,
selection_search_enabled: true,
};
search_bar.deploy(&deploy, cx);
});
cx.run_until_parked();
search_bar
.update(cx, |search_bar, cx| search_bar.search("aaa", None, cx))
.await
.unwrap();
editor.update(cx, |editor, cx| {
assert_eq!(
editor.search_background_highlights(cx),
&[
Point::new(1, 0)..Point::new(1, 3),
Point::new(5, 8)..Point::new(5, 11),
Point::new(6, 0)..Point::new(6, 3),
]
);
});
}
#[gpui::test]
async fn test_invalid_regexp_search_after_valid(cx: &mut TestAppContext) {
let (editor, search_bar, cx) = init_test(cx);
let display_points_of = |background_highlights: Vec<(Range<DisplayPoint>, Hsla)>| {
background_highlights
.into_iter()
.map(|(range, _)| range)
.collect::<Vec<_>>()
};
// Search using valid regexp
search_bar
.update(cx, |search_bar, cx| {

View File

@@ -25,6 +25,7 @@ actions!(
ToggleIncludeIgnored,
ToggleRegex,
ToggleReplace,
ToggleSelection,
SelectNextMatch,
SelectPrevMatch,
SelectAllMatches,

View File

@@ -972,6 +972,7 @@ impl SearchableItem for TerminalView {
word: false,
regex: true,
replacement: false,
selection: false,
}
}

View File

@@ -357,7 +357,7 @@ impl Render for ContextMenu {
.unwrap_or_else(|| {
KeyBinding::for_action(&**action, cx)
})
.map(|binding| div().ml_1().child(binding))
.map(|binding| div().ml_4().child(binding))
})),
)
.on_click(move |_, cx| {

View File

@@ -169,6 +169,7 @@ pub enum IconName {
Save,
Screen,
SelectAll,
SearchSelection,
Server,
Settings,
Shift,
@@ -290,6 +291,7 @@ impl IconName {
IconName::Save => "icons/save.svg",
IconName::Screen => "icons/desktop.svg",
IconName::SelectAll => "icons/select_all.svg",
IconName::SearchSelection => "icons/search_selection.svg",
IconName::Server => "icons/server.svg",
IconName::Settings => "icons/file_icons/settings.svg",
IconName::Shift => "icons/shift.svg",

View File

@@ -77,13 +77,14 @@ impl RenderOnce for KeyBinding {
.join(" ")
)
})
.gap(rems(0.125))
.flex_none()
.children(self.key_binding.keystrokes().iter().map(|keystroke| {
let key_icon = Self::icon_for_key(keystroke);
h_flex()
.flex_none()
.p_0p5()
.py_0p5()
.rounded_sm()
.text_color(cx.theme().colors().text_muted)
.when(keystroke.modifiers.function, |el| {

View File

@@ -13,6 +13,51 @@ pub trait PopoverTrigger: IntoElement + Clickable + Selectable + 'static {}
impl<T: IntoElement + Clickable + Selectable + 'static> PopoverTrigger for T {}
pub struct PopoverMenuHandle<M>(Rc<RefCell<Option<PopoverMenuHandleState<M>>>>);
impl<M> Clone for PopoverMenuHandle<M> {
fn clone(&self) -> Self {
Self(self.0.clone())
}
}
impl<M> Default for PopoverMenuHandle<M> {
fn default() -> Self {
Self(Rc::default())
}
}
struct PopoverMenuHandleState<M> {
menu_builder: Rc<dyn Fn(&mut WindowContext) -> Option<View<M>>>,
menu: Rc<RefCell<Option<View<M>>>>,
}
impl<M: ManagedView> PopoverMenuHandle<M> {
pub fn show(&self, cx: &mut WindowContext) {
if let Some(state) = self.0.borrow().as_ref() {
show_menu(&state.menu_builder, &state.menu, cx);
}
}
pub fn hide(&self, cx: &mut WindowContext) {
if let Some(state) = self.0.borrow().as_ref() {
if let Some(menu) = state.menu.borrow().as_ref() {
menu.update(cx, |_, cx| cx.emit(DismissEvent));
}
}
}
pub fn toggle(&self, cx: &mut WindowContext) {
if let Some(state) = self.0.borrow().as_ref() {
if state.menu.borrow().is_some() {
self.hide(cx);
} else {
self.show(cx);
}
}
}
}
pub struct PopoverMenu<M: ManagedView> {
id: ElementId,
child_builder: Option<
@@ -28,6 +73,7 @@ pub struct PopoverMenu<M: ManagedView> {
anchor: AnchorCorner,
attach: Option<AnchorCorner>,
offset: Option<Point<Pixels>>,
trigger_handle: Option<PopoverMenuHandle<M>>,
}
impl<M: ManagedView> PopoverMenu<M> {
@@ -36,35 +82,17 @@ impl<M: ManagedView> PopoverMenu<M> {
self
}
pub fn with_handle(mut self, handle: PopoverMenuHandle<M>) -> Self {
self.trigger_handle = Some(handle);
self
}
pub fn trigger<T: PopoverTrigger>(mut self, t: T) -> Self {
self.child_builder = Some(Box::new(|menu, builder| {
let open = menu.borrow().is_some();
t.selected(open)
.when_some(builder, |el, builder| {
el.on_click({
move |_, cx| {
let Some(new_menu) = (builder)(cx) else {
return;
};
let menu2 = menu.clone();
let previous_focus_handle = cx.focused();
cx.subscribe(&new_menu, move |modal, _: &DismissEvent, cx| {
if modal.focus_handle(cx).contains_focused(cx) {
if let Some(previous_focus_handle) =
previous_focus_handle.as_ref()
{
cx.focus(previous_focus_handle);
}
}
*menu2.borrow_mut() = None;
cx.refresh();
})
.detach();
cx.focus_view(&new_menu);
*menu.borrow_mut() = Some(new_menu);
}
})
el.on_click(move |_, cx| show_menu(&builder, &menu, cx))
})
.into_any_element()
}));
@@ -111,6 +139,32 @@ impl<M: ManagedView> PopoverMenu<M> {
}
}
fn show_menu<M: ManagedView>(
builder: &Rc<dyn Fn(&mut WindowContext) -> Option<View<M>>>,
menu: &Rc<RefCell<Option<View<M>>>>,
cx: &mut WindowContext,
) {
let Some(new_menu) = (builder)(cx) else {
return;
};
let menu2 = menu.clone();
let previous_focus_handle = cx.focused();
cx.subscribe(&new_menu, move |modal, _: &DismissEvent, cx| {
if modal.focus_handle(cx).contains_focused(cx) {
if let Some(previous_focus_handle) = previous_focus_handle.as_ref() {
cx.focus(previous_focus_handle);
}
}
*menu2.borrow_mut() = None;
cx.refresh();
})
.detach();
cx.focus_view(&new_menu);
*menu.borrow_mut() = Some(new_menu);
cx.refresh();
}
/// Creates a [`PopoverMenu`]
pub fn popover_menu<M: ManagedView>(id: impl Into<ElementId>) -> PopoverMenu<M> {
PopoverMenu {
@@ -120,6 +174,7 @@ pub fn popover_menu<M: ManagedView>(id: impl Into<ElementId>) -> PopoverMenu<M>
anchor: AnchorCorner::TopLeft,
attach: None,
offset: None,
trigger_handle: None,
}
}
@@ -190,6 +245,15 @@ impl<M: ManagedView> Element for PopoverMenu<M> {
(child_builder)(element_state.menu.clone(), self.menu_builder.clone())
});
if let Some(trigger_handle) = self.trigger_handle.take() {
if let Some(menu_builder) = self.menu_builder.clone() {
*trigger_handle.0.borrow_mut() = Some(PopoverMenuHandleState {
menu_builder,
menu: element_state.menu.clone(),
});
}
}
let child_layout_id = child_element
.as_mut()
.map(|child_element| child_element.request_layout(cx));

View File

@@ -109,7 +109,7 @@ impl BranchListDelegate {
.get_first_worktree_root_repo(cx)
.context("failed to get root repository for first worktree")?;
let all_branches = repo.lock().branches()?;
let all_branches = repo.branches()?;
Ok(Self {
matches: vec![],
workspace: handle,
@@ -237,7 +237,6 @@ impl PickerDelegate for BranchListDelegate {
.get_first_worktree_root_repo(cx)
.context("failed to get root repository for first worktree")?;
let status = repo
.lock()
.change_branch(&current_pick);
if status.is_err() {
this.delegate.display_error_toast(format!("Failed to checkout branch '{current_pick}', check for conflicts or unstashed files"), cx);
@@ -316,8 +315,6 @@ impl PickerDelegate for BranchListDelegate {
let repo = project
.get_first_worktree_root_repo(cx)
.context("failed to get root repository for first worktree")?;
let repo = repo
.lock();
let status = repo
.create_branch(&current_pick);
if status.is_err() {

View File

@@ -39,8 +39,9 @@ pub struct SearchOptions {
pub case: bool,
pub word: bool,
pub regex: bool,
/// Specifies whether the item supports search & replace.
/// Specifies whether the supports search & replace.
pub replacement: bool,
pub selection: bool,
}
pub trait SearchableItem: Item + EventEmitter<SearchEvent> {
@@ -52,15 +53,18 @@ pub trait SearchableItem: Item + EventEmitter<SearchEvent> {
word: true,
regex: true,
replacement: true,
selection: true,
}
}
fn search_bar_visibility_changed(&mut self, _visible: bool, _cx: &mut ViewContext<Self>) {}
fn has_filtered_search_ranges(&mut self) -> bool {
false
Self::supported_options().selection
}
fn toggle_filtered_search_ranges(&mut self, _enabled: bool, _cx: &mut ViewContext<Self>) {}
fn clear_matches(&mut self, cx: &mut ViewContext<Self>);
fn update_matches(&mut self, matches: &[Self::Match], cx: &mut ViewContext<Self>);
fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> String;
@@ -138,6 +142,8 @@ pub trait SearchableItemHandle: ItemHandle {
cx: &mut WindowContext,
) -> Option<usize>;
fn search_bar_visibility_changed(&self, visible: bool, cx: &mut WindowContext);
fn toggle_filtered_search_ranges(&mut self, enabled: bool, cx: &mut WindowContext);
}
impl<T: SearchableItem> SearchableItemHandle for View<T> {
@@ -240,6 +246,12 @@ impl<T: SearchableItem> SearchableItemHandle for View<T> {
this.search_bar_visibility_changed(visible, cx)
});
}
fn toggle_filtered_search_ranges(&mut self, enabled: bool, cx: &mut WindowContext) {
self.update(cx, |this, cx| {
this.toggle_filtered_search_ranges(enabled, cx)
});
}
}
impl From<Box<dyn SearchableItemHandle>> for AnyView {

View File

@@ -311,14 +311,14 @@ struct BackgroundScannerState {
#[derive(Debug, Clone)]
pub struct LocalRepositoryEntry {
pub(crate) git_dir_scan_id: usize,
pub(crate) repo_ptr: Arc<Mutex<dyn GitRepository>>,
pub(crate) repo_ptr: Arc<dyn GitRepository>,
/// Path to the actual .git folder.
/// Note: if .git is a file, this points to the folder indicated by the .git file
pub(crate) git_dir_path: Arc<Path>,
}
impl LocalRepositoryEntry {
pub fn repo(&self) -> &Arc<Mutex<dyn GitRepository>> {
pub fn repo(&self) -> &Arc<dyn GitRepository> {
&self.repo_ptr
}
}
@@ -1145,7 +1145,7 @@ impl LocalWorktree {
if abs_path_metadata.is_dir || abs_path_metadata.is_symlink {
None
} else {
git_repo.lock().load_index_text(&repo_path)
git_repo.load_index_text(&repo_path)
}
}
}));
@@ -2236,7 +2236,7 @@ impl LocalSnapshot {
Some((repo_entry, self.git_repositories.get(&work_directory_id)?))
}
pub fn local_git_repo(&self, path: &Path) -> Option<Arc<Mutex<dyn GitRepository>>> {
pub fn local_git_repo(&self, path: &Path) -> Option<Arc<dyn GitRepository>> {
self.repo_for_path(path)
.map(|(_, entry)| entry.repo_ptr.clone())
}
@@ -2556,7 +2556,6 @@ impl BackgroundScannerState {
work_directory: workdir_path,
statuses: repo
.repo_ptr
.lock()
.statuses(&repo_path)
.log_err()
.unwrap_or_default(),
@@ -2704,7 +2703,7 @@ impl BackgroundScannerState {
&mut self,
dot_git_path: Arc<Path>,
fs: &dyn Fs,
) -> Option<(RepositoryWorkDirectory, Arc<Mutex<dyn GitRepository>>)> {
) -> Option<(RepositoryWorkDirectory, Arc<dyn GitRepository>)> {
let work_dir_path: Arc<Path> = match dot_git_path.parent() {
Some(parent_dir) => {
// Guard against repositories inside the repository metadata
@@ -2739,7 +2738,7 @@ impl BackgroundScannerState {
dot_git_path: Arc<Path>,
location_in_repo: Option<Arc<Path>>,
fs: &dyn Fs,
) -> Option<(RepositoryWorkDirectory, Arc<Mutex<dyn GitRepository>>)> {
) -> Option<(RepositoryWorkDirectory, Arc<dyn GitRepository>)> {
let work_dir_id = self
.snapshot
.entry_for_path(work_dir_path.clone())
@@ -2750,14 +2749,16 @@ impl BackgroundScannerState {
}
let abs_path = self.snapshot.abs_path.join(&dot_git_path);
let t0 = Instant::now();
let repository = fs.open_repo(&abs_path)?;
log::trace!("constructed libgit2 repo in {:?}", t0.elapsed());
let work_directory = RepositoryWorkDirectory(work_dir_path.clone());
self.snapshot.repository_entries.insert(
work_directory.clone(),
RepositoryEntry {
work_directory: work_dir_id.into(),
branch: repository.lock().branch_name().map(Into::into),
branch: repository.branch_name().map(Into::into),
location_in_repo,
},
);
@@ -3827,16 +3828,17 @@ impl BackgroundScanner {
let child_path: Arc<Path> = job.path.join(child_name).into();
if child_name == *DOT_GIT {
if let Some((work_directory, repository)) = self
let repo = self
.state
.lock()
.build_git_repository(child_path.clone(), self.fs.as_ref())
{
.build_git_repository(child_path.clone(), self.fs.as_ref());
if let Some((work_directory, repository)) = repo {
let t0 = Instant::now();
let statuses = repository
.lock()
.statuses(Path::new(""))
.log_err()
.unwrap_or_default();
log::trace!("computed git status in {:?}", t0.elapsed());
containing_repository = Some(ScanJobContainingRepository {
work_directory,
statuses,
@@ -4077,7 +4079,7 @@ impl BackgroundScanner {
if !is_dir && !fs_entry.is_ignored && !fs_entry.is_external {
if let Some((repo_entry, repo)) = state.snapshot.repo_for_path(path) {
if let Ok(repo_path) = repo_entry.relativize(&state.snapshot, path) {
fs_entry.git_status = repo.repo_ptr.lock().status(&repo_path);
fs_entry.git_status = repo.repo_ptr.status(&repo_path);
}
}
}
@@ -4263,7 +4265,7 @@ impl BackgroundScanner {
if !entry.is_dir() && !entry.is_ignored && !entry.is_external {
if let Some((ref repo_entry, local_repo)) = repo {
if let Ok(repo_path) = repo_entry.relativize(&snapshot, &entry.path) {
entry.git_status = local_repo.repo_ptr.lock().status(&repo_path);
entry.git_status = local_repo.repo_ptr.status(&repo_path);
}
}
}
@@ -4326,7 +4328,7 @@ impl BackgroundScanner {
};
log::info!("reload git repository {dot_git_dir:?}");
let repo = repository.repo_ptr.lock();
let repo = &repository.repo_ptr;
let branch = repo.branch_name();
repo.reload_index();
@@ -4344,7 +4346,6 @@ impl BackgroundScanner {
};
let statuses = repository
.lock()
.statuses(Path::new(""))
.log_err()
.unwrap_or_default();

View File

@@ -35,7 +35,10 @@
arguments: (arguments
.
(argument
(encapsed_string (string_value) @name)
[
(encapsed_string (string_value) @name)
(string (string_value) @name)
]
)
)
) @item

View File

@@ -94,7 +94,10 @@
arguments: (arguments
.
(argument
(encapsed_string (string_value) @run)
[
(encapsed_string (string_value) @run)
(string (string_value) @run)
]
)
)
) @pest-test