Compare commits
28 Commits
linux-fix-
...
search-in-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b3da4995b0 | ||
|
|
23007f304a | ||
|
|
e1846f4dd3 | ||
|
|
fb6cbbe7e0 | ||
|
|
6dba6860ea | ||
|
|
f990bc050b | ||
|
|
9c7972d5db | ||
|
|
af2bbb190d | ||
|
|
8c450ae78c | ||
|
|
03f5c1b969 | ||
|
|
fea45a2d8b | ||
|
|
41f1f7f907 | ||
|
|
457c4c8fe0 | ||
|
|
b09b74e32b | ||
|
|
85710c99f0 | ||
|
|
ba2a4d789f | ||
|
|
c4cca7a527 | ||
|
|
39a2cdb13f | ||
|
|
8f942bf647 | ||
|
|
1ecd13ba50 | ||
|
|
c118012223 | ||
|
|
7a30937e21 | ||
|
|
3c5d141a04 | ||
|
|
bf7c6a676a | ||
|
|
a259042f92 | ||
|
|
436a8fa0ce | ||
|
|
55c47305c8 | ||
|
|
6ff01b17ca |
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
@@ -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
|
||||
|
||||
4
.github/workflows/release_nightly.yml
vendored
4
.github/workflows/release_nightly.yml
vendored
@@ -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
3
Cargo.lock
generated
@@ -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]]
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
1
assets/icons/search_selection.svg
Normal file
1
assets/icons/search_selection.svg
Normal 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 |
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
]
|
||||
);
|
||||
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
84
crates/assistant/src/model_selector.rs
Normal file
84
crates/assistant/src/model_selector.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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, ¤t_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, ¤t_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 })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -765,6 +765,7 @@ impl SearchableItem for LspLogView {
|
||||
regex: true,
|
||||
// LSP log is read-only.
|
||||
replacement: false,
|
||||
selection: false,
|
||||
}
|
||||
}
|
||||
fn active_match_index(
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,3 +20,4 @@ isahc.workspace = true
|
||||
schemars = { workspace = true, optional = true }
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
strum.workspace = true
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 = "<latest-version>"
|
||||
tokio = { version = "<latest-version>", features = ["full"] }
|
||||
tower = "<latest-version>"
|
||||
</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 hyper’s <code>http1</code> feature</td><td>Yes</td></tr>
|
||||
<tr><td><code>http2</code></td><td>Enables hyper’s <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 request’s 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 request’s 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 hyper’s `http1` feature | Yes |
|
||||
| `http2` | Enables hyper’s `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 request’s router path and the `MatchedPath` extractor | Yes |
|
||||
| `multipart` | Enables parsing `multipart/form-data` requests with `Multipart` | No |
|
||||
| `original-uri` | Enables capturing of every request’s 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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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| {
|
||||
|
||||
@@ -25,6 +25,7 @@ actions!(
|
||||
ToggleIncludeIgnored,
|
||||
ToggleRegex,
|
||||
ToggleReplace,
|
||||
ToggleSelection,
|
||||
SelectNextMatch,
|
||||
SelectPrevMatch,
|
||||
SelectAllMatches,
|
||||
|
||||
@@ -972,6 +972,7 @@ impl SearchableItem for TerminalView {
|
||||
word: false,
|
||||
regex: true,
|
||||
replacement: false,
|
||||
selection: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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| {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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| {
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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(¤t_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(¤t_pick);
|
||||
if status.is_err() {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -35,7 +35,10 @@
|
||||
arguments: (arguments
|
||||
.
|
||||
(argument
|
||||
(encapsed_string (string_value) @name)
|
||||
[
|
||||
(encapsed_string (string_value) @name)
|
||||
(string (string_value) @name)
|
||||
]
|
||||
)
|
||||
)
|
||||
) @item
|
||||
|
||||
@@ -94,7 +94,10 @@
|
||||
arguments: (arguments
|
||||
.
|
||||
(argument
|
||||
(encapsed_string (string_value) @run)
|
||||
[
|
||||
(encapsed_string (string_value) @run)
|
||||
(string (string_value) @run)
|
||||
]
|
||||
)
|
||||
)
|
||||
) @pest-test
|
||||
|
||||
Reference in New Issue
Block a user