Compare commits
32 Commits
linux-fix-
...
simpler-vi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a55e496a23 | ||
|
|
f89996ac21 | ||
|
|
d12b8c3945 | ||
|
|
356fcec337 | ||
|
|
08123a270a | ||
|
|
6eb8e83411 | ||
|
|
4c51ee7816 | ||
|
|
660cf214c7 | ||
|
|
b2565fadfb | ||
|
|
2cff075c53 | ||
|
|
819bb2663d | ||
|
|
dc141d0f61 | ||
|
|
22cf73acec | ||
|
|
1d46a52c62 | ||
|
|
fda975fb76 | ||
|
|
0f32145ecb | ||
|
|
6fe665ab94 | ||
|
|
279c5ab81f | ||
|
|
99901801f4 | ||
|
|
4dc98026c4 | ||
|
|
c83d1c23d7 | ||
|
|
39a2cdb13f | ||
|
|
8f942bf647 | ||
|
|
1ecd13ba50 | ||
|
|
c118012223 | ||
|
|
7a30937e21 | ||
|
|
3c5d141a04 | ||
|
|
bf7c6a676a | ||
|
|
a259042f92 | ||
|
|
436a8fa0ce | ||
|
|
55c47305c8 | ||
|
|
6ff01b17ca |
10
.github/workflows/ci.yml
vendored
10
.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
|
||||
@@ -99,7 +101,9 @@ jobs:
|
||||
- name: Build other binaries and features
|
||||
run: cargo build --workspace --bins --all-features; cargo check -p gpui --features "macos-blade"
|
||||
|
||||
# todo(linux): Actually run the tests
|
||||
linux_tests:
|
||||
timeout-minutes: 60
|
||||
name: (Linux) Run Clippy and tests
|
||||
runs-on:
|
||||
- self-hosted
|
||||
@@ -116,14 +120,12 @@ jobs:
|
||||
- name: cargo clippy
|
||||
run: cargo xtask clippy
|
||||
|
||||
- name: Run tests
|
||||
uses: ./.github/actions/run_tests
|
||||
|
||||
- name: Build Zed
|
||||
run: cargo build -p zed
|
||||
|
||||
# todo(windows): Actually run the tests
|
||||
windows_tests:
|
||||
timeout-minutes: 60
|
||||
name: (Windows) Run Clippy and tests
|
||||
runs-on: hosted-windows-1
|
||||
steps:
|
||||
@@ -144,6 +146,7 @@ jobs:
|
||||
run: cargo build -p zed
|
||||
|
||||
bundle-mac:
|
||||
timeout-minutes: 60
|
||||
name: Create a macOS bundle
|
||||
runs-on:
|
||||
- self-hosted
|
||||
@@ -254,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:
|
||||
|
||||
5
Cargo.lock
generated
5
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",
|
||||
@@ -3391,7 +3393,7 @@ version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412"
|
||||
dependencies = [
|
||||
"libloading 0.8.0",
|
||||
"libloading 0.7.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6983,6 +6985,7 @@ dependencies = [
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"strum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -494,5 +494,10 @@ non_canonical_partial_ord_impl = "allow"
|
||||
reversed_empty_ranges = "allow"
|
||||
type_complexity = "allow"
|
||||
|
||||
[workspace.lints.rust]
|
||||
unexpected_cfgs = { level = "warn", check-cfg = [
|
||||
'cfg(gles)', # used in gpui
|
||||
] }
|
||||
|
||||
[workspace.metadata.cargo-machete]
|
||||
ignored = ["bindgen", "cbindgen", "prost_build", "serde"]
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -214,10 +214,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"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
@@ -35,7 +36,7 @@ use futures::future::Shared;
|
||||
use futures::{FutureExt, StreamExt};
|
||||
use gpui::{
|
||||
canvas, div, point, relative, rems, uniform_list, Action, AnyElement, AnyView, AppContext,
|
||||
AsyncAppContext, AsyncWindowContext, AvailableSpace, ClipboardItem, Context, Empty,
|
||||
AsyncAppContext, AsyncWindowContext, AvailableSpace, ClipboardItem, StaticContext, Empty,
|
||||
EventEmitter, FocusHandle, FocusableView, FontStyle, FontWeight, HighlightStyle,
|
||||
InteractiveElement, IntoElement, Model, ModelContext, ParentElement, Pixels, Render,
|
||||
SharedString, StatefulInteractiveElement, Styled, Subscription, Task, TextStyle,
|
||||
@@ -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())
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -391,7 +391,7 @@ mod tests {
|
||||
|
||||
use super::*;
|
||||
use futures::stream::{self};
|
||||
use gpui::{Context, TestAppContext};
|
||||
use gpui::{StaticContext, TestAppContext};
|
||||
use indoc::indoc;
|
||||
use language::{
|
||||
language_settings, tree_sitter_rust, Buffer, Language, LanguageConfig, LanguageMatcher,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
810
crates/assistant/src/prompt_library.rs
Normal file
810
crates/assistant/src/prompt_library.rs
Normal file
@@ -0,0 +1,810 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use chrono::{DateTime, Utc};
|
||||
use collections::HashMap;
|
||||
use editor::Editor;
|
||||
use futures::{
|
||||
future::{self, BoxFuture, Shared},
|
||||
FutureExt,
|
||||
};
|
||||
use fuzzy::StringMatchCandidate;
|
||||
use gpui::{
|
||||
actions, point, size, AppContext, BackgroundExecutor, Bounds, DevicePixels, Empty,
|
||||
EventEmitter, Global, PromptLevel, ReadGlobal, Subscription, Task, TitlebarOptions, View,
|
||||
WindowBounds, WindowHandle, WindowOptions,
|
||||
};
|
||||
use heed::{types::SerdeBincode, Database, RoTxn};
|
||||
use language::{language_settings::SoftWrap, Buffer, LanguageRegistry};
|
||||
use parking_lot::Mutex;
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
cmp::Reverse,
|
||||
future::Future,
|
||||
path::PathBuf,
|
||||
sync::{atomic::AtomicBool, Arc},
|
||||
};
|
||||
use ui::{
|
||||
div, prelude::*, IconButtonShape, ListItem, ListItemSpacing, ParentElement, Render,
|
||||
SharedString, Styled, TitleBar, Tooltip, ViewContext, VisualContext,
|
||||
};
|
||||
use util::{paths::PROMPTS_DIR, ResultExt};
|
||||
use uuid::Uuid;
|
||||
|
||||
actions!(
|
||||
prompt_library,
|
||||
[NewPrompt, SavePrompt, DeletePrompt, ToggleDefaultPrompt]
|
||||
);
|
||||
|
||||
/// Init starts loading the PromptStore in the background and assigns
|
||||
/// a shared future to a global.
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
let db_path = PROMPTS_DIR.join("prompts-library-db.0.mdb");
|
||||
let prompt_store_future = PromptStore::new(db_path, cx.background_executor().clone())
|
||||
.then(|result| future::ready(result.map(Arc::new).map_err(Arc::new)))
|
||||
.boxed()
|
||||
.shared();
|
||||
cx.set_global(GlobalPromptStore(prompt_store_future))
|
||||
}
|
||||
|
||||
/// This function opens a new prompt library window if one doesn't exist already.
|
||||
/// If one exists, it brings it to the foreground.
|
||||
///
|
||||
/// Note that, when opening a new window, this waits for the PromptStore to be
|
||||
/// initialized. If it was initialized successfully, it returns a window handle
|
||||
/// to a prompt library.
|
||||
pub fn open_prompt_library(
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<WindowHandle<PromptLibrary>>> {
|
||||
let existing_window = cx
|
||||
.windows()
|
||||
.into_iter()
|
||||
.find_map(|window| window.downcast::<PromptLibrary>());
|
||||
if let Some(existing_window) = existing_window {
|
||||
existing_window
|
||||
.update(cx, |_, cx| cx.activate_window())
|
||||
.ok();
|
||||
Task::ready(Ok(existing_window))
|
||||
} else {
|
||||
let store = PromptStore::global(cx);
|
||||
cx.spawn(|cx| async move {
|
||||
let store = store.await?;
|
||||
cx.update(|cx| {
|
||||
let bounds = Bounds::centered(
|
||||
None,
|
||||
size(DevicePixels::from(1024), DevicePixels::from(768)),
|
||||
cx,
|
||||
);
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
titlebar: Some(TitlebarOptions {
|
||||
title: None,
|
||||
appears_transparent: true,
|
||||
traffic_light_position: Some(point(px(9.0), px(9.0))),
|
||||
}),
|
||||
window_bounds: Some(WindowBounds::Windowed(bounds)),
|
||||
..Default::default()
|
||||
},
|
||||
|cx| cx.new_view(|cx| PromptLibrary::new(store, language_registry, cx)),
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PromptLibrary {
|
||||
store: Arc<PromptStore>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
prompt_editors: HashMap<PromptId, View<Editor>>,
|
||||
active_prompt_id: Option<PromptId>,
|
||||
picker: View<Picker<PromptPickerDelegate>>,
|
||||
pending_load: Task<()>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
struct PromptPickerDelegate {
|
||||
store: Arc<PromptStore>,
|
||||
selected_index: usize,
|
||||
matches: Vec<PromptMetadata>,
|
||||
}
|
||||
|
||||
enum PromptPickerEvent {
|
||||
Confirmed { prompt_id: PromptId },
|
||||
Deleted { prompt_id: PromptId },
|
||||
ToggledDefault { prompt_id: PromptId },
|
||||
}
|
||||
|
||||
impl EventEmitter<PromptPickerEvent> for Picker<PromptPickerDelegate> {}
|
||||
|
||||
impl PickerDelegate for PromptPickerDelegate {
|
||||
type ListItem = ListItem;
|
||||
|
||||
fn match_count(&self) -> usize {
|
||||
self.matches.len()
|
||||
}
|
||||
|
||||
fn selected_index(&self) -> usize {
|
||||
self.selected_index
|
||||
}
|
||||
|
||||
fn set_selected_index(&mut self, ix: usize, _cx: &mut ViewContext<Picker<Self>>) {
|
||||
self.selected_index = ix;
|
||||
}
|
||||
|
||||
fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
|
||||
"Search...".into()
|
||||
}
|
||||
|
||||
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
|
||||
let search = self.store.search(query);
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let matches = search.await;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.delegate.selected_index = 0;
|
||||
this.delegate.matches = matches;
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
|
||||
if let Some(prompt) = self.matches.get(self.selected_index) {
|
||||
cx.emit(PromptPickerEvent::Confirmed {
|
||||
prompt_id: prompt.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn dismissed(&mut self, _cx: &mut ViewContext<Picker<Self>>) {}
|
||||
|
||||
fn render_match(
|
||||
&self,
|
||||
ix: usize,
|
||||
selected: bool,
|
||||
cx: &mut ViewContext<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
let prompt = self.matches.get(ix)?;
|
||||
let default = prompt.default;
|
||||
let prompt_id = prompt.id;
|
||||
Some(
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.selected(selected)
|
||||
.child(Label::new(
|
||||
prompt.title.clone().unwrap_or("Untitled".into()),
|
||||
))
|
||||
.end_slot(if default {
|
||||
IconButton::new("toggle-default-prompt", IconName::StarFilled)
|
||||
.shape(IconButtonShape::Square)
|
||||
.into_any_element()
|
||||
} else {
|
||||
Empty.into_any()
|
||||
})
|
||||
.end_hover_slot(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
IconButton::new("delete-prompt", IconName::Trash)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(move |cx| Tooltip::text("Delete Prompt", cx))
|
||||
.on_click(cx.listener(move |_, _, cx| {
|
||||
cx.emit(PromptPickerEvent::Deleted { prompt_id })
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
IconButton::new(
|
||||
"toggle-default-prompt",
|
||||
if default {
|
||||
IconName::StarFilled
|
||||
} else {
|
||||
IconName::Star
|
||||
},
|
||||
)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::text(
|
||||
if default {
|
||||
"Remove from Default Prompt"
|
||||
} else {
|
||||
"Add to Default Prompt"
|
||||
},
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.on_click(cx.listener(move |_, _, cx| {
|
||||
cx.emit(PromptPickerEvent::ToggledDefault { prompt_id })
|
||||
})),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl PromptLibrary {
|
||||
fn new(
|
||||
store: Arc<PromptStore>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let delegate = PromptPickerDelegate {
|
||||
store: store.clone(),
|
||||
selected_index: 0,
|
||||
matches: Vec::new(),
|
||||
};
|
||||
|
||||
let picker = cx.new_view(|cx| {
|
||||
let picker = Picker::uniform_list(delegate, cx)
|
||||
.modal(false)
|
||||
.max_height(None);
|
||||
picker.focus(cx);
|
||||
picker
|
||||
});
|
||||
let mut this = Self {
|
||||
store: store.clone(),
|
||||
language_registry,
|
||||
prompt_editors: HashMap::default(),
|
||||
active_prompt_id: None,
|
||||
pending_load: Task::ready(()),
|
||||
_subscriptions: vec![cx.subscribe(&picker, Self::handle_picker_event)],
|
||||
picker,
|
||||
};
|
||||
if let Some(prompt_id) = store.most_recently_saved() {
|
||||
this.load_prompt(prompt_id, false, cx);
|
||||
}
|
||||
this
|
||||
}
|
||||
|
||||
fn handle_picker_event(
|
||||
&mut self,
|
||||
_: View<Picker<PromptPickerDelegate>>,
|
||||
event: &PromptPickerEvent,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
match event {
|
||||
PromptPickerEvent::Confirmed { prompt_id } => {
|
||||
self.load_prompt(*prompt_id, true, cx);
|
||||
}
|
||||
PromptPickerEvent::ToggledDefault { prompt_id } => {
|
||||
self.toggle_default_for_prompt(*prompt_id, cx);
|
||||
}
|
||||
PromptPickerEvent::Deleted { prompt_id } => {
|
||||
self.delete_prompt(*prompt_id, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_prompt(&mut self, cx: &mut ViewContext<Self>) {
|
||||
let prompt_id = PromptId::new();
|
||||
let save = self.store.save(prompt_id, None, false, "".into());
|
||||
self.picker.update(cx, |picker, cx| picker.refresh(cx));
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
save.await?;
|
||||
this.update(&mut cx, |this, cx| this.load_prompt(prompt_id, true, cx))
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
pub fn save_active_prompt(&mut self, cx: &mut ViewContext<Self>) {
|
||||
if let Some(active_prompt_id) = self.active_prompt_id {
|
||||
let prompt_metadata = self.store.metadata(active_prompt_id).unwrap();
|
||||
let body = self
|
||||
.prompt_editors
|
||||
.get_mut(&active_prompt_id)
|
||||
.unwrap()
|
||||
.update(cx, |editor, cx| editor.snapshot(cx));
|
||||
|
||||
let title = title_from_body(body.buffer_chars_at(0).map(|(c, _)| c));
|
||||
self.store
|
||||
.save(
|
||||
active_prompt_id,
|
||||
title,
|
||||
prompt_metadata.default,
|
||||
body.text(),
|
||||
)
|
||||
.detach_and_log_err(cx);
|
||||
self.picker.update(cx, |picker, cx| picker.refresh(cx));
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn delete_active_prompt(&mut self, cx: &mut ViewContext<Self>) {
|
||||
if let Some(active_prompt_id) = self.active_prompt_id {
|
||||
self.delete_prompt(active_prompt_id, cx);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle_default_for_active_prompt(&mut self, cx: &mut ViewContext<Self>) {
|
||||
if let Some(active_prompt_id) = self.active_prompt_id {
|
||||
self.toggle_default_for_prompt(active_prompt_id, cx);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle_default_for_prompt(&mut self, prompt_id: PromptId, cx: &mut ViewContext<Self>) {
|
||||
if let Some(prompt_metadata) = self.store.metadata(prompt_id) {
|
||||
self.store
|
||||
.save_metadata(prompt_id, prompt_metadata.title, !prompt_metadata.default)
|
||||
.detach_and_log_err(cx);
|
||||
self.picker.update(cx, |picker, cx| picker.refresh(cx));
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load_prompt(&mut self, prompt_id: PromptId, focus: bool, cx: &mut ViewContext<Self>) {
|
||||
if let Some(prompt_editor) = self.prompt_editors.get(&prompt_id) {
|
||||
if focus {
|
||||
prompt_editor.update(cx, |editor, cx| editor.focus(cx));
|
||||
}
|
||||
self.active_prompt_id = Some(prompt_id);
|
||||
} else {
|
||||
let language_registry = self.language_registry.clone();
|
||||
let prompt = self.store.load(prompt_id);
|
||||
self.pending_load = cx.spawn(|this, mut cx| async move {
|
||||
let prompt = prompt.await;
|
||||
let markdown = language_registry.language_for_name("Markdown").await;
|
||||
this.update(&mut cx, |this, cx| match prompt {
|
||||
Ok(prompt) => {
|
||||
let buffer = cx.new_model(|cx| {
|
||||
let mut buffer = Buffer::local(prompt, cx);
|
||||
buffer.set_language(markdown.log_err(), cx);
|
||||
buffer.set_language_registry(language_registry);
|
||||
buffer
|
||||
});
|
||||
let editor = cx.new_view(|cx| {
|
||||
let mut editor = Editor::for_buffer(buffer, None, cx);
|
||||
editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
|
||||
editor.set_show_gutter(false, cx);
|
||||
editor.set_show_wrap_guides(false, cx);
|
||||
editor.set_show_indent_guides(false, cx);
|
||||
if focus {
|
||||
editor.focus(cx);
|
||||
}
|
||||
editor
|
||||
});
|
||||
this.prompt_editors.insert(prompt_id, editor);
|
||||
this.active_prompt_id = Some(prompt_id);
|
||||
cx.notify();
|
||||
}
|
||||
Err(error) => {
|
||||
// TODO: we should show the error in the UI.
|
||||
log::error!("error while loading prompt: {:?}", error);
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub fn delete_prompt(&mut self, prompt_id: PromptId, cx: &mut ViewContext<Self>) {
|
||||
if let Some(metadata) = self.store.metadata(prompt_id) {
|
||||
let confirmation = cx.prompt(
|
||||
PromptLevel::Warning,
|
||||
&format!(
|
||||
"Are you sure you want to delete {}",
|
||||
metadata.title.unwrap_or("Untitled".into())
|
||||
),
|
||||
None,
|
||||
&["Delete", "Cancel"],
|
||||
);
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
if confirmation.await.ok() == Some(0) {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
if this.active_prompt_id == Some(prompt_id) {
|
||||
this.active_prompt_id = None;
|
||||
}
|
||||
this.prompt_editors.remove(&prompt_id);
|
||||
this.store.delete(prompt_id).detach_and_log_err(cx);
|
||||
this.picker.update(cx, |picker, cx| picker.refresh(cx));
|
||||
cx.notify();
|
||||
})?;
|
||||
}
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn render_prompt_list(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
v_flex()
|
||||
.id("prompt-list")
|
||||
.bg(cx.theme().colors().surface_background)
|
||||
.h_full()
|
||||
.w_1_3()
|
||||
.overflow_x_hidden()
|
||||
.child(
|
||||
h_flex()
|
||||
.bg(cx.theme().colors().background)
|
||||
.p(Spacing::Small.rems(cx))
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.h(TitleBar::height(cx))
|
||||
.w_full()
|
||||
.flex_none()
|
||||
.justify_end()
|
||||
.child(
|
||||
IconButton::new("new-prompt", IconName::Plus)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(move |cx| Tooltip::for_action("New Prompt", &NewPrompt, cx))
|
||||
.on_click(|_, cx| {
|
||||
cx.dispatch_action(Box::new(NewPrompt));
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(div().flex_grow().child(self.picker.clone()))
|
||||
}
|
||||
|
||||
fn render_active_prompt(&mut self, cx: &mut ViewContext<PromptLibrary>) -> gpui::Stateful<Div> {
|
||||
div()
|
||||
.w_2_3()
|
||||
.h_full()
|
||||
.id("prompt-editor")
|
||||
.border_l_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.flex_none()
|
||||
.min_w_64()
|
||||
.children(self.active_prompt_id.and_then(|prompt_id| {
|
||||
let prompt_metadata = self.store.metadata(prompt_id)?;
|
||||
let editor = self.prompt_editors[&prompt_id].clone();
|
||||
Some(
|
||||
v_flex()
|
||||
.size_full()
|
||||
.child(
|
||||
h_flex()
|
||||
.h(TitleBar::height(cx))
|
||||
.px(Spacing::Large.rems(cx))
|
||||
.justify_between()
|
||||
.child(
|
||||
Label::new(prompt_metadata.title.unwrap_or("Untitled".into()))
|
||||
.size(LabelSize::Large),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_4()
|
||||
.child(
|
||||
IconButton::new("save-prompt", IconName::Save)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::for_action(
|
||||
"Save Prompt",
|
||||
&SavePrompt,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.on_click(|_, cx| {
|
||||
cx.dispatch_action(Box::new(SavePrompt));
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
IconButton::new(
|
||||
"toggle-default-prompt",
|
||||
if prompt_metadata.default {
|
||||
IconName::StarFilled
|
||||
} else {
|
||||
IconName::Star
|
||||
},
|
||||
)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::for_action(
|
||||
if prompt_metadata.default {
|
||||
"Remove from Default Prompt"
|
||||
} else {
|
||||
"Add to Default Prompt"
|
||||
},
|
||||
&ToggleDefaultPrompt,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.on_click(
|
||||
|_, cx| {
|
||||
cx.dispatch_action(Box::new(
|
||||
ToggleDefaultPrompt,
|
||||
));
|
||||
},
|
||||
),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("delete-prompt", IconName::Trash)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::for_action(
|
||||
"Delete Prompt",
|
||||
&DeletePrompt,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.on_click(|_, cx| {
|
||||
cx.dispatch_action(Box::new(DeletePrompt));
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(div().flex_grow().p(Spacing::Large.rems(cx)).child(editor)),
|
||||
)
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for PromptLibrary {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
h_flex()
|
||||
.id("prompt-manager")
|
||||
.key_context("PromptLibrary")
|
||||
.on_action(cx.listener(|this, &NewPrompt, cx| this.new_prompt(cx)))
|
||||
.on_action(cx.listener(|this, &SavePrompt, cx| this.save_active_prompt(cx)))
|
||||
.on_action(cx.listener(|this, &DeletePrompt, cx| this.delete_active_prompt(cx)))
|
||||
.on_action(cx.listener(|this, &ToggleDefaultPrompt, cx| {
|
||||
this.toggle_default_for_active_prompt(cx)
|
||||
}))
|
||||
.size_full()
|
||||
.overflow_hidden()
|
||||
.child(self.render_prompt_list(cx))
|
||||
.child(self.render_active_prompt(cx))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct PromptMetadata {
|
||||
pub id: PromptId,
|
||||
pub title: Option<SharedString>,
|
||||
pub default: bool,
|
||||
pub saved_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub struct PromptId(Uuid);
|
||||
|
||||
impl PromptId {
|
||||
pub fn new() -> PromptId {
|
||||
PromptId(Uuid::new_v4())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PromptStore {
|
||||
executor: BackgroundExecutor,
|
||||
env: heed::Env,
|
||||
bodies: Database<SerdeBincode<PromptId>, SerdeBincode<String>>,
|
||||
metadata: Database<SerdeBincode<PromptId>, SerdeBincode<PromptMetadata>>,
|
||||
metadata_cache: Mutex<MetadataCache>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct MetadataCache {
|
||||
metadata: Vec<PromptMetadata>,
|
||||
metadata_by_id: HashMap<PromptId, PromptMetadata>,
|
||||
}
|
||||
|
||||
impl MetadataCache {
|
||||
fn from_db(
|
||||
db: Database<SerdeBincode<PromptId>, SerdeBincode<PromptMetadata>>,
|
||||
txn: &RoTxn,
|
||||
) -> Result<Self> {
|
||||
let mut cache = MetadataCache::default();
|
||||
for result in db.iter(txn)? {
|
||||
let (prompt_id, metadata) = result?;
|
||||
cache.metadata.push(metadata.clone());
|
||||
cache.metadata_by_id.insert(prompt_id, metadata);
|
||||
}
|
||||
cache
|
||||
.metadata
|
||||
.sort_unstable_by_key(|metadata| Reverse(metadata.saved_at));
|
||||
Ok(cache)
|
||||
}
|
||||
|
||||
fn insert(&mut self, metadata: PromptMetadata) {
|
||||
self.metadata_by_id.insert(metadata.id, metadata.clone());
|
||||
if let Some(old_metadata) = self.metadata.iter_mut().find(|m| m.id == metadata.id) {
|
||||
*old_metadata = metadata;
|
||||
} else {
|
||||
self.metadata.push(metadata);
|
||||
}
|
||||
self.metadata.sort_by_key(|m| Reverse(m.saved_at));
|
||||
}
|
||||
|
||||
fn remove(&mut self, id: PromptId) {
|
||||
self.metadata.retain(|metadata| metadata.id != id);
|
||||
self.metadata_by_id.remove(&id);
|
||||
}
|
||||
}
|
||||
|
||||
impl PromptStore {
|
||||
pub fn global(cx: &AppContext) -> impl Future<Output = Result<Arc<Self>>> {
|
||||
let store = GlobalPromptStore::global(cx).0.clone();
|
||||
async move { store.await.map_err(|err| anyhow!(err)) }
|
||||
}
|
||||
|
||||
pub fn new(db_path: PathBuf, executor: BackgroundExecutor) -> Task<Result<Self>> {
|
||||
executor.spawn({
|
||||
let executor = executor.clone();
|
||||
async move {
|
||||
std::fs::create_dir_all(&db_path)?;
|
||||
|
||||
let db_env = unsafe {
|
||||
heed::EnvOpenOptions::new()
|
||||
.map_size(1024 * 1024 * 1024) // 1GB
|
||||
.max_dbs(2) // bodies and metadata
|
||||
.open(db_path)?
|
||||
};
|
||||
|
||||
let mut txn = db_env.write_txn()?;
|
||||
let bodies = db_env.create_database(&mut txn, Some("bodies"))?;
|
||||
let metadata = db_env.create_database(&mut txn, Some("metadata"))?;
|
||||
let metadata_cache = MetadataCache::from_db(metadata, &txn)?;
|
||||
txn.commit()?;
|
||||
|
||||
Ok(PromptStore {
|
||||
executor,
|
||||
env: db_env,
|
||||
bodies,
|
||||
metadata,
|
||||
metadata_cache: Mutex::new(metadata_cache),
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn load(&self, id: PromptId) -> Task<Result<String>> {
|
||||
let env = self.env.clone();
|
||||
let bodies = self.bodies;
|
||||
self.executor.spawn(async move {
|
||||
let txn = env.read_txn()?;
|
||||
Ok(bodies
|
||||
.get(&txn, &id)?
|
||||
.ok_or_else(|| anyhow!("prompt not found"))?)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn delete(&self, id: PromptId) -> Task<Result<()>> {
|
||||
self.metadata_cache.lock().remove(id);
|
||||
|
||||
let db_connection = self.env.clone();
|
||||
let bodies = self.bodies;
|
||||
let metadata = self.metadata;
|
||||
|
||||
self.executor.spawn(async move {
|
||||
let mut txn = db_connection.write_txn()?;
|
||||
|
||||
metadata.delete(&mut txn, &id)?;
|
||||
bodies.delete(&mut txn, &id)?;
|
||||
|
||||
txn.commit()?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn metadata(&self, id: PromptId) -> Option<PromptMetadata> {
|
||||
self.metadata_cache.lock().metadata_by_id.get(&id).cloned()
|
||||
}
|
||||
|
||||
pub fn id_for_title(&self, title: &str) -> Option<PromptId> {
|
||||
let metadata_cache = self.metadata_cache.lock();
|
||||
let metadata = metadata_cache
|
||||
.metadata
|
||||
.iter()
|
||||
.find(|metadata| metadata.title.as_ref().map(|title| &***title) == Some(title))?;
|
||||
Some(metadata.id)
|
||||
}
|
||||
|
||||
pub fn search(&self, query: String) -> Task<Vec<PromptMetadata>> {
|
||||
let cached_metadata = self.metadata_cache.lock().metadata.clone();
|
||||
let executor = self.executor.clone();
|
||||
self.executor.spawn(async move {
|
||||
if query.is_empty() {
|
||||
cached_metadata
|
||||
} else {
|
||||
let candidates = cached_metadata
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(ix, metadata)| {
|
||||
Some(StringMatchCandidate::new(
|
||||
ix,
|
||||
metadata.title.as_ref()?.to_string(),
|
||||
))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let matches = fuzzy::match_strings(
|
||||
&candidates,
|
||||
&query,
|
||||
false,
|
||||
100,
|
||||
&AtomicBool::default(),
|
||||
executor,
|
||||
)
|
||||
.await;
|
||||
matches
|
||||
.into_iter()
|
||||
.map(|mat| cached_metadata[mat.candidate_id].clone())
|
||||
.collect()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn save(
|
||||
&self,
|
||||
id: PromptId,
|
||||
title: Option<SharedString>,
|
||||
default: bool,
|
||||
body: String,
|
||||
) -> Task<Result<()>> {
|
||||
let prompt_metadata = PromptMetadata {
|
||||
id,
|
||||
title,
|
||||
default,
|
||||
saved_at: Utc::now(),
|
||||
};
|
||||
self.metadata_cache.lock().insert(prompt_metadata.clone());
|
||||
|
||||
let db_connection = self.env.clone();
|
||||
let bodies = self.bodies;
|
||||
let metadata = self.metadata;
|
||||
|
||||
self.executor.spawn(async move {
|
||||
let mut txn = db_connection.write_txn()?;
|
||||
|
||||
metadata.put(&mut txn, &id, &prompt_metadata)?;
|
||||
bodies.put(&mut txn, &id, &body)?;
|
||||
|
||||
txn.commit()?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn save_metadata(
|
||||
&self,
|
||||
id: PromptId,
|
||||
title: Option<SharedString>,
|
||||
default: bool,
|
||||
) -> Task<Result<()>> {
|
||||
let prompt_metadata = PromptMetadata {
|
||||
id,
|
||||
title,
|
||||
default,
|
||||
saved_at: Utc::now(),
|
||||
};
|
||||
self.metadata_cache.lock().insert(prompt_metadata.clone());
|
||||
|
||||
let db_connection = self.env.clone();
|
||||
let metadata = self.metadata;
|
||||
|
||||
self.executor.spawn(async move {
|
||||
let mut txn = db_connection.write_txn()?;
|
||||
metadata.put(&mut txn, &id, &prompt_metadata)?;
|
||||
txn.commit()?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn most_recently_saved(&self) -> Option<PromptId> {
|
||||
self.metadata_cache
|
||||
.lock()
|
||||
.metadata
|
||||
.first()
|
||||
.map(|metadata| metadata.id)
|
||||
}
|
||||
}
|
||||
|
||||
/// Wraps a shared future to a prompt store so it can be assigned as a context global.
|
||||
pub struct GlobalPromptStore(
|
||||
Shared<BoxFuture<'static, Result<Arc<PromptStore>, Arc<anyhow::Error>>>>,
|
||||
);
|
||||
|
||||
impl Global for GlobalPromptStore {}
|
||||
|
||||
fn title_from_body<'a>(body: impl IntoIterator<Item = char>) -> Option<SharedString> {
|
||||
let mut chars = body.into_iter().take_while(|c| *c != '\n').peekable();
|
||||
|
||||
let mut level = 0;
|
||||
while let Some('#') = chars.peek() {
|
||||
level += 1;
|
||||
chars.next();
|
||||
}
|
||||
|
||||
if level > 0 {
|
||||
Some(chars.collect::<String>().trim().to_string().into())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -73,7 +73,7 @@ fn line_similarity(line1: &str, line2: &str) -> f64 {
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use gpui::{AppContext, Context as _};
|
||||
use gpui::{AppContext, StaticContext as _};
|
||||
use language::Buffer;
|
||||
use unindent::Unindent as _;
|
||||
use util::test::marked_text_ranges;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -17,10 +17,17 @@ impl RustdocSlashCommand {
|
||||
async fn build_message(
|
||||
http_client: Arc<HttpClientWithUrl>,
|
||||
crate_name: String,
|
||||
module_path: Vec<String>,
|
||||
) -> Result<String> {
|
||||
let version = "latest";
|
||||
let path = format!(
|
||||
"{crate_name}/{version}/{crate_name}/{module_path}",
|
||||
module_path = module_path.join("/")
|
||||
);
|
||||
|
||||
let mut response = http_client
|
||||
.get(
|
||||
&format!("https://docs.rs/{crate_name}"),
|
||||
&format!("https://docs.rs/{path}"),
|
||||
AsyncBody::default(),
|
||||
true,
|
||||
)
|
||||
@@ -51,11 +58,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 {
|
||||
@@ -87,14 +94,28 @@ impl SlashCommand for RustdocSlashCommand {
|
||||
};
|
||||
|
||||
let http_client = workspace.read(cx).client().http_client();
|
||||
let crate_name = argument.to_string();
|
||||
let mut path_components = argument.split("::");
|
||||
let crate_name = match path_components
|
||||
.next()
|
||||
.ok_or_else(|| anyhow!("missing crate name"))
|
||||
{
|
||||
Ok(crate_name) => crate_name.to_string(),
|
||||
Err(err) => return Task::ready(Err(err)),
|
||||
};
|
||||
let module_path = path_components.map(ToString::to_string).collect::<Vec<_>>();
|
||||
|
||||
let text = cx.background_executor().spawn({
|
||||
let crate_name = crate_name.clone();
|
||||
async move { Self::build_message(http_client, crate_name).await }
|
||||
let module_path = module_path.clone();
|
||||
async move { Self::build_message(http_client, crate_name, module_path).await }
|
||||
});
|
||||
|
||||
let crate_name = SharedString::from(crate_name);
|
||||
let module_path = if module_path.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(SharedString::from(module_path.join("::")))
|
||||
};
|
||||
cx.foreground_executor().spawn(async move {
|
||||
let text = text.await?;
|
||||
let range = 0..text.len();
|
||||
@@ -107,6 +128,7 @@ impl SlashCommand for RustdocSlashCommand {
|
||||
id,
|
||||
unfold,
|
||||
crate_name: crate_name.clone(),
|
||||
module_path: module_path.clone(),
|
||||
}
|
||||
.into_any_element()
|
||||
}),
|
||||
@@ -121,17 +143,23 @@ struct RustdocPlaceholder {
|
||||
pub id: ElementId,
|
||||
pub unfold: Arc<dyn Fn(&mut WindowContext)>,
|
||||
pub crate_name: SharedString,
|
||||
pub module_path: Option<SharedString>,
|
||||
}
|
||||
|
||||
impl RenderOnce for RustdocPlaceholder {
|
||||
fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
|
||||
let unfold = self.unfold;
|
||||
|
||||
let crate_path = self
|
||||
.module_path
|
||||
.map(|module_path| format!("{crate_name}::{module_path}", crate_name = self.crate_name))
|
||||
.unwrap_or(self.crate_name.to_string());
|
||||
|
||||
ButtonLike::new(self.id)
|
||||
.style(ButtonStyle::Filled)
|
||||
.layer(ElevationIndex::ElevatedSurface)
|
||||
.child(Icon::new(IconName::FileRust))
|
||||
.child(Label::new(format!("rustdoc: {}", self.crate_name)))
|
||||
.child(Label::new(format!("rustdoc: {crate_path}")))
|
||||
.on_click(move |_, cx| unfold(cx))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -6,8 +6,8 @@ use db::kvp::KEY_VALUE_STORE;
|
||||
use db::RELEASE_CHANNEL;
|
||||
use editor::{Editor, MultiBuffer};
|
||||
use gpui::{
|
||||
actions, AppContext, AsyncAppContext, Context as _, Global, Model, ModelContext,
|
||||
SemanticVersion, SharedString, Task, View, ViewContext, VisualContext, WindowContext,
|
||||
actions, AppContext, AsyncAppContext, Global, Model, ModelContext, SemanticVersion,
|
||||
SharedString, StaticContext as _, Task, View, ViewContext, VisualContext, WindowContext,
|
||||
};
|
||||
use isahc::AsyncBody;
|
||||
|
||||
|
||||
@@ -9,8 +9,8 @@ use client::{proto, ChannelId, Client, TypedEnvelope, User, UserStore, ZED_ALWAY
|
||||
use collections::HashSet;
|
||||
use futures::{channel::oneshot, future::Shared, Future, FutureExt};
|
||||
use gpui::{
|
||||
AppContext, AsyncAppContext, Context, EventEmitter, Global, Model, ModelContext, Subscription,
|
||||
Task, WeakModel,
|
||||
AppContext, AsyncAppContext, EventEmitter, Global, Model, ModelContext, StaticContext,
|
||||
Subscription, Task, WeakModel,
|
||||
};
|
||||
use postage::watch;
|
||||
use project::Project;
|
||||
|
||||
@@ -12,7 +12,7 @@ use collections::{BTreeMap, HashMap, HashSet};
|
||||
use fs::Fs;
|
||||
use futures::{FutureExt, StreamExt};
|
||||
use gpui::{
|
||||
AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Task, WeakModel,
|
||||
AppContext, AsyncAppContext, StaticContext, EventEmitter, Model, ModelContext, Task, WeakModel,
|
||||
};
|
||||
use language::LanguageRegistry;
|
||||
use live_kit_client::{LocalAudioTrack, LocalTrackPublication, LocalVideoTrack, RoomUpdate};
|
||||
|
||||
@@ -2,7 +2,7 @@ use crate::{Channel, ChannelStore};
|
||||
use anyhow::Result;
|
||||
use client::{ChannelId, Client, Collaborator, UserStore, ZED_ALWAYS_ACTIVE};
|
||||
use collections::HashMap;
|
||||
use gpui::{AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Task};
|
||||
use gpui::{AppContext, AsyncAppContext, EventEmitter, Model, ModelContext, StaticContext, Task};
|
||||
use language::proto::serialize_version;
|
||||
use rpc::{
|
||||
proto::{self, PeerId},
|
||||
|
||||
@@ -8,7 +8,7 @@ use client::{
|
||||
use collections::HashSet;
|
||||
use futures::lock::Mutex;
|
||||
use gpui::{
|
||||
AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Task, WeakModel,
|
||||
AppContext, AsyncAppContext, EventEmitter, Model, ModelContext, StaticContext, Task, WeakModel,
|
||||
};
|
||||
use rand::prelude::*;
|
||||
use std::{
|
||||
|
||||
@@ -7,8 +7,8 @@ use client::{ChannelId, Client, ClientSettings, ProjectId, Subscription, User, U
|
||||
use collections::{hash_map, HashMap, HashSet};
|
||||
use futures::{channel::mpsc, future::Shared, Future, FutureExt, StreamExt};
|
||||
use gpui::{
|
||||
AppContext, AsyncAppContext, Context, EventEmitter, Global, Model, ModelContext, SharedString,
|
||||
Task, WeakModel,
|
||||
AppContext, AsyncAppContext, EventEmitter, Global, Model, ModelContext, SharedString,
|
||||
StaticContext, Task, WeakModel,
|
||||
};
|
||||
use language::Capability;
|
||||
use rpc::{
|
||||
@@ -62,6 +62,7 @@ pub struct ChannelStore {
|
||||
opened_buffers: HashMap<ChannelId, OpenedModelHandle<ChannelBuffer>>,
|
||||
opened_chats: HashMap<ChannelId, OpenedModelHandle<ChannelChat>>,
|
||||
client: Arc<Client>,
|
||||
did_subscribe: bool,
|
||||
user_store: Model<UserStore>,
|
||||
_rpc_subscriptions: [Subscription; 2],
|
||||
_watch_connection_status: Task<Option<()>>,
|
||||
@@ -243,6 +244,20 @@ impl ChannelStore {
|
||||
.log_err();
|
||||
}),
|
||||
channel_states: Default::default(),
|
||||
did_subscribe: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn initialize(&mut self) {
|
||||
if !self.did_subscribe {
|
||||
if self
|
||||
.client
|
||||
.send(proto::SubscribeToChannels {})
|
||||
.log_err()
|
||||
.is_some()
|
||||
{
|
||||
self.did_subscribe = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1035,7 +1050,7 @@ impl ChannelStore {
|
||||
|
||||
fn handle_disconnect(&mut self, wait_for_reconnect: bool, cx: &mut ModelContext<Self>) {
|
||||
cx.notify();
|
||||
|
||||
self.did_subscribe = false;
|
||||
self.disconnect_channel_buffers_task.get_or_insert_with(|| {
|
||||
cx.spawn(move |this, mut cx| async move {
|
||||
if wait_for_reconnect {
|
||||
|
||||
@@ -3,7 +3,7 @@ use crate::channel_chat::ChannelChatEvent;
|
||||
use super::*;
|
||||
use client::{test::FakeServer, Client, UserStore};
|
||||
use clock::FakeSystemClock;
|
||||
use gpui::{AppContext, Context, Model, TestAppContext};
|
||||
use gpui::{AppContext, Model, StaticContext, TestAppContext};
|
||||
use http::FakeHttpClient;
|
||||
use rpc::proto::{self};
|
||||
use settings::SettingsStore;
|
||||
|
||||
@@ -1701,7 +1701,7 @@ mod tests {
|
||||
use crate::test::FakeServer;
|
||||
|
||||
use clock::FakeSystemClock;
|
||||
use gpui::{BackgroundExecutor, Context, TestAppContext};
|
||||
use gpui::{BackgroundExecutor, StaticContext, TestAppContext};
|
||||
use http::FakeHttpClient;
|
||||
use parking_lot::Mutex;
|
||||
use settings::SettingsStore;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::{Client, Connection, Credentials, EstablishConnectionError, UserStore};
|
||||
use anyhow::{anyhow, Result};
|
||||
use futures::{stream::BoxStream, StreamExt};
|
||||
use gpui::{BackgroundExecutor, Context, Model, TestAppContext};
|
||||
use gpui::{BackgroundExecutor, StaticContext, Model, TestAppContext};
|
||||
use parking_lot::Mutex;
|
||||
use rpc::{
|
||||
proto::{self, GetPrivateUserInfo, GetPrivateUserInfoResponse},
|
||||
|
||||
@@ -654,6 +654,7 @@ pub struct ChannelsForUser {
|
||||
pub channel_memberships: Vec<channel_member::Model>,
|
||||
pub channel_participants: HashMap<ChannelId, Vec<UserId>>,
|
||||
pub hosted_projects: Vec<proto::HostedProject>,
|
||||
pub invited_channels: Vec<Channel>,
|
||||
|
||||
pub observed_buffer_versions: Vec<proto::ChannelBufferVersion>,
|
||||
pub observed_channel_messages: Vec<proto::ChannelMessageId>,
|
||||
|
||||
@@ -416,7 +416,9 @@ impl Database {
|
||||
user_id: UserId,
|
||||
tx: &DatabaseTransaction,
|
||||
) -> Result<MembershipUpdated> {
|
||||
let new_channels = self.get_user_channels(user_id, Some(channel), tx).await?;
|
||||
let new_channels = self
|
||||
.get_user_channels(user_id, Some(channel), false, tx)
|
||||
.await?;
|
||||
let removed_channels = self
|
||||
.get_channel_descendants_excluding_self([channel], tx)
|
||||
.await?
|
||||
@@ -481,44 +483,10 @@ impl Database {
|
||||
.await
|
||||
}
|
||||
|
||||
/// Returns all channel invites for the user with the given ID.
|
||||
pub async fn get_channel_invites_for_user(&self, user_id: UserId) -> Result<Vec<Channel>> {
|
||||
self.transaction(|tx| async move {
|
||||
let mut role_for_channel: HashMap<ChannelId, ChannelRole> = HashMap::default();
|
||||
|
||||
let channel_invites = channel_member::Entity::find()
|
||||
.filter(
|
||||
channel_member::Column::UserId
|
||||
.eq(user_id)
|
||||
.and(channel_member::Column::Accepted.eq(false)),
|
||||
)
|
||||
.all(&*tx)
|
||||
.await?;
|
||||
|
||||
for invite in channel_invites {
|
||||
role_for_channel.insert(invite.channel_id, invite.role);
|
||||
}
|
||||
|
||||
let channels = channel::Entity::find()
|
||||
.filter(channel::Column::Id.is_in(role_for_channel.keys().copied()))
|
||||
.all(&*tx)
|
||||
.await?;
|
||||
|
||||
let channels = channels.into_iter().map(Channel::from_model).collect();
|
||||
|
||||
Ok(channels)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
/// Returns all channels for the user with the given ID.
|
||||
pub async fn get_channels_for_user(&self, user_id: UserId) -> Result<ChannelsForUser> {
|
||||
self.transaction(|tx| async move {
|
||||
let tx = tx;
|
||||
|
||||
self.get_user_channels(user_id, None, &tx).await
|
||||
})
|
||||
.await
|
||||
self.transaction(|tx| async move { self.get_user_channels(user_id, None, true, &tx).await })
|
||||
.await
|
||||
}
|
||||
|
||||
/// Returns all channels for the user with the given ID that are descendants
|
||||
@@ -527,25 +495,37 @@ impl Database {
|
||||
&self,
|
||||
user_id: UserId,
|
||||
ancestor_channel: Option<&channel::Model>,
|
||||
include_invites: bool,
|
||||
tx: &DatabaseTransaction,
|
||||
) -> Result<ChannelsForUser> {
|
||||
let mut filter = channel_member::Column::UserId
|
||||
.eq(user_id)
|
||||
.and(channel_member::Column::Accepted.eq(true));
|
||||
|
||||
let mut filter = channel_member::Column::UserId.eq(user_id);
|
||||
if !include_invites {
|
||||
filter = filter.and(channel_member::Column::Accepted.eq(true))
|
||||
}
|
||||
if let Some(ancestor) = ancestor_channel {
|
||||
filter = filter.and(channel_member::Column::ChannelId.eq(ancestor.root_id()));
|
||||
}
|
||||
|
||||
let channel_memberships = channel_member::Entity::find()
|
||||
let mut channels = Vec::<channel::Model>::new();
|
||||
let mut invited_channels = Vec::<Channel>::new();
|
||||
let mut channel_memberships = Vec::<channel_member::Model>::new();
|
||||
let mut rows = channel_member::Entity::find()
|
||||
.filter(filter)
|
||||
.all(tx)
|
||||
.await?;
|
||||
|
||||
let channels = channel::Entity::find()
|
||||
.filter(channel::Column::Id.is_in(channel_memberships.iter().map(|m| m.channel_id)))
|
||||
.all(tx)
|
||||
.inner_join(channel::Entity)
|
||||
.select_also(channel::Entity)
|
||||
.stream(tx)
|
||||
.await?;
|
||||
while let Some(row) = rows.next().await {
|
||||
if let (membership, Some(channel)) = row? {
|
||||
if membership.accepted {
|
||||
channel_memberships.push(membership);
|
||||
channels.push(channel);
|
||||
} else {
|
||||
invited_channels.push(Channel::from_model(channel));
|
||||
}
|
||||
}
|
||||
}
|
||||
drop(rows);
|
||||
|
||||
let mut descendants = self
|
||||
.get_channel_descendants_excluding_self(channels.iter(), tx)
|
||||
@@ -643,6 +623,7 @@ impl Database {
|
||||
Ok(ChannelsForUser {
|
||||
channel_memberships,
|
||||
channels,
|
||||
invited_channels,
|
||||
hosted_projects,
|
||||
channel_participants,
|
||||
latest_buffer_versions,
|
||||
|
||||
@@ -176,23 +176,23 @@ async fn test_channel_invites(db: &Arc<Database>) {
|
||||
.unwrap();
|
||||
|
||||
let user_2_invites = db
|
||||
.get_channel_invites_for_user(user_2) // -> [channel_1_1, channel_1_2]
|
||||
.get_channels_for_user(user_2)
|
||||
.await
|
||||
.unwrap()
|
||||
.invited_channels
|
||||
.into_iter()
|
||||
.map(|channel| channel.id)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(user_2_invites, &[channel_1_1, channel_1_2]);
|
||||
|
||||
let user_3_invites = db
|
||||
.get_channel_invites_for_user(user_3) // -> [channel_1_1]
|
||||
.get_channels_for_user(user_3)
|
||||
.await
|
||||
.unwrap()
|
||||
.invited_channels
|
||||
.into_iter()
|
||||
.map(|channel| channel.id)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(user_3_invites, &[channel_1_1]);
|
||||
|
||||
let (mut members, _) = db
|
||||
|
||||
@@ -557,6 +557,7 @@ impl Server {
|
||||
.add_request_handler(user_handler(request_contact))
|
||||
.add_request_handler(user_handler(remove_contact))
|
||||
.add_request_handler(user_handler(respond_to_contact_request))
|
||||
.add_message_handler(subscribe_to_channels)
|
||||
.add_request_handler(user_handler(create_channel))
|
||||
.add_request_handler(user_handler(delete_channel))
|
||||
.add_request_handler(user_handler(invite_channel_member))
|
||||
@@ -1105,34 +1106,25 @@ impl Server {
|
||||
.await?;
|
||||
}
|
||||
|
||||
let (contacts, channels_for_user, channel_invites, dev_server_projects) =
|
||||
future::try_join4(
|
||||
self.app_state.db.get_contacts(user.id),
|
||||
self.app_state.db.get_channels_for_user(user.id),
|
||||
self.app_state.db.get_channel_invites_for_user(user.id),
|
||||
self.app_state.db.dev_server_projects_update(user.id),
|
||||
)
|
||||
.await?;
|
||||
let (contacts, dev_server_projects) = future::try_join(
|
||||
self.app_state.db.get_contacts(user.id),
|
||||
self.app_state.db.dev_server_projects_update(user.id),
|
||||
)
|
||||
.await?;
|
||||
|
||||
{
|
||||
let mut pool = self.connection_pool.lock();
|
||||
pool.add_connection(connection_id, user.id, user.admin, zed_version);
|
||||
for membership in &channels_for_user.channel_memberships {
|
||||
pool.subscribe_to_channel(user.id, membership.channel_id, membership.role)
|
||||
}
|
||||
self.peer.send(
|
||||
connection_id,
|
||||
build_initial_contacts_update(contacts, &pool),
|
||||
)?;
|
||||
self.peer.send(
|
||||
connection_id,
|
||||
build_update_user_channels(&channels_for_user),
|
||||
)?;
|
||||
self.peer.send(
|
||||
connection_id,
|
||||
build_channels_update(channels_for_user, channel_invites),
|
||||
)?;
|
||||
}
|
||||
|
||||
if should_auto_subscribe_to_channels(zed_version) {
|
||||
subscribe_user_to_channels(user.id, session).await?;
|
||||
}
|
||||
|
||||
send_dev_server_projects_update(user.id, dev_server_projects, session).await;
|
||||
|
||||
if let Some(incoming_call) =
|
||||
@@ -3399,6 +3391,36 @@ async fn remove_contact(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn should_auto_subscribe_to_channels(version: ZedVersion) -> bool {
|
||||
version.0.minor() < 139
|
||||
}
|
||||
|
||||
async fn subscribe_to_channels(_: proto::SubscribeToChannels, session: Session) -> Result<()> {
|
||||
subscribe_user_to_channels(
|
||||
session.user_id().ok_or_else(|| anyhow!("must be a user"))?,
|
||||
&session,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn subscribe_user_to_channels(user_id: UserId, session: &Session) -> Result<(), Error> {
|
||||
let channels_for_user = session.db().await.get_channels_for_user(user_id).await?;
|
||||
let mut pool = session.connection_pool().await;
|
||||
for membership in &channels_for_user.channel_memberships {
|
||||
pool.subscribe_to_channel(user_id, membership.channel_id, membership.role)
|
||||
}
|
||||
session.peer.send(
|
||||
session.connection_id,
|
||||
build_update_user_channels(&channels_for_user),
|
||||
)?;
|
||||
session.peer.send(
|
||||
session.connection_id,
|
||||
build_channels_update(channels_for_user),
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Creates a new channel.
|
||||
async fn create_channel(
|
||||
request: proto::CreateChannel,
|
||||
@@ -5034,7 +5056,7 @@ fn notify_membership_updated(
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let mut update = build_channels_update(result.new_channels, vec![]);
|
||||
let mut update = build_channels_update(result.new_channels);
|
||||
update.delete_channels = result
|
||||
.removed_channels
|
||||
.into_iter()
|
||||
@@ -5064,10 +5086,7 @@ fn build_update_user_channels(channels: &ChannelsForUser) -> proto::UpdateUserCh
|
||||
}
|
||||
}
|
||||
|
||||
fn build_channels_update(
|
||||
channels: ChannelsForUser,
|
||||
channel_invites: Vec<db::Channel>,
|
||||
) -> proto::UpdateChannels {
|
||||
fn build_channels_update(channels: ChannelsForUser) -> proto::UpdateChannels {
|
||||
let mut update = proto::UpdateChannels::default();
|
||||
|
||||
for channel in channels.channels {
|
||||
@@ -5086,7 +5105,7 @@ fn build_channels_update(
|
||||
});
|
||||
}
|
||||
|
||||
for channel in channel_invites {
|
||||
for channel in channels.invited_channels {
|
||||
update.channel_invitations.push(channel.to_proto());
|
||||
}
|
||||
|
||||
|
||||
@@ -572,8 +572,7 @@ async fn test_save_as_remote(cx1: &mut gpui::TestAppContext, cx2: &mut gpui::Tes
|
||||
|
||||
let title = remote_workspace
|
||||
.update(&mut cx, |ws, cx| {
|
||||
let active_item = ws.active_item(cx).unwrap();
|
||||
active_item.tab_description(0, &cx).unwrap()
|
||||
ws.active_item(cx).unwrap().tab_description(0, &cx).unwrap()
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ use collab_ui::{
|
||||
};
|
||||
use editor::{Editor, ExcerptRange, MultiBuffer};
|
||||
use gpui::{
|
||||
point, BackgroundExecutor, BorrowAppContext, Context, Entity, SharedString, TestAppContext,
|
||||
point, BackgroundExecutor, BorrowAppContext, StaticContext, Entity, SharedString, TestAppContext,
|
||||
View, VisualContext, VisualTestContext,
|
||||
};
|
||||
use language::Capability;
|
||||
|
||||
@@ -18,7 +18,7 @@ use collections::{HashMap, HashSet};
|
||||
use fs::FakeFs;
|
||||
use futures::{channel::oneshot, StreamExt as _};
|
||||
use git::GitHostingProviderRegistry;
|
||||
use gpui::{BackgroundExecutor, Context, Model, Task, TestAppContext, View, VisualTestContext};
|
||||
use gpui::{BackgroundExecutor, StaticContext, Model, Task, TestAppContext, View, VisualTestContext};
|
||||
use http::FakeHttpClient;
|
||||
use language::LanguageRegistry;
|
||||
use node_runtime::FakeNodeRuntime;
|
||||
@@ -277,7 +277,11 @@ impl TestServer {
|
||||
node_runtime: FakeNodeRuntime::new(),
|
||||
});
|
||||
|
||||
let os_keymap = "keymaps/default-macos.json";
|
||||
let os_keymap = if cfg!(target_os = "linux") {
|
||||
"keymaps/default-linux.json"
|
||||
} else {
|
||||
"keymaps/default-macos.json"
|
||||
};
|
||||
|
||||
cx.update(|cx| {
|
||||
theme::init(theme::LoadThemes::JustBase, cx);
|
||||
|
||||
@@ -2161,6 +2161,9 @@ impl CollabPanel {
|
||||
}
|
||||
|
||||
fn render_signed_in(&mut self, cx: &mut ViewContext<Self>) -> Div {
|
||||
self.channel_store.update(cx, |channel_store, _| {
|
||||
channel_store.initialize();
|
||||
});
|
||||
v_flex()
|
||||
.size_full()
|
||||
.child(list(self.list_state.clone()).size_full())
|
||||
|
||||
@@ -9,7 +9,7 @@ use collections::{HashMap, HashSet};
|
||||
use command_palette_hooks::CommandPaletteFilter;
|
||||
use futures::{channel::oneshot, future::Shared, Future, FutureExt, TryFutureExt};
|
||||
use gpui::{
|
||||
actions, AppContext, AsyncAppContext, Context, Entity, EntityId, EventEmitter, Global, Model,
|
||||
actions, AppContext, AsyncAppContext, StaticContext, Entity, EntityId, EventEmitter, Global, Model,
|
||||
ModelContext, Task, WeakModel,
|
||||
};
|
||||
use http::github::latest_github_release;
|
||||
|
||||
@@ -292,7 +292,7 @@ mod tests {
|
||||
};
|
||||
use fs::FakeFs;
|
||||
use futures::StreamExt;
|
||||
use gpui::{BackgroundExecutor, Context, TestAppContext, UpdateGlobal};
|
||||
use gpui::{BackgroundExecutor, StaticContext, TestAppContext, UpdateGlobal};
|
||||
use indoc::indoc;
|
||||
use language::{
|
||||
language_settings::{AllLanguageSettings, AllLanguageSettingsContent},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use anyhow::Result;
|
||||
use gpui::{AppContext, AsyncAppContext, Context, Global, Model, ModelContext, SharedString, Task};
|
||||
use gpui::{AppContext, AsyncAppContext, StaticContext, Global, Model, ModelContext, SharedString, Task};
|
||||
use rpc::{
|
||||
proto::{self, DevServerStatus},
|
||||
TypedEnvelope,
|
||||
|
||||
@@ -19,9 +19,9 @@ use futures::{
|
||||
StreamExt as _,
|
||||
};
|
||||
use gpui::{
|
||||
actions, div, svg, AnyElement, AnyView, AppContext, Context, EventEmitter, FocusHandle,
|
||||
FocusableView, HighlightStyle, InteractiveElement, IntoElement, Model, ParentElement, Render,
|
||||
SharedString, Styled, StyledText, Subscription, Task, View, ViewContext, VisualContext,
|
||||
actions, div, svg, AnyElement, AnyView, AppContext, EventEmitter, FocusHandle, FocusableView,
|
||||
HighlightStyle, InteractiveElement, IntoElement, Model, ParentElement, Render, SharedString,
|
||||
StaticContext, Styled, StyledText, Subscription, Task, View, ViewContext, VisualContext,
|
||||
WeakView, WindowContext,
|
||||
};
|
||||
use language::{
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -1054,7 +1054,7 @@ pub mod tests {
|
||||
movement,
|
||||
test::{editor_test_context::EditorTestContext, marked_display_snapshot},
|
||||
};
|
||||
use gpui::{div, font, observe, px, AppContext, BorrowAppContext, Context, Element, Hsla};
|
||||
use gpui::{div, font, observe, px, AppContext, BorrowAppContext, StaticContext, Element, Hsla};
|
||||
use language::{
|
||||
language_settings::{AllLanguageSettings, AllLanguageSettingsContent},
|
||||
Buffer, Language, LanguageConfig, LanguageMatcher, SelectionGoal,
|
||||
|
||||
@@ -3,7 +3,7 @@ use super::{
|
||||
tab_map::{self, TabEdit, TabPoint, TabSnapshot},
|
||||
Highlights,
|
||||
};
|
||||
use gpui::{AppContext, Context, Font, LineWrapper, Model, ModelContext, Pixels, Task};
|
||||
use gpui::{AppContext, Font, LineWrapper, Model, ModelContext, Pixels, StaticContext, Task};
|
||||
use language::{Chunk, Point};
|
||||
use lazy_static::lazy_static;
|
||||
use multi_buffer::MultiBufferSnapshot;
|
||||
|
||||
@@ -39,6 +39,7 @@ pub mod tasks;
|
||||
|
||||
#[cfg(test)]
|
||||
mod editor_tests;
|
||||
pub mod sketch;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub mod test;
|
||||
use ::git::diff::{DiffHunk, DiffHunkStatus};
|
||||
@@ -67,11 +68,11 @@ use git::diff_hunk_to_display;
|
||||
use gpui::{
|
||||
div, impl_actions, point, prelude::*, px, relative, size, uniform_list, Action, AnyElement,
|
||||
AppContext, AsyncWindowContext, AvailableSpace, BackgroundExecutor, Bounds, ClipboardItem,
|
||||
Context, DispatchPhase, ElementId, EventEmitter, FocusHandle, FocusableView, FontId, FontStyle,
|
||||
DispatchPhase, ElementId, EventEmitter, FocusHandle, FocusableView, FontId, FontStyle,
|
||||
FontWeight, HighlightStyle, Hsla, InteractiveText, KeyContext, Model, MouseButton, PaintQuad,
|
||||
ParentElement, Pixels, Render, SharedString, Size, StrikethroughStyle, Styled, StyledText,
|
||||
Subscription, Task, TextStyle, UnderlineStyle, UniformListScrollHandle, View, ViewContext,
|
||||
ViewInputHandler, VisualContext, WeakView, WhiteSpace, WindowContext,
|
||||
ParentElement, Pixels, Render, SharedString, Size, StaticContext, StrikethroughStyle, Styled,
|
||||
StyledText, Subscription, Task, TextStyle, UnderlineStyle, UniformListScrollHandle, View,
|
||||
ViewContext, ViewInputHandler, VisualContext, WeakView, WhiteSpace, WindowContext,
|
||||
};
|
||||
use highlight_matching_bracket::refresh_matching_bracket_highlights;
|
||||
use hover_popover::{hide_hover, HoverState};
|
||||
@@ -3761,7 +3762,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 +9958,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 +9993,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 +10014,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),
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
@@ -5569,7 +5569,7 @@ mod tests {
|
||||
use language::language_settings;
|
||||
use log::info;
|
||||
use std::num::NonZeroU32;
|
||||
use ui::Context;
|
||||
use ui::StaticContext;
|
||||
use util::test::sample_text;
|
||||
|
||||
#[gpui::test]
|
||||
|
||||
@@ -107,7 +107,7 @@ pub fn diff_hunk_to_display(
|
||||
mod tests {
|
||||
use crate::Point;
|
||||
use crate::{editor_tests::init_test, hunk_status};
|
||||
use gpui::{Context, TestAppContext};
|
||||
use gpui::{StaticContext, TestAppContext};
|
||||
use language::Capability::ReadWrite;
|
||||
use multi_buffer::{ExcerptRange, MultiBuffer, MultiBufferRow};
|
||||
use project::{FakeFs, Project};
|
||||
|
||||
@@ -519,7 +519,7 @@ async fn parse_markdown(text: &str, language_registry: &Arc<LanguageRegistry>) -
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use gpui::Context;
|
||||
use gpui::StaticContext;
|
||||
use language::{Point, Rope};
|
||||
use project::FakeFs;
|
||||
use rand::prelude::*;
|
||||
|
||||
@@ -13,7 +13,8 @@ use multi_buffer::{
|
||||
use settings::{Settings, SettingsStore};
|
||||
use text::{BufferId, Point};
|
||||
use ui::{
|
||||
div, ActiveTheme, Context as _, IntoElement, ParentElement, Styled, ViewContext, VisualContext,
|
||||
div, ActiveTheme, IntoElement, ParentElement, StaticContext as _, Styled, ViewContext,
|
||||
VisualContext,
|
||||
};
|
||||
use util::{debug_panic, RangeExt};
|
||||
|
||||
|
||||
@@ -1268,7 +1268,7 @@ pub mod tests {
|
||||
ExcerptRange,
|
||||
};
|
||||
use futures::StreamExt;
|
||||
use gpui::{Context, TestAppContext, WindowHandle};
|
||||
use gpui::{StaticContext, TestAppContext, WindowHandle};
|
||||
use itertools::Itertools;
|
||||
use language::{
|
||||
language_settings::AllLanguageSettingsContent, Capability, FakeLspAdapter, Language,
|
||||
|
||||
@@ -8,7 +8,7 @@ use collections::HashSet;
|
||||
use futures::future::try_join_all;
|
||||
use git::repository::GitFileStatus;
|
||||
use gpui::{
|
||||
point, AnyElement, AppContext, AsyncWindowContext, Context, Entity, EntityId, EventEmitter,
|
||||
point, AnyElement, AppContext, AsyncWindowContext, StaticContext, Entity, EntityId, EventEmitter,
|
||||
IntoElement, Model, ParentElement, Pixels, SharedString, Styled, Task, View, ViewContext,
|
||||
VisualContext, WeakView, WindowContext,
|
||||
};
|
||||
|
||||
@@ -579,7 +579,7 @@ mod tests {
|
||||
test::{editor_test_context::EditorTestContext, marked_display_snapshot},
|
||||
Buffer, DisplayMap, DisplayRow, ExcerptRange, FoldPlaceholder, InlayId, MultiBuffer,
|
||||
};
|
||||
use gpui::{font, Context as _};
|
||||
use gpui::{font, StaticContext as _};
|
||||
use language::Capability;
|
||||
use project::Project;
|
||||
use settings::SettingsStore;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Context as _;
|
||||
use gpui::{Context, View, ViewContext, VisualContext, WindowContext};
|
||||
use gpui::{StaticContext, View, ViewContext, VisualContext, WindowContext};
|
||||
use language::Language;
|
||||
use multi_buffer::MultiBuffer;
|
||||
use project::lsp_ext_command::ExpandMacro;
|
||||
|
||||
27
crates/editor/src/sketch.rs
Normal file
27
crates/editor/src/sketch.rs
Normal file
@@ -0,0 +1,27 @@
|
||||
use crate::{Editor, EditorElement, EditorStyle};
|
||||
use gpui::{sketch::View, Model};
|
||||
use ui::ViewContext;
|
||||
|
||||
pub struct PaneItemProps {}
|
||||
struct TabProps {}
|
||||
|
||||
pub trait RenderEditor {
|
||||
fn tab_view(&self, props: EditorStyle) -> View<Editor, TabProps>;
|
||||
fn render(&self, props: EditorStyle) -> EditorElement;
|
||||
}
|
||||
|
||||
impl RenderEditor for Model<Editor> {
|
||||
fn tab_view(&self, style: EditorStyle) -> View<Editor, TabProps> {
|
||||
View::new(
|
||||
self.clone(),
|
||||
move |_, _: TabProps, cx: &mut ViewContext<Editor>| {
|
||||
cx.view().model.render(style.clone())
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn render(&self, style: EditorStyle) -> EditorElement {
|
||||
todo!()
|
||||
// EditorElement::new(self, style)
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ use crate::{
|
||||
display_map::{DisplayMap, DisplaySnapshot, ToDisplayPoint},
|
||||
DisplayPoint, Editor, EditorMode, FoldPlaceholder, MultiBuffer,
|
||||
};
|
||||
use gpui::{Context, Font, FontFeatures, FontStyle, FontWeight, Model, Pixels, ViewContext};
|
||||
use gpui::{Font, FontFeatures, FontStyle, FontWeight, Model, Pixels, StaticContext, ViewContext};
|
||||
use project::Project;
|
||||
use util::test::{marked_text_offsets, marked_text_ranges};
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ use std::{
|
||||
},
|
||||
};
|
||||
|
||||
use ui::Context;
|
||||
use ui::StaticContext;
|
||||
use util::{
|
||||
assert_set_eq,
|
||||
test::{generate_marked_text, marked_text_ranges},
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ use futures::{
|
||||
select_biased, AsyncReadExt as _, Future, FutureExt as _, StreamExt as _,
|
||||
};
|
||||
use gpui::{
|
||||
actions, AppContext, AsyncAppContext, Context, EventEmitter, Global, Model, ModelContext, Task,
|
||||
actions, AppContext, AsyncAppContext, StaticContext, EventEmitter, Global, Model, ModelContext, Task,
|
||||
WeakModel,
|
||||
};
|
||||
use http::{AsyncBody, HttpClient, HttpClientWithUrl};
|
||||
@@ -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);
|
||||
|
||||
@@ -10,7 +10,7 @@ use async_compression::futures::bufread::GzipEncoder;
|
||||
use collections::BTreeMap;
|
||||
use fs::{FakeFs, Fs, RealFs};
|
||||
use futures::{io::BufReader, AsyncReadExt, StreamExt};
|
||||
use gpui::{Context, TestAppContext};
|
||||
use gpui::{StaticContext, TestAppContext};
|
||||
use http::{FakeHttpClient, Response};
|
||||
use language::{LanguageMatcher, LanguageRegistry, LanguageServerBinaryStatus, LanguageServerName};
|
||||
use node_runtime::FakeNodeRuntime;
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ use util::ResultExt;
|
||||
|
||||
use crate::{
|
||||
current_platform, init_app_menus, Action, ActionRegistry, Any, AnyView, AnyWindowHandle,
|
||||
AppMetadata, AssetCache, AssetSource, BackgroundExecutor, ClipboardItem, Context,
|
||||
AppMetadata, AssetCache, AssetSource, BackgroundExecutor, ClipboardItem, StaticContext,
|
||||
DispatchPhase, DisplayId, Entity, EventEmitter, ForegroundExecutor, Global, KeyBinding, Keymap,
|
||||
Keystroke, LayoutId, Menu, MenuItem, PathPromptOptions, Pixels, Platform, PlatformDisplay,
|
||||
Point, PromptBuilder, PromptHandle, PromptLevel, Render, RenderablePromptHandle, Reservation,
|
||||
@@ -1263,7 +1263,7 @@ impl AppContext {
|
||||
}
|
||||
}
|
||||
|
||||
impl Context for AppContext {
|
||||
impl StaticContext for AppContext {
|
||||
type Result<T> = T;
|
||||
|
||||
/// Build an entity that is owned by the application. The given function will be invoked with
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use crate::{
|
||||
AnyView, AnyWindowHandle, AppCell, AppContext, BackgroundExecutor, BorrowAppContext, Context,
|
||||
AnyView, AnyWindowHandle, AppCell, AppContext, BackgroundExecutor, BorrowAppContext,
|
||||
DismissEvent, FocusableView, ForegroundExecutor, Global, Model, ModelContext, PromptLevel,
|
||||
Render, Reservation, Result, Task, View, ViewContext, VisualContext, WindowContext,
|
||||
WindowHandle,
|
||||
Render, Reservation, Result, StaticContext, Task, View, ViewContext, VisualContext,
|
||||
WindowContext, WindowHandle,
|
||||
};
|
||||
use anyhow::{anyhow, Context as _};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
@@ -19,7 +19,7 @@ pub struct AsyncAppContext {
|
||||
pub(crate) foreground_executor: ForegroundExecutor,
|
||||
}
|
||||
|
||||
impl Context for AsyncAppContext {
|
||||
impl StaticContext for AsyncAppContext {
|
||||
type Result<T> = Result<T>;
|
||||
|
||||
fn new_model<T: 'static>(
|
||||
@@ -301,7 +301,7 @@ impl AsyncWindowContext {
|
||||
}
|
||||
}
|
||||
|
||||
impl Context for AsyncWindowContext {
|
||||
impl StaticContext for AsyncWindowContext {
|
||||
type Result<T> = Result<T>;
|
||||
|
||||
fn new_model<T>(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::{seal::Sealed, AppContext, Context, Entity, ModelContext};
|
||||
use crate::{seal::Sealed, AppContext, StaticContext, Entity, ModelContext};
|
||||
use anyhow::{anyhow, Result};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use parking_lot::{RwLock, RwLockUpgradableReadGuard};
|
||||
@@ -398,7 +398,7 @@ impl<T: 'static> Model<T> {
|
||||
}
|
||||
|
||||
/// Read the entity referenced by this model with the given function.
|
||||
pub fn read_with<R, C: Context>(
|
||||
pub fn read_with<R, C: StaticContext>(
|
||||
&self,
|
||||
cx: &C,
|
||||
f: impl FnOnce(&T, &AppContext) -> R,
|
||||
@@ -417,7 +417,7 @@ impl<T: 'static> Model<T> {
|
||||
update: impl FnOnce(&mut T, &mut ModelContext<'_, T>) -> R,
|
||||
) -> C::Result<R>
|
||||
where
|
||||
C: Context,
|
||||
C: StaticContext,
|
||||
{
|
||||
cx.update_model(self, update)
|
||||
}
|
||||
@@ -595,7 +595,7 @@ impl<T: 'static> WeakModel<T> {
|
||||
update: impl FnOnce(&mut T, &mut ModelContext<'_, T>) -> R,
|
||||
) -> Result<R>
|
||||
where
|
||||
C: Context,
|
||||
C: StaticContext,
|
||||
Result<C::Result<R>>: crate::Flatten<R>,
|
||||
{
|
||||
crate::Flatten::flatten(
|
||||
@@ -610,7 +610,7 @@ impl<T: 'static> WeakModel<T> {
|
||||
/// been released.
|
||||
pub fn read_with<C, R>(&self, cx: &C, read: impl FnOnce(&T, &AppContext) -> R) -> Result<R>
|
||||
where
|
||||
C: Context,
|
||||
C: StaticContext,
|
||||
Result<C::Result<R>>: crate::Flatten<R>,
|
||||
{
|
||||
crate::Flatten::flatten(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
AnyView, AnyWindowHandle, AppContext, AsyncAppContext, Context, Effect, Entity, EntityId,
|
||||
EventEmitter, Model, Reservation, Subscription, Task, View, WeakModel, WindowContext,
|
||||
AnyView, AnyWindowHandle, AppContext, AsyncAppContext, Effect, Entity, EntityId, EventEmitter,
|
||||
Model, Reservation, StaticContext, Subscription, Task, View, WeakModel, WindowContext,
|
||||
WindowHandle,
|
||||
};
|
||||
use anyhow::Result;
|
||||
@@ -220,7 +220,7 @@ impl<'a, T> ModelContext<'a, T> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> Context for ModelContext<'a, T> {
|
||||
impl<'a, T> StaticContext for ModelContext<'a, T> {
|
||||
type Result<U> = U;
|
||||
|
||||
fn new_model<U: 'static>(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
Action, AnyView, AnyWindowHandle, AppCell, AppContext, AsyncAppContext, AvailableSpace,
|
||||
BackgroundExecutor, BorrowAppContext, Bounds, ClipboardItem, Context, DrawPhase, Drawable,
|
||||
BackgroundExecutor, BorrowAppContext, Bounds, ClipboardItem, StaticContext, DrawPhase, Drawable,
|
||||
Element, Empty, Entity, EventEmitter, ForegroundExecutor, Global, InputEvent, Keystroke, Model,
|
||||
ModelContext, Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent,
|
||||
MouseUpEvent, Pixels, Platform, Point, Render, Result, Size, Task, TestDispatcher,
|
||||
@@ -29,7 +29,7 @@ pub struct TestAppContext {
|
||||
on_quit: Rc<RefCell<Vec<Box<dyn FnOnce() + 'static>>>>,
|
||||
}
|
||||
|
||||
impl Context for TestAppContext {
|
||||
impl StaticContext for TestAppContext {
|
||||
type Result<T> = T;
|
||||
|
||||
fn new_model<T: 'static>(
|
||||
@@ -844,8 +844,8 @@ impl VisualTestContext {
|
||||
}
|
||||
}
|
||||
|
||||
impl Context for VisualTestContext {
|
||||
type Result<T> = <TestAppContext as Context>::Result<T>;
|
||||
impl StaticContext for VisualTestContext {
|
||||
type Result<T> = <TestAppContext as StaticContext>::Result<T>;
|
||||
|
||||
fn new_model<T: 'static>(
|
||||
&mut self,
|
||||
|
||||
@@ -87,6 +87,8 @@ pub mod prelude;
|
||||
mod scene;
|
||||
mod shared_string;
|
||||
mod shared_uri;
|
||||
/// todo!
|
||||
pub mod sketch;
|
||||
mod style;
|
||||
mod styled;
|
||||
mod subscription;
|
||||
@@ -156,7 +158,7 @@ use taffy::TaffyLayoutEngine;
|
||||
|
||||
/// The context trait, allows the different contexts in GPUI to be used
|
||||
/// interchangeably for certain operations.
|
||||
pub trait Context {
|
||||
pub trait StaticContext {
|
||||
/// The result type for this context, used for async contexts that
|
||||
/// can't hold a direct reference to the application context.
|
||||
type Result<T>;
|
||||
@@ -226,7 +228,7 @@ impl<T: 'static> Reservation<T> {
|
||||
|
||||
/// This trait is used for the different visual contexts in GPUI that
|
||||
/// require a window to be present.
|
||||
pub trait VisualContext: Context {
|
||||
pub trait VisualContext: StaticContext {
|
||||
/// Construct a new view in the window referenced by this context.
|
||||
fn new_view<V>(
|
||||
&mut self,
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
//! application to avoid having to import each trait individually.
|
||||
|
||||
pub use crate::{
|
||||
util::FluentBuilder, BorrowAppContext, BorrowWindow, Context, Element, FocusableElement,
|
||||
util::FluentBuilder, BorrowAppContext, BorrowWindow, Element, FocusableElement,
|
||||
InteractiveElement, IntoElement, ParentElement, Refineable, Render, RenderOnce,
|
||||
StatefulInteractiveElement, Styled, VisualContext,
|
||||
StatefulInteractiveElement, StaticContext, Styled, VisualContext,
|
||||
};
|
||||
|
||||
107
crates/gpui/src/sketch.rs
Normal file
107
crates/gpui/src/sketch.rs
Normal file
@@ -0,0 +1,107 @@
|
||||
use crate::{AnyElement, AnyModel, IntoElement, ViewContext, WindowContext};
|
||||
use std::{any::Any, marker::PhantomData, sync::Arc};
|
||||
|
||||
pub struct Model<M> {
|
||||
state_type: PhantomData<M>,
|
||||
}
|
||||
|
||||
pub trait Context {
|
||||
type ModelContext<'a, M>;
|
||||
|
||||
fn update<M, F, R>(&mut self, model: &Model<M>, update: F) -> R
|
||||
where
|
||||
M: 'static,
|
||||
F: for<'a> FnOnce(&mut M, &mut Self::ModelContext<'a, M>) -> R;
|
||||
}
|
||||
|
||||
impl Context for WindowContext<'_> {
|
||||
type ModelContext<'a, M> = ViewContext<'a, M>;
|
||||
|
||||
fn update<M, F, R>(&mut self, model: &Model<M>, update: F) -> R
|
||||
where
|
||||
M: 'static,
|
||||
F: for<'a> FnOnce(&mut M, &mut Self::ModelContext<'a, M>) -> R,
|
||||
{
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl<M> Model<M> {
|
||||
pub fn update<C, F, R>(&self, cx: &mut C, update: F) -> R
|
||||
where
|
||||
C: Context,
|
||||
F: for<'a> FnOnce(&mut M, &mut C::ModelContext<'a, M>) -> R,
|
||||
{
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
/// A view is the combination of a model with a compatible render function for that model.
|
||||
pub struct View<M, P> {
|
||||
/// A handle to the state we will render
|
||||
pub model: Model<M>,
|
||||
/// A recipe for displaying the state based on properties
|
||||
pub component: Arc<dyn StatefulComponent<M, P>>,
|
||||
}
|
||||
|
||||
impl<M: 'static, P: 'static> View<M, P> {
|
||||
/// Creates a new `View` with the specified model and render function.
|
||||
pub fn new<F, E>(model: Model<M>, render: F) -> Self
|
||||
where
|
||||
F: 'static + Fn(&mut M, P, &mut ViewContext<M>) -> E,
|
||||
E: IntoElement,
|
||||
{
|
||||
View {
|
||||
model,
|
||||
component: Arc::new(
|
||||
move |model: &mut M, props: P, cx: &mut ViewContext<'_, M>| {
|
||||
render(model, props, cx).into_any_element()
|
||||
},
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render(&self, props: P, cx: &mut WindowContext) -> AnyElement {
|
||||
self.model
|
||||
.update(cx, |model, cx| self.component.render(model, props, cx))
|
||||
}
|
||||
}
|
||||
|
||||
/// A mapping from properties P to an element tree.
|
||||
pub trait Component<P>: 'static {
|
||||
/// Render the properties
|
||||
fn render(&self, props: P, cx: &mut WindowContext) -> AnyElement;
|
||||
}
|
||||
|
||||
/// A mapping from a stateful model M and properties P to an element tree.
|
||||
pub trait StatefulComponent<M, P>: 'static {
|
||||
/// Render the model with the given properties
|
||||
fn render(&self, model: &mut M, props: P, cx: &mut ViewContext<M>) -> AnyElement;
|
||||
}
|
||||
|
||||
impl<P, F> Component<P> for F
|
||||
where
|
||||
F: Fn(P, &mut WindowContext) -> AnyElement + 'static,
|
||||
{
|
||||
fn render(&self, props: P, cx: &mut WindowContext) -> AnyElement {
|
||||
(self)(props, cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl<M, P, F> StatefulComponent<M, P> for F
|
||||
where
|
||||
F: for<'a, 'b, 'c> Fn(&'a mut M, P, &'b mut ViewContext<'c, M>) -> AnyElement + 'static,
|
||||
{
|
||||
fn render(&self, model: &mut M, props: P, cx: &mut ViewContext<M>) -> AnyElement {
|
||||
(self)(model, props, cx)
|
||||
}
|
||||
}
|
||||
|
||||
/// A dynamically typed view. It can be rendered with props P or downcast back to a typed view.
|
||||
pub struct AnyView<P> {
|
||||
model: AnyModel,
|
||||
/// An upcasted render function that takes the dynamic reference.
|
||||
render: Arc<dyn Fn(&AnyModel, P, &mut WindowContext) -> AnyElement>,
|
||||
/// The original render function to enable downcasting to a View.
|
||||
typed_render: Arc<dyn Any>,
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::{
|
||||
hash, point, prelude::*, px, size, transparent_black, Action, AnyDrag, AnyElement, AnyTooltip,
|
||||
AnyView, AppContext, Arena, Asset, AsyncWindowContext, AvailableSpace, Bounds, BoxShadow,
|
||||
Context, Corners, CursorStyle, DevicePixels, DispatchActionListener, DispatchNodeId,
|
||||
StaticContext, Corners, CursorStyle, DevicePixels, DispatchActionListener, DispatchNodeId,
|
||||
DispatchTree, DisplayId, Edges, Effect, Entity, EntityId, EventEmitter, FileDropEvent, Flatten,
|
||||
FontId, Global, GlobalElementId, GlyphId, Hsla, ImageData, InputHandler, IsZero, KeyBinding,
|
||||
KeyContext, KeyDownEvent, KeyEvent, KeyMatch, KeymatchResult, Keystroke, KeystrokeEvent,
|
||||
@@ -3536,7 +3536,7 @@ impl WindowContext<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
impl Context for WindowContext<'_> {
|
||||
impl StaticContext for WindowContext<'_> {
|
||||
type Result<T> = T;
|
||||
|
||||
fn new_model<T>(&mut self, build_model: impl FnOnce(&mut ModelContext<'_, T>) -> T) -> Model<T>
|
||||
@@ -4198,7 +4198,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<V> Context for ViewContext<'_, V> {
|
||||
impl<V> StaticContext for ViewContext<'_, V> {
|
||||
type Result<U> = U;
|
||||
|
||||
fn new_model<T: 'static>(
|
||||
@@ -4348,7 +4348,7 @@ impl<V: 'static + Render> WindowHandle<V> {
|
||||
/// This will fail if the window is closed or if the root view's type does not match `V`.
|
||||
pub fn root<C>(&self, cx: &mut C) -> Result<View<V>>
|
||||
where
|
||||
C: Context,
|
||||
C: StaticContext,
|
||||
{
|
||||
Flatten::flatten(cx.update_window(self.any_handle, |root_view, _| {
|
||||
root_view
|
||||
@@ -4366,7 +4366,7 @@ impl<V: 'static + Render> WindowHandle<V> {
|
||||
update: impl FnOnce(&mut V, &mut ViewContext<'_, V>) -> R,
|
||||
) -> Result<R>
|
||||
where
|
||||
C: Context,
|
||||
C: StaticContext,
|
||||
{
|
||||
cx.update_window(self.any_handle, |root_view, cx| {
|
||||
let view = root_view
|
||||
@@ -4400,7 +4400,7 @@ impl<V: 'static + Render> WindowHandle<V> {
|
||||
/// This will fail if the window is closed or if the root view's type does not match `V`.
|
||||
pub fn read_with<C, R>(&self, cx: &C, read_with: impl FnOnce(&V, &AppContext) -> R) -> Result<R>
|
||||
where
|
||||
C: Context,
|
||||
C: StaticContext,
|
||||
{
|
||||
cx.read_window(self, |root_view, cx| read_with(root_view.read(cx), cx))
|
||||
}
|
||||
@@ -4410,7 +4410,7 @@ impl<V: 'static + Render> WindowHandle<V> {
|
||||
/// This will fail if the window is closed or if the root view's type does not match `V`.
|
||||
pub fn root_view<C>(&self, cx: &C) -> Result<View<V>>
|
||||
where
|
||||
C: Context,
|
||||
C: StaticContext,
|
||||
{
|
||||
cx.read_window(self, |root_view, _cx| root_view.clone())
|
||||
}
|
||||
@@ -4491,7 +4491,7 @@ impl AnyWindowHandle {
|
||||
update: impl FnOnce(AnyView, &mut WindowContext<'_>) -> R,
|
||||
) -> Result<R>
|
||||
where
|
||||
C: Context,
|
||||
C: StaticContext,
|
||||
{
|
||||
cx.update_window(self, update)
|
||||
}
|
||||
@@ -4501,7 +4501,7 @@ impl AnyWindowHandle {
|
||||
/// This will fail if the window has been closed.
|
||||
pub fn read<T, C, R>(self, cx: &C, read: impl FnOnce(View<T>, &AppContext) -> R) -> Result<R>
|
||||
where
|
||||
C: Context,
|
||||
C: StaticContext,
|
||||
T: 'static,
|
||||
{
|
||||
let view = self
|
||||
|
||||
@@ -3,7 +3,9 @@ use client::DevServerProjectId;
|
||||
use client::{user::UserStore, Client, ClientSettings};
|
||||
use fs::Fs;
|
||||
use futures::Future;
|
||||
use gpui::{AppContext, AsyncAppContext, Context, Global, Model, ModelContext, Task, WeakModel};
|
||||
use gpui::{
|
||||
AppContext, AsyncAppContext, Global, Model, ModelContext, StaticContext, Task, WeakModel,
|
||||
};
|
||||
use language::LanguageRegistry;
|
||||
use node_runtime::NodeRuntime;
|
||||
use postage::stream::Stream;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use gpui::{
|
||||
canvas, div, fill, img, opaque_grey, point, size, AnyElement, AppContext, Bounds, Context,
|
||||
canvas, div, fill, img, opaque_grey, point, size, AnyElement, AppContext, Bounds, StaticContext,
|
||||
EventEmitter, FocusHandle, FocusableView, Img, InteractiveElement, IntoElement, Model,
|
||||
ObjectFit, ParentElement, Render, Styled, Task, View, ViewContext, VisualContext, WeakView,
|
||||
WindowContext,
|
||||
|
||||
@@ -3153,10 +3153,7 @@ impl BufferSnapshot {
|
||||
range: Range<Anchor>,
|
||||
cx: &AppContext,
|
||||
) -> Vec<IndentGuide> {
|
||||
fn tab_size_for_row(this: &BufferSnapshot, row: BufferRow, cx: &AppContext) -> u32 {
|
||||
let language = this.language_at(Point::new(row, 0));
|
||||
language_settings(language, None, cx).tab_size.get() as u32
|
||||
}
|
||||
let tab_size = language_settings(self.language(), None, cx).tab_size.get() as u32;
|
||||
|
||||
let start_row = range.start.to_point(self).row;
|
||||
let end_row = range.end.to_point(self).row;
|
||||
@@ -3167,9 +3164,6 @@ impl BufferSnapshot {
|
||||
let mut result_vec = Vec::new();
|
||||
let mut indent_stack = SmallVec::<[IndentGuide; 8]>::new();
|
||||
|
||||
// TODO: This should be calculated for every row but it is pretty expensive
|
||||
let tab_size = tab_size_for_row(self, start_row, cx);
|
||||
|
||||
while let Some((first_row, mut line_indent)) = row_indents.next() {
|
||||
let current_depth = indent_stack.len() as u32;
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ use clock::ReplicaId;
|
||||
use collections::BTreeMap;
|
||||
use futures::FutureExt as _;
|
||||
use gpui::{AppContext, BorrowAppContext, Model};
|
||||
use gpui::{Context, TestAppContext};
|
||||
use gpui::{StaticContext, TestAppContext};
|
||||
use indoc::indoc;
|
||||
use proto::deserialize_operation;
|
||||
use rand::prelude::*;
|
||||
|
||||
@@ -3,7 +3,7 @@ use copilot::Copilot;
|
||||
use editor::{actions::MoveToEnd, Editor, EditorEvent};
|
||||
use futures::{channel::mpsc, StreamExt};
|
||||
use gpui::{
|
||||
actions, div, AnchorCorner, AnyElement, AppContext, Context, EventEmitter, FocusHandle,
|
||||
actions, div, AnchorCorner, AnyElement, AppContext, StaticContext, EventEmitter, FocusHandle,
|
||||
FocusableView, IntoElement, Model, ModelContext, ParentElement, Render, Styled, Subscription,
|
||||
View, ViewContext, VisualContext, WeakModel, WindowContext,
|
||||
};
|
||||
|
||||
@@ -4,7 +4,7 @@ use crate::lsp_log::LogMenuItem;
|
||||
|
||||
use super::*;
|
||||
use futures::StreamExt;
|
||||
use gpui::{Context, TestAppContext, VisualTestContext};
|
||||
use gpui::{StaticContext, TestAppContext, VisualTestContext};
|
||||
use language::{
|
||||
tree_sitter_rust, FakeLspAdapter, Language, LanguageConfig, LanguageMatcher, LanguageServerName,
|
||||
};
|
||||
|
||||
@@ -319,7 +319,7 @@ async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServ
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use gpui::{BorrowAppContext, Context, TestAppContext};
|
||||
use gpui::{BorrowAppContext, StaticContext, TestAppContext};
|
||||
use language::{language_settings::AllLanguageSettings, AutoindentMode, Buffer};
|
||||
use settings::SettingsStore;
|
||||
use std::num::NonZeroU32;
|
||||
|
||||
@@ -135,7 +135,7 @@ async fn get_cached_server_binary(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use gpui::{Context, TestAppContext};
|
||||
use gpui::{StaticContext, TestAppContext};
|
||||
use unindent::Unindent;
|
||||
|
||||
#[gpui::test]
|
||||
|
||||
@@ -201,7 +201,7 @@ pub(super) fn python_task_context() -> ContextProviderWithTasks {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use gpui::{BorrowAppContext, Context, ModelContext, TestAppContext};
|
||||
use gpui::{BorrowAppContext, ModelContext, StaticContext, TestAppContext};
|
||||
use language::{language_settings::AllLanguageSettings, AutoindentMode, Buffer};
|
||||
use settings::SettingsStore;
|
||||
use std::num::NonZeroU32;
|
||||
|
||||
@@ -504,7 +504,7 @@ mod tests {
|
||||
|
||||
use super::*;
|
||||
use crate::language;
|
||||
use gpui::{BorrowAppContext, Context, Hsla, TestAppContext};
|
||||
use gpui::{BorrowAppContext, Hsla, StaticContext, TestAppContext};
|
||||
use language::language_settings::AllLanguageSettings;
|
||||
use settings::SettingsStore;
|
||||
use theme::SyntaxTheme;
|
||||
|
||||
@@ -435,7 +435,7 @@ async fn get_cached_eslint_server_binary(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use gpui::{Context, TestAppContext};
|
||||
use gpui::{StaticContext, TestAppContext};
|
||||
use unindent::Unindent;
|
||||
|
||||
#[gpui::test]
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
; Properties
|
||||
|
||||
(property_identifier) @property
|
||||
(shorthand_property_identifier) @property
|
||||
(shorthand_property_identifier_pattern) @property
|
||||
|
||||
; Function and method calls
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ use theme::SyntaxTheme;
|
||||
use util::post_inc;
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
use gpui::Context;
|
||||
use gpui::StaticContext;
|
||||
|
||||
const NEWLINES: &[u8] = &[b'\n'; u8::MAX as usize];
|
||||
|
||||
@@ -4649,7 +4649,7 @@ where
|
||||
mod tests {
|
||||
use super::*;
|
||||
use futures::StreamExt;
|
||||
use gpui::{AppContext, Context, TestAppContext};
|
||||
use gpui::{AppContext, StaticContext, TestAppContext};
|
||||
use language::{Buffer, Rope};
|
||||
use parking_lot::RwLock;
|
||||
use rand::prelude::*;
|
||||
|
||||
@@ -4,7 +4,7 @@ use client::{ChannelId, Client, UserStore};
|
||||
use collections::HashMap;
|
||||
use db::smol::stream::StreamExt;
|
||||
use gpui::{
|
||||
AppContext, AsyncAppContext, Context as _, EventEmitter, Global, Model, ModelContext, Task,
|
||||
AppContext, AsyncAppContext, StaticContext as _, EventEmitter, Global, Model, ModelContext, Task,
|
||||
};
|
||||
use rpc::{proto, Notification, TypedEnvelope};
|
||||
use std::{ops::Range, sync::Arc};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -3,7 +3,7 @@ use anyhow::Result;
|
||||
use client::Client;
|
||||
use collections::{HashMap, HashSet};
|
||||
use futures::{FutureExt, StreamExt};
|
||||
use gpui::{AppContext, AsyncAppContext, Context, Global, Model, ModelContext, Task, WeakModel};
|
||||
use gpui::{AppContext, AsyncAppContext, StaticContext, Global, Model, ModelContext, Task, WeakModel};
|
||||
use postage::stream::Stream;
|
||||
use rpc::proto;
|
||||
use std::{sync::Arc, time::Duration};
|
||||
|
||||
@@ -35,8 +35,9 @@ use fuzzy::CharBag;
|
||||
use git::{blame::Blame, repository::GitRepository};
|
||||
use globset::{Glob, GlobSet, GlobSetBuilder};
|
||||
use gpui::{
|
||||
AnyModel, AppContext, AsyncAppContext, BackgroundExecutor, BorrowAppContext, Context, Entity,
|
||||
EventEmitter, Model, ModelContext, PromptLevel, SharedString, Task, WeakModel, WindowContext,
|
||||
AnyModel, AppContext, AsyncAppContext, BackgroundExecutor, BorrowAppContext, Entity,
|
||||
EventEmitter, Model, ModelContext, PromptLevel, SharedString, StaticContext, Task, WeakModel,
|
||||
WindowContext,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use language::{
|
||||
@@ -7856,10 +7857,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 +8192,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 +8200,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 +8250,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 {
|
||||
|
||||
@@ -13,7 +13,7 @@ use futures::{
|
||||
channel::mpsc::{unbounded, UnboundedSender},
|
||||
StreamExt,
|
||||
};
|
||||
use gpui::{AppContext, Context, Model, ModelContext, Task};
|
||||
use gpui::{AppContext, Model, ModelContext, StaticContext, Task};
|
||||
use itertools::Itertools;
|
||||
use language::{ContextProvider, Language, Location};
|
||||
use task::{
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use crate::Project;
|
||||
use collections::HashMap;
|
||||
use gpui::{
|
||||
AnyWindowHandle, AppContext, Context, Entity, Model, ModelContext, SharedString, WeakModel,
|
||||
AnyWindowHandle, AppContext, Entity, Model, ModelContext, SharedString, StaticContext,
|
||||
WeakModel,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use settings::{Settings, SettingsLocation};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user