diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 2cd2050013..147402b285 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -2,11 +2,4 @@ Release Notes: -- N/A - -or - - (Added|Fixed|Improved) ... ([#](https://github.com/zed-industries/community/issues/)). - -If the release notes are only intended for a specific release channel only, add `(-only)` to the end of the release note line. -These will be removed by the person making the release. diff --git a/.github/workflows/release_actions.yml b/.github/workflows/release_actions.yml index f767324e4f..550eda882b 100644 --- a/.github/workflows/release_actions.yml +++ b/.github/workflows/release_actions.yml @@ -6,8 +6,8 @@ jobs: discord_release: runs-on: ubuntu-latest steps: - - name: Get appropriate URL - id: get-appropriate-url + - name: Get release URL + id: get-release-url run: | if [ "${{ github.event.release.prerelease }}" == "true" ]; then URL="https://zed.dev/releases/preview/latest" @@ -15,14 +15,17 @@ jobs: URL="https://zed.dev/releases/stable/latest" fi echo "::set-output name=URL::$URL" + - name: Get content + uses: 2428392/gh-truncate-string-action@v1.2.0 + id: get-content + with: + stringToTruncate: | + 📣 Zed [${{ github.event.release.tag_name }}](${{ steps.get-release-url.outputs.URL }}) was just released! + ${{ github.event.release.body }} + maxLength: 2000 - name: Discord Webhook Action uses: tsickert/discord-webhook@v5.3.0 with: webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }} - content: | - 📣 Zed ${{ github.event.release.tag_name }} was just released! - - Restart your Zed or head to ${{ steps.get-appropriate-url.outputs.URL }} to grab it. - - ${{ github.event.release.body }} + content: ${{ steps.get-content.outputs.string }} diff --git a/Cargo.lock b/Cargo.lock index cd36221de0..6b92170b88 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -103,7 +103,7 @@ dependencies = [ "rusqlite", "serde", "serde_json", - "tiktoken-rs 0.5.4", + "tiktoken-rs", "util", ] @@ -316,12 +316,13 @@ dependencies = [ "regex", "schemars", "search", + "semantic_index", "serde", "serde_json", "settings", "smol", "theme", - "tiktoken-rs 0.4.5", + "tiktoken-rs", "util", "uuid 1.4.1", "workspace", @@ -1466,7 +1467,7 @@ dependencies = [ [[package]] name = "collab" -version = "0.23.3" +version = "0.25.0" dependencies = [ "anyhow", "async-trait", @@ -1501,6 +1502,7 @@ dependencies = [ "log", "lsp", "nanoid", + "node_runtime", "parking_lot 0.11.2", "pretty_assertions", "project", @@ -1623,6 +1625,7 @@ dependencies = [ "theme", "util", "workspace", + "zed-actions", ] [[package]] @@ -2403,7 +2406,6 @@ dependencies = [ "parking_lot 0.11.2", "postage", "project", - "pulldown-cmark", "rand 0.8.5", "rich_text", "rpc", @@ -3988,6 +3990,7 @@ dependencies = [ "lsp", "parking_lot 0.11.2", "postage", + "pulldown-cmark", "rand 0.8.5", "regex", "rpc", @@ -5517,6 +5520,26 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "prettier" +version = "0.1.0" +dependencies = [ + "anyhow", + "client", + "collections", + "fs", + "futures 0.3.28", + "gpui", + "language", + "log", + "lsp", + "node_runtime", + "serde", + "serde_derive", + "serde_json", + "util", +] + [[package]] name = "pretty_assertions" version = "1.4.0" @@ -5629,8 +5652,10 @@ dependencies = [ "lazy_static", "log", "lsp", + "node_runtime", "parking_lot 0.11.2", "postage", + "prettier", "pretty_assertions", "rand 0.8.5", "regex", @@ -6600,12 +6625,6 @@ dependencies = [ "untrusted", ] -[[package]] -name = "rustversion" -version = "1.0.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" - [[package]] name = "rustybuzz" version = "0.3.0" @@ -6925,7 +6944,7 @@ dependencies = [ "smol", "tempdir", "theme", - "tiktoken-rs 0.5.4", + "tiktoken-rs", "tree-sitter", "tree-sitter-cpp", "tree-sitter-elixir", @@ -6939,7 +6958,6 @@ dependencies = [ "unindent", "util", "workspace", - "zed", ] [[package]] @@ -7659,28 +7677,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" -[[package]] -name = "storybook" -version = "0.1.0" -dependencies = [ - "anyhow", - "chrono", - "clap 4.4.4", - "fs", - "futures 0.3.28", - "gpui2", - "itertools 0.11.0", - "log", - "rust-embed", - "serde", - "settings", - "simplelog", - "strum", - "theme", - "ui", - "util", -] - [[package]] name = "stringprep" version = "0.1.4" @@ -7703,22 +7699,6 @@ name = "strum" version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" -dependencies = [ - "strum_macros", -] - -[[package]] -name = "strum_macros" -version = "0.25.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad8d03b598d3d0fff69bf533ee3ef19b8eeb342729596df84bcc7e1f96ec4059" -dependencies = [ - "heck 0.4.1", - "proc-macro2", - "quote", - "rustversion", - "syn 2.0.37", -] [[package]] name = "subtle" @@ -8139,21 +8119,6 @@ dependencies = [ "weezl", ] -[[package]] -name = "tiktoken-rs" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52aacc1cff93ba9d5f198c62c49c77fa0355025c729eed3326beaf7f33bc8614" -dependencies = [ - "anyhow", - "base64 0.21.4", - "bstr", - "fancy-regex", - "lazy_static", - "parking_lot 0.12.1", - "rustc-hash", -] - [[package]] name = "tiktoken-rs" version = "0.5.4" @@ -8814,6 +8779,15 @@ dependencies = [ "tree-sitter", ] +[[package]] +name = "tree-sitter-vue" +version = "0.0.1" +source = "git+https://github.com/zed-industries/tree-sitter-vue?rev=95b2890#95b28908d90e928c308866f7631e73ef6e1d4b5f" +dependencies = [ + "cc", + "tree-sitter", +] + [[package]] name = "tree-sitter-yaml" version = "0.0.1" @@ -8885,21 +8859,6 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" -[[package]] -name = "ui" -version = "0.1.0" -dependencies = [ - "anyhow", - "chrono", - "gpui2", - "rand 0.8.5", - "serde", - "settings", - "smallvec", - "strum", - "theme", -] - [[package]] name = "unicase" version = "2.7.0" @@ -9986,6 +9945,7 @@ dependencies = [ "lazy_static", "log", "menu", + "node_runtime", "parking_lot 0.11.2", "postage", "project", @@ -10081,9 +10041,10 @@ dependencies = [ [[package]] name = "zed" -version = "0.108.0" +version = "0.110.0" dependencies = [ "activity_indicator", + "ai", "anyhow", "assistant", "async-compression", @@ -10196,6 +10157,7 @@ dependencies = [ "tree-sitter-svelte", "tree-sitter-toml", "tree-sitter-typescript", + "tree-sitter-vue", "tree-sitter-yaml", "unindent", "url", @@ -10213,6 +10175,7 @@ name = "zed-actions" version = "0.1.0" dependencies = [ "gpui", + "serde", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 7dae3bd81f..995cd15edd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,6 +52,7 @@ members = [ "crates/plugin", "crates/plugin_macros", "crates/plugin_runtime", + "crates/prettier", "crates/project", "crates/project_panel", "crates/project_symbols", @@ -65,13 +66,11 @@ members = [ "crates/sqlez_macros", "crates/feature_flags", "crates/rich_text", - "crates/storybook", "crates/sum_tree", "crates/terminal", "crates/text", "crates/theme", "crates/theme_selector", - "crates/ui", "crates/util", "crates/semantic_index", "crates/vim", @@ -150,7 +149,7 @@ tree-sitter-yaml = { git = "https://github.com/zed-industries/tree-sitter-yaml", tree-sitter-lua = "0.0.14" tree-sitter-nix = { git = "https://github.com/nix-community/tree-sitter-nix", rev = "66e3e9ce9180ae08fc57372061006ef83f0abde7" } tree-sitter-nu = { git = "https://github.com/nushell/tree-sitter-nu", rev = "786689b0562b9799ce53e824cb45a1a2a04dc673"} - +tree-sitter-vue = {git = "https://github.com/zed-industries/tree-sitter-vue", rev = "95b2890"} [patch.crates-io] tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "35a6052fbcafc5e5fc0f9415b8652be7dcaf7222" } async-task = { git = "https://github.com/zed-industries/async-task", rev = "341b57d6de98cdfd7b418567b8de2022ca993a6e" } diff --git a/Procfile b/Procfile index 2eb7de20fb..3f42c3a967 100644 --- a/Procfile +++ b/Procfile @@ -1,4 +1,4 @@ web: cd ../zed.dev && PORT=3000 npm run dev -collab: cd crates/collab && RUST_LOG=${RUST_LOG:-collab=info} cargo run serve +collab: cd crates/collab && RUST_LOG=${RUST_LOG:-warn,collab=info} cargo run serve livekit: livekit-server --dev postgrest: postgrest crates/collab/admin_api.conf diff --git a/assets/icons/link.svg b/assets/icons/link.svg new file mode 100644 index 0000000000..4925bd8e00 --- /dev/null +++ b/assets/icons/link.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/public.svg b/assets/icons/public.svg new file mode 100644 index 0000000000..38278cdaba --- /dev/null +++ b/assets/icons/public.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/update.svg b/assets/icons/update.svg new file mode 100644 index 0000000000..b529b2b08b --- /dev/null +++ b/assets/icons/update.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 07ba8a121f..ea025747d8 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -408,6 +408,7 @@ "vim::PushOperator", "Yank" ], + "shift-y": "vim::YankLine", "i": "vim::InsertBefore", "shift-i": "vim::InsertFirstNonWhitespace", "a": "vim::InsertAfter", diff --git a/assets/settings/default.json b/assets/settings/default.json index 8fb73a2ecb..4143e5dd41 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -50,6 +50,9 @@ // Whether to pop the completions menu while typing in an editor without // explicitly requesting it. "show_completions_on_input": true, + // Whether to display inline and alongside documentation for items in the + // completions menu + "show_completion_documentation": true, // Whether to show wrap guides in the editor. Setting this to true will // show a guide at the 'preferred_line_length' value if softwrap is set to // 'preferred_line_length', and will show any additional guides as specified @@ -76,7 +79,7 @@ // Settings related to calls in Zed "calls": { // Join calls with the microphone muted by default - "mute_on_join": true + "mute_on_join": false }, // Scrollbar related settings "scrollbar": { @@ -199,7 +202,12 @@ // "arguments": ["--stdin-filepath", "{buffer_path}"] // } // } - "formatter": "language_server", + // 3. Format code using Zed's Prettier integration: + // "formatter": "prettier" + // 4. Default. Format files using Zed's Prettier integration (if applicable), + // or falling back to formatting via language server: + // "formatter": "auto" + "formatter": "auto", // How to soft-wrap long lines of text. This setting can take // three values: // @@ -429,6 +437,16 @@ "tab_size": 2 } }, + // Zed's Prettier integration settings. + // If Prettier is enabled, Zed will use this its Prettier instance for any applicable file, if + // project has no other Prettier installed. + "prettier": { + // Use regular Prettier json configuration: + // "trailingComma": "es5", + // "tabWidth": 4, + // "semi": false, + // "singleQuote": true + }, // LSP Specific settings. "lsp": { // Specify the LSP name as a key here. diff --git a/crates/ai/src/embedding.rs b/crates/ai/src/embedding.rs index 332470aa54..4587ece0a2 100644 --- a/crates/ai/src/embedding.rs +++ b/crates/ai/src/embedding.rs @@ -85,25 +85,6 @@ impl Embedding { } } -// impl FromSql for Embedding { -// fn column_result(value: ValueRef) -> FromSqlResult { -// let bytes = value.as_blob()?; -// let embedding: Result, Box> = bincode::deserialize(bytes); -// if embedding.is_err() { -// return Err(rusqlite::types::FromSqlError::Other(embedding.unwrap_err())); -// } -// Ok(Embedding(embedding.unwrap())) -// } -// } - -// impl ToSql for Embedding { -// fn to_sql(&self) -> rusqlite::Result { -// let bytes = bincode::serialize(&self.0) -// .map_err(|err| rusqlite::Error::ToSqlConversionFailure(Box::new(err)))?; -// Ok(ToSqlOutput::Owned(rusqlite::types::Value::Blob(bytes))) -// } -// } - #[derive(Clone)] pub struct OpenAIEmbeddings { pub client: Arc, @@ -300,6 +281,7 @@ impl EmbeddingProvider for OpenAIEmbeddings { request_timeout, ) .await?; + request_number += 1; match response.status() { diff --git a/crates/assistant/Cargo.toml b/crates/assistant/Cargo.toml index f1daf47bab..9cfdd3301a 100644 --- a/crates/assistant/Cargo.toml +++ b/crates/assistant/Cargo.toml @@ -22,8 +22,11 @@ settings = { path = "../settings" } theme = { path = "../theme" } util = { path = "../util" } workspace = { path = "../workspace" } -uuid.workspace = true +semantic_index = { path = "../semantic_index" } +project = { path = "../project" } +uuid.workspace = true +log.workspace = true anyhow.workspace = true chrono = { version = "0.4", features = ["serde"] } futures.workspace = true @@ -36,7 +39,7 @@ schemars.workspace = true serde.workspace = true serde_json.workspace = true smol.workspace = true -tiktoken-rs = "0.4" +tiktoken-rs = "0.5" [dev-dependencies] editor = { path = "../editor", features = ["test-support"] } diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index b69c12a2a3..65edb1832f 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -1,7 +1,7 @@ use crate::{ assistant_settings::{AssistantDockPosition, AssistantSettings, OpenAIModel}, codegen::{self, Codegen, CodegenKind}, - prompts::generate_content_prompt, + prompts::{generate_content_prompt, PromptCodeSnippet}, MessageId, MessageMetadata, MessageStatus, Role, SavedConversation, SavedConversationMetadata, SavedMessage, }; @@ -17,7 +17,7 @@ use editor::{ BlockContext, BlockDisposition, BlockId, BlockProperties, BlockStyle, ToDisplayPoint, }, scroll::autoscroll::{Autoscroll, AutoscrollStrategy}, - Anchor, Editor, MoveDown, MoveUp, MultiBufferSnapshot, ToOffset, + Anchor, Editor, MoveDown, MoveUp, MultiBufferSnapshot, ToOffset, ToPoint, }; use fs::Fs; use futures::StreamExt; @@ -29,13 +29,15 @@ use gpui::{ }, fonts::HighlightStyle, geometry::vector::{vec2f, Vector2F}, - platform::{CursorStyle, MouseButton}, + platform::{CursorStyle, MouseButton, PromptLevel}, Action, AnyElement, AppContext, AsyncAppContext, ClipboardItem, Element, Entity, ModelContext, - ModelHandle, SizeConstraint, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, - WindowContext, + ModelHandle, SizeConstraint, Subscription, Task, View, ViewContext, ViewHandle, + WeakModelHandle, WeakViewHandle, WindowContext, }; use language::{language_settings::SoftWrap, Buffer, LanguageRegistry, ToOffset as _}; +use project::Project; use search::BufferSearchBar; +use semantic_index::{SemanticIndex, SemanticIndexStatus}; use settings::SettingsStore; use std::{ cell::{Cell, RefCell}, @@ -46,7 +48,7 @@ use std::{ path::{Path, PathBuf}, rc::Rc, sync::Arc, - time::Duration, + time::{Duration, Instant}, }; use theme::{ components::{action_button::Button, ComponentExt}, @@ -72,6 +74,7 @@ actions!( ResetKey, InlineAssist, ToggleIncludeConversation, + ToggleRetrieveContext, ] ); @@ -108,6 +111,7 @@ pub fn init(cx: &mut AppContext) { cx.add_action(InlineAssistant::confirm); cx.add_action(InlineAssistant::cancel); cx.add_action(InlineAssistant::toggle_include_conversation); + cx.add_action(InlineAssistant::toggle_retrieve_context); cx.add_action(InlineAssistant::move_up); cx.add_action(InlineAssistant::move_down); } @@ -145,6 +149,8 @@ pub struct AssistantPanel { include_conversation_in_next_inline_assist: bool, inline_prompt_history: VecDeque, _watch_saved_conversations: Task>, + semantic_index: Option>, + retrieve_context_in_next_inline_assist: bool, } impl AssistantPanel { @@ -191,6 +197,9 @@ impl AssistantPanel { toolbar.add_item(cx.add_view(|cx| BufferSearchBar::new(cx)), cx); toolbar }); + + let semantic_index = SemanticIndex::global(cx); + let mut this = Self { workspace: workspace_handle, active_editor_index: Default::default(), @@ -215,6 +224,8 @@ impl AssistantPanel { include_conversation_in_next_inline_assist: false, inline_prompt_history: Default::default(), _watch_saved_conversations, + semantic_index, + retrieve_context_in_next_inline_assist: false, }; let mut old_dock_position = this.position(cx); @@ -262,12 +273,19 @@ impl AssistantPanel { return; }; + let project = workspace.project(); + this.update(cx, |assistant, cx| { - assistant.new_inline_assist(&active_editor, cx) + assistant.new_inline_assist(&active_editor, cx, project) }); } - fn new_inline_assist(&mut self, editor: &ViewHandle, cx: &mut ViewContext) { + fn new_inline_assist( + &mut self, + editor: &ViewHandle, + cx: &mut ViewContext, + project: &ModelHandle, + ) { let api_key = if let Some(api_key) = self.api_key.borrow().clone() { api_key } else { @@ -278,26 +296,61 @@ impl AssistantPanel { if selection.start.excerpt_id() != selection.end.excerpt_id() { return; } + let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx); + + // Extend the selection to the start and the end of the line. + let mut point_selection = selection.map(|selection| selection.to_point(&snapshot)); + if point_selection.end > point_selection.start { + point_selection.start.column = 0; + // If the selection ends at the start of the line, we don't want to include it. + if point_selection.end.column == 0 { + point_selection.end.row -= 1; + } + point_selection.end.column = snapshot.line_len(point_selection.end.row); + } + + let codegen_kind = if point_selection.start == point_selection.end { + CodegenKind::Generate { + position: snapshot.anchor_after(point_selection.start), + } + } else { + CodegenKind::Transform { + range: snapshot.anchor_before(point_selection.start) + ..snapshot.anchor_after(point_selection.end), + } + }; let inline_assist_id = post_inc(&mut self.next_inline_assist_id); - let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx); let provider = Arc::new(OpenAICompletionProvider::new( api_key, cx.background().clone(), )); - let codegen_kind = if editor.read(cx).selections.newest::(cx).is_empty() { - CodegenKind::Generate { - position: selection.start, - } - } else { - CodegenKind::Transform { - range: selection.start..selection.end, - } - }; + let codegen = cx.add_model(|cx| { Codegen::new(editor.read(cx).buffer().clone(), codegen_kind, provider, cx) }); + if let Some(semantic_index) = self.semantic_index.clone() { + let project = project.clone(); + cx.spawn(|_, mut cx| async move { + let previously_indexed = semantic_index + .update(&mut cx, |index, cx| { + index.project_previously_indexed(&project, cx) + }) + .await + .unwrap_or(false); + if previously_indexed { + let _ = semantic_index + .update(&mut cx, |index, cx| { + index.index_project(project.clone(), cx) + }) + .await; + } + anyhow::Ok(()) + }) + .detach_and_log_err(cx); + } + let measurements = Rc::new(Cell::new(BlockMeasurements::default())); let inline_assistant = cx.add_view(|cx| { let assistant = InlineAssistant::new( @@ -308,6 +361,9 @@ impl AssistantPanel { codegen.clone(), self.workspace.clone(), cx, + self.retrieve_context_in_next_inline_assist, + self.semantic_index.clone(), + project.clone(), ); cx.focus_self(); assistant @@ -319,7 +375,7 @@ impl AssistantPanel { editor.insert_blocks( [BlockProperties { style: BlockStyle::Flex, - position: selection.head().bias_left(&snapshot), + position: snapshot.anchor_before(point_selection.head()), height: 2, render: Arc::new({ let inline_assistant = inline_assistant.clone(); @@ -348,6 +404,7 @@ impl AssistantPanel { editor: editor.downgrade(), inline_assistant: Some((block_id, inline_assistant.clone())), codegen: codegen.clone(), + project: project.downgrade(), _subscriptions: vec![ cx.subscribe(&inline_assistant, Self::handle_inline_assistant_event), cx.subscribe(editor, { @@ -426,8 +483,15 @@ impl AssistantPanel { InlineAssistantEvent::Confirmed { prompt, include_conversation, + retrieve_context, } => { - self.confirm_inline_assist(assist_id, prompt, *include_conversation, cx); + self.confirm_inline_assist( + assist_id, + prompt, + *include_conversation, + cx, + *retrieve_context, + ); } InlineAssistantEvent::Canceled => { self.finish_inline_assist(assist_id, true, cx); @@ -440,6 +504,9 @@ impl AssistantPanel { } => { self.include_conversation_in_next_inline_assist = *include_conversation; } + InlineAssistantEvent::RetrieveContextToggled { retrieve_context } => { + self.retrieve_context_in_next_inline_assist = *retrieve_context + } } } @@ -518,6 +585,7 @@ impl AssistantPanel { user_prompt: &str, include_conversation: bool, cx: &mut ViewContext, + retrieve_context: bool, ) { let conversation = if include_conversation { self.active_editor() @@ -539,6 +607,8 @@ impl AssistantPanel { return; }; + let project = pending_assist.project.clone(); + self.inline_prompt_history .retain(|prompt| prompt != user_prompt); self.inline_prompt_history.push_back(user_prompt.into()); @@ -578,14 +648,63 @@ impl AssistantPanel { let codegen_kind = codegen.read(cx).kind().clone(); let user_prompt = user_prompt.to_string(); - let prompt = cx.background().spawn(async move { - let language_name = language_name.as_deref(); - generate_content_prompt(user_prompt, language_name, &buffer, range, codegen_kind) - }); - let mut messages = Vec::new(); + + let snippets = if retrieve_context { + let Some(project) = project.upgrade(cx) else { + return; + }; + + let search_results = if let Some(semantic_index) = self.semantic_index.clone() { + let search_results = semantic_index.update(cx, |this, cx| { + this.search_project(project, user_prompt.to_string(), 10, vec![], vec![], cx) + }); + + cx.background() + .spawn(async move { search_results.await.unwrap_or_default() }) + } else { + Task::ready(Vec::new()) + }; + + let snippets = cx.spawn(|_, cx| async move { + let mut snippets = Vec::new(); + for result in search_results.await { + snippets.push(PromptCodeSnippet::new(result, &cx)); + + // snippets.push(result.buffer.read_with(&cx, |buffer, _| { + // buffer + // .snapshot() + // .text_for_range(result.range) + // .collect::() + // })); + } + snippets + }); + snippets + } else { + Task::ready(Vec::new()) + }; + let mut model = settings::get::(cx) .default_open_ai_model .clone(); + let model_name = model.full_name(); + + let prompt = cx.background().spawn(async move { + let snippets = snippets.await; + + let language_name = language_name.as_deref(); + generate_content_prompt( + user_prompt, + language_name, + &buffer, + range, + codegen_kind, + snippets, + model_name, + ) + }); + + let mut messages = Vec::new(); if let Some(conversation) = conversation { let conversation = conversation.read(cx); let buffer = conversation.buffer.read(cx); @@ -1498,12 +1617,14 @@ impl Conversation { Role::Assistant => "assistant".into(), Role::System => "system".into(), }, - content: self - .buffer - .read(cx) - .text_for_range(message.offset_range) - .collect(), + content: Some( + self.buffer + .read(cx) + .text_for_range(message.offset_range) + .collect(), + ), name: None, + function_call: None, }) }) .collect::>(); @@ -2622,12 +2743,16 @@ enum InlineAssistantEvent { Confirmed { prompt: String, include_conversation: bool, + retrieve_context: bool, }, Canceled, Dismissed, IncludeConversationToggled { include_conversation: bool, }, + RetrieveContextToggled { + retrieve_context: bool, + }, } struct InlineAssistant { @@ -2643,6 +2768,11 @@ struct InlineAssistant { pending_prompt: String, codegen: ModelHandle, _subscriptions: Vec, + retrieve_context: bool, + semantic_index: Option>, + semantic_permissioned: Option, + project: WeakModelHandle, + maintain_rate_limit: Option>, } impl Entity for InlineAssistant { @@ -2659,51 +2789,65 @@ impl View for InlineAssistant { let theme = theme::current(cx); Flex::row() - .with_child( - Flex::row() - .with_child( - Button::action(ToggleIncludeConversation) - .with_tooltip("Include Conversation", theme.tooltip.clone()) + .with_children([Flex::row() + .with_child( + Button::action(ToggleIncludeConversation) + .with_tooltip("Include Conversation", theme.tooltip.clone()) + .with_id(self.id) + .with_contents(theme::components::svg::Svg::new("icons/ai.svg")) + .toggleable(self.include_conversation) + .with_style(theme.assistant.inline.include_conversation.clone()) + .element() + .aligned(), + ) + .with_children(if SemanticIndex::enabled(cx) { + Some( + Button::action(ToggleRetrieveContext) + .with_tooltip("Retrieve Context", theme.tooltip.clone()) .with_id(self.id) - .with_contents(theme::components::svg::Svg::new("icons/ai.svg")) - .toggleable(self.include_conversation) - .with_style(theme.assistant.inline.include_conversation.clone()) + .with_contents(theme::components::svg::Svg::new( + "icons/magnifying_glass.svg", + )) + .toggleable(self.retrieve_context) + .with_style(theme.assistant.inline.retrieve_context.clone()) .element() .aligned(), ) - .with_children(if let Some(error) = self.codegen.read(cx).error() { - Some( - Svg::new("icons/error.svg") - .with_color(theme.assistant.error_icon.color) - .constrained() - .with_width(theme.assistant.error_icon.width) - .contained() - .with_style(theme.assistant.error_icon.container) - .with_tooltip::( - self.id, - error.to_string(), - None, - theme.tooltip.clone(), - cx, - ) - .aligned(), - ) - } else { - None - }) - .aligned() - .constrained() - .dynamically({ - let measurements = self.measurements.clone(); - move |constraint, _, _| { - let measurements = measurements.get(); - SizeConstraint { - min: vec2f(measurements.gutter_width, constraint.min.y()), - max: vec2f(measurements.gutter_width, constraint.max.y()), - } + } else { + None + }) + .with_children(if let Some(error) = self.codegen.read(cx).error() { + Some( + Svg::new("icons/error.svg") + .with_color(theme.assistant.error_icon.color) + .constrained() + .with_width(theme.assistant.error_icon.width) + .contained() + .with_style(theme.assistant.error_icon.container) + .with_tooltip::( + self.id, + error.to_string(), + None, + theme.tooltip.clone(), + cx, + ) + .aligned(), + ) + } else { + None + }) + .aligned() + .constrained() + .dynamically({ + let measurements = self.measurements.clone(); + move |constraint, _, _| { + let measurements = measurements.get(); + SizeConstraint { + min: vec2f(measurements.gutter_width, constraint.min.y()), + max: vec2f(measurements.gutter_width, constraint.max.y()), } - }), - ) + } + })]) .with_child(Empty::new().constrained().dynamically({ let measurements = self.measurements.clone(); move |constraint, _, _| { @@ -2726,6 +2870,16 @@ impl View for InlineAssistant { .left() .flex(1., true), ) + .with_children(if self.retrieve_context { + Some( + Flex::row() + .with_children(self.retrieve_context_status(cx)) + .flex(1., true) + .aligned(), + ) + } else { + None + }) .contained() .with_style(theme.assistant.inline.container) .into_any() @@ -2751,6 +2905,9 @@ impl InlineAssistant { codegen: ModelHandle, workspace: WeakViewHandle, cx: &mut ViewContext, + retrieve_context: bool, + semantic_index: Option>, + project: ModelHandle, ) -> Self { let prompt_editor = cx.add_view(|cx| { let mut editor = Editor::single_line( @@ -2764,11 +2921,16 @@ impl InlineAssistant { editor.set_placeholder_text(placeholder, cx); editor }); - let subscriptions = vec![ + let mut subscriptions = vec![ cx.observe(&codegen, Self::handle_codegen_changed), cx.subscribe(&prompt_editor, Self::handle_prompt_editor_events), ]; - Self { + + if let Some(semantic_index) = semantic_index.clone() { + subscriptions.push(cx.observe(&semantic_index, Self::semantic_index_changed)); + } + + let assistant = Self { id, prompt_editor, workspace, @@ -2781,7 +2943,33 @@ impl InlineAssistant { pending_prompt: String::new(), codegen, _subscriptions: subscriptions, + retrieve_context, + semantic_permissioned: None, + semantic_index, + project: project.downgrade(), + maintain_rate_limit: None, + }; + + assistant.index_project(cx).log_err(); + + assistant + } + + fn semantic_permissioned(&self, cx: &mut ViewContext) -> Task> { + if let Some(value) = self.semantic_permissioned { + return Task::ready(Ok(value)); } + + let Some(project) = self.project.upgrade(cx) else { + return Task::ready(Err(anyhow!("project was dropped"))); + }; + + self.semantic_index + .as_ref() + .map(|semantic| { + semantic.update(cx, |this, cx| this.project_previously_indexed(&project, cx)) + }) + .unwrap_or(Task::ready(Ok(false))) } fn handle_prompt_editor_events( @@ -2796,6 +2984,37 @@ impl InlineAssistant { } } + fn semantic_index_changed( + &mut self, + semantic_index: ModelHandle, + cx: &mut ViewContext, + ) { + let Some(project) = self.project.upgrade(cx) else { + return; + }; + + let status = semantic_index.read(cx).status(&project); + match status { + SemanticIndexStatus::Indexing { + rate_limit_expiry: Some(_), + .. + } => { + if self.maintain_rate_limit.is_none() { + self.maintain_rate_limit = Some(cx.spawn(|this, mut cx| async move { + loop { + cx.background().timer(Duration::from_secs(1)).await; + this.update(&mut cx, |_, cx| cx.notify()).log_err(); + } + })); + } + return; + } + _ => { + self.maintain_rate_limit = None; + } + } + } + fn handle_codegen_changed(&mut self, _: ModelHandle, cx: &mut ViewContext) { let is_read_only = !self.codegen.read(cx).idle(); self.prompt_editor.update(cx, |editor, cx| { @@ -2845,12 +3064,241 @@ impl InlineAssistant { cx.emit(InlineAssistantEvent::Confirmed { prompt, include_conversation: self.include_conversation, + retrieve_context: self.retrieve_context, }); self.confirmed = true; cx.notify(); } } + fn toggle_retrieve_context(&mut self, _: &ToggleRetrieveContext, cx: &mut ViewContext) { + let semantic_permissioned = self.semantic_permissioned(cx); + + let Some(project) = self.project.upgrade(cx) else { + return; + }; + + let project_name = project + .read(cx) + .worktree_root_names(cx) + .collect::>() + .join("/"); + let is_plural = project_name.chars().filter(|letter| *letter == '/').count() > 0; + let prompt_text = format!("Would you like to index the '{}' project{} for context retrieval? This requires sending code to the OpenAI API", project_name, + if is_plural { + "s" + } else {""}); + + cx.spawn(|this, mut cx| async move { + // If Necessary prompt user + if !semantic_permissioned.await.unwrap_or(false) { + let mut answer = this.update(&mut cx, |_, cx| { + cx.prompt( + PromptLevel::Info, + prompt_text.as_str(), + &["Continue", "Cancel"], + ) + })?; + + if answer.next().await == Some(0) { + this.update(&mut cx, |this, _| { + this.semantic_permissioned = Some(true); + })?; + } else { + return anyhow::Ok(()); + } + } + + // If permissioned, update context appropriately + this.update(&mut cx, |this, cx| { + this.retrieve_context = !this.retrieve_context; + + cx.emit(InlineAssistantEvent::RetrieveContextToggled { + retrieve_context: this.retrieve_context, + }); + + if this.retrieve_context { + this.index_project(cx).log_err(); + } + + cx.notify(); + })?; + + anyhow::Ok(()) + }) + .detach_and_log_err(cx); + } + + fn index_project(&self, cx: &mut ViewContext) -> anyhow::Result<()> { + let Some(project) = self.project.upgrade(cx) else { + return Err(anyhow!("project was dropped!")); + }; + + let semantic_permissioned = self.semantic_permissioned(cx); + if let Some(semantic_index) = SemanticIndex::global(cx) { + cx.spawn(|_, mut cx| async move { + // This has to be updated to accomodate for semantic_permissions + if semantic_permissioned.await.unwrap_or(false) { + semantic_index + .update(&mut cx, |index, cx| index.index_project(project, cx)) + .await + } else { + Err(anyhow!("project is not permissioned for semantic indexing")) + } + }) + .detach_and_log_err(cx); + } + + anyhow::Ok(()) + } + + fn retrieve_context_status( + &self, + cx: &mut ViewContext, + ) -> Option> { + enum ContextStatusIcon {} + + let Some(project) = self.project.upgrade(cx) else { + return None; + }; + + if let Some(semantic_index) = SemanticIndex::global(cx) { + let status = semantic_index.update(cx, |index, _| index.status(&project)); + let theme = theme::current(cx); + match status { + SemanticIndexStatus::NotAuthenticated {} => Some( + Svg::new("icons/error.svg") + .with_color(theme.assistant.error_icon.color) + .constrained() + .with_width(theme.assistant.error_icon.width) + .contained() + .with_style(theme.assistant.error_icon.container) + .with_tooltip::( + self.id, + "Not Authenticated. Please ensure you have a valid 'OPENAI_API_KEY' in your environment variables.", + None, + theme.tooltip.clone(), + cx, + ) + .aligned() + .into_any(), + ), + SemanticIndexStatus::NotIndexed {} => Some( + Svg::new("icons/error.svg") + .with_color(theme.assistant.inline.context_status.error_icon.color) + .constrained() + .with_width(theme.assistant.inline.context_status.error_icon.width) + .contained() + .with_style(theme.assistant.inline.context_status.error_icon.container) + .with_tooltip::( + self.id, + "Not Indexed", + None, + theme.tooltip.clone(), + cx, + ) + .aligned() + .into_any(), + ), + SemanticIndexStatus::Indexing { + remaining_files, + rate_limit_expiry, + } => { + + let mut status_text = if remaining_files == 0 { + "Indexing...".to_string() + } else { + format!("Remaining files to index: {remaining_files}") + }; + + if let Some(rate_limit_expiry) = rate_limit_expiry { + let remaining_seconds = rate_limit_expiry.duration_since(Instant::now()); + if remaining_seconds > Duration::from_secs(0) && remaining_files > 0 { + write!( + status_text, + " (rate limit expires in {}s)", + remaining_seconds.as_secs() + ) + .unwrap(); + } + } + Some( + Svg::new("icons/update.svg") + .with_color(theme.assistant.inline.context_status.in_progress_icon.color) + .constrained() + .with_width(theme.assistant.inline.context_status.in_progress_icon.width) + .contained() + .with_style(theme.assistant.inline.context_status.in_progress_icon.container) + .with_tooltip::( + self.id, + status_text, + None, + theme.tooltip.clone(), + cx, + ) + .aligned() + .into_any(), + ) + } + SemanticIndexStatus::Indexed {} => Some( + Svg::new("icons/check.svg") + .with_color(theme.assistant.inline.context_status.complete_icon.color) + .constrained() + .with_width(theme.assistant.inline.context_status.complete_icon.width) + .contained() + .with_style(theme.assistant.inline.context_status.complete_icon.container) + .with_tooltip::( + self.id, + "Index up to date", + None, + theme.tooltip.clone(), + cx, + ) + .aligned() + .into_any(), + ), + } + } else { + None + } + } + + // fn retrieve_context_status(&self, cx: &mut ViewContext) -> String { + // let project = self.project.clone(); + // if let Some(semantic_index) = self.semantic_index.clone() { + // let status = semantic_index.update(cx, |index, cx| index.status(&project)); + // return match status { + // // This theoretically shouldnt be a valid code path + // // As the inline assistant cant be launched without an API key + // // We keep it here for safety + // semantic_index::SemanticIndexStatus::NotAuthenticated => { + // "Not Authenticated!\nPlease ensure you have an `OPENAI_API_KEY` in your environment variables.".to_string() + // } + // semantic_index::SemanticIndexStatus::Indexed => { + // "Indexing Complete!".to_string() + // } + // semantic_index::SemanticIndexStatus::Indexing { remaining_files, rate_limit_expiry } => { + + // let mut status = format!("Remaining files to index for Context Retrieval: {remaining_files}"); + + // if let Some(rate_limit_expiry) = rate_limit_expiry { + // let remaining_seconds = + // rate_limit_expiry.duration_since(Instant::now()); + // if remaining_seconds > Duration::from_secs(0) { + // write!(status, " (rate limit resets in {}s)", remaining_seconds.as_secs()).unwrap(); + // } + // } + // status + // } + // semantic_index::SemanticIndexStatus::NotIndexed => { + // "Not Indexed for Context Retrieval".to_string() + // } + // }; + // } + + // "".to_string() + // } + fn toggle_include_conversation( &mut self, _: &ToggleIncludeConversation, @@ -2913,6 +3361,7 @@ struct PendingInlineAssist { inline_assistant: Option<(BlockId, ViewHandle)>, codegen: ModelHandle, _subscriptions: Vec, + project: WeakModelHandle, } fn merge_ranges(ranges: &mut Vec>, buffer: &MultiBufferSnapshot) { diff --git a/crates/assistant/src/codegen.rs b/crates/assistant/src/codegen.rs index e956d72260..b6ef6b5cfa 100644 --- a/crates/assistant/src/codegen.rs +++ b/crates/assistant/src/codegen.rs @@ -1,9 +1,7 @@ use crate::streaming_diff::{Hunk, StreamingDiff}; use ai::completion::{CompletionProvider, OpenAIRequest}; use anyhow::Result; -use editor::{ - multi_buffer, Anchor, AnchorRangeExt, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint, -}; +use editor::{multi_buffer, Anchor, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint}; use futures::{channel::mpsc, SinkExt, Stream, StreamExt}; use gpui::{Entity, ModelContext, ModelHandle, Task}; use language::{Rope, TransactionId}; @@ -40,26 +38,11 @@ impl Entity for Codegen { impl Codegen { pub fn new( buffer: ModelHandle, - mut kind: CodegenKind, + kind: CodegenKind, provider: Arc, cx: &mut ModelContext, ) -> Self { let snapshot = buffer.read(cx).snapshot(cx); - match &mut kind { - CodegenKind::Transform { range } => { - let mut point_range = range.to_point(&snapshot); - point_range.start.column = 0; - if point_range.end.column > 0 || point_range.start.row == point_range.end.row { - point_range.end.column = snapshot.line_len(point_range.end.row); - } - range.start = snapshot.anchor_before(point_range.start); - range.end = snapshot.anchor_after(point_range.end); - } - CodegenKind::Generate { position } => { - *position = position.bias_right(&snapshot); - } - } - Self { provider, buffer: buffer.clone(), @@ -386,7 +369,7 @@ mod tests { let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); let range = buffer.read_with(cx, |buffer, cx| { let snapshot = buffer.snapshot(cx); - snapshot.anchor_before(Point::new(1, 4))..snapshot.anchor_after(Point::new(4, 4)) + snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(4, 5)) }); let provider = Arc::new(TestCompletionProvider::new()); let codegen = cx.add_model(|cx| { diff --git a/crates/assistant/src/prompts.rs b/crates/assistant/src/prompts.rs index bf041dff52..18e9e18f7d 100644 --- a/crates/assistant/src/prompts.rs +++ b/crates/assistant/src/prompts.rs @@ -1,9 +1,62 @@ use crate::codegen::CodegenKind; +use gpui::AsyncAppContext; use language::{BufferSnapshot, OffsetRangeExt, ToOffset}; +use semantic_index::SearchResult; use std::cmp::{self, Reverse}; use std::fmt::Write; use std::ops::Range; +use std::path::PathBuf; +use tiktoken_rs::ChatCompletionRequestMessage; +pub struct PromptCodeSnippet { + path: Option, + language_name: Option, + content: String, +} + +impl PromptCodeSnippet { + pub fn new(search_result: SearchResult, cx: &AsyncAppContext) -> Self { + let (content, language_name, file_path) = + search_result.buffer.read_with(cx, |buffer, _| { + let snapshot = buffer.snapshot(); + let content = snapshot + .text_for_range(search_result.range.clone()) + .collect::(); + + let language_name = buffer + .language() + .and_then(|language| Some(language.name().to_string())); + + let file_path = buffer + .file() + .and_then(|file| Some(file.path().to_path_buf())); + + (content, language_name, file_path) + }); + + PromptCodeSnippet { + path: file_path, + language_name, + content, + } + } +} + +impl ToString for PromptCodeSnippet { + fn to_string(&self) -> String { + let path = self + .path + .as_ref() + .and_then(|path| Some(path.to_string_lossy().to_string())) + .unwrap_or("".to_string()); + let language_name = self.language_name.clone().unwrap_or("".to_string()); + let content = self.content.clone(); + + format!("The below code snippet may be relevant from file: {path}\n```{language_name}\n{content}\n```") + } +} + +#[allow(dead_code)] fn summarize(buffer: &BufferSnapshot, selected_range: Range) -> String { #[derive(Debug)] struct Match { @@ -120,70 +173,135 @@ pub fn generate_content_prompt( buffer: &BufferSnapshot, range: Range, kind: CodegenKind, + search_results: Vec, + model: &str, ) -> String { - let mut prompt = String::new(); + const MAXIMUM_SNIPPET_TOKEN_COUNT: usize = 500; + const RESERVED_TOKENS_FOR_GENERATION: usize = 1000; + + let mut prompts = Vec::new(); + let range = range.to_offset(buffer); // General Preamble if let Some(language_name) = language_name { - writeln!(prompt, "You're an expert {language_name} engineer.\n").unwrap(); + prompts.push(format!("You're an expert {language_name} engineer.\n")); } else { - writeln!(prompt, "You're an expert engineer.\n").unwrap(); + prompts.push("You're an expert engineer.\n".to_string()); } - let outline = summarize(buffer, range); - writeln!( - prompt, - "The file you are currently working on has the following outline:" - ) - .unwrap(); + // Snippets + let mut snippet_position = prompts.len() - 1; + + let mut content = String::new(); + content.extend(buffer.text_for_range(0..range.start)); + if range.start == range.end { + content.push_str("<|START|>"); + } else { + content.push_str("<|START|"); + } + content.extend(buffer.text_for_range(range.clone())); + if range.start != range.end { + content.push_str("|END|>"); + } + content.extend(buffer.text_for_range(range.end..buffer.len())); + + prompts.push("The file you are currently working on has the following content:\n".to_string()); + if let Some(language_name) = language_name { let language_name = language_name.to_lowercase(); - writeln!(prompt, "```{language_name}\n{outline}\n```").unwrap(); + prompts.push(format!("```{language_name}\n{content}\n```")); } else { - writeln!(prompt, "```\n{outline}\n```").unwrap(); + prompts.push(format!("```\n{content}\n```")); } match kind { CodegenKind::Generate { position: _ } => { - writeln!(prompt, "In particular, the user's cursor is current on the '<|START|>' span in the above outline, with no text selected.").unwrap(); - writeln!( - prompt, - "Assume the cursor is located where the `<|START|` marker is." - ) - .unwrap(); - writeln!( - prompt, + prompts.push("In particular, the user's cursor is currently on the '<|START|>' span in the above outline, with no text selected.".to_string()); + prompts + .push("Assume the cursor is located where the `<|START|` marker is.".to_string()); + prompts.push( "Text can't be replaced, so assume your answer will be inserted at the cursor." - ) - .unwrap(); - writeln!( - prompt, + .to_string(), + ); + prompts.push(format!( "Generate text based on the users prompt: {user_prompt}" - ) - .unwrap(); + )); } CodegenKind::Transform { range: _ } => { - writeln!(prompt, "In particular, the user has selected a section of the text between the '<|START|' and '|END|>' spans.").unwrap(); - writeln!( - prompt, - "Modify the users code selected text based upon the users prompt: {user_prompt}" - ) - .unwrap(); - writeln!( - prompt, - "You MUST reply with only the adjusted code (within the '<|START|' and '|END|>' spans), not the entire file." - ) - .unwrap(); + prompts.push("In particular, the user has selected a section of the text between the '<|START|' and '|END|>' spans.".to_string()); + prompts.push(format!( + "Modify the users code selected text based upon the users prompt: '{user_prompt}'" + )); + prompts.push("You MUST reply with only the adjusted code (within the '<|START|' and '|END|>' spans), not the entire file.".to_string()); } } if let Some(language_name) = language_name { - writeln!(prompt, "Your answer MUST always be valid {language_name}").unwrap(); + prompts.push(format!( + "Your answer MUST always and only be valid {language_name}" + )); } - writeln!(prompt, "Always wrap your response in a Markdown codeblock").unwrap(); - writeln!(prompt, "Never make remarks about the output.").unwrap(); + prompts.push("Never make remarks about the output.".to_string()); + prompts.push("Do not return any text, except the generated code.".to_string()); + prompts.push("Always wrap your code in a Markdown block".to_string()); - prompt + let current_messages = [ChatCompletionRequestMessage { + role: "user".to_string(), + content: Some(prompts.join("\n")), + function_call: None, + name: None, + }]; + + let mut remaining_token_count = if let Ok(current_token_count) = + tiktoken_rs::num_tokens_from_messages(model, ¤t_messages) + { + let max_token_count = tiktoken_rs::model::get_context_size(model); + let intermediate_token_count = if max_token_count > current_token_count { + max_token_count - current_token_count + } else { + 0 + }; + + if intermediate_token_count < RESERVED_TOKENS_FOR_GENERATION { + 0 + } else { + intermediate_token_count - RESERVED_TOKENS_FOR_GENERATION + } + } else { + // If tiktoken fails to count token count, assume we have no space remaining. + 0 + }; + + // TODO: + // - add repository name to snippet + // - add file path + // - add language + if let Ok(encoding) = tiktoken_rs::get_bpe_from_model(model) { + let mut template = "You are working inside a large repository, here are a few code snippets that may be useful"; + + for search_result in search_results { + let mut snippet_prompt = template.to_string(); + let snippet = search_result.to_string(); + writeln!(snippet_prompt, "```\n{snippet}\n```").unwrap(); + + let token_count = encoding + .encode_with_special_tokens(snippet_prompt.as_str()) + .len(); + if token_count <= remaining_token_count { + if token_count < MAXIMUM_SNIPPET_TOKEN_COUNT { + prompts.insert(snippet_position, snippet_prompt); + snippet_position += 1; + remaining_token_count -= token_count; + // If you have already added the template to the prompt, remove the template. + template = ""; + } + } else { + break; + } + } + } + + prompts.join("\n") } #[cfg(test)] diff --git a/crates/call/src/room.rs b/crates/call/src/room.rs index 43354fd5a2..4e52f57f60 100644 --- a/crates/call/src/room.rs +++ b/crates/call/src/room.rs @@ -18,7 +18,7 @@ use live_kit_client::{ LocalAudioTrack, LocalTrackPublication, LocalVideoTrack, RemoteAudioTrackUpdate, RemoteVideoTrackUpdate, }; -use postage::stream::Stream; +use postage::{sink::Sink, stream::Stream, watch}; use project::Project; use std::{future::Future, mem, pin::Pin, sync::Arc, time::Duration}; use util::{post_inc, ResultExt, TryFutureExt}; @@ -70,6 +70,8 @@ pub struct Room { user_store: ModelHandle, follows_by_leader_id_project_id: HashMap<(PeerId, u64), Vec>, subscriptions: Vec, + room_update_completed_tx: watch::Sender>, + room_update_completed_rx: watch::Receiver>, pending_room_update: Option>, maintain_connection: Option>>, } @@ -211,6 +213,8 @@ impl Room { Audio::play_sound(Sound::Joined, cx); + let (room_update_completed_tx, room_update_completed_rx) = watch::channel(); + Self { id, channel_id, @@ -230,6 +234,8 @@ impl Room { user_store, follows_by_leader_id_project_id: Default::default(), maintain_connection: Some(maintain_connection), + room_update_completed_tx, + room_update_completed_rx, } } @@ -599,7 +605,7 @@ impl Room { } /// Returns the most 'active' projects, defined as most people in the project - pub fn most_active_project(&self) -> Option<(u64, u64)> { + pub fn most_active_project(&self, cx: &AppContext) -> Option<(u64, u64)> { let mut project_hosts_and_guest_counts = HashMap::, u32)>::default(); for participant in self.remote_participants.values() { match participant.location { @@ -619,6 +625,15 @@ impl Room { } } + if let Some(user) = self.user_store.read(cx).current_user() { + for project in &self.local_participant.projects { + project_hosts_and_guest_counts + .entry(project.id) + .or_default() + .0 = Some(user.id); + } + } + project_hosts_and_guest_counts .into_iter() .filter_map(|(id, (host, guest_count))| Some((id, host?, guest_count))) @@ -858,6 +873,7 @@ impl Room { }); this.check_invariants(); + this.room_update_completed_tx.try_send(Some(())).ok(); cx.notify(); }); })); @@ -866,6 +882,17 @@ impl Room { Ok(()) } + pub fn room_update_completed(&mut self) -> impl Future { + let mut done_rx = self.room_update_completed_rx.clone(); + async move { + while let Some(result) = done_rx.next().await { + if result.is_some() { + break; + } + } + } + } + fn remote_video_track_updated( &mut self, change: RemoteVideoTrackUpdate, diff --git a/crates/channel/src/channel_buffer.rs b/crates/channel/src/channel_buffer.rs index 7de8b956f1..ab7ea78ac1 100644 --- a/crates/channel/src/channel_buffer.rs +++ b/crates/channel/src/channel_buffer.rs @@ -99,6 +99,10 @@ impl ChannelBuffer { })) } + pub fn remote_id(&self, cx: &AppContext) -> u64 { + self.buffer.read(cx).remote_id() + } + pub fn user_store(&self) -> &ModelHandle { &self.user_store } diff --git a/crates/channel/src/channel_store.rs b/crates/channel/src/channel_store.rs index 8cdd11b4ec..9c80dcc2b7 100644 --- a/crates/channel/src/channel_store.rs +++ b/crates/channel/src/channel_store.rs @@ -5,10 +5,11 @@ use anyhow::{anyhow, Result}; use channel_index::ChannelIndex; use client::{Client, Subscription, User, UserId, UserStore}; use collections::{hash_map, HashMap, HashSet}; +use db::RELEASE_CHANNEL; use futures::{channel::mpsc, future::Shared, Future, FutureExt, StreamExt}; use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task, WeakModelHandle}; use rpc::{ - proto::{self, ChannelEdge, ChannelPermission}, + proto::{self, ChannelEdge, ChannelPermission, ChannelRole, ChannelVisibility}, TypedEnvelope, }; use serde_derive::{Deserialize, Serialize}; @@ -48,17 +49,63 @@ pub type ChannelData = (Channel, ChannelPath); pub struct Channel { pub id: ChannelId, pub name: String, + pub visibility: proto::ChannelVisibility, pub unseen_note_version: Option<(u64, clock::Global)>, pub unseen_message_id: Option, } +impl Channel { + pub fn link(&self) -> String { + RELEASE_CHANNEL.link_prefix().to_owned() + + "channel/" + + &self.slug() + + "-" + + &self.id.to_string() + } + + pub fn slug(&self) -> String { + let slug: String = self + .name + .chars() + .map(|c| if c.is_alphanumeric() { c } else { '-' }) + .collect(); + + slug.trim_matches(|c| c == '-').to_string() + } +} + #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Serialize, Deserialize)] pub struct ChannelPath(Arc<[ChannelId]>); pub struct ChannelMembership { pub user: Arc, pub kind: proto::channel_member::Kind, - pub admin: bool, + pub role: proto::ChannelRole, +} +impl ChannelMembership { + pub fn sort_key(&self) -> MembershipSortKey { + MembershipSortKey { + role_order: match self.role { + proto::ChannelRole::Admin => 0, + proto::ChannelRole::Member => 1, + proto::ChannelRole::Banned => 2, + proto::ChannelRole::Guest => 3, + }, + kind_order: match self.kind { + proto::channel_member::Kind::Member => 0, + proto::channel_member::Kind::AncestorMember => 1, + proto::channel_member::Kind::Invitee => 2, + }, + username_order: self.user.github_login.as_str(), + } + } +} + +#[derive(PartialOrd, Ord, PartialEq, Eq)] +pub struct MembershipSortKey<'a> { + role_order: u8, + kind_order: u8, + username_order: &'a str, } pub enum ChannelEvent { @@ -93,12 +140,21 @@ impl ChannelStore { let watch_connection_status = cx.spawn_weak(|this, mut cx| async move { while let Some(status) = connection_status.next().await { let this = this.upgrade(&cx)?; + match status { + client::Status::Connected { .. } => { + this.update(&mut cx, |this, cx| this.handle_connect(cx)) + .await + .log_err()?; + } + client::Status::SignedOut | client::Status::UpgradeRequired => { + this.update(&mut cx, |this, cx| this.handle_disconnect(false, cx)); + } + _ => { + this.update(&mut cx, |this, cx| this.handle_disconnect(true, cx)); + } + } if status.is_connected() { - this.update(&mut cx, |this, cx| this.handle_connect(cx)) - .await - .log_err()?; } else { - this.update(&mut cx, |this, cx| this.handle_disconnect(cx)); } } Some(()) @@ -415,7 +471,7 @@ impl ChannelStore { insert_edge: parent_edge, channel_permissions: vec![ChannelPermission { channel_id, - is_admin: true, + role: ChannelRole::Admin.into(), }], ..Default::default() }, @@ -487,11 +543,30 @@ impl ChannelStore { }) } + pub fn set_channel_visibility( + &mut self, + channel_id: ChannelId, + visibility: ChannelVisibility, + cx: &mut ModelContext, + ) -> Task> { + let client = self.client.clone(); + cx.spawn(|_, _| async move { + let _ = client + .request(proto::SetChannelVisibility { + channel_id, + visibility: visibility.into(), + }) + .await?; + + Ok(()) + }) + } + pub fn invite_member( &mut self, channel_id: ChannelId, user_id: UserId, - admin: bool, + role: proto::ChannelRole, cx: &mut ModelContext, ) -> Task> { if !self.outgoing_invites.insert((channel_id, user_id)) { @@ -505,7 +580,7 @@ impl ChannelStore { .request(proto::InviteChannelMember { channel_id, user_id, - admin, + role: role.into(), }) .await; @@ -549,11 +624,11 @@ impl ChannelStore { }) } - pub fn set_member_admin( + pub fn set_member_role( &mut self, channel_id: ChannelId, user_id: UserId, - admin: bool, + role: proto::ChannelRole, cx: &mut ModelContext, ) -> Task> { if !self.outgoing_invites.insert((channel_id, user_id)) { @@ -564,10 +639,10 @@ impl ChannelStore { let client = self.client.clone(); cx.spawn(|this, mut cx| async move { let result = client - .request(proto::SetChannelMemberAdmin { + .request(proto::SetChannelMemberRole { channel_id, user_id, - admin, + role: role.into(), }) .await; @@ -655,8 +730,8 @@ impl ChannelStore { .filter_map(|(user, member)| { Some(ChannelMembership { user, - admin: member.admin, - kind: proto::channel_member::Kind::from_i32(member.kind)?, + role: member.role(), + kind: member.kind(), }) }) .collect()) @@ -802,7 +877,7 @@ impl ChannelStore { }) } - fn handle_disconnect(&mut self, cx: &mut ModelContext) { + fn handle_disconnect(&mut self, wait_for_reconnect: bool, cx: &mut ModelContext) { self.channel_index.clear(); self.channel_invitations.clear(); self.channel_participants.clear(); @@ -813,7 +888,10 @@ impl ChannelStore { self.disconnect_channel_buffers_task.get_or_insert_with(|| { cx.spawn_weak(|this, mut cx| async move { - cx.background().timer(RECONNECT_TIMEOUT).await; + if wait_for_reconnect { + cx.background().timer(RECONNECT_TIMEOUT).await; + } + if let Some(this) = this.upgrade(&cx) { this.update(&mut cx, |this, cx| { for (_, buffer) in this.opened_buffers.drain() { @@ -848,6 +926,7 @@ impl ChannelStore { ix, Arc::new(Channel { id: channel.id, + visibility: channel.visibility(), name: channel.name, unseen_note_version: None, unseen_message_id: None, @@ -914,7 +993,7 @@ impl ChannelStore { } for permission in payload.channel_permissions { - if permission.is_admin { + if permission.role() == proto::ChannelRole::Admin { self.channels_with_admin_privileges .insert(permission.channel_id); } else { diff --git a/crates/channel/src/channel_store/channel_index.rs b/crates/channel/src/channel_store/channel_index.rs index bf0de1b644..36379a3942 100644 --- a/crates/channel/src/channel_store/channel_index.rs +++ b/crates/channel/src/channel_store/channel_index.rs @@ -123,12 +123,15 @@ impl<'a> ChannelPathsInsertGuard<'a> { pub fn insert(&mut self, channel_proto: proto::Channel) { if let Some(existing_channel) = self.channels_by_id.get_mut(&channel_proto.id) { - Arc::make_mut(existing_channel).name = channel_proto.name; + let existing_channel = Arc::make_mut(existing_channel); + existing_channel.visibility = channel_proto.visibility(); + existing_channel.name = channel_proto.name; } else { self.channels_by_id.insert( channel_proto.id, Arc::new(Channel { id: channel_proto.id, + visibility: channel_proto.visibility(), name: channel_proto.name, unseen_note_version: None, unseen_message_id: None, diff --git a/crates/channel/src/channel_store_tests.rs b/crates/channel/src/channel_store_tests.rs index 9303a52092..23f2e11a03 100644 --- a/crates/channel/src/channel_store_tests.rs +++ b/crates/channel/src/channel_store_tests.rs @@ -3,7 +3,7 @@ use crate::channel_chat::ChannelChatEvent; use super::*; use client::{test::FakeServer, Client, UserStore}; use gpui::{AppContext, ModelHandle, TestAppContext}; -use rpc::proto; +use rpc::proto::{self}; use settings::SettingsStore; use util::http::FakeHttpClient; @@ -18,15 +18,17 @@ fn test_update_channels(cx: &mut AppContext) { proto::Channel { id: 1, name: "b".to_string(), + visibility: proto::ChannelVisibility::Members as i32, }, proto::Channel { id: 2, name: "a".to_string(), + visibility: proto::ChannelVisibility::Members as i32, }, ], channel_permissions: vec![proto::ChannelPermission { channel_id: 1, - is_admin: true, + role: proto::ChannelRole::Admin.into(), }], ..Default::default() }, @@ -49,10 +51,12 @@ fn test_update_channels(cx: &mut AppContext) { proto::Channel { id: 3, name: "x".to_string(), + visibility: proto::ChannelVisibility::Members as i32, }, proto::Channel { id: 4, name: "y".to_string(), + visibility: proto::ChannelVisibility::Members as i32, }, ], insert_edge: vec![ @@ -92,14 +96,17 @@ fn test_dangling_channel_paths(cx: &mut AppContext) { proto::Channel { id: 0, name: "a".to_string(), + visibility: proto::ChannelVisibility::Members as i32, }, proto::Channel { id: 1, name: "b".to_string(), + visibility: proto::ChannelVisibility::Members as i32, }, proto::Channel { id: 2, name: "c".to_string(), + visibility: proto::ChannelVisibility::Members as i32, }, ], insert_edge: vec![ @@ -114,7 +121,7 @@ fn test_dangling_channel_paths(cx: &mut AppContext) { ], channel_permissions: vec![proto::ChannelPermission { channel_id: 0, - is_admin: true, + role: proto::ChannelRole::Admin.into(), }], ..Default::default() }, @@ -158,6 +165,7 @@ async fn test_channel_messages(cx: &mut TestAppContext) { channels: vec![proto::Channel { id: channel_id, name: "the-channel".to_string(), + visibility: proto::ChannelVisibility::Members as i32, }], ..Default::default() }); diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 2f742814a8..69cfb7102b 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -182,6 +182,7 @@ impl Bundle { kCFStringEncodingUTF8, ptr::null(), )); + // equivalent to: open zed-cli:... -a /Applications/Zed\ Preview.app let urls_to_open = CFArray::from_copyable(&[url_to_open.as_concrete_TypeRef()]); LSOpenFromURLSpec( &LSLaunchURLSpec { diff --git a/crates/client/src/telemetry.rs b/crates/client/src/telemetry.rs index 70878bf2e4..fd93aaeec8 100644 --- a/crates/client/src/telemetry.rs +++ b/crates/client/src/telemetry.rs @@ -4,7 +4,9 @@ use lazy_static::lazy_static; use parking_lot::Mutex; use serde::Serialize; use std::{env, io::Write, mem, path::PathBuf, sync::Arc, time::Duration}; -use sysinfo::{Pid, PidExt, ProcessExt, System, SystemExt}; +use sysinfo::{ + CpuRefreshKind, Pid, PidExt, ProcessExt, ProcessRefreshKind, RefreshKind, System, SystemExt, +}; use tempfile::NamedTempFile; use util::http::HttpClient; use util::{channel::ReleaseChannel, TryFutureExt}; @@ -166,8 +168,16 @@ impl Telemetry { let this = self.clone(); cx.spawn(|mut cx| async move { - let mut system = System::new_all(); - system.refresh_all(); + // Avoiding calling `System::new_all()`, as there have been crashes related to it + let refresh_kind = RefreshKind::new() + .with_memory() // For memory usage + .with_processes(ProcessRefreshKind::everything()) // For process usage + .with_cpu(CpuRefreshKind::everything()); // For core count + + let mut system = System::new_with_specifics(refresh_kind); + + // Avoiding calling `refresh_all()`, just update what we need + system.refresh_specifics(refresh_kind); loop { // Waiting some amount of time before the first query is important to get a reasonable value @@ -175,8 +185,7 @@ impl Telemetry { const DURATION_BETWEEN_SYSTEM_EVENTS: Duration = Duration::from_secs(60); smol::Timer::after(DURATION_BETWEEN_SYSTEM_EVENTS).await; - system.refresh_memory(); - system.refresh_processes(); + system.refresh_specifics(refresh_kind); let current_process = Pid::from_u32(std::process::id()); let Some(process) = system.processes().get(¤t_process) else { diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index 6177c23620..bc6e09f3bd 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -3,7 +3,7 @@ authors = ["Nathan Sobo "] default-run = "collab" edition = "2021" name = "collab" -version = "0.23.3" +version = "0.25.0" publish = false [[bin]] @@ -72,6 +72,7 @@ fs = { path = "../fs", features = ["test-support"] } git = { path = "../git", features = ["test-support"] } live_kit_client = { path = "../live_kit_client", features = ["test-support"] } lsp = { path = "../lsp", features = ["test-support"] } +node_runtime = { path = "../node_runtime" } project = { path = "../project", features = ["test-support"] } rpc = { path = "../rpc", features = ["test-support"] } settings = { path = "../settings", features = ["test-support"] } diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql index 2d963ff15f..8eb6b52fd8 100644 --- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql +++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql @@ -37,12 +37,14 @@ CREATE INDEX "index_contacts_user_id_b" ON "contacts" ("user_id_b"); CREATE TABLE "rooms" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT, "live_kit_room" VARCHAR NOT NULL, + "enviroment" VARCHAR, "channel_id" INTEGER REFERENCES channels (id) ON DELETE CASCADE ); +CREATE UNIQUE INDEX "index_rooms_on_channel_id" ON "rooms" ("channel_id"); CREATE TABLE "projects" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT, - "room_id" INTEGER REFERENCES rooms (id) NOT NULL, + "room_id" INTEGER REFERENCES rooms (id) ON DELETE CASCADE NOT NULL, "host_user_id" INTEGER REFERENCES users (id) NOT NULL, "host_connection_id" INTEGER, "host_connection_server_id" INTEGER REFERENCES servers (id) ON DELETE CASCADE, @@ -190,7 +192,8 @@ CREATE INDEX "index_followers_on_room_id" ON "followers" ("room_id"); CREATE TABLE "channels" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT, "name" VARCHAR NOT NULL, - "created_at" TIMESTAMP NOT NULL DEFAULT now + "created_at" TIMESTAMP NOT NULL DEFAULT now, + "visibility" VARCHAR NOT NULL ); CREATE TABLE IF NOT EXISTS "channel_chat_participants" ( @@ -224,6 +227,7 @@ CREATE TABLE "channel_members" ( "channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE, "user_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE, "admin" BOOLEAN NOT NULL DEFAULT false, + "role" VARCHAR, "accepted" BOOLEAN NOT NULL DEFAULT false, "updated_at" TIMESTAMP NOT NULL DEFAULT now ); diff --git a/crates/collab/migrations/20231009181554_add_release_channel_to_rooms.sql b/crates/collab/migrations/20231009181554_add_release_channel_to_rooms.sql new file mode 100644 index 0000000000..8f3a704add --- /dev/null +++ b/crates/collab/migrations/20231009181554_add_release_channel_to_rooms.sql @@ -0,0 +1 @@ +ALTER TABLE rooms ADD COLUMN enviroment TEXT; diff --git a/crates/collab/migrations/20231010114600_add_unique_index_on_rooms_channel_id.sql b/crates/collab/migrations/20231010114600_add_unique_index_on_rooms_channel_id.sql new file mode 100644 index 0000000000..21ec4cfbb7 --- /dev/null +++ b/crates/collab/migrations/20231010114600_add_unique_index_on_rooms_channel_id.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "index_rooms_on_channel_id" ON "rooms" ("channel_id"); diff --git a/crates/collab/migrations/20231011214412_add_guest_role.sql b/crates/collab/migrations/20231011214412_add_guest_role.sql new file mode 100644 index 0000000000..1713547158 --- /dev/null +++ b/crates/collab/migrations/20231011214412_add_guest_role.sql @@ -0,0 +1,4 @@ +ALTER TABLE channel_members ADD COLUMN role TEXT; +UPDATE channel_members SET role = CASE WHEN admin THEN 'admin' ELSE 'member' END; + +ALTER TABLE channels ADD COLUMN visibility TEXT NOT NULL DEFAULT 'members'; diff --git a/crates/collab/migrations/20231017185833_projects_room_id_fkey_on_delete_cascade.sql b/crates/collab/migrations/20231017185833_projects_room_id_fkey_on_delete_cascade.sql new file mode 100644 index 0000000000..be535ff7fa --- /dev/null +++ b/crates/collab/migrations/20231017185833_projects_room_id_fkey_on_delete_cascade.sql @@ -0,0 +1,8 @@ +-- Add migration script here + +ALTER TABLE projects + DROP CONSTRAINT projects_room_id_fkey, + ADD CONSTRAINT projects_room_id_fkey + FOREIGN KEY (room_id) + REFERENCES rooms (id) + ON DELETE CASCADE; diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index e60b7cc33d..08f78c685d 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -432,6 +432,7 @@ pub struct NewUserResult { pub struct Channel { pub id: ChannelId, pub name: String, + pub visibility: ChannelVisibility, } #[derive(Debug, PartialEq)] diff --git a/crates/collab/src/db/ids.rs b/crates/collab/src/db/ids.rs index 23bb9e53bf..f0de4c255e 100644 --- a/crates/collab/src/db/ids.rs +++ b/crates/collab/src/db/ids.rs @@ -1,4 +1,5 @@ use crate::Result; +use rpc::proto; use sea_orm::{entity::prelude::*, DbErr}; use serde::{Deserialize, Serialize}; @@ -80,3 +81,101 @@ id_type!(SignupId); id_type!(UserId); id_type!(ChannelBufferCollaboratorId); id_type!(FlagId); + +#[derive(Eq, PartialEq, Copy, Clone, Debug, EnumIter, DeriveActiveEnum, Default)] +#[sea_orm(rs_type = "String", db_type = "String(None)")] +pub enum ChannelRole { + #[sea_orm(string_value = "admin")] + Admin, + #[sea_orm(string_value = "member")] + #[default] + Member, + #[sea_orm(string_value = "guest")] + Guest, + #[sea_orm(string_value = "banned")] + Banned, +} + +impl ChannelRole { + pub fn should_override(&self, other: Self) -> bool { + use ChannelRole::*; + match self { + Admin => matches!(other, Member | Banned | Guest), + Member => matches!(other, Banned | Guest), + Banned => matches!(other, Guest), + Guest => false, + } + } + + pub fn max(&self, other: Self) -> Self { + if self.should_override(other) { + *self + } else { + other + } + } +} + +impl From for ChannelRole { + fn from(value: proto::ChannelRole) -> Self { + match value { + proto::ChannelRole::Admin => ChannelRole::Admin, + proto::ChannelRole::Member => ChannelRole::Member, + proto::ChannelRole::Guest => ChannelRole::Guest, + proto::ChannelRole::Banned => ChannelRole::Banned, + } + } +} + +impl Into for ChannelRole { + fn into(self) -> proto::ChannelRole { + match self { + ChannelRole::Admin => proto::ChannelRole::Admin, + ChannelRole::Member => proto::ChannelRole::Member, + ChannelRole::Guest => proto::ChannelRole::Guest, + ChannelRole::Banned => proto::ChannelRole::Banned, + } + } +} + +impl Into for ChannelRole { + fn into(self) -> i32 { + let proto: proto::ChannelRole = self.into(); + proto.into() + } +} + +#[derive(Eq, PartialEq, Copy, Clone, Debug, EnumIter, DeriveActiveEnum, Default, Hash)] +#[sea_orm(rs_type = "String", db_type = "String(None)")] +pub enum ChannelVisibility { + #[sea_orm(string_value = "public")] + Public, + #[sea_orm(string_value = "members")] + #[default] + Members, +} + +impl From for ChannelVisibility { + fn from(value: proto::ChannelVisibility) -> Self { + match value { + proto::ChannelVisibility::Public => ChannelVisibility::Public, + proto::ChannelVisibility::Members => ChannelVisibility::Members, + } + } +} + +impl Into for ChannelVisibility { + fn into(self) -> proto::ChannelVisibility { + match self { + ChannelVisibility::Public => proto::ChannelVisibility::Public, + ChannelVisibility::Members => proto::ChannelVisibility::Members, + } + } +} + +impl Into for ChannelVisibility { + fn into(self) -> i32 { + let proto: proto::ChannelVisibility = self.into(); + proto.into() + } +} diff --git a/crates/collab/src/db/queries/buffers.rs b/crates/collab/src/db/queries/buffers.rs index c85432f2bb..69f100e6b8 100644 --- a/crates/collab/src/db/queries/buffers.rs +++ b/crates/collab/src/db/queries/buffers.rs @@ -482,7 +482,9 @@ impl Database { ) .await?; - channel_members = self.get_channel_members_internal(channel_id, &*tx).await?; + channel_members = self + .get_channel_participants_internal(channel_id, &*tx) + .await?; let collaborators = self .get_channel_buffer_collaborators_internal(channel_id, &*tx) .await?; diff --git a/crates/collab/src/db/queries/channels.rs b/crates/collab/src/db/queries/channels.rs index ab31f59541..ee989b2ea0 100644 --- a/crates/collab/src/db/queries/channels.rs +++ b/crates/collab/src/db/queries/channels.rs @@ -1,8 +1,5 @@ use super::*; -use rpc::proto::ChannelEdge; -use smallvec::SmallVec; - -type ChannelDescendants = HashMap>; +use rpc::proto::{channel_member::Kind, ChannelEdge}; impl Database { #[cfg(test)] @@ -19,21 +16,14 @@ impl Database { .await } - pub async fn create_root_channel( - &self, - name: &str, - live_kit_room: &str, - creator_id: UserId, - ) -> Result { - self.create_channel(name, None, live_kit_room, creator_id) - .await + pub async fn create_root_channel(&self, name: &str, creator_id: UserId) -> Result { + self.create_channel(name, None, creator_id).await } pub async fn create_channel( &self, name: &str, parent: Option, - live_kit_room: &str, creator_id: UserId, ) -> Result { let name = Self::sanitize_channel_name(name)?; @@ -44,8 +34,9 @@ impl Database { } let channel = channel::ActiveModel { + id: ActiveValue::NotSet, name: ActiveValue::Set(name.to_string()), - ..Default::default() + visibility: ActiveValue::Set(ChannelVisibility::Members), } .insert(&*tx) .await?; @@ -81,19 +72,11 @@ impl Database { } channel_member::ActiveModel { + id: ActiveValue::NotSet, channel_id: ActiveValue::Set(channel.id), user_id: ActiveValue::Set(creator_id), accepted: ActiveValue::Set(true), - admin: ActiveValue::Set(true), - ..Default::default() - } - .insert(&*tx) - .await?; - - room::ActiveModel { - channel_id: ActiveValue::Set(Some(channel.id)), - live_kit_room: ActiveValue::Set(live_kit_room.to_string()), - ..Default::default() + role: ActiveValue::Set(ChannelRole::Admin), } .insert(&*tx) .await?; @@ -103,6 +86,116 @@ impl Database { .await } + pub async fn join_channel( + &self, + channel_id: ChannelId, + user_id: UserId, + connection: ConnectionId, + environment: &str, + ) -> Result<(JoinRoom, Option)> { + self.transaction(move |tx| async move { + let mut joined_channel_id = None; + + let channel = channel::Entity::find() + .filter(channel::Column::Id.eq(channel_id)) + .one(&*tx) + .await?; + + let mut role = self + .channel_role_for_user(channel_id, user_id, &*tx) + .await?; + + if role.is_none() && channel.is_some() { + if let Some(invitation) = self + .pending_invite_for_channel(channel_id, user_id, &*tx) + .await? + { + // note, this may be a parent channel + joined_channel_id = Some(invitation.channel_id); + role = Some(invitation.role); + + channel_member::Entity::update(channel_member::ActiveModel { + accepted: ActiveValue::Set(true), + ..invitation.into_active_model() + }) + .exec(&*tx) + .await?; + + debug_assert!( + self.channel_role_for_user(channel_id, user_id, &*tx) + .await? + == role + ); + } + } + if role.is_none() + && channel.as_ref().map(|c| c.visibility) == Some(ChannelVisibility::Public) + { + let channel_id_to_join = self + .most_public_ancestor_for_channel(channel_id, &*tx) + .await? + .unwrap_or(channel_id); + // TODO: change this back to Guest. + role = Some(ChannelRole::Member); + joined_channel_id = Some(channel_id_to_join); + + channel_member::Entity::insert(channel_member::ActiveModel { + id: ActiveValue::NotSet, + channel_id: ActiveValue::Set(channel_id_to_join), + user_id: ActiveValue::Set(user_id), + accepted: ActiveValue::Set(true), + // TODO: change this back to Guest. + role: ActiveValue::Set(ChannelRole::Member), + }) + .exec(&*tx) + .await?; + + debug_assert!( + self.channel_role_for_user(channel_id, user_id, &*tx) + .await? + == role + ); + } + + if channel.is_none() || role.is_none() || role == Some(ChannelRole::Banned) { + Err(anyhow!("no such channel, or not allowed"))? + } + + let live_kit_room = format!("channel-{}", nanoid::nanoid!(30)); + let room_id = self + .get_or_create_channel_room(channel_id, &live_kit_room, environment, &*tx) + .await?; + + self.join_channel_room_internal(channel_id, room_id, user_id, connection, &*tx) + .await + .map(|jr| (jr, joined_channel_id)) + }) + .await + } + + pub async fn set_channel_visibility( + &self, + channel_id: ChannelId, + visibility: ChannelVisibility, + user_id: UserId, + ) -> Result { + self.transaction(move |tx| async move { + self.check_user_is_channel_admin(channel_id, user_id, &*tx) + .await?; + + let channel = channel::ActiveModel { + id: ActiveValue::Unchanged(channel_id), + visibility: ActiveValue::Set(visibility), + ..Default::default() + } + .update(&*tx) + .await?; + + Ok(channel) + }) + .await + } + pub async fn delete_channel( &self, channel_id: ChannelId, @@ -113,17 +206,19 @@ impl Database { .await?; // Don't remove descendant channels that have additional parents. - let mut channels_to_remove = self.get_channel_descendants([channel_id], &*tx).await?; + let mut channels_to_remove: HashSet = HashSet::default(); + channels_to_remove.insert(channel_id); + + let graph = self.get_channel_descendants([channel_id], &*tx).await?; + for edge in graph.iter() { + channels_to_remove.insert(ChannelId::from_proto(edge.channel_id)); + } + { let mut channels_to_keep = channel_path::Entity::find() .filter( channel_path::Column::ChannelId - .is_in( - channels_to_remove - .keys() - .copied() - .filter(|&id| id != channel_id), - ) + .is_in(channels_to_remove.iter().copied()) .and( channel_path::Column::IdPath .not_like(&format!("%/{}/%", channel_id)), @@ -148,7 +243,7 @@ impl Database { .await?; channel::Entity::delete_many() - .filter(channel::Column::Id.is_in(channels_to_remove.keys().copied())) + .filter(channel::Column::Id.is_in(channels_to_remove.iter().copied())) .exec(&*tx) .await?; @@ -165,7 +260,7 @@ impl Database { ); tx.execute(channel_paths_stmt).await?; - Ok((channels_to_remove.into_keys().collect(), members_to_notify)) + Ok((channels_to_remove.into_iter().collect(), members_to_notify)) }) .await } @@ -174,19 +269,19 @@ impl Database { &self, channel_id: ChannelId, invitee_id: UserId, - inviter_id: UserId, - is_admin: bool, + admin_id: UserId, + role: ChannelRole, ) -> Result<()> { self.transaction(move |tx| async move { - self.check_user_is_channel_admin(channel_id, inviter_id, &*tx) + self.check_user_is_channel_admin(channel_id, admin_id, &*tx) .await?; channel_member::ActiveModel { + id: ActiveValue::NotSet, channel_id: ActiveValue::Set(channel_id), user_id: ActiveValue::Set(invitee_id), accepted: ActiveValue::Set(false), - admin: ActiveValue::Set(is_admin), - ..Default::default() + role: ActiveValue::Set(role), } .insert(&*tx) .await?; @@ -209,14 +304,14 @@ impl Database { channel_id: ChannelId, user_id: UserId, new_name: &str, - ) -> Result { + ) -> Result { self.transaction(move |tx| async move { let new_name = Self::sanitize_channel_name(new_name)?.to_string(); self.check_user_is_channel_admin(channel_id, user_id, &*tx) .await?; - channel::ActiveModel { + let channel = channel::ActiveModel { id: ActiveValue::Unchanged(channel_id), name: ActiveValue::Set(new_name.clone()), ..Default::default() @@ -224,7 +319,11 @@ impl Database { .update(&*tx) .await?; - Ok(new_name) + Ok(Channel { + id: channel.id, + name: channel.name, + visibility: channel.visibility, + }) }) .await } @@ -275,10 +374,10 @@ impl Database { &self, channel_id: ChannelId, member_id: UserId, - remover_id: UserId, + admin_id: UserId, ) -> Result<()> { self.transaction(|tx| async move { - self.check_user_is_channel_admin(channel_id, remover_id, &*tx) + self.check_user_is_channel_admin(channel_id, admin_id, &*tx) .await?; let result = channel_member::Entity::delete_many() @@ -326,6 +425,7 @@ impl Database { .map(|channel| Channel { id: channel.id, name: channel.name, + visibility: channel.visibility, }) .collect(); @@ -334,49 +434,6 @@ impl Database { .await } - async fn get_channel_graph( - &self, - parents_by_child_id: ChannelDescendants, - trim_dangling_parents: bool, - tx: &DatabaseTransaction, - ) -> Result { - let mut channels = Vec::with_capacity(parents_by_child_id.len()); - { - let mut rows = channel::Entity::find() - .filter(channel::Column::Id.is_in(parents_by_child_id.keys().copied())) - .stream(&*tx) - .await?; - while let Some(row) = rows.next().await { - let row = row?; - channels.push(Channel { - id: row.id, - name: row.name, - }) - } - } - - let mut edges = Vec::with_capacity(parents_by_child_id.len()); - for (channel, parents) in parents_by_child_id.iter() { - for parent in parents.into_iter() { - if trim_dangling_parents { - if parents_by_child_id.contains_key(parent) { - edges.push(ChannelEdge { - channel_id: channel.to_proto(), - parent_id: parent.to_proto(), - }); - } - } else { - edges.push(ChannelEdge { - channel_id: channel.to_proto(), - parent_id: parent.to_proto(), - }); - } - } - } - - Ok(ChannelGraph { channels, edges }) - } - pub async fn get_channels_for_user(&self, user_id: UserId) -> Result { self.transaction(|tx| async move { let tx = tx; @@ -426,19 +483,108 @@ impl Database { channel_memberships: Vec, tx: &DatabaseTransaction, ) -> Result { - let parents_by_child_id = self + let mut edges = self .get_channel_descendants(channel_memberships.iter().map(|m| m.channel_id), &*tx) .await?; - let channels_with_admin_privileges = channel_memberships - .iter() - .filter_map(|membership| membership.admin.then_some(membership.channel_id)) - .collect(); + let mut role_for_channel: HashMap = HashMap::default(); - let graph = self - .get_channel_graph(parents_by_child_id, true, &tx) + for membership in channel_memberships.iter() { + role_for_channel.insert(membership.channel_id, membership.role); + } + + for ChannelEdge { + parent_id, + channel_id, + } in edges.iter() + { + let parent_id = ChannelId::from_proto(*parent_id); + let channel_id = ChannelId::from_proto(*channel_id); + debug_assert!(role_for_channel.get(&parent_id).is_some()); + let parent_role = role_for_channel[&parent_id]; + if let Some(existing_role) = role_for_channel.get(&channel_id) { + if existing_role.should_override(parent_role) { + continue; + } + } + role_for_channel.insert(channel_id, parent_role); + } + + let mut channels: Vec = Vec::new(); + let mut channels_with_admin_privileges: HashSet = HashSet::default(); + let mut channels_to_remove: HashSet = HashSet::default(); + + let mut rows = channel::Entity::find() + .filter(channel::Column::Id.is_in(role_for_channel.keys().copied())) + .stream(&*tx) .await?; + while let Some(row) = rows.next().await { + let channel = row?; + let role = role_for_channel[&channel.id]; + + if role == ChannelRole::Banned + || role == ChannelRole::Guest && channel.visibility != ChannelVisibility::Public + { + channels_to_remove.insert(channel.id.0 as u64); + continue; + } + + channels.push(Channel { + id: channel.id, + name: channel.name, + visibility: channel.visibility, + }); + + if role == ChannelRole::Admin { + channels_with_admin_privileges.insert(channel.id); + } + } + drop(rows); + + if !channels_to_remove.is_empty() { + // Note: this code assumes each channel has one parent. + // If there are multiple valid public paths to a channel, + // e.g. + // If both of these paths are present (* indicating public): + // - zed* -> projects -> vim* + // - zed* -> conrad -> public-projects* -> vim* + // Users would only see one of them (based on edge sort order) + let mut replacement_parent: HashMap = HashMap::default(); + for ChannelEdge { + parent_id, + channel_id, + } in edges.iter() + { + if channels_to_remove.contains(channel_id) { + replacement_parent.insert(*channel_id, *parent_id); + } + } + + let mut new_edges: Vec = Vec::new(); + 'outer: for ChannelEdge { + mut parent_id, + channel_id, + } in edges.iter() + { + if channels_to_remove.contains(channel_id) { + continue; + } + while channels_to_remove.contains(&parent_id) { + if let Some(new_parent_id) = replacement_parent.get(&parent_id) { + parent_id = *new_parent_id; + } else { + continue 'outer; + } + } + new_edges.push(ChannelEdge { + parent_id, + channel_id: *channel_id, + }) + } + edges = new_edges; + } + #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] enum QueryUserIdsAndChannelIds { ChannelId, @@ -449,7 +595,7 @@ impl Database { { let mut rows = room_participant::Entity::find() .inner_join(room::Entity) - .filter(room::Column::ChannelId.is_in(graph.channels.iter().map(|c| c.id))) + .filter(room::Column::ChannelId.is_in(channels.iter().map(|c| c.id))) .select_only() .column(room::Column::ChannelId) .column(room_participant::Column::UserId) @@ -462,7 +608,7 @@ impl Database { } } - let channel_ids = graph.channels.iter().map(|c| c.id).collect::>(); + let channel_ids = channels.iter().map(|c| c.id).collect::>(); let channel_buffer_changes = self .unseen_channel_buffer_changes(user_id, &channel_ids, &*tx) .await?; @@ -472,7 +618,7 @@ impl Database { .await?; Ok(ChannelsForUser { - channels: graph, + channels: ChannelGraph { channels, edges }, channel_participants, channels_with_admin_privileges, unseen_buffer_changes: channel_buffer_changes, @@ -481,116 +627,143 @@ impl Database { } pub async fn get_channel_members(&self, id: ChannelId) -> Result> { - self.transaction(|tx| async move { self.get_channel_members_internal(id, &*tx).await }) + self.transaction(|tx| async move { self.get_channel_participants_internal(id, &*tx).await }) .await } - pub async fn set_channel_member_admin( + pub async fn set_channel_member_role( &self, channel_id: ChannelId, - from: UserId, + admin_id: UserId, for_user: UserId, - admin: bool, - ) -> Result<()> { + role: ChannelRole, + ) -> Result { self.transaction(|tx| async move { - self.check_user_is_channel_admin(channel_id, from, &*tx) + self.check_user_is_channel_admin(channel_id, admin_id, &*tx) .await?; - let result = channel_member::Entity::update_many() + let membership = channel_member::Entity::find() .filter( channel_member::Column::ChannelId .eq(channel_id) .and(channel_member::Column::UserId.eq(for_user)), ) - .set(channel_member::ActiveModel { - admin: ActiveValue::set(admin), - ..Default::default() - }) - .exec(&*tx) + .one(&*tx) .await?; - if result.rows_affected == 0 { - Err(anyhow!("no such member"))?; - } + let Some(membership) = membership else { + Err(anyhow!("no such member"))? + }; - Ok(()) + let mut update = membership.into_active_model(); + update.role = ActiveValue::Set(role); + let updated = channel_member::Entity::update(update).exec(&*tx).await?; + + Ok(updated) }) .await } - pub async fn get_channel_member_details( + pub async fn get_channel_participant_details( &self, channel_id: ChannelId, - user_id: UserId, + admin_id: UserId, ) -> Result> { self.transaction(|tx| async move { - self.check_user_is_channel_admin(channel_id, user_id, &*tx) + self.check_user_is_channel_admin(channel_id, admin_id, &*tx) .await?; + let channel_visibility = channel::Entity::find() + .filter(channel::Column::Id.eq(channel_id)) + .one(&*tx) + .await? + .map(|channel| channel.visibility) + .unwrap_or(ChannelVisibility::Members); + #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] enum QueryMemberDetails { UserId, - Admin, + Role, IsDirectMember, Accepted, + Visibility, } let tx = tx; let ancestor_ids = self.get_channel_ancestors(channel_id, &*tx).await?; let mut stream = channel_member::Entity::find() - .distinct() + .left_join(channel::Entity) .filter(channel_member::Column::ChannelId.is_in(ancestor_ids.iter().copied())) .select_only() .column(channel_member::Column::UserId) - .column(channel_member::Column::Admin) + .column(channel_member::Column::Role) .column_as( channel_member::Column::ChannelId.eq(channel_id), QueryMemberDetails::IsDirectMember, ) .column(channel_member::Column::Accepted) - .order_by_asc(channel_member::Column::UserId) + .column(channel::Column::Visibility) .into_values::<_, QueryMemberDetails>() .stream(&*tx) .await?; - let mut rows = Vec::::new(); - while let Some(row) = stream.next().await { - let (user_id, is_admin, is_direct_member, is_invite_accepted): ( + struct UserDetail { + kind: Kind, + channel_role: ChannelRole, + } + let mut user_details: HashMap = HashMap::default(); + + while let Some(user_membership) = stream.next().await { + let (user_id, channel_role, is_direct_member, is_invite_accepted, visibility): ( UserId, + ChannelRole, bool, bool, - bool, - ) = row?; + ChannelVisibility, + ) = user_membership?; let kind = match (is_direct_member, is_invite_accepted) { (true, true) => proto::channel_member::Kind::Member, (true, false) => proto::channel_member::Kind::Invitee, (false, true) => proto::channel_member::Kind::AncestorMember, (false, false) => continue, }; - let user_id = user_id.to_proto(); - let kind = kind.into(); - if let Some(last_row) = rows.last_mut() { - if last_row.user_id == user_id { - if is_direct_member { - last_row.kind = kind; - last_row.admin = is_admin; - } - continue; - } + + if channel_role == ChannelRole::Guest + && visibility != ChannelVisibility::Public + && channel_visibility != ChannelVisibility::Public + { + continue; + } + + if let Some(details_mut) = user_details.get_mut(&user_id) { + if channel_role.should_override(details_mut.channel_role) { + details_mut.channel_role = channel_role; + } + if kind == Kind::Member { + details_mut.kind = kind; + // the UI is going to be a bit confusing if you already have permissions + // that are greater than or equal to the ones you're being invited to. + } else if kind == Kind::Invitee && details_mut.kind == Kind::AncestorMember { + details_mut.kind = kind; + } + } else { + user_details.insert(user_id, UserDetail { kind, channel_role }); } - rows.push(proto::ChannelMember { - user_id, - kind, - admin: is_admin, - }); } - Ok(rows) + Ok(user_details + .into_iter() + .map(|(user_id, details)| proto::ChannelMember { + user_id: user_id.to_proto(), + kind: details.kind.into(), + role: details.channel_role.into(), + }) + .collect()) }) .await } - pub async fn get_channel_members_internal( + pub async fn get_channel_participants_internal( &self, id: ChannelId, tx: &DatabaseTransaction, @@ -611,46 +784,197 @@ impl Database { Ok(user_ids) } - pub async fn check_user_is_channel_member( - &self, - channel_id: ChannelId, - user_id: UserId, - tx: &DatabaseTransaction, - ) -> Result<()> { - let channel_ids = self.get_channel_ancestors(channel_id, tx).await?; - channel_member::Entity::find() - .filter( - channel_member::Column::ChannelId - .is_in(channel_ids) - .and(channel_member::Column::UserId.eq(user_id)), - ) - .one(&*tx) - .await? - .ok_or_else(|| anyhow!("user is not a channel member or channel does not exist"))?; - Ok(()) - } - pub async fn check_user_is_channel_admin( &self, channel_id: ChannelId, user_id: UserId, tx: &DatabaseTransaction, ) -> Result<()> { + match self.channel_role_for_user(channel_id, user_id, tx).await? { + Some(ChannelRole::Admin) => Ok(()), + Some(ChannelRole::Member) + | Some(ChannelRole::Banned) + | Some(ChannelRole::Guest) + | None => Err(anyhow!( + "user is not a channel admin or channel does not exist" + ))?, + } + } + + pub async fn check_user_is_channel_member( + &self, + channel_id: ChannelId, + user_id: UserId, + tx: &DatabaseTransaction, + ) -> Result<()> { + match self.channel_role_for_user(channel_id, user_id, tx).await? { + Some(ChannelRole::Admin) | Some(ChannelRole::Member) => Ok(()), + Some(ChannelRole::Banned) | Some(ChannelRole::Guest) | None => Err(anyhow!( + "user is not a channel member or channel does not exist" + ))?, + } + } + + pub async fn check_user_is_channel_participant( + &self, + channel_id: ChannelId, + user_id: UserId, + tx: &DatabaseTransaction, + ) -> Result<()> { + match self.channel_role_for_user(channel_id, user_id, tx).await? { + Some(ChannelRole::Admin) | Some(ChannelRole::Member) | Some(ChannelRole::Guest) => { + Ok(()) + } + Some(ChannelRole::Banned) | None => Err(anyhow!( + "user is not a channel participant or channel does not exist" + ))?, + } + } + + pub async fn pending_invite_for_channel( + &self, + channel_id: ChannelId, + user_id: UserId, + tx: &DatabaseTransaction, + ) -> Result> { let channel_ids = self.get_channel_ancestors(channel_id, tx).await?; - channel_member::Entity::find() + + let row = channel_member::Entity::find() + .filter(channel_member::Column::ChannelId.is_in(channel_ids)) + .filter(channel_member::Column::UserId.eq(user_id)) + .filter(channel_member::Column::Accepted.eq(false)) + .one(&*tx) + .await?; + + Ok(row) + } + + pub async fn most_public_ancestor_for_channel( + &self, + channel_id: ChannelId, + tx: &DatabaseTransaction, + ) -> Result> { + // Note: if there are many paths to a channel, this will return just one + let arbitary_path = channel_path::Entity::find() + .filter(channel_path::Column::ChannelId.eq(channel_id)) + .order_by(channel_path::Column::IdPath, sea_orm::Order::Desc) + .one(tx) + .await?; + + let Some(path) = arbitary_path else { + return Ok(None); + }; + + let ancestor_ids: Vec = path + .id_path + .trim_matches('/') + .split('/') + .map(|id| ChannelId::from_proto(id.parse().unwrap())) + .collect(); + + let rows = channel::Entity::find() + .filter(channel::Column::Id.is_in(ancestor_ids.iter().copied())) + .filter(channel::Column::Visibility.eq(ChannelVisibility::Public)) + .all(&*tx) + .await?; + + let mut visible_channels: HashSet = HashSet::default(); + + for row in rows { + visible_channels.insert(row.id); + } + + for ancestor in ancestor_ids { + if visible_channels.contains(&ancestor) { + return Ok(Some(ancestor)); + } + } + + Ok(None) + } + + pub async fn channel_role_for_user( + &self, + channel_id: ChannelId, + user_id: UserId, + tx: &DatabaseTransaction, + ) -> Result> { + let channel_ids = self.get_channel_ancestors(channel_id, tx).await?; + + #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] + enum QueryChannelMembership { + ChannelId, + Role, + Visibility, + } + + let mut rows = channel_member::Entity::find() + .left_join(channel::Entity) .filter( channel_member::Column::ChannelId .is_in(channel_ids) .and(channel_member::Column::UserId.eq(user_id)) - .and(channel_member::Column::Admin.eq(true)), + .and(channel_member::Column::Accepted.eq(true)), ) - .one(&*tx) - .await? - .ok_or_else(|| anyhow!("user is not a channel admin or channel does not exist"))?; - Ok(()) + .select_only() + .column(channel_member::Column::ChannelId) + .column(channel_member::Column::Role) + .column(channel::Column::Visibility) + .into_values::<_, QueryChannelMembership>() + .stream(&*tx) + .await?; + + let mut user_role: Option = None; + + let mut is_participant = false; + let mut current_channel_visibility = None; + + // note these channels are not iterated in any particular order, + // our current logic takes the highest permission available. + while let Some(row) = rows.next().await { + let (membership_channel, role, visibility): ( + ChannelId, + ChannelRole, + ChannelVisibility, + ) = row?; + + match role { + ChannelRole::Admin | ChannelRole::Member | ChannelRole::Banned => { + if let Some(users_role) = user_role { + user_role = Some(users_role.max(role)); + } else { + user_role = Some(role) + } + } + ChannelRole::Guest if visibility == ChannelVisibility::Public => { + is_participant = true + } + ChannelRole::Guest => {} + } + if channel_id == membership_channel { + current_channel_visibility = Some(visibility); + } + } + // free up database connection + drop(rows); + + if is_participant && user_role.is_none() { + if current_channel_visibility.is_none() { + current_channel_visibility = channel::Entity::find() + .filter(channel::Column::Id.eq(channel_id)) + .one(&*tx) + .await? + .map(|channel| channel.visibility); + } + if current_channel_visibility == Some(ChannelVisibility::Public) { + user_role = Some(ChannelRole::Guest); + } + } + + Ok(user_role) } - /// Returns the channel ancestors, deepest first + /// Returns the channel ancestors in arbitrary order pub async fn get_channel_ancestors( &self, channel_id: ChannelId, @@ -675,25 +999,14 @@ impl Database { Ok(channel_ids) } - /// Returns the channel descendants, - /// Structured as a map from child ids to their parent ids - /// For example, the descendants of 'a' in this DAG: - /// - /// /- b -\ - /// a -- c -- d - /// - /// would be: - /// { - /// a: [], - /// b: [a], - /// c: [a], - /// d: [a, c], - /// } + // Returns the channel desendants as a sorted list of edges for further processing. + // The edges are sorted such that you will see unknown channel ids as children + // before you see them as parents. async fn get_channel_descendants( &self, channel_ids: impl IntoIterator, tx: &DatabaseTransaction, - ) -> Result { + ) -> Result> { let mut values = String::new(); for id in channel_ids { if !values.is_empty() { @@ -703,7 +1016,7 @@ impl Database { } if values.is_empty() { - return Ok(HashMap::default()); + return Ok(vec![]); } let sql = format!( @@ -714,103 +1027,90 @@ impl Database { channel_paths parent_paths, channel_paths descendant_paths WHERE parent_paths.channel_id IN ({values}) AND + descendant_paths.id_path != parent_paths.id_path AND descendant_paths.id_path LIKE (parent_paths.id_path || '%') + ORDER BY + descendant_paths.id_path "# ); let stmt = Statement::from_string(self.pool.get_database_backend(), sql); - let mut parents_by_child_id: ChannelDescendants = HashMap::default(); let mut paths = channel_path::Entity::find() .from_raw_sql(stmt) .stream(tx) .await?; + let mut results: Vec = Vec::new(); while let Some(path) = paths.next().await { let path = path?; - let ids = path.id_path.trim_matches('/').split('/'); - let mut parent_id = None; - for id in ids { - if let Ok(id) = id.parse() { - let id = ChannelId::from_proto(id); - if id == path.channel_id { - break; - } - parent_id = Some(id); - } - } - let entry = parents_by_child_id.entry(path.channel_id).or_default(); - if let Some(parent_id) = parent_id { - entry.insert(parent_id); - } + let ids: Vec<&str> = path.id_path.trim_matches('/').split('/').collect(); + + debug_assert!(ids.len() >= 2); + debug_assert!(ids[ids.len() - 1] == path.channel_id.to_string()); + + results.push(ChannelEdge { + parent_id: ids[ids.len() - 2].parse().unwrap(), + channel_id: ids[ids.len() - 1].parse().unwrap(), + }) } - Ok(parents_by_child_id) + Ok(results) } - /// Returns the channel with the given ID and: - /// - true if the user is a member - /// - false if the user hasn't accepted the invitation yet - pub async fn get_channel( - &self, - channel_id: ChannelId, - user_id: UserId, - ) -> Result> { + /// Returns the channel with the given ID + pub async fn get_channel(&self, channel_id: ChannelId, user_id: UserId) -> Result { self.transaction(|tx| async move { - let tx = tx; + self.check_user_is_channel_participant(channel_id, user_id, &*tx) + .await?; let channel = channel::Entity::find_by_id(channel_id).one(&*tx).await?; + let Some(channel) = channel else { + Err(anyhow!("no such channel"))? + }; - if let Some(channel) = channel { - if self - .check_user_is_channel_member(channel_id, user_id, &*tx) - .await - .is_err() - { - return Ok(None); - } - - let channel_membership = channel_member::Entity::find() - .filter( - channel_member::Column::ChannelId - .eq(channel_id) - .and(channel_member::Column::UserId.eq(user_id)), - ) - .one(&*tx) - .await?; - - let is_accepted = channel_membership - .map(|membership| membership.accepted) - .unwrap_or(false); - - Ok(Some(( - Channel { - id: channel.id, - name: channel.name, - }, - is_accepted, - ))) - } else { - Ok(None) - } + Ok(Channel { + id: channel.id, + visibility: channel.visibility, + name: channel.name, + }) }) .await } - pub async fn room_id_for_channel(&self, channel_id: ChannelId) -> Result { - self.transaction(|tx| async move { - let tx = tx; - let room = channel::Model { - id: channel_id, - ..Default::default() - } - .find_related(room::Entity) + pub(crate) async fn get_or_create_channel_room( + &self, + channel_id: ChannelId, + live_kit_room: &str, + environment: &str, + tx: &DatabaseTransaction, + ) -> Result { + let room = room::Entity::find() + .filter(room::Column::ChannelId.eq(channel_id)) .one(&*tx) - .await? - .ok_or_else(|| anyhow!("invalid channel"))?; - Ok(room.id) - }) - .await + .await?; + + let room_id = if let Some(room) = room { + if let Some(env) = room.enviroment { + if &env != environment { + Err(anyhow!("must join using the {} release", env))?; + } + } + room.id + } else { + let result = room::Entity::insert(room::ActiveModel { + channel_id: ActiveValue::Set(Some(channel_id)), + live_kit_room: ActiveValue::Set(live_kit_room.to_string()), + enviroment: ActiveValue::Set(Some(environment.to_string())), + ..Default::default() + }) + .exec(&*tx) + .await?; + + result.last_insert_id + }; + + Ok(room_id) } // Insert an edge from the given channel to the given other channel. @@ -836,10 +1136,11 @@ impl Database { &self, user: UserId, channel: ChannelId, - to: ChannelId, + new_parent: ChannelId, tx: &DatabaseTransaction, ) -> Result { - self.check_user_is_channel_admin(to, user, &*tx).await?; + self.check_user_is_channel_admin(new_parent, user, &*tx) + .await?; let paths = channel_path::Entity::find() .filter(channel_path::Column::IdPath.like(&format!("%/{}/%", channel))) @@ -857,7 +1158,7 @@ impl Database { } let paths_to_new_parent = channel_path::Entity::find() - .filter(channel_path::Column::ChannelId.eq(to)) + .filter(channel_path::Column::ChannelId.eq(new_parent)) .all(tx) .await?; @@ -887,18 +1188,23 @@ impl Database { .await?; } - let mut channel_descendants = self.get_channel_descendants([channel], &*tx).await?; - if let Some(channel) = channel_descendants.get_mut(&channel) { - // Remove the other parents - channel.clear(); - channel.insert(to); - } - - let channels = self - .get_channel_graph(channel_descendants, false, &*tx) + let membership = channel_member::Entity::find() + .filter( + channel_member::Column::ChannelId + .eq(channel) + .and(channel_member::Column::UserId.eq(user)), + ) + .all(tx) .await?; - Ok(channels) + let mut channel_info = self.get_user_channels(user, membership, &*tx).await?; + + channel_info.channels.edges.push(ChannelEdge { + channel_id: channel.to_proto(), + parent_id: new_parent.to_proto(), + }); + + Ok(channel_info.channels) } /// Unlink a channel from a given parent. This will add in a root edge if @@ -1050,38 +1356,3 @@ impl PartialEq for ChannelGraph { self.channels == other.channels && self.edges == other.edges } } - -struct SmallSet(SmallVec<[T; 1]>); - -impl Deref for SmallSet { - type Target = [T]; - - fn deref(&self) -> &Self::Target { - self.0.deref() - } -} - -impl Default for SmallSet { - fn default() -> Self { - Self(SmallVec::new()) - } -} - -impl SmallSet { - fn insert(&mut self, value: T) -> bool - where - T: Ord, - { - match self.binary_search(&value) { - Ok(_) => false, - Err(ix) => { - self.0.insert(ix, value); - true - } - } - } - - fn clear(&mut self) { - self.0.clear(); - } -} diff --git a/crates/collab/src/db/queries/messages.rs b/crates/collab/src/db/queries/messages.rs index a48d425d90..06e954103d 100644 --- a/crates/collab/src/db/queries/messages.rs +++ b/crates/collab/src/db/queries/messages.rs @@ -180,7 +180,9 @@ impl Database { ) .await?; - let mut channel_members = self.get_channel_members_internal(channel_id, &*tx).await?; + let mut channel_members = self + .get_channel_participants_internal(channel_id, &*tx) + .await?; channel_members.retain(|member| !participant_user_ids.contains(member)); Ok(( @@ -337,8 +339,22 @@ impl Database { .filter(channel_message::Column::SenderId.eq(user_id)) .exec(&*tx) .await?; + if result.rows_affected == 0 { - Err(anyhow!("no such message"))?; + if self + .check_user_is_channel_admin(channel_id, user_id, &*tx) + .await + .is_ok() + { + let result = channel_message::Entity::delete_by_id(message_id) + .exec(&*tx) + .await?; + if result.rows_affected == 0 { + Err(anyhow!("no such message"))?; + } + } else { + Err(anyhow!("operation could not be completed"))?; + } } Ok(participant_connection_ids) diff --git a/crates/collab/src/db/queries/rooms.rs b/crates/collab/src/db/queries/rooms.rs index b103ae1c73..d2120495b0 100644 --- a/crates/collab/src/db/queries/rooms.rs +++ b/crates/collab/src/db/queries/rooms.rs @@ -53,7 +53,9 @@ impl Database { let (channel_id, room) = self.get_channel_room(room_id, &tx).await?; let channel_members; if let Some(channel_id) = channel_id { - channel_members = self.get_channel_members_internal(channel_id, &tx).await?; + channel_members = self + .get_channel_participants_internal(channel_id, &tx) + .await?; } else { channel_members = Vec::new(); @@ -107,10 +109,12 @@ impl Database { user_id: UserId, connection: ConnectionId, live_kit_room: &str, + release_channel: &str, ) -> Result { self.transaction(|tx| async move { let room = room::ActiveModel { live_kit_room: ActiveValue::set(live_kit_room.into()), + enviroment: ActiveValue::set(Some(release_channel.to_string())), ..Default::default() } .insert(&*tx) @@ -270,112 +274,165 @@ impl Database { room_id: RoomId, user_id: UserId, connection: ConnectionId, + enviroment: &str, ) -> Result> { self.room_transaction(room_id, |tx| async move { #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] - enum QueryChannelId { + enum QueryChannelIdAndEnviroment { ChannelId, + Enviroment, } - let channel_id: Option = room::Entity::find() - .select_only() - .column(room::Column::ChannelId) - .filter(room::Column::Id.eq(room_id)) - .into_values::<_, QueryChannelId>() - .one(&*tx) - .await? - .ok_or_else(|| anyhow!("no such room"))?; - #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] - enum QueryParticipantIndices { - ParticipantIndex, + let (channel_id, release_channel): (Option, Option) = + room::Entity::find() + .select_only() + .column(room::Column::ChannelId) + .column(room::Column::Enviroment) + .filter(room::Column::Id.eq(room_id)) + .into_values::<_, QueryChannelIdAndEnviroment>() + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("no such room"))?; + + if let Some(release_channel) = release_channel { + if &release_channel != enviroment { + Err(anyhow!("must join using the {} release", release_channel))?; + } } - let existing_participant_indices: Vec = room_participant::Entity::find() - .filter( - room_participant::Column::RoomId - .eq(room_id) - .and(room_participant::Column::ParticipantIndex.is_not_null()), - ) - .select_only() - .column(room_participant::Column::ParticipantIndex) - .into_values::<_, QueryParticipantIndices>() - .all(&*tx) + + if channel_id.is_some() { + Err(anyhow!("tried to join channel call directly"))? + } + + let participant_index = self + .get_next_participant_index_internal(room_id, &*tx) .await?; - let mut participant_index = 0; - while existing_participant_indices.contains(&participant_index) { - participant_index += 1; - } - if let Some(channel_id) = channel_id { - self.check_user_is_channel_member(channel_id, user_id, &*tx) - .await?; - - room_participant::Entity::insert_many([room_participant::ActiveModel { - room_id: ActiveValue::set(room_id), - user_id: ActiveValue::set(user_id), + let result = room_participant::Entity::update_many() + .filter( + Condition::all() + .add(room_participant::Column::RoomId.eq(room_id)) + .add(room_participant::Column::UserId.eq(user_id)) + .add(room_participant::Column::AnsweringConnectionId.is_null()), + ) + .set(room_participant::ActiveModel { + participant_index: ActiveValue::Set(Some(participant_index)), answering_connection_id: ActiveValue::set(Some(connection.id as i32)), answering_connection_server_id: ActiveValue::set(Some(ServerId( connection.owner_id as i32, ))), answering_connection_lost: ActiveValue::set(false), - calling_user_id: ActiveValue::set(user_id), - calling_connection_id: ActiveValue::set(connection.id as i32), - calling_connection_server_id: ActiveValue::set(Some(ServerId( - connection.owner_id as i32, - ))), - participant_index: ActiveValue::Set(Some(participant_index)), ..Default::default() - }]) - .on_conflict( - OnConflict::columns([room_participant::Column::UserId]) - .update_columns([ - room_participant::Column::AnsweringConnectionId, - room_participant::Column::AnsweringConnectionServerId, - room_participant::Column::AnsweringConnectionLost, - room_participant::Column::ParticipantIndex, - ]) - .to_owned(), - ) + }) .exec(&*tx) .await?; - } else { - let result = room_participant::Entity::update_many() - .filter( - Condition::all() - .add(room_participant::Column::RoomId.eq(room_id)) - .add(room_participant::Column::UserId.eq(user_id)) - .add(room_participant::Column::AnsweringConnectionId.is_null()), - ) - .set(room_participant::ActiveModel { - participant_index: ActiveValue::Set(Some(participant_index)), - answering_connection_id: ActiveValue::set(Some(connection.id as i32)), - answering_connection_server_id: ActiveValue::set(Some(ServerId( - connection.owner_id as i32, - ))), - answering_connection_lost: ActiveValue::set(false), - ..Default::default() - }) - .exec(&*tx) - .await?; - if result.rows_affected == 0 { - Err(anyhow!("room does not exist or was already joined"))?; - } + if result.rows_affected == 0 { + Err(anyhow!("room does not exist or was already joined"))?; } let room = self.get_room(room_id, &tx).await?; - let channel_members = if let Some(channel_id) = channel_id { - self.get_channel_members_internal(channel_id, &tx).await? - } else { - Vec::new() - }; Ok(JoinRoom { room, - channel_id, - channel_members, + channel_id: None, + channel_members: vec![], }) }) .await } + async fn get_next_participant_index_internal( + &self, + room_id: RoomId, + tx: &DatabaseTransaction, + ) -> Result { + #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] + enum QueryParticipantIndices { + ParticipantIndex, + } + let existing_participant_indices: Vec = room_participant::Entity::find() + .filter( + room_participant::Column::RoomId + .eq(room_id) + .and(room_participant::Column::ParticipantIndex.is_not_null()), + ) + .select_only() + .column(room_participant::Column::ParticipantIndex) + .into_values::<_, QueryParticipantIndices>() + .all(&*tx) + .await?; + + let mut participant_index = 0; + while existing_participant_indices.contains(&participant_index) { + participant_index += 1; + } + + Ok(participant_index) + } + + pub async fn channel_id_for_room(&self, room_id: RoomId) -> Result> { + self.transaction(|tx| async move { + let room: Option = room::Entity::find() + .filter(room::Column::Id.eq(room_id)) + .one(&*tx) + .await?; + + Ok(room.and_then(|room| room.channel_id)) + }) + .await + } + + pub(crate) async fn join_channel_room_internal( + &self, + channel_id: ChannelId, + room_id: RoomId, + user_id: UserId, + connection: ConnectionId, + tx: &DatabaseTransaction, + ) -> Result { + let participant_index = self + .get_next_participant_index_internal(room_id, &*tx) + .await?; + + room_participant::Entity::insert_many([room_participant::ActiveModel { + room_id: ActiveValue::set(room_id), + user_id: ActiveValue::set(user_id), + answering_connection_id: ActiveValue::set(Some(connection.id as i32)), + answering_connection_server_id: ActiveValue::set(Some(ServerId( + connection.owner_id as i32, + ))), + answering_connection_lost: ActiveValue::set(false), + calling_user_id: ActiveValue::set(user_id), + calling_connection_id: ActiveValue::set(connection.id as i32), + calling_connection_server_id: ActiveValue::set(Some(ServerId( + connection.owner_id as i32, + ))), + participant_index: ActiveValue::Set(Some(participant_index)), + ..Default::default() + }]) + .on_conflict( + OnConflict::columns([room_participant::Column::UserId]) + .update_columns([ + room_participant::Column::AnsweringConnectionId, + room_participant::Column::AnsweringConnectionServerId, + room_participant::Column::AnsweringConnectionLost, + room_participant::Column::ParticipantIndex, + ]) + .to_owned(), + ) + .exec(&*tx) + .await?; + + let room = self.get_room(room_id, &tx).await?; + let channel_members = self + .get_channel_participants_internal(channel_id, &tx) + .await?; + Ok(JoinRoom { + room, + channel_id: Some(channel_id), + channel_members, + }) + } + pub async fn rejoin_room( &self, rejoin_room: proto::RejoinRoom, @@ -667,7 +724,8 @@ impl Database { let (channel_id, room) = self.get_channel_room(room_id, &tx).await?; let channel_members = if let Some(channel_id) = channel_id { - self.get_channel_members_internal(channel_id, &tx).await? + self.get_channel_participants_internal(channel_id, &tx) + .await? } else { Vec::new() }; @@ -818,17 +876,15 @@ impl Database { let (channel_id, room) = self.get_channel_room(room_id, &tx).await?; let deleted = if room.participants.is_empty() { - let result = room::Entity::delete_by_id(room_id) - .filter(room::Column::ChannelId.is_null()) - .exec(&*tx) - .await?; + let result = room::Entity::delete_by_id(room_id).exec(&*tx).await?; result.rows_affected > 0 } else { false }; let channel_members = if let Some(channel_id) = channel_id { - self.get_channel_members_internal(channel_id, &tx).await? + self.get_channel_participants_internal(channel_id, &tx) + .await? } else { Vec::new() }; diff --git a/crates/collab/src/db/tables/channel.rs b/crates/collab/src/db/tables/channel.rs index 54f12defc1..0975a8cc30 100644 --- a/crates/collab/src/db/tables/channel.rs +++ b/crates/collab/src/db/tables/channel.rs @@ -1,4 +1,4 @@ -use crate::db::ChannelId; +use crate::db::{ChannelId, ChannelVisibility}; use sea_orm::entity::prelude::*; #[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel)] @@ -7,6 +7,7 @@ pub struct Model { #[sea_orm(primary_key)] pub id: ChannelId, pub name: String, + pub visibility: ChannelVisibility, } impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab/src/db/tables/channel_member.rs b/crates/collab/src/db/tables/channel_member.rs index ba3db5a155..5498a00856 100644 --- a/crates/collab/src/db/tables/channel_member.rs +++ b/crates/collab/src/db/tables/channel_member.rs @@ -1,7 +1,7 @@ -use crate::db::{channel_member, ChannelId, ChannelMemberId, UserId}; +use crate::db::{channel_member, ChannelId, ChannelMemberId, ChannelRole, UserId}; use sea_orm::entity::prelude::*; -#[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel)] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] #[sea_orm(table_name = "channel_members")] pub struct Model { #[sea_orm(primary_key)] @@ -9,7 +9,7 @@ pub struct Model { pub channel_id: ChannelId, pub user_id: UserId, pub accepted: bool, - pub admin: bool, + pub role: ChannelRole, } impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab/src/db/tables/room.rs b/crates/collab/src/db/tables/room.rs index f72f7000a7..4150c741ac 100644 --- a/crates/collab/src/db/tables/room.rs +++ b/crates/collab/src/db/tables/room.rs @@ -8,6 +8,7 @@ pub struct Model { pub id: RoomId, pub live_kit_room: String, pub channel_id: Option, + pub enviroment: Option, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] diff --git a/crates/collab/src/db/tests.rs b/crates/collab/src/db/tests.rs index 75584ff90b..99a605106e 100644 --- a/crates/collab/src/db/tests.rs +++ b/crates/collab/src/db/tests.rs @@ -12,6 +12,8 @@ use sea_orm::ConnectionTrait; use sqlx::migrate::MigrateDatabase; use std::sync::Arc; +const TEST_RELEASE_CHANNEL: &'static str = "test"; + pub struct TestDb { pub db: Option>, pub connection: Option, @@ -157,6 +159,7 @@ fn graph(channels: &[(ChannelId, &'static str)], edges: &[(ChannelId, ChannelId) graph.channels.push(Channel { id: *id, name: name.to_string(), + visibility: ChannelVisibility::Members, }) } diff --git a/crates/collab/src/db/tests/buffer_tests.rs b/crates/collab/src/db/tests/buffer_tests.rs index f6e91b91f0..51ba9bf655 100644 --- a/crates/collab/src/db/tests/buffer_tests.rs +++ b/crates/collab/src/db/tests/buffer_tests.rs @@ -54,9 +54,9 @@ async fn test_channel_buffers(db: &Arc) { let owner_id = db.create_server("production").await.unwrap().0 as u32; - let zed_id = db.create_root_channel("zed", "1", a_id).await.unwrap(); + let zed_id = db.create_root_channel("zed", a_id).await.unwrap(); - db.invite_channel_member(zed_id, b_id, a_id, false) + db.invite_channel_member(zed_id, b_id, a_id, ChannelRole::Member) .await .unwrap(); @@ -141,7 +141,7 @@ async fn test_channel_buffers(db: &Arc) { assert_eq!(left_buffer.connections, &[connection_id_a],); - let cargo_id = db.create_root_channel("cargo", "2", a_id).await.unwrap(); + let cargo_id = db.create_root_channel("cargo", a_id).await.unwrap(); let _ = db .join_channel_buffer(cargo_id, a_id, connection_id_a) .await @@ -207,11 +207,11 @@ async fn test_channel_buffers_last_operations(db: &Database) { let mut text_buffers = Vec::new(); for i in 0..3 { let channel = db - .create_root_channel(&format!("channel-{i}"), &format!("room-{i}"), user_id) + .create_root_channel(&format!("channel-{i}"), user_id) .await .unwrap(); - db.invite_channel_member(channel, observer_id, user_id, false) + db.invite_channel_member(channel, observer_id, user_id, ChannelRole::Member) .await .unwrap(); db.respond_to_channel_invite(channel, observer_id, true) diff --git a/crates/collab/src/db/tests/channel_tests.rs b/crates/collab/src/db/tests/channel_tests.rs index 429852d128..40842aff5c 100644 --- a/crates/collab/src/db/tests/channel_tests.rs +++ b/crates/collab/src/db/tests/channel_tests.rs @@ -5,10 +5,17 @@ use rpc::{ }; use crate::{ - db::{queries::channels::ChannelGraph, tests::graph, ChannelId, Database, NewUserParams}, + db::{ + queries::channels::ChannelGraph, + tests::{graph, TEST_RELEASE_CHANNEL}, + ChannelId, ChannelRole, Database, NewUserParams, RoomId, UserId, + }, test_both_dbs, }; -use std::sync::Arc; +use std::sync::{ + atomic::{AtomicI32, Ordering}, + Arc, +}; test_both_dbs!(test_channels, test_channels_postgres, test_channels_sqlite); @@ -41,12 +48,12 @@ async fn test_channels(db: &Arc) { .unwrap() .user_id; - let zed_id = db.create_root_channel("zed", "1", a_id).await.unwrap(); + let zed_id = db.create_root_channel("zed", a_id).await.unwrap(); // Make sure that people cannot read channels they haven't been invited to - assert!(db.get_channel(zed_id, b_id).await.unwrap().is_none()); + assert!(db.get_channel(zed_id, b_id).await.is_err()); - db.invite_channel_member(zed_id, b_id, a_id, false) + db.invite_channel_member(zed_id, b_id, a_id, ChannelRole::Member) .await .unwrap(); @@ -54,16 +61,13 @@ async fn test_channels(db: &Arc) { .await .unwrap(); - let crdb_id = db - .create_channel("crdb", Some(zed_id), "2", a_id) - .await - .unwrap(); + let crdb_id = db.create_channel("crdb", Some(zed_id), a_id).await.unwrap(); let livestreaming_id = db - .create_channel("livestreaming", Some(zed_id), "3", a_id) + .create_channel("livestreaming", Some(zed_id), a_id) .await .unwrap(); let replace_id = db - .create_channel("replace", Some(zed_id), "4", a_id) + .create_channel("replace", Some(zed_id), a_id) .await .unwrap(); @@ -71,14 +75,14 @@ async fn test_channels(db: &Arc) { members.sort(); assert_eq!(members, &[a_id, b_id]); - let rust_id = db.create_root_channel("rust", "5", a_id).await.unwrap(); + let rust_id = db.create_root_channel("rust", a_id).await.unwrap(); let cargo_id = db - .create_channel("cargo", Some(rust_id), "6", a_id) + .create_channel("cargo", Some(rust_id), a_id) .await .unwrap(); let cargo_ra_id = db - .create_channel("cargo-ra", Some(cargo_id), "7", a_id) + .create_channel("cargo-ra", Some(cargo_id), a_id) .await .unwrap(); @@ -124,9 +128,13 @@ async fn test_channels(db: &Arc) { ); // Update member permissions - let set_subchannel_admin = db.set_channel_member_admin(crdb_id, a_id, b_id, true).await; + let set_subchannel_admin = db + .set_channel_member_role(crdb_id, a_id, b_id, ChannelRole::Admin) + .await; assert!(set_subchannel_admin.is_err()); - let set_channel_admin = db.set_channel_member_admin(zed_id, a_id, b_id, true).await; + let set_channel_admin = db + .set_channel_member_role(zed_id, a_id, b_id, ChannelRole::Admin) + .await; assert!(set_channel_admin.is_ok()); let result = db.get_channels_for_user(b_id).await.unwrap(); @@ -149,7 +157,7 @@ async fn test_channels(db: &Arc) { // Remove a single channel db.delete_channel(crdb_id, a_id).await.unwrap(); - assert!(db.get_channel(crdb_id, a_id).await.unwrap().is_none()); + assert!(db.get_channel(crdb_id, a_id).await.is_err()); // Remove a channel tree let (mut channel_ids, user_ids) = db.delete_channel(rust_id, a_id).await.unwrap(); @@ -157,9 +165,9 @@ async fn test_channels(db: &Arc) { assert_eq!(channel_ids, &[rust_id, cargo_id, cargo_ra_id]); assert_eq!(user_ids, &[a_id]); - assert!(db.get_channel(rust_id, a_id).await.unwrap().is_none()); - assert!(db.get_channel(cargo_id, a_id).await.unwrap().is_none()); - assert!(db.get_channel(cargo_ra_id, a_id).await.unwrap().is_none()); + assert!(db.get_channel(rust_id, a_id).await.is_err()); + assert!(db.get_channel(cargo_id, a_id).await.is_err()); + assert!(db.get_channel(cargo_ra_id, a_id).await.is_err()); } test_both_dbs!( @@ -198,23 +206,30 @@ async fn test_joining_channels(db: &Arc) { .unwrap() .user_id; - let channel_1 = db - .create_root_channel("channel_1", "1", user_1) - .await - .unwrap(); - let room_1 = db.room_id_for_channel(channel_1).await.unwrap(); + let channel_1 = db.create_root_channel("channel_1", user_1).await.unwrap(); // can join a room with membership to its channel - let joined_room = db - .join_room(room_1, user_1, ConnectionId { owner_id, id: 1 }) + let (joined_room, _) = db + .join_channel( + channel_1, + user_1, + ConnectionId { owner_id, id: 1 }, + TEST_RELEASE_CHANNEL, + ) .await .unwrap(); assert_eq!(joined_room.room.participants.len(), 1); + let room_id = RoomId::from_proto(joined_room.room.id); drop(joined_room); // cannot join a room without membership to its channel assert!(db - .join_room(room_1, user_2, ConnectionId { owner_id, id: 1 }) + .join_room( + room_id, + user_2, + ConnectionId { owner_id, id: 1 }, + TEST_RELEASE_CHANNEL + ) .await .is_err()); } @@ -228,64 +243,21 @@ test_both_dbs!( async fn test_channel_invites(db: &Arc) { db.create_server("test").await.unwrap(); - let user_1 = db - .create_user( - "user1@example.com", - false, - NewUserParams { - github_login: "user1".into(), - github_user_id: 5, - invite_count: 0, - }, - ) - .await - .unwrap() - .user_id; - let user_2 = db - .create_user( - "user2@example.com", - false, - NewUserParams { - github_login: "user2".into(), - github_user_id: 6, - invite_count: 0, - }, - ) - .await - .unwrap() - .user_id; + let user_1 = new_test_user(db, "user1@example.com").await; + let user_2 = new_test_user(db, "user2@example.com").await; + let user_3 = new_test_user(db, "user3@example.com").await; - let user_3 = db - .create_user( - "user3@example.com", - false, - NewUserParams { - github_login: "user3".into(), - github_user_id: 7, - invite_count: 0, - }, - ) - .await - .unwrap() - .user_id; + let channel_1_1 = db.create_root_channel("channel_1", user_1).await.unwrap(); - let channel_1_1 = db - .create_root_channel("channel_1", "1", user_1) + let channel_1_2 = db.create_root_channel("channel_2", user_1).await.unwrap(); + + db.invite_channel_member(channel_1_1, user_2, user_1, ChannelRole::Member) .await .unwrap(); - - let channel_1_2 = db - .create_root_channel("channel_2", "2", user_1) + db.invite_channel_member(channel_1_2, user_2, user_1, ChannelRole::Member) .await .unwrap(); - - db.invite_channel_member(channel_1_1, user_2, user_1, false) - .await - .unwrap(); - db.invite_channel_member(channel_1_2, user_2, user_1, false) - .await - .unwrap(); - db.invite_channel_member(channel_1_1, user_3, user_1, true) + db.invite_channel_member(channel_1_1, user_3, user_1, ChannelRole::Admin) .await .unwrap(); @@ -309,27 +281,29 @@ async fn test_channel_invites(db: &Arc) { assert_eq!(user_3_invites, &[channel_1_1]); - let members = db - .get_channel_member_details(channel_1_1, user_1) + let mut members = db + .get_channel_participant_details(channel_1_1, user_1) .await .unwrap(); + + members.sort_by_key(|member| member.user_id); assert_eq!( members, &[ proto::ChannelMember { user_id: user_1.to_proto(), kind: proto::channel_member::Kind::Member.into(), - admin: true, + role: proto::ChannelRole::Admin.into(), }, proto::ChannelMember { user_id: user_2.to_proto(), kind: proto::channel_member::Kind::Invitee.into(), - admin: false, + role: proto::ChannelRole::Member.into(), }, proto::ChannelMember { user_id: user_3.to_proto(), kind: proto::channel_member::Kind::Invitee.into(), - admin: true, + role: proto::ChannelRole::Admin.into(), }, ] ); @@ -339,12 +313,12 @@ async fn test_channel_invites(db: &Arc) { .unwrap(); let channel_1_3 = db - .create_channel("channel_3", Some(channel_1_1), "1", user_1) + .create_channel("channel_3", Some(channel_1_1), user_1) .await .unwrap(); let members = db - .get_channel_member_details(channel_1_3, user_1) + .get_channel_participant_details(channel_1_3, user_1) .await .unwrap(); assert_eq!( @@ -353,12 +327,12 @@ async fn test_channel_invites(db: &Arc) { proto::ChannelMember { user_id: user_1.to_proto(), kind: proto::channel_member::Kind::Member.into(), - admin: true, + role: proto::ChannelRole::Admin.into(), }, proto::ChannelMember { user_id: user_2.to_proto(), kind: proto::channel_member::Kind::AncestorMember.into(), - admin: false, + role: proto::ChannelRole::Member.into(), }, ] ); @@ -401,7 +375,7 @@ async fn test_channel_renames(db: &Arc) { .unwrap() .user_id; - let zed_id = db.create_root_channel("zed", "1", user_1).await.unwrap(); + let zed_id = db.create_root_channel("zed", user_1).await.unwrap(); db.rename_channel(zed_id, user_1, "#zed-archive") .await @@ -409,11 +383,7 @@ async fn test_channel_renames(db: &Arc) { let zed_archive_id = zed_id; - let (channel, _) = db - .get_channel(zed_archive_id, user_1) - .await - .unwrap() - .unwrap(); + let channel = db.get_channel(zed_archive_id, user_1).await.unwrap(); assert_eq!(channel.name, "zed-archive"); let non_permissioned_rename = db @@ -446,25 +416,22 @@ async fn test_db_channel_moving(db: &Arc) { .unwrap() .user_id; - let zed_id = db.create_root_channel("zed", "1", a_id).await.unwrap(); + let zed_id = db.create_root_channel("zed", a_id).await.unwrap(); - let crdb_id = db - .create_channel("crdb", Some(zed_id), "2", a_id) - .await - .unwrap(); + let crdb_id = db.create_channel("crdb", Some(zed_id), a_id).await.unwrap(); let gpui2_id = db - .create_channel("gpui2", Some(zed_id), "3", a_id) + .create_channel("gpui2", Some(zed_id), a_id) .await .unwrap(); let livestreaming_id = db - .create_channel("livestreaming", Some(crdb_id), "4", a_id) + .create_channel("livestreaming", Some(crdb_id), a_id) .await .unwrap(); let livestreaming_dag_id = db - .create_channel("livestreaming_dag", Some(livestreaming_id), "5", a_id) + .create_channel("livestreaming_dag", Some(livestreaming_id), a_id) .await .unwrap(); @@ -517,12 +484,7 @@ async fn test_db_channel_moving(db: &Arc) { // ======================================================================== // Create a new channel below a channel with multiple parents let livestreaming_dag_sub_id = db - .create_channel( - "livestreaming_dag_sub", - Some(livestreaming_dag_id), - "6", - a_id, - ) + .create_channel("livestreaming_dag_sub", Some(livestreaming_dag_id), a_id) .await .unwrap(); @@ -812,15 +774,15 @@ async fn test_db_channel_moving_bugs(db: &Arc) { .unwrap() .user_id; - let zed_id = db.create_root_channel("zed", "1", user_id).await.unwrap(); + let zed_id = db.create_root_channel("zed", user_id).await.unwrap(); let projects_id = db - .create_channel("projects", Some(zed_id), "2", user_id) + .create_channel("projects", Some(zed_id), user_id) .await .unwrap(); let livestreaming_id = db - .create_channel("livestreaming", Some(projects_id), "3", user_id) + .create_channel("livestreaming", Some(projects_id), user_id) .await .unwrap(); @@ -849,6 +811,284 @@ async fn test_db_channel_moving_bugs(db: &Arc) { ); } +test_both_dbs!( + test_user_is_channel_participant, + test_user_is_channel_participant_postgres, + test_user_is_channel_participant_sqlite +); + +async fn test_user_is_channel_participant(db: &Arc) { + let admin = new_test_user(db, "admin@example.com").await; + let member = new_test_user(db, "member@example.com").await; + let guest = new_test_user(db, "guest@example.com").await; + + let zed_channel = db.create_root_channel("zed", admin).await.unwrap(); + let active_channel = db + .create_channel("active", Some(zed_channel), admin) + .await + .unwrap(); + let vim_channel = db + .create_channel("vim", Some(active_channel), admin) + .await + .unwrap(); + + db.set_channel_visibility(vim_channel, crate::db::ChannelVisibility::Public, admin) + .await + .unwrap(); + db.invite_channel_member(active_channel, member, admin, ChannelRole::Member) + .await + .unwrap(); + db.invite_channel_member(vim_channel, guest, admin, ChannelRole::Guest) + .await + .unwrap(); + + db.respond_to_channel_invite(active_channel, member, true) + .await + .unwrap(); + + db.transaction(|tx| async move { + db.check_user_is_channel_participant(vim_channel, admin, &*tx) + .await + }) + .await + .unwrap(); + db.transaction(|tx| async move { + db.check_user_is_channel_participant(vim_channel, member, &*tx) + .await + }) + .await + .unwrap(); + + let mut members = db + .get_channel_participant_details(vim_channel, admin) + .await + .unwrap(); + + members.sort_by_key(|member| member.user_id); + + assert_eq!( + members, + &[ + proto::ChannelMember { + user_id: admin.to_proto(), + kind: proto::channel_member::Kind::Member.into(), + role: proto::ChannelRole::Admin.into(), + }, + proto::ChannelMember { + user_id: member.to_proto(), + kind: proto::channel_member::Kind::AncestorMember.into(), + role: proto::ChannelRole::Member.into(), + }, + proto::ChannelMember { + user_id: guest.to_proto(), + kind: proto::channel_member::Kind::Invitee.into(), + role: proto::ChannelRole::Guest.into(), + }, + ] + ); + + db.respond_to_channel_invite(vim_channel, guest, true) + .await + .unwrap(); + + db.transaction(|tx| async move { + db.check_user_is_channel_participant(vim_channel, guest, &*tx) + .await + }) + .await + .unwrap(); + + let channels = db.get_channels_for_user(guest).await.unwrap().channels; + assert_dag(channels, &[(vim_channel, None)]); + let channels = db.get_channels_for_user(member).await.unwrap().channels; + assert_dag( + channels, + &[(active_channel, None), (vim_channel, Some(active_channel))], + ); + + db.set_channel_member_role(vim_channel, admin, guest, ChannelRole::Banned) + .await + .unwrap(); + assert!(db + .transaction(|tx| async move { + db.check_user_is_channel_participant(vim_channel, guest, &*tx) + .await + }) + .await + .is_err()); + + let mut members = db + .get_channel_participant_details(vim_channel, admin) + .await + .unwrap(); + + members.sort_by_key(|member| member.user_id); + + assert_eq!( + members, + &[ + proto::ChannelMember { + user_id: admin.to_proto(), + kind: proto::channel_member::Kind::Member.into(), + role: proto::ChannelRole::Admin.into(), + }, + proto::ChannelMember { + user_id: member.to_proto(), + kind: proto::channel_member::Kind::AncestorMember.into(), + role: proto::ChannelRole::Member.into(), + }, + proto::ChannelMember { + user_id: guest.to_proto(), + kind: proto::channel_member::Kind::Member.into(), + role: proto::ChannelRole::Banned.into(), + }, + ] + ); + + db.remove_channel_member(vim_channel, guest, admin) + .await + .unwrap(); + + db.set_channel_visibility(zed_channel, crate::db::ChannelVisibility::Public, admin) + .await + .unwrap(); + + db.invite_channel_member(zed_channel, guest, admin, ChannelRole::Guest) + .await + .unwrap(); + + // currently people invited to parent channels are not shown here + let mut members = db + .get_channel_participant_details(vim_channel, admin) + .await + .unwrap(); + + members.sort_by_key(|member| member.user_id); + + assert_eq!( + members, + &[ + proto::ChannelMember { + user_id: admin.to_proto(), + kind: proto::channel_member::Kind::Member.into(), + role: proto::ChannelRole::Admin.into(), + }, + proto::ChannelMember { + user_id: member.to_proto(), + kind: proto::channel_member::Kind::AncestorMember.into(), + role: proto::ChannelRole::Member.into(), + }, + ] + ); + + db.respond_to_channel_invite(zed_channel, guest, true) + .await + .unwrap(); + + db.transaction(|tx| async move { + db.check_user_is_channel_participant(zed_channel, guest, &*tx) + .await + }) + .await + .unwrap(); + assert!(db + .transaction(|tx| async move { + db.check_user_is_channel_participant(active_channel, guest, &*tx) + .await + }) + .await + .is_err(),); + + db.transaction(|tx| async move { + db.check_user_is_channel_participant(vim_channel, guest, &*tx) + .await + }) + .await + .unwrap(); + + let mut members = db + .get_channel_participant_details(vim_channel, admin) + .await + .unwrap(); + + members.sort_by_key(|member| member.user_id); + + assert_eq!( + members, + &[ + proto::ChannelMember { + user_id: admin.to_proto(), + kind: proto::channel_member::Kind::Member.into(), + role: proto::ChannelRole::Admin.into(), + }, + proto::ChannelMember { + user_id: member.to_proto(), + kind: proto::channel_member::Kind::AncestorMember.into(), + role: proto::ChannelRole::Member.into(), + }, + proto::ChannelMember { + user_id: guest.to_proto(), + kind: proto::channel_member::Kind::AncestorMember.into(), + role: proto::ChannelRole::Guest.into(), + }, + ] + ); + + let channels = db.get_channels_for_user(guest).await.unwrap().channels; + assert_dag( + channels, + &[(zed_channel, None), (vim_channel, Some(zed_channel))], + ) +} + +test_both_dbs!( + test_user_joins_correct_channel, + test_user_joins_correct_channel_postgres, + test_user_joins_correct_channel_sqlite +); + +async fn test_user_joins_correct_channel(db: &Arc) { + let admin = new_test_user(db, "admin@example.com").await; + + let zed_channel = db.create_root_channel("zed", admin).await.unwrap(); + + let active_channel = db + .create_channel("active", Some(zed_channel), admin) + .await + .unwrap(); + + let vim_channel = db + .create_channel("vim", Some(active_channel), admin) + .await + .unwrap(); + + let vim2_channel = db + .create_channel("vim2", Some(vim_channel), admin) + .await + .unwrap(); + + db.set_channel_visibility(zed_channel, crate::db::ChannelVisibility::Public, admin) + .await + .unwrap(); + + db.set_channel_visibility(vim_channel, crate::db::ChannelVisibility::Public, admin) + .await + .unwrap(); + + db.set_channel_visibility(vim2_channel, crate::db::ChannelVisibility::Public, admin) + .await + .unwrap(); + + let most_public = db + .transaction( + |tx| async move { db.most_public_ancestor_for_channel(vim_channel, &*tx).await }, + ) + .await + .unwrap(); + + assert_eq!(most_public, Some(zed_channel)) +} + #[track_caller] fn assert_dag(actual: ChannelGraph, expected: &[(ChannelId, Option)]) { let mut actual_map: HashMap> = HashMap::default(); @@ -873,3 +1113,20 @@ fn assert_dag(actual: ChannelGraph, expected: &[(ChannelId, Option)]) pretty_assertions::assert_eq!(actual_map, expected_map) } + +static GITHUB_USER_ID: AtomicI32 = AtomicI32::new(5); + +async fn new_test_user(db: &Arc, email: &str) -> UserId { + db.create_user( + email, + false, + NewUserParams { + github_login: email[0..email.find("@").unwrap()].to_string(), + github_user_id: GITHUB_USER_ID.fetch_add(1, Ordering::SeqCst), + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id +} diff --git a/crates/collab/src/db/tests/db_tests.rs b/crates/collab/src/db/tests/db_tests.rs index 9a617166fe..1520e081c0 100644 --- a/crates/collab/src/db/tests/db_tests.rs +++ b/crates/collab/src/db/tests/db_tests.rs @@ -479,7 +479,7 @@ async fn test_project_count(db: &Arc) { .unwrap(); let room_id = RoomId::from_proto( - db.create_room(user1.user_id, ConnectionId { owner_id, id: 0 }, "") + db.create_room(user1.user_id, ConnectionId { owner_id, id: 0 }, "", "dev") .await .unwrap() .id, @@ -493,9 +493,14 @@ async fn test_project_count(db: &Arc) { ) .await .unwrap(); - db.join_room(room_id, user2.user_id, ConnectionId { owner_id, id: 1 }) - .await - .unwrap(); + db.join_room( + room_id, + user2.user_id, + ConnectionId { owner_id, id: 1 }, + "dev", + ) + .await + .unwrap(); assert_eq!(db.project_count_excluding_admins().await.unwrap(), 0); db.share_project(room_id, ConnectionId { owner_id, id: 1 }, &[]) @@ -575,6 +580,85 @@ async fn test_fuzzy_search_users() { } } +test_both_dbs!( + test_non_matching_release_channels, + test_non_matching_release_channels_postgres, + test_non_matching_release_channels_sqlite +); + +async fn test_non_matching_release_channels(db: &Arc) { + let owner_id = db.create_server("test").await.unwrap().0 as u32; + + let user1 = db + .create_user( + &format!("admin@example.com"), + true, + NewUserParams { + github_login: "admin".into(), + github_user_id: 0, + invite_count: 0, + }, + ) + .await + .unwrap(); + let user2 = db + .create_user( + &format!("user@example.com"), + false, + NewUserParams { + github_login: "user".into(), + github_user_id: 1, + invite_count: 0, + }, + ) + .await + .unwrap(); + + let room = db + .create_room( + user1.user_id, + ConnectionId { owner_id, id: 0 }, + "", + "stable", + ) + .await + .unwrap(); + + db.call( + RoomId::from_proto(room.id), + user1.user_id, + ConnectionId { owner_id, id: 0 }, + user2.user_id, + None, + ) + .await + .unwrap(); + + // User attempts to join from preview + let result = db + .join_room( + RoomId::from_proto(room.id), + user2.user_id, + ConnectionId { owner_id, id: 1 }, + "preview", + ) + .await; + + assert!(result.is_err()); + + // User switches to stable + let result = db + .join_room( + RoomId::from_proto(room.id), + user2.user_id, + ConnectionId { owner_id, id: 1 }, + "stable", + ) + .await; + + assert!(result.is_ok()) +} + fn build_background_executor() -> Arc { Deterministic::new(0).build_background() } diff --git a/crates/collab/src/db/tests/message_tests.rs b/crates/collab/src/db/tests/message_tests.rs index 464aaba207..272d8e0100 100644 --- a/crates/collab/src/db/tests/message_tests.rs +++ b/crates/collab/src/db/tests/message_tests.rs @@ -1,5 +1,5 @@ use crate::{ - db::{Database, MessageId, NewUserParams}, + db::{ChannelRole, Database, MessageId, NewUserParams}, test_both_dbs, }; use std::sync::Arc; @@ -25,10 +25,7 @@ async fn test_channel_message_retrieval(db: &Arc) { .await .unwrap() .user_id; - let channel = db - .create_channel("channel", None, "room", user) - .await - .unwrap(); + let channel = db.create_channel("channel", None, user).await.unwrap(); let owner_id = db.create_server("test").await.unwrap().0 as u32; db.join_channel_chat(channel, rpc::ConnectionId { owner_id, id: 0 }, user) @@ -90,10 +87,7 @@ async fn test_channel_message_nonces(db: &Arc) { .await .unwrap() .user_id; - let channel = db - .create_channel("channel", None, "room", user) - .await - .unwrap(); + let channel = db.create_channel("channel", None, user).await.unwrap(); let owner_id = db.create_server("test").await.unwrap().0 as u32; @@ -157,17 +151,11 @@ async fn test_channel_message_new_notification(db: &Arc) { .unwrap() .user_id; - let channel_1 = db - .create_channel("channel", None, "room", user) - .await - .unwrap(); + let channel_1 = db.create_channel("channel", None, user).await.unwrap(); - let channel_2 = db - .create_channel("channel-2", None, "room", user) - .await - .unwrap(); + let channel_2 = db.create_channel("channel-2", None, user).await.unwrap(); - db.invite_channel_member(channel_1, observer, user, false) + db.invite_channel_member(channel_1, observer, user, ChannelRole::Member) .await .unwrap(); @@ -175,7 +163,7 @@ async fn test_channel_message_new_notification(db: &Arc) { .await .unwrap(); - db.invite_channel_member(channel_2, observer, user, false) + db.invite_channel_member(channel_2, observer, user, ChannelRole::Member) .await .unwrap(); diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 5eb434e167..15ea3b24e1 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -3,8 +3,8 @@ mod connection_pool; use crate::{ auth, db::{ - self, BufferId, ChannelId, ChannelsForUser, Database, MessageId, ProjectId, RoomId, - ServerId, User, UserId, + self, BufferId, ChannelId, ChannelVisibility, ChannelsForUser, Database, MessageId, + ProjectId, RoomId, ServerId, User, UserId, }, executor::Executor, AppState, Result, @@ -63,6 +63,7 @@ use time::OffsetDateTime; use tokio::sync::{watch, Semaphore}; use tower::ServiceBuilder; use tracing::{info_span, instrument, Instrument}; +use util::channel::RELEASE_CHANNEL_NAME; pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30); pub const CLEANUP_TIMEOUT: Duration = Duration::from_secs(10); @@ -224,6 +225,7 @@ impl Server { .add_request_handler(forward_project_request::) .add_request_handler(forward_project_request::) .add_request_handler(forward_project_request::) + .add_request_handler(forward_project_request::) .add_request_handler(forward_project_request::) .add_request_handler(forward_project_request::) .add_request_handler(forward_project_request::) @@ -253,7 +255,8 @@ impl Server { .add_request_handler(delete_channel) .add_request_handler(invite_channel_member) .add_request_handler(remove_channel_member) - .add_request_handler(set_channel_member_admin) + .add_request_handler(set_channel_member_role) + .add_request_handler(set_channel_visibility) .add_request_handler(rename_channel) .add_request_handler(join_channel_buffer) .add_request_handler(leave_channel_buffer) @@ -937,11 +940,6 @@ async fn create_room( util::async_iife!({ let live_kit = live_kit?; - live_kit - .create_room(live_kit_room.clone()) - .await - .trace_err()?; - let token = live_kit .room_token(&live_kit_room, &session.user_id.to_string()) .trace_err()?; @@ -957,7 +955,12 @@ async fn create_room( let room = session .db() .await - .create_room(session.user_id, session.connection_id, &live_kit_room) + .create_room( + session.user_id, + session.connection_id, + &live_kit_room, + RELEASE_CHANNEL_NAME.as_str(), + ) .await?; response.send(proto::CreateRoomResponse { @@ -975,26 +978,28 @@ async fn join_room( session: Session, ) -> Result<()> { let room_id = RoomId::from_proto(request.id); + + let channel_id = session.db().await.channel_id_for_room(room_id).await?; + + if let Some(channel_id) = channel_id { + return join_channel_internal(channel_id, Box::new(response), session).await; + } + let joined_room = { let room = session .db() .await - .join_room(room_id, session.user_id, session.connection_id) + .join_room( + room_id, + session.user_id, + session.connection_id, + RELEASE_CHANNEL_NAME.as_str(), + ) .await?; room_updated(&room.room, &session.peer); room.into_inner() }; - if let Some(channel_id) = joined_room.channel_id { - channel_updated( - channel_id, - &joined_room.room, - &joined_room.channel_members, - &session.peer, - &*session.connection_pool().await, - ) - } - for connection_id in session .connection_pool() .await @@ -1032,7 +1037,7 @@ async fn join_room( response.send(proto::JoinRoomResponse { room: Some(joined_room.room), - channel_id: joined_room.channel_id.map(|id| id.to_proto()), + channel_id: None, live_kit_connection_info, })?; @@ -2195,20 +2200,16 @@ async fn create_channel( session: Session, ) -> Result<()> { let db = session.db().await; - let live_kit_room = format!("channel-{}", nanoid::nanoid!(30)); - - if let Some(live_kit) = session.live_kit_client.as_ref() { - live_kit.create_room(live_kit_room.clone()).await?; - } let parent_id = request.parent_id.map(|id| ChannelId::from_proto(id)); let id = db - .create_channel(&request.name, parent_id, &live_kit_room, session.user_id) + .create_channel(&request.name, parent_id, session.user_id) .await?; let channel = proto::Channel { id: id.to_proto(), name: request.name, + visibility: proto::ChannelVisibility::Members as i32, }; response.send(proto::CreateChannelResponse { @@ -2281,17 +2282,20 @@ async fn invite_channel_member( let db = session.db().await; let channel_id = ChannelId::from_proto(request.channel_id); let invitee_id = UserId::from_proto(request.user_id); - db.invite_channel_member(channel_id, invitee_id, session.user_id, request.admin) - .await?; + db.invite_channel_member( + channel_id, + invitee_id, + session.user_id, + request.role().into(), + ) + .await?; - let (channel, _) = db - .get_channel(channel_id, session.user_id) - .await? - .ok_or_else(|| anyhow!("channel not found"))?; + let channel = db.get_channel(channel_id, session.user_id).await?; let mut update = proto::UpdateChannels::default(); update.channel_invitations.push(proto::Channel { id: channel.id.to_proto(), + visibility: channel.visibility.into(), name: channel.name, }); for connection_id in session @@ -2333,27 +2337,63 @@ async fn remove_channel_member( Ok(()) } -async fn set_channel_member_admin( - request: proto::SetChannelMemberAdmin, - response: Response, +async fn set_channel_visibility( + request: proto::SetChannelVisibility, + response: Response, + session: Session, +) -> Result<()> { + let db = session.db().await; + let channel_id = ChannelId::from_proto(request.channel_id); + let visibility = request.visibility().into(); + + let channel = db + .set_channel_visibility(channel_id, visibility, session.user_id) + .await?; + + let mut update = proto::UpdateChannels::default(); + update.channels.push(proto::Channel { + id: channel.id.to_proto(), + name: channel.name, + visibility: channel.visibility.into(), + }); + + let member_ids = db.get_channel_members(channel_id).await?; + + let connection_pool = session.connection_pool().await; + for member_id in member_ids { + for connection_id in connection_pool.user_connection_ids(member_id) { + session.peer.send(connection_id, update.clone())?; + } + } + + response.send(proto::Ack {})?; + Ok(()) +} + +async fn set_channel_member_role( + request: proto::SetChannelMemberRole, + response: Response, session: Session, ) -> Result<()> { let db = session.db().await; let channel_id = ChannelId::from_proto(request.channel_id); let member_id = UserId::from_proto(request.user_id); - db.set_channel_member_admin(channel_id, session.user_id, member_id, request.admin) + let channel_member = db + .set_channel_member_role( + channel_id, + session.user_id, + member_id, + request.role().into(), + ) .await?; - let (channel, has_accepted) = db - .get_channel(channel_id, member_id) - .await? - .ok_or_else(|| anyhow!("channel not found"))?; + let channel = db.get_channel(channel_id, session.user_id).await?; let mut update = proto::UpdateChannels::default(); - if has_accepted { + if channel_member.accepted { update.channel_permissions.push(proto::ChannelPermission { channel_id: channel.id.to_proto(), - is_admin: request.admin, + role: request.role, }); } @@ -2376,13 +2416,14 @@ async fn rename_channel( ) -> Result<()> { let db = session.db().await; let channel_id = ChannelId::from_proto(request.channel_id); - let new_name = db + let channel = db .rename_channel(channel_id, session.user_id, &request.name) .await?; let channel = proto::Channel { - id: request.channel_id, - name: new_name, + id: channel.id.to_proto(), + name: channel.name, + visibility: channel.visibility.into(), }; response.send(proto::RenameChannelResponse { channel: Some(channel.clone()), @@ -2420,6 +2461,7 @@ async fn link_channel( .into_iter() .map(|channel| proto::Channel { id: channel.id.to_proto(), + visibility: channel.visibility.into(), name: channel.name, }) .collect(), @@ -2511,6 +2553,7 @@ async fn move_channel( .into_iter() .map(|channel| proto::Channel { id: channel.id.to_proto(), + visibility: channel.visibility.into(), name: channel.name, }) .collect(), @@ -2536,7 +2579,7 @@ async fn get_channel_members( let db = session.db().await; let channel_id = ChannelId::from_proto(request.channel_id); let members = db - .get_channel_member_details(channel_id, session.user_id) + .get_channel_participant_details(channel_id, session.user_id) .await?; response.send(proto::GetChannelMembersResponse { members })?; Ok(()) @@ -2552,53 +2595,68 @@ async fn respond_to_channel_invite( db.respond_to_channel_invite(channel_id, session.user_id, request.accept) .await?; + if request.accept { + channel_membership_updated(db, channel_id, &session).await?; + } else { + let mut update = proto::UpdateChannels::default(); + update + .remove_channel_invitations + .push(channel_id.to_proto()); + session.peer.send(session.connection_id, update)?; + } + response.send(proto::Ack {})?; + + Ok(()) +} + +async fn channel_membership_updated( + db: tokio::sync::MutexGuard<'_, DbHandle>, + channel_id: ChannelId, + session: &Session, +) -> Result<(), crate::Error> { let mut update = proto::UpdateChannels::default(); update .remove_channel_invitations .push(channel_id.to_proto()); - if request.accept { - let result = db.get_channel_for_user(channel_id, session.user_id).await?; - update - .channels - .extend( - result - .channels - .channels - .into_iter() - .map(|channel| proto::Channel { - id: channel.id.to_proto(), - name: channel.name, - }), - ); - update.unseen_channel_messages = result.channel_messages; - update.unseen_channel_buffer_changes = result.unseen_buffer_changes; - update.insert_edge = result.channels.edges; - update - .channel_participants - .extend( - result - .channel_participants - .into_iter() - .map(|(channel_id, user_ids)| proto::ChannelParticipants { - channel_id: channel_id.to_proto(), - participant_user_ids: user_ids.into_iter().map(UserId::to_proto).collect(), - }), - ); - update - .channel_permissions - .extend( - result - .channels_with_admin_privileges - .into_iter() - .map(|channel_id| proto::ChannelPermission { - channel_id: channel_id.to_proto(), - is_admin: true, - }), - ); - } - session.peer.send(session.connection_id, update)?; - response.send(proto::Ack {})?; + let result = db.get_channel_for_user(channel_id, session.user_id).await?; + update.channels.extend( + result + .channels + .channels + .into_iter() + .map(|channel| proto::Channel { + id: channel.id.to_proto(), + visibility: channel.visibility.into(), + name: channel.name, + }), + ); + update.unseen_channel_messages = result.channel_messages; + update.unseen_channel_buffer_changes = result.unseen_buffer_changes; + update.insert_edge = result.channels.edges; + update + .channel_participants + .extend( + result + .channel_participants + .into_iter() + .map(|(channel_id, user_ids)| proto::ChannelParticipants { + channel_id: channel_id.to_proto(), + participant_user_ids: user_ids.into_iter().map(UserId::to_proto).collect(), + }), + ); + update + .channel_permissions + .extend( + result + .channels_with_admin_privileges + .into_iter() + .map(|channel_id| proto::ChannelPermission { + channel_id: channel_id.to_proto(), + role: proto::ChannelRole::Admin.into(), + }), + ); + session.peer.send(session.connection_id, update)?; Ok(()) } @@ -2608,15 +2666,39 @@ async fn join_channel( session: Session, ) -> Result<()> { let channel_id = ChannelId::from_proto(request.channel_id); + join_channel_internal(channel_id, Box::new(response), session).await +} +trait JoinChannelInternalResponse { + fn send(self, result: proto::JoinRoomResponse) -> Result<()>; +} +impl JoinChannelInternalResponse for Response { + fn send(self, result: proto::JoinRoomResponse) -> Result<()> { + Response::::send(self, result) + } +} +impl JoinChannelInternalResponse for Response { + fn send(self, result: proto::JoinRoomResponse) -> Result<()> { + Response::::send(self, result) + } +} + +async fn join_channel_internal( + channel_id: ChannelId, + response: Box, + session: Session, +) -> Result<()> { let joined_room = { leave_room_for_session(&session).await?; let db = session.db().await; - let room_id = db.room_id_for_channel(channel_id).await?; - - let joined_room = db - .join_room(room_id, session.user_id, session.connection_id) + let (joined_room, joined_channel) = db + .join_channel( + channel_id, + session.user_id, + session.connection_id, + RELEASE_CHANNEL_NAME.as_str(), + ) .await?; let live_kit_connection_info = session.live_kit_client.as_ref().and_then(|live_kit| { @@ -2639,9 +2721,13 @@ async fn join_channel( live_kit_connection_info, })?; + if let Some(joined_channel) = joined_channel { + channel_membership_updated(db, joined_channel, &session).await? + } + room_updated(&joined_room.room, &session.peer); - joined_room.into_inner() + joined_room }; channel_updated( @@ -2653,7 +2739,6 @@ async fn join_channel( ); update_user_contacts(session.user_id, &session).await?; - Ok(()) } @@ -3063,6 +3148,7 @@ fn build_initial_channels_update( update.channels.push(proto::Channel { id: channel.id.to_proto(), name: channel.name, + visibility: channel.visibility.into(), }); } @@ -3087,7 +3173,7 @@ fn build_initial_channels_update( .into_iter() .map(|id| proto::ChannelPermission { channel_id: id.to_proto(), - is_admin: true, + role: proto::ChannelRole::Admin.into(), }), ); @@ -3095,6 +3181,8 @@ fn build_initial_channels_update( update.channel_invitations.push(proto::Channel { id: channel.id.to_proto(), name: channel.name, + // TODO: Visibility + visibility: ChannelVisibility::Public.into(), }); } diff --git a/crates/collab/src/tests/channel_buffer_tests.rs b/crates/collab/src/tests/channel_buffer_tests.rs index a0b9b52484..14ae159ab8 100644 --- a/crates/collab/src/tests/channel_buffer_tests.rs +++ b/crates/collab/src/tests/channel_buffer_tests.rs @@ -11,7 +11,10 @@ use collections::HashMap; use editor::{Anchor, Editor, ToOffset}; use futures::future; use gpui::{executor::Deterministic, ModelHandle, TestAppContext, ViewContext}; -use rpc::{proto::PeerId, RECEIVE_TIMEOUT}; +use rpc::{ + proto::{self, PeerId}, + RECEIVE_TIMEOUT, +}; use serde_json::json; use std::{ops::Range, sync::Arc}; @@ -445,6 +448,7 @@ fn channel(id: u64, name: &'static str) -> Channel { Channel { id, name: name.to_string(), + visibility: proto::ChannelVisibility::Members, unseen_note_version: None, unseen_message_id: None, } diff --git a/crates/collab/src/tests/channel_tests.rs b/crates/collab/src/tests/channel_tests.rs index 6bdcee6af3..1bb8c92ac8 100644 --- a/crates/collab/src/tests/channel_tests.rs +++ b/crates/collab/src/tests/channel_tests.rs @@ -6,7 +6,10 @@ use call::ActiveCall; use channel::{ChannelId, ChannelMembership, ChannelStore}; use client::User; use gpui::{executor::Deterministic, ModelHandle, TestAppContext}; -use rpc::{proto, RECEIVE_TIMEOUT}; +use rpc::{ + proto::{self, ChannelRole}, + RECEIVE_TIMEOUT, +}; use std::sync::Arc; #[gpui::test] @@ -68,7 +71,12 @@ async fn test_core_channels( .update(cx_a, |store, cx| { assert!(!store.has_pending_channel_invite(channel_a_id, client_b.user_id().unwrap())); - let invite = store.invite_member(channel_a_id, client_b.user_id().unwrap(), false, cx); + let invite = store.invite_member( + channel_a_id, + client_b.user_id().unwrap(), + proto::ChannelRole::Member, + cx, + ); // Make sure we're synchronously storing the pending invite assert!(store.has_pending_channel_invite(channel_a_id, client_b.user_id().unwrap())); @@ -103,12 +111,12 @@ async fn test_core_channels( &[ ( client_a.user_id().unwrap(), - true, + proto::ChannelRole::Admin, proto::channel_member::Kind::Member, ), ( client_b.user_id().unwrap(), - false, + proto::ChannelRole::Member, proto::channel_member::Kind::Invitee, ), ], @@ -183,7 +191,12 @@ async fn test_core_channels( client_a .channel_store() .update(cx_a, |store, cx| { - store.set_member_admin(channel_a_id, client_b.user_id().unwrap(), true, cx) + store.set_member_role( + channel_a_id, + client_b.user_id().unwrap(), + proto::ChannelRole::Admin, + cx, + ) }) .await .unwrap(); @@ -305,12 +318,12 @@ fn assert_participants_eq(participants: &[Arc], expected_partitipants: &[u #[track_caller] fn assert_members_eq( members: &[ChannelMembership], - expected_members: &[(u64, bool, proto::channel_member::Kind)], + expected_members: &[(u64, proto::ChannelRole, proto::channel_member::Kind)], ) { assert_eq!( members .iter() - .map(|member| (member.user.id, member.admin, member.kind)) + .map(|member| (member.user.id, member.role, member.kind)) .collect::>(), expected_members ); @@ -380,6 +393,8 @@ async fn test_channel_room( // Give everyone a chance to observe user A joining deterministic.run_until_parked(); + let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone()); + room_a.read_with(cx_a, |room, _| assert!(room.is_connected())); client_a.channel_store().read_with(cx_a, |channels, _| { assert_participants_eq( @@ -609,7 +624,12 @@ async fn test_permissions_update_while_invited( client_a .channel_store() .update(cx_a, |channel_store, cx| { - channel_store.invite_member(rust_id, client_b.user_id().unwrap(), false, cx) + channel_store.invite_member( + rust_id, + client_b.user_id().unwrap(), + proto::ChannelRole::Member, + cx, + ) }) .await .unwrap(); @@ -632,7 +652,12 @@ async fn test_permissions_update_while_invited( client_a .channel_store() .update(cx_a, |channel_store, cx| { - channel_store.set_member_admin(rust_id, client_b.user_id().unwrap(), true, cx) + channel_store.set_member_role( + rust_id, + client_b.user_id().unwrap(), + proto::ChannelRole::Admin, + cx, + ) }) .await .unwrap(); @@ -801,7 +826,12 @@ async fn test_lost_channel_creation( client_a .channel_store() .update(cx_a, |channel_store, cx| { - channel_store.invite_member(channel_id, client_b.user_id().unwrap(), false, cx) + channel_store.invite_member( + channel_id, + client_b.user_id().unwrap(), + proto::ChannelRole::Member, + cx, + ) }) .await .unwrap(); @@ -882,6 +912,119 @@ async fn test_lost_channel_creation( ], ); } +#[gpui::test] +async fn test_guest_access( + deterministic: Arc, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { + deterministic.forbid_parking(); + + let mut server = TestServer::start(&deterministic).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + + let channels = server + .make_channel_tree(&[("channel-a", None)], (&client_a, cx_a)) + .await; + let channel_a_id = channels[0]; + + let active_call_b = cx_b.read(ActiveCall::global); + + // should not be allowed to join + assert!(active_call_b + .update(cx_b, |call, cx| call.join_channel(channel_a_id, cx)) + .await + .is_err()); + + client_a + .channel_store() + .update(cx_a, |channel_store, cx| { + channel_store.set_channel_visibility(channel_a_id, proto::ChannelVisibility::Public, cx) + }) + .await + .unwrap(); + + active_call_b + .update(cx_b, |call, cx| call.join_channel(channel_a_id, cx)) + .await + .unwrap(); + + deterministic.run_until_parked(); + + assert!(client_b + .channel_store() + .update(cx_b, |channel_store, _| channel_store + .channel_for_id(channel_a_id) + .is_some())); + + client_a.channel_store().update(cx_a, |channel_store, _| { + let participants = channel_store.channel_participants(channel_a_id); + assert_eq!(participants.len(), 1); + assert_eq!(participants[0].id, client_b.user_id().unwrap()); + }) +} + +#[gpui::test] +async fn test_invite_access( + deterministic: Arc, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { + deterministic.forbid_parking(); + + let mut server = TestServer::start(&deterministic).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + + let channels = server + .make_channel_tree( + &[("channel-a", None), ("channel-b", Some("channel-a"))], + (&client_a, cx_a), + ) + .await; + let channel_a_id = channels[0]; + let channel_b_id = channels[0]; + + let active_call_b = cx_b.read(ActiveCall::global); + + // should not be allowed to join + assert!(active_call_b + .update(cx_b, |call, cx| call.join_channel(channel_b_id, cx)) + .await + .is_err()); + + client_a + .channel_store() + .update(cx_a, |channel_store, cx| { + channel_store.invite_member( + channel_a_id, + client_b.user_id().unwrap(), + ChannelRole::Member, + cx, + ) + }) + .await + .unwrap(); + + active_call_b + .update(cx_b, |call, cx| call.join_channel(channel_b_id, cx)) + .await + .unwrap(); + + deterministic.run_until_parked(); + + client_b.channel_store().update(cx_b, |channel_store, _| { + assert!(channel_store.channel_for_id(channel_b_id).is_some()); + assert!(channel_store.channel_for_id(channel_a_id).is_some()); + }); + + client_a.channel_store().update(cx_a, |channel_store, _| { + let participants = channel_store.channel_participants(channel_b_id); + assert_eq!(participants.len(), 1); + assert_eq!(participants[0].id, client_b.user_id().unwrap()); + }) +} #[gpui::test] async fn test_channel_moving( diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index 4008a941dd..d6d449fd47 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -15,12 +15,14 @@ use gpui::{executor::Deterministic, test::EmptyView, AppContext, ModelHandle, Te use indoc::indoc; use language::{ language_settings::{AllLanguageSettings, Formatter, InlayHintSettings}, - tree_sitter_rust, Anchor, Diagnostic, DiagnosticEntry, FakeLspAdapter, Language, - LanguageConfig, LineEnding, OffsetRangeExt, Point, Rope, + tree_sitter_rust, Anchor, BundledFormatter, Diagnostic, DiagnosticEntry, FakeLspAdapter, + Language, LanguageConfig, LineEnding, OffsetRangeExt, Point, Rope, }; use live_kit_client::MacOSDisplay; use lsp::LanguageServerId; -use project::{search::SearchQuery, DiagnosticSummary, HoverBlockKind, Project, ProjectPath}; +use project::{ + search::SearchQuery, DiagnosticSummary, FormatTrigger, HoverBlockKind, Project, ProjectPath, +}; use rand::prelude::*; use serde_json::json; use settings::SettingsStore; @@ -4407,8 +4409,6 @@ async fn test_formatting_buffer( cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, ) { - use project::FormatTrigger; - let mut server = TestServer::start(&deterministic).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; @@ -4511,6 +4511,134 @@ async fn test_formatting_buffer( ); } +#[gpui::test(iterations = 10)] +async fn test_prettier_formatting_buffer( + deterministic: Arc, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { + let mut server = TestServer::start(&deterministic).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + .await; + let active_call_a = cx_a.read(ActiveCall::global); + + // Set up a fake language server. + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let test_plugin = "test_plugin"; + let mut fake_language_servers = language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + enabled_formatters: vec![BundledFormatter::Prettier { + parser_name: Some("test_parser"), + plugin_names: vec![test_plugin], + }], + ..Default::default() + })) + .await; + let language = Arc::new(language); + client_a.language_registry().add(Arc::clone(&language)); + + // Here we insert a fake tree with a directory that exists on disk. This is needed + // because later we'll invoke a command, which requires passing a working directory + // that points to a valid location on disk. + let directory = env::current_dir().unwrap(); + let buffer_text = "let one = \"two\""; + client_a + .fs() + .insert_tree(&directory, json!({ "a.rs": buffer_text })) + .await; + let (project_a, worktree_id) = client_a.build_local_project(&directory, cx_a).await; + let prettier_format_suffix = project_a.update(cx_a, |project, _| { + let suffix = project.enable_test_prettier(&[test_plugin]); + project.languages().add(language); + suffix + }); + let buffer_a = cx_a + .background() + .spawn(project_a.update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx))) + .await + .unwrap(); + + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) + .await + .unwrap(); + let project_b = client_b.build_remote_project(project_id, cx_b).await; + let buffer_b = cx_b + .background() + .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx))) + .await + .unwrap(); + + cx_a.update(|cx| { + cx.update_global(|store: &mut SettingsStore, cx| { + store.update_user_settings::(cx, |file| { + file.defaults.formatter = Some(Formatter::Auto); + }); + }); + }); + cx_b.update(|cx| { + cx.update_global(|store: &mut SettingsStore, cx| { + store.update_user_settings::(cx, |file| { + file.defaults.formatter = Some(Formatter::LanguageServer); + }); + }); + }); + let fake_language_server = fake_language_servers.next().await.unwrap(); + fake_language_server.handle_request::(|_, _| async move { + panic!( + "Unexpected: prettier should be preferred since it's enabled and language supports it" + ) + }); + + project_b + .update(cx_b, |project, cx| { + project.format( + HashSet::from_iter([buffer_b.clone()]), + true, + FormatTrigger::Save, + cx, + ) + }) + .await + .unwrap(); + cx_a.foreground().run_until_parked(); + cx_b.foreground().run_until_parked(); + assert_eq!( + buffer_b.read_with(cx_b, |buffer, _| buffer.text()), + buffer_text.to_string() + "\n" + prettier_format_suffix, + "Prettier formatting was not applied to client buffer after client's request" + ); + + project_a + .update(cx_a, |project, cx| { + project.format( + HashSet::from_iter([buffer_a.clone()]), + true, + FormatTrigger::Manual, + cx, + ) + }) + .await + .unwrap(); + cx_a.foreground().run_until_parked(); + cx_b.foreground().run_until_parked(); + assert_eq!( + buffer_b.read_with(cx_b, |buffer, _| buffer.text()), + buffer_text.to_string() + "\n" + prettier_format_suffix + "\n" + prettier_format_suffix, + "Prettier formatting was not applied to client buffer after host's request" + ); +} + #[gpui::test(iterations = 10)] async fn test_definition( deterministic: Arc, diff --git a/crates/collab/src/tests/random_channel_buffer_tests.rs b/crates/collab/src/tests/random_channel_buffer_tests.rs index ad0181602c..1b24c7a3d2 100644 --- a/crates/collab/src/tests/random_channel_buffer_tests.rs +++ b/crates/collab/src/tests/random_channel_buffer_tests.rs @@ -1,3 +1,5 @@ +use crate::db::ChannelRole; + use super::{run_randomized_test, RandomizedTest, TestClient, TestError, TestServer, UserTestPlan}; use anyhow::Result; use async_trait::async_trait; @@ -46,16 +48,11 @@ impl RandomizedTest for RandomChannelBufferTest { let db = &server.app_state.db; for ix in 0..CHANNEL_COUNT { let id = db - .create_channel( - &format!("channel-{ix}"), - None, - &format!("livekit-room-{ix}"), - users[0].user_id, - ) + .create_channel(&format!("channel-{ix}"), None, users[0].user_id) .await .unwrap(); for user in &users[1..] { - db.invite_channel_member(id, user.user_id, users[0].user_id, false) + db.invite_channel_member(id, user.user_id, users[0].user_id, ChannelRole::Member) .await .unwrap(); db.respond_to_channel_invite(id, user.user_id, true) diff --git a/crates/collab/src/tests/test_server.rs b/crates/collab/src/tests/test_server.rs index 2e13874125..c37ea19d52 100644 --- a/crates/collab/src/tests/test_server.rs +++ b/crates/collab/src/tests/test_server.rs @@ -15,9 +15,10 @@ use fs::FakeFs; use futures::{channel::oneshot, StreamExt as _}; use gpui::{executor::Deterministic, ModelHandle, Task, TestAppContext, WindowHandle}; use language::LanguageRegistry; +use node_runtime::FakeNodeRuntime; use parking_lot::Mutex; use project::{Project, WorktreeId}; -use rpc::RECEIVE_TIMEOUT; +use rpc::{proto::ChannelRole, RECEIVE_TIMEOUT}; use settings::SettingsStore; use std::{ cell::{Ref, RefCell, RefMut}, @@ -218,6 +219,7 @@ impl TestServer { build_window_options: |_, _, _| Default::default(), initialize_workspace: |_, _, _, _| Task::ready(Ok(())), background_actions: || &[], + node_runtime: FakeNodeRuntime::new(), }); cx.update(|cx| { @@ -325,7 +327,7 @@ impl TestServer { channel_store.invite_member( channel_id, member_client.user_id().unwrap(), - false, + ChannelRole::Member, cx, ) }) @@ -567,6 +569,7 @@ impl TestClient { cx.update(|cx| { Project::local( self.client().clone(), + self.app_state.node_runtime.clone(), self.app_state.user_store.clone(), self.app_state.languages.clone(), self.app_state.fs.clone(), @@ -613,7 +616,12 @@ impl TestClient { cx_self .read(ChannelStore::global) .update(cx_self, |channel_store, cx| { - channel_store.invite_member(channel, other_client.user_id().unwrap(), true, cx) + channel_store.invite_member( + channel, + other_client.user_id().unwrap(), + ChannelRole::Admin, + cx, + ) }) .await .unwrap(); diff --git a/crates/collab_ui/src/channel_view.rs b/crates/collab_ui/src/channel_view.rs index b2e65eb2fa..e62ee8ef4b 100644 --- a/crates/collab_ui/src/channel_view.rs +++ b/crates/collab_ui/src/channel_view.rs @@ -24,7 +24,7 @@ use workspace::{ item::{FollowableItem, Item, ItemHandle}, register_followable_item, searchable::SearchableItemHandle, - ItemNavHistory, Pane, ViewId, Workspace, WorkspaceId, + ItemNavHistory, Pane, SaveIntent, ViewId, Workspace, WorkspaceId, }; actions!(channel_view, [Deploy]); @@ -93,15 +93,36 @@ impl ChannelView { } pane.update(&mut cx, |pane, cx| { - pane.items_of_type::() - .find(|channel_view| channel_view.read(cx).channel_buffer == channel_buffer) - .unwrap_or_else(|| { - cx.add_view(|cx| { - let mut this = Self::new(project, channel_store, channel_buffer, cx); - this.acknowledge_buffer_version(cx); - this - }) - }) + let buffer_id = channel_buffer.read(cx).remote_id(cx); + + let existing_view = pane + .items_of_type::() + .find(|view| view.read(cx).channel_buffer.read(cx).remote_id(cx) == buffer_id); + + // If this channel buffer is already open in this pane, just return it. + if let Some(existing_view) = existing_view.clone() { + if existing_view.read(cx).channel_buffer == channel_buffer { + return existing_view; + } + } + + let view = cx.add_view(|cx| { + let mut this = Self::new(project, channel_store, channel_buffer, cx); + this.acknowledge_buffer_version(cx); + this + }); + + // If the pane contained a disconnected view for this channel buffer, + // replace that. + if let Some(existing_item) = existing_view { + if let Some(ix) = pane.index_for_item(&existing_item) { + pane.close_item_by_id(existing_item.id(), SaveIntent::Skip, cx) + .detach(); + pane.add_item(Box::new(view.clone()), true, true, Some(ix), cx); + } + } + + view }) .ok_or_else(|| anyhow!("pane was dropped")) }) @@ -285,10 +306,14 @@ impl FollowableItem for ChannelView { } fn to_state_proto(&self, cx: &AppContext) -> Option { - let channel = self.channel_buffer.read(cx).channel(); + let channel_buffer = self.channel_buffer.read(cx); + if !channel_buffer.is_connected() { + return None; + } + Some(proto::view::Variant::ChannelView( proto::view::ChannelView { - channel_id: channel.id, + channel_id: channel_buffer.channel().id, editor: if let Some(proto::view::Variant::Editor(proto)) = self.editor.read(cx).to_state_proto(cx) { diff --git a/crates/collab_ui/src/chat_panel.rs b/crates/collab_ui/src/chat_panel.rs index 1a17b48f19..a8c4006cb8 100644 --- a/crates/collab_ui/src/chat_panel.rs +++ b/crates/collab_ui/src/chat_panel.rs @@ -355,8 +355,12 @@ impl ChatPanel { } fn render_message(&mut self, ix: usize, cx: &mut ViewContext) -> AnyElement { - let (message, is_continuation, is_last) = { + let (message, is_continuation, is_last, is_admin) = { let active_chat = self.active_chat.as_ref().unwrap().0.read(cx); + let is_admin = self + .channel_store + .read(cx) + .is_user_admin(active_chat.channel().id); let last_message = active_chat.message(ix.saturating_sub(1)); let this_message = active_chat.message(ix); let is_continuation = last_message.id != this_message.id @@ -366,6 +370,7 @@ impl ChatPanel { active_chat.message(ix).clone(), is_continuation, active_chat.message_count() == ix + 1, + is_admin, ) }; @@ -386,12 +391,13 @@ impl ChatPanel { }; let belongs_to_user = Some(message.sender.id) == self.client.user_id(); - let message_id_to_remove = - if let (ChannelMessageId::Saved(id), true) = (message.id, belongs_to_user) { - Some(id) - } else { - None - }; + let message_id_to_remove = if let (ChannelMessageId::Saved(id), true) = + (message.id, belongs_to_user || is_admin) + { + Some(id) + } else { + None + }; enum MessageBackgroundHighlight {} MouseEventHandler::new::(ix, cx, |state, cx| { diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 9e09abbd6a..2e68a1c939 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -11,7 +11,10 @@ use anyhow::Result; use call::ActiveCall; use channel::{Channel, ChannelData, ChannelEvent, ChannelId, ChannelPath, ChannelStore}; use channel_modal::ChannelModal; -use client::{proto::PeerId, Client, Contact, User, UserStore}; +use client::{ + proto::{self, PeerId}, + Client, Contact, User, UserStore, +}; use contact_finder::ContactFinder; use context_menu::{ContextMenu, ContextMenuItem}; use db::kvp::KEY_VALUE_STORE; @@ -34,8 +37,8 @@ use gpui::{ }, impl_actions, platform::{CursorStyle, MouseButton, PromptLevel}, - serde_json, AnyElement, AppContext, AsyncAppContext, Element, Entity, FontCache, ModelHandle, - Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, + serde_json, AnyElement, AppContext, AsyncAppContext, ClipboardItem, Element, Entity, FontCache, + ModelHandle, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, }; use menu::{Confirm, SelectNext, SelectPrev}; use project::{Fs, Project}; @@ -100,6 +103,11 @@ pub struct JoinChannelChat { pub channel_id: u64, } +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct CopyChannelLink { + pub channel_id: u64, +} + #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] struct StartMoveChannelFor { channel_id: ChannelId, @@ -157,6 +165,7 @@ impl_actions!( OpenChannelNotes, JoinChannelCall, JoinChannelChat, + CopyChannelLink, LinkChannel, StartMoveChannelFor, StartLinkChannelFor, @@ -205,6 +214,7 @@ pub fn init(cx: &mut AppContext) { cx.add_action(CollabPanel::expand_selected_channel); cx.add_action(CollabPanel::open_channel_notes); cx.add_action(CollabPanel::join_channel_chat); + cx.add_action(CollabPanel::copy_channel_link); cx.add_action( |panel: &mut CollabPanel, action: &ToggleSelectedIx, cx: &mut ViewContext| { @@ -421,7 +431,7 @@ enum ListEntry { is_last: bool, }, ParticipantScreen { - peer_id: PeerId, + peer_id: Option, is_last: bool, }, IncomingRequest(Arc), @@ -435,6 +445,9 @@ enum ListEntry { ChannelNotes { channel_id: ChannelId, }, + ChannelChat { + channel_id: ChannelId, + }, ChannelEditor { depth: usize, }, @@ -595,6 +608,13 @@ impl CollabPanel { ix, cx, ), + ListEntry::ChannelChat { channel_id } => this.render_channel_chat( + *channel_id, + &theme.collab_panel, + is_selected, + ix, + cx, + ), ListEntry::ChannelInvite(channel) => Self::render_channel_invite( channel.clone(), this.channel_store.clone(), @@ -797,7 +817,8 @@ impl CollabPanel { let room = room.read(cx); if let Some(channel_id) = room.channel_id() { - self.entries.push(ListEntry::ChannelNotes { channel_id }) + self.entries.push(ListEntry::ChannelNotes { channel_id }); + self.entries.push(ListEntry::ChannelChat { channel_id }) } // Populate the active user. @@ -829,7 +850,13 @@ impl CollabPanel { project_id: project.id, worktree_root_names: project.worktree_root_names.clone(), host_user_id: user_id, - is_last: projects.peek().is_none(), + is_last: projects.peek().is_none() && !room.is_screen_sharing(), + }); + } + if room.is_screen_sharing() { + self.entries.push(ListEntry::ParticipantScreen { + peer_id: None, + is_last: true, }); } } @@ -873,7 +900,7 @@ impl CollabPanel { } if !participant.video_tracks.is_empty() { self.entries.push(ListEntry::ParticipantScreen { - peer_id: participant.peer_id, + peer_id: Some(participant.peer_id), is_last: true, }); } @@ -1218,14 +1245,18 @@ impl CollabPanel { ) -> AnyElement { enum CallParticipant {} enum CallParticipantTooltip {} + enum LeaveCallButton {} + enum LeaveCallTooltip {} let collab_theme = &theme.collab_panel; let is_current_user = user_store.read(cx).current_user().map(|user| user.id) == Some(user.id); - let content = - MouseEventHandler::new::(user.id as usize, cx, |mouse_state, _| { + let content = MouseEventHandler::new::( + user.id as usize, + cx, + |mouse_state, cx| { let style = if is_current_user { *collab_theme .contact_row @@ -1261,14 +1292,32 @@ impl CollabPanel { Label::new("Calling", collab_theme.calling_indicator.text.clone()) .contained() .with_style(collab_theme.calling_indicator.container) - .aligned(), + .aligned() + .into_any(), ) } else if is_current_user { Some( - Label::new("You", collab_theme.calling_indicator.text.clone()) - .contained() - .with_style(collab_theme.calling_indicator.container) - .aligned(), + MouseEventHandler::new::(0, cx, |state, _| { + render_icon_button( + theme + .collab_panel + .leave_call_button + .style_for(is_selected, state), + "icons/exit.svg", + ) + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, |_, _, cx| { + Self::leave_call(cx); + }) + .with_tooltip::( + 0, + "Leave call", + None, + theme.tooltip.clone(), + cx, + ) + .into_any(), ) } else { None @@ -1277,7 +1326,8 @@ impl CollabPanel { .with_height(collab_theme.row_height) .contained() .with_style(style) - }); + }, + ); if is_current_user || is_pending || peer_id.is_none() { return content.into_any(); @@ -1399,7 +1449,7 @@ impl CollabPanel { } fn render_participant_screen( - peer_id: PeerId, + peer_id: Option, is_last: bool, is_selected: bool, theme: &theme::CollabPanel, @@ -1414,8 +1464,8 @@ impl CollabPanel { .unwrap_or(0.); let tree_branch = theme.tree_branch; - MouseEventHandler::new::( - peer_id.as_u64() as usize, + let handler = MouseEventHandler::new::( + peer_id.map(|id| id.as_u64()).unwrap_or(0) as usize, cx, |mouse_state, cx| { let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state); @@ -1453,16 +1503,20 @@ impl CollabPanel { .contained() .with_style(row.container) }, - ) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, move |_, this, cx| { - if let Some(workspace) = this.workspace.upgrade(cx) { - workspace.update(cx, |workspace, cx| { - workspace.open_shared_screen(peer_id, cx) - }); - } - }) - .into_any() + ); + if peer_id.is_none() { + return handler.into_any(); + } + handler + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, move |_, this, cx| { + if let Some(workspace) = this.workspace.upgrade(cx) { + workspace.update(cx, |workspace, cx| { + workspace.open_shared_screen(peer_id.unwrap(), cx) + }); + } + }) + .into_any() } fn take_editing_state(&mut self, cx: &mut ViewContext) -> bool { @@ -1489,23 +1543,32 @@ impl CollabPanel { enum AddChannel {} let tooltip_style = &theme.tooltip; + let mut channel_link = None; + let mut channel_tooltip_text = None; + let mut channel_icon = None; + let text = match section { Section::ActiveCall => { let channel_name = iife!({ let channel_id = ActiveCall::global(cx).read(cx).channel_id(cx)?; - let name = self - .channel_store - .read(cx) - .channel_for_id(channel_id)? - .name - .as_str(); + let channel = self.channel_store.read(cx).channel_for_id(channel_id)?; - Some(name) + channel_link = Some(channel.link()); + (channel_icon, channel_tooltip_text) = match channel.visibility { + proto::ChannelVisibility::Public => { + (Some("icons/public.svg"), Some("Copy public channel link.")) + } + proto::ChannelVisibility::Members => { + (Some("icons/hash.svg"), Some("Copy private channel link.")) + } + }; + + Some(channel.name.as_str()) }); if let Some(name) = channel_name { - Cow::Owned(format!("#{}", name)) + Cow::Owned(format!("{}", name)) } else { Cow::Borrowed("Current Call") } @@ -1520,28 +1583,30 @@ impl CollabPanel { enum AddContact {} let button = match section { - Section::ActiveCall => Some( + Section::ActiveCall => channel_link.map(|channel_link| { + let channel_link_copy = channel_link.clone(); MouseEventHandler::new::(0, cx, |state, _| { render_icon_button( theme .collab_panel .leave_call_button .style_for(is_selected, state), - "icons/exit.svg", + "icons/link.svg", ) }) .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, |_, _, cx| { - Self::leave_call(cx); + .on_click(MouseButton::Left, move |_, _, cx| { + let item = ClipboardItem::new(channel_link_copy.clone()); + cx.write_to_clipboard(item) }) .with_tooltip::( 0, - "Leave call", + channel_tooltip_text.unwrap(), None, tooltip_style.clone(), cx, - ), - ), + ) + }), Section::Contacts => Some( MouseEventHandler::new::(0, cx, |state, _| { render_icon_button( @@ -1626,6 +1691,21 @@ impl CollabPanel { theme.collab_panel.contact_username.container.margin.left, ), ) + } else if let Some(channel_icon) = channel_icon { + Some( + Svg::new(channel_icon) + .with_color(header_style.text.color) + .constrained() + .with_max_width(icon_size) + .with_max_height(icon_size) + .aligned() + .constrained() + .with_width(icon_size) + .contained() + .with_margin_right( + theme.collab_panel.contact_username.container.margin.left, + ), + ) } else { None }) @@ -1901,6 +1981,12 @@ impl CollabPanel { let channel_id = channel.id; let collab_theme = &theme.collab_panel; let has_children = self.channel_store.read(cx).has_children(channel_id); + let is_public = self + .channel_store + .read(cx) + .channel_for_id(channel_id) + .map(|channel| channel.visibility) + == Some(proto::ChannelVisibility::Public); let other_selected = self.selected_channel().map(|channel| channel.0.id) == Some(channel.id); let disclosed = has_children.then(|| !self.collapsed_channels.binary_search(&path).is_ok()); @@ -1958,12 +2044,16 @@ impl CollabPanel { Flex::::row() .with_child( - Svg::new("icons/hash.svg") - .with_color(collab_theme.channel_hash.color) - .constrained() - .with_width(collab_theme.channel_hash.width) - .aligned() - .left(), + Svg::new(if is_public { + "icons/public.svg" + } else { + "icons/hash.svg" + }) + .with_color(collab_theme.channel_hash.color) + .constrained() + .with_width(collab_theme.channel_hash.width) + .aligned() + .left(), ) .with_child({ let style = collab_theme.channel_name.inactive_state(); @@ -2268,7 +2358,7 @@ impl CollabPanel { .with_child(render_tree_branch( tree_branch, &row.name.text, - true, + false, vec2f(host_avatar_width, theme.row_height), cx.font_cache(), )) @@ -2301,6 +2391,62 @@ impl CollabPanel { .into_any() } + fn render_channel_chat( + &self, + channel_id: ChannelId, + theme: &theme::CollabPanel, + is_selected: bool, + ix: usize, + cx: &mut ViewContext, + ) -> AnyElement { + enum ChannelChat {} + let host_avatar_width = theme + .contact_avatar + .width + .or(theme.contact_avatar.height) + .unwrap_or(0.); + + MouseEventHandler::new::(ix as usize, cx, |state, cx| { + let tree_branch = *theme.tree_branch.in_state(is_selected).style_for(state); + let row = theme.project_row.in_state(is_selected).style_for(state); + + Flex::::row() + .with_child(render_tree_branch( + tree_branch, + &row.name.text, + true, + vec2f(host_avatar_width, theme.row_height), + cx.font_cache(), + )) + .with_child( + Svg::new("icons/conversations.svg") + .with_color(theme.channel_hash.color) + .constrained() + .with_width(theme.channel_hash.width) + .aligned() + .left(), + ) + .with_child( + Label::new("chat", theme.channel_name.text.clone()) + .contained() + .with_style(theme.channel_name.container) + .aligned() + .left() + .flex(1., true), + ) + .constrained() + .with_height(theme.row_height) + .contained() + .with_style(*theme.channel_row.style_for(is_selected, state)) + .with_padding_left(theme.channel_row.default_style().padding.left) + }) + .on_click(MouseButton::Left, move |_, this, cx| { + this.join_channel_chat(&JoinChannelChat { channel_id }, cx); + }) + .with_cursor_style(CursorStyle::PointingHand) + .into_any() + } + fn render_channel_invite( channel: Arc, channel_store: ModelHandle, @@ -2568,6 +2714,13 @@ impl CollabPanel { }, )); + items.push(ContextMenuItem::action( + "Copy Channel Link", + CopyChannelLink { + channel_id: path.channel_id(), + }, + )); + if self.channel_store.read(cx).is_user_admin(path.channel_id()) { let parent_id = path.parent_id(); @@ -2757,6 +2910,9 @@ impl CollabPanel { } } ListEntry::ParticipantScreen { peer_id, .. } => { + let Some(peer_id) = peer_id else { + return; + }; if let Some(workspace) = self.workspace.upgrade(cx) { workspace.update(cx, |workspace, cx| { workspace.open_shared_screen(*peer_id, cx) @@ -3187,49 +3343,19 @@ impl CollabPanel { } fn join_channel(&self, channel_id: u64, cx: &mut ViewContext) { - let workspace = self.workspace.clone(); - let window = cx.window(); - let active_call = ActiveCall::global(cx); - cx.spawn(|_, mut cx| async move { - if active_call.read_with(&mut cx, |active_call, cx| { - if let Some(room) = active_call.room() { - let room = room.read(cx); - room.is_sharing_project() && room.remote_participants().len() > 0 - } else { - false - } - }) { - let answer = window.prompt( - PromptLevel::Warning, - "Leaving this call will unshare your current project.\nDo you want to switch channels?", - &["Yes, Join Channel", "Cancel"], - &mut cx, - ); - - if let Some(mut answer) = answer { - if answer.next().await == Some(1) { - return anyhow::Ok(()); - } - } - } - - let room = active_call - .update(&mut cx, |call, cx| call.join_channel(channel_id, cx)) - .await?; - - let task = room.update(&mut cx, |room, cx| { - let workspace = workspace.upgrade(cx)?; - let (project, host) = room.most_active_project()?; - let app_state = workspace.read(cx).app_state().clone(); - Some(workspace::join_remote_project(project, host, app_state, cx)) - }); - if let Some(task) = task { - task.await?; - } - - anyhow::Ok(()) - }) - .detach_and_log_err(cx); + let Some(workspace) = self.workspace.upgrade(cx) else { + return; + }; + let Some(handle) = cx.window().downcast::() else { + return; + }; + workspace::join_channel( + channel_id, + workspace.read(cx).app_state().clone(), + Some(handle), + cx, + ) + .detach_and_log_err(cx) } fn join_channel_chat(&mut self, action: &JoinChannelChat, cx: &mut ViewContext) { @@ -3246,6 +3372,15 @@ impl CollabPanel { }); } } + + fn copy_channel_link(&mut self, action: &CopyChannelLink, cx: &mut ViewContext) { + let channel_store = self.channel_store.read(cx); + let Some(channel) = channel_store.channel_for_id(action.channel_id) else { + return; + }; + let item = ClipboardItem::new(channel.link()); + cx.write_to_clipboard(item) + } } fn render_tree_branch( @@ -3505,6 +3640,14 @@ impl PartialEq for ListEntry { return channel_id == other_id; } } + ListEntry::ChannelChat { channel_id } => { + if let ListEntry::ChannelChat { + channel_id: other_id, + } = other + { + return channel_id == other_id; + } + } ListEntry::ChannelInvite(channel_1) => { if let ListEntry::ChannelInvite(channel_2) = other { return channel_1.id == channel_2.id; diff --git a/crates/collab_ui/src/collab_panel/channel_modal.rs b/crates/collab_ui/src/collab_panel/channel_modal.rs index 4c811a2df5..0ccf0894b2 100644 --- a/crates/collab_ui/src/collab_panel/channel_modal.rs +++ b/crates/collab_ui/src/collab_panel/channel_modal.rs @@ -1,12 +1,16 @@ use channel::{ChannelId, ChannelMembership, ChannelStore}; -use client::{proto, User, UserId, UserStore}; +use client::{ + proto::{self, ChannelRole, ChannelVisibility}, + User, UserId, UserStore, +}; use context_menu::{ContextMenu, ContextMenuItem}; use fuzzy::{match_strings, StringMatchCandidate}; use gpui::{ actions, elements::*, platform::{CursorStyle, MouseButton}, - AppContext, Entity, ModelHandle, MouseState, Task, View, ViewContext, ViewHandle, + AppContext, ClipboardItem, Entity, ModelHandle, MouseState, Task, View, ViewContext, + ViewHandle, }; use picker::{Picker, PickerDelegate, PickerEvent}; use std::sync::Arc; @@ -96,11 +100,14 @@ impl ChannelModal { let channel_id = self.channel_id; cx.spawn(|this, mut cx| async move { if mode == Mode::ManageMembers { - let members = channel_store + let mut members = channel_store .update(&mut cx, |channel_store, cx| { channel_store.get_channel_member_details(channel_id, cx) }) .await?; + + members.sort_by(|a, b| a.sort_key().cmp(&b.sort_key())); + this.update(&mut cx, |this, cx| { this.picker .update(cx, |picker, _| picker.delegate_mut().members = members); @@ -182,6 +189,81 @@ impl View for ChannelModal { .into_any() } + fn render_visibility( + channel_id: ChannelId, + visibility: ChannelVisibility, + theme: &theme::TabbedModal, + cx: &mut ViewContext, + ) -> AnyElement { + enum TogglePublic {} + + if visibility == ChannelVisibility::Members { + return Flex::row() + .with_child( + MouseEventHandler::new::(0, cx, move |state, _| { + let style = theme.visibility_toggle.style_for(state); + Label::new(format!("{}", "Public access: OFF"), style.text.clone()) + .contained() + .with_style(style.container.clone()) + }) + .on_click(MouseButton::Left, move |_, this, cx| { + this.channel_store + .update(cx, |channel_store, cx| { + channel_store.set_channel_visibility( + channel_id, + ChannelVisibility::Public, + cx, + ) + }) + .detach_and_log_err(cx); + }) + .with_cursor_style(CursorStyle::PointingHand), + ) + .into_any(); + } + + Flex::row() + .with_child( + MouseEventHandler::new::(0, cx, move |state, _| { + let style = theme.visibility_toggle.style_for(state); + Label::new(format!("{}", "Public access: ON"), style.text.clone()) + .contained() + .with_style(style.container.clone()) + }) + .on_click(MouseButton::Left, move |_, this, cx| { + this.channel_store + .update(cx, |channel_store, cx| { + channel_store.set_channel_visibility( + channel_id, + ChannelVisibility::Members, + cx, + ) + }) + .detach_and_log_err(cx); + }) + .with_cursor_style(CursorStyle::PointingHand), + ) + .with_spacing(14.0) + .with_child( + MouseEventHandler::new::(1, cx, move |state, _| { + let style = theme.channel_link.style_for(state); + Label::new(format!("{}", "copy link"), style.text.clone()) + .contained() + .with_style(style.container.clone()) + }) + .on_click(MouseButton::Left, move |_, this, cx| { + if let Some(channel) = + this.channel_store.read(cx).channel_for_id(channel_id) + { + let item = ClipboardItem::new(channel.link()); + cx.write_to_clipboard(item); + } + }) + .with_cursor_style(CursorStyle::PointingHand), + ) + .into_any() + } + Flex::column() .with_child( Flex::column() @@ -190,6 +272,7 @@ impl View for ChannelModal { .contained() .with_style(theme.title.container.clone()), ) + .with_child(render_visibility(channel.id, channel.visibility, theme, cx)) .with_child(Flex::row().with_children([ render_mode_button::( Mode::InviteMembers, @@ -343,9 +426,11 @@ impl PickerDelegate for ChannelModalDelegate { } fn confirm(&mut self, _: bool, cx: &mut ViewContext>) { - if let Some((selected_user, admin)) = self.user_at_index(self.selected_index) { + if let Some((selected_user, role)) = self.user_at_index(self.selected_index) { match self.mode { - Mode::ManageMembers => self.show_context_menu(admin.unwrap_or(false), cx), + Mode::ManageMembers => { + self.show_context_menu(role.unwrap_or(ChannelRole::Member), cx) + } Mode::InviteMembers => match self.member_status(selected_user.id, cx) { Some(proto::channel_member::Kind::Invitee) => { self.remove_selected_member(cx); @@ -373,7 +458,7 @@ impl PickerDelegate for ChannelModalDelegate { let full_theme = &theme::current(cx); let theme = &full_theme.collab_panel.channel_modal; let tabbed_modal = &full_theme.collab_panel.tabbed_modal; - let (user, admin) = self.user_at_index(ix).unwrap(); + let (user, role) = self.user_at_index(ix).unwrap(); let request_status = self.member_status(user.id, cx); let style = tabbed_modal @@ -409,15 +494,25 @@ impl PickerDelegate for ChannelModalDelegate { }, ) }) - .with_children(admin.and_then(|admin| { - (in_manage && admin).then(|| { + .with_children(if in_manage && role == Some(ChannelRole::Admin) { + Some( Label::new("Admin", theme.member_tag.text.clone()) .contained() .with_style(theme.member_tag.container) .aligned() - .left() - }) - })) + .left(), + ) + } else if in_manage && role == Some(ChannelRole::Guest) { + Some( + Label::new("Guest", theme.member_tag.text.clone()) + .contained() + .with_style(theme.member_tag.container) + .aligned() + .left(), + ) + } else { + None + }) .with_children({ let svg = match self.mode { Mode::ManageMembers => Some( @@ -502,13 +597,13 @@ impl ChannelModalDelegate { }) } - fn user_at_index(&self, ix: usize) -> Option<(Arc, Option)> { + fn user_at_index(&self, ix: usize) -> Option<(Arc, Option)> { match self.mode { Mode::ManageMembers => self.matching_member_indices.get(ix).and_then(|ix| { let channel_membership = self.members.get(*ix)?; Some(( channel_membership.user.clone(), - Some(channel_membership.admin), + Some(channel_membership.role), )) }), Mode::InviteMembers => Some((self.matching_users.get(ix).cloned()?, None)), @@ -516,17 +611,21 @@ impl ChannelModalDelegate { } fn toggle_selected_member_admin(&mut self, cx: &mut ViewContext>) -> Option<()> { - let (user, admin) = self.user_at_index(self.selected_index)?; - let admin = !admin.unwrap_or(false); + let (user, role) = self.user_at_index(self.selected_index)?; + let new_role = if role == Some(ChannelRole::Admin) { + ChannelRole::Member + } else { + ChannelRole::Admin + }; let update = self.channel_store.update(cx, |store, cx| { - store.set_member_admin(self.channel_id, user.id, admin, cx) + store.set_member_role(self.channel_id, user.id, new_role, cx) }); cx.spawn(|picker, mut cx| async move { update.await?; picker.update(&mut cx, |picker, cx| { let this = picker.delegate_mut(); if let Some(member) = this.members.iter_mut().find(|m| m.user.id == user.id) { - member.admin = admin; + member.role = new_role; } cx.focus_self(); cx.notify(); @@ -572,25 +671,30 @@ impl ChannelModalDelegate { fn invite_member(&mut self, user: Arc, cx: &mut ViewContext>) { let invite_member = self.channel_store.update(cx, |store, cx| { - store.invite_member(self.channel_id, user.id, false, cx) + store.invite_member(self.channel_id, user.id, ChannelRole::Member, cx) }); cx.spawn(|this, mut cx| async move { invite_member.await?; this.update(&mut cx, |this, cx| { - this.delegate_mut().members.push(ChannelMembership { + let new_member = ChannelMembership { user, kind: proto::channel_member::Kind::Invitee, - admin: false, - }); + role: ChannelRole::Member, + }; + let members = &mut this.delegate_mut().members; + match members.binary_search_by_key(&new_member.sort_key(), |k| k.sort_key()) { + Ok(ix) | Err(ix) => members.insert(ix, new_member), + } + cx.notify(); }) }) .detach_and_log_err(cx); } - fn show_context_menu(&mut self, user_is_admin: bool, cx: &mut ViewContext>) { + fn show_context_menu(&mut self, role: ChannelRole, cx: &mut ViewContext>) { self.context_menu.update(cx, |context_menu, cx| { context_menu.show( Default::default(), @@ -598,7 +702,7 @@ impl ChannelModalDelegate { vec![ ContextMenuItem::action("Remove", RemoveMember), ContextMenuItem::action( - if user_is_admin { + if role == ChannelRole::Admin { "Make non-admin" } else { "Make admin" diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index d85aca164a..211ee863e8 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -2,6 +2,7 @@ use crate::{ contact_notification::ContactNotification, face_pile::FacePile, toggle_deafen, toggle_mute, toggle_screen_sharing, LeaveCall, ToggleDeafen, ToggleMute, ToggleScreenSharing, }; +use auto_update::AutoUpdateStatus; use call::{ActiveCall, ParticipantLocation, Room}; use client::{proto::PeerId, Client, ContactEventKind, SignIn, SignOut, User, UserStore}; use clock::ReplicaId; @@ -1177,22 +1178,38 @@ impl CollabTitlebarItem { .with_style(theme.titlebar.offline_icon.container) .into_any(), ), - client::Status::UpgradeRequired => Some( - MouseEventHandler::new::(0, cx, |_, _| { - Label::new( - "Please update Zed to collaborate", - theme.titlebar.outdated_warning.text.clone(), - ) - .contained() - .with_style(theme.titlebar.outdated_warning.container) - .aligned() - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, |_, _, cx| { - auto_update::check(&Default::default(), cx); - }) - .into_any(), - ), + client::Status::UpgradeRequired => { + let auto_updater = auto_update::AutoUpdater::get(cx); + let label = match auto_updater.map(|auto_update| auto_update.read(cx).status()) { + Some(AutoUpdateStatus::Updated) => "Please restart Zed to Collaborate", + Some(AutoUpdateStatus::Installing) + | Some(AutoUpdateStatus::Downloading) + | Some(AutoUpdateStatus::Checking) => "Updating...", + Some(AutoUpdateStatus::Idle) | Some(AutoUpdateStatus::Errored) | None => { + "Please update Zed to Collaborate" + } + }; + + Some( + MouseEventHandler::new::(0, cx, |_, _| { + Label::new(label, theme.titlebar.outdated_warning.text.clone()) + .contained() + .with_style(theme.titlebar.outdated_warning.container) + .aligned() + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, |_, _, cx| { + if let Some(auto_updater) = auto_update::AutoUpdater::get(cx) { + if auto_updater.read(cx).status() == AutoUpdateStatus::Updated { + workspace::restart(&Default::default(), cx); + return; + } + } + auto_update::check(&Default::default(), cx); + }) + .into_any(), + ) + } _ => None, } } diff --git a/crates/command_palette/Cargo.toml b/crates/command_palette/Cargo.toml index 95ba452c14..b42a3b5f41 100644 --- a/crates/command_palette/Cargo.toml +++ b/crates/command_palette/Cargo.toml @@ -19,6 +19,7 @@ settings = { path = "../settings" } util = { path = "../util" } theme = { path = "../theme" } workspace = { path = "../workspace" } +zed-actions = { path = "../zed-actions" } [dev-dependencies] gpui = { path = "../gpui", features = ["test-support"] } diff --git a/crates/command_palette/src/command_palette.rs b/crates/command_palette/src/command_palette.rs index 10c9ba7b86..ce762876a4 100644 --- a/crates/command_palette/src/command_palette.rs +++ b/crates/command_palette/src/command_palette.rs @@ -6,8 +6,12 @@ use gpui::{ }; use picker::{Picker, PickerDelegate, PickerEvent}; use std::cmp::{self, Reverse}; -use util::ResultExt; +use util::{ + channel::{parse_zed_link, ReleaseChannel, RELEASE_CHANNEL}, + ResultExt, +}; use workspace::Workspace; +use zed_actions::OpenZedURL; pub fn init(cx: &mut AppContext) { cx.add_action(toggle_command_palette); @@ -167,13 +171,22 @@ impl PickerDelegate for CommandPaletteDelegate { ) .await }; - let intercept_result = cx.read(|cx| { + let mut intercept_result = cx.read(|cx| { if cx.has_global::() { cx.global::()(&query, cx) } else { None } }); + if *RELEASE_CHANNEL == ReleaseChannel::Dev { + if parse_zed_link(&query).is_some() { + intercept_result = Some(CommandInterceptResult { + action: OpenZedURL { url: query.clone() }.boxed_clone(), + string: query.clone(), + positions: vec![], + }) + } + } if let Some(CommandInterceptResult { action, string, diff --git a/crates/diagnostics/src/items.rs b/crates/diagnostics/src/items.rs index c3733018b6..8d3c2fedd6 100644 --- a/crates/diagnostics/src/items.rs +++ b/crates/diagnostics/src/items.rs @@ -38,6 +38,10 @@ impl DiagnosticIndicator { this.in_progress_checks.remove(language_server_id); cx.notify(); } + project::Event::DiagnosticsUpdated { .. } => { + this.summary = project.read(cx).diagnostic_summary(cx); + cx.notify(); + } _ => {} }) .detach(); diff --git a/crates/editor/Cargo.toml b/crates/editor/Cargo.toml index 2c3d6227a9..d03e1c1106 100644 --- a/crates/editor/Cargo.toml +++ b/crates/editor/Cargo.toml @@ -57,7 +57,6 @@ log.workspace = true ordered-float.workspace = true parking_lot.workspace = true postage.workspace = true -pulldown-cmark = { version = "0.9.2", default-features = false } rand.workspace = true schemars.workspace = true serde.workspace = true diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 01b060fdb6..82945fc00b 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -25,7 +25,7 @@ use ::git::diff::DiffHunk; use aho_corasick::AhoCorasick; use anyhow::{anyhow, Context, Result}; use blink_manager::BlinkManager; -use client::{ClickhouseEvent, Collaborator, ParticipantIndex, TelemetrySettings}; +use client::{ClickhouseEvent, Client, Collaborator, ParticipantIndex, TelemetrySettings}; use clock::{Global, ReplicaId}; use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque}; use convert_case::{Case, Casing}; @@ -48,9 +48,9 @@ use gpui::{ impl_actions, keymap_matcher::KeymapContext, platform::{CursorStyle, MouseButton}, - serde_json, AnyElement, AnyViewHandle, AppContext, AsyncAppContext, ClipboardItem, Element, - Entity, ModelHandle, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, - WindowContext, + serde_json, AnyElement, AnyViewHandle, AppContext, AsyncAppContext, ClipboardItem, + CursorRegion, Element, Entity, ModelHandle, MouseRegion, Subscription, Task, View, ViewContext, + ViewHandle, WeakViewHandle, WindowContext, }; use highlight_matching_bracket::refresh_matching_bracket_highlights; use hover_popover::{hide_hover, HoverState}; @@ -60,10 +60,10 @@ use itertools::Itertools; pub use language::{char_kind, CharKind}; use language::{ language_settings::{self, all_language_settings, InlayHintSettings}, - point_from_lsp, AutoindentMode, BracketPair, Buffer, CodeAction, CodeLabel, Completion, - CursorShape, Diagnostic, DiagnosticSeverity, File, IndentKind, IndentSize, Language, - LanguageServerName, OffsetRangeExt, OffsetUtf16, Point, Selection, SelectionGoal, - TransactionId, + markdown, point_from_lsp, AutoindentMode, BracketPair, Buffer, CodeAction, CodeLabel, + Completion, CursorShape, Diagnostic, DiagnosticSeverity, Documentation, File, IndentKind, + IndentSize, Language, LanguageRegistry, LanguageServerName, OffsetRangeExt, OffsetUtf16, Point, + Selection, SelectionGoal, TransactionId, }; use link_go_to_definition::{ hide_link_definition, show_link_definition, GoToDefinitionLink, InlayHighlight, @@ -78,9 +78,10 @@ pub use multi_buffer::{ ToPoint, }; use ordered_float::OrderedFloat; +use parking_lot::RwLock; use project::{FormatTrigger, Location, Project, ProjectPath, ProjectTransaction}; use rand::{seq::SliceRandom, thread_rng}; -use rpc::proto::PeerId; +use rpc::proto::{self, PeerId}; use scroll::{ autoscroll::Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager, ScrollbarAutoHide, }; @@ -119,6 +120,67 @@ pub const DOCUMENT_HIGHLIGHTS_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis pub const FORMAT_TIMEOUT: Duration = Duration::from_secs(2); +pub fn render_parsed_markdown( + parsed: &language::ParsedMarkdown, + editor_style: &EditorStyle, + workspace: Option>, + cx: &mut ViewContext, +) -> Text { + enum RenderedMarkdown {} + + let parsed = parsed.clone(); + let view_id = cx.view_id(); + let code_span_background_color = editor_style.document_highlight_read_background; + + let mut region_id = 0; + + Text::new(parsed.text, editor_style.text.clone()) + .with_highlights( + parsed + .highlights + .iter() + .filter_map(|(range, highlight)| { + let highlight = highlight.to_highlight_style(&editor_style.syntax)?; + Some((range.clone(), highlight)) + }) + .collect::>(), + ) + .with_custom_runs(parsed.region_ranges, move |ix, bounds, cx| { + region_id += 1; + let region = parsed.regions[ix].clone(); + + if let Some(link) = region.link { + cx.scene().push_cursor_region(CursorRegion { + bounds, + style: CursorStyle::PointingHand, + }); + cx.scene().push_mouse_region( + MouseRegion::new::<(RenderedMarkdown, Tag)>(view_id, region_id, bounds) + .on_down::(MouseButton::Left, move |_, _, cx| match &link { + markdown::Link::Web { url } => cx.platform().open_url(url), + markdown::Link::Path { path } => { + if let Some(workspace) = &workspace { + _ = workspace.update(cx, |workspace, cx| { + workspace.open_abs_path(path.clone(), false, cx).detach(); + }); + } + } + }), + ); + } + + if region.code { + cx.scene().push_quad(gpui::Quad { + bounds, + background: Some(code_span_background_color), + border: Default::default(), + corner_radii: (2.0).into(), + }); + } + }) + .with_soft_wrap(true) +} + #[derive(Clone, Deserialize, PartialEq, Default)] pub struct SelectNext { #[serde(default)] @@ -595,7 +657,7 @@ pub struct Editor { background_highlights: BTreeMap, inlay_background_highlights: TreeMap, InlayBackgroundHighlight>, nav_history: Option, - context_menu: Option, + context_menu: RwLock>, mouse_context_menu: ViewHandle, completion_tasks: Vec<(CompletionId, Task>)>, next_completion_id: CompletionId, @@ -788,10 +850,14 @@ enum ContextMenu { } impl ContextMenu { - fn select_first(&mut self, cx: &mut ViewContext) -> bool { + fn select_first( + &mut self, + project: Option<&ModelHandle>, + cx: &mut ViewContext, + ) -> bool { if self.visible() { match self { - ContextMenu::Completions(menu) => menu.select_first(cx), + ContextMenu::Completions(menu) => menu.select_first(project, cx), ContextMenu::CodeActions(menu) => menu.select_first(cx), } true @@ -800,10 +866,14 @@ impl ContextMenu { } } - fn select_prev(&mut self, cx: &mut ViewContext) -> bool { + fn select_prev( + &mut self, + project: Option<&ModelHandle>, + cx: &mut ViewContext, + ) -> bool { if self.visible() { match self { - ContextMenu::Completions(menu) => menu.select_prev(cx), + ContextMenu::Completions(menu) => menu.select_prev(project, cx), ContextMenu::CodeActions(menu) => menu.select_prev(cx), } true @@ -812,10 +882,14 @@ impl ContextMenu { } } - fn select_next(&mut self, cx: &mut ViewContext) -> bool { + fn select_next( + &mut self, + project: Option<&ModelHandle>, + cx: &mut ViewContext, + ) -> bool { if self.visible() { match self { - ContextMenu::Completions(menu) => menu.select_next(cx), + ContextMenu::Completions(menu) => menu.select_next(project, cx), ContextMenu::CodeActions(menu) => menu.select_next(cx), } true @@ -824,10 +898,14 @@ impl ContextMenu { } } - fn select_last(&mut self, cx: &mut ViewContext) -> bool { + fn select_last( + &mut self, + project: Option<&ModelHandle>, + cx: &mut ViewContext, + ) -> bool { if self.visible() { match self { - ContextMenu::Completions(menu) => menu.select_last(cx), + ContextMenu::Completions(menu) => menu.select_last(project, cx), ContextMenu::CodeActions(menu) => menu.select_last(cx), } true @@ -847,99 +925,350 @@ impl ContextMenu { &self, cursor_position: DisplayPoint, style: EditorStyle, + workspace: Option>, cx: &mut ViewContext, ) -> (DisplayPoint, AnyElement) { match self { - ContextMenu::Completions(menu) => (cursor_position, menu.render(style, cx)), + ContextMenu::Completions(menu) => (cursor_position, menu.render(style, workspace, cx)), ContextMenu::CodeActions(menu) => menu.render(cursor_position, style, cx), } } } +#[derive(Clone)] struct CompletionsMenu { id: CompletionId, initial_position: Anchor, buffer: ModelHandle, - project: Option>, - completions: Arc<[Completion]>, - match_candidates: Vec, + completions: Arc>>, + match_candidates: Arc<[StringMatchCandidate]>, matches: Arc<[StringMatch]>, selected_item: usize, list: UniformListState, } impl CompletionsMenu { - fn select_first(&mut self, cx: &mut ViewContext) { + fn select_first( + &mut self, + project: Option<&ModelHandle>, + cx: &mut ViewContext, + ) { self.selected_item = 0; self.list.scroll_to(ScrollTarget::Show(self.selected_item)); + self.attempt_resolve_selected_completion_documentation(project, cx); cx.notify(); } - fn select_prev(&mut self, cx: &mut ViewContext) { + fn select_prev( + &mut self, + project: Option<&ModelHandle>, + cx: &mut ViewContext, + ) { if self.selected_item > 0 { self.selected_item -= 1; self.list.scroll_to(ScrollTarget::Show(self.selected_item)); } + self.attempt_resolve_selected_completion_documentation(project, cx); cx.notify(); } - fn select_next(&mut self, cx: &mut ViewContext) { + fn select_next( + &mut self, + project: Option<&ModelHandle>, + cx: &mut ViewContext, + ) { if self.selected_item + 1 < self.matches.len() { self.selected_item += 1; self.list.scroll_to(ScrollTarget::Show(self.selected_item)); } + self.attempt_resolve_selected_completion_documentation(project, cx); cx.notify(); } - fn select_last(&mut self, cx: &mut ViewContext) { + fn select_last( + &mut self, + project: Option<&ModelHandle>, + cx: &mut ViewContext, + ) { self.selected_item = self.matches.len() - 1; self.list.scroll_to(ScrollTarget::Show(self.selected_item)); + self.attempt_resolve_selected_completion_documentation(project, cx); cx.notify(); } + fn pre_resolve_completion_documentation( + &self, + project: Option>, + cx: &mut ViewContext, + ) { + let settings = settings::get::(cx); + if !settings.show_completion_documentation { + return; + } + + let Some(project) = project else { + return; + }; + let client = project.read(cx).client(); + let language_registry = project.read(cx).languages().clone(); + + let is_remote = project.read(cx).is_remote(); + let project_id = project.read(cx).remote_id(); + + let completions = self.completions.clone(); + let completion_indices: Vec<_> = self.matches.iter().map(|m| m.candidate_id).collect(); + + cx.spawn(move |this, mut cx| async move { + if is_remote { + let Some(project_id) = project_id else { + log::error!("Remote project without remote_id"); + return; + }; + + for completion_index in completion_indices { + let completions_guard = completions.read(); + let completion = &completions_guard[completion_index]; + if completion.documentation.is_some() { + continue; + } + + let server_id = completion.server_id; + let completion = completion.lsp_completion.clone(); + drop(completions_guard); + + Self::resolve_completion_documentation_remote( + project_id, + server_id, + completions.clone(), + completion_index, + completion, + client.clone(), + language_registry.clone(), + ) + .await; + + _ = this.update(&mut cx, |_, cx| cx.notify()); + } + } else { + for completion_index in completion_indices { + let completions_guard = completions.read(); + let completion = &completions_guard[completion_index]; + if completion.documentation.is_some() { + continue; + } + + let server_id = completion.server_id; + let completion = completion.lsp_completion.clone(); + drop(completions_guard); + + let server = project.read_with(&mut cx, |project, _| { + project.language_server_for_id(server_id) + }); + let Some(server) = server else { + return; + }; + + Self::resolve_completion_documentation_local( + server, + completions.clone(), + completion_index, + completion, + language_registry.clone(), + ) + .await; + + _ = this.update(&mut cx, |_, cx| cx.notify()); + } + } + }) + .detach(); + } + + fn attempt_resolve_selected_completion_documentation( + &mut self, + project: Option<&ModelHandle>, + cx: &mut ViewContext, + ) { + let settings = settings::get::(cx); + if !settings.show_completion_documentation { + return; + } + + let completion_index = self.matches[self.selected_item].candidate_id; + let Some(project) = project else { + return; + }; + let language_registry = project.read(cx).languages().clone(); + + let completions = self.completions.clone(); + let completions_guard = completions.read(); + let completion = &completions_guard[completion_index]; + if completion.documentation.is_some() { + return; + } + + let server_id = completion.server_id; + let completion = completion.lsp_completion.clone(); + drop(completions_guard); + + if project.read(cx).is_remote() { + let Some(project_id) = project.read(cx).remote_id() else { + log::error!("Remote project without remote_id"); + return; + }; + + let client = project.read(cx).client(); + + cx.spawn(move |this, mut cx| async move { + Self::resolve_completion_documentation_remote( + project_id, + server_id, + completions.clone(), + completion_index, + completion, + client, + language_registry.clone(), + ) + .await; + + _ = this.update(&mut cx, |_, cx| cx.notify()); + }) + .detach(); + } else { + let Some(server) = project.read(cx).language_server_for_id(server_id) else { + return; + }; + + cx.spawn(move |this, mut cx| async move { + Self::resolve_completion_documentation_local( + server, + completions, + completion_index, + completion, + language_registry, + ) + .await; + + _ = this.update(&mut cx, |_, cx| cx.notify()); + }) + .detach(); + } + } + + async fn resolve_completion_documentation_remote( + project_id: u64, + server_id: LanguageServerId, + completions: Arc>>, + completion_index: usize, + completion: lsp::CompletionItem, + client: Arc, + language_registry: Arc, + ) { + let request = proto::ResolveCompletionDocumentation { + project_id, + language_server_id: server_id.0 as u64, + lsp_completion: serde_json::to_string(&completion).unwrap().into_bytes(), + }; + + let Some(response) = client + .request(request) + .await + .context("completion documentation resolve proto request") + .log_err() + else { + return; + }; + + if response.text.is_empty() { + let mut completions = completions.write(); + let completion = &mut completions[completion_index]; + completion.documentation = Some(Documentation::Undocumented); + } + + let documentation = if response.is_markdown { + Documentation::MultiLineMarkdown( + markdown::parse_markdown(&response.text, &language_registry, None).await, + ) + } else if response.text.lines().count() <= 1 { + Documentation::SingleLine(response.text) + } else { + Documentation::MultiLinePlainText(response.text) + }; + + let mut completions = completions.write(); + let completion = &mut completions[completion_index]; + completion.documentation = Some(documentation); + } + + async fn resolve_completion_documentation_local( + server: Arc, + completions: Arc>>, + completion_index: usize, + completion: lsp::CompletionItem, + language_registry: Arc, + ) { + let can_resolve = server + .capabilities() + .completion_provider + .as_ref() + .and_then(|options| options.resolve_provider) + .unwrap_or(false); + if !can_resolve { + return; + } + + let request = server.request::(completion); + let Some(completion_item) = request.await.log_err() else { + return; + }; + + if let Some(lsp_documentation) = completion_item.documentation { + let documentation = language::prepare_completion_documentation( + &lsp_documentation, + &language_registry, + None, // TODO: Try to reasonably work out which language the completion is for + ) + .await; + + let mut completions = completions.write(); + let completion = &mut completions[completion_index]; + completion.documentation = Some(documentation); + } else { + let mut completions = completions.write(); + let completion = &mut completions[completion_index]; + completion.documentation = Some(Documentation::Undocumented); + } + } + fn visible(&self) -> bool { !self.matches.is_empty() } - fn render(&self, style: EditorStyle, cx: &mut ViewContext) -> AnyElement { + fn render( + &self, + style: EditorStyle, + workspace: Option>, + cx: &mut ViewContext, + ) -> AnyElement { enum CompletionTag {} - let language_servers = self.project.as_ref().map(|project| { - project - .read(cx) - .language_servers_for_buffer(self.buffer.read(cx), cx) - .filter(|(_, server)| server.capabilities().completion_provider.is_some()) - .map(|(adapter, server)| (server.server_id(), adapter.short_name)) - .collect::>() - }); - let needs_server_name = language_servers - .as_ref() - .map_or(false, |servers| servers.len() > 1); - - let get_server_name = - move |lookup_server_id: lsp::LanguageServerId| -> Option<&'static str> { - language_servers - .iter() - .flatten() - .find_map(|(server_id, server_name)| { - if *server_id == lookup_server_id { - Some(*server_name) - } else { - None - } - }) - }; + let settings = settings::get::(cx); + let show_completion_documentation = settings.show_completion_documentation; let widest_completion_ix = self .matches .iter() .enumerate() .max_by_key(|(_, mat)| { - let completion = &self.completions[mat.candidate_id]; - let mut len = completion.label.text.chars().count(); + let completions = self.completions.read(); + let completion = &completions[mat.candidate_id]; + let documentation = &completion.documentation; - if let Some(server_name) = get_server_name(completion.server_id) { - len += server_name.chars().count(); + let mut len = completion.label.text.chars().count(); + if let Some(Documentation::SingleLine(text)) = documentation { + if show_completion_documentation { + len += text.chars().count(); + } } len @@ -949,16 +1278,24 @@ impl CompletionsMenu { let completions = self.completions.clone(); let matches = self.matches.clone(); let selected_item = self.selected_item; - let container_style = style.autocomplete.container; - UniformList::new( - self.list.clone(), - matches.len(), - cx, + + let list = UniformList::new(self.list.clone(), matches.len(), cx, { + let style = style.clone(); move |_, range, items, cx| { let start_ix = range.start; + let completions_guard = completions.read(); + for (ix, mat) in matches[range].iter().enumerate() { - let completion = &completions[mat.candidate_id]; let item_ix = start_ix + ix; + let candidate_id = mat.candidate_id; + let completion = &completions_guard[candidate_id]; + + let documentation = if show_completion_documentation { + &completion.documentation + } else { + &None + }; + items.push( MouseEventHandler::new::( mat.candidate_id, @@ -987,22 +1324,18 @@ impl CompletionsMenu { ), ); - if let Some(server_name) = get_server_name(completion.server_id) { + if let Some(Documentation::SingleLine(text)) = documentation { Flex::row() .with_child(completion_label) .with_children((|| { - if !needs_server_name { - return None; - } - let text_style = TextStyle { - color: style.autocomplete.server_name_color, + color: style.autocomplete.inline_docs_color, font_size: style.text.font_size - * style.autocomplete.server_name_size_percent, + * style.autocomplete.inline_docs_size_percent, ..style.text.clone() }; - let label = Text::new(server_name, text_style) + let label = Text::new(text.clone(), text_style) .aligned() .constrained() .dynamically(move |constraint, _, _| { @@ -1022,7 +1355,7 @@ impl CompletionsMenu { .with_style( style .autocomplete - .server_name_container, + .inline_docs_container, ) .into_any(), ) @@ -1061,15 +1394,59 @@ impl CompletionsMenu { ) .map(|task| task.detach()); }) + .constrained() + .with_min_width(style.autocomplete.completion_min_width) + .with_max_width(style.autocomplete.completion_max_width) .into_any(), ); } - }, - ) - .with_width_from_item(widest_completion_ix) - .contained() - .with_style(container_style) - .into_any() + } + }) + .with_width_from_item(widest_completion_ix); + + enum MultiLineDocumentation {} + + Flex::row() + .with_child(list.flex(1., false)) + .with_children({ + let mat = &self.matches[selected_item]; + let completions = self.completions.read(); + let completion = &completions[mat.candidate_id]; + let documentation = &completion.documentation; + + match documentation { + Some(Documentation::MultiLinePlainText(text)) => Some( + Flex::column() + .scrollable::(0, None, cx) + .with_child( + Text::new(text.clone(), style.text.clone()).with_soft_wrap(true), + ) + .contained() + .with_style(style.autocomplete.alongside_docs_container) + .constrained() + .with_max_width(style.autocomplete.alongside_docs_max_width) + .flex(1., false), + ), + + Some(Documentation::MultiLineMarkdown(parsed)) => Some( + Flex::column() + .scrollable::(0, None, cx) + .with_child(render_parsed_markdown::( + parsed, &style, workspace, cx, + )) + .contained() + .with_style(style.autocomplete.alongside_docs_container) + .constrained() + .with_max_width(style.autocomplete.alongside_docs_max_width) + .flex(1., false), + ), + + _ => None, + } + }) + .contained() + .with_style(style.autocomplete.container) + .into_any() } pub async fn filter(&mut self, query: Option<&str>, executor: Arc) { @@ -1096,13 +1473,13 @@ impl CompletionsMenu { .collect() }; - //Remove all candidates where the query's start does not match the start of any word in the candidate + // Remove all candidates where the query's start does not match the start of any word in the candidate if let Some(query) = query { if let Some(query_start) = query.chars().next() { matches.retain(|string_match| { split_words(&string_match.string).any(|word| { - //Check that the first codepoint of the word as lowercase matches the first - //codepoint of the query as lowercase + // Check that the first codepoint of the word as lowercase matches the first + // codepoint of the query as lowercase word.chars() .flat_map(|codepoint| codepoint.to_lowercase()) .zip(query_start.to_lowercase()) @@ -1112,23 +1489,27 @@ impl CompletionsMenu { } } + let completions = self.completions.read(); matches.sort_unstable_by_key(|mat| { - let completion = &self.completions[mat.candidate_id]; + let completion = &completions[mat.candidate_id]; ( completion.lsp_completion.sort_text.as_ref(), Reverse(OrderedFloat(mat.score)), completion.sort_key(), ) }); + drop(completions); for mat in &mut matches { - let filter_start = self.completions[mat.candidate_id].label.filter_range.start; + let completions = self.completions.read(); + let filter_start = completions[mat.candidate_id].label.filter_range.start; for position in &mut mat.positions { *position += filter_start; } } self.matches = matches.into(); + self.selected_item = 0; } } @@ -1564,7 +1945,7 @@ impl Editor { background_highlights: Default::default(), inlay_background_highlights: Default::default(), nav_history: None, - context_menu: None, + context_menu: RwLock::new(None), mouse_context_menu: cx .add_view(|cx| context_menu::ContextMenu::new(editor_view_id, cx)), completion_tasks: Default::default(), @@ -1859,10 +2240,12 @@ impl Editor { if local { let new_cursor_position = self.selections.newest_anchor().head(); - let completion_menu = match self.context_menu.as_mut() { + let mut context_menu = self.context_menu.write(); + let completion_menu = match context_menu.as_ref() { Some(ContextMenu::Completions(menu)) => Some(menu), + _ => { - self.context_menu.take(); + *context_menu = None; None } }; @@ -1874,13 +2257,39 @@ impl Editor { if kind == Some(CharKind::Word) && word_range.to_inclusive().contains(&cursor_position) { + let mut completion_menu = completion_menu.clone(); + drop(context_menu); + let query = Self::completion_query(buffer, cursor_position); - cx.background() - .block(completion_menu.filter(query.as_deref(), cx.background().clone())); + cx.spawn(move |this, mut cx| async move { + completion_menu + .filter(query.as_deref(), cx.background().clone()) + .await; + + this.update(&mut cx, |this, cx| { + let mut context_menu = this.context_menu.write(); + let Some(ContextMenu::Completions(menu)) = context_menu.as_ref() else { + return; + }; + + if menu.id > completion_menu.id { + return; + } + + *context_menu = Some(ContextMenu::Completions(completion_menu)); + drop(context_menu); + cx.notify(); + }) + }) + .detach(); + self.show_completions(&ShowCompletions, cx); } else { + drop(context_menu); self.hide_context_menu(cx); } + } else { + drop(context_menu); } hide_hover(self, cx); @@ -2878,8 +3287,10 @@ impl Editor { i = 0; } else if pair_state.range.start.to_offset(buffer) > range.end { break; - } else if pair_state.selection_id == selection.id { - enclosing = Some(pair_state); + } else { + if pair_state.selection_id == selection.id { + enclosing = Some(pair_state); + } i += 1; } } @@ -2913,6 +3324,7 @@ impl Editor { false }); } + fn completion_query(buffer: &MultiBufferSnapshot, position: impl ToOffset) -> Option { let offset = position.to_offset(buffer); let (word_range, kind) = buffer.surrounding_word(offset); @@ -3159,7 +3571,6 @@ impl Editor { }); let id = post_inc(&mut self.next_completion_id); - let project = self.project.clone(); let task = cx.spawn(|this, mut cx| { async move { let menu = if let Some(completions) = completions.await.log_err() { @@ -3178,8 +3589,7 @@ impl Editor { }) .collect(), buffer, - project, - completions: completions.into(), + completions: Arc::new(RwLock::new(completions.into())), matches: Vec::new().into(), selected_item: 0, list: Default::default(), @@ -3188,6 +3598,9 @@ impl Editor { if menu.matches.is_empty() { None } else { + _ = this.update(&mut cx, |editor, cx| { + menu.pre_resolve_completion_documentation(editor.project.clone(), cx); + }); Some(menu) } } else { @@ -3197,23 +3610,30 @@ impl Editor { this.update(&mut cx, |this, cx| { this.completion_tasks.retain(|(task_id, _)| *task_id > id); - match this.context_menu.as_ref() { + let mut context_menu = this.context_menu.write(); + match context_menu.as_ref() { None => {} + Some(ContextMenu::Completions(prev_menu)) => { if prev_menu.id > id { return; } } + _ => return, } if this.focused && menu.is_some() { let menu = menu.unwrap(); - this.show_context_menu(ContextMenu::Completions(menu), cx); + *context_menu = Some(ContextMenu::Completions(menu)); + drop(context_menu); + this.discard_copilot_suggestion(cx); + cx.notify(); } else if this.completion_tasks.is_empty() { // If there are no more completion tasks and the last menu was // empty, we should hide it. If it was already hidden, we should // also show the copilot suggestion when available. + drop(context_menu); if this.hide_context_menu(cx).is_none() { this.update_visible_copilot_suggestion(cx); } @@ -3244,7 +3664,8 @@ impl Editor { .matches .get(action.item_ix.unwrap_or(completions_menu.selected_item))?; let buffer_handle = completions_menu.buffer; - let completion = completions_menu.completions.get(mat.candidate_id)?; + let completions = completions_menu.completions.read(); + let completion = completions.get(mat.candidate_id)?; let snippet; let text; @@ -3357,14 +3778,13 @@ impl Editor { } pub fn toggle_code_actions(&mut self, action: &ToggleCodeActions, cx: &mut ViewContext) { - if matches!( - self.context_menu.as_ref(), - Some(ContextMenu::CodeActions(_)) - ) { - self.context_menu.take(); + let mut context_menu = self.context_menu.write(); + if matches!(context_menu.as_ref(), Some(ContextMenu::CodeActions(_))) { + *context_menu = None; cx.notify(); return; } + drop(context_menu); let deployed_from_indicator = action.deployed_from_indicator; let mut task = self.code_actions_task.take(); @@ -3377,16 +3797,16 @@ impl Editor { this.update(&mut cx, |this, cx| { if this.focused { if let Some((buffer, actions)) = this.available_code_actions.clone() { - this.show_context_menu( - ContextMenu::CodeActions(CodeActionsMenu { + this.completion_tasks.clear(); + this.discard_copilot_suggestion(cx); + *this.context_menu.write() = + Some(ContextMenu::CodeActions(CodeActionsMenu { buffer, actions, selected_item: Default::default(), list: Default::default(), deployed_from_indicator, - }), - cx, - ); + })); } } })?; @@ -3749,7 +4169,10 @@ impl Editor { if self.has_active_copilot_suggestion(cx) { self.cycle_copilot_suggestions(Direction::Next, cx); } else { - self.refresh_copilot_suggestions(false, cx); + let is_copilot_disabled = self.refresh_copilot_suggestions(false, cx).is_none(); + if is_copilot_disabled { + cx.propagate_action(); + } } } @@ -3761,7 +4184,10 @@ impl Editor { if self.has_active_copilot_suggestion(cx) { self.cycle_copilot_suggestions(Direction::Prev, cx); } else { - self.refresh_copilot_suggestions(false, cx); + let is_copilot_disabled = self.refresh_copilot_suggestions(false, cx).is_none(); + if is_copilot_disabled { + cx.propagate_action(); + } } } @@ -3850,7 +4276,7 @@ impl Editor { let selection = self.selections.newest_anchor(); let cursor = selection.head(); - if self.context_menu.is_some() + if self.context_menu.read().is_some() || !self.completion_tasks.is_empty() || selection.start != selection.end { @@ -3984,6 +4410,7 @@ impl Editor { pub fn context_menu_visible(&self) -> bool { self.context_menu + .read() .as_ref() .map_or(false, |menu| menu.visible()) } @@ -3994,24 +4421,20 @@ impl Editor { style: EditorStyle, cx: &mut ViewContext, ) -> Option<(DisplayPoint, AnyElement)> { - self.context_menu - .as_ref() - .map(|menu| menu.render(cursor_position, style, cx)) - } - - fn show_context_menu(&mut self, menu: ContextMenu, cx: &mut ViewContext) { - if !matches!(menu, ContextMenu::Completions(_)) { - self.completion_tasks.clear(); - } - self.context_menu = Some(menu); - self.discard_copilot_suggestion(cx); - cx.notify(); + self.context_menu.read().as_ref().map(|menu| { + menu.render( + cursor_position, + style, + self.workspace.as_ref().map(|(w, _)| w.clone()), + cx, + ) + }) } fn hide_context_menu(&mut self, cx: &mut ViewContext) -> Option { cx.notify(); self.completion_tasks.clear(); - let context_menu = self.context_menu.take(); + let context_menu = self.context_menu.write().take(); if context_menu.is_some() { self.update_visible_copilot_suggestion(cx); } @@ -5393,8 +5816,9 @@ impl Editor { if self .context_menu + .write() .as_mut() - .map(|menu| menu.select_last(cx)) + .map(|menu| menu.select_last(self.project.as_ref(), cx)) .unwrap_or(false) { return; @@ -5447,26 +5871,26 @@ impl Editor { } pub fn context_menu_first(&mut self, _: &ContextMenuFirst, cx: &mut ViewContext) { - if let Some(context_menu) = self.context_menu.as_mut() { - context_menu.select_first(cx); + if let Some(context_menu) = self.context_menu.write().as_mut() { + context_menu.select_first(self.project.as_ref(), cx); } } pub fn context_menu_prev(&mut self, _: &ContextMenuPrev, cx: &mut ViewContext) { - if let Some(context_menu) = self.context_menu.as_mut() { - context_menu.select_prev(cx); + if let Some(context_menu) = self.context_menu.write().as_mut() { + context_menu.select_prev(self.project.as_ref(), cx); } } pub fn context_menu_next(&mut self, _: &ContextMenuNext, cx: &mut ViewContext) { - if let Some(context_menu) = self.context_menu.as_mut() { - context_menu.select_next(cx); + if let Some(context_menu) = self.context_menu.write().as_mut() { + context_menu.select_next(self.project.as_ref(), cx); } } pub fn context_menu_last(&mut self, _: &ContextMenuLast, cx: &mut ViewContext) { - if let Some(context_menu) = self.context_menu.as_mut() { - context_menu.select_last(cx); + if let Some(context_menu) = self.context_menu.write().as_mut() { + context_menu.select_last(self.project.as_ref(), cx); } } @@ -8975,7 +9399,7 @@ impl View for Editor { keymap.add_identifier("renaming"); } if self.context_menu_visible() { - match self.context_menu.as_ref() { + match self.context_menu.read().as_ref() { Some(ContextMenu::Completions(_)) => { keymap.add_identifier("menu"); keymap.add_identifier("showing_completions") diff --git a/crates/editor/src/editor_settings.rs b/crates/editor/src/editor_settings.rs index b06f23429a..75f8b800f9 100644 --- a/crates/editor/src/editor_settings.rs +++ b/crates/editor/src/editor_settings.rs @@ -7,6 +7,7 @@ pub struct EditorSettings { pub cursor_blink: bool, pub hover_popover_enabled: bool, pub show_completions_on_input: bool, + pub show_completion_documentation: bool, pub use_on_type_format: bool, pub scrollbar: Scrollbar, pub relative_line_numbers: bool, @@ -33,6 +34,7 @@ pub struct EditorSettingsContent { pub cursor_blink: Option, pub hover_popover_enabled: Option, pub show_completions_on_input: Option, + pub show_completion_documentation: Option, pub use_on_type_format: Option, pub scrollbar: Option, pub relative_line_numbers: Option, diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index affe9f60a2..6cc78385a3 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -19,8 +19,8 @@ use gpui::{ use indoc::indoc; use language::{ language_settings::{AllLanguageSettings, AllLanguageSettingsContent, LanguageSettingsContent}, - BracketPairConfig, FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageRegistry, - Override, Point, + BracketPairConfig, BundledFormatter, FakeLspAdapter, LanguageConfig, LanguageConfigOverride, + LanguageRegistry, Override, Point, }; use parking_lot::Mutex; use project::project_settings::{LspSettings, ProjectSettings}; @@ -1334,7 +1334,7 @@ async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut gpui::TestAppCon cx.update_editor(|editor, cx| editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, cx)); cx.assert_editor_state( - &r#"ˇone + &r#"one two three @@ -1345,9 +1345,22 @@ async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut gpui::TestAppCon .unindent(), ); - cx.update_editor(|editor, cx| editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, cx)); + cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx)); cx.assert_editor_state( - &r#"ˇone + &r#"one + two + + three + four + five + ˇ + six"# + .unindent(), + ); + + cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx)); + cx.assert_editor_state( + &r#"one two ˇ three @@ -1367,32 +1380,6 @@ async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut gpui::TestAppCon four five - sixˇ"# - .unindent(), - ); - - cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx)); - cx.assert_editor_state( - &r#"one - two - - three - four - five - ˇ - sixˇ"# - .unindent(), - ); - - cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx)); - cx.assert_editor_state( - &r#"one - two - ˇ - three - four - five - ˇ six"# .unindent(), ); @@ -5090,7 +5077,9 @@ async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) { - init_test(cx, |_| {}); + init_test(cx, |settings| { + settings.defaults.formatter = Some(language_settings::Formatter::LanguageServer) + }); let mut language = Language::new( LanguageConfig { @@ -5106,6 +5095,12 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) { document_formatting_provider: Some(lsp::OneOf::Left(true)), ..Default::default() }, + // Enable Prettier formatting for the same buffer, and ensure + // LSP is called instead of Prettier. + enabled_formatters: vec![BundledFormatter::Prettier { + parser_name: Some("test_parser"), + plugin_names: Vec::new(), + }], ..Default::default() })) .await; @@ -5114,7 +5109,10 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) { fs.insert_file("/file.rs", Default::default()).await; let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; - project.update(cx, |project, _| project.languages().add(Arc::new(language))); + project.update(cx, |project, _| { + project.enable_test_prettier(&[]); + project.languages().add(Arc::new(language)); + }); let buffer = project .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx)) .await @@ -5232,7 +5230,9 @@ async fn test_concurrent_format_requests(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_strip_whitespace_and_format_via_lsp(cx: &mut gpui::TestAppContext) { - init_test(cx, |_| {}); + init_test(cx, |settings| { + settings.defaults.formatter = Some(language_settings::Formatter::Auto) + }); let mut cx = EditorLspTestContext::new_rust( lsp::ServerCapabilities { @@ -5431,9 +5431,9 @@ async fn test_completion(cx: &mut gpui::TestAppContext) { additional edit "}); cx.simulate_keystroke(" "); - assert!(cx.editor(|e, _| e.context_menu.is_none())); + assert!(cx.editor(|e, _| e.context_menu.read().is_none())); cx.simulate_keystroke("s"); - assert!(cx.editor(|e, _| e.context_menu.is_none())); + assert!(cx.editor(|e, _| e.context_menu.read().is_none())); cx.assert_editor_state(indoc! {" one.second_completion @@ -5495,12 +5495,12 @@ async fn test_completion(cx: &mut gpui::TestAppContext) { }); cx.set_state("editorˇ"); cx.simulate_keystroke("."); - assert!(cx.editor(|e, _| e.context_menu.is_none())); + assert!(cx.editor(|e, _| e.context_menu.read().is_none())); cx.simulate_keystroke("c"); cx.simulate_keystroke("l"); cx.simulate_keystroke("o"); cx.assert_editor_state("editor.cloˇ"); - assert!(cx.editor(|e, _| e.context_menu.is_none())); + assert!(cx.editor(|e, _| e.context_menu.read().is_none())); cx.update_editor(|editor, cx| { editor.show_completions(&ShowCompletions, cx); }); @@ -7789,7 +7789,7 @@ async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui: cx.simulate_keystroke("-"); cx.foreground().run_until_parked(); cx.update_editor(|editor, _| { - if let Some(ContextMenu::Completions(menu)) = &editor.context_menu { + if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() { assert_eq!( menu.matches.iter().map(|m| &m.string).collect::>(), &["bg-red", "bg-blue", "bg-yellow"] @@ -7802,7 +7802,7 @@ async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui: cx.simulate_keystroke("l"); cx.foreground().run_until_parked(); cx.update_editor(|editor, _| { - if let Some(ContextMenu::Completions(menu)) = &editor.context_menu { + if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() { assert_eq!( menu.matches.iter().map(|m| &m.string).collect::>(), &["bg-blue", "bg-yellow"] @@ -7818,7 +7818,7 @@ async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui: cx.simulate_keystroke("l"); cx.foreground().run_until_parked(); cx.update_editor(|editor, _| { - if let Some(ContextMenu::Completions(menu)) = &editor.context_menu { + if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() { assert_eq!( menu.matches.iter().map(|m| &m.string).collect::>(), &["bg-yellow"] @@ -7829,6 +7829,75 @@ async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui: }); } +#[gpui::test] +async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) { + init_test(cx, |settings| { + settings.defaults.formatter = Some(language_settings::Formatter::Prettier) + }); + + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + + let test_plugin = "test_plugin"; + let _ = language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + enabled_formatters: vec![BundledFormatter::Prettier { + parser_name: Some("test_parser"), + plugin_names: vec![test_plugin], + }], + ..Default::default() + })) + .await; + + let fs = FakeFs::new(cx.background()); + fs.insert_file("/file.rs", Default::default()).await; + + let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; + let prettier_format_suffix = project.update(cx, |project, _| { + let suffix = project.enable_test_prettier(&[test_plugin]); + project.languages().add(Arc::new(language)); + suffix + }); + let buffer = project + .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx)) + .await + .unwrap(); + + let buffer_text = "one\ntwo\nthree\n"; + let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); + let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx); + editor.update(cx, |editor, cx| editor.set_text(buffer_text, cx)); + + let format = editor.update(cx, |editor, cx| { + editor.perform_format(project.clone(), FormatTrigger::Manual, cx) + }); + format.await.unwrap(); + assert_eq!( + editor.read_with(cx, |editor, cx| editor.text(cx)), + buffer_text.to_string() + prettier_format_suffix, + "Test prettier formatting was not applied to the original buffer text", + ); + + update_test_language_settings(cx, |settings| { + settings.defaults.formatter = Some(language_settings::Formatter::Auto) + }); + let format = editor.update(cx, |editor, cx| { + editor.perform_format(project.clone(), FormatTrigger::Manual, cx) + }); + format.await.unwrap(); + assert_eq!( + editor.read_with(cx, |editor, cx| editor.text(cx)), + buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix, + "Autoformatting (via test prettier) was not applied to the original buffer text", + ); +} + fn empty_range(row: usize, column: usize) -> Range { let point = DisplayPoint::new(row as u32, column as u32); point..point diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 39d91fe899..7b1155890f 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -2372,7 +2372,7 @@ impl Element for EditorElement { } let active = matches!( - editor.context_menu, + editor.context_menu.read().as_ref(), Some(crate::ContextMenu::CodeActions(_)) ); @@ -2383,9 +2383,13 @@ impl Element for EditorElement { } let visible_rows = start_row..start_row + line_layouts.len() as u32; - let mut hover = editor - .hover_state - .render(&snapshot, &style, visible_rows, cx); + let mut hover = editor.hover_state.render( + &snapshot, + &style, + visible_rows, + editor.workspace.as_ref().map(|(w, _)| w.clone()), + cx, + ); let mode = editor.mode; let mut fold_indicators = editor.render_fold_indicators( diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 553cb321c3..5b3985edf9 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -9,13 +9,15 @@ use gpui::{ actions, elements::{Flex, MouseEventHandler, Padding, ParentElement, Text}, platform::{CursorStyle, MouseButton}, - AnyElement, AppContext, Element, ModelHandle, Task, ViewContext, + AnyElement, AppContext, Element, ModelHandle, Task, ViewContext, WeakViewHandle, +}; +use language::{ + markdown, Bias, DiagnosticEntry, DiagnosticSeverity, Language, LanguageRegistry, ParsedMarkdown, }; -use language::{Bias, DiagnosticEntry, DiagnosticSeverity, Language, LanguageRegistry}; use project::{HoverBlock, HoverBlockKind, InlayHintLabelPart, Project}; -use rich_text::{new_paragraph, render_code, render_markdown_mut, RichText}; use std::{ops::Range, sync::Arc, time::Duration}; use util::TryFutureExt; +use workspace::Workspace; pub const HOVER_DELAY_MILLIS: u64 = 350; pub const HOVER_REQUEST_DELAY_MILLIS: u64 = 200; @@ -105,12 +107,15 @@ pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut Vie this.hover_state.diagnostic_popover = None; })?; + let language_registry = project.update(&mut cx, |p, _| p.languages().clone()); + let blocks = vec![inlay_hover.tooltip]; + let parsed_content = parse_blocks(&blocks, &language_registry, None).await; + let hover_popover = InfoPopover { project: project.clone(), symbol_range: RangeInEditor::Inlay(inlay_hover.range.clone()), - blocks: vec![inlay_hover.tooltip], - language: None, - rendered_content: None, + blocks, + parsed_content, }; this.update(&mut cx, |this, cx| { @@ -288,35 +293,38 @@ fn show_hover( }); })?; - // Construct new hover popover from hover request - let hover_popover = hover_request.await.ok().flatten().and_then(|hover_result| { - if hover_result.is_empty() { - return None; + let hover_result = hover_request.await.ok().flatten(); + let hover_popover = match hover_result { + Some(hover_result) if !hover_result.is_empty() => { + // Create symbol range of anchors for highlighting and filtering of future requests. + let range = if let Some(range) = hover_result.range { + let start = snapshot + .buffer_snapshot + .anchor_in_excerpt(excerpt_id.clone(), range.start); + let end = snapshot + .buffer_snapshot + .anchor_in_excerpt(excerpt_id.clone(), range.end); + + start..end + } else { + anchor..anchor + }; + + let language_registry = project.update(&mut cx, |p, _| p.languages().clone()); + let blocks = hover_result.contents; + let language = hover_result.language; + let parsed_content = parse_blocks(&blocks, &language_registry, language).await; + + Some(InfoPopover { + project: project.clone(), + symbol_range: RangeInEditor::Text(range), + blocks, + parsed_content, + }) } - // Create symbol range of anchors for highlighting and filtering - // of future requests. - let range = if let Some(range) = hover_result.range { - let start = snapshot - .buffer_snapshot - .anchor_in_excerpt(excerpt_id.clone(), range.start); - let end = snapshot - .buffer_snapshot - .anchor_in_excerpt(excerpt_id.clone(), range.end); - - start..end - } else { - anchor..anchor - }; - - Some(InfoPopover { - project: project.clone(), - symbol_range: RangeInEditor::Text(range), - blocks: hover_result.contents, - language: hover_result.language, - rendered_content: None, - }) - }); + _ => None, + }; this.update(&mut cx, |this, cx| { if let Some(symbol_range) = hover_popover @@ -345,44 +353,56 @@ fn show_hover( editor.hover_state.info_task = Some(task); } -fn render_blocks( +async fn parse_blocks( blocks: &[HoverBlock], language_registry: &Arc, - language: Option<&Arc>, -) -> RichText { - let mut data = RichText { - text: Default::default(), - highlights: Default::default(), - region_ranges: Default::default(), - regions: Default::default(), - }; + language: Option>, +) -> markdown::ParsedMarkdown { + let mut text = String::new(); + let mut highlights = Vec::new(); + let mut region_ranges = Vec::new(); + let mut regions = Vec::new(); for block in blocks { match &block.kind { HoverBlockKind::PlainText => { - new_paragraph(&mut data.text, &mut Vec::new()); - data.text.push_str(&block.text); + markdown::new_paragraph(&mut text, &mut Vec::new()); + text.push_str(&block.text); } + HoverBlockKind::Markdown => { - render_markdown_mut(&block.text, language_registry, language, &mut data) + markdown::parse_markdown_block( + &block.text, + language_registry, + language.clone(), + &mut text, + &mut highlights, + &mut region_ranges, + &mut regions, + ) + .await } + HoverBlockKind::Code { language } => { if let Some(language) = language_registry .language_for_name(language) .now_or_never() .and_then(Result::ok) { - render_code(&mut data.text, &mut data.highlights, &block.text, &language); + markdown::highlight_code(&mut text, &mut highlights, &block.text, &language); } else { - data.text.push_str(&block.text); + text.push_str(&block.text); } } } } - data.text = data.text.trim().to_string(); - - data + ParsedMarkdown { + text: text.trim().to_string(), + highlights, + region_ranges, + regions, + } } #[derive(Default)] @@ -403,6 +423,7 @@ impl HoverState { snapshot: &EditorSnapshot, style: &EditorStyle, visible_rows: Range, + workspace: Option>, cx: &mut ViewContext, ) -> Option<(DisplayPoint, Vec>)> { // If there is a diagnostic, position the popovers based on that. @@ -432,7 +453,7 @@ impl HoverState { elements.push(diagnostic_popover.render(style, cx)); } if let Some(info_popover) = self.info_popover.as_mut() { - elements.push(info_popover.render(style, cx)); + elements.push(info_popover.render(style, workspace, cx)); } Some((point, elements)) @@ -444,32 +465,23 @@ pub struct InfoPopover { pub project: ModelHandle, symbol_range: RangeInEditor, pub blocks: Vec, - language: Option>, - rendered_content: Option, + parsed_content: ParsedMarkdown, } impl InfoPopover { pub fn render( &mut self, style: &EditorStyle, + workspace: Option>, cx: &mut ViewContext, ) -> AnyElement { - let rendered_content = self.rendered_content.get_or_insert_with(|| { - render_blocks( - &self.blocks, - self.project.read(cx).languages(), - self.language.as_ref(), - ) - }); - - MouseEventHandler::new::(0, cx, move |_, cx| { - let code_span_background_color = style.document_highlight_read_background; + MouseEventHandler::new::(0, cx, |_, cx| { Flex::column() - .scrollable::(1, None, cx) - .with_child(rendered_content.element( - style.syntax.clone(), - style.text.clone(), - code_span_background_color, + .scrollable::(0, None, cx) + .with_child(crate::render_parsed_markdown::( + &self.parsed_content, + style, + workspace, cx, )) .contained() @@ -572,7 +584,6 @@ mod tests { use language::{language_settings::InlayHintSettings, Diagnostic, DiagnosticSet}; use lsp::LanguageServerId; use project::{HoverBlock, HoverBlockKind}; - use rich_text::Highlight; use smol::stream::StreamExt; use unindent::Unindent; use util::test::marked_text_ranges; @@ -793,7 +804,7 @@ mod tests { }], ); - let rendered = render_blocks(&blocks, &Default::default(), None); + let rendered = smol::block_on(parse_blocks(&blocks, &Default::default(), None)); assert_eq!( rendered.text, code_str.trim(), @@ -900,7 +911,7 @@ mod tests { // Links Row { blocks: vec![HoverBlock { - text: "one [two](the-url) three".to_string(), + text: "one [two](https://the-url) three".to_string(), kind: HoverBlockKind::Markdown, }], expected_marked_text: "one «two» three".to_string(), @@ -921,7 +932,7 @@ mod tests { - a - b * two - - [c](the-url) + - [c](https://the-url) - d" .unindent(), kind: HoverBlockKind::Markdown, @@ -985,7 +996,7 @@ mod tests { expected_styles, } in &rows[0..] { - let rendered = render_blocks(&blocks, &Default::default(), None); + let rendered = smol::block_on(parse_blocks(&blocks, &Default::default(), None)); let (expected_text, ranges) = marked_text_ranges(expected_marked_text, false); let expected_highlights = ranges @@ -1001,11 +1012,8 @@ mod tests { .highlights .iter() .filter_map(|(range, highlight)| { - let style = match highlight { - Highlight::Id(id) => id.style(&style.syntax)?, - Highlight::Highlight(style) => style.clone(), - }; - Some((range.clone(), style)) + let highlight = highlight.to_highlight_style(&style.syntax)?; + Some((range.clone(), highlight)) }) .collect(); @@ -1258,11 +1266,7 @@ mod tests { "Popover range should match the new type label part" ); assert_eq!( - popover - .rendered_content - .as_ref() - .expect("should have label text for new type hint") - .text, + popover.parsed_content.text, format!("A tooltip for `{new_type_label}`"), "Rendered text should not anyhow alter backticks" ); @@ -1316,11 +1320,7 @@ mod tests { "Popover range should match the struct label part" ); assert_eq!( - popover - .rendered_content - .as_ref() - .expect("should have label text for struct hint") - .text, + popover.parsed_content.text, format!("A tooltip for {struct_label}"), "Rendered markdown element should remove backticks from text" ); diff --git a/crates/editor/src/movement.rs b/crates/editor/src/movement.rs index 62fd931ecc..8f5d0d802c 100644 --- a/crates/editor/src/movement.rs +++ b/crates/editor/src/movement.rs @@ -263,7 +263,7 @@ pub fn start_of_paragraph( ) -> DisplayPoint { let point = display_point.to_point(map); if point.row == 0 { - return map.max_point(); + return DisplayPoint::zero(); } let mut found_non_blank_line = false; @@ -290,7 +290,7 @@ pub fn end_of_paragraph( ) -> DisplayPoint { let point = display_point.to_point(map); if point.row == map.max_buffer_row() { - return DisplayPoint::zero(); + return map.max_point(); } let mut found_non_blank_line = false; diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index c5d17dfd2e..23a117405c 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -498,77 +498,91 @@ impl MultiBuffer { } } - for (buffer_id, mut edits) in buffer_edits { - edits.sort_unstable_by_key(|edit| edit.range.start); - self.buffers.borrow()[&buffer_id] - .buffer - .update(cx, |buffer, cx| { - let mut edits = edits.into_iter().peekable(); - let mut insertions = Vec::new(); - let mut original_indent_columns = Vec::new(); - let mut deletions = Vec::new(); - let empty_str: Arc = "".into(); - while let Some(BufferEdit { - mut range, - new_text, - mut is_insertion, - original_indent_column, - }) = edits.next() - { + drop(cursor); + drop(snapshot); + // Non-generic part of edit, hoisted out to avoid blowing up LLVM IR. + fn tail( + this: &mut MultiBuffer, + buffer_edits: HashMap>, + autoindent_mode: Option, + edited_excerpt_ids: Vec, + cx: &mut ModelContext, + ) { + for (buffer_id, mut edits) in buffer_edits { + edits.sort_unstable_by_key(|edit| edit.range.start); + this.buffers.borrow()[&buffer_id] + .buffer + .update(cx, |buffer, cx| { + let mut edits = edits.into_iter().peekable(); + let mut insertions = Vec::new(); + let mut original_indent_columns = Vec::new(); + let mut deletions = Vec::new(); + let empty_str: Arc = "".into(); while let Some(BufferEdit { - range: next_range, - is_insertion: next_is_insertion, - .. - }) = edits.peek() + mut range, + new_text, + mut is_insertion, + original_indent_column, + }) = edits.next() { - if range.end >= next_range.start { - range.end = cmp::max(next_range.end, range.end); - is_insertion |= *next_is_insertion; - edits.next(); - } else { - break; + while let Some(BufferEdit { + range: next_range, + is_insertion: next_is_insertion, + .. + }) = edits.peek() + { + if range.end >= next_range.start { + range.end = cmp::max(next_range.end, range.end); + is_insertion |= *next_is_insertion; + edits.next(); + } else { + break; + } + } + + if is_insertion { + original_indent_columns.push(original_indent_column); + insertions.push(( + buffer.anchor_before(range.start) + ..buffer.anchor_before(range.end), + new_text.clone(), + )); + } else if !range.is_empty() { + deletions.push(( + buffer.anchor_before(range.start) + ..buffer.anchor_before(range.end), + empty_str.clone(), + )); } } - if is_insertion { - original_indent_columns.push(original_indent_column); - insertions.push(( - buffer.anchor_before(range.start)..buffer.anchor_before(range.end), - new_text.clone(), - )); - } else if !range.is_empty() { - deletions.push(( - buffer.anchor_before(range.start)..buffer.anchor_before(range.end), - empty_str.clone(), - )); - } - } + let deletion_autoindent_mode = + if let Some(AutoindentMode::Block { .. }) = autoindent_mode { + Some(AutoindentMode::Block { + original_indent_columns: Default::default(), + }) + } else { + None + }; + let insertion_autoindent_mode = + if let Some(AutoindentMode::Block { .. }) = autoindent_mode { + Some(AutoindentMode::Block { + original_indent_columns, + }) + } else { + None + }; - let deletion_autoindent_mode = - if let Some(AutoindentMode::Block { .. }) = autoindent_mode { - Some(AutoindentMode::Block { - original_indent_columns: Default::default(), - }) - } else { - None - }; - let insertion_autoindent_mode = - if let Some(AutoindentMode::Block { .. }) = autoindent_mode { - Some(AutoindentMode::Block { - original_indent_columns, - }) - } else { - None - }; + buffer.edit(deletions, deletion_autoindent_mode, cx); + buffer.edit(insertions, insertion_autoindent_mode, cx); + }) + } - buffer.edit(deletions, deletion_autoindent_mode, cx); - buffer.edit(insertions, insertion_autoindent_mode, cx); - }) + cx.emit(Event::ExcerptsEdited { + ids: edited_excerpt_ids, + }); } - - cx.emit(Event::ExcerptsEdited { - ids: edited_excerpt_ids, - }); + tail(self, buffer_edits, autoindent_mode, edited_excerpt_ids, cx); } pub fn start_transaction(&mut self, cx: &mut ModelContext) -> Option { diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index 222a9c650a..b7a4a387ab 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -222,6 +222,10 @@ fn toggle_or_cycle_file_finder( .as_ref() .and_then(|found_path| found_path.absolute.as_ref()) }) + .filter(|(_, history_abs_path)| match history_abs_path { + Some(abs_path) => history_file_exists(abs_path), + None => true, + }) .map(|(history_path, abs_path)| FoundPath::new(history_path, abs_path)), ) .collect(); @@ -246,6 +250,16 @@ fn toggle_or_cycle_file_finder( } } +#[cfg(not(test))] +fn history_file_exists(abs_path: &PathBuf) -> bool { + abs_path.exists() +} + +#[cfg(test)] +fn history_file_exists(abs_path: &PathBuf) -> bool { + !abs_path.ends_with("nonexistent.rs") +} + pub enum Event { Selected(ProjectPath), Dismissed, @@ -515,12 +529,7 @@ impl PickerDelegate for FileFinderDelegate { project .worktree_for_id(history_item.project.worktree_id, cx) .is_some() - || (project.is_local() - && history_item - .absolute - .as_ref() - .filter(|abs_path| abs_path.exists()) - .is_some()) + || (project.is_local() && history_item.absolute.is_some()) }) .cloned() .map(|p| (p, None)) @@ -1900,13 +1909,8 @@ mod tests { .matches .search .iter() - .map(|e| e.path.to_path_buf()) + .map(|path_match| path_match.path.to_path_buf()) .collect::>(); - assert_eq!( - search_entries.len(), - 4, - "All history and the new file should be found after query {query} as search results" - ); assert_eq!( search_entries, vec![ @@ -1920,6 +1924,100 @@ mod tests { }); } + #[gpui::test] + async fn test_nonexistent_history_items_not_shown( + deterministic: Arc, + cx: &mut gpui::TestAppContext, + ) { + let app_state = init_test(cx); + + app_state + .fs + .as_fake() + .insert_tree( + "/src", + json!({ + "test": { + "first.rs": "// First Rust file", + "nonexistent.rs": "// Second Rust file", + "third.rs": "// Third Rust file", + } + }), + ) + .await; + + let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; + let window = cx.add_window(|cx| Workspace::test_new(project, cx)); + let workspace = window.root(cx); + // generate some history to select from + open_close_queried_buffer( + "fir", + 1, + "first.rs", + window.into(), + &workspace, + &deterministic, + cx, + ) + .await; + open_close_queried_buffer( + "non", + 1, + "nonexistent.rs", + window.into(), + &workspace, + &deterministic, + cx, + ) + .await; + open_close_queried_buffer( + "thi", + 1, + "third.rs", + window.into(), + &workspace, + &deterministic, + cx, + ) + .await; + open_close_queried_buffer( + "fir", + 1, + "first.rs", + window.into(), + &workspace, + &deterministic, + cx, + ) + .await; + + cx.dispatch_action(window.into(), Toggle); + let query = "rs"; + let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); + finder + .update(cx, |finder, cx| { + finder.delegate_mut().update_matches(query.to_string(), cx) + }) + .await; + finder.read_with(cx, |finder, _| { + let delegate = finder.delegate(); + let history_entries = delegate + .matches + .history + .iter() + .map(|(_, path_match)| path_match.as_ref().expect("should have a path match").path.to_path_buf()) + .collect::>(); + assert_eq!( + history_entries, + vec![ + PathBuf::from("test/first.rs"), + PathBuf::from("test/third.rs"), + ], + "Should have all opened files in the history, except the ones that do not exist on disk" + ); + }); + } + async fn open_close_queried_buffer( input: &str, expected_matches: usize, diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index 1d95db9b6c..1bc8fa9a24 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -85,7 +85,7 @@ pub struct RemoveOptions { pub ignore_if_not_exists: bool, } -#[derive(Clone, Debug)] +#[derive(Copy, Clone, Debug)] pub struct Metadata { pub inode: u64, pub mtime: SystemTime, diff --git a/crates/gpui/src/elements/flex.rs b/crates/gpui/src/elements/flex.rs index cdce0423fd..ba387c5e48 100644 --- a/crates/gpui/src/elements/flex.rs +++ b/crates/gpui/src/elements/flex.rs @@ -2,7 +2,8 @@ use std::{any::Any, cell::Cell, f32::INFINITY, ops::Range, rc::Rc}; use crate::{ json::{self, ToJson, Value}, - AnyElement, Axis, Element, ElementStateHandle, SizeConstraint, Vector2FExt, ViewContext, + AnyElement, Axis, Element, ElementStateHandle, SizeConstraint, TypeTag, Vector2FExt, + ViewContext, }; use pathfinder_geometry::{ rect::RectF, @@ -10,10 +11,10 @@ use pathfinder_geometry::{ }; use serde_json::json; -#[derive(Default)] struct ScrollState { scroll_to: Cell>, scroll_position: Cell, + type_tag: TypeTag, } pub struct Flex { @@ -66,8 +67,14 @@ impl Flex { where Tag: 'static, { - let scroll_state = cx.default_element_state::>(element_id); - scroll_state.read(cx).scroll_to.set(scroll_to); + let scroll_state = cx.element_state::>( + element_id, + Rc::new(ScrollState { + scroll_to: Cell::new(scroll_to), + scroll_position: Default::default(), + type_tag: TypeTag::new::(), + }), + ); self.scroll_state = Some((scroll_state, cx.handle().id())); self } @@ -276,38 +283,44 @@ impl Element for Flex { if let Some((scroll_state, id)) = &self.scroll_state { let scroll_state = scroll_state.read(cx).clone(); cx.scene().push_mouse_region( - crate::MouseRegion::new::(*id, 0, bounds) - .on_scroll({ - let axis = self.axis; - move |e, _: &mut V, cx| { - if remaining_space < 0. { - let scroll_delta = e.delta.raw(); + crate::MouseRegion::from_handlers( + scroll_state.type_tag, + *id, + 0, + bounds, + Default::default(), + ) + .on_scroll({ + let axis = self.axis; + move |e, _: &mut V, cx| { + if remaining_space < 0. { + let scroll_delta = e.delta.raw(); - let mut delta = match axis { - Axis::Horizontal => { - if scroll_delta.x().abs() >= scroll_delta.y().abs() { - scroll_delta.x() - } else { - scroll_delta.y() - } + let mut delta = match axis { + Axis::Horizontal => { + if scroll_delta.x().abs() >= scroll_delta.y().abs() { + scroll_delta.x() + } else { + scroll_delta.y() } - Axis::Vertical => scroll_delta.y(), - }; - if !e.delta.precise() { - delta *= 20.; } - - scroll_state - .scroll_position - .set(scroll_state.scroll_position.get() - delta); - - cx.notify(); - } else { - cx.propagate_event(); + Axis::Vertical => scroll_delta.y(), + }; + if !e.delta.precise() { + delta *= 20.; } + + scroll_state + .scroll_position + .set(scroll_state.scroll_position.get() - delta); + + cx.notify(); + } else { + cx.propagate_event(); } - }) - .on_move(|_, _: &mut V, _| { /* Capture move events */ }), + } + }) + .on_move(|_, _: &mut V, _| { /* Capture move events */ }), ) } diff --git a/crates/gpui/src/platform/mac/platform.rs b/crates/gpui/src/platform/mac/platform.rs index 656f2e4475..24ad774759 100644 --- a/crates/gpui/src/platform/mac/platform.rs +++ b/crates/gpui/src/platform/mac/platform.rs @@ -140,6 +140,10 @@ unsafe fn build_classes() { sel!(application:openURLs:), open_urls as extern "C" fn(&mut Object, Sel, id, id), ); + decl.add_method( + sel!(application:continueUserActivity:restorationHandler:), + continue_user_activity as extern "C" fn(&mut Object, Sel, id, id, id), + ); decl.register() } } @@ -1009,6 +1013,26 @@ extern "C" fn open_urls(this: &mut Object, _: Sel, _: id, urls: id) { } } +extern "C" fn continue_user_activity(this: &mut Object, _: Sel, _: id, user_activity: id, _: id) { + let url = unsafe { + let url: id = msg_send!(user_activity, webpageURL); + if url == nil { + log::error!("got unexpected user activity"); + None + } else { + Some( + CStr::from_ptr(url.absoluteString().UTF8String()) + .to_string_lossy() + .to_string(), + ) + } + }; + let platform = unsafe { get_foreground_platform(this) }; + if let Some(callback) = platform.0.borrow_mut().open_urls.as_mut() { + callback(url.into_iter().collect()); + } +} + extern "C" fn handle_menu_item(this: &mut Object, _: Sel, item: id) { unsafe { let platform = get_foreground_platform(this); diff --git a/crates/language/Cargo.toml b/crates/language/Cargo.toml index cf468020ce..f152d34919 100644 --- a/crates/language/Cargo.toml +++ b/crates/language/Cargo.toml @@ -45,6 +45,7 @@ lazy_static.workspace = true log.workspace = true parking_lot.workspace = true postage.workspace = true +pulldown-cmark = { version = "0.9.2", default-features = false } regex.workspace = true schemars.workspace = true serde.workspace = true diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 19e5e290b9..063c7616a8 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -1,11 +1,13 @@ pub use crate::{ diagnostic_set::DiagnosticSet, highlight_map::{HighlightId, HighlightMap}, + markdown::ParsedMarkdown, proto, BracketPair, Grammar, Language, LanguageConfig, LanguageRegistry, PLAIN_TEXT, }; use crate::{ diagnostic_set::{DiagnosticEntry, DiagnosticGroup}, language_settings::{language_settings, LanguageSettings}, + markdown::parse_markdown, outline::OutlineItem, syntax_map::{ SyntaxLayerInfo, SyntaxMap, SyntaxMapCapture, SyntaxMapCaptures, SyntaxMapMatches, @@ -143,11 +145,51 @@ pub struct Diagnostic { pub is_unnecessary: bool, } +pub async fn prepare_completion_documentation( + documentation: &lsp::Documentation, + language_registry: &Arc, + language: Option>, +) -> Documentation { + match documentation { + lsp::Documentation::String(text) => { + if text.lines().count() <= 1 { + Documentation::SingleLine(text.clone()) + } else { + Documentation::MultiLinePlainText(text.clone()) + } + } + + lsp::Documentation::MarkupContent(lsp::MarkupContent { kind, value }) => match kind { + lsp::MarkupKind::PlainText => { + if value.lines().count() <= 1 { + Documentation::SingleLine(value.clone()) + } else { + Documentation::MultiLinePlainText(value.clone()) + } + } + + lsp::MarkupKind::Markdown => { + let parsed = parse_markdown(value, language_registry, language).await; + Documentation::MultiLineMarkdown(parsed) + } + }, + } +} + +#[derive(Clone, Debug)] +pub enum Documentation { + Undocumented, + SingleLine(String), + MultiLinePlainText(String), + MultiLineMarkdown(ParsedMarkdown), +} + #[derive(Clone, Debug)] pub struct Completion { pub old_range: Range, pub new_text: String, pub label: CodeLabel, + pub documentation: Option, pub server_id: LanguageServerId, pub lsp_completion: lsp::CompletionItem, } @@ -1406,82 +1448,95 @@ impl Buffer { return None; } - self.start_transaction(); - self.pending_autoindent.take(); - let autoindent_request = autoindent_mode - .and_then(|mode| self.language.as_ref().map(|_| (self.snapshot(), mode))); + // Non-generic part hoisted out to reduce LLVM IR size. + fn tail( + this: &mut Buffer, + edits: Vec<(Range, Arc)>, + autoindent_mode: Option, + cx: &mut ModelContext, + ) -> Option { + this.start_transaction(); + this.pending_autoindent.take(); + let autoindent_request = autoindent_mode + .and_then(|mode| this.language.as_ref().map(|_| (this.snapshot(), mode))); - let edit_operation = self.text.edit(edits.iter().cloned()); - let edit_id = edit_operation.timestamp(); + let edit_operation = this.text.edit(edits.iter().cloned()); + let edit_id = edit_operation.timestamp(); - if let Some((before_edit, mode)) = autoindent_request { - let mut delta = 0isize; - let entries = edits - .into_iter() - .enumerate() - .zip(&edit_operation.as_edit().unwrap().new_text) - .map(|((ix, (range, _)), new_text)| { - let new_text_length = new_text.len(); - let old_start = range.start.to_point(&before_edit); - let new_start = (delta + range.start as isize) as usize; - delta += new_text_length as isize - (range.end as isize - range.start as isize); + if let Some((before_edit, mode)) = autoindent_request { + let mut delta = 0isize; + let entries = edits + .into_iter() + .enumerate() + .zip(&edit_operation.as_edit().unwrap().new_text) + .map(|((ix, (range, _)), new_text)| { + let new_text_length = new_text.len(); + let old_start = range.start.to_point(&before_edit); + let new_start = (delta + range.start as isize) as usize; + delta += + new_text_length as isize - (range.end as isize - range.start as isize); - let mut range_of_insertion_to_indent = 0..new_text_length; - let mut first_line_is_new = false; - let mut original_indent_column = None; + let mut range_of_insertion_to_indent = 0..new_text_length; + let mut first_line_is_new = false; + let mut original_indent_column = None; - // When inserting an entire line at the beginning of an existing line, - // treat the insertion as new. - if new_text.contains('\n') - && old_start.column <= before_edit.indent_size_for_line(old_start.row).len - { - first_line_is_new = true; - } - - // When inserting text starting with a newline, avoid auto-indenting the - // previous line. - if new_text.starts_with('\n') { - range_of_insertion_to_indent.start += 1; - first_line_is_new = true; - } - - // Avoid auto-indenting after the insertion. - if let AutoindentMode::Block { - original_indent_columns, - } = &mode - { - original_indent_column = - Some(original_indent_columns.get(ix).copied().unwrap_or_else(|| { - indent_size_for_text( - new_text[range_of_insertion_to_indent.clone()].chars(), - ) - .len - })); - if new_text[range_of_insertion_to_indent.clone()].ends_with('\n') { - range_of_insertion_to_indent.end -= 1; + // When inserting an entire line at the beginning of an existing line, + // treat the insertion as new. + if new_text.contains('\n') + && old_start.column + <= before_edit.indent_size_for_line(old_start.row).len + { + first_line_is_new = true; } - } - AutoindentRequestEntry { - first_line_is_new, - original_indent_column, - indent_size: before_edit.language_indent_size_at(range.start, cx), - range: self.anchor_before(new_start + range_of_insertion_to_indent.start) - ..self.anchor_after(new_start + range_of_insertion_to_indent.end), - } - }) - .collect(); + // When inserting text starting with a newline, avoid auto-indenting the + // previous line. + if new_text.starts_with('\n') { + range_of_insertion_to_indent.start += 1; + first_line_is_new = true; + } - self.autoindent_requests.push(Arc::new(AutoindentRequest { - before_edit, - entries, - is_block_mode: matches!(mode, AutoindentMode::Block { .. }), - })); + // Avoid auto-indenting after the insertion. + if let AutoindentMode::Block { + original_indent_columns, + } = &mode + { + original_indent_column = Some( + original_indent_columns.get(ix).copied().unwrap_or_else(|| { + indent_size_for_text( + new_text[range_of_insertion_to_indent.clone()].chars(), + ) + .len + }), + ); + if new_text[range_of_insertion_to_indent.clone()].ends_with('\n') { + range_of_insertion_to_indent.end -= 1; + } + } + + AutoindentRequestEntry { + first_line_is_new, + original_indent_column, + indent_size: before_edit.language_indent_size_at(range.start, cx), + range: this + .anchor_before(new_start + range_of_insertion_to_indent.start) + ..this.anchor_after(new_start + range_of_insertion_to_indent.end), + } + }) + .collect(); + + this.autoindent_requests.push(Arc::new(AutoindentRequest { + before_edit, + entries, + is_block_mode: matches!(mode, AutoindentMode::Block { .. }), + })); + } + + this.end_transaction(cx); + this.send_operation(Operation::Buffer(edit_operation), cx); + Some(edit_id) } - - self.end_transaction(cx); - self.send_operation(Operation::Buffer(edit_operation), cx); - Some(edit_id) + tail(self, edits, autoindent_mode, cx) } fn did_edit( diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 7d113a88af..0b49d92125 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -2,6 +2,7 @@ mod buffer; mod diagnostic_set; mod highlight_map; pub mod language_settings; +pub mod markdown; mod outline; pub mod proto; mod syntax_map; @@ -110,7 +111,6 @@ pub struct LanguageServerName(pub Arc); pub struct CachedLspAdapter { pub name: LanguageServerName, pub short_name: &'static str, - pub initialization_options: Option, pub disk_based_diagnostic_sources: Vec, pub disk_based_diagnostics_progress_token: Option, pub language_ids: HashMap, @@ -121,7 +121,6 @@ impl CachedLspAdapter { pub async fn new(adapter: Arc) -> Arc { let name = adapter.name().await; let short_name = adapter.short_name(); - let initialization_options = adapter.initialization_options().await; let disk_based_diagnostic_sources = adapter.disk_based_diagnostic_sources().await; let disk_based_diagnostics_progress_token = adapter.disk_based_diagnostics_progress_token().await; @@ -130,7 +129,6 @@ impl CachedLspAdapter { Arc::new(CachedLspAdapter { name, short_name, - initialization_options, disk_based_diagnostic_sources, disk_based_diagnostics_progress_token, language_ids, @@ -227,6 +225,10 @@ impl CachedLspAdapter { ) -> Option { self.adapter.label_for_symbol(name, kind, language).await } + + pub fn enabled_formatters(&self) -> Vec { + self.adapter.enabled_formatters() + } } pub trait LspAdapterDelegate: Send + Sync { @@ -333,6 +335,33 @@ pub trait LspAdapter: 'static + Send + Sync { async fn language_ids(&self) -> HashMap { Default::default() } + + fn enabled_formatters(&self) -> Vec { + Vec::new() + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum BundledFormatter { + Prettier { + // See https://prettier.io/docs/en/options.html#parser for a list of valid values. + // Usually, every language has a single parser (standard or plugin-provided), hence `Some("parser_name")` can be used. + // There can not be multiple parsers for a single language, in case of a conflict, we would attempt to select the one with most plugins. + // + // But exceptions like Tailwind CSS exist, which uses standard parsers for CSS/JS/HTML/etc. but require an extra plugin to be installed. + // For those cases, `None` will install the plugin but apply other, regular parser defined for the language, and this would not be a conflict. + parser_name: Option<&'static str>, + plugin_names: Vec<&'static str>, + }, +} + +impl BundledFormatter { + pub fn prettier(parser_name: &'static str) -> Self { + Self::Prettier { + parser_name: Some(parser_name), + plugin_names: Vec::new(), + } + } } #[derive(Clone, Debug, PartialEq, Eq)] @@ -467,6 +496,7 @@ pub struct FakeLspAdapter { pub initializer: Option>, pub disk_based_diagnostics_progress_token: Option, pub disk_based_diagnostics_sources: Vec, + pub enabled_formatters: Vec, } #[derive(Clone, Debug, Default)] @@ -1729,6 +1759,7 @@ impl Default for FakeLspAdapter { disk_based_diagnostics_progress_token: None, initialization_options: None, disk_based_diagnostics_sources: Vec::new(), + enabled_formatters: Vec::new(), } } } @@ -1785,6 +1816,10 @@ impl LspAdapter for Arc { async fn initialization_options(&self) -> Option { self.initialization_options.clone() } + + fn enabled_formatters(&self) -> Vec { + self.enabled_formatters.clone() + } } fn get_capture_indices(query: &Query, captures: &mut [(&str, &mut Option)]) { diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index c3f706802a..9cac5c523e 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -50,6 +50,7 @@ pub struct LanguageSettings { pub remove_trailing_whitespace_on_save: bool, pub ensure_final_newline_on_save: bool, pub formatter: Formatter, + pub prettier: HashMap, pub enable_language_server: bool, pub show_copilot_suggestions: bool, pub show_whitespaces: ShowWhitespaceSetting, @@ -98,6 +99,8 @@ pub struct LanguageSettingsContent { #[serde(default)] pub formatter: Option, #[serde(default)] + pub prettier: Option>, + #[serde(default)] pub enable_language_server: Option, #[serde(default)] pub show_copilot_suggestions: Option, @@ -149,10 +152,13 @@ pub enum ShowWhitespaceSetting { All, } -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum Formatter { + #[default] + Auto, LanguageServer, + Prettier, External { command: Arc, arguments: Arc<[String]>, @@ -392,6 +398,7 @@ fn merge_settings(settings: &mut LanguageSettings, src: &LanguageSettingsContent src.preferred_line_length, ); merge(&mut settings.formatter, src.formatter.clone()); + merge(&mut settings.prettier, src.prettier.clone()); merge(&mut settings.format_on_save, src.format_on_save.clone()); merge( &mut settings.remove_trailing_whitespace_on_save, diff --git a/crates/language/src/markdown.rs b/crates/language/src/markdown.rs new file mode 100644 index 0000000000..7f57eba309 --- /dev/null +++ b/crates/language/src/markdown.rs @@ -0,0 +1,301 @@ +use std::sync::Arc; +use std::{ops::Range, path::PathBuf}; + +use crate::{HighlightId, Language, LanguageRegistry}; +use gpui::fonts::{self, HighlightStyle, Weight}; +use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag}; + +#[derive(Debug, Clone)] +pub struct ParsedMarkdown { + pub text: String, + pub highlights: Vec<(Range, MarkdownHighlight)>, + pub region_ranges: Vec>, + pub regions: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum MarkdownHighlight { + Style(MarkdownHighlightStyle), + Code(HighlightId), +} + +impl MarkdownHighlight { + pub fn to_highlight_style(&self, theme: &theme::SyntaxTheme) -> Option { + match self { + MarkdownHighlight::Style(style) => { + let mut highlight = HighlightStyle::default(); + + if style.italic { + highlight.italic = Some(true); + } + + if style.underline { + highlight.underline = Some(fonts::Underline { + thickness: 1.0.into(), + ..Default::default() + }); + } + + if style.weight != fonts::Weight::default() { + highlight.weight = Some(style.weight); + } + + Some(highlight) + } + + MarkdownHighlight::Code(id) => id.style(theme), + } + } +} + +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct MarkdownHighlightStyle { + pub italic: bool, + pub underline: bool, + pub weight: Weight, +} + +#[derive(Debug, Clone)] +pub struct ParsedRegion { + pub code: bool, + pub link: Option, +} + +#[derive(Debug, Clone)] +pub enum Link { + Web { url: String }, + Path { path: PathBuf }, +} + +impl Link { + fn identify(text: String) -> Option { + if text.starts_with("http") { + return Some(Link::Web { url: text }); + } + + let path = PathBuf::from(text); + if path.is_absolute() { + return Some(Link::Path { path }); + } + + None + } +} + +pub async fn parse_markdown( + markdown: &str, + language_registry: &Arc, + language: Option>, +) -> ParsedMarkdown { + let mut text = String::new(); + let mut highlights = Vec::new(); + let mut region_ranges = Vec::new(); + let mut regions = Vec::new(); + + parse_markdown_block( + markdown, + language_registry, + language, + &mut text, + &mut highlights, + &mut region_ranges, + &mut regions, + ) + .await; + + ParsedMarkdown { + text, + highlights, + region_ranges, + regions, + } +} + +pub async fn parse_markdown_block( + markdown: &str, + language_registry: &Arc, + language: Option>, + text: &mut String, + highlights: &mut Vec<(Range, MarkdownHighlight)>, + region_ranges: &mut Vec>, + regions: &mut Vec, +) { + let mut bold_depth = 0; + let mut italic_depth = 0; + let mut link_url = None; + let mut current_language = None; + let mut list_stack = Vec::new(); + + for event in Parser::new_ext(&markdown, Options::all()) { + let prev_len = text.len(); + match event { + Event::Text(t) => { + if let Some(language) = ¤t_language { + highlight_code(text, highlights, t.as_ref(), language); + } else { + text.push_str(t.as_ref()); + + let mut style = MarkdownHighlightStyle::default(); + + if bold_depth > 0 { + style.weight = Weight::BOLD; + } + + if italic_depth > 0 { + style.italic = true; + } + + if let Some(link) = link_url.clone().and_then(|u| Link::identify(u)) { + region_ranges.push(prev_len..text.len()); + regions.push(ParsedRegion { + code: false, + link: Some(link), + }); + style.underline = true; + } + + if style != MarkdownHighlightStyle::default() { + let mut new_highlight = true; + if let Some((last_range, MarkdownHighlight::Style(last_style))) = + highlights.last_mut() + { + if last_range.end == prev_len && last_style == &style { + last_range.end = text.len(); + new_highlight = false; + } + } + if new_highlight { + let range = prev_len..text.len(); + highlights.push((range, MarkdownHighlight::Style(style))); + } + } + } + } + + Event::Code(t) => { + text.push_str(t.as_ref()); + region_ranges.push(prev_len..text.len()); + + let link = link_url.clone().and_then(|u| Link::identify(u)); + if link.is_some() { + highlights.push(( + prev_len..text.len(), + MarkdownHighlight::Style(MarkdownHighlightStyle { + underline: true, + ..Default::default() + }), + )); + } + regions.push(ParsedRegion { code: true, link }); + } + + Event::Start(tag) => match tag { + Tag::Paragraph => new_paragraph(text, &mut list_stack), + + Tag::Heading(_, _, _) => { + new_paragraph(text, &mut list_stack); + bold_depth += 1; + } + + Tag::CodeBlock(kind) => { + new_paragraph(text, &mut list_stack); + current_language = if let CodeBlockKind::Fenced(language) = kind { + language_registry + .language_for_name(language.as_ref()) + .await + .ok() + } else { + language.clone() + } + } + + Tag::Emphasis => italic_depth += 1, + + Tag::Strong => bold_depth += 1, + + Tag::Link(_, url, _) => link_url = Some(url.to_string()), + + Tag::List(number) => { + list_stack.push((number, false)); + } + + Tag::Item => { + let len = list_stack.len(); + if let Some((list_number, has_content)) = list_stack.last_mut() { + *has_content = false; + if !text.is_empty() && !text.ends_with('\n') { + text.push('\n'); + } + for _ in 0..len - 1 { + text.push_str(" "); + } + if let Some(number) = list_number { + text.push_str(&format!("{}. ", number)); + *number += 1; + *has_content = false; + } else { + text.push_str("- "); + } + } + } + + _ => {} + }, + + Event::End(tag) => match tag { + Tag::Heading(_, _, _) => bold_depth -= 1, + Tag::CodeBlock(_) => current_language = None, + Tag::Emphasis => italic_depth -= 1, + Tag::Strong => bold_depth -= 1, + Tag::Link(_, _, _) => link_url = None, + Tag::List(_) => drop(list_stack.pop()), + _ => {} + }, + + Event::HardBreak => text.push('\n'), + + Event::SoftBreak => text.push(' '), + + _ => {} + } + } +} + +pub fn highlight_code( + text: &mut String, + highlights: &mut Vec<(Range, MarkdownHighlight)>, + content: &str, + language: &Arc, +) { + let prev_len = text.len(); + text.push_str(content); + for (range, highlight_id) in language.highlight_text(&content.into(), 0..content.len()) { + let highlight = MarkdownHighlight::Code(highlight_id); + highlights.push((prev_len + range.start..prev_len + range.end, highlight)); + } +} + +pub fn new_paragraph(text: &mut String, list_stack: &mut Vec<(Option, bool)>) { + let mut is_subsequent_paragraph_of_list = false; + if let Some((_, has_content)) = list_stack.last_mut() { + if *has_content { + is_subsequent_paragraph_of_list = true; + } else { + *has_content = true; + return; + } + } + + if !text.is_empty() { + if !text.ends_with('\n') { + text.push('\n'); + } + text.push('\n'); + } + for _ in 0..list_stack.len().saturating_sub(1) { + text.push_str(" "); + } + if is_subsequent_paragraph_of_list { + text.push_str(" "); + } +} diff --git a/crates/language/src/proto.rs b/crates/language/src/proto.rs index c4abe39d47..957f4ee7fb 100644 --- a/crates/language/src/proto.rs +++ b/crates/language/src/proto.rs @@ -482,6 +482,7 @@ pub async fn deserialize_completion( lsp_completion.filter_text.as_deref(), ) }), + documentation: None, server_id: LanguageServerId(completion.server_id as usize), lsp_completion, }) diff --git a/crates/language_tools/src/lsp_log.rs b/crates/language_tools/src/lsp_log.rs index faed37a97c..c75fea256d 100644 --- a/crates/language_tools/src/lsp_log.rs +++ b/crates/language_tools/src/lsp_log.rs @@ -1,5 +1,5 @@ -use collections::HashMap; -use editor::Editor; +use collections::{HashMap, VecDeque}; +use editor::{Editor, MoveToEnd}; use futures::{channel::mpsc, StreamExt}; use gpui::{ actions, @@ -11,7 +11,7 @@ use gpui::{ AnyElement, AppContext, Element, Entity, ModelContext, ModelHandle, Subscription, View, ViewContext, ViewHandle, WeakModelHandle, }; -use language::{Buffer, LanguageServerId, LanguageServerName}; +use language::{LanguageServerId, LanguageServerName}; use lsp::IoKind; use project::{search::SearchQuery, Project}; use std::{borrow::Cow, sync::Arc}; @@ -22,8 +22,9 @@ use workspace::{ ToolbarItemLocation, ToolbarItemView, Workspace, WorkspaceCreated, }; -const SEND_LINE: &str = "// Send:\n"; -const RECEIVE_LINE: &str = "// Receive:\n"; +const SEND_LINE: &str = "// Send:"; +const RECEIVE_LINE: &str = "// Receive:"; +const MAX_STORED_LOG_ENTRIES: usize = 2000; pub struct LogStore { projects: HashMap, ProjectState>, @@ -36,24 +37,25 @@ struct ProjectState { } struct LanguageServerState { - log_buffer: ModelHandle, + log_messages: VecDeque, rpc_state: Option, _io_logs_subscription: Option, _lsp_logs_subscription: Option, } struct LanguageServerRpcState { - buffer: ModelHandle, + rpc_messages: VecDeque, last_message_kind: Option, } pub struct LspLogView { pub(crate) editor: ViewHandle, + editor_subscription: Subscription, log_store: ModelHandle, current_server_id: Option, is_showing_rpc_trace: bool, project: ModelHandle, - _log_store_subscription: Subscription, + _log_store_subscriptions: Vec, } pub struct LspLogToolbarItemView { @@ -122,10 +124,9 @@ impl LogStore { io_tx, }; cx.spawn_weak(|this, mut cx| async move { - while let Some((project, server_id, io_kind, mut message)) = io_rx.next().await { + while let Some((project, server_id, io_kind, message)) = io_rx.next().await { if let Some(this) = this.upgrade(&cx) { this.update(&mut cx, |this, cx| { - message.push('\n'); this.on_io(project, server_id, io_kind, &message, cx); }); } @@ -168,15 +169,13 @@ impl LogStore { project: &ModelHandle, id: LanguageServerId, cx: &mut ModelContext, - ) -> Option> { + ) -> Option<&mut LanguageServerState> { let project_state = self.projects.get_mut(&project.downgrade())?; let server_state = project_state.servers.entry(id).or_insert_with(|| { cx.notify(); LanguageServerState { rpc_state: None, - log_buffer: cx - .add_model(|cx| Buffer::new(0, cx.model_id() as u64, "")) - .clone(), + log_messages: VecDeque::with_capacity(MAX_STORED_LOG_ENTRIES), _io_logs_subscription: None, _lsp_logs_subscription: None, } @@ -186,7 +185,7 @@ impl LogStore { if let Some(server) = server.as_deref() { if server.has_notification_handler::() { // Another event wants to re-add the server that was already added and subscribed to, avoid doing it again. - return Some(server_state.log_buffer.clone()); + return Some(server_state); } } @@ -215,7 +214,7 @@ impl LogStore { } }) }); - Some(server_state.log_buffer.clone()) + Some(server_state) } fn add_language_server_log( @@ -225,24 +224,26 @@ impl LogStore { message: &str, cx: &mut ModelContext, ) -> Option<()> { - let buffer = match self + let language_server_state = match self .projects .get_mut(&project.downgrade())? .servers - .get(&id) - .map(|state| state.log_buffer.clone()) + .get_mut(&id) { - Some(existing_buffer) => existing_buffer, + Some(existing_state) => existing_state, None => self.add_language_server(&project, id, cx)?, }; - buffer.update(cx, |buffer, cx| { - let len = buffer.len(); - let has_newline = message.ends_with("\n"); - buffer.edit([(len..len, message)], None, cx); - if !has_newline { - let len = buffer.len(); - buffer.edit([(len..len, "\n")], None, cx); - } + + let log_lines = &mut language_server_state.log_messages; + while log_lines.len() >= MAX_STORED_LOG_ENTRIES { + log_lines.pop_front(); + } + let message = message.trim(); + log_lines.push_back(message.to_string()); + cx.emit(Event::NewServerLogEntry { + id, + entry: message.to_string(), + is_rpc: false, }); cx.notify(); Some(()) @@ -260,46 +261,32 @@ impl LogStore { Some(()) } - pub fn log_buffer_for_server( + fn server_logs( &self, project: &ModelHandle, server_id: LanguageServerId, - ) -> Option> { + ) -> Option<&VecDeque> { let weak_project = project.downgrade(); let project_state = self.projects.get(&weak_project)?; let server_state = project_state.servers.get(&server_id)?; - Some(server_state.log_buffer.clone()) + Some(&server_state.log_messages) } fn enable_rpc_trace_for_language_server( &mut self, project: &ModelHandle, server_id: LanguageServerId, - cx: &mut ModelContext, - ) -> Option> { + ) -> Option<&mut LanguageServerRpcState> { let weak_project = project.downgrade(); let project_state = self.projects.get_mut(&weak_project)?; let server_state = project_state.servers.get_mut(&server_id)?; - let rpc_state = server_state.rpc_state.get_or_insert_with(|| { - let language = project.read(cx).languages().language_for_name("JSON"); - let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "")); - cx.spawn_weak({ - let buffer = buffer.clone(); - |_, mut cx| async move { - let language = language.await.ok(); - buffer.update(&mut cx, |buffer, cx| { - buffer.set_language(language, cx); - }); - } - }) - .detach(); - - LanguageServerRpcState { - buffer, + let rpc_state = server_state + .rpc_state + .get_or_insert_with(|| LanguageServerRpcState { + rpc_messages: VecDeque::with_capacity(MAX_STORED_LOG_ENTRIES), last_message_kind: None, - } - }); - Some(rpc_state.buffer.clone()) + }); + Some(rpc_state) } pub fn disable_rpc_trace_for_language_server( @@ -328,7 +315,7 @@ impl LogStore { IoKind::StdIn => false, IoKind::StdErr => { let project = project.upgrade(cx)?; - let message = format!("stderr: {}\n", message.trim()); + let message = format!("stderr: {}", message.trim()); self.add_language_server_log(&project, language_server_id, &message, cx); return Some(()); } @@ -341,24 +328,37 @@ impl LogStore { .get_mut(&language_server_id)? .rpc_state .as_mut()?; - state.buffer.update(cx, |buffer, cx| { - let kind = if is_received { - MessageKind::Receive - } else { - MessageKind::Send + let kind = if is_received { + MessageKind::Receive + } else { + MessageKind::Send + }; + + let rpc_log_lines = &mut state.rpc_messages; + if state.last_message_kind != Some(kind) { + let line_before_message = match kind { + MessageKind::Send => SEND_LINE, + MessageKind::Receive => RECEIVE_LINE, }; - if state.last_message_kind != Some(kind) { - let len = buffer.len(); - let line = match kind { - MessageKind::Send => SEND_LINE, - MessageKind::Receive => RECEIVE_LINE, - }; - buffer.edit([(len..len, line)], None, cx); - state.last_message_kind = Some(kind); - } - let len = buffer.len(); - buffer.edit([(len..len, message)], None, cx); + rpc_log_lines.push_back(line_before_message.to_string()); + cx.emit(Event::NewServerLogEntry { + id: language_server_id, + entry: line_before_message.to_string(), + is_rpc: true, + }); + } + + while rpc_log_lines.len() >= MAX_STORED_LOG_ENTRIES { + rpc_log_lines.pop_front(); + } + let message = message.trim(); + rpc_log_lines.push_back(message.to_string()); + cx.emit(Event::NewServerLogEntry { + id: language_server_id, + entry: message.to_string(), + is_rpc: true, }); + cx.notify(); Some(()) } } @@ -374,8 +374,7 @@ impl LspLogView { .projects .get(&project.downgrade()) .and_then(|project| project.servers.keys().copied().next()); - let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "")); - let _log_store_subscription = cx.observe(&log_store, |this, store, cx| { + let model_changes_subscription = cx.observe(&log_store, |this, store, cx| { (|| -> Option<()> { let project_state = store.read(cx).projects.get(&this.project.downgrade())?; if let Some(current_lsp) = this.current_server_id { @@ -411,13 +410,31 @@ impl LspLogView { cx.notify(); }); + let events_subscriptions = cx.subscribe(&log_store, |log_view, _, e, cx| match e { + Event::NewServerLogEntry { id, entry, is_rpc } => { + if log_view.current_server_id == Some(*id) { + if (*is_rpc && log_view.is_showing_rpc_trace) + || (!*is_rpc && !log_view.is_showing_rpc_trace) + { + log_view.editor.update(cx, |editor, cx| { + editor.set_read_only(false); + editor.handle_input(entry.trim(), cx); + editor.handle_input("\n", cx); + editor.set_read_only(true); + }); + } + } + } + }); + let (editor, editor_subscription) = Self::editor_for_logs(String::new(), cx); let mut this = Self { - editor: Self::editor_for_buffer(project.clone(), buffer, cx), + editor, + editor_subscription, project, log_store, current_server_id: None, is_showing_rpc_trace: false, - _log_store_subscription, + _log_store_subscriptions: vec![model_changes_subscription, events_subscriptions], }; if let Some(server_id) = server_id { this.show_logs_for_server(server_id, cx); @@ -425,20 +442,19 @@ impl LspLogView { this } - fn editor_for_buffer( - project: ModelHandle, - buffer: ModelHandle, + fn editor_for_logs( + log_contents: String, cx: &mut ViewContext, - ) -> ViewHandle { + ) -> (ViewHandle, Subscription) { let editor = cx.add_view(|cx| { - let mut editor = Editor::for_buffer(buffer, Some(project), cx); + let mut editor = Editor::multi_line(None, cx); + editor.set_text(log_contents, cx); + editor.move_to_end(&MoveToEnd, cx); editor.set_read_only(true); - editor.move_to_end(&Default::default(), cx); editor }); - cx.subscribe(&editor, |_, _, event, cx| cx.emit(event.clone())) - .detach(); - editor + let editor_subscription = cx.subscribe(&editor, |_, _, event, cx| cx.emit(event.clone())); + (editor, editor_subscription) } pub(crate) fn menu_items<'a>(&'a self, cx: &'a AppContext) -> Option> { @@ -487,14 +503,17 @@ impl LspLogView { } fn show_logs_for_server(&mut self, server_id: LanguageServerId, cx: &mut ViewContext) { - let buffer = self + let log_contents = self .log_store .read(cx) - .log_buffer_for_server(&self.project, server_id); - if let Some(buffer) = buffer { + .server_logs(&self.project, server_id) + .map(log_contents); + if let Some(log_contents) = log_contents { self.current_server_id = Some(server_id); self.is_showing_rpc_trace = false; - self.editor = Self::editor_for_buffer(self.project.clone(), buffer, cx); + let (editor, editor_subscription) = Self::editor_for_logs(log_contents, cx); + self.editor = editor; + self.editor_subscription = editor_subscription; cx.notify(); } } @@ -504,13 +523,37 @@ impl LspLogView { server_id: LanguageServerId, cx: &mut ViewContext, ) { - let buffer = self.log_store.update(cx, |log_set, cx| { - log_set.enable_rpc_trace_for_language_server(&self.project, server_id, cx) + let rpc_log = self.log_store.update(cx, |log_store, _| { + log_store + .enable_rpc_trace_for_language_server(&self.project, server_id) + .map(|state| log_contents(&state.rpc_messages)) }); - if let Some(buffer) = buffer { + if let Some(rpc_log) = rpc_log { self.current_server_id = Some(server_id); self.is_showing_rpc_trace = true; - self.editor = Self::editor_for_buffer(self.project.clone(), buffer, cx); + let (editor, editor_subscription) = Self::editor_for_logs(rpc_log, cx); + let language = self.project.read(cx).languages().language_for_name("JSON"); + editor + .read(cx) + .buffer() + .read(cx) + .as_singleton() + .expect("log buffer should be a singleton") + .update(cx, |_, cx| { + cx.spawn_weak({ + let buffer = cx.handle(); + |_, mut cx| async move { + let language = language.await.ok(); + buffer.update(&mut cx, |buffer, cx| { + buffer.set_language(language, cx); + }); + } + }) + .detach(); + }); + + self.editor = editor; + self.editor_subscription = editor_subscription; cx.notify(); } } @@ -523,7 +566,7 @@ impl LspLogView { ) { self.log_store.update(cx, |log_store, cx| { if enabled { - log_store.enable_rpc_trace_for_language_server(&self.project, server_id, cx); + log_store.enable_rpc_trace_for_language_server(&self.project, server_id); } else { log_store.disable_rpc_trace_for_language_server(&self.project, server_id, cx); } @@ -535,6 +578,16 @@ impl LspLogView { } } +fn log_contents(lines: &VecDeque) -> String { + let (a, b) = lines.as_slices(); + let log_contents = a.join("\n"); + if b.is_empty() { + log_contents + } else { + log_contents + "\n" + &b.join("\n") + } +} + impl View for LspLogView { fn ui_name() -> &'static str { "LspLogView" @@ -685,6 +738,7 @@ impl View for LspLogToolbarItemView { }); let server_selected = current_server.is_some(); + enum LspLogScroll {} enum Menu {} let lsp_menu = Stack::new() .with_child(Self::render_language_server_menu_header( @@ -697,7 +751,7 @@ impl View for LspLogToolbarItemView { Overlay::new( MouseEventHandler::new::(0, cx, move |_, cx| { Flex::column() - .scrollable::(0, None, cx) + .scrollable::(0, None, cx) .with_children(menu_rows.into_iter().map(|row| { Self::render_language_server_menu_item( row.server_id, @@ -876,6 +930,7 @@ impl LspLogToolbarItemView { ) -> impl Element { enum ActivateLog {} enum ActivateRpcTrace {} + enum LanguageServerCheckbox {} Flex::column() .with_child({ @@ -921,7 +976,7 @@ impl LspLogToolbarItemView { .with_height(theme.toolbar_dropdown_menu.row_height), ) .with_child( - ui::checkbox_with_label::( + ui::checkbox_with_label::( Empty::new(), &theme.welcome.checkbox, rpc_trace_enabled, @@ -947,8 +1002,16 @@ impl LspLogToolbarItemView { } } +pub enum Event { + NewServerLogEntry { + id: LanguageServerId, + entry: String, + is_rpc: bool, + }, +} + impl Entity for LogStore { - type Event = (); + type Event = Event; } impl Entity for LspLogView { diff --git a/crates/live_kit_client/src/test.rs b/crates/live_kit_client/src/test.rs index 704760bab7..8df8ab4abb 100644 --- a/crates/live_kit_client/src/test.rs +++ b/crates/live_kit_client/src/test.rs @@ -91,9 +91,8 @@ impl TestServer { let identity = claims.sub.unwrap().to_string(); let room_name = claims.video.room.unwrap(); let mut server_rooms = self.rooms.lock(); - let room = server_rooms - .get_mut(&*room_name) - .ok_or_else(|| anyhow!("room {:?} does not exist", room_name))?; + let room = (*server_rooms).entry(room_name.to_string()).or_default(); + if room.client_rooms.contains_key(&identity) { Err(anyhow!( "{:?} attempted to join room {:?} twice", diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index 33581721ae..b4099e2f6e 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -466,7 +466,10 @@ impl LanguageServer { completion_item: Some(CompletionItemCapability { snippet_support: Some(true), resolve_support: Some(CompletionItemCapabilityResolveSupport { - properties: vec!["additionalTextEdits".to_string()], + properties: vec![ + "documentation".to_string(), + "additionalTextEdits".to_string(), + ], }), ..Default::default() }), @@ -748,6 +751,15 @@ impl LanguageServer { ) } + // some child of string literal (be it "" or ``) which is the child of an attribute + + // + // + // + // + // const classes = "awesome "; + // + fn request_internal( next_id: &AtomicUsize, response_handlers: &Mutex>>, diff --git a/crates/node_runtime/src/node_runtime.rs b/crates/node_runtime/src/node_runtime.rs index 820a8b6f81..dcb8833f8c 100644 --- a/crates/node_runtime/src/node_runtime.rs +++ b/crates/node_runtime/src/node_runtime.rs @@ -220,29 +220,129 @@ impl NodeRuntime for RealNodeRuntime { } } -pub struct FakeNodeRuntime; +pub struct FakeNodeRuntime(Option); + +struct PrettierSupport { + plugins: Vec<&'static str>, +} impl FakeNodeRuntime { pub fn new() -> Arc { - Arc::new(FakeNodeRuntime) + Arc::new(FakeNodeRuntime(None)) + } + + pub fn with_prettier_support(plugins: &[&'static str]) -> Arc { + Arc::new(FakeNodeRuntime(Some(PrettierSupport::new(plugins)))) } } #[async_trait::async_trait] impl NodeRuntime for FakeNodeRuntime { - async fn binary_path(&self) -> Result { - unreachable!() + async fn binary_path(&self) -> anyhow::Result { + if let Some(prettier_support) = &self.0 { + prettier_support.binary_path().await + } else { + unreachable!() + } + } + + async fn run_npm_subcommand( + &self, + directory: Option<&Path>, + subcommand: &str, + args: &[&str], + ) -> anyhow::Result { + if let Some(prettier_support) = &self.0 { + prettier_support + .run_npm_subcommand(directory, subcommand, args) + .await + } else { + unreachable!() + } + } + + async fn npm_package_latest_version(&self, name: &str) -> anyhow::Result { + if let Some(prettier_support) = &self.0 { + prettier_support.npm_package_latest_version(name).await + } else { + unreachable!() + } + } + + async fn npm_install_packages( + &self, + directory: &Path, + packages: &[(&str, &str)], + ) -> anyhow::Result<()> { + if let Some(prettier_support) = &self.0 { + prettier_support + .npm_install_packages(directory, packages) + .await + } else { + unreachable!() + } + } +} + +impl PrettierSupport { + const PACKAGE_VERSION: &str = "0.0.1"; + + fn new(plugins: &[&'static str]) -> Self { + Self { + plugins: plugins.to_vec(), + } + } +} + +#[async_trait::async_trait] +impl NodeRuntime for PrettierSupport { + async fn binary_path(&self) -> anyhow::Result { + Ok(PathBuf::from("prettier_fake_node")) } async fn run_npm_subcommand(&self, _: Option<&Path>, _: &str, _: &[&str]) -> Result { unreachable!() } - async fn npm_package_latest_version(&self, _: &str) -> Result { - unreachable!() + async fn npm_package_latest_version(&self, name: &str) -> anyhow::Result { + if name == "prettier" || self.plugins.contains(&name) { + Ok(Self::PACKAGE_VERSION.to_string()) + } else { + panic!("Unexpected package name: {name}") + } } - async fn npm_install_packages(&self, _: &Path, _: &[(&str, &str)]) -> Result<()> { - unreachable!() + async fn npm_install_packages( + &self, + _: &Path, + packages: &[(&str, &str)], + ) -> anyhow::Result<()> { + assert_eq!( + packages.len(), + self.plugins.len() + 1, + "Unexpected packages length to install: {:?}, expected `prettier` + {:?}", + packages, + self.plugins + ); + for (name, version) in packages { + assert!( + name == &"prettier" || self.plugins.contains(name), + "Unexpected package `{}` to install in packages {:?}, expected {} for `prettier` + {:?}", + name, + packages, + Self::PACKAGE_VERSION, + self.plugins + ); + assert_eq!( + version, + &Self::PACKAGE_VERSION, + "Unexpected package version `{}` to install in packages {:?}, expected {} for `prettier` + {:?}", + version, + packages, + Self::PACKAGE_VERSION, + self.plugins + ); + } + Ok(()) } } diff --git a/crates/prettier/Cargo.toml b/crates/prettier/Cargo.toml new file mode 100644 index 0000000000..997fa87126 --- /dev/null +++ b/crates/prettier/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "prettier" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +path = "src/prettier.rs" +doctest = false + +[features] +test-support = [] + +[dependencies] +client = { path = "../client" } +collections = { path = "../collections"} +language = { path = "../language" } +gpui = { path = "../gpui" } +fs = { path = "../fs" } +lsp = { path = "../lsp" } +node_runtime = { path = "../node_runtime"} +util = { path = "../util" } + +log.workspace = true +serde.workspace = true +serde_derive.workspace = true +serde_json.workspace = true +anyhow.workspace = true +futures.workspace = true + +[dev-dependencies] +language = { path = "../language", features = ["test-support"] } +gpui = { path = "../gpui", features = ["test-support"] } +fs = { path = "../fs", features = ["test-support"] } diff --git a/crates/prettier/src/prettier.rs b/crates/prettier/src/prettier.rs new file mode 100644 index 0000000000..c3811b567b --- /dev/null +++ b/crates/prettier/src/prettier.rs @@ -0,0 +1,513 @@ +use std::collections::VecDeque; +use std::path::{Path, PathBuf}; +use std::sync::Arc; + +use anyhow::Context; +use collections::{HashMap, HashSet}; +use fs::Fs; +use gpui::{AsyncAppContext, ModelHandle}; +use language::language_settings::language_settings; +use language::{Buffer, BundledFormatter, Diff}; +use lsp::{LanguageServer, LanguageServerId}; +use node_runtime::NodeRuntime; +use serde::{Deserialize, Serialize}; +use util::paths::DEFAULT_PRETTIER_DIR; + +pub enum Prettier { + Real(RealPrettier), + #[cfg(any(test, feature = "test-support"))] + Test(TestPrettier), +} + +pub struct RealPrettier { + worktree_id: Option, + default: bool, + prettier_dir: PathBuf, + server: Arc, +} + +#[cfg(any(test, feature = "test-support"))] +pub struct TestPrettier { + worktree_id: Option, + prettier_dir: PathBuf, + default: bool, +} + +#[derive(Debug)] +pub struct LocateStart { + pub worktree_root_path: Arc, + pub starting_path: Arc, +} + +pub const PRETTIER_SERVER_FILE: &str = "prettier_server.js"; +pub const PRETTIER_SERVER_JS: &str = include_str!("./prettier_server.js"); +const PRETTIER_PACKAGE_NAME: &str = "prettier"; +const TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME: &str = "prettier-plugin-tailwindcss"; + +impl Prettier { + pub const CONFIG_FILE_NAMES: &'static [&'static str] = &[ + ".prettierrc", + ".prettierrc.json", + ".prettierrc.json5", + ".prettierrc.yaml", + ".prettierrc.yml", + ".prettierrc.toml", + ".prettierrc.js", + ".prettierrc.cjs", + "package.json", + "prettier.config.js", + "prettier.config.cjs", + ".editorconfig", + ]; + + #[cfg(any(test, feature = "test-support"))] + pub const FORMAT_SUFFIX: &str = "\nformatted by test prettier"; + + pub async fn locate( + starting_path: Option, + fs: Arc, + ) -> anyhow::Result { + let paths_to_check = match starting_path.as_ref() { + Some(starting_path) => { + let worktree_root = starting_path + .worktree_root_path + .components() + .into_iter() + .take_while(|path_component| { + path_component.as_os_str().to_string_lossy() != "node_modules" + }) + .collect::(); + + if worktree_root != starting_path.worktree_root_path.as_ref() { + vec![worktree_root] + } else { + let (worktree_root_metadata, start_path_metadata) = if starting_path + .starting_path + .as_ref() + == Path::new("") + { + let worktree_root_data = + fs.metadata(&worktree_root).await.with_context(|| { + format!( + "FS metadata fetch for worktree root path {worktree_root:?}", + ) + })?; + (worktree_root_data.unwrap_or_else(|| { + panic!("cannot query prettier for non existing worktree root at {worktree_root_data:?}") + }), None) + } else { + let full_starting_path = worktree_root.join(&starting_path.starting_path); + let (worktree_root_data, start_path_data) = futures::try_join!( + fs.metadata(&worktree_root), + fs.metadata(&full_starting_path), + ) + .with_context(|| { + format!("FS metadata fetch for starting path {full_starting_path:?}",) + })?; + ( + worktree_root_data.unwrap_or_else(|| { + panic!("cannot query prettier for non existing worktree root at {worktree_root_data:?}") + }), + start_path_data, + ) + }; + + match start_path_metadata { + Some(start_path_metadata) => { + anyhow::ensure!(worktree_root_metadata.is_dir, + "For non-empty start path, worktree root {starting_path:?} should be a directory"); + anyhow::ensure!( + !start_path_metadata.is_dir, + "For non-empty start path, it should not be a directory {starting_path:?}" + ); + anyhow::ensure!( + !start_path_metadata.is_symlink, + "For non-empty start path, it should not be a symlink {starting_path:?}" + ); + + let file_to_format = starting_path.starting_path.as_ref(); + let mut paths_to_check = VecDeque::from(vec![worktree_root.clone()]); + let mut current_path = worktree_root; + for path_component in file_to_format.components().into_iter() { + current_path = current_path.join(path_component); + paths_to_check.push_front(current_path.clone()); + if path_component.as_os_str().to_string_lossy() == "node_modules" { + break; + } + } + paths_to_check.pop_front(); // last one is the file itself or node_modules, skip it + Vec::from(paths_to_check) + } + None => { + anyhow::ensure!( + !worktree_root_metadata.is_dir, + "For empty start path, worktree root should not be a directory {starting_path:?}" + ); + anyhow::ensure!( + !worktree_root_metadata.is_symlink, + "For empty start path, worktree root should not be a symlink {starting_path:?}" + ); + worktree_root + .parent() + .map(|path| vec![path.to_path_buf()]) + .unwrap_or_default() + } + } + } + } + None => Vec::new(), + }; + + match find_closest_prettier_dir(paths_to_check, fs.as_ref()) + .await + .with_context(|| format!("finding prettier starting with {starting_path:?}"))? + { + Some(prettier_dir) => Ok(prettier_dir), + None => Ok(DEFAULT_PRETTIER_DIR.to_path_buf()), + } + } + + #[cfg(any(test, feature = "test-support"))] + pub async fn start( + worktree_id: Option, + _: LanguageServerId, + prettier_dir: PathBuf, + _: Arc, + _: AsyncAppContext, + ) -> anyhow::Result { + Ok( + #[cfg(any(test, feature = "test-support"))] + Self::Test(TestPrettier { + worktree_id, + default: prettier_dir == DEFAULT_PRETTIER_DIR.as_path(), + prettier_dir, + }), + ) + } + + #[cfg(not(any(test, feature = "test-support")))] + pub async fn start( + worktree_id: Option, + server_id: LanguageServerId, + prettier_dir: PathBuf, + node: Arc, + cx: AsyncAppContext, + ) -> anyhow::Result { + use lsp::LanguageServerBinary; + + let backgroud = cx.background(); + anyhow::ensure!( + prettier_dir.is_dir(), + "Prettier dir {prettier_dir:?} is not a directory" + ); + let prettier_server = DEFAULT_PRETTIER_DIR.join(PRETTIER_SERVER_FILE); + anyhow::ensure!( + prettier_server.is_file(), + "no prettier server package found at {prettier_server:?}" + ); + + let node_path = backgroud + .spawn(async move { node.binary_path().await }) + .await?; + let server = LanguageServer::new( + server_id, + LanguageServerBinary { + path: node_path, + arguments: vec![prettier_server.into(), prettier_dir.as_path().into()], + }, + Path::new("/"), + None, + cx, + ) + .context("prettier server creation")?; + let server = backgroud + .spawn(server.initialize(None)) + .await + .context("prettier server initialization")?; + Ok(Self::Real(RealPrettier { + worktree_id, + server, + default: prettier_dir == DEFAULT_PRETTIER_DIR.as_path(), + prettier_dir, + })) + } + + pub async fn format( + &self, + buffer: &ModelHandle, + buffer_path: Option, + cx: &AsyncAppContext, + ) -> anyhow::Result { + match self { + Self::Real(local) => { + let params = buffer.read_with(cx, |buffer, cx| { + let buffer_language = buffer.language(); + let parsers_with_plugins = buffer_language + .into_iter() + .flat_map(|language| { + language + .lsp_adapters() + .iter() + .flat_map(|adapter| adapter.enabled_formatters()) + .filter_map(|formatter| match formatter { + BundledFormatter::Prettier { + parser_name, + plugin_names, + } => Some((parser_name, plugin_names)), + }) + }) + .fold( + HashMap::default(), + |mut parsers_with_plugins, (parser_name, plugins)| { + match parser_name { + Some(parser_name) => parsers_with_plugins + .entry(parser_name) + .or_insert_with(HashSet::default) + .extend(plugins), + None => parsers_with_plugins.values_mut().for_each(|existing_plugins| { + existing_plugins.extend(plugins.iter()); + }), + } + parsers_with_plugins + }, + ); + + let selected_parser_with_plugins = parsers_with_plugins.iter().max_by_key(|(_, plugins)| plugins.len()); + if parsers_with_plugins.len() > 1 { + log::warn!("Found multiple parsers with plugins {parsers_with_plugins:?}, will select only one: {selected_parser_with_plugins:?}"); + } + + let prettier_node_modules = self.prettier_dir().join("node_modules"); + anyhow::ensure!(prettier_node_modules.is_dir(), "Prettier node_modules dir does not exist: {prettier_node_modules:?}"); + let plugin_name_into_path = |plugin_name: &str| { + let prettier_plugin_dir = prettier_node_modules.join(plugin_name); + for possible_plugin_path in [ + prettier_plugin_dir.join("dist").join("index.mjs"), + prettier_plugin_dir.join("dist").join("index.js"), + prettier_plugin_dir.join("dist").join("plugin.js"), + prettier_plugin_dir.join("index.mjs"), + prettier_plugin_dir.join("index.js"), + prettier_plugin_dir.join("plugin.js"), + prettier_plugin_dir, + ] { + if possible_plugin_path.is_file() { + return Some(possible_plugin_path); + } + } + None + }; + let (parser, located_plugins) = match selected_parser_with_plugins { + Some((parser, plugins)) => { + // Tailwind plugin requires being added last + // https://github.com/tailwindlabs/prettier-plugin-tailwindcss#compatibility-with-other-prettier-plugins + let mut add_tailwind_back = false; + + let mut plugins = plugins.into_iter().filter(|&&plugin_name| { + if plugin_name == TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME { + add_tailwind_back = true; + false + } else { + true + } + }).map(|plugin_name| (plugin_name, plugin_name_into_path(plugin_name))).collect::>(); + if add_tailwind_back { + plugins.push((&TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME, plugin_name_into_path(TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME))); + } + (Some(parser.to_string()), plugins) + }, + None => (None, Vec::new()), + }; + + let prettier_options = if self.is_default() { + let language_settings = language_settings(buffer_language, buffer.file(), cx); + let mut options = language_settings.prettier.clone(); + if !options.contains_key("tabWidth") { + options.insert( + "tabWidth".to_string(), + serde_json::Value::Number(serde_json::Number::from( + language_settings.tab_size.get(), + )), + ); + } + if !options.contains_key("printWidth") { + options.insert( + "printWidth".to_string(), + serde_json::Value::Number(serde_json::Number::from( + language_settings.preferred_line_length, + )), + ); + } + Some(options) + } else { + None + }; + + let plugins = located_plugins.into_iter().filter_map(|(plugin_name, located_plugin_path)| { + match located_plugin_path { + Some(path) => Some(path), + None => { + log::error!("Have not found plugin path for {plugin_name:?} inside {prettier_node_modules:?}"); + None}, + } + }).collect(); + log::debug!("Formatting file {:?} with prettier, plugins :{plugins:?}, options: {prettier_options:?}", buffer.file().map(|f| f.full_path(cx))); + + anyhow::Ok(FormatParams { + text: buffer.text(), + options: FormatOptions { + parser, + plugins, + path: buffer_path, + prettier_options, + }, + }) + }).context("prettier params calculation")?; + let response = local + .server + .request::(params) + .await + .context("prettier format request")?; + let diff_task = buffer.read_with(cx, |buffer, cx| buffer.diff(response.text, cx)); + Ok(diff_task.await) + } + #[cfg(any(test, feature = "test-support"))] + Self::Test(_) => Ok(buffer + .read_with(cx, |buffer, cx| { + let formatted_text = buffer.text() + Self::FORMAT_SUFFIX; + buffer.diff(formatted_text, cx) + }) + .await), + } + } + + pub async fn clear_cache(&self) -> anyhow::Result<()> { + match self { + Self::Real(local) => local + .server + .request::(()) + .await + .context("prettier clear cache"), + #[cfg(any(test, feature = "test-support"))] + Self::Test(_) => Ok(()), + } + } + + pub fn server(&self) -> Option<&Arc> { + match self { + Self::Real(local) => Some(&local.server), + #[cfg(any(test, feature = "test-support"))] + Self::Test(_) => None, + } + } + + pub fn is_default(&self) -> bool { + match self { + Self::Real(local) => local.default, + #[cfg(any(test, feature = "test-support"))] + Self::Test(test_prettier) => test_prettier.default, + } + } + + pub fn prettier_dir(&self) -> &Path { + match self { + Self::Real(local) => &local.prettier_dir, + #[cfg(any(test, feature = "test-support"))] + Self::Test(test_prettier) => &test_prettier.prettier_dir, + } + } + + pub fn worktree_id(&self) -> Option { + match self { + Self::Real(local) => local.worktree_id, + #[cfg(any(test, feature = "test-support"))] + Self::Test(test_prettier) => test_prettier.worktree_id, + } + } +} + +async fn find_closest_prettier_dir( + paths_to_check: Vec, + fs: &dyn Fs, +) -> anyhow::Result> { + for path in paths_to_check { + let possible_package_json = path.join("package.json"); + if let Some(package_json_metadata) = fs + .metadata(&possible_package_json) + .await + .with_context(|| format!("Fetching metadata for {possible_package_json:?}"))? + { + if !package_json_metadata.is_dir && !package_json_metadata.is_symlink { + let package_json_contents = fs + .load(&possible_package_json) + .await + .with_context(|| format!("reading {possible_package_json:?} file contents"))?; + if let Ok(json_contents) = serde_json::from_str::>( + &package_json_contents, + ) { + if let Some(serde_json::Value::Object(o)) = json_contents.get("dependencies") { + if o.contains_key(PRETTIER_PACKAGE_NAME) { + return Ok(Some(path)); + } + } + if let Some(serde_json::Value::Object(o)) = json_contents.get("devDependencies") + { + if o.contains_key(PRETTIER_PACKAGE_NAME) { + return Ok(Some(path)); + } + } + } + } + } + + let possible_node_modules_location = path.join("node_modules").join(PRETTIER_PACKAGE_NAME); + if let Some(node_modules_location_metadata) = fs + .metadata(&possible_node_modules_location) + .await + .with_context(|| format!("fetching metadata for {possible_node_modules_location:?}"))? + { + if node_modules_location_metadata.is_dir { + return Ok(Some(path)); + } + } + } + Ok(None) +} + +enum Format {} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +struct FormatParams { + text: String, + options: FormatOptions, +} + +#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +struct FormatOptions { + plugins: Vec, + parser: Option, + #[serde(rename = "filepath")] + path: Option, + prettier_options: Option>, +} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +struct FormatResult { + text: String, +} + +impl lsp::request::Request for Format { + type Params = FormatParams; + type Result = FormatResult; + const METHOD: &'static str = "prettier/format"; +} + +enum ClearCache {} + +impl lsp::request::Request for ClearCache { + type Params = (); + type Result = (); + const METHOD: &'static str = "prettier/clear_cache"; +} diff --git a/crates/prettier/src/prettier_server.js b/crates/prettier/src/prettier_server.js new file mode 100644 index 0000000000..a56c220f20 --- /dev/null +++ b/crates/prettier/src/prettier_server.js @@ -0,0 +1,217 @@ +const { Buffer } = require('buffer'); +const fs = require("fs"); +const path = require("path"); +const { once } = require('events'); + +const prettierContainerPath = process.argv[2]; +if (prettierContainerPath == null || prettierContainerPath.length == 0) { + process.stderr.write(`Prettier path argument was not specified or empty.\nUsage: ${process.argv[0]} ${process.argv[1]} prettier/path\n`); + process.exit(1); +} +fs.stat(prettierContainerPath, (err, stats) => { + if (err) { + process.stderr.write(`Path '${prettierContainerPath}' does not exist\n`); + process.exit(1); + } + + if (!stats.isDirectory()) { + process.stderr.write(`Path '${prettierContainerPath}' exists but is not a directory\n`); + process.exit(1); + } +}); +const prettierPath = path.join(prettierContainerPath, 'node_modules/prettier'); + +class Prettier { + constructor(path, prettier, config) { + this.path = path; + this.prettier = prettier; + this.config = config; + } +} + +(async () => { + let prettier; + let config; + try { + prettier = await loadPrettier(prettierPath); + config = await prettier.resolveConfig(prettierPath) || {}; + } catch (e) { + process.stderr.write(`Failed to load prettier: ${e}\n`); + process.exit(1); + } + process.stderr.write(`Prettier at path '${prettierPath}' loaded successfully, config: ${JSON.stringify(config)}\n`); + process.stdin.resume(); + handleBuffer(new Prettier(prettierPath, prettier, config)); +})() + +async function handleBuffer(prettier) { + for await (const messageText of readStdin()) { + let message; + try { + message = JSON.parse(messageText); + } catch (e) { + sendResponse(makeError(`Failed to parse message '${messageText}': ${e}`)); + continue; + } + // allow concurrent request handling by not `await`ing the message handling promise (async function) + handleMessage(message, prettier).catch(e => { + sendResponse({ id: message.id, ...makeError(`error during message handling: ${e}`) }); + }); + } +} + +const headerSeparator = "\r\n"; +const contentLengthHeaderName = 'Content-Length'; + +async function* readStdin() { + let buffer = Buffer.alloc(0); + let streamEnded = false; + process.stdin.on('end', () => { + streamEnded = true; + }); + process.stdin.on('data', (data) => { + buffer = Buffer.concat([buffer, data]); + }); + + async function handleStreamEnded(errorMessage) { + sendResponse(makeError(errorMessage)); + buffer = Buffer.alloc(0); + messageLength = null; + await once(process.stdin, 'readable'); + streamEnded = false; + } + + try { + let headersLength = null; + let messageLength = null; + main_loop: while (true) { + if (messageLength === null) { + while (buffer.indexOf(`${headerSeparator}${headerSeparator}`) === -1) { + if (streamEnded) { + await handleStreamEnded('Unexpected end of stream: headers not found'); + continue main_loop; + } else if (buffer.length > contentLengthHeaderName.length * 10) { + await handleStreamEnded(`Unexpected stream of bytes: no headers end found after ${buffer.length} bytes of input`); + continue main_loop; + } + await once(process.stdin, 'readable'); + } + const headers = buffer.subarray(0, buffer.indexOf(`${headerSeparator}${headerSeparator}`)).toString('ascii'); + const contentLengthHeader = headers.split(headerSeparator) + .map(header => header.split(':')) + .filter(header => header[2] === undefined) + .filter(header => (header[1] || '').length > 0) + .find(header => (header[0] || '').trim() === contentLengthHeaderName); + const contentLength = (contentLengthHeader || [])[1]; + if (contentLength === undefined) { + await handleStreamEnded(`Missing or incorrect ${contentLengthHeaderName} header: ${headers}`); + continue main_loop; + } + headersLength = headers.length + headerSeparator.length * 2; + messageLength = parseInt(contentLength, 10); + } + + while (buffer.length < (headersLength + messageLength)) { + if (streamEnded) { + await handleStreamEnded( + `Unexpected end of stream: buffer length ${buffer.length} does not match expected header length ${headersLength} + body length ${messageLength}`); + continue main_loop; + } + await once(process.stdin, 'readable'); + } + + const messageEnd = headersLength + messageLength; + const message = buffer.subarray(headersLength, messageEnd); + buffer = buffer.subarray(messageEnd); + headersLength = null; + messageLength = null; + yield message.toString('utf8'); + } + } catch (e) { + sendResponse(makeError(`Error reading stdin: ${e}`)); + } finally { + process.stdin.off('data', () => { }); + } +} + +async function handleMessage(message, prettier) { + const { method, id, params } = message; + if (method === undefined) { + throw new Error(`Message method is undefined: ${JSON.stringify(message)}`); + } + if (id === undefined) { + throw new Error(`Message id is undefined: ${JSON.stringify(message)}`); + } + + if (method === 'prettier/format') { + if (params === undefined || params.text === undefined) { + throw new Error(`Message params.text is undefined: ${JSON.stringify(message)}`); + } + if (params.options === undefined) { + throw new Error(`Message params.options is undefined: ${JSON.stringify(message)}`); + } + + let resolvedConfig = {}; + if (params.options.filepath !== undefined) { + resolvedConfig = await prettier.prettier.resolveConfig(params.options.filepath) || {}; + } + + const options = { + ...(params.options.prettierOptions || prettier.config), + ...resolvedConfig, + parser: params.options.parser, + plugins: params.options.plugins, + path: params.options.filepath + }; + process.stderr.write(`Resolved config: ${JSON.stringify(resolvedConfig)}, will format file '${params.options.filepath || ''}' with options: ${JSON.stringify(options)}\n`); + const formattedText = await prettier.prettier.format(params.text, options); + sendResponse({ id, result: { text: formattedText } }); + } else if (method === 'prettier/clear_cache') { + prettier.prettier.clearConfigCache(); + prettier.config = await prettier.prettier.resolveConfig(prettier.path) || {}; + sendResponse({ id, result: null }); + } else if (method === 'initialize') { + sendResponse({ + id, + result: { + "capabilities": {} + } + }); + } else { + throw new Error(`Unknown method: ${method}`); + } +} + +function makeError(message) { + return { + error: { + "code": -32600, // invalid request code + message, + } + }; +} + +function sendResponse(response) { + const responsePayloadString = JSON.stringify({ + jsonrpc: "2.0", + ...response + }); + const headers = `${contentLengthHeaderName}: ${Buffer.byteLength(responsePayloadString)}${headerSeparator}${headerSeparator}`; + process.stdout.write(headers + responsePayloadString); +} + +function loadPrettier(prettierPath) { + return new Promise((resolve, reject) => { + fs.access(prettierPath, fs.constants.F_OK, (err) => { + if (err) { + reject(`Path '${prettierPath}' does not exist.Error: ${err}`); + } else { + try { + resolve(require(prettierPath)); + } catch (err) { + reject(`Error requiring prettier module from path '${prettierPath}'.Error: ${err}`); + } + } + }); + }); +} diff --git a/crates/project/Cargo.toml b/crates/project/Cargo.toml index ffea6646e9..cfa623d534 100644 --- a/crates/project/Cargo.toml +++ b/crates/project/Cargo.toml @@ -15,6 +15,7 @@ test-support = [ "language/test-support", "settings/test-support", "text/test-support", + "prettier/test-support", ] [dependencies] @@ -31,6 +32,8 @@ git = { path = "../git" } gpui = { path = "../gpui" } language = { path = "../language" } lsp = { path = "../lsp" } +node_runtime = { path = "../node_runtime" } +prettier = { path = "../prettier" } rpc = { path = "../rpc" } settings = { path = "../settings" } sum_tree = { path = "../sum_tree" } @@ -73,6 +76,7 @@ gpui = { path = "../gpui", features = ["test-support"] } language = { path = "../language", features = ["test-support"] } lsp = { path = "../lsp", features = ["test-support"] } settings = { path = "../settings", features = ["test-support"] } +prettier = { path = "../prettier", features = ["test-support"] } util = { path = "../util", features = ["test-support"] } rpc = { path = "../rpc", features = ["test-support"] } git2.workspace = true diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 8beaea5031..72d79ca979 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -10,7 +10,7 @@ use futures::future; use gpui::{AppContext, AsyncAppContext, ModelHandle}; use language::{ language_settings::{language_settings, InlayHintKind}, - point_from_lsp, point_to_lsp, + point_from_lsp, point_to_lsp, prepare_completion_documentation, proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version}, range_from_lsp, range_to_lsp, Anchor, Bias, Buffer, BufferSnapshot, CachedLspAdapter, CharKind, CodeAction, Completion, OffsetRangeExt, PointUtf16, ToOffset, ToPointUtf16, Transaction, @@ -1341,7 +1341,7 @@ impl LspCommand for GetCompletions { async fn response_from_lsp( self, completions: Option, - _: ModelHandle, + project: ModelHandle, buffer: ModelHandle, server_id: LanguageServerId, cx: AsyncAppContext, @@ -1358,10 +1358,11 @@ impl LspCommand for GetCompletions { } } } else { - Default::default() + Vec::new() }; - let completions = buffer.read_with(&cx, |buffer, _| { + let completions = buffer.read_with(&cx, |buffer, cx| { + let language_registry = project.read(cx).languages().clone(); let language = buffer.language().cloned(); let snapshot = buffer.snapshot(); let clipped_position = buffer.clip_point_utf16(Unclipped(self.position), Bias::Left); @@ -1370,6 +1371,14 @@ impl LspCommand for GetCompletions { completions .into_iter() .filter_map(move |mut lsp_completion| { + if let Some(response_list) = &response_list { + if let Some(item_defaults) = &response_list.item_defaults { + if let Some(data) = &item_defaults.data { + lsp_completion.data = Some(data.clone()); + } + } + } + let (old_range, mut new_text) = match lsp_completion.text_edit.as_ref() { // If the language server provides a range to overwrite, then // check that the range is valid. @@ -1445,14 +1454,30 @@ impl LspCommand for GetCompletions { } }; - let language = language.clone(); LineEnding::normalize(&mut new_text); + let language_registry = language_registry.clone(); + let language = language.clone(); + Some(async move { let mut label = None; - if let Some(language) = language { + if let Some(language) = language.as_ref() { language.process_completion(&mut lsp_completion).await; label = language.label_for_completion(&lsp_completion).await; } + + let documentation = if let Some(lsp_docs) = &lsp_completion.documentation { + Some( + prepare_completion_documentation( + lsp_docs, + &language_registry, + language.clone(), + ) + .await, + ) + } else { + None + }; + Completion { old_range, new_text, @@ -1462,6 +1487,7 @@ impl LspCommand for GetCompletions { lsp_completion.filter_text.as_deref(), ) }), + documentation, server_id, lsp_completion, } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index a50e02a631..e91c91f7c3 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -20,7 +20,7 @@ use futures::{ mpsc::{self, UnboundedReceiver}, oneshot, }, - future::{try_join_all, Shared}, + future::{self, try_join_all, Shared}, stream::FuturesUnordered, AsyncWriteExt, Future, FutureExt, StreamExt, TryFutureExt, }; @@ -31,17 +31,19 @@ use gpui::{ }; use itertools::Itertools; use language::{ - language_settings::{language_settings, FormatOnSave, Formatter, InlayHintKind}, + language_settings::{ + language_settings, FormatOnSave, Formatter, InlayHintKind, LanguageSettings, + }, point_to_lsp, proto::{ deserialize_anchor, deserialize_fingerprint, deserialize_line_ending, deserialize_version, serialize_anchor, serialize_version, split_operations, }, - range_from_lsp, range_to_lsp, Bias, Buffer, BufferSnapshot, CachedLspAdapter, CodeAction, - CodeLabel, Completion, Diagnostic, DiagnosticEntry, DiagnosticSet, Diff, Event as BufferEvent, - File as _, Language, LanguageRegistry, LanguageServerName, LocalFile, LspAdapterDelegate, - OffsetRangeExt, Operation, Patch, PendingLanguageServer, PointUtf16, TextBufferSnapshot, - ToOffset, ToPointUtf16, Transaction, Unclipped, + range_from_lsp, range_to_lsp, Bias, Buffer, BufferSnapshot, BundledFormatter, CachedLspAdapter, + CodeAction, CodeLabel, Completion, Diagnostic, DiagnosticEntry, DiagnosticSet, Diff, + Event as BufferEvent, File as _, Language, LanguageRegistry, LanguageServerName, LocalFile, + LspAdapterDelegate, OffsetRangeExt, Operation, Patch, PendingLanguageServer, PointUtf16, + TextBufferSnapshot, ToOffset, ToPointUtf16, Transaction, Unclipped, }; use log::error; use lsp::{ @@ -49,7 +51,9 @@ use lsp::{ DocumentHighlightKind, LanguageServer, LanguageServerBinary, LanguageServerId, OneOf, }; use lsp_command::*; +use node_runtime::NodeRuntime; use postage::watch; +use prettier::{LocateStart, Prettier, PRETTIER_SERVER_FILE, PRETTIER_SERVER_JS}; use project_settings::{LspSettings, ProjectSettings}; use rand::prelude::*; use search::SearchQuery; @@ -75,10 +79,13 @@ use std::{ time::{Duration, Instant}, }; use terminals::Terminals; -use text::Anchor; +use text::{Anchor, LineEnding, Rope}; use util::{ - debug_panic, defer, http::HttpClient, merge_json_value_into, - paths::LOCAL_SETTINGS_RELATIVE_PATH, post_inc, ResultExt, TryFutureExt as _, + debug_panic, defer, + http::HttpClient, + merge_json_value_into, + paths::{DEFAULT_PRETTIER_DIR, LOCAL_SETTINGS_RELATIVE_PATH}, + post_inc, ResultExt, TryFutureExt as _, }; pub use fs::*; @@ -152,6 +159,11 @@ pub struct Project { copilot_lsp_subscription: Option, copilot_log_subscription: Option, current_lsp_settings: HashMap, LspSettings>, + node: Option>, + prettier_instances: HashMap< + (Option, PathBuf), + Shared, Arc>>>, + >, } struct DelayedDebounced { @@ -580,6 +592,7 @@ impl Project { client.add_model_request_handler(Self::handle_apply_code_action); client.add_model_request_handler(Self::handle_on_type_formatting); client.add_model_request_handler(Self::handle_inlay_hints); + client.add_model_request_handler(Self::handle_resolve_completion_documentation); client.add_model_request_handler(Self::handle_resolve_inlay_hint); client.add_model_request_handler(Self::handle_refresh_inlay_hints); client.add_model_request_handler(Self::handle_reload_buffers); @@ -605,6 +618,7 @@ impl Project { pub fn local( client: Arc, + node: Arc, user_store: ModelHandle, languages: Arc, fs: Arc, @@ -660,6 +674,8 @@ impl Project { copilot_lsp_subscription, copilot_log_subscription: None, current_lsp_settings: settings::get::(cx).lsp.clone(), + node: Some(node), + prettier_instances: HashMap::default(), } }) } @@ -757,6 +773,8 @@ impl Project { copilot_lsp_subscription, copilot_log_subscription: None, current_lsp_settings: settings::get::(cx).lsp.clone(), + node: None, + prettier_instances: HashMap::default(), }; for worktree in worktrees { let _ = this.add_worktree(&worktree, cx); @@ -795,8 +813,16 @@ impl Project { let http_client = util::http::FakeHttpClient::with_404_response(); let client = cx.update(|cx| client::Client::new(http_client.clone(), cx)); let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx)); - let project = - cx.update(|cx| Project::local(client, user_store, Arc::new(languages), fs, cx)); + let project = cx.update(|cx| { + Project::local( + client, + node_runtime::FakeNodeRuntime::new(), + user_store, + Arc::new(languages), + fs, + cx, + ) + }); for path in root_paths { let (tree, _) = project .update(cx, |project, cx| { @@ -810,19 +836,37 @@ impl Project { project } + /// Enables a prettier mock that avoids interacting with node runtime, prettier LSP wrapper, or any real file changes. + /// Instead, if appends the suffix to every input, this suffix is returned by this method. + #[cfg(any(test, feature = "test-support"))] + pub fn enable_test_prettier(&mut self, plugins: &[&'static str]) -> &'static str { + self.node = Some(node_runtime::FakeNodeRuntime::with_prettier_support( + plugins, + )); + Prettier::FORMAT_SUFFIX + } + fn on_settings_changed(&mut self, cx: &mut ModelContext) { let mut language_servers_to_start = Vec::new(); + let mut language_formatters_to_check = Vec::new(); for buffer in self.opened_buffers.values() { if let Some(buffer) = buffer.upgrade(cx) { let buffer = buffer.read(cx); - if let Some((file, language)) = buffer.file().zip(buffer.language()) { - let settings = language_settings(Some(language), Some(file), cx); + let buffer_file = File::from_dyn(buffer.file()); + let buffer_language = buffer.language(); + let settings = language_settings(buffer_language, buffer.file(), cx); + if let Some(language) = buffer_language { if settings.enable_language_server { - if let Some(file) = File::from_dyn(Some(file)) { + if let Some(file) = buffer_file { language_servers_to_start - .push((file.worktree.clone(), language.clone())); + .push((file.worktree.clone(), Arc::clone(language))); } } + language_formatters_to_check.push(( + buffer_file.map(|f| f.worktree_id(cx)), + Arc::clone(language), + settings.clone(), + )); } } } @@ -875,6 +919,11 @@ impl Project { .detach(); } + for (worktree, language, settings) in language_formatters_to_check { + self.install_default_formatters(worktree, &language, &settings, cx) + .detach_and_log_err(cx); + } + // Start all the newly-enabled language servers. for (worktree, language) in language_servers_to_start { let worktree_path = worktree.read(cx).abs_path(); @@ -2623,7 +2672,26 @@ impl Project { } }); - if let Some(file) = File::from_dyn(buffer.read(cx).file()) { + let buffer_file = buffer.read(cx).file().cloned(); + let settings = language_settings(Some(&new_language), buffer_file.as_ref(), cx).clone(); + let buffer_file = File::from_dyn(buffer_file.as_ref()); + let worktree = buffer_file.as_ref().map(|f| f.worktree_id(cx)); + + let task_buffer = buffer.clone(); + let prettier_installation_task = + self.install_default_formatters(worktree, &new_language, &settings, cx); + cx.spawn(|project, mut cx| async move { + prettier_installation_task.await?; + let _ = project + .update(&mut cx, |project, cx| { + project.prettier_instance_for_buffer(&task_buffer, cx) + }) + .await; + anyhow::Ok(()) + }) + .detach_and_log_err(cx); + + if let Some(file) = buffer_file { let worktree = file.worktree.clone(); if let Some(tree) = worktree.read(cx).as_local() { self.start_language_servers(&worktree, tree.abs_path().clone(), new_language, cx); @@ -2684,15 +2752,6 @@ impl Project { let lsp = project_settings.lsp.get(&adapter.name.0); let override_options = lsp.map(|s| s.initialization_options.clone()).flatten(); - let mut initialization_options = adapter.initialization_options.clone(); - match (&mut initialization_options, override_options) { - (Some(initialization_options), Some(override_options)) => { - merge_json_value_into(override_options, initialization_options); - } - (None, override_options) => initialization_options = override_options, - _ => {} - } - let server_id = pending_server.server_id; let container_dir = pending_server.container_dir.clone(); let state = LanguageServerState::Starting({ @@ -2704,7 +2763,7 @@ impl Project { cx.spawn_weak(|this, mut cx| async move { let result = Self::setup_and_insert_language_server( this, - initialization_options, + override_options, pending_server, adapter.clone(), language.clone(), @@ -2807,7 +2866,7 @@ impl Project { async fn setup_and_insert_language_server( this: WeakModelHandle, - initialization_options: Option, + override_initialization_options: Option, pending_server: PendingLanguageServer, adapter: Arc, language: Arc, @@ -2817,7 +2876,7 @@ impl Project { ) -> Result>> { let setup = Self::setup_pending_language_server( this, - initialization_options, + override_initialization_options, pending_server, adapter.clone(), server_id, @@ -2849,7 +2908,7 @@ impl Project { async fn setup_pending_language_server( this: WeakModelHandle, - initialization_options: Option, + override_options: Option, pending_server: PendingLanguageServer, adapter: Arc, server_id: LanguageServerId, @@ -2867,8 +2926,8 @@ impl Project { move |mut params, mut cx| { let this = this; let adapter = adapter.clone(); - adapter.process_diagnostics(&mut params); if let Some(this) = this.upgrade(&cx) { + adapter.process_diagnostics(&mut params); this.update(&mut cx, |this, cx| { this.update_diagnostics( server_id, @@ -2995,6 +3054,14 @@ impl Project { } }) .detach(); + let mut initialization_options = adapter.adapter.initialization_options().await; + match (&mut initialization_options, override_options) { + (Some(initialization_options), Some(override_options)) => { + merge_json_value_into(override_options, initialization_options); + } + (None, override_options) => initialization_options = override_options, + _ => {} + } let language_server = language_server.initialize(initialization_options).await?; @@ -3949,7 +4016,7 @@ impl Project { push_to_history: bool, trigger: FormatTrigger, cx: &mut ModelContext, - ) -> Task> { + ) -> Task> { if self.is_local() { let mut buffers_with_paths_and_servers = buffers .into_iter() @@ -4027,6 +4094,7 @@ impl Project { enum FormatOperation { Lsp(Vec<(Range, String)>), External(Diff), + Prettier(Diff), } // Apply language-specific formatting using either a language server @@ -4062,8 +4130,8 @@ impl Project { | (_, FormatOnSave::External { command, arguments }) => { if let Some(buffer_abs_path) = buffer_abs_path { format_operation = Self::format_via_external_command( - &buffer, - &buffer_abs_path, + buffer, + buffer_abs_path, &command, &arguments, &mut cx, @@ -4076,6 +4144,69 @@ impl Project { .map(FormatOperation::External); } } + (Formatter::Auto, FormatOnSave::On | FormatOnSave::Off) => { + if let Some(prettier_task) = this + .update(&mut cx, |project, cx| { + project.prettier_instance_for_buffer(buffer, cx) + }).await { + match prettier_task.await + { + Ok(prettier) => { + let buffer_path = buffer.read_with(&cx, |buffer, cx| { + File::from_dyn(buffer.file()).map(|file| file.abs_path(cx)) + }); + format_operation = Some(FormatOperation::Prettier( + prettier + .format(buffer, buffer_path, &cx) + .await + .context("formatting via prettier")?, + )); + } + Err(e) => anyhow::bail!( + "Failed to create prettier instance for buffer during autoformatting: {e:#}" + ), + } + } else if let Some((language_server, buffer_abs_path)) = + language_server.as_ref().zip(buffer_abs_path.as_ref()) + { + format_operation = Some(FormatOperation::Lsp( + Self::format_via_lsp( + &this, + &buffer, + buffer_abs_path, + &language_server, + tab_size, + &mut cx, + ) + .await + .context("failed to format via language server")?, + )); + } + } + (Formatter::Prettier { .. }, FormatOnSave::On | FormatOnSave::Off) => { + if let Some(prettier_task) = this + .update(&mut cx, |project, cx| { + project.prettier_instance_for_buffer(buffer, cx) + }).await { + match prettier_task.await + { + Ok(prettier) => { + let buffer_path = buffer.read_with(&cx, |buffer, cx| { + File::from_dyn(buffer.file()).map(|file| file.abs_path(cx)) + }); + format_operation = Some(FormatOperation::Prettier( + prettier + .format(buffer, buffer_path, &cx) + .await + .context("formatting via prettier")?, + )); + } + Err(e) => anyhow::bail!( + "Failed to create prettier instance for buffer during formatting: {e:#}" + ), + } + } + } }; buffer.update(&mut cx, |b, cx| { @@ -4100,6 +4231,9 @@ impl Project { FormatOperation::External(diff) => { b.apply_diff(diff, cx); } + FormatOperation::Prettier(diff) => { + b.apply_diff(diff, cx); + } } if let Some(transaction_id) = whitespace_transaction_id { @@ -5873,6 +6007,7 @@ impl Project { this.update_local_worktree_buffers(&worktree, changes, cx); this.update_local_worktree_language_servers(&worktree, changes, cx); this.update_local_worktree_settings(&worktree, changes, cx); + this.update_prettier_settings(&worktree, changes, cx); cx.emit(Event::WorktreeUpdatedEntries( worktree.read(cx).id(), changes.clone(), @@ -6252,6 +6387,69 @@ impl Project { .detach(); } + fn update_prettier_settings( + &self, + worktree: &ModelHandle, + changes: &[(Arc, ProjectEntryId, PathChange)], + cx: &mut ModelContext<'_, Project>, + ) { + let prettier_config_files = Prettier::CONFIG_FILE_NAMES + .iter() + .map(Path::new) + .collect::>(); + + let prettier_config_file_changed = changes + .iter() + .filter(|(_, _, change)| !matches!(change, PathChange::Loaded)) + .filter(|(path, _, _)| { + !path + .components() + .any(|component| component.as_os_str().to_string_lossy() == "node_modules") + }) + .find(|(path, _, _)| prettier_config_files.contains(path.as_ref())); + let current_worktree_id = worktree.read(cx).id(); + if let Some((config_path, _, _)) = prettier_config_file_changed { + log::info!( + "Prettier config file {config_path:?} changed, reloading prettier instances for worktree {current_worktree_id}" + ); + let prettiers_to_reload = self + .prettier_instances + .iter() + .filter_map(|((worktree_id, prettier_path), prettier_task)| { + if worktree_id.is_none() || worktree_id == &Some(current_worktree_id) { + Some((*worktree_id, prettier_path.clone(), prettier_task.clone())) + } else { + None + } + }) + .collect::>(); + + cx.background() + .spawn(async move { + for task_result in future::join_all(prettiers_to_reload.into_iter().map(|(worktree_id, prettier_path, prettier_task)| { + async move { + prettier_task.await? + .clear_cache() + .await + .with_context(|| { + format!( + "clearing prettier {prettier_path:?} cache for worktree {worktree_id:?} on prettier settings update" + ) + }) + .map_err(Arc::new) + } + })) + .await + { + if let Err(e) = task_result { + log::error!("Failed to clear cache for prettier: {e:#}"); + } + } + }) + .detach(); + } + } + pub fn set_active_path(&mut self, entry: Option, cx: &mut ModelContext) { let new_active_entry = entry.and_then(|project_path| { let worktree = self.worktree_for_id(project_path.worktree_id, cx)?; @@ -7155,6 +7353,40 @@ impl Project { }) } + async fn handle_resolve_completion_documentation( + this: ModelHandle, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result { + let lsp_completion = serde_json::from_slice(&envelope.payload.lsp_completion)?; + + let completion = this + .read_with(&mut cx, |this, _| { + let id = LanguageServerId(envelope.payload.language_server_id as usize); + let Some(server) = this.language_server_for_id(id) else { + return Err(anyhow!("No language server {id}")); + }; + + Ok(server.request::(lsp_completion)) + })? + .await?; + + let mut is_markdown = false; + let text = match completion.documentation { + Some(lsp::Documentation::String(text)) => text, + + Some(lsp::Documentation::MarkupContent(lsp::MarkupContent { kind, value })) => { + is_markdown = kind == lsp::MarkupKind::Markdown; + value + } + + _ => String::new(), + }; + + Ok(proto::ResolveCompletionDocumentationResponse { text, is_markdown }) + } + async fn handle_apply_code_action( this: ModelHandle, envelope: TypedEnvelope, @@ -8109,6 +8341,236 @@ impl Project { Vec::new() } } + + fn prettier_instance_for_buffer( + &mut self, + buffer: &ModelHandle, + cx: &mut ModelContext, + ) -> Task, Arc>>>>> { + let buffer = buffer.read(cx); + let buffer_file = buffer.file(); + let Some(buffer_language) = buffer.language() else { + return Task::ready(None); + }; + if !buffer_language + .lsp_adapters() + .iter() + .flat_map(|adapter| adapter.enabled_formatters()) + .any(|formatter| matches!(formatter, BundledFormatter::Prettier { .. })) + { + return Task::ready(None); + } + + let buffer_file = File::from_dyn(buffer_file); + let buffer_path = buffer_file.map(|file| Arc::clone(file.path())); + let worktree_path = buffer_file + .as_ref() + .and_then(|file| Some(file.worktree.read(cx).abs_path())); + let worktree_id = buffer_file.map(|file| file.worktree_id(cx)); + if self.is_local() || worktree_id.is_none() || worktree_path.is_none() { + let Some(node) = self.node.as_ref().map(Arc::clone) else { + return Task::ready(None); + }; + cx.spawn(|this, mut cx| async move { + let fs = this.update(&mut cx, |project, _| Arc::clone(&project.fs)); + let prettier_dir = match cx + .background() + .spawn(Prettier::locate( + worktree_path.zip(buffer_path).map( + |(worktree_root_path, starting_path)| LocateStart { + worktree_root_path, + starting_path, + }, + ), + fs, + )) + .await + { + Ok(path) => path, + Err(e) => { + return Some( + Task::ready(Err(Arc::new(e.context( + "determining prettier path for worktree {worktree_path:?}", + )))) + .shared(), + ); + } + }; + + if let Some(existing_prettier) = this.update(&mut cx, |project, _| { + project + .prettier_instances + .get(&(worktree_id, prettier_dir.clone())) + .cloned() + }) { + return Some(existing_prettier); + } + + log::info!("Found prettier in {prettier_dir:?}, starting."); + let task_prettier_dir = prettier_dir.clone(); + let weak_project = this.downgrade(); + let new_server_id = + this.update(&mut cx, |this, _| this.languages.next_language_server_id()); + let new_prettier_task = cx + .spawn(|mut cx| async move { + let prettier = Prettier::start( + worktree_id.map(|id| id.to_usize()), + new_server_id, + task_prettier_dir, + node, + cx.clone(), + ) + .await + .context("prettier start") + .map_err(Arc::new)?; + log::info!("Started prettier in {:?}", prettier.prettier_dir()); + + if let Some((project, prettier_server)) = + weak_project.upgrade(&mut cx).zip(prettier.server()) + { + project.update(&mut cx, |project, cx| { + let name = if prettier.is_default() { + LanguageServerName(Arc::from("prettier (default)")) + } else { + let prettier_dir = prettier.prettier_dir(); + let worktree_path = prettier + .worktree_id() + .map(WorktreeId::from_usize) + .and_then(|id| project.worktree_for_id(id, cx)) + .map(|worktree| worktree.read(cx).abs_path()); + match worktree_path { + Some(worktree_path) => { + if worktree_path.as_ref() == prettier_dir { + LanguageServerName(Arc::from(format!( + "prettier ({})", + prettier_dir + .file_name() + .and_then(|name| name.to_str()) + .unwrap_or_default() + ))) + } else { + let dir_to_display = match prettier_dir + .strip_prefix(&worktree_path) + .ok() + { + Some(relative_path) => relative_path, + None => prettier_dir, + }; + LanguageServerName(Arc::from(format!( + "prettier ({})", + dir_to_display.display(), + ))) + } + } + None => LanguageServerName(Arc::from(format!( + "prettier ({})", + prettier_dir.display(), + ))), + } + }; + + project + .supplementary_language_servers + .insert(new_server_id, (name, Arc::clone(prettier_server))); + cx.emit(Event::LanguageServerAdded(new_server_id)); + }); + } + Ok(Arc::new(prettier)).map_err(Arc::new) + }) + .shared(); + this.update(&mut cx, |project, _| { + project + .prettier_instances + .insert((worktree_id, prettier_dir), new_prettier_task.clone()); + }); + Some(new_prettier_task) + }) + } else if self.remote_id().is_some() { + return Task::ready(None); + } else { + Task::ready(Some( + Task::ready(Err(Arc::new(anyhow!("project does not have a remote id")))).shared(), + )) + } + } + + fn install_default_formatters( + &self, + worktree: Option, + new_language: &Language, + language_settings: &LanguageSettings, + cx: &mut ModelContext, + ) -> Task> { + match &language_settings.formatter { + Formatter::Prettier { .. } | Formatter::Auto => {} + Formatter::LanguageServer | Formatter::External { .. } => return Task::ready(Ok(())), + }; + let Some(node) = self.node.as_ref().cloned() else { + return Task::ready(Ok(())); + }; + + let mut prettier_plugins = None; + for formatter in new_language + .lsp_adapters() + .into_iter() + .flat_map(|adapter| adapter.enabled_formatters()) + { + match formatter { + BundledFormatter::Prettier { plugin_names, .. } => prettier_plugins + .get_or_insert_with(|| HashSet::default()) + .extend(plugin_names), + } + } + let Some(prettier_plugins) = prettier_plugins else { + return Task::ready(Ok(())); + }; + + let default_prettier_dir = DEFAULT_PRETTIER_DIR.as_path(); + let already_running_prettier = self + .prettier_instances + .get(&(worktree, default_prettier_dir.to_path_buf())) + .cloned(); + + let fs = Arc::clone(&self.fs); + cx.background() + .spawn(async move { + let prettier_wrapper_path = default_prettier_dir.join(PRETTIER_SERVER_FILE); + // method creates parent directory if it doesn't exist + fs.save(&prettier_wrapper_path, &Rope::from(PRETTIER_SERVER_JS), LineEnding::Unix).await + .with_context(|| format!("writing {PRETTIER_SERVER_FILE} file at {prettier_wrapper_path:?}"))?; + + let packages_to_versions = future::try_join_all( + prettier_plugins + .iter() + .chain(Some(&"prettier")) + .map(|package_name| async { + let returned_package_name = package_name.to_string(); + let latest_version = node.npm_package_latest_version(package_name) + .await + .with_context(|| { + format!("fetching latest npm version for package {returned_package_name}") + })?; + anyhow::Ok((returned_package_name, latest_version)) + }), + ) + .await + .context("fetching latest npm versions")?; + + log::info!("Fetching default prettier and plugins: {packages_to_versions:?}"); + let borrowed_packages = packages_to_versions.iter().map(|(package, version)| { + (package.as_str(), version.as_str()) + }).collect::>(); + node.npm_install_packages(default_prettier_dir, &borrowed_packages).await.context("fetching formatter packages")?; + + if !prettier_plugins.is_empty() { + if let Some(prettier) = already_running_prettier { + prettier.await.map_err(|e| anyhow::anyhow!("Default prettier startup await failure: {e:#}"))?.clear_cache().await.context("clearing default prettier cache after plugins install")?; + } + } + + anyhow::Ok(()) + }) + } } fn subscribe_for_copilot_events( diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index a38e43cd87..f6fae0c98b 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -2027,11 +2027,16 @@ impl LocalSnapshot { fn ignore_stack_for_abs_path(&self, abs_path: &Path, is_dir: bool) -> Arc { let mut new_ignores = Vec::new(); - for ancestor in abs_path.ancestors().skip(1) { - if let Some((ignore, _)) = self.ignores_by_parent_abs_path.get(ancestor) { - new_ignores.push((ancestor, Some(ignore.clone()))); - } else { - new_ignores.push((ancestor, None)); + for (index, ancestor) in abs_path.ancestors().enumerate() { + if index > 0 { + if let Some((ignore, _)) = self.ignores_by_parent_abs_path.get(ancestor) { + new_ignores.push((ancestor, Some(ignore.clone()))); + } else { + new_ignores.push((ancestor, None)); + } + } + if ancestor.join(&*DOT_GIT).is_dir() { + break; } } @@ -2048,7 +2053,6 @@ impl LocalSnapshot { if ignore_stack.is_abs_path_ignored(abs_path, is_dir) { ignore_stack = IgnoreStack::all(); } - ignore_stack } @@ -3064,14 +3068,21 @@ impl BackgroundScanner { // Populate ignores above the root. let root_abs_path = self.state.lock().snapshot.abs_path.clone(); - for ancestor in root_abs_path.ancestors().skip(1) { - if let Ok(ignore) = build_gitignore(&ancestor.join(&*GITIGNORE), self.fs.as_ref()).await - { - self.state - .lock() - .snapshot - .ignores_by_parent_abs_path - .insert(ancestor.into(), (ignore.into(), false)); + for (index, ancestor) in root_abs_path.ancestors().enumerate() { + if index != 0 { + if let Ok(ignore) = + build_gitignore(&ancestor.join(&*GITIGNORE), self.fs.as_ref()).await + { + self.state + .lock() + .snapshot + .ignores_by_parent_abs_path + .insert(ancestor.into(), (ignore.into(), false)); + } + } + if ancestor.join(&*DOT_GIT).is_dir() { + // Reached root of git repository. + break; } } diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 3501e70e6a..0ca10b381a 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -89,88 +89,91 @@ message Envelope { FormatBuffersResponse format_buffers_response = 70; GetCompletions get_completions = 71; GetCompletionsResponse get_completions_response = 72; - ApplyCompletionAdditionalEdits apply_completion_additional_edits = 73; - ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 74; - GetCodeActions get_code_actions = 75; - GetCodeActionsResponse get_code_actions_response = 76; - GetHover get_hover = 77; - GetHoverResponse get_hover_response = 78; - ApplyCodeAction apply_code_action = 79; - ApplyCodeActionResponse apply_code_action_response = 80; - PrepareRename prepare_rename = 81; - PrepareRenameResponse prepare_rename_response = 82; - PerformRename perform_rename = 83; - PerformRenameResponse perform_rename_response = 84; - SearchProject search_project = 85; - SearchProjectResponse search_project_response = 86; + ResolveCompletionDocumentation resolve_completion_documentation = 73; + ResolveCompletionDocumentationResponse resolve_completion_documentation_response = 74; + ApplyCompletionAdditionalEdits apply_completion_additional_edits = 75; + ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 76; + GetCodeActions get_code_actions = 77; + GetCodeActionsResponse get_code_actions_response = 78; + GetHover get_hover = 79; + GetHoverResponse get_hover_response = 80; + ApplyCodeAction apply_code_action = 81; + ApplyCodeActionResponse apply_code_action_response = 82; + PrepareRename prepare_rename = 83; + PrepareRenameResponse prepare_rename_response = 84; + PerformRename perform_rename = 85; + PerformRenameResponse perform_rename_response = 86; + SearchProject search_project = 87; + SearchProjectResponse search_project_response = 88; - UpdateContacts update_contacts = 87; - UpdateInviteInfo update_invite_info = 88; - ShowContacts show_contacts = 89; + UpdateContacts update_contacts = 89; + UpdateInviteInfo update_invite_info = 90; + ShowContacts show_contacts = 91; - GetUsers get_users = 90; - FuzzySearchUsers fuzzy_search_users = 91; - UsersResponse users_response = 92; - RequestContact request_contact = 93; - RespondToContactRequest respond_to_contact_request = 94; - RemoveContact remove_contact = 95; + GetUsers get_users = 92; + FuzzySearchUsers fuzzy_search_users = 93; + UsersResponse users_response = 94; + RequestContact request_contact = 95; + RespondToContactRequest respond_to_contact_request = 96; + RemoveContact remove_contact = 97; - Follow follow = 96; - FollowResponse follow_response = 97; - UpdateFollowers update_followers = 98; - Unfollow unfollow = 99; - GetPrivateUserInfo get_private_user_info = 100; - GetPrivateUserInfoResponse get_private_user_info_response = 101; - UpdateDiffBase update_diff_base = 102; + Follow follow = 98; + FollowResponse follow_response = 99; + UpdateFollowers update_followers = 100; + Unfollow unfollow = 101; + GetPrivateUserInfo get_private_user_info = 102; + GetPrivateUserInfoResponse get_private_user_info_response = 103; + UpdateDiffBase update_diff_base = 104; - OnTypeFormatting on_type_formatting = 103; - OnTypeFormattingResponse on_type_formatting_response = 104; + OnTypeFormatting on_type_formatting = 105; + OnTypeFormattingResponse on_type_formatting_response = 106; - UpdateWorktreeSettings update_worktree_settings = 105; + UpdateWorktreeSettings update_worktree_settings = 107; - InlayHints inlay_hints = 106; - InlayHintsResponse inlay_hints_response = 107; - ResolveInlayHint resolve_inlay_hint = 108; - ResolveInlayHintResponse resolve_inlay_hint_response = 109; - RefreshInlayHints refresh_inlay_hints = 110; + InlayHints inlay_hints = 108; + InlayHintsResponse inlay_hints_response = 109; + ResolveInlayHint resolve_inlay_hint = 110; + ResolveInlayHintResponse resolve_inlay_hint_response = 111; + RefreshInlayHints refresh_inlay_hints = 112; - CreateChannel create_channel = 111; - CreateChannelResponse create_channel_response = 112; - InviteChannelMember invite_channel_member = 113; - RemoveChannelMember remove_channel_member = 114; - RespondToChannelInvite respond_to_channel_invite = 115; - UpdateChannels update_channels = 116; - JoinChannel join_channel = 117; - DeleteChannel delete_channel = 118; - GetChannelMembers get_channel_members = 119; - GetChannelMembersResponse get_channel_members_response = 120; - SetChannelMemberAdmin set_channel_member_admin = 121; - RenameChannel rename_channel = 122; - RenameChannelResponse rename_channel_response = 123; + CreateChannel create_channel = 113; + CreateChannelResponse create_channel_response = 114; + InviteChannelMember invite_channel_member = 115; + RemoveChannelMember remove_channel_member = 116; + RespondToChannelInvite respond_to_channel_invite = 117; + UpdateChannels update_channels = 118; + JoinChannel join_channel = 119; + DeleteChannel delete_channel = 120; + GetChannelMembers get_channel_members = 121; + GetChannelMembersResponse get_channel_members_response = 122; + SetChannelMemberRole set_channel_member_role = 123; + RenameChannel rename_channel = 124; + RenameChannelResponse rename_channel_response = 125; - JoinChannelBuffer join_channel_buffer = 124; - JoinChannelBufferResponse join_channel_buffer_response = 125; - UpdateChannelBuffer update_channel_buffer = 126; - LeaveChannelBuffer leave_channel_buffer = 127; - UpdateChannelBufferCollaborators update_channel_buffer_collaborators = 128; - RejoinChannelBuffers rejoin_channel_buffers = 129; - RejoinChannelBuffersResponse rejoin_channel_buffers_response = 130; - AckBufferOperation ack_buffer_operation = 143; + JoinChannelBuffer join_channel_buffer = 126; + JoinChannelBufferResponse join_channel_buffer_response = 127; + UpdateChannelBuffer update_channel_buffer = 128; + LeaveChannelBuffer leave_channel_buffer = 129; + UpdateChannelBufferCollaborators update_channel_buffer_collaborators = 130; + RejoinChannelBuffers rejoin_channel_buffers = 131; + RejoinChannelBuffersResponse rejoin_channel_buffers_response = 132; + AckBufferOperation ack_buffer_operation = 145; - JoinChannelChat join_channel_chat = 131; - JoinChannelChatResponse join_channel_chat_response = 132; - LeaveChannelChat leave_channel_chat = 133; - SendChannelMessage send_channel_message = 134; - SendChannelMessageResponse send_channel_message_response = 135; - ChannelMessageSent channel_message_sent = 136; - GetChannelMessages get_channel_messages = 137; - GetChannelMessagesResponse get_channel_messages_response = 138; - RemoveChannelMessage remove_channel_message = 139; - AckChannelMessage ack_channel_message = 144; + JoinChannelChat join_channel_chat = 133; + JoinChannelChatResponse join_channel_chat_response = 134; + LeaveChannelChat leave_channel_chat = 135; + SendChannelMessage send_channel_message = 136; + SendChannelMessageResponse send_channel_message_response = 137; + ChannelMessageSent channel_message_sent = 138; + GetChannelMessages get_channel_messages = 139; + GetChannelMessagesResponse get_channel_messages_response = 140; + RemoveChannelMessage remove_channel_message = 141; + AckChannelMessage ack_channel_message = 146; - LinkChannel link_channel = 140; - UnlinkChannel unlink_channel = 141; - MoveChannel move_channel = 142; // current max: 144 + LinkChannel link_channel = 142; + UnlinkChannel unlink_channel = 143; + MoveChannel move_channel = 144; + SetChannelVisibility set_channel_visibility = 147; // current max: 147 } } @@ -832,6 +835,17 @@ message ResolveState { } } +message ResolveCompletionDocumentation { + uint64 project_id = 1; + uint64 language_server_id = 2; + bytes lsp_completion = 3; +} + +message ResolveCompletionDocumentationResponse { + string text = 1; + bool is_markdown = 2; +} + message ResolveInlayHint { uint64 project_id = 1; uint64 buffer_id = 2; @@ -979,7 +993,7 @@ message ChannelEdge { message ChannelPermission { uint64 channel_id = 1; - bool is_admin = 2; + ChannelRole role = 3; } message ChannelParticipants { @@ -1005,8 +1019,8 @@ message GetChannelMembersResponse { message ChannelMember { uint64 user_id = 1; - bool admin = 2; Kind kind = 3; + ChannelRole role = 4; enum Kind { Member = 0; @@ -1028,7 +1042,7 @@ message CreateChannelResponse { message InviteChannelMember { uint64 channel_id = 1; uint64 user_id = 2; - bool admin = 3; + ChannelRole role = 4; } message RemoveChannelMember { @@ -1036,10 +1050,22 @@ message RemoveChannelMember { uint64 user_id = 2; } -message SetChannelMemberAdmin { +enum ChannelRole { + Admin = 0; + Member = 1; + Guest = 2; + Banned = 3; +} + +message SetChannelMemberRole { uint64 channel_id = 1; uint64 user_id = 2; - bool admin = 3; + ChannelRole role = 3; +} + +message SetChannelVisibility { + uint64 channel_id = 1; + ChannelVisibility visibility = 2; } message RenameChannel { @@ -1533,9 +1559,15 @@ message Nonce { uint64 lower_half = 2; } +enum ChannelVisibility { + Public = 0; + Members = 1; +} + message Channel { uint64 id = 1; string name = 2; + ChannelVisibility visibility = 3; } message Contact { diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index f0d7937f6f..682b275548 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -205,6 +205,8 @@ messages!( (OnTypeFormattingResponse, Background), (InlayHints, Background), (InlayHintsResponse, Background), + (ResolveCompletionDocumentation, Background), + (ResolveCompletionDocumentationResponse, Background), (ResolveInlayHint, Background), (ResolveInlayHintResponse, Background), (RefreshInlayHints, Foreground), @@ -230,7 +232,8 @@ messages!( (SaveBuffer, Foreground), (RenameChannel, Foreground), (RenameChannelResponse, Foreground), - (SetChannelMemberAdmin, Foreground), + (SetChannelMemberRole, Foreground), + (SetChannelVisibility, Foreground), (SearchProject, Background), (SearchProjectResponse, Background), (ShareProject, Foreground), @@ -318,6 +321,10 @@ request_messages!( (PrepareRename, PrepareRenameResponse), (OnTypeFormatting, OnTypeFormattingResponse), (InlayHints, InlayHintsResponse), + ( + ResolveCompletionDocumentation, + ResolveCompletionDocumentationResponse + ), (ResolveInlayHint, ResolveInlayHintResponse), (RefreshInlayHints, Ack), (ReloadBuffers, ReloadBuffersResponse), @@ -326,7 +333,8 @@ request_messages!( (RemoveContact, Ack), (RespondToContactRequest, Ack), (RespondToChannelInvite, Ack), - (SetChannelMemberAdmin, Ack), + (SetChannelMemberRole, Ack), + (SetChannelVisibility, Ack), (SendChannelMessage, SendChannelMessageResponse), (GetChannelMessages, GetChannelMessagesResponse), (GetChannelMembers, GetChannelMembersResponse), @@ -381,6 +389,7 @@ entity_messages!( PerformRename, OnTypeFormatting, InlayHints, + ResolveCompletionDocumentation, ResolveInlayHint, RefreshInlayHints, PrepareRename, diff --git a/crates/rpc/src/rpc.rs b/crates/rpc/src/rpc.rs index 942672b94b..5ba531a50e 100644 --- a/crates/rpc/src/rpc.rs +++ b/crates/rpc/src/rpc.rs @@ -6,4 +6,4 @@ pub use conn::Connection; pub use peer::*; mod macros; -pub const PROTOCOL_VERSION: u32 = 64; +pub const PROTOCOL_VERSION: u32 = 65; diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 07c1eef3ff..ef8c56f2a7 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -537,6 +537,7 @@ impl BufferSearchBar { self.active_searchable_item .as_ref() .map(|searchable_item| searchable_item.query_suggestion(cx)) + .filter(|suggestion| !suggestion.is_empty()) } pub fn set_replacement(&mut self, replacement: Option<&str>, cx: &mut ViewContext) { diff --git a/crates/semantic_index/Cargo.toml b/crates/semantic_index/Cargo.toml index 34850f7035..1febb2af78 100644 --- a/crates/semantic_index/Cargo.toml +++ b/crates/semantic_index/Cargo.toml @@ -51,7 +51,6 @@ workspace = { path = "../workspace", features = ["test-support"] } settings = { path = "../settings", features = ["test-support"]} rust-embed = { version = "8.0", features = ["include-exclude"] } client = { path = "../client" } -zed = { path = "../zed"} node_runtime = { path = "../node_runtime"} pretty_assertions.workspace = true @@ -70,6 +69,3 @@ tree-sitter-elixir.workspace = true tree-sitter-lua.workspace = true tree-sitter-ruby.workspace = true tree-sitter-php.workspace = true - -[[example]] -name = "eval" diff --git a/crates/storybook/Cargo.lock b/crates/storybook/Cargo.lock deleted file mode 100644 index b652dbbd13..0000000000 --- a/crates/storybook/Cargo.lock +++ /dev/null @@ -1,2919 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "addr2line" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4fa78e18c64fce05e902adecd7a5eed15a5e0a3439f7b0e169f0252214865e3" -dependencies = [ - "gimli", -] - -[[package]] -name = "adler" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" - -[[package]] -name = "adler32" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" - -[[package]] -name = "aho-corasick" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" -dependencies = [ - "memchr", -] - -[[package]] -name = "anyhow" -version = "1.0.71" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" - -[[package]] -name = "arrayref" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" - -[[package]] -name = "arrayvec" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" - -[[package]] -name = "arrayvec" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" - -[[package]] -name = "async-channel" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" -dependencies = [ - "concurrent-queue", - "event-listener", - "futures-core", -] - -[[package]] -name = "async-executor" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fa3dc5f2a8564f07759c008b9109dc0d39de92a88d5588b8a5036d286383afb" -dependencies = [ - "async-lock", - "async-task", - "concurrent-queue", - "fastrand", - "futures-lite", - "slab", -] - -[[package]] -name = "async-fs" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "279cf904654eeebfa37ac9bb1598880884924aab82e290aa65c9e77a0e142e06" -dependencies = [ - "async-lock", - "autocfg", - "blocking", - "futures-lite", -] - -[[package]] -name = "async-io" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af" -dependencies = [ - "async-lock", - "autocfg", - "cfg-if", - "concurrent-queue", - "futures-lite", - "log", - "parking", - "polling", - "rustix", - "slab", - "socket2", - "waker-fn", -] - -[[package]] -name = "async-lock" -version = "2.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa24f727524730b077666307f2734b4a1a1c57acb79193127dcc8914d5242dd7" -dependencies = [ - "event-listener", -] - -[[package]] -name = "async-net" -version = "1.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4051e67316bc7eff608fe723df5d32ed639946adcd69e07df41fd42a7b411f1f" -dependencies = [ - "async-io", - "autocfg", - "blocking", - "futures-lite", -] - -[[package]] -name = "async-process" -version = "1.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a9d28b1d97e08915212e2e45310d47854eafa69600756fc735fb788f75199c9" -dependencies = [ - "async-io", - "async-lock", - "autocfg", - "blocking", - "cfg-if", - "event-listener", - "futures-lite", - "rustix", - "signal-hook", - "windows-sys", -] - -[[package]] -name = "async-task" -version = "4.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc7ab41815b3c653ccd2978ec3255c81349336702dfdf62ee6f7069b12a3aae" - -[[package]] -name = "atomic" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba" - -[[package]] -name = "atomic-waker" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1181e1e0d1fce796a03db1ae795d67167da795f9cf4a39c37589e85ef57f26d3" - -[[package]] -name = "autocfg" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" - -[[package]] -name = "backtrace" -version = "0.3.68" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4319208da049c43661739c5fade2ba182f09d1dc2299b32298d3a31692b17e12" -dependencies = [ - "addr2line", - "cc", - "cfg-if", - "libc", - "miniz_oxide 0.7.1", - "object", - "rustc-demangle", -] - -[[package]] -name = "base64" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" - -[[package]] -name = "bindgen" -version = "0.65.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfdf7b466f9a4903edc73f95d6d2bcd5baf8ae620638762244d3f60143643cc5" -dependencies = [ - "bitflags", - "cexpr", - "clang-sys", - "lazy_static", - "lazycell", - "log", - "peeking_take_while", - "prettyplease", - "proc-macro2", - "quote", - "regex", - "rustc-hash", - "shlex", - "syn 2.0.25", - "which", -] - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "block" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - -[[package]] -name = "blocking" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77231a1c8f801696fc0123ec6150ce92cffb8e164a02afb9c8ddee0e9b65ad65" -dependencies = [ - "async-channel", - "async-lock", - "async-task", - "atomic-waker", - "fastrand", - "futures-lite", - "log", -] - -[[package]] -name = "bstr" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6798148dccfbff0fae41c7574d2fa8f1ef3492fba0face179de5d8d447d67b05" -dependencies = [ - "memchr", - "serde", -] - -[[package]] -name = "bytemuck" -version = "1.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17febce684fd15d89027105661fec94afb475cb995fbc59d2865198446ba2eea" - -[[package]] -name = "byteorder" -version = "1.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" - -[[package]] -name = "bytes" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" - -[[package]] -name = "castaway" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2698f953def977c68f935bb0dfa959375ad4638570e969e2f1e9f433cbf1af6" - -[[package]] -name = "cc" -version = "1.0.79" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" - -[[package]] -name = "cexpr" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" -dependencies = [ - "nom", -] - -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "clang-sys" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c688fc74432808e3eb684cae8830a86be1d66a2bd58e1f248ed0960a590baf6f" -dependencies = [ - "glob", - "libc", - "libloading 0.7.4", -] - -[[package]] -name = "cmake" -version = "0.1.50" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31c789563b815f77f4250caee12365734369f942439b7defd71e18a48197130" -dependencies = [ - "cc", -] - -[[package]] -name = "cocoa" -version = "0.24.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f425db7937052c684daec3bd6375c8abe2d146dca4b8b143d6db777c39138f3a" -dependencies = [ - "bitflags", - "block", - "cocoa-foundation", - "core-foundation", - "core-graphics", - "foreign-types", - "libc", - "objc", -] - -[[package]] -name = "cocoa-foundation" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "931d3837c286f56e3c58423ce4eba12d08db2374461a785c86f672b08b5650d6" -dependencies = [ - "bitflags", - "block", - "core-foundation", - "core-graphics-types", - "foreign-types", - "libc", - "objc", -] - -[[package]] -name = "collections" -version = "0.1.0" - -[[package]] -name = "color_quant" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" - -[[package]] -name = "concurrent-queue" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62ec6771ecfa0762d24683ee5a32ad78487a3d3afdc0fb8cae19d2c5deb50b7c" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "const-cstr" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed3d0b5ff30645a68f35ece8cea4556ca14ef8a1651455f789a099a0513532a6" - -[[package]] -name = "core-foundation" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" -dependencies = [ - "core-foundation-sys", - "libc", - "uuid 0.5.1", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" - -[[package]] -name = "core-graphics" -version = "0.22.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2581bbab3b8ffc6fcbd550bf46c355135d16e9ff2a6ea032ad6b9bf1d7efe4fb" -dependencies = [ - "bitflags", - "core-foundation", - "core-graphics-types", - "foreign-types", - "libc", -] - -[[package]] -name = "core-graphics-types" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bb142d41022986c1d8ff29103a1411c8a3dfad3552f87a4f8dc50d61d4f4e33" -dependencies = [ - "bitflags", - "core-foundation", - "libc", -] - -[[package]] -name = "core-text" -version = "19.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d74ada66e07c1cefa18f8abfba765b486f250de2e4a999e5727fc0dd4b4a25" -dependencies = [ - "core-foundation", - "core-graphics", - "foreign-types", - "libc", -] - -[[package]] -name = "cpufeatures" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1" -dependencies = [ - "libc", -] - -[[package]] -name = "crc32fast" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "crossbeam-channel" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" -dependencies = [ - "cfg-if", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-deque" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" -dependencies = [ - "cfg-if", - "crossbeam-epoch", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.9.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7" -dependencies = [ - "autocfg", - "cfg-if", - "crossbeam-utils", - "memoffset", - "scopeguard", -] - -[[package]] -name = "crossbeam-queue" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add" -dependencies = [ - "cfg-if", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "crypto-common" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" -dependencies = [ - "generic-array", - "typenum", -] - -[[package]] -name = "ctor" -version = "0.1.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d2301688392eb071b0bf1a37be05c469d3cc4dbbd95df672fe28ab021e6a096" -dependencies = [ - "quote", - "syn 1.0.109", -] - -[[package]] -name = "curl" -version = "0.4.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "509bd11746c7ac09ebd19f0b17782eae80aadee26237658a6b4808afb5c11a22" -dependencies = [ - "curl-sys", - "libc", - "openssl-probe", - "openssl-sys", - "schannel", - "socket2", - "winapi", -] - -[[package]] -name = "curl-sys" -version = "0.4.63+curl-8.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aeb0fef7046022a1e2ad67a004978f0e3cacb9e3123dc62ce768f92197b771dc" -dependencies = [ - "cc", - "libc", - "libz-sys", - "openssl-sys", - "pkg-config", - "vcpkg", - "winapi", -] - -[[package]] -name = "data-url" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a30bfce702bcfa94e906ef82421f2c0e61c076ad76030c16ee5d2e9a32fe193" -dependencies = [ - "matches", -] - -[[package]] -name = "deflate" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73770f8e1fe7d64df17ca66ad28994a0a623ea497fa69486e14984e715c5d174" -dependencies = [ - "adler32", - "byteorder", -] - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "crypto-common", -] - -[[package]] -name = "dirs" -version = "3.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30baa043103c9d0c2a57cf537cc2f35623889dc0d405e6c3cccfadbc81c71309" -dependencies = [ - "dirs-sys", -] - -[[package]] -name = "dirs-next" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" -dependencies = [ - "cfg-if", - "dirs-sys-next", -] - -[[package]] -name = "dirs-sys" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" -dependencies = [ - "libc", - "redox_users", - "winapi", -] - -[[package]] -name = "dirs-sys-next" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" -dependencies = [ - "libc", - "redox_users", - "winapi", -] - -[[package]] -name = "dlib" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" -dependencies = [ - "libloading 0.8.0", -] - -[[package]] -name = "dwrote" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439a1c2ba5611ad3ed731280541d36d2e9c4ac5e7fb818a27b604bdc5a6aa65b" -dependencies = [ - "lazy_static", - "libc", - "winapi", - "wio", -] - -[[package]] -name = "dyn-clone" -version = "1.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68b0cf012f1230e43cd00ebb729c6bb58707ecfa8ad08b52ef3a4ccd2697fc30" - -[[package]] -name = "either" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" - -[[package]] -name = "encoding_rs" -version = "0.8.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "equivalent" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" - -[[package]] -name = "erased-serde" -version = "0.3.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f94c0e13118e7d7533271f754a168ae8400e6a1cc043f2bfd53cc7290f1a1de3" -dependencies = [ - "serde", -] - -[[package]] -name = "errno" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" -dependencies = [ - "errno-dragonfly", - "libc", - "windows-sys", -] - -[[package]] -name = "errno-dragonfly" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" -dependencies = [ - "cc", - "libc", -] - -[[package]] -name = "etagere" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcf22f748754352918e082e0039335ee92454a5d62bcaf69b5e8daf5907d9644" -dependencies = [ - "euclid", - "svg_fmt", -] - -[[package]] -name = "euclid" -version = "0.22.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87f253bc5c813ca05792837a0ff4b3a580336b224512d48f7eda1d7dd9210787" -dependencies = [ - "num-traits", -] - -[[package]] -name = "event-listener" -version = "2.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" - -[[package]] -name = "fastrand" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" -dependencies = [ - "instant", -] - -[[package]] -name = "flate2" -version = "1.0.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743" -dependencies = [ - "crc32fast", - "miniz_oxide 0.7.1", -] - -[[package]] -name = "float-cmp" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75224bec9bfe1a65e2d34132933f2de7fe79900c96a0174307554244ece8150e" - -[[package]] -name = "float-ord" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bad48618fdb549078c333a7a8528acb57af271d0433bdecd523eb620628364e" - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "font-kit" -version = "0.11.0" -source = "git+https://github.com/zed-industries/font-kit?rev=b2f77d56f450338aa4f7dd2f0197d8c9acb0cf18#b2f77d56f450338aa4f7dd2f0197d8c9acb0cf18" -dependencies = [ - "bitflags", - "byteorder", - "core-foundation", - "core-graphics", - "core-text", - "dirs-next", - "dwrote", - "float-ord", - "freetype", - "lazy_static", - "libc", - "log", - "pathfinder_geometry", - "pathfinder_simd", - "walkdir", - "winapi", - "yeslogic-fontconfig-sys", -] - -[[package]] -name = "fontdb" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e58903f4f8d5b58c7d300908e4ebe5289c1bfdf5587964330f12023b8ff17fd1" -dependencies = [ - "log", - "memmap2", - "ttf-parser 0.12.3", -] - -[[package]] -name = "foreign-types" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" -dependencies = [ - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-shared" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" - -[[package]] -name = "form_urlencoded" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "freetype" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bee38378a9e3db1cc693b4f88d166ae375338a0ff75cb8263e1c601d51f35dc6" -dependencies = [ - "freetype-sys", - "libc", -] - -[[package]] -name = "freetype-sys" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a37d4011c0cc628dfa766fcc195454f4b068d7afdc2adfd28861191d866e731a" -dependencies = [ - "cmake", - "libc", - "pkg-config", -] - -[[package]] -name = "futures" -version = "0.3.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-core" -version = "0.3.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" - -[[package]] -name = "futures-executor" -version = "0.3.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" - -[[package]] -name = "futures-lite" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" -dependencies = [ - "fastrand", - "futures-core", - "futures-io", - "memchr", - "parking", - "pin-project-lite", - "waker-fn", -] - -[[package]] -name = "futures-macro" -version = "0.3.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.25", -] - -[[package]] -name = "futures-sink" -version = "0.3.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" - -[[package]] -name = "futures-task" -version = "0.3.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" - -[[package]] -name = "futures-util" -version = "0.3.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "pin-utils", - "slab", -] - -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - -[[package]] -name = "getrandom" -version = "0.2.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" -dependencies = [ - "cfg-if", - "libc", - "wasi", -] - -[[package]] -name = "gif" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3edd93c6756b4dfaf2709eafcc345ba2636565295c198a9cfbf75fa5e3e00b06" -dependencies = [ - "color_quant", - "weezl", -] - -[[package]] -name = "gimli" -version = "0.27.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e" - -[[package]] -name = "glob" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" - -[[package]] -name = "globset" -version = "0.4.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1391ab1f92ffcc08911957149833e682aa3fe252b9f45f966d2ef972274c97df" -dependencies = [ - "aho-corasick", - "bstr", - "fnv", - "log", - "regex", -] - -[[package]] -name = "gpui" -version = "0.1.0" -dependencies = [ - "anyhow", - "async-task", - "bindgen", - "block", - "cc", - "cocoa", - "collections", - "core-foundation", - "core-graphics", - "core-text", - "ctor", - "etagere", - "font-kit", - "foreign-types", - "futures", - "gpui_macros", - "image", - "itertools", - "lazy_static", - "log", - "media", - "metal", - "num_cpus", - "objc", - "ordered-float", - "parking", - "parking_lot 0.11.2", - "pathfinder_color", - "pathfinder_geometry", - "postage", - "rand", - "resvg", - "schemars", - "seahash", - "serde", - "serde_derive", - "serde_json", - "smallvec", - "smol", - "sqlez", - "sum_tree", - "time", - "tiny-skia", - "usvg", - "util", - "uuid 1.4.0", - "waker-fn", -] - -[[package]] -name = "gpui_macros" -version = "0.1.0" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "hashbrown" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" - -[[package]] -name = "hermit-abi" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" - -[[package]] -name = "http" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - -[[package]] -name = "idna" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" -dependencies = [ - "unicode-bidi", - "unicode-normalization", -] - -[[package]] -name = "image" -version = "0.23.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24ffcb7e7244a9bf19d35bf2883b9c080c4ced3c07a9895572178cdb8f13f6a1" -dependencies = [ - "bytemuck", - "byteorder", - "color_quant", - "gif", - "jpeg-decoder", - "num-iter", - "num-rational", - "num-traits", - "png", - "scoped_threadpool", - "tiff", -] - -[[package]] -name = "indexmap" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" -dependencies = [ - "equivalent", - "hashbrown", -] - -[[package]] -name = "indoc" -version = "1.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa799dd5ed20a7e349f3b4639aa80d74549c81716d9ec4f994c9b5815598306" - -[[package]] -name = "instant" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "io-lifetimes" -version = "1.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" -dependencies = [ - "hermit-abi", - "libc", - "windows-sys", -] - -[[package]] -name = "isahc" -version = "1.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "334e04b4d781f436dc315cb1e7515bd96826426345d498149e4bde36b67f8ee9" -dependencies = [ - "async-channel", - "castaway", - "crossbeam-utils", - "curl", - "curl-sys", - "encoding_rs", - "event-listener", - "futures-lite", - "http", - "log", - "mime", - "once_cell", - "polling", - "slab", - "sluice", - "tracing", - "tracing-futures", - "url", - "waker-fn", -] - -[[package]] -name = "itertools" -version = "0.10.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" -dependencies = [ - "either", -] - -[[package]] -name = "itoa" -version = "1.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b02a5381cc465bd3041d84623d0fa3b66738b52b8e2fc3bab8ad63ab032f4a" - -[[package]] -name = "jpeg-decoder" -version = "0.1.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "229d53d58899083193af11e15917b5640cd40b29ff475a1fe4ef725deb02d0f2" -dependencies = [ - "rayon", -] - -[[package]] -name = "kurbo" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a53776d271cfb873b17c618af0298445c88afc52837f3e948fa3fafd131f449" -dependencies = [ - "arrayvec 0.7.4", -] - -[[package]] -name = "lazy_static" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" - -[[package]] -name = "lazycell" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" - -[[package]] -name = "libc" -version = "0.2.147" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" - -[[package]] -name = "libloading" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" -dependencies = [ - "cfg-if", - "winapi", -] - -[[package]] -name = "libloading" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d580318f95776505201b28cf98eb1fa5e4be3b689633ba6a3e6cd880ff22d8cb" -dependencies = [ - "cfg-if", - "windows-sys", -] - -[[package]] -name = "libsqlite3-sys" -version = "0.24.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "898745e570c7d0453cc1fbc4a701eb6c662ed54e8fec8b7d14be137ebeeb9d14" -dependencies = [ - "cc", - "pkg-config", - "vcpkg", -] - -[[package]] -name = "libz-sys" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56ee889ecc9568871456d42f603d6a0ce59ff328d291063a45cbdf0036baf6db" -dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", -] - -[[package]] -name = "linux-raw-sys" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" - -[[package]] -name = "lock_api" -version = "0.4.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" -dependencies = [ - "autocfg", - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" -dependencies = [ - "serde", - "value-bag", -] - -[[package]] -name = "malloc_buf" -version = "0.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" -dependencies = [ - "libc", -] - -[[package]] -name = "matches" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" - -[[package]] -name = "media" -version = "0.1.0" -dependencies = [ - "anyhow", - "bindgen", - "block", - "bytes", - "core-foundation", - "foreign-types", - "metal", - "objc", -] - -[[package]] -name = "memchr" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" - -[[package]] -name = "memmap2" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "723e3ebdcdc5c023db1df315364573789f8857c11b631a2fdfad7c00f5c046b4" -dependencies = [ - "libc", -] - -[[package]] -name = "memoffset" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" -dependencies = [ - "autocfg", -] - -[[package]] -name = "metal" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4598d719460ade24c7d91f335daf055bf2a7eec030728ce751814c50cdd6a26c" -dependencies = [ - "bitflags", - "block", - "cocoa-foundation", - "foreign-types", - "log", - "objc", -] - -[[package]] -name = "mime" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" - -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - -[[package]] -name = "miniz_oxide" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "791daaae1ed6889560f8c4359194f56648355540573244a5448a83ba1ecc7435" -dependencies = [ - "adler32", -] - -[[package]] -name = "miniz_oxide" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b" -dependencies = [ - "adler", - "autocfg", -] - -[[package]] -name = "miniz_oxide" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" -dependencies = [ - "adler", -] - -[[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - -[[package]] -name = "num-integer" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" -dependencies = [ - "autocfg", - "num-traits", -] - -[[package]] -name = "num-iter" -version = "0.1.43" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", -] - -[[package]] -name = "num-rational" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" -dependencies = [ - "autocfg", -] - -[[package]] -name = "num_cpus" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" -dependencies = [ - "hermit-abi", - "libc", -] - -[[package]] -name = "objc" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" -dependencies = [ - "malloc_buf", - "objc_exception", -] - -[[package]] -name = "objc_exception" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad970fb455818ad6cba4c122ad012fae53ae8b4795f86378bce65e4f6bab2ca4" -dependencies = [ - "cc", -] - -[[package]] -name = "object" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bda667d9f2b5051b8833f59f3bf748b28ef54f850f4fcb389a252aa383866d1" -dependencies = [ - "memchr", -] - -[[package]] -name = "once_cell" -version = "1.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" - -[[package]] -name = "openssl-probe" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" - -[[package]] -name = "openssl-sys" -version = "0.9.90" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "374533b0e45f3a7ced10fcaeccca020e66656bc03dac384f852e4e5a7a8104a6" -dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", -] - -[[package]] -name = "ordered-float" -version = "2.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7940cf2ca942593318d07fcf2596cdca60a85c9e7fab408a5e21a4f9dcd40d87" -dependencies = [ - "num-traits", -] - -[[package]] -name = "parking" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14f2252c834a40ed9bb5422029649578e63aa341ac401f74e719dd1afda8394e" - -[[package]] -name = "parking_lot" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" -dependencies = [ - "instant", - "lock_api", - "parking_lot_core 0.8.6", -] - -[[package]] -name = "parking_lot" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" -dependencies = [ - "lock_api", - "parking_lot_core 0.9.8", -] - -[[package]] -name = "parking_lot_core" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" -dependencies = [ - "cfg-if", - "instant", - "libc", - "redox_syscall 0.2.16", - "smallvec", - "winapi", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall 0.3.5", - "smallvec", - "windows-targets", -] - -[[package]] -name = "pathfinder_color" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69bdc0d277d559e35e1b374de56df9262a6b71e091ca04a8831a239f8c7f0c62" -dependencies = [ - "pathfinder_simd", -] - -[[package]] -name = "pathfinder_geometry" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b7e7b4ea703700ce73ebf128e1450eb69c3a8329199ffbfb9b2a0418e5ad3" -dependencies = [ - "log", - "pathfinder_simd", -] - -[[package]] -name = "pathfinder_simd" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39fe46acc5503595e5949c17b818714d26fdf9b4920eacf3b2947f0199f4a6ff" -dependencies = [ - "rustc_version", -] - -[[package]] -name = "peeking_take_while" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" - -[[package]] -name = "percent-encoding" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" - -[[package]] -name = "pest" -version = "2.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f73935e4d55e2abf7f130186537b19e7a4abc886a0252380b59248af473a3fc9" -dependencies = [ - "thiserror", - "ucd-trie", -] - -[[package]] -name = "pico-args" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db8bcd96cb740d03149cbad5518db9fd87126a10ab519c011893b1754134c468" - -[[package]] -name = "pin-project" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "030ad2bc4db10a8944cb0d837f158bdfec4d4a4873ab701a95046770d11f8842" -dependencies = [ - "pin-project-internal", -] - -[[package]] -name = "pin-project-internal" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec2e072ecce94ec471b13398d5402c188e76ac03cf74dd1a975161b23a3f6d9c" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.25", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c40d25201921e5ff0c862a505c6557ea88568a4e3ace775ab55e93f2f4f9d57" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "pkg-config" -version = "0.3.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" - -[[package]] -name = "storybook" -version = "0.1.0" -dependencies = [ - "gpui", -] - -[[package]] -name = "png" -version = "0.16.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c3287920cb847dee3de33d301c463fba14dda99db24214ddf93f83d3021f4c6" -dependencies = [ - "bitflags", - "crc32fast", - "deflate", - "miniz_oxide 0.3.7", -] - -[[package]] -name = "polling" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" -dependencies = [ - "autocfg", - "bitflags", - "cfg-if", - "concurrent-queue", - "libc", - "log", - "pin-project-lite", - "windows-sys", -] - -[[package]] -name = "pollster" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5da3b0203fd7ee5720aa0b5e790b591aa5d3f41c3ed2c34a3a393382198af2f7" - -[[package]] -name = "postage" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af3fb618632874fb76937c2361a7f22afd393c982a2165595407edc75b06d3c1" -dependencies = [ - "atomic", - "crossbeam-queue", - "futures", - "log", - "parking_lot 0.12.1", - "pin-project", - "pollster", - "static_assertions", - "thiserror", -] - -[[package]] -name = "ppv-lite86" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" - -[[package]] -name = "prettyplease" -version = "0.2.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92139198957b410250d43fad93e630d956499a625c527eda65175c8680f83387" -dependencies = [ - "proc-macro2", - "syn 2.0.25", -] - -[[package]] -name = "proc-macro2" -version = "1.0.64" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78803b62cbf1f46fde80d7c0e803111524b9877184cfe7c3033659490ac7a7da" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "573015e8ab27661678357f27dc26460738fd2b6c86e46f386fde94cb5d913105" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha", - "rand_core", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom", -] - -[[package]] -name = "rayon" -version = "1.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d2df5196e37bcc87abebc0053e20787d73847bb33134a69841207dd0a47f03b" -dependencies = [ - "either", - "rayon-core", -] - -[[package]] -name = "rayon-core" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d" -dependencies = [ - "crossbeam-channel", - "crossbeam-deque", - "crossbeam-utils", - "num_cpus", -] - -[[package]] -name = "rctree" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be9e29cb19c8fe84169fcb07f8f11e66bc9e6e0280efd4715c54818296f8a4a8" - -[[package]] -name = "redox_syscall" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" -dependencies = [ - "bitflags", -] - -[[package]] -name = "redox_syscall" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" -dependencies = [ - "bitflags", -] - -[[package]] -name = "redox_users" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" -dependencies = [ - "getrandom", - "redox_syscall 0.2.16", - "thiserror", -] - -[[package]] -name = "regex" -version = "1.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2eae68fc220f7cf2532e4494aded17545fce192d59cd996e0fe7887f4ceb575" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39354c10dd07468c2e73926b23bb9c2caca74c5501e38a35da70406f1d923310" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2" - -[[package]] -name = "resvg" -version = "0.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09697862c5c3f940cbaffef91969c62188b5c8ed385b0aef43a5ff01ddc8000f" -dependencies = [ - "jpeg-decoder", - "log", - "pico-args", - "png", - "rgb", - "svgfilters", - "tiny-skia", - "usvg", -] - -[[package]] -name = "rgb" -version = "0.8.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20ec2d3e3fc7a92ced357df9cebd5a10b6fb2aa1ee797bf7e9ce2f17dffc8f59" -dependencies = [ - "bytemuck", -] - -[[package]] -name = "roxmltree" -version = "0.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "921904a62e410e37e215c40381b7117f830d9d89ba60ab5236170541dd25646b" -dependencies = [ - "xmlparser", -] - -[[package]] -name = "rust-embed" -version = "6.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a36224c3276f8c4ebc8c20f158eca7ca4359c8db89991c4925132aaaf6702661" -dependencies = [ - "rust-embed-impl", - "rust-embed-utils", - "walkdir", -] - -[[package]] -name = "rust-embed-impl" -version = "6.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49b94b81e5b2c284684141a2fb9e2a31be90638caf040bf9afbc5a0416afe1ac" -dependencies = [ - "proc-macro2", - "quote", - "rust-embed-utils", - "syn 2.0.25", - "walkdir", -] - -[[package]] -name = "rust-embed-utils" -version = "7.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d38ff6bf570dc3bb7100fce9f7b60c33fa71d80e88da3f2580df4ff2bdded74" -dependencies = [ - "globset", - "sha2", - "walkdir", -] - -[[package]] -name = "rustc-demangle" -version = "0.1.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" - -[[package]] -name = "rustc-hash" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" - -[[package]] -name = "rustc_version" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee" -dependencies = [ - "semver", -] - -[[package]] -name = "rustix" -version = "0.37.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d69718bf81c6127a49dc64e44a742e8bb9213c0ff8869a22c308f84c1d4ab06" -dependencies = [ - "bitflags", - "errno", - "io-lifetimes", - "libc", - "linux-raw-sys", - "windows-sys", -] - -[[package]] -name = "rustybuzz" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ab463a295d00f3692e0974a0bfd83c7a9bcd119e27e07c2beecdb1b44a09d10" -dependencies = [ - "bitflags", - "bytemuck", - "smallvec", - "ttf-parser 0.9.0", - "unicode-bidi-mirroring", - "unicode-ccc", - "unicode-general-category", - "unicode-script", -] - -[[package]] -name = "ryu" -version = "1.0.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe232bdf6be8c8de797b22184ee71118d63780ea42ac85b61d1baa6d3b782ae9" - -[[package]] -name = "safe_arch" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1ff3d6d9696af502cc3110dacce942840fb06ff4514cad92236ecc455f2ce05" -dependencies = [ - "bytemuck", -] - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "schannel" -version = "0.1.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" -dependencies = [ - "windows-sys", -] - -[[package]] -name = "schemars" -version = "0.8.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02c613288622e5f0c3fdc5dbd4db1c5fbe752746b1d1a56a0630b78fd00de44f" -dependencies = [ - "dyn-clone", - "schemars_derive", - "serde", - "serde_json", -] - -[[package]] -name = "schemars_derive" -version = "0.8.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "109da1e6b197438deb6db99952990c7f959572794b80ff93707d55a232545e7c" -dependencies = [ - "proc-macro2", - "quote", - "serde_derive_internals", - "syn 1.0.109", -] - -[[package]] -name = "scoped_threadpool" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d51f5df5af43ab3f1360b429fa5e0152ac5ce8c0bd6485cae490332e96846a8" - -[[package]] -name = "scopeguard" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" - -[[package]] -name = "seahash" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" - -[[package]] -name = "semver" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6" -dependencies = [ - "semver-parser", -] - -[[package]] -name = "semver-parser" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0bef5b7f9e0df16536d3961cfb6e84331c065b4066afb39768d0e319411f7" -dependencies = [ - "pest", -] - -[[package]] -name = "serde" -version = "1.0.171" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30e27d1e4fd7659406c492fd6cfaf2066ba8773de45ca75e855590f856dc34a9" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.171" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389894603bd18c46fa56231694f8d827779c0951a667087194cf9de94ed24682" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.25", -] - -[[package]] -name = "serde_derive_internals" -version = "0.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85bf8229e7920a9f636479437026331ce11aa132b4dde37d121944a44d6e5f3c" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "serde_fmt" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1d4ddca14104cd60529e8c7f7ba71a2c8acd8f7f5cfcdc2faf97eeb7c3010a4" -dependencies = [ - "serde", -] - -[[package]] -name = "serde_json" -version = "1.0.102" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5062a995d481b2308b6064e9af76011f2921c35f97b0468811ed9f6cd91dfed" -dependencies = [ - "indexmap", - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "sha2" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "shlex" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" - -[[package]] -name = "signal-hook" -version = "0.3.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "732768f1176d21d09e076c23a93123d40bba92d50c4058da34d45c8de8e682b9" -dependencies = [ - "libc", - "signal-hook-registry", -] - -[[package]] -name = "signal-hook-registry" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" -dependencies = [ - "libc", -] - -[[package]] -name = "simplecss" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a11be7c62927d9427e9f40f3444d5499d868648e2edbc4e2116de69e7ec0e89d" -dependencies = [ - "log", -] - -[[package]] -name = "siphasher" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b8de496cf83d4ed58b6be86c3a275b8602f6ffe98d3024a869e124147a9a3ac" - -[[package]] -name = "slab" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" -dependencies = [ - "autocfg", -] - -[[package]] -name = "sluice" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d7400c0eff44aa2fcb5e31a5f24ba9716ed90138769e4977a2ba6014ae63eb5" -dependencies = [ - "async-channel", - "futures-core", - "futures-io", -] - -[[package]] -name = "smallvec" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" - -[[package]] -name = "smol" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13f2b548cd8447f8de0fdf1c592929f70f4fc7039a05e47404b0d096ec6987a1" -dependencies = [ - "async-channel", - "async-executor", - "async-fs", - "async-io", - "async-lock", - "async-net", - "async-process", - "blocking", - "futures-lite", -] - -[[package]] -name = "socket2" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" -dependencies = [ - "libc", - "winapi", -] - -[[package]] -name = "sqlez" -version = "0.1.0" -dependencies = [ - "anyhow", - "futures", - "indoc", - "lazy_static", - "libsqlite3-sys", - "parking_lot 0.11.2", - "smol", - "thread_local", - "uuid 1.4.0", -] - -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - -[[package]] -name = "sum_tree" -version = "0.1.0" -dependencies = [ - "arrayvec 0.7.4", - "log", -] - -[[package]] -name = "sval" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b031320a434d3e9477ccf9b5756d57d4272937b8d22cb88af80b7633a1b78b1" - -[[package]] -name = "sval_buffer" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bf7e9412af26b342f3f2cc5cc4122b0105e9d16eb76046cd14ed10106cf6028" -dependencies = [ - "sval", - "sval_ref", -] - -[[package]] -name = "sval_dynamic" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0ef628e8a77a46ed3338db8d1b08af77495123cc229453084e47cd716d403cf" -dependencies = [ - "sval", -] - -[[package]] -name = "sval_fmt" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dc09e9364c2045ab5fa38f7b04d077b3359d30c4c2b3ec4bae67a358bd64326" -dependencies = [ - "itoa", - "ryu", - "sval", -] - -[[package]] -name = "sval_json" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ada6f627e38cbb8860283649509d87bc4a5771141daa41c78fd31f2b9485888d" -dependencies = [ - "itoa", - "ryu", - "sval", -] - -[[package]] -name = "sval_ref" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "703ca1942a984bd0d9b5a4c0a65ab8b4b794038d080af4eb303c71bc6bf22d7c" -dependencies = [ - "sval", -] - -[[package]] -name = "sval_serde" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830926cd0581f7c3e5d51efae4d35c6b6fc4db583842652891ba2f1bed8db046" -dependencies = [ - "serde", - "sval", - "sval_buffer", - "sval_fmt", -] - -[[package]] -name = "svg_fmt" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fb1df15f412ee2e9dfc1c504260fa695c1c3f10fe9f4a6ee2d2184d7d6450e2" - -[[package]] -name = "svgfilters" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb0dce2fee79ac40c21dafba48565ff7a5fa275e23ffe9ce047a40c9574ba34e" -dependencies = [ - "float-cmp", - "rgb", -] - -[[package]] -name = "svgtypes" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c536faaff1a10837cfe373142583f6e27d81e96beba339147e77b67c9f260ff" -dependencies = [ - "float-cmp", - "siphasher", -] - -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "2.0.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15e3fc8c0c74267e2df136e5e5fb656a464158aa57624053375eb9c8c6e25ae2" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "take-until" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bdb6fa0dfa67b38c1e66b7041ba9dcf23b99d8121907cd31c807a332f7a0bbb" - -[[package]] -name = "thiserror" -version = "1.0.43" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a35fc5b8971143ca348fa6df4f024d4d55264f3468c71ad1c2f365b0a4d58c42" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.43" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "463fe12d7993d3b327787537ce8dd4dfa058de32fc2b195ef3cde03dc4771e8f" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.25", -] - -[[package]] -name = "thread_local" -version = "1.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" -dependencies = [ - "cfg-if", - "once_cell", -] - -[[package]] -name = "tiff" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a53f4706d65497df0c4349241deddf35f84cee19c87ed86ea8ca590f4464437" -dependencies = [ - "jpeg-decoder", - "miniz_oxide 0.4.4", - "weezl", -] - -[[package]] -name = "time" -version = "0.3.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59e399c068f43a5d116fedaf73b203fa4f9c519f17e2b34f63221d3792f81446" -dependencies = [ - "itoa", - "serde", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" - -[[package]] -name = "time-macros" -version = "0.2.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96ba15a897f3c86766b757e5ac7221554c6750054d74d5b28844fce5fb36a6c4" -dependencies = [ - "time-core", -] - -[[package]] -name = "tiny-skia" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bf81f2900d2e235220e6f31ec9f63ade6a7f59090c556d74fe949bb3b15e9fe" -dependencies = [ - "arrayref", - "arrayvec 0.5.2", - "bytemuck", - "cfg-if", - "png", - "safe_arch", -] - -[[package]] -name = "tinyvec" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - -[[package]] -name = "tracing" -version = "0.1.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" -dependencies = [ - "cfg-if", - "log", - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.25", -] - -[[package]] -name = "tracing-core" -version = "0.1.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" -dependencies = [ - "once_cell", -] - -[[package]] -name = "tracing-futures" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2" -dependencies = [ - "pin-project", - "tracing", -] - -[[package]] -name = "ttf-parser" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62ddb402ac6c2af6f7a2844243887631c4e94b51585b229fcfddb43958cd55ca" - -[[package]] -name = "ttf-parser" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ae2f58a822f08abdaf668897e96a5656fe72f5a9ce66422423e8849384872e6" - -[[package]] -name = "typenum" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" - -[[package]] -name = "ucd-trie" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" - -[[package]] -name = "unicode-bidi" -version = "0.3.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" - -[[package]] -name = "unicode-bidi-mirroring" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56d12260fb92d52f9008be7e4bca09f584780eb2266dc8fecc6a192bec561694" - -[[package]] -name = "unicode-ccc" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc2520efa644f8268dce4dcd3050eaa7fc044fca03961e9998ac7e2e92b77cf1" - -[[package]] -name = "unicode-general-category" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f9af028e052a610d99e066b33304625dea9613170a2563314490a4e6ec5cf7f" - -[[package]] -name = "unicode-ident" -version = "1.0.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22049a19f4a68748a168c0fc439f9516686aa045927ff767eca0a85101fb6e73" - -[[package]] -name = "unicode-normalization" -version = "0.1.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" -dependencies = [ - "tinyvec", -] - -[[package]] -name = "unicode-script" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d817255e1bed6dfd4ca47258685d14d2bdcfbc64fdc9e3819bd5848057b8ecc" - -[[package]] -name = "unicode-vo" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94" - -[[package]] -name = "url" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", -] - -[[package]] -name = "usvg" -version = "0.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef8352f317d8f9a918ba5154797fb2a93e2730244041cf7d5be35148266adfa5" -dependencies = [ - "base64", - "data-url", - "flate2", - "fontdb", - "kurbo", - "log", - "memmap2", - "pico-args", - "rctree", - "roxmltree", - "rustybuzz", - "simplecss", - "siphasher", - "svgtypes", - "ttf-parser 0.12.3", - "unicode-bidi", - "unicode-script", - "unicode-vo", - "xmlwriter", -] - -[[package]] -name = "util" -version = "0.1.0" -dependencies = [ - "anyhow", - "backtrace", - "dirs", - "futures", - "isahc", - "lazy_static", - "log", - "rand", - "rust-embed", - "serde", - "serde_json", - "smol", - "take-until", - "url", -] - -[[package]] -name = "uuid" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcc7e3b898aa6f6c08e5295b6c89258d1331e9ac578cc992fb818759951bdc22" - -[[package]] -name = "uuid" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d023da39d1fde5a8a3fe1f3e01ca9632ada0a63e9797de55a879d6e2236277be" -dependencies = [ - "getrandom", -] - -[[package]] -name = "value-bag" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d92ccd67fb88503048c01b59152a04effd0782d035a83a6d256ce6085f08f4a3" -dependencies = [ - "value-bag-serde1", - "value-bag-sval2", -] - -[[package]] -name = "value-bag-serde1" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0b9f3feef403a50d4d67e9741a6d8fc688bcbb4e4f31bd4aab72cc690284394" -dependencies = [ - "erased-serde", - "serde", - "serde_fmt", -] - -[[package]] -name = "value-bag-sval2" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30b24f4146b6f3361e91cbf527d1fb35e9376c3c0cef72ca5ec5af6d640fad7d" -dependencies = [ - "sval", - "sval_buffer", - "sval_dynamic", - "sval_fmt", - "sval_json", - "sval_ref", - "sval_serde", -] - -[[package]] -name = "vcpkg" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" - -[[package]] -name = "version_check" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" - -[[package]] -name = "waker-fn" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" - -[[package]] -name = "walkdir" -version = "2.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698" -dependencies = [ - "same-file", - "winapi-util", -] - -[[package]] -name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" - -[[package]] -name = "weezl" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9193164d4de03a926d909d3bc7c30543cecb35400c02114792c2cae20d5e2dbb" - -[[package]] -name = "which" -version = "4.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2441c784c52b289a054b7201fc93253e288f094e2f4be9058343127c4226a269" -dependencies = [ - "either", - "libc", - "once_cell", -] - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-util" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" -dependencies = [ - "winapi", -] - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-targets" -version = "0.48.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" - -[[package]] -name = "windows_i686_gnu" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" - -[[package]] -name = "windows_i686_msvc" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" - -[[package]] -name = "wio" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d129932f4644ac2396cb456385cbf9e63b5b30c6e8dc4820bdca4eb082037a5" -dependencies = [ - "winapi", -] - -[[package]] -name = "xmlparser" -version = "0.13.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d25c75bf9ea12c4040a97f829154768bbbce366287e2dc044af160cd79a13fd" - -[[package]] -name = "xmlwriter" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" - -[[package]] -name = "yeslogic-fontconfig-sys" -version = "3.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2bbd69036d397ebbff671b1b8e4d918610c181c5a16073b96f984a38d08c386" -dependencies = [ - "const-cstr", - "dlib", - "once_cell", - "pkg-config", -] diff --git a/crates/storybook/Cargo.toml b/crates/storybook/Cargo.toml deleted file mode 100644 index 43890dd01a..0000000000 --- a/crates/storybook/Cargo.toml +++ /dev/null @@ -1,30 +0,0 @@ -[package] -name = "storybook" -version = "0.1.0" -edition = "2021" -publish = false - -[[bin]] -name = "storybook" -path = "src/storybook.rs" - -[dependencies] -anyhow.workspace = true -clap = { version = "4.4", features = ["derive", "string"] } -chrono = "0.4" -fs = { path = "../fs" } -futures.workspace = true -gpui2 = { path = "../gpui2" } -itertools = "0.11.0" -log.workspace = true -rust-embed.workspace = true -serde.workspace = true -settings = { path = "../settings" } -simplelog = "0.9" -strum = { version = "0.25.0", features = ["derive"] } -theme = { path = "../theme" } -ui = { path = "../ui" } -util = { path = "../util" } - -[dev-dependencies] -gpui2 = { path = "../gpui2", features = ["test-support"] } diff --git a/crates/storybook/docs/thoughts.md b/crates/storybook/docs/thoughts.md deleted file mode 100644 index 9416f25933..0000000000 --- a/crates/storybook/docs/thoughts.md +++ /dev/null @@ -1,72 +0,0 @@ -Much of element styling is now handled by an external engine. - - -How do I make an element hover. - -There's a hover style. - -Hoverable needs to wrap another element. That element can be styled. - -```rs -struct Hoverable { - -} - -impl Element for Hoverable { - -} - -``` - - - -```rs -#[derive(Styled, Interactive)] -pub struct Div { - declared_style: StyleRefinement, - interactions: Interactions -} - -pub trait Styled { - fn declared_style(&mut self) -> &mut StyleRefinement; - fn compute_style(&mut self) -> Style { - Style::default().refine(self.declared_style()) - } - - // All the tailwind classes, modifying self.declared_style() -} - -impl Style { - pub fn paint_background(layout: Layout, cx: &mut PaintContext); - pub fn paint_foreground(layout: Layout, cx: &mut PaintContext); -} - -pub trait Interactive { - fn interactions(&mut self) -> &mut Interactions; - - fn on_click(self, ) -} - -struct Interactions { - click: SmallVec<[; 1]>, -} - - -``` - - -```rs - - -trait Stylable { - type Style; - - fn with_style(self, style: Self::Style) -> Self; -} - - - - - - -``` diff --git a/crates/storybook/src/stories.rs b/crates/storybook/src/stories.rs deleted file mode 100644 index 95b8844157..0000000000 --- a/crates/storybook/src/stories.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod components; -pub mod elements; -pub mod kitchen_sink; diff --git a/crates/storybook/src/stories/components.rs b/crates/storybook/src/stories/components.rs deleted file mode 100644 index 85d5ce088f..0000000000 --- a/crates/storybook/src/stories/components.rs +++ /dev/null @@ -1,22 +0,0 @@ -pub mod assistant_panel; -pub mod breadcrumb; -pub mod buffer; -pub mod chat_panel; -pub mod collab_panel; -pub mod context_menu; -pub mod facepile; -pub mod keybinding; -pub mod language_selector; -pub mod multi_buffer; -pub mod palette; -pub mod panel; -pub mod project_panel; -pub mod recent_projects; -pub mod status_bar; -pub mod tab; -pub mod tab_bar; -pub mod terminal; -pub mod theme_selector; -pub mod title_bar; -pub mod toolbar; -pub mod traffic_lights; diff --git a/crates/storybook/src/stories/components/assistant_panel.rs b/crates/storybook/src/stories/components/assistant_panel.rs deleted file mode 100644 index 09f964756e..0000000000 --- a/crates/storybook/src/stories/components/assistant_panel.rs +++ /dev/null @@ -1,16 +0,0 @@ -use ui::prelude::*; -use ui::AssistantPanel; - -use crate::story::Story; - -#[derive(Element, Default)] -pub struct AssistantPanelStory {} - -impl AssistantPanelStory { - fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { - Story::container(cx) - .child(Story::title_for::<_, AssistantPanel>(cx)) - .child(Story::label(cx, "Default")) - .child(AssistantPanel::new()) - } -} diff --git a/crates/storybook/src/stories/components/breadcrumb.rs b/crates/storybook/src/stories/components/breadcrumb.rs deleted file mode 100644 index 002b6140e1..0000000000 --- a/crates/storybook/src/stories/components/breadcrumb.rs +++ /dev/null @@ -1,45 +0,0 @@ -use std::path::PathBuf; -use std::str::FromStr; - -use ui::prelude::*; -use ui::{Breadcrumb, HighlightedText, Symbol}; - -use crate::story::Story; - -#[derive(Element, Default)] -pub struct BreadcrumbStory {} - -impl BreadcrumbStory { - fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { - let theme = theme(cx); - - Story::container(cx) - .child(Story::title_for::<_, Breadcrumb>(cx)) - .child(Story::label(cx, "Default")) - .child(Breadcrumb::new( - PathBuf::from_str("crates/ui/src/components/toolbar.rs").unwrap(), - vec![ - Symbol(vec![ - HighlightedText { - text: "impl ".to_string(), - color: HighlightColor::Keyword.hsla(&theme), - }, - HighlightedText { - text: "BreadcrumbStory".to_string(), - color: HighlightColor::Function.hsla(&theme), - }, - ]), - Symbol(vec![ - HighlightedText { - text: "fn ".to_string(), - color: HighlightColor::Keyword.hsla(&theme), - }, - HighlightedText { - text: "render".to_string(), - color: HighlightColor::Function.hsla(&theme), - }, - ]), - ], - )) - } -} diff --git a/crates/storybook/src/stories/components/buffer.rs b/crates/storybook/src/stories/components/buffer.rs deleted file mode 100644 index 0b3268421b..0000000000 --- a/crates/storybook/src/stories/components/buffer.rs +++ /dev/null @@ -1,36 +0,0 @@ -use gpui2::geometry::rems; -use ui::prelude::*; -use ui::{ - empty_buffer_example, hello_world_rust_buffer_example, - hello_world_rust_buffer_with_status_example, Buffer, -}; - -use crate::story::Story; - -#[derive(Element, Default)] -pub struct BufferStory {} - -impl BufferStory { - fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { - let theme = theme(cx); - - Story::container(cx) - .child(Story::title_for::<_, Buffer>(cx)) - .child(Story::label(cx, "Default")) - .child(div().w(rems(64.)).h_96().child(empty_buffer_example())) - .child(Story::label(cx, "Hello World (Rust)")) - .child( - div() - .w(rems(64.)) - .h_96() - .child(hello_world_rust_buffer_example(&theme)), - ) - .child(Story::label(cx, "Hello World (Rust) with Status")) - .child( - div() - .w(rems(64.)) - .h_96() - .child(hello_world_rust_buffer_with_status_example(&theme)), - ) - } -} diff --git a/crates/storybook/src/stories/components/chat_panel.rs b/crates/storybook/src/stories/components/chat_panel.rs deleted file mode 100644 index e87ac0afa2..0000000000 --- a/crates/storybook/src/stories/components/chat_panel.rs +++ /dev/null @@ -1,46 +0,0 @@ -use chrono::DateTime; -use ui::prelude::*; -use ui::{ChatMessage, ChatPanel, Panel}; - -use crate::story::Story; - -#[derive(Element, Default)] -pub struct ChatPanelStory {} - -impl ChatPanelStory { - fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { - Story::container(cx) - .child(Story::title_for::<_, ChatPanel>(cx)) - .child(Story::label(cx, "Default")) - .child(Panel::new( - ScrollState::default(), - |_, _| vec![ChatPanel::new(ScrollState::default()).into_any()], - Box::new(()), - )) - .child(Story::label(cx, "With Mesages")) - .child(Panel::new( - ScrollState::default(), - |_, _| { - vec![ChatPanel::new(ScrollState::default()) - .with_messages(vec![ - ChatMessage::new( - "osiewicz".to_string(), - "is this thing on?".to_string(), - DateTime::parse_from_rfc3339("2023-09-27T15:40:52.707Z") - .unwrap() - .naive_local(), - ), - ChatMessage::new( - "maxdeviant".to_string(), - "Reading you loud and clear!".to_string(), - DateTime::parse_from_rfc3339("2023-09-28T15:40:52.707Z") - .unwrap() - .naive_local(), - ), - ]) - .into_any()] - }, - Box::new(()), - )) - } -} diff --git a/crates/storybook/src/stories/components/collab_panel.rs b/crates/storybook/src/stories/components/collab_panel.rs deleted file mode 100644 index 6a66f0d47f..0000000000 --- a/crates/storybook/src/stories/components/collab_panel.rs +++ /dev/null @@ -1,16 +0,0 @@ -use ui::prelude::*; -use ui::CollabPanel; - -use crate::story::Story; - -#[derive(Element, Default)] -pub struct CollabPanelStory {} - -impl CollabPanelStory { - fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { - Story::container(cx) - .child(Story::title_for::<_, CollabPanel>(cx)) - .child(Story::label(cx, "Default")) - .child(CollabPanel::new(ScrollState::default())) - } -} diff --git a/crates/storybook/src/stories/components/context_menu.rs b/crates/storybook/src/stories/components/context_menu.rs deleted file mode 100644 index 71776ca66a..0000000000 --- a/crates/storybook/src/stories/components/context_menu.rs +++ /dev/null @@ -1,21 +0,0 @@ -use ui::prelude::*; -use ui::{ContextMenu, ContextMenuItem, Label}; - -use crate::story::Story; - -#[derive(Element, Default)] -pub struct ContextMenuStory {} - -impl ContextMenuStory { - fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { - Story::container(cx) - //.fill(theme.middle.base.default.background) - .child(Story::title_for::<_, ContextMenu>(cx)) - .child(Story::label(cx, "Default")) - .child(ContextMenu::new([ - ContextMenuItem::header("Section header"), - ContextMenuItem::Separator, - ContextMenuItem::entry(Label::new("Some entry")), - ])) - } -} diff --git a/crates/storybook/src/stories/components/facepile.rs b/crates/storybook/src/stories/components/facepile.rs deleted file mode 100644 index bbd08ae984..0000000000 --- a/crates/storybook/src/stories/components/facepile.rs +++ /dev/null @@ -1,25 +0,0 @@ -use ui::prelude::*; -use ui::{static_players, Facepile}; - -use crate::story::Story; - -#[derive(Element, Default)] -pub struct FacepileStory {} - -impl FacepileStory { - fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { - let players = static_players(); - - Story::container(cx) - .child(Story::title_for::<_, Facepile>(cx)) - .child(Story::label(cx, "Default")) - .child( - div() - .flex() - .gap_3() - .child(Facepile::new(players.clone().into_iter().take(1))) - .child(Facepile::new(players.clone().into_iter().take(2))) - .child(Facepile::new(players.clone().into_iter().take(3))), - ) - } -} diff --git a/crates/storybook/src/stories/components/keybinding.rs b/crates/storybook/src/stories/components/keybinding.rs deleted file mode 100644 index 1acf59fe3b..0000000000 --- a/crates/storybook/src/stories/components/keybinding.rs +++ /dev/null @@ -1,64 +0,0 @@ -use itertools::Itertools; -use strum::IntoEnumIterator; -use ui::prelude::*; -use ui::{Keybinding, ModifierKey, ModifierKeys}; - -use crate::story::Story; - -#[derive(Element, Default)] -pub struct KeybindingStory {} - -impl KeybindingStory { - fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { - let all_modifier_permutations = ModifierKey::iter().permutations(2); - - Story::container(cx) - .child(Story::title_for::<_, Keybinding>(cx)) - .child(Story::label(cx, "Single Key")) - .child(Keybinding::new("Z".to_string(), ModifierKeys::new())) - .child(Story::label(cx, "Single Key with Modifier")) - .child( - div() - .flex() - .gap_3() - .children(ModifierKey::iter().map(|modifier| { - Keybinding::new("C".to_string(), ModifierKeys::new().add(modifier)) - })), - ) - .child(Story::label(cx, "Single Key with Modifier (Permuted)")) - .child( - div().flex().flex_col().children( - all_modifier_permutations - .chunks(4) - .into_iter() - .map(|chunk| { - div() - .flex() - .gap_4() - .py_3() - .children(chunk.map(|permutation| { - let mut modifiers = ModifierKeys::new(); - - for modifier in permutation { - modifiers = modifiers.add(modifier); - } - - Keybinding::new("X".to_string(), modifiers) - })) - }), - ), - ) - .child(Story::label(cx, "Single Key with All Modifiers")) - .child(Keybinding::new("Z".to_string(), ModifierKeys::all())) - .child(Story::label(cx, "Chord")) - .child(Keybinding::new_chord( - ("A".to_string(), ModifierKeys::new()), - ("Z".to_string(), ModifierKeys::new()), - )) - .child(Story::label(cx, "Chord with Modifier")) - .child(Keybinding::new_chord( - ("A".to_string(), ModifierKeys::new().control(true)), - ("Z".to_string(), ModifierKeys::new().shift(true)), - )) - } -} diff --git a/crates/storybook/src/stories/components/language_selector.rs b/crates/storybook/src/stories/components/language_selector.rs deleted file mode 100644 index c6dbd13d3f..0000000000 --- a/crates/storybook/src/stories/components/language_selector.rs +++ /dev/null @@ -1,16 +0,0 @@ -use ui::prelude::*; -use ui::LanguageSelector; - -use crate::story::Story; - -#[derive(Element, Default)] -pub struct LanguageSelectorStory {} - -impl LanguageSelectorStory { - fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { - Story::container(cx) - .child(Story::title_for::<_, LanguageSelector>(cx)) - .child(Story::label(cx, "Default")) - .child(LanguageSelector::new()) - } -} diff --git a/crates/storybook/src/stories/components/multi_buffer.rs b/crates/storybook/src/stories/components/multi_buffer.rs deleted file mode 100644 index cd760c54dc..0000000000 --- a/crates/storybook/src/stories/components/multi_buffer.rs +++ /dev/null @@ -1,24 +0,0 @@ -use ui::prelude::*; -use ui::{hello_world_rust_buffer_example, MultiBuffer}; - -use crate::story::Story; - -#[derive(Element, Default)] -pub struct MultiBufferStory {} - -impl MultiBufferStory { - fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { - let theme = theme(cx); - - Story::container(cx) - .child(Story::title_for::<_, MultiBuffer>(cx)) - .child(Story::label(cx, "Default")) - .child(MultiBuffer::new(vec![ - hello_world_rust_buffer_example(&theme), - hello_world_rust_buffer_example(&theme), - hello_world_rust_buffer_example(&theme), - hello_world_rust_buffer_example(&theme), - hello_world_rust_buffer_example(&theme), - ])) - } -} diff --git a/crates/storybook/src/stories/components/palette.rs b/crates/storybook/src/stories/components/palette.rs deleted file mode 100644 index d14fac6697..0000000000 --- a/crates/storybook/src/stories/components/palette.rs +++ /dev/null @@ -1,53 +0,0 @@ -use ui::prelude::*; -use ui::{Keybinding, ModifierKeys, Palette, PaletteItem}; - -use crate::story::Story; - -#[derive(Element, Default)] -pub struct PaletteStory {} - -impl PaletteStory { - fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { - Story::container(cx) - .child(Story::title_for::<_, Palette>(cx)) - .child(Story::label(cx, "Default")) - .child(Palette::new(ScrollState::default())) - .child(Story::label(cx, "With Items")) - .child( - Palette::new(ScrollState::default()) - .placeholder("Execute a command...") - .items(vec![ - PaletteItem::new("theme selector: toggle").keybinding( - Keybinding::new_chord( - ("k".to_string(), ModifierKeys::new().command(true)), - ("t".to_string(), ModifierKeys::new().command(true)), - ), - ), - PaletteItem::new("assistant: inline assist").keybinding(Keybinding::new( - "enter".to_string(), - ModifierKeys::new().command(true), - )), - PaletteItem::new("assistant: quote selection").keybinding(Keybinding::new( - ">".to_string(), - ModifierKeys::new().command(true), - )), - PaletteItem::new("assistant: toggle focus").keybinding(Keybinding::new( - "?".to_string(), - ModifierKeys::new().command(true), - )), - PaletteItem::new("auto update: check"), - PaletteItem::new("auto update: view release notes"), - PaletteItem::new("branches: open recent").keybinding(Keybinding::new( - "b".to_string(), - ModifierKeys::new().command(true).alt(true), - )), - PaletteItem::new("chat panel: toggle focus"), - PaletteItem::new("cli: install"), - PaletteItem::new("client: sign in"), - PaletteItem::new("client: sign out"), - PaletteItem::new("editor: cancel") - .keybinding(Keybinding::new("escape".to_string(), ModifierKeys::new())), - ]), - ) - } -} diff --git a/crates/storybook/src/stories/components/panel.rs b/crates/storybook/src/stories/components/panel.rs deleted file mode 100644 index 39a5ceafa2..0000000000 --- a/crates/storybook/src/stories/components/panel.rs +++ /dev/null @@ -1,25 +0,0 @@ -use ui::prelude::*; -use ui::{Label, Panel}; - -use crate::story::Story; - -#[derive(Element, Default)] -pub struct PanelStory {} - -impl PanelStory { - fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { - Story::container(cx) - .child(Story::title_for::<_, Panel>(cx)) - .child(Story::label(cx, "Default")) - .child(Panel::new( - ScrollState::default(), - |_, _| { - vec![div() - .overflow_y_scroll(ScrollState::default()) - .children((0..100).map(|ix| Label::new(format!("Item {}", ix + 1)))) - .into_any()] - }, - Box::new(()), - )) - } -} diff --git a/crates/storybook/src/stories/components/project_panel.rs b/crates/storybook/src/stories/components/project_panel.rs deleted file mode 100644 index cba71cd21a..0000000000 --- a/crates/storybook/src/stories/components/project_panel.rs +++ /dev/null @@ -1,20 +0,0 @@ -use ui::prelude::*; -use ui::{Panel, ProjectPanel}; - -use crate::story::Story; - -#[derive(Element, Default)] -pub struct ProjectPanelStory {} - -impl ProjectPanelStory { - fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { - Story::container(cx) - .child(Story::title_for::<_, ProjectPanel>(cx)) - .child(Story::label(cx, "Default")) - .child(Panel::new( - ScrollState::default(), - |_, _| vec![ProjectPanel::new(ScrollState::default()).into_any()], - Box::new(()), - )) - } -} diff --git a/crates/storybook/src/stories/components/recent_projects.rs b/crates/storybook/src/stories/components/recent_projects.rs deleted file mode 100644 index f924654695..0000000000 --- a/crates/storybook/src/stories/components/recent_projects.rs +++ /dev/null @@ -1,16 +0,0 @@ -use ui::prelude::*; -use ui::RecentProjects; - -use crate::story::Story; - -#[derive(Element, Default)] -pub struct RecentProjectsStory {} - -impl RecentProjectsStory { - fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { - Story::container(cx) - .child(Story::title_for::<_, RecentProjects>(cx)) - .child(Story::label(cx, "Default")) - .child(RecentProjects::new()) - } -} diff --git a/crates/storybook/src/stories/components/status_bar.rs b/crates/storybook/src/stories/components/status_bar.rs deleted file mode 100644 index ed3d047c6d..0000000000 --- a/crates/storybook/src/stories/components/status_bar.rs +++ /dev/null @@ -1,16 +0,0 @@ -use ui::prelude::*; -use ui::StatusBar; - -use crate::story::Story; - -#[derive(Element, Default)] -pub struct StatusBarStory {} - -impl StatusBarStory { - fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { - Story::container(cx) - .child(Story::title_for::<_, StatusBar>(cx)) - .child(Story::label(cx, "Default")) - .child(StatusBar::new()) - } -} diff --git a/crates/storybook/src/stories/components/tab.rs b/crates/storybook/src/stories/components/tab.rs deleted file mode 100644 index 6a154ce644..0000000000 --- a/crates/storybook/src/stories/components/tab.rs +++ /dev/null @@ -1,91 +0,0 @@ -use strum::IntoEnumIterator; -use ui::prelude::*; -use ui::{h_stack, v_stack, Tab}; - -use crate::story::Story; - -#[derive(Element, Default)] -pub struct TabStory {} - -impl TabStory { - fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { - let git_statuses = GitStatus::iter(); - let fs_statuses = FileSystemStatus::iter(); - - Story::container(cx) - .child(Story::title_for::<_, Tab>(cx)) - .child( - h_stack().child( - v_stack() - .gap_2() - .child(Story::label(cx, "Default")) - .child(Tab::new()), - ), - ) - .child( - h_stack().child( - v_stack().gap_2().child(Story::label(cx, "Current")).child( - h_stack() - .gap_4() - .child(Tab::new().title("Current".to_string()).current(true)) - .child(Tab::new().title("Not Current".to_string()).current(false)), - ), - ), - ) - .child( - h_stack().child( - v_stack() - .gap_2() - .child(Story::label(cx, "Titled")) - .child(Tab::new().title("label".to_string())), - ), - ) - .child( - h_stack().child( - v_stack() - .gap_2() - .child(Story::label(cx, "With Icon")) - .child( - Tab::new() - .title("label".to_string()) - .icon(Some(ui::Icon::Envelope)), - ), - ), - ) - .child( - h_stack().child( - v_stack() - .gap_2() - .child(Story::label(cx, "Close Side")) - .child( - h_stack() - .gap_4() - .child( - Tab::new() - .title("Left".to_string()) - .close_side(IconSide::Left), - ) - .child(Tab::new().title("Right".to_string())), - ), - ), - ) - .child( - v_stack() - .gap_2() - .child(Story::label(cx, "Git Status")) - .child(h_stack().gap_4().children(git_statuses.map(|git_status| { - Tab::new() - .title(git_status.to_string()) - .git_status(git_status) - }))), - ) - .child( - v_stack() - .gap_2() - .child(Story::label(cx, "File System Status")) - .child(h_stack().gap_4().children(fs_statuses.map(|fs_status| { - Tab::new().title(fs_status.to_string()).fs_status(fs_status) - }))), - ) - } -} diff --git a/crates/storybook/src/stories/components/tab_bar.rs b/crates/storybook/src/stories/components/tab_bar.rs deleted file mode 100644 index b5fa45dfd6..0000000000 --- a/crates/storybook/src/stories/components/tab_bar.rs +++ /dev/null @@ -1,46 +0,0 @@ -use ui::prelude::*; -use ui::{Tab, TabBar}; - -use crate::story::Story; - -#[derive(Element, Default)] -pub struct TabBarStory {} - -impl TabBarStory { - fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { - Story::container(cx) - .child(Story::title_for::<_, TabBar>(cx)) - .child(Story::label(cx, "Default")) - .child(TabBar::new(vec![ - Tab::new() - .title("Cargo.toml".to_string()) - .current(false) - .git_status(GitStatus::Modified), - Tab::new() - .title("Channels Panel".to_string()) - .current(false), - Tab::new() - .title("channels_panel.rs".to_string()) - .current(true) - .git_status(GitStatus::Modified), - Tab::new() - .title("workspace.rs".to_string()) - .current(false) - .git_status(GitStatus::Modified), - Tab::new() - .title("icon_button.rs".to_string()) - .current(false), - Tab::new() - .title("storybook.rs".to_string()) - .current(false) - .git_status(GitStatus::Created), - Tab::new().title("theme.rs".to_string()).current(false), - Tab::new() - .title("theme_registry.rs".to_string()) - .current(false), - Tab::new() - .title("styleable_helpers.rs".to_string()) - .current(false), - ])) - } -} diff --git a/crates/storybook/src/stories/components/terminal.rs b/crates/storybook/src/stories/components/terminal.rs deleted file mode 100644 index 2bce2e27e5..0000000000 --- a/crates/storybook/src/stories/components/terminal.rs +++ /dev/null @@ -1,16 +0,0 @@ -use ui::prelude::*; -use ui::Terminal; - -use crate::story::Story; - -#[derive(Element, Default)] -pub struct TerminalStory {} - -impl TerminalStory { - fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { - Story::container(cx) - .child(Story::title_for::<_, Terminal>(cx)) - .child(Story::label(cx, "Default")) - .child(Terminal::new()) - } -} diff --git a/crates/storybook/src/stories/components/theme_selector.rs b/crates/storybook/src/stories/components/theme_selector.rs deleted file mode 100644 index 43e2a704e7..0000000000 --- a/crates/storybook/src/stories/components/theme_selector.rs +++ /dev/null @@ -1,16 +0,0 @@ -use ui::prelude::*; -use ui::ThemeSelector; - -use crate::story::Story; - -#[derive(Element, Default)] -pub struct ThemeSelectorStory {} - -impl ThemeSelectorStory { - fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { - Story::container(cx) - .child(Story::title_for::<_, ThemeSelector>(cx)) - .child(Story::label(cx, "Default")) - .child(ThemeSelector::new()) - } -} diff --git a/crates/storybook/src/stories/components/title_bar.rs b/crates/storybook/src/stories/components/title_bar.rs deleted file mode 100644 index 3c4682b3ba..0000000000 --- a/crates/storybook/src/stories/components/title_bar.rs +++ /dev/null @@ -1,16 +0,0 @@ -use ui::prelude::*; -use ui::TitleBar; - -use crate::story::Story; - -#[derive(Element, Default)] -pub struct TitleBarStory {} - -impl TitleBarStory { - fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { - Story::container(cx) - .child(Story::title_for::<_, TitleBar>(cx)) - .child(Story::label(cx, "Default")) - .child(TitleBar::new(cx)) - } -} diff --git a/crates/storybook/src/stories/components/toolbar.rs b/crates/storybook/src/stories/components/toolbar.rs deleted file mode 100644 index 1413c463b4..0000000000 --- a/crates/storybook/src/stories/components/toolbar.rs +++ /dev/null @@ -1,70 +0,0 @@ -use std::path::PathBuf; -use std::str::FromStr; -use std::sync::Arc; - -use ui::prelude::*; -use ui::{theme, Breadcrumb, HighlightColor, HighlightedText, Icon, IconButton, Symbol, Toolbar}; - -use crate::story::Story; - -#[derive(Element, Default)] -pub struct ToolbarStory {} - -impl ToolbarStory { - fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { - let theme = theme(cx); - - struct LeftItemsPayload { - pub theme: Arc, - } - - Story::container(cx) - .child(Story::title_for::<_, Toolbar>(cx)) - .child(Story::label(cx, "Default")) - .child(Toolbar::new( - |_, payload| { - let payload = payload.downcast_ref::().unwrap(); - - let theme = payload.theme.clone(); - - vec![Breadcrumb::new( - PathBuf::from_str("crates/ui/src/components/toolbar.rs").unwrap(), - vec![ - Symbol(vec![ - HighlightedText { - text: "impl ".to_string(), - color: HighlightColor::Keyword.hsla(&theme), - }, - HighlightedText { - text: "ToolbarStory".to_string(), - color: HighlightColor::Function.hsla(&theme), - }, - ]), - Symbol(vec![ - HighlightedText { - text: "fn ".to_string(), - color: HighlightColor::Keyword.hsla(&theme), - }, - HighlightedText { - text: "render".to_string(), - color: HighlightColor::Function.hsla(&theme), - }, - ]), - ], - ) - .into_any()] - }, - Box::new(LeftItemsPayload { - theme: theme.clone(), - }), - |_, _| { - vec![ - IconButton::new(Icon::InlayHint).into_any(), - IconButton::new(Icon::MagnifyingGlass).into_any(), - IconButton::new(Icon::MagicWand).into_any(), - ] - }, - Box::new(()), - )) - } -} diff --git a/crates/storybook/src/stories/components/traffic_lights.rs b/crates/storybook/src/stories/components/traffic_lights.rs deleted file mode 100644 index 3b759a43a3..0000000000 --- a/crates/storybook/src/stories/components/traffic_lights.rs +++ /dev/null @@ -1,18 +0,0 @@ -use ui::prelude::*; -use ui::TrafficLights; - -use crate::story::Story; - -#[derive(Element, Default)] -pub struct TrafficLightsStory {} - -impl TrafficLightsStory { - fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { - Story::container(cx) - .child(Story::title_for::<_, TrafficLights>(cx)) - .child(Story::label(cx, "Default")) - .child(TrafficLights::new()) - .child(Story::label(cx, "Unfocused")) - .child(TrafficLights::new().window_has_focus(false)) - } -} diff --git a/crates/storybook/src/stories/elements.rs b/crates/storybook/src/stories/elements.rs deleted file mode 100644 index f7afec4d88..0000000000 --- a/crates/storybook/src/stories/elements.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub mod avatar; -pub mod button; -pub mod icon; -pub mod input; -pub mod label; diff --git a/crates/storybook/src/stories/elements/avatar.rs b/crates/storybook/src/stories/elements/avatar.rs deleted file mode 100644 index a277fa6a1e..0000000000 --- a/crates/storybook/src/stories/elements/avatar.rs +++ /dev/null @@ -1,23 +0,0 @@ -use ui::prelude::*; -use ui::Avatar; - -use crate::story::Story; - -#[derive(Element, Default)] -pub struct AvatarStory {} - -impl AvatarStory { - fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { - Story::container(cx) - .child(Story::title_for::<_, Avatar>(cx)) - .child(Story::label(cx, "Default")) - .child(Avatar::new( - "https://avatars.githubusercontent.com/u/1714999?v=4", - )) - .child(Story::label(cx, "Rounded rectangle")) - .child( - Avatar::new("https://avatars.githubusercontent.com/u/1714999?v=4") - .shape(Shape::RoundedRectangle), - ) - } -} diff --git a/crates/storybook/src/stories/elements/button.rs b/crates/storybook/src/stories/elements/button.rs deleted file mode 100644 index 7ff3fdd30c..0000000000 --- a/crates/storybook/src/stories/elements/button.rs +++ /dev/null @@ -1,192 +0,0 @@ -use gpui2::elements::div; -use gpui2::geometry::rems; -use gpui2::{Element, IntoElement, ViewContext}; -use strum::IntoEnumIterator; -use ui::prelude::*; -use ui::{h_stack, v_stack, Button, Icon, IconPosition, Label}; - -use crate::story::Story; - -#[derive(Element, Default)] -pub struct ButtonStory {} - -impl ButtonStory { - fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { - let states = InteractionState::iter(); - - Story::container(cx) - .child(Story::title_for::<_, Button>(cx)) - .child( - div() - .flex() - .gap_8() - .child( - div() - .child(Story::label(cx, "Ghost (Default)")) - .child(h_stack().gap_2().children(states.clone().map(|state| { - v_stack() - .gap_1() - .child( - Label::new(state.to_string()) - .color(ui::LabelColor::Muted) - .size(ui::LabelSize::Small), - ) - .child( - Button::new("Label") - .variant(ButtonVariant::Ghost) - .state(state), - ) - }))) - .child(Story::label(cx, "Ghost – Left Icon")) - .child(h_stack().gap_2().children(states.clone().map(|state| { - v_stack() - .gap_1() - .child( - Label::new(state.to_string()) - .color(ui::LabelColor::Muted) - .size(ui::LabelSize::Small), - ) - .child( - Button::new("Label") - .variant(ButtonVariant::Ghost) - .icon(Icon::Plus) - .icon_position(IconPosition::Left) - .state(state), - ) - }))) - .child(Story::label(cx, "Ghost – Right Icon")) - .child(h_stack().gap_2().children(states.clone().map(|state| { - v_stack() - .gap_1() - .child( - Label::new(state.to_string()) - .color(ui::LabelColor::Muted) - .size(ui::LabelSize::Small), - ) - .child( - Button::new("Label") - .variant(ButtonVariant::Ghost) - .icon(Icon::Plus) - .icon_position(IconPosition::Right) - .state(state), - ) - }))), - ) - .child( - div() - .child(Story::label(cx, "Filled")) - .child(h_stack().gap_2().children(states.clone().map(|state| { - v_stack() - .gap_1() - .child( - Label::new(state.to_string()) - .color(ui::LabelColor::Muted) - .size(ui::LabelSize::Small), - ) - .child( - Button::new("Label") - .variant(ButtonVariant::Filled) - .state(state), - ) - }))) - .child(Story::label(cx, "Filled – Left Button")) - .child(h_stack().gap_2().children(states.clone().map(|state| { - v_stack() - .gap_1() - .child( - Label::new(state.to_string()) - .color(ui::LabelColor::Muted) - .size(ui::LabelSize::Small), - ) - .child( - Button::new("Label") - .variant(ButtonVariant::Filled) - .icon(Icon::Plus) - .icon_position(IconPosition::Left) - .state(state), - ) - }))) - .child(Story::label(cx, "Filled – Right Button")) - .child(h_stack().gap_2().children(states.clone().map(|state| { - v_stack() - .gap_1() - .child( - Label::new(state.to_string()) - .color(ui::LabelColor::Muted) - .size(ui::LabelSize::Small), - ) - .child( - Button::new("Label") - .variant(ButtonVariant::Filled) - .icon(Icon::Plus) - .icon_position(IconPosition::Right) - .state(state), - ) - }))), - ) - .child( - div() - .child(Story::label(cx, "Fixed With")) - .child(h_stack().gap_2().children(states.clone().map(|state| { - v_stack() - .gap_1() - .child( - Label::new(state.to_string()) - .color(ui::LabelColor::Muted) - .size(ui::LabelSize::Small), - ) - .child( - Button::new("Label") - .variant(ButtonVariant::Filled) - .state(state) - .width(Some(rems(6.).into())), - ) - }))) - .child(Story::label(cx, "Fixed With – Left Icon")) - .child(h_stack().gap_2().children(states.clone().map(|state| { - v_stack() - .gap_1() - .child( - Label::new(state.to_string()) - .color(ui::LabelColor::Muted) - .size(ui::LabelSize::Small), - ) - .child( - Button::new("Label") - .variant(ButtonVariant::Filled) - .state(state) - .icon(Icon::Plus) - .icon_position(IconPosition::Left) - .width(Some(rems(6.).into())), - ) - }))) - .child(Story::label(cx, "Fixed With – Right Icon")) - .child(h_stack().gap_2().children(states.clone().map(|state| { - v_stack() - .gap_1() - .child( - Label::new(state.to_string()) - .color(ui::LabelColor::Muted) - .size(ui::LabelSize::Small), - ) - .child( - Button::new("Label") - .variant(ButtonVariant::Filled) - .state(state) - .icon(Icon::Plus) - .icon_position(IconPosition::Right) - .width(Some(rems(6.).into())), - ) - }))), - ), - ) - .child(Story::label(cx, "Button with `on_click`")) - .child( - Button::new("Label") - .variant(ButtonVariant::Ghost) - // NOTE: There currently appears to be a bug in GPUI2 where only the last event handler will fire. - // So adding additional buttons with `on_click`s after this one will cause this `on_click` to not fire. - .on_click(|_view, _cx| println!("Button clicked.")), - ) - } -} diff --git a/crates/storybook/src/stories/elements/icon.rs b/crates/storybook/src/stories/elements/icon.rs deleted file mode 100644 index 66d3abc0b3..0000000000 --- a/crates/storybook/src/stories/elements/icon.rs +++ /dev/null @@ -1,19 +0,0 @@ -use strum::IntoEnumIterator; -use ui::prelude::*; -use ui::{Icon, IconElement}; - -use crate::story::Story; - -#[derive(Element, Default)] -pub struct IconStory {} - -impl IconStory { - fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { - let icons = Icon::iter(); - - Story::container(cx) - .child(Story::title_for::<_, IconElement>(cx)) - .child(Story::label(cx, "All Icons")) - .child(div().flex().gap_3().children(icons.map(IconElement::new))) - } -} diff --git a/crates/storybook/src/stories/elements/input.rs b/crates/storybook/src/stories/elements/input.rs deleted file mode 100644 index 8617b7daaf..0000000000 --- a/crates/storybook/src/stories/elements/input.rs +++ /dev/null @@ -1,16 +0,0 @@ -use ui::prelude::*; -use ui::Input; - -use crate::story::Story; - -#[derive(Element, Default)] -pub struct InputStory {} - -impl InputStory { - fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { - Story::container(cx) - .child(Story::title_for::<_, Input>(cx)) - .child(Story::label(cx, "Default")) - .child(div().flex().child(Input::new("Search"))) - } -} diff --git a/crates/storybook/src/stories/elements/label.rs b/crates/storybook/src/stories/elements/label.rs deleted file mode 100644 index 1b63cb3a3a..0000000000 --- a/crates/storybook/src/stories/elements/label.rs +++ /dev/null @@ -1,18 +0,0 @@ -use ui::prelude::*; -use ui::Label; - -use crate::story::Story; - -#[derive(Element, Default)] -pub struct LabelStory {} - -impl LabelStory { - fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { - Story::container(cx) - .child(Story::title_for::<_, Label>(cx)) - .child(Story::label(cx, "Default")) - .child(Label::new("Hello, world!")) - .child(Story::label(cx, "Highlighted")) - .child(Label::new("Hello, world!").with_highlights(vec![0, 1, 2, 7, 8, 12])) - } -} diff --git a/crates/storybook/src/stories/kitchen_sink.rs b/crates/storybook/src/stories/kitchen_sink.rs deleted file mode 100644 index ae826f934e..0000000000 --- a/crates/storybook/src/stories/kitchen_sink.rs +++ /dev/null @@ -1,26 +0,0 @@ -use strum::IntoEnumIterator; -use ui::prelude::*; - -use crate::story::Story; -use crate::story_selector::{ComponentStory, ElementStory}; - -#[derive(Element, Default)] -pub struct KitchenSinkStory {} - -impl KitchenSinkStory { - fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { - let element_stories = ElementStory::iter().map(|selector| selector.story()); - let component_stories = ComponentStory::iter().map(|selector| selector.story()); - - Story::container(cx) - .overflow_y_scroll(ScrollState::default()) - .child(Story::title(cx, "Kitchen Sink")) - .child(Story::label(cx, "Elements")) - .child(div().flex().flex_col().children_any(element_stories)) - .child(Story::label(cx, "Components")) - .child(div().flex().flex_col().children_any(component_stories)) - // Add a bit of space at the bottom of the kitchen sink so elements - // don't end up squished right up against the bottom of the screen. - .child(div().p_4()) - } -} diff --git a/crates/storybook/src/story.rs b/crates/storybook/src/story.rs deleted file mode 100644 index 16eae50f88..0000000000 --- a/crates/storybook/src/story.rs +++ /dev/null @@ -1,44 +0,0 @@ -use gpui2::elements::div::Div; -use ui::prelude::*; -use ui::theme; - -pub struct Story {} - -impl Story { - pub fn container(cx: &mut ViewContext) -> Div { - let theme = theme(cx); - - div() - .size_full() - .flex() - .flex_col() - .pt_2() - .px_4() - .font("Zed Mono Extended") - .fill(theme.lowest.base.default.background) - } - - pub fn title(cx: &mut ViewContext, title: &str) -> impl Element { - let theme = theme(cx); - - div() - .text_xl() - .text_color(theme.lowest.base.default.foreground) - .child(title.to_owned()) - } - - pub fn title_for(cx: &mut ViewContext) -> impl Element { - Self::title(cx, std::any::type_name::()) - } - - pub fn label(cx: &mut ViewContext, label: &str) -> impl Element { - let theme = theme(cx); - - div() - .mt_4() - .mb_2() - .text_xs() - .text_color(theme.lowest.base.default.foreground) - .child(label.to_owned()) - } -} diff --git a/crates/storybook/src/story_selector.rs b/crates/storybook/src/story_selector.rs deleted file mode 100644 index 6b0a9ac78d..0000000000 --- a/crates/storybook/src/story_selector.rs +++ /dev/null @@ -1,178 +0,0 @@ -use std::str::FromStr; -use std::sync::OnceLock; - -use anyhow::{anyhow, Context}; -use clap::builder::PossibleValue; -use clap::ValueEnum; -use gpui2::{AnyElement, Element}; -use strum::{EnumIter, EnumString, IntoEnumIterator}; - -#[derive(Debug, PartialEq, Eq, Clone, Copy, strum::Display, EnumString, EnumIter)] -#[strum(serialize_all = "snake_case")] -pub enum ElementStory { - Avatar, - Button, - Icon, - Input, - Label, -} - -impl ElementStory { - pub fn story(&self) -> AnyElement { - use crate::stories::elements; - - match self { - Self::Avatar => elements::avatar::AvatarStory::default().into_any(), - Self::Button => elements::button::ButtonStory::default().into_any(), - Self::Icon => elements::icon::IconStory::default().into_any(), - Self::Input => elements::input::InputStory::default().into_any(), - Self::Label => elements::label::LabelStory::default().into_any(), - } - } -} - -#[derive(Debug, PartialEq, Eq, Clone, Copy, strum::Display, EnumString, EnumIter)] -#[strum(serialize_all = "snake_case")] -pub enum ComponentStory { - AssistantPanel, - Breadcrumb, - Buffer, - ContextMenu, - ChatPanel, - CollabPanel, - Facepile, - Keybinding, - LanguageSelector, - MultiBuffer, - Palette, - Panel, - ProjectPanel, - RecentProjects, - StatusBar, - Tab, - TabBar, - Terminal, - ThemeSelector, - TitleBar, - Toolbar, - TrafficLights, -} - -impl ComponentStory { - pub fn story(&self) -> AnyElement { - use crate::stories::components; - - match self { - Self::AssistantPanel => { - components::assistant_panel::AssistantPanelStory::default().into_any() - } - Self::Breadcrumb => components::breadcrumb::BreadcrumbStory::default().into_any(), - Self::Buffer => components::buffer::BufferStory::default().into_any(), - Self::ContextMenu => components::context_menu::ContextMenuStory::default().into_any(), - Self::ChatPanel => components::chat_panel::ChatPanelStory::default().into_any(), - Self::CollabPanel => components::collab_panel::CollabPanelStory::default().into_any(), - Self::Facepile => components::facepile::FacepileStory::default().into_any(), - Self::Keybinding => components::keybinding::KeybindingStory::default().into_any(), - Self::LanguageSelector => { - components::language_selector::LanguageSelectorStory::default().into_any() - } - Self::MultiBuffer => components::multi_buffer::MultiBufferStory::default().into_any(), - Self::Palette => components::palette::PaletteStory::default().into_any(), - Self::Panel => components::panel::PanelStory::default().into_any(), - Self::ProjectPanel => { - components::project_panel::ProjectPanelStory::default().into_any() - } - Self::RecentProjects => { - components::recent_projects::RecentProjectsStory::default().into_any() - } - Self::StatusBar => components::status_bar::StatusBarStory::default().into_any(), - Self::Tab => components::tab::TabStory::default().into_any(), - Self::TabBar => components::tab_bar::TabBarStory::default().into_any(), - Self::Terminal => components::terminal::TerminalStory::default().into_any(), - Self::ThemeSelector => { - components::theme_selector::ThemeSelectorStory::default().into_any() - } - Self::TitleBar => components::title_bar::TitleBarStory::default().into_any(), - Self::Toolbar => components::toolbar::ToolbarStory::default().into_any(), - Self::TrafficLights => { - components::traffic_lights::TrafficLightsStory::default().into_any() - } - } - } -} - -#[derive(Debug, PartialEq, Eq, Clone, Copy)] -pub enum StorySelector { - Element(ElementStory), - Component(ComponentStory), - KitchenSink, -} - -impl FromStr for StorySelector { - type Err = anyhow::Error; - - fn from_str(raw_story_name: &str) -> std::result::Result { - let story = raw_story_name.to_ascii_lowercase(); - - if story == "kitchen_sink" { - return Ok(Self::KitchenSink); - } - - if let Some((_, story)) = story.split_once("elements/") { - let element_story = ElementStory::from_str(story) - .with_context(|| format!("story not found for element '{story}'"))?; - - return Ok(Self::Element(element_story)); - } - - if let Some((_, story)) = story.split_once("components/") { - let component_story = ComponentStory::from_str(story) - .with_context(|| format!("story not found for component '{story}'"))?; - - return Ok(Self::Component(component_story)); - } - - Err(anyhow!("story not found for '{raw_story_name}'")) - } -} - -impl StorySelector { - pub fn story(&self) -> AnyElement { - match self { - Self::Element(element_story) => element_story.story(), - Self::Component(component_story) => component_story.story(), - Self::KitchenSink => { - crate::stories::kitchen_sink::KitchenSinkStory::default().into_any() - } - } - } -} - -/// The list of all stories available in the storybook. -static ALL_STORY_SELECTORS: OnceLock> = OnceLock::new(); - -impl ValueEnum for StorySelector { - fn value_variants<'a>() -> &'a [Self] { - let stories = ALL_STORY_SELECTORS.get_or_init(|| { - let element_stories = ElementStory::iter().map(StorySelector::Element); - let component_stories = ComponentStory::iter().map(StorySelector::Component); - - element_stories - .chain(component_stories) - .chain(std::iter::once(StorySelector::KitchenSink)) - .collect::>() - }); - - stories - } - - fn to_possible_value(&self) -> Option { - let value = match self { - Self::Element(story) => format!("elements/{story}"), - Self::Component(story) => format!("components/{story}"), - Self::KitchenSink => "kitchen_sink".to_string(), - }; - - Some(PossibleValue::new(value)) - } -} diff --git a/crates/storybook/src/storybook.rs b/crates/storybook/src/storybook.rs deleted file mode 100644 index afae0d5ebe..0000000000 --- a/crates/storybook/src/storybook.rs +++ /dev/null @@ -1,198 +0,0 @@ -#![allow(dead_code, unused_variables)] - -mod stories; -mod story; -mod story_selector; - -use std::{process::Command, sync::Arc}; - -use ::theme as legacy_theme; -use clap::Parser; -use gpui2::{ - serde_json, vec2f, view, Element, IntoElement, ParentElement, RectF, ViewContext, WindowBounds, -}; -use legacy_theme::{ThemeRegistry, ThemeSettings}; -use log::LevelFilter; -use settings::{default_settings, SettingsStore}; -use simplelog::SimpleLogger; -use ui::prelude::*; -use ui::{ElementExt, Theme, WorkspaceElement}; - -use crate::story_selector::StorySelector; - -gpui2::actions! { - storybook, - [ToggleInspector] -} - -#[derive(Parser)] -#[command(author, version, about, long_about = None)] -struct Args { - #[arg(value_enum)] - story: Option, - - /// The name of the theme to use in the storybook. - /// - /// If not provided, the default theme will be used. - #[arg(long)] - theme: Option, -} - -async fn watch_zed_changes(fs: Arc) -> Option<()> { - if std::env::var("ZED_HOT_RELOAD").is_err() { - return None; - } - use futures::StreamExt; - let mut events = fs - .watch(".".as_ref(), std::time::Duration::from_millis(100)) - .await; - let mut current_child: Option = None; - while let Some(events) = events.next().await { - if !events.iter().any(|event| { - event - .path - .to_str() - .map(|path| path.contains("/crates/")) - .unwrap_or_default() - }) { - continue; - } - let child = current_child.take().map(|mut child| child.kill()); - log::info!("Storybook changed, rebuilding..."); - current_child = Some( - Command::new("cargo") - .args(["run", "-p", "storybook"]) - .spawn() - .ok()?, - ); - } - Some(()) -} - -fn main() { - SimpleLogger::init(LevelFilter::Info, Default::default()).expect("could not initialize logger"); - - let args = Args::parse(); - - let fs = Arc::new(fs::RealFs); - - gpui2::App::new(Assets).unwrap().run(move |cx| { - let mut store = SettingsStore::default(); - store - .set_default_settings(default_settings().as_ref(), cx) - .unwrap(); - cx.set_global(store); - legacy_theme::init(Assets, cx); - // load_embedded_fonts(cx.platform().as_ref()); - - let theme_registry = cx.global::>(); - - let theme_override = args - .theme - .and_then(|theme| { - theme_registry - .list_names(true) - .find(|known_theme| theme == *known_theme) - }) - .and_then(|theme_name| theme_registry.get(&theme_name).ok()); - - cx.spawn(|_| async move { - watch_zed_changes(fs).await; - }) - .detach(); - cx.add_window( - gpui2::WindowOptions { - bounds: WindowBounds::Fixed(RectF::new(vec2f(0., 0.), vec2f(1700., 980.))), - center: true, - ..Default::default() - }, - |cx| match args.story { - Some(selector) => view(move |cx| { - render_story( - &mut ViewContext::new(cx), - theme_override.clone(), - div().flex().flex_col().h_full().child_any(selector.story()), - ) - }), - None => view(move |cx| { - render_story( - &mut ViewContext::new(cx), - theme_override.clone(), - WorkspaceElement::default(), - ) - }), - }, - ); - cx.platform().activate(true); - }); -} - -fn render_story>( - cx: &mut ViewContext, - theme_override: Option>, - story: S, -) -> impl Element { - let theme = current_theme(cx, theme_override); - - story.into_element().themed(theme) -} - -fn current_theme( - cx: &mut ViewContext, - theme_override: Option>, -) -> Theme { - let legacy_theme = - theme_override.unwrap_or_else(|| settings::get::(cx).theme.clone()); - - let new_theme: Theme = serde_json::from_value(legacy_theme.base_theme.clone()).unwrap(); - - add_base_theme_to_legacy_theme(&legacy_theme, new_theme) -} - -// Nathan: During the transition to gpui2, we will include the base theme on the legacy Theme struct. -fn add_base_theme_to_legacy_theme(legacy_theme: &legacy_theme::Theme, new_theme: Theme) -> Theme { - legacy_theme - .deserialized_base_theme - .lock() - .get_or_insert_with(|| Box::new(new_theme)) - .downcast_ref::() - .unwrap() - .clone() -} - -use anyhow::{anyhow, Result}; -use gpui2::AssetSource; -use rust_embed::RustEmbed; - -#[derive(RustEmbed)] -#[folder = "../../assets"] -#[include = "themes/**/*"] -#[include = "fonts/**/*"] -#[include = "icons/**/*"] -#[exclude = "*.DS_Store"] -pub struct Assets; - -impl AssetSource for Assets { - fn load(&self, path: &str) -> Result> { - Self::get(path) - .map(|f| f.data) - .ok_or_else(|| anyhow!("could not find asset at path \"{}\"", path)) - } - - fn list(&self, path: &str) -> Vec> { - Self::iter().filter(|p| p.starts_with(path)).collect() - } -} - -// fn load_embedded_fonts(platform: &dyn gpui2::Platform) { -// let font_paths = Assets.list("fonts"); -// let mut embedded_fonts = Vec::new(); -// for font_path in &font_paths { -// if font_path.ends_with(".ttf") { -// let font_path = &*font_path; -// let font_bytes = Assets.load(font_path).unwrap().to_vec(); -// embedded_fonts.push(Arc::from(font_bytes)); -// } -// } -// platform.fonts().add_fonts(&embedded_fonts).unwrap(); -// } diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index cd939b5604..5a13efd07a 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -150,11 +150,14 @@ impl TerminalView { cx.notify(); cx.emit(Event::Wakeup); } + Event::Bell => { this.has_bell = true; cx.emit(Event::Wakeup); } + Event::BlinkChanged => this.blinking_on = !this.blinking_on, + Event::TitleChanged => { if let Some(foreground_info) = &this.terminal().read(cx).foreground_process_info { let cwd = foreground_info.cwd.clone(); @@ -171,6 +174,7 @@ impl TerminalView { .detach(); } } + Event::NewNavigationTarget(maybe_navigation_target) => { this.can_navigate_to_selected_word = match maybe_navigation_target { Some(MaybeNavigationTarget::Url(_)) => true, @@ -180,8 +184,10 @@ impl TerminalView { None => false, } } + Event::Open(maybe_navigation_target) => match maybe_navigation_target { MaybeNavigationTarget::Url(url) => cx.platform().open_url(url), + MaybeNavigationTarget::PathLike(maybe_path) => { if !this.can_navigate_to_selected_word { return; @@ -246,6 +252,7 @@ impl TerminalView { } } }, + _ => cx.emit(event.clone()), }) .detach(); diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index e534ba4260..bfa52fd315 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -286,6 +286,8 @@ pub struct TabbedModal { pub header: ContainerStyle, pub body: ContainerStyle, pub title: ContainedText, + pub visibility_toggle: Interactive, + pub channel_link: Interactive, pub picker: Picker, pub max_height: f32, pub max_width: f32, @@ -867,9 +869,13 @@ pub struct AutocompleteStyle { pub selected_item: ContainerStyle, pub hovered_item: ContainerStyle, pub match_highlight: HighlightStyle, - pub server_name_container: ContainerStyle, - pub server_name_color: Color, - pub server_name_size_percent: f32, + pub completion_min_width: f32, + pub completion_max_width: f32, + pub inline_docs_container: ContainerStyle, + pub inline_docs_color: Color, + pub inline_docs_size_percent: f32, + pub alongside_docs_max_width: f32, + pub alongside_docs_container: ContainerStyle, } #[derive(Clone, Copy, Default, Deserialize, JsonSchema)] @@ -1195,6 +1201,15 @@ pub struct InlineAssistantStyle { pub disabled_editor: FieldEditor, pub pending_edit_background: Color, pub include_conversation: ToggleIconButtonStyle, + pub retrieve_context: ToggleIconButtonStyle, + pub context_status: ContextStatusStyle, +} + +#[derive(Clone, Deserialize, Default, JsonSchema)] +pub struct ContextStatusStyle { + pub error_icon: Icon, + pub in_progress_icon: Icon, + pub complete_icon: Icon, } #[derive(Clone, Deserialize, Default, JsonSchema)] diff --git a/crates/ui/Cargo.toml b/crates/ui/Cargo.toml deleted file mode 100644 index 7bd9d912a0..0000000000 --- a/crates/ui/Cargo.toml +++ /dev/null @@ -1,16 +0,0 @@ -[package] -name = "ui" -version = "0.1.0" -edition = "2021" -publish = false - -[dependencies] -anyhow.workspace = true -chrono = "0.4" -gpui2 = { path = "../gpui2" } -serde.workspace = true -settings = { path = "../settings" } -smallvec.workspace = true -strum = { version = "0.25.0", features = ["derive"] } -theme = { path = "../theme" } -rand = "0.8" diff --git a/crates/ui/docs/_project.md b/crates/ui/docs/_project.md deleted file mode 100644 index a3b72a9d61..0000000000 --- a/crates/ui/docs/_project.md +++ /dev/null @@ -1,13 +0,0 @@ -## Project Plan - -- Port existing UI to GPUI2 -- Update UI in places that GPUI1 was limiting us* -- Understand the needs &/|| struggles the engineers have been having with building UI in the past and address as many of those as possible as we go -- Ship a simple, straightforward system with documentation that is easy to use to build UI - -## Component Classification - -To simplify the understanding of components and minimize unnecessary cognitive load, let's categorize components into two types: - -- An element refers to a standalone component that doesn't import any other 'ui' components. -- A component indicates a component that utilizes or imports other 'ui' components. diff --git a/crates/ui/docs/elevation.md b/crates/ui/docs/elevation.md deleted file mode 100644 index bd34de3396..0000000000 --- a/crates/ui/docs/elevation.md +++ /dev/null @@ -1,57 +0,0 @@ -# Elevation - -Elevation in Zed applies to all surfaces and components. Elevation is categorized into levels. - -Elevation accomplishes the following: -- Allows surfaces to move in front of or behind others, such as content scrolling beneath app top bars. -- Reflects spatial relationships, for instance, how a floating action button’s shadow intimates its disconnection from a collection of cards. -- Directs attention to structures at the highest elevation, like a temporary dialog arising in front of other surfaces. - -Elevations are the initial elevation values assigned to components by default. - -Components may transition to a higher elevation in some cases, like user interations. - -On such occasions, components transition to predetermined dynamic elevation offsets. These are the typical elevations to which components move when they are not at rest. - -## Understanding Elevation - -Elevation can be thought of as the physical closeness of an element to the user. Elements with lower elevations are physically further away from the user on the z-axis and appear to be underneath elements with higher elevations. - -Material Design 3 has a some great visualizations of elevation that may be helpful to understanding the mental modal of elevation. [Material Design – Elevation](https://m3.material.io/styles/elevation/overview) - -## Elevation Levels - -Zed integrates six unique elevation levels in its design system. The elevation of a surface is expressed as a whole number ranging from 0 to 5, both numbers inclusive. A component’s elevation is ascertained by combining the component’s resting elevation with any dynamic elevation offsets. - -The levels are detailed as follows: - -0. App Background -1. UI Surface -2. Elevated Elements -3. Wash -4. Focused Element -5. Dragged Element - -### 0. App Background - -The app background constitutes the lowest elevation layer, appearing behind all other surfaces and components. It is predominantly used for the background color of the app. - -### 1. UI Surface - -The UI Surface is the standard elevation for components and is placed above the app background. It is generally used for the background color of the app bar, card, and sheet. - -### 2. Elevated Elements - -Elevated elements appear above the UI surface layer surfaces and components. Elevated elements are predominantly used for creating popovers, context menus, and tooltips. - -### 3. Wash - -Wash denotes a distinct elevation reserved to isolate app UI layers from high elevation components such as modals, notifications, and overlaid panels. The wash may not consistently be visible when these components are active. This layer is often referred to as a scrim or overlay and the background color of the wash is typically deployed in its design. - -### 4. Focused Element - -Focused elements obtain a higher elevation above surfaces and components at wash elevation. They are often used for modals, notifications, and overlaid panels and indicate that they are the sole element the user is interacting with at the moment. - -### 5. Dragged Element - -Dragged elements gain the highest elevation, thus appearing above surfaces and components at the elevation of focused elements. These are typically used for elements that are being dragged, following the cursor diff --git a/crates/ui/src/children.rs b/crates/ui/src/children.rs deleted file mode 100644 index 947f8d98cf..0000000000 --- a/crates/ui/src/children.rs +++ /dev/null @@ -1,7 +0,0 @@ -use std::any::Any; - -use gpui2::{AnyElement, ViewContext}; - -pub type HackyChildren = fn(&mut ViewContext, &dyn Any) -> Vec>; - -pub type HackyChildrenPayload = Box; diff --git a/crates/ui/src/components.rs b/crates/ui/src/components.rs deleted file mode 100644 index 65b0218565..0000000000 --- a/crates/ui/src/components.rs +++ /dev/null @@ -1,163 +0,0 @@ -mod assistant_panel; -mod breadcrumb; -mod buffer; -mod chat_panel; -mod collab_panel; -mod command_palette; -mod context_menu; -mod editor_pane; -mod facepile; -mod icon_button; -mod keybinding; -mod language_selector; -mod list; -mod multi_buffer; -mod palette; -mod panel; -mod panes; -mod player_stack; -mod project_panel; -mod recent_projects; -mod status_bar; -mod tab; -mod tab_bar; -mod terminal; -mod theme_selector; -mod title_bar; -mod toast; -mod toolbar; -mod traffic_lights; -mod workspace; - -pub use assistant_panel::*; -pub use breadcrumb::*; -pub use buffer::*; -pub use chat_panel::*; -pub use collab_panel::*; -pub use command_palette::*; -pub use context_menu::*; -pub use editor_pane::*; -pub use facepile::*; -pub use icon_button::*; -pub use keybinding::*; -pub use language_selector::*; -pub use list::*; -pub use multi_buffer::*; -pub use palette::*; -pub use panel::*; -pub use panes::*; -pub use player_stack::*; -pub use project_panel::*; -pub use recent_projects::*; -pub use status_bar::*; -pub use tab::*; -pub use tab_bar::*; -pub use terminal::*; -pub use theme_selector::*; -pub use title_bar::*; -pub use toast::*; -pub use toolbar::*; -pub use traffic_lights::*; -pub use workspace::*; - -// Nate: Commenting this out for now, unsure if we need it. - -// use std::marker::PhantomData; -// use std::rc::Rc; - -// use gpui2::elements::div; -// use gpui2::interactive::Interactive; -// use gpui2::platform::MouseButton; -// use gpui2::{ArcCow, Element, EventContext, IntoElement, ParentElement, ViewContext}; - -// struct ButtonHandlers { -// click: Option)>>, -// } - -// impl Default for ButtonHandlers { -// fn default() -> Self { -// Self { click: None } -// } -// } - -// #[derive(Element)] -// pub struct Button { -// handlers: ButtonHandlers, -// label: Option>, -// icon: Option>, -// data: Rc, -// view_type: PhantomData, -// } - -// // Impl block for buttons without data. -// // See below for an impl block for any button. -// impl Button { -// fn new() -> Self { -// Self { -// handlers: ButtonHandlers::default(), -// label: None, -// icon: None, -// data: Rc::new(()), -// view_type: PhantomData, -// } -// } - -// pub fn data(self, data: D) -> Button { -// Button { -// handlers: ButtonHandlers::default(), -// label: self.label, -// icon: self.icon, -// data: Rc::new(data), -// view_type: PhantomData, -// } -// } -// } - -// // Impl block for button regardless of its data type. -// impl Button { -// pub fn label(mut self, label: impl Into>) -> Self { -// self.label = Some(label.into()); -// self -// } - -// pub fn icon(mut self, icon: impl Into>) -> Self { -// self.icon = Some(icon.into()); -// self -// } - -// pub fn on_click( -// mut self, -// handler: impl Fn(&mut V, &D, &mut EventContext) + 'static, -// ) -> Self { -// self.handlers.click = Some(Rc::new(handler)); -// self -// } -// } - -// pub fn button() -> Button { -// Button::new() -// } - -// impl Button { -// fn render( -// &mut self, -// view: &mut V, -// cx: &mut ViewContext, -// ) -> impl IntoElement + Interactive { -// // let colors = &cx.theme::().colors; - -// let button = div() -// // .fill(colors.error(0.5)) -// .h_4() -// .children(self.label.clone()); - -// if let Some(handler) = self.handlers.click.clone() { -// let data = self.data.clone(); -// button.on_mouse_down(MouseButton::Left, move |view, event, cx| { -// handler(view, data.as_ref(), cx) -// }) -// } else { -// button -// } -// } -// } diff --git a/crates/ui/src/components/assistant_panel.rs b/crates/ui/src/components/assistant_panel.rs deleted file mode 100644 index a0a0c52882..0000000000 --- a/crates/ui/src/components/assistant_panel.rs +++ /dev/null @@ -1,91 +0,0 @@ -use std::marker::PhantomData; - -use gpui2::geometry::rems; - -use crate::prelude::*; -use crate::theme::theme; -use crate::{Icon, IconButton, Label, Panel, PanelSide}; - -#[derive(Element)] -pub struct AssistantPanel { - view_type: PhantomData, - scroll_state: ScrollState, - current_side: PanelSide, -} - -impl AssistantPanel { - pub fn new() -> Self { - Self { - view_type: PhantomData, - scroll_state: ScrollState::default(), - current_side: PanelSide::default(), - } - } - - pub fn side(mut self, side: PanelSide) -> Self { - self.current_side = side; - self - } - - fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { - let theme = theme(cx); - - struct PanelPayload { - pub scroll_state: ScrollState, - } - - Panel::new( - self.scroll_state.clone(), - |_, payload| { - let payload = payload.downcast_ref::().unwrap(); - - vec![div() - .flex() - .flex_col() - .h_full() - .px_2() - .gap_2() - // Header - .child( - div() - .flex() - .justify_between() - .gap_2() - .child( - div() - .flex() - .child(IconButton::new(Icon::Menu)) - .child(Label::new("New Conversation")), - ) - .child( - div() - .flex() - .items_center() - .gap_px() - .child(IconButton::new(Icon::SplitMessage)) - .child(IconButton::new(Icon::Quote)) - .child(IconButton::new(Icon::MagicWand)) - .child(IconButton::new(Icon::Plus)) - .child(IconButton::new(Icon::Maximize)), - ), - ) - // Chat Body - .child( - div() - .w_full() - .flex() - .flex_col() - .gap_3() - .overflow_y_scroll(payload.scroll_state.clone()) - .child(Label::new("Is this thing on?")), - ) - .into_any()] - }, - Box::new(PanelPayload { - scroll_state: self.scroll_state.clone(), - }), - ) - .side(self.current_side) - .width(rems(32.)) - } -} diff --git a/crates/ui/src/components/breadcrumb.rs b/crates/ui/src/components/breadcrumb.rs deleted file mode 100644 index c14e89ee7b..0000000000 --- a/crates/ui/src/components/breadcrumb.rs +++ /dev/null @@ -1,71 +0,0 @@ -use std::path::PathBuf; - -use gpui2::elements::div::Div; - -use crate::{h_stack, theme}; -use crate::{prelude::*, HighlightedText}; - -#[derive(Clone)] -pub struct Symbol(pub Vec); - -#[derive(Element)] -pub struct Breadcrumb { - path: PathBuf, - symbols: Vec, -} - -impl Breadcrumb { - pub fn new(path: PathBuf, symbols: Vec) -> Self { - Self { path, symbols } - } - - fn render_separator(&self, theme: &Theme) -> Div { - div() - .child(" › ") - .text_color(HighlightColor::Default.hsla(theme)) - } - - fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { - let theme = theme(cx); - - let symbols_len = self.symbols.len(); - - h_stack() - .px_1() - // TODO: Read font from theme (or settings?). - .font("Zed Mono Extended") - .text_sm() - .text_color(theme.middle.base.default.foreground) - .rounded_md() - .hover() - .fill(theme.highest.base.hovered.background) - .child(self.path.clone().to_str().unwrap().to_string()) - .child(if !self.symbols.is_empty() { - self.render_separator(&theme) - } else { - div() - }) - .child( - div().flex().children( - self.symbols - .iter() - .enumerate() - // TODO: Could use something like `intersperse` here instead. - .flat_map(|(ix, symbol)| { - let mut items = - vec![div().flex().children(symbol.0.iter().map(|segment| { - div().child(segment.text.clone()).text_color(segment.color) - }))]; - - let is_last_segment = ix == symbols_len - 1; - if !is_last_segment { - items.push(self.render_separator(&theme)); - } - - items - }) - .collect::>(), - ), - ) - } -} diff --git a/crates/ui/src/components/buffer.rs b/crates/ui/src/components/buffer.rs deleted file mode 100644 index 00e5daee55..0000000000 --- a/crates/ui/src/components/buffer.rs +++ /dev/null @@ -1,233 +0,0 @@ -use gpui2::{Hsla, WindowContext}; - -use crate::prelude::*; -use crate::{h_stack, theme, v_stack, Icon, IconElement}; - -#[derive(Default, PartialEq, Copy, Clone)] -pub struct PlayerCursor { - color: Hsla, - index: usize, -} - -#[derive(Default, PartialEq, Clone)] -pub struct HighlightedText { - pub text: String, - pub color: Hsla, -} - -#[derive(Default, PartialEq, Clone)] -pub struct HighlightedLine { - pub highlighted_texts: Vec, -} - -#[derive(Default, PartialEq, Clone)] -pub struct BufferRow { - pub line_number: usize, - pub code_action: bool, - pub current: bool, - pub line: Option, - pub cursors: Option>, - pub status: GitStatus, - pub show_line_number: bool, -} - -#[derive(Clone)] -pub struct BufferRows { - pub show_line_numbers: bool, - pub rows: Vec, -} - -impl Default for BufferRows { - fn default() -> Self { - Self { - show_line_numbers: true, - rows: vec![BufferRow { - line_number: 1, - code_action: false, - current: true, - line: None, - cursors: None, - status: GitStatus::None, - show_line_number: true, - }], - } - } -} - -impl BufferRow { - pub fn new(line_number: usize) -> Self { - Self { - line_number, - code_action: false, - current: false, - line: None, - cursors: None, - status: GitStatus::None, - show_line_number: true, - } - } - - pub fn set_line(mut self, line: Option) -> Self { - self.line = line; - self - } - - pub fn set_cursors(mut self, cursors: Option>) -> Self { - self.cursors = cursors; - self - } - - pub fn add_cursor(mut self, cursor: PlayerCursor) -> Self { - if let Some(cursors) = &mut self.cursors { - cursors.push(cursor); - } else { - self.cursors = Some(vec![cursor]); - } - self - } - - pub fn set_status(mut self, status: GitStatus) -> Self { - self.status = status; - self - } - - pub fn set_show_line_number(mut self, show_line_number: bool) -> Self { - self.show_line_number = show_line_number; - self - } - - pub fn set_code_action(mut self, code_action: bool) -> Self { - self.code_action = code_action; - self - } - - pub fn set_current(mut self, current: bool) -> Self { - self.current = current; - self - } -} - -#[derive(Element, Clone)] -pub struct Buffer { - scroll_state: ScrollState, - rows: Option, - readonly: bool, - language: Option, - title: Option, - path: Option, -} - -impl Buffer { - pub fn new() -> Self { - Self { - scroll_state: ScrollState::default(), - rows: Some(BufferRows::default()), - readonly: false, - language: None, - title: Some("untitled".to_string()), - path: None, - } - } - - pub fn bind_scroll_state(&mut self, scroll_state: ScrollState) { - self.scroll_state = scroll_state; - } - - pub fn set_title>>(mut self, title: T) -> Self { - self.title = title.into(); - self - } - - pub fn set_path>>(mut self, path: P) -> Self { - self.path = path.into(); - self - } - - pub fn set_readonly(mut self, readonly: bool) -> Self { - self.readonly = readonly; - self - } - - pub fn set_rows>>(mut self, rows: R) -> Self { - self.rows = rows.into(); - self - } - - pub fn set_language>>(mut self, language: L) -> Self { - self.language = language.into(); - self - } - - fn render_row(row: BufferRow, cx: &WindowContext) -> impl IntoElement { - let theme = theme(cx); - let system_color = SystemColor::new(); - - let line_background = if row.current { - theme.middle.base.default.background - } else { - system_color.transparent - }; - - let line_number_color = if row.current { - HighlightColor::Default.hsla(&theme) - } else { - HighlightColor::Comment.hsla(&theme) - }; - - h_stack() - .fill(line_background) - .w_full() - .gap_2() - .px_1() - .child( - h_stack() - .w_4() - .h_full() - .px_0p5() - .when(row.code_action, |c| { - div().child(IconElement::new(Icon::Bolt)) - }), - ) - .when(row.show_line_number, |this| { - this.child( - h_stack().justify_end().px_0p5().w_3().child( - div() - .text_color(line_number_color) - .child(row.line_number.to_string()), - ), - ) - }) - .child(div().mx_0p5().w_1().h_full().fill(row.status.hsla(cx))) - .children(row.line.map(|line| { - div() - .flex() - .children(line.highlighted_texts.iter().map(|highlighted_text| { - div() - .text_color(highlighted_text.color) - .child(highlighted_text.text.clone()) - })) - })) - } - - fn render_rows(&self, cx: &WindowContext) -> Vec> { - match &self.rows { - Some(rows) => rows - .rows - .iter() - .map(|row| Self::render_row(row.clone(), cx)) - .collect(), - None => vec![], - } - } - - fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { - let theme = theme(cx); - let rows = self.render_rows(cx); - v_stack() - .flex_1() - .w_full() - .h_full() - .fill(theme.highest.base.default.background) - .children(rows) - } -} diff --git a/crates/ui/src/components/chat_panel.rs b/crates/ui/src/components/chat_panel.rs deleted file mode 100644 index 5ae66967b6..0000000000 --- a/crates/ui/src/components/chat_panel.rs +++ /dev/null @@ -1,108 +0,0 @@ -use std::marker::PhantomData; - -use chrono::NaiveDateTime; - -use crate::prelude::*; -use crate::theme::theme; -use crate::{Icon, IconButton, Input, Label, LabelColor}; - -#[derive(Element)] -pub struct ChatPanel { - view_type: PhantomData, - scroll_state: ScrollState, - messages: Vec, -} - -impl ChatPanel { - pub fn new(scroll_state: ScrollState) -> Self { - Self { - view_type: PhantomData, - scroll_state, - messages: Vec::new(), - } - } - - pub fn with_messages(mut self, messages: Vec) -> Self { - self.messages = messages; - self - } - - fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { - let theme = theme(cx); - - div() - .flex() - .flex_col() - .justify_between() - .h_full() - .px_2() - .gap_2() - // Header - .child( - div() - .flex() - .justify_between() - .py_2() - .child(div().flex().child(Label::new("#design"))) - .child( - div() - .flex() - .items_center() - .gap_px() - .child(IconButton::new(Icon::File)) - .child(IconButton::new(Icon::AudioOn)), - ), - ) - .child( - div() - .flex() - .flex_col() - // Chat Body - .child( - div() - .w_full() - .flex() - .flex_col() - .gap_3() - .overflow_y_scroll(self.scroll_state.clone()) - .children(self.messages.clone()), - ) - // Composer - .child(div().flex().my_2().child(Input::new("Message #design"))), - ) - } -} - -#[derive(Element, Clone)] -pub struct ChatMessage { - author: String, - text: String, - sent_at: NaiveDateTime, -} - -impl ChatMessage { - pub fn new(author: String, text: String, sent_at: NaiveDateTime) -> Self { - Self { - author, - text, - sent_at, - } - } - - fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { - div() - .flex() - .flex_col() - .child( - div() - .flex() - .gap_2() - .child(Label::new(self.author.clone())) - .child( - Label::new(self.sent_at.format("%m/%d/%Y").to_string()) - .color(LabelColor::Muted), - ), - ) - .child(div().child(Label::new(self.text.clone()))) - } -} diff --git a/crates/ui/src/components/collab_panel.rs b/crates/ui/src/components/collab_panel.rs deleted file mode 100644 index 14dd43294f..0000000000 --- a/crates/ui/src/components/collab_panel.rs +++ /dev/null @@ -1,161 +0,0 @@ -use std::marker::PhantomData; - -use gpui2::elements::{img, svg}; -use gpui2::ArcCow; - -use crate::prelude::*; -use crate::theme::{theme, Theme}; -use crate::{ - static_collab_panel_channels, static_collab_panel_current_call, v_stack, Icon, List, - ListHeader, ToggleState, -}; - -#[derive(Element)] -pub struct CollabPanel { - view_type: PhantomData, - scroll_state: ScrollState, -} - -impl CollabPanel { - pub fn new(scroll_state: ScrollState) -> Self { - Self { - view_type: PhantomData, - scroll_state, - } - } - - fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { - let theme = theme(cx); - - v_stack() - .w_64() - .h_full() - .fill(theme.middle.base.default.background) - .child( - v_stack() - .w_full() - .overflow_y_scroll(self.scroll_state.clone()) - .child( - div() - .fill(theme.lowest.base.default.background) - .pb_1() - .border_color(theme.lowest.base.default.border) - .border_b() - .child( - List::new(static_collab_panel_current_call()) - .header( - ListHeader::new("CRDB") - .left_icon(Icon::Hash.into()) - .set_toggle(ToggleState::Toggled), - ) - .set_toggle(ToggleState::Toggled), - ), - ) - .child( - v_stack().py_1().child( - List::new(static_collab_panel_channels()) - .header( - ListHeader::new("CHANNELS").set_toggle(ToggleState::Toggled), - ) - .empty_message("No channels yet. Add a channel to get started.") - .set_toggle(ToggleState::Toggled), - ), - ) - .child( - v_stack().py_1().child( - List::new(static_collab_panel_current_call()) - .header( - ListHeader::new("CONTACTS – ONLINE") - .set_toggle(ToggleState::Toggled), - ) - .set_toggle(ToggleState::Toggled), - ), - ) - .child( - v_stack().py_1().child( - List::new(static_collab_panel_current_call()) - .header( - ListHeader::new("CONTACTS – OFFLINE") - .set_toggle(ToggleState::NotToggled), - ) - .set_toggle(ToggleState::NotToggled), - ), - ), - ) - .child( - div() - .h_7() - .px_2() - .border_t() - .border_color(theme.middle.variant.default.border) - .flex() - .items_center() - .child( - div() - .text_sm() - .text_color(theme.middle.variant.default.foreground) - .child("Find..."), - ), - ) - } - - fn list_section_header( - &self, - label: impl Into>, - expanded: bool, - theme: &Theme, - ) -> impl Element { - div() - .h_7() - .px_2() - .flex() - .justify_between() - .items_center() - .child(div().flex().gap_1().text_sm().child(label)) - .child( - div().flex().h_full().gap_1().items_center().child( - svg() - .path(if expanded { - "icons/caret_down.svg" - } else { - "icons/caret_up.svg" - }) - .w_3p5() - .h_3p5() - .fill(theme.middle.variant.default.foreground), - ), - ) - } - - fn list_item( - &self, - avatar_uri: impl Into>, - label: impl Into>, - theme: &Theme, - ) -> impl Element { - div() - .h_7() - .px_2() - .flex() - .items_center() - .hover() - .fill(theme.lowest.variant.hovered.background) - .active() - .fill(theme.lowest.variant.pressed.background) - .child( - div() - .flex() - .items_center() - .gap_1() - .text_sm() - .child( - img() - .uri(avatar_uri) - .size_3p5() - .rounded_full() - .fill(theme.middle.positive.default.foreground), - ) - .child(label), - ) - } -} diff --git a/crates/ui/src/components/command_palette.rs b/crates/ui/src/components/command_palette.rs deleted file mode 100644 index 797876c05e..0000000000 --- a/crates/ui/src/components/command_palette.rs +++ /dev/null @@ -1,29 +0,0 @@ -use std::marker::PhantomData; - -use crate::prelude::*; -use crate::{example_editor_actions, OrderMethod, Palette}; - -#[derive(Element)] -pub struct CommandPalette { - view_type: PhantomData, - scroll_state: ScrollState, -} - -impl CommandPalette { - pub fn new(scroll_state: ScrollState) -> Self { - Self { - view_type: PhantomData, - scroll_state, - } - } - - fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { - div().child( - Palette::new(self.scroll_state.clone()) - .items(example_editor_actions()) - .placeholder("Execute a command...") - .empty_string("No items found.") - .default_order(OrderMethod::Ascending), - ) - } -} diff --git a/crates/ui/src/components/context_menu.rs b/crates/ui/src/components/context_menu.rs deleted file mode 100644 index 0808bf6556..0000000000 --- a/crates/ui/src/components/context_menu.rs +++ /dev/null @@ -1,65 +0,0 @@ -use crate::prelude::*; -use crate::theme::theme; -use crate::{ - v_stack, Label, List, ListEntry, ListItem, ListItemVariant, ListSeparator, ListSubHeader, -}; - -#[derive(Clone)] -pub enum ContextMenuItem { - Header(&'static str), - Entry(Label), - Separator, -} - -impl ContextMenuItem { - fn to_list_item(self) -> ListItem { - match self { - ContextMenuItem::Header(label) => ListSubHeader::new(label).into(), - ContextMenuItem::Entry(label) => { - ListEntry::new(label).variant(ListItemVariant::Inset).into() - } - ContextMenuItem::Separator => ListSeparator::new().into(), - } - } - pub fn header(label: &'static str) -> Self { - Self::Header(label) - } - pub fn separator() -> Self { - Self::Separator - } - pub fn entry(label: Label) -> Self { - Self::Entry(label) - } -} - -#[derive(Element)] -pub struct ContextMenu { - items: Vec, -} - -impl ContextMenu { - pub fn new(items: impl IntoIterator) -> Self { - Self { - items: items.into_iter().collect(), - } - } - fn render(&mut self, view: &mut V, cx: &mut ViewContext) -> impl IntoElement { - let theme = theme(cx); - v_stack() - .flex() - .fill(theme.lowest.base.default.background) - .border() - .border_color(theme.lowest.base.default.border) - .child( - List::new( - self.items - .clone() - .into_iter() - .map(ContextMenuItem::to_list_item) - .collect(), - ) - .set_toggle(ToggleState::Toggled), - ) - //div().p_1().children(self.items.clone()) - } -} diff --git a/crates/ui/src/components/editor_pane.rs b/crates/ui/src/components/editor_pane.rs deleted file mode 100644 index 561081164c..0000000000 --- a/crates/ui/src/components/editor_pane.rs +++ /dev/null @@ -1,60 +0,0 @@ -use std::marker::PhantomData; -use std::path::PathBuf; - -use crate::prelude::*; -use crate::{v_stack, Breadcrumb, Buffer, Icon, IconButton, Symbol, Tab, TabBar, Toolbar}; - -pub struct Editor { - pub tabs: Vec, - pub path: PathBuf, - pub symbols: Vec, - pub buffer: Buffer, -} - -#[derive(Element)] -pub struct EditorPane { - view_type: PhantomData, - editor: Editor, -} - -impl EditorPane { - pub fn new(editor: Editor) -> Self { - Self { - view_type: PhantomData, - editor, - } - } - - fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { - struct LeftItemsPayload { - path: PathBuf, - symbols: Vec, - } - - v_stack() - .w_full() - .h_full() - .flex_1() - .child(TabBar::new(self.editor.tabs.clone())) - .child(Toolbar::new( - |_, payload| { - let payload = payload.downcast_ref::().unwrap(); - - vec![Breadcrumb::new(payload.path.clone(), payload.symbols.clone()).into_any()] - }, - Box::new(LeftItemsPayload { - path: self.editor.path.clone(), - symbols: self.editor.symbols.clone(), - }), - |_, _| { - vec![ - IconButton::new(Icon::InlayHint).into_any(), - IconButton::new(Icon::MagnifyingGlass).into_any(), - IconButton::new(Icon::MagicWand).into_any(), - ] - }, - Box::new(()), - )) - .child(self.editor.buffer.clone()) - } -} diff --git a/crates/ui/src/components/facepile.rs b/crates/ui/src/components/facepile.rs deleted file mode 100644 index 949e226c25..0000000000 --- a/crates/ui/src/components/facepile.rs +++ /dev/null @@ -1,28 +0,0 @@ -use crate::prelude::*; -use crate::{theme, Avatar, Player}; - -#[derive(Element)] -pub struct Facepile { - players: Vec, -} - -impl Facepile { - pub fn new>(players: P) -> Self { - Self { - players: players.collect(), - } - } - - fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { - let theme = theme(cx); - let player_count = self.players.len(); - let player_list = self.players.iter().enumerate().map(|(ix, player)| { - let isnt_last = ix < player_count - 1; - - div() - .when(isnt_last, |div| div.neg_mr_1()) - .child(Avatar::new(player.avatar_src().to_string())) - }); - div().p_1().flex().items_center().children(player_list) - } -} diff --git a/crates/ui/src/components/icon_button.rs b/crates/ui/src/components/icon_button.rs deleted file mode 100644 index 8ac122d3eb..0000000000 --- a/crates/ui/src/components/icon_button.rs +++ /dev/null @@ -1,67 +0,0 @@ -use crate::prelude::*; -use crate::{theme, Icon, IconColor, IconElement}; - -#[derive(Element)] -pub struct IconButton { - icon: Icon, - color: IconColor, - variant: ButtonVariant, - state: InteractionState, -} - -impl IconButton { - pub fn new(icon: Icon) -> Self { - Self { - icon, - color: IconColor::default(), - variant: ButtonVariant::default(), - state: InteractionState::default(), - } - } - - pub fn icon(mut self, icon: Icon) -> Self { - self.icon = icon; - self - } - - pub fn color(mut self, color: IconColor) -> Self { - self.color = color; - self - } - - pub fn variant(mut self, variant: ButtonVariant) -> Self { - self.variant = variant; - self - } - - pub fn state(mut self, state: InteractionState) -> Self { - self.state = state; - self - } - - fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { - let theme = theme(cx); - - let icon_color = match (self.state, self.color) { - (InteractionState::Disabled, _) => IconColor::Disabled, - _ => self.color, - }; - - let mut div = div(); - if self.variant == ButtonVariant::Filled { - div = div.fill(theme.highest.on.default.background); - } - - div.w_7() - .h_6() - .flex() - .items_center() - .justify_center() - .rounded_md() - .hover() - .fill(theme.highest.base.hovered.background) - .active() - .fill(theme.highest.base.pressed.background) - .child(IconElement::new(self.icon).color(icon_color)) - } -} diff --git a/crates/ui/src/components/keybinding.rs b/crates/ui/src/components/keybinding.rs deleted file mode 100644 index 63c5f11039..0000000000 --- a/crates/ui/src/components/keybinding.rs +++ /dev/null @@ -1,158 +0,0 @@ -use std::collections::HashSet; - -use strum::{EnumIter, IntoEnumIterator}; - -use crate::prelude::*; -use crate::theme; - -#[derive(Element, Clone)] -pub struct Keybinding { - /// A keybinding consists of a key and a set of modifier keys. - /// More then one keybinding produces a chord. - /// - /// This should always contain at least one element. - keybinding: Vec<(String, ModifierKeys)>, -} - -impl Keybinding { - pub fn new(key: String, modifiers: ModifierKeys) -> Self { - Self { - keybinding: vec![(key, modifiers)], - } - } - - pub fn new_chord( - first_note: (String, ModifierKeys), - second_note: (String, ModifierKeys), - ) -> Self { - Self { - keybinding: vec![first_note, second_note], - } - } - - fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { - div() - .flex() - .gap_2() - .children(self.keybinding.iter().map(|(key, modifiers)| { - div() - .flex() - .gap_1() - .children(ModifierKey::iter().filter_map(|modifier| { - if modifiers.0.contains(&modifier) { - Some(Key::new(modifier.glyph())) - } else { - None - } - })) - .child(Key::new(key.clone())) - })) - } -} - -#[derive(Element)] -pub struct Key { - key: String, -} - -impl Key { - pub fn new(key: K) -> Self - where - K: Into, - { - Self { key: key.into() } - } - - fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { - let theme = theme(cx); - - div() - .px_2() - .py_0() - .rounded_md() - .text_sm() - .text_color(theme.lowest.on.default.foreground) - .fill(theme.lowest.on.default.background) - .child(self.key.clone()) - } -} - -// NOTE: The order the modifier keys appear in this enum impacts the order in -// which they are rendered in the UI. -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)] -pub enum ModifierKey { - Control, - Alt, - Command, - Shift, -} - -impl ModifierKey { - /// Returns the glyph for the [`ModifierKey`]. - pub fn glyph(&self) -> char { - match self { - Self::Control => '^', - Self::Alt => '⌥', - Self::Command => '⌘', - Self::Shift => '⇧', - } - } -} - -#[derive(Clone)] -pub struct ModifierKeys(HashSet); - -impl ModifierKeys { - pub fn new() -> Self { - Self(HashSet::new()) - } - - pub fn all() -> Self { - Self(HashSet::from_iter(ModifierKey::iter())) - } - - pub fn add(mut self, modifier: ModifierKey) -> Self { - self.0.insert(modifier); - self - } - - pub fn control(mut self, control: bool) -> Self { - if control { - self.0.insert(ModifierKey::Control); - } else { - self.0.remove(&ModifierKey::Control); - } - - self - } - - pub fn alt(mut self, alt: bool) -> Self { - if alt { - self.0.insert(ModifierKey::Alt); - } else { - self.0.remove(&ModifierKey::Alt); - } - - self - } - - pub fn command(mut self, command: bool) -> Self { - if command { - self.0.insert(ModifierKey::Command); - } else { - self.0.remove(&ModifierKey::Command); - } - - self - } - - pub fn shift(mut self, shift: bool) -> Self { - if shift { - self.0.insert(ModifierKey::Shift); - } else { - self.0.remove(&ModifierKey::Shift); - } - - self - } -} diff --git a/crates/ui/src/components/language_selector.rs b/crates/ui/src/components/language_selector.rs deleted file mode 100644 index 124d7f13ee..0000000000 --- a/crates/ui/src/components/language_selector.rs +++ /dev/null @@ -1,36 +0,0 @@ -use crate::prelude::*; -use crate::{OrderMethod, Palette, PaletteItem}; - -#[derive(Element)] -pub struct LanguageSelector { - scroll_state: ScrollState, -} - -impl LanguageSelector { - pub fn new() -> Self { - Self { - scroll_state: ScrollState::default(), - } - } - - fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { - div().child( - Palette::new(self.scroll_state.clone()) - .items(vec![ - PaletteItem::new("C"), - PaletteItem::new("C++"), - PaletteItem::new("CSS"), - PaletteItem::new("Elixir"), - PaletteItem::new("Elm"), - PaletteItem::new("ERB"), - PaletteItem::new("Rust (current)"), - PaletteItem::new("Scheme"), - PaletteItem::new("TOML"), - PaletteItem::new("TypeScript"), - ]) - .placeholder("Select a language...") - .empty_string("No matches") - .default_order(OrderMethod::Ascending), - ) - } -} diff --git a/crates/ui/src/components/list.rs b/crates/ui/src/components/list.rs deleted file mode 100644 index b7dff6b2c5..0000000000 --- a/crates/ui/src/components/list.rs +++ /dev/null @@ -1,512 +0,0 @@ -use gpui2::elements::div::Div; -use gpui2::{Hsla, WindowContext}; - -use crate::prelude::*; -use crate::{ - h_stack, theme, token, v_stack, Avatar, DisclosureControlVisibility, Icon, IconColor, - IconElement, IconSize, InteractionState, Label, LabelColor, LabelSize, SystemColor, - ToggleState, -}; - -#[derive(Clone, Copy, Default, Debug, PartialEq)] -pub enum ListItemVariant { - /// The list item extends to the far left and right of the list. - #[default] - FullWidth, - Inset, -} - -#[derive(Element, Clone, Copy)] -pub struct ListHeader { - label: &'static str, - left_icon: Option, - variant: ListItemVariant, - state: InteractionState, - toggleable: Toggleable, -} - -impl ListHeader { - pub fn new(label: &'static str) -> Self { - Self { - label, - left_icon: None, - variant: ListItemVariant::default(), - state: InteractionState::default(), - toggleable: Toggleable::default(), - } - } - - pub fn set_toggle(mut self, toggle: ToggleState) -> Self { - self.toggleable = toggle.into(); - self - } - - pub fn set_toggleable(mut self, toggleable: Toggleable) -> Self { - self.toggleable = toggleable; - self - } - - pub fn left_icon(mut self, left_icon: Option) -> Self { - self.left_icon = left_icon; - self - } - - pub fn state(mut self, state: InteractionState) -> Self { - self.state = state; - self - } - - fn disclosure_control(&self) -> Div { - let is_toggleable = self.toggleable != Toggleable::NotToggleable; - let is_toggled = Toggleable::is_toggled(&self.toggleable); - - match (is_toggleable, is_toggled) { - (false, _) => div(), - (_, true) => div().child(IconElement::new(Icon::ChevronRight).color(IconColor::Muted)), - (_, false) => div().child(IconElement::new(Icon::ChevronDown).size(IconSize::Small)), - } - } - - fn background_color(&self, cx: &WindowContext) -> Hsla { - let theme = theme(cx); - let system_color = SystemColor::new(); - - match self.state { - InteractionState::Hovered => theme.lowest.base.hovered.background, - InteractionState::Active => theme.lowest.base.pressed.background, - InteractionState::Enabled => theme.lowest.on.default.background, - _ => system_color.transparent, - } - } - - fn label_color(&self) -> LabelColor { - match self.state { - InteractionState::Disabled => LabelColor::Disabled, - _ => Default::default(), - } - } - - fn icon_color(&self) -> IconColor { - match self.state { - InteractionState::Disabled => IconColor::Disabled, - _ => Default::default(), - } - } - - fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { - let theme = theme(cx); - let token = token(); - let system_color = SystemColor::new(); - let background_color = self.background_color(cx); - - let is_toggleable = self.toggleable != Toggleable::NotToggleable; - let is_toggled = Toggleable::is_toggled(&self.toggleable); - - let disclosure_control = self.disclosure_control(); - - h_stack() - .flex_1() - .w_full() - .fill(background_color) - .when(self.state == InteractionState::Focused, |this| { - this.border() - .border_color(theme.lowest.accent.default.border) - }) - .relative() - .py_1() - .child( - div() - .h_6() - .when(self.variant == ListItemVariant::Inset, |this| this.px_2()) - .flex() - .flex_1() - .w_full() - .gap_1() - .items_center() - .justify_between() - .child( - div() - .flex() - .gap_1() - .items_center() - .children(self.left_icon.map(|i| { - IconElement::new(i) - .color(IconColor::Muted) - .size(IconSize::Small) - })) - .child( - Label::new(self.label) - .color(LabelColor::Muted) - .size(LabelSize::Small), - ), - ) - .child(disclosure_control), - ) - } -} - -#[derive(Element, Clone, Copy)] -pub struct ListSubHeader { - label: &'static str, - left_icon: Option, - variant: ListItemVariant, -} - -impl ListSubHeader { - pub fn new(label: &'static str) -> Self { - Self { - label, - left_icon: None, - variant: ListItemVariant::default(), - } - } - - pub fn left_icon(mut self, left_icon: Option) -> Self { - self.left_icon = left_icon; - self - } - - fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { - let theme = theme(cx); - let token = token(); - - h_stack().flex_1().w_full().relative().py_1().child( - div() - .h_6() - .when(self.variant == ListItemVariant::Inset, |this| this.px_2()) - .flex() - .flex_1() - .w_full() - .gap_1() - .items_center() - .justify_between() - .child( - div() - .flex() - .gap_1() - .items_center() - .children(self.left_icon.map(|i| { - IconElement::new(i) - .color(IconColor::Muted) - .size(IconSize::Small) - })) - .child( - Label::new(self.label) - .color(LabelColor::Muted) - .size(LabelSize::Small), - ), - ), - ) - } -} - -#[derive(Clone)] -pub enum LeftContent { - Icon(Icon), - Avatar(&'static str), -} - -#[derive(Default, PartialEq, Copy, Clone)] -pub enum ListEntrySize { - #[default] - Small, - Medium, -} - -#[derive(Clone, Element)] -pub enum ListItem { - Entry(ListEntry), - Separator(ListSeparator), - Header(ListSubHeader), -} - -impl From for ListItem { - fn from(entry: ListEntry) -> Self { - Self::Entry(entry) - } -} - -impl From for ListItem { - fn from(entry: ListSeparator) -> Self { - Self::Separator(entry) - } -} - -impl From for ListItem { - fn from(entry: ListSubHeader) -> Self { - Self::Header(entry) - } -} - -impl ListItem { - fn render(&mut self, v: &mut V, cx: &mut ViewContext) -> impl IntoElement { - match self { - ListItem::Entry(entry) => div().child(entry.render(v, cx)), - ListItem::Separator(separator) => div().child(separator.render(v, cx)), - ListItem::Header(header) => div().child(header.render(v, cx)), - } - } - pub fn new(label: Label) -> Self { - Self::Entry(ListEntry::new(label)) - } - pub fn as_entry(&mut self) -> Option<&mut ListEntry> { - if let Self::Entry(entry) = self { - Some(entry) - } else { - None - } - } -} - -#[derive(Element, Clone)] -pub struct ListEntry { - disclosure_control_style: DisclosureControlVisibility, - indent_level: u32, - label: Label, - left_content: Option, - variant: ListItemVariant, - size: ListEntrySize, - state: InteractionState, - toggle: Option, -} - -impl ListEntry { - pub fn new(label: Label) -> Self { - Self { - disclosure_control_style: DisclosureControlVisibility::default(), - indent_level: 0, - label, - variant: ListItemVariant::default(), - left_content: None, - size: ListEntrySize::default(), - state: InteractionState::default(), - toggle: None, - } - } - pub fn variant(mut self, variant: ListItemVariant) -> Self { - self.variant = variant; - self - } - pub fn indent_level(mut self, indent_level: u32) -> Self { - self.indent_level = indent_level; - self - } - - pub fn set_toggle(mut self, toggle: ToggleState) -> Self { - self.toggle = Some(toggle); - self - } - - pub fn left_content(mut self, left_content: LeftContent) -> Self { - self.left_content = Some(left_content); - self - } - - pub fn left_icon(mut self, left_icon: Icon) -> Self { - self.left_content = Some(LeftContent::Icon(left_icon)); - self - } - - pub fn left_avatar(mut self, left_avatar: &'static str) -> Self { - self.left_content = Some(LeftContent::Avatar(left_avatar)); - self - } - - pub fn state(mut self, state: InteractionState) -> Self { - self.state = state; - self - } - - pub fn size(mut self, size: ListEntrySize) -> Self { - self.size = size; - self - } - - pub fn disclosure_control_style( - mut self, - disclosure_control_style: DisclosureControlVisibility, - ) -> Self { - self.disclosure_control_style = disclosure_control_style; - self - } - - fn background_color(&self, cx: &WindowContext) -> Hsla { - let theme = theme(cx); - let system_color = SystemColor::new(); - - match self.state { - InteractionState::Hovered => theme.lowest.base.hovered.background, - InteractionState::Active => theme.lowest.base.pressed.background, - InteractionState::Enabled => theme.lowest.on.default.background, - _ => system_color.transparent, - } - } - - fn label_color(&self) -> LabelColor { - match self.state { - InteractionState::Disabled => LabelColor::Disabled, - _ => Default::default(), - } - } - - fn icon_color(&self) -> IconColor { - match self.state { - InteractionState::Disabled => IconColor::Disabled, - _ => Default::default(), - } - } - - fn disclosure_control( - &mut self, - cx: &mut ViewContext, - ) -> Option> { - let theme = theme(cx); - let token = token(); - - let disclosure_control_icon = if let Some(ToggleState::Toggled) = self.toggle { - IconElement::new(Icon::ChevronDown) - } else { - IconElement::new(Icon::ChevronRight) - } - .color(IconColor::Muted) - .size(IconSize::Small); - - match (self.toggle, self.disclosure_control_style) { - (Some(_), DisclosureControlVisibility::OnHover) => { - Some(div().absolute().neg_left_5().child(disclosure_control_icon)) - } - (Some(_), DisclosureControlVisibility::Always) => { - Some(div().child(disclosure_control_icon)) - } - (None, _) => None, - } - } - - fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { - let theme = theme(cx); - let token = token(); - let system_color = SystemColor::new(); - let background_color = self.background_color(cx); - - let left_content = match self.left_content { - Some(LeftContent::Icon(i)) => { - Some(h_stack().child(IconElement::new(i).size(IconSize::Small))) - } - Some(LeftContent::Avatar(src)) => Some(h_stack().child(Avatar::new(src))), - None => None, - }; - - let sized_item = match self.size { - ListEntrySize::Small => div().h_6(), - ListEntrySize::Medium => div().h_7(), - }; - - div() - .fill(background_color) - .when(self.state == InteractionState::Focused, |this| { - this.border() - .border_color(theme.lowest.accent.default.border) - }) - .relative() - .py_1() - .child( - sized_item - .when(self.variant == ListItemVariant::Inset, |this| this.px_2()) - // .ml(rems(0.75 * self.indent_level as f32)) - .children((0..self.indent_level).map(|_| { - div() - .w(token.list_indent_depth) - .h_full() - .flex() - .justify_center() - .child(h_stack().child(div().w_px().h_full()).child( - div().w_px().h_full().fill(theme.middle.base.default.border), - )) - })) - .flex() - .gap_1() - .items_center() - .relative() - .children(self.disclosure_control(cx)) - .children(left_content) - .child(self.label.clone()), - ) - } -} - -#[derive(Clone, Default, Element)] -pub struct ListSeparator; - -impl ListSeparator { - pub fn new() -> Self { - Self::default() - } - - fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { - let theme = theme(cx); - - div().h_px().w_full().fill(theme.lowest.base.default.border) - } -} - -#[derive(Element)] -pub struct List { - items: Vec, - empty_message: &'static str, - header: Option, - toggleable: Toggleable, -} - -impl List { - pub fn new(items: Vec) -> Self { - Self { - items, - empty_message: "No items", - header: None, - toggleable: Toggleable::default(), - } - } - - pub fn empty_message(mut self, empty_message: &'static str) -> Self { - self.empty_message = empty_message; - self - } - - pub fn header(mut self, header: ListHeader) -> Self { - self.header = Some(header); - self - } - - pub fn set_toggle(mut self, toggle: ToggleState) -> Self { - self.toggleable = toggle.into(); - self - } - - fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { - let theme = theme(cx); - let token = token(); - let is_toggleable = self.toggleable != Toggleable::NotToggleable; - let is_toggled = Toggleable::is_toggled(&self.toggleable); - - let disclosure_control = if is_toggleable { - IconElement::new(Icon::ChevronRight) - } else { - IconElement::new(Icon::ChevronDown) - }; - - let list_content = match (self.items.is_empty(), is_toggled) { - (_, false) => div(), - (false, _) => div().children(self.items.iter().cloned()), - (true, _) => div().child(Label::new(self.empty_message).color(LabelColor::Muted)), - }; - - v_stack() - .py_1() - .children( - self.header - .clone() - .map(|header| header.set_toggleable(self.toggleable)), - ) - .child(list_content) - } -} diff --git a/crates/ui/src/components/multi_buffer.rs b/crates/ui/src/components/multi_buffer.rs deleted file mode 100644 index d38603457a..0000000000 --- a/crates/ui/src/components/multi_buffer.rs +++ /dev/null @@ -1,42 +0,0 @@ -use std::marker::PhantomData; - -use crate::prelude::*; -use crate::{v_stack, Buffer, Icon, IconButton, Label, LabelSize}; - -#[derive(Element)] -pub struct MultiBuffer { - view_type: PhantomData, - buffers: Vec, -} - -impl MultiBuffer { - pub fn new(buffers: Vec) -> Self { - Self { - view_type: PhantomData, - buffers, - } - } - - fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { - let theme = theme(cx); - - v_stack() - .w_full() - .h_full() - .flex_1() - .children(self.buffers.clone().into_iter().map(|buffer| { - v_stack() - .child( - div() - .flex() - .items_center() - .justify_between() - .p_4() - .fill(theme.lowest.base.default.background) - .child(Label::new("main.rs").size(LabelSize::Small)) - .child(IconButton::new(Icon::ArrowUpRight)), - ) - .child(buffer) - })) - } -} diff --git a/crates/ui/src/components/palette.rs b/crates/ui/src/components/palette.rs deleted file mode 100644 index 16001e50c1..0000000000 --- a/crates/ui/src/components/palette.rs +++ /dev/null @@ -1,152 +0,0 @@ -use std::marker::PhantomData; - -use crate::prelude::*; -use crate::theme::theme; -use crate::{h_stack, v_stack, Keybinding, Label, LabelColor}; - -#[derive(Element)] -pub struct Palette { - view_type: PhantomData, - scroll_state: ScrollState, - input_placeholder: &'static str, - empty_string: &'static str, - items: Vec, - default_order: OrderMethod, -} - -impl Palette { - pub fn new(scroll_state: ScrollState) -> Self { - Self { - view_type: PhantomData, - scroll_state, - input_placeholder: "Find something...", - empty_string: "No items found.", - items: vec![], - default_order: OrderMethod::default(), - } - } - - pub fn items(mut self, items: Vec) -> Self { - self.items = items; - self - } - - pub fn placeholder(mut self, input_placeholder: &'static str) -> Self { - self.input_placeholder = input_placeholder; - self - } - - pub fn empty_string(mut self, empty_string: &'static str) -> Self { - self.empty_string = empty_string; - self - } - - // TODO: Hook up sort order - pub fn default_order(mut self, default_order: OrderMethod) -> Self { - self.default_order = default_order; - self - } - - fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { - let theme = theme(cx); - - v_stack() - .w_96() - .rounded_lg() - .fill(theme.lowest.base.default.background) - .border() - .border_color(theme.lowest.base.default.border) - .child( - v_stack() - .gap_px() - .child(v_stack().py_0p5().px_1().child( - div().px_2().py_0p5().child( - Label::new(self.input_placeholder).color(LabelColor::Placeholder), - ), - )) - .child(div().h_px().w_full().fill(theme.lowest.base.default.border)) - .child( - v_stack() - .py_0p5() - .px_1() - .grow() - .max_h_96() - .overflow_y_scroll(self.scroll_state.clone()) - .children( - vec![if self.items.is_empty() { - Some(h_stack().justify_between().px_2().py_1().child( - Label::new(self.empty_string).color(LabelColor::Muted), - )) - } else { - None - }] - .into_iter() - .flatten(), - ) - .children(self.items.iter().map(|item| { - h_stack() - .justify_between() - .px_2() - .py_0p5() - .rounded_lg() - .hover() - .fill(theme.lowest.base.hovered.background) - .active() - .fill(theme.lowest.base.pressed.background) - .child(item.clone()) - })), - ), - ) - } -} - -#[derive(Element, Clone)] -pub struct PaletteItem { - pub label: &'static str, - pub sublabel: Option<&'static str>, - pub keybinding: Option, -} - -impl PaletteItem { - pub fn new(label: &'static str) -> Self { - Self { - label, - sublabel: None, - keybinding: None, - } - } - - pub fn label(mut self, label: &'static str) -> Self { - self.label = label; - self - } - - pub fn sublabel>>(mut self, sublabel: L) -> Self { - self.sublabel = sublabel.into(); - self - } - - pub fn keybinding(mut self, keybinding: K) -> Self - where - K: Into>, - { - self.keybinding = keybinding.into(); - self - } - - fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { - let theme = theme(cx); - - div() - .flex() - .flex_row() - .grow() - .justify_between() - .child( - v_stack() - .child(Label::new(self.label)) - .children(self.sublabel.map(|sublabel| Label::new(sublabel))), - ) - .children(self.keybinding.clone()) - } -} diff --git a/crates/ui/src/components/panel.rs b/crates/ui/src/components/panel.rs deleted file mode 100644 index cbcf502670..0000000000 --- a/crates/ui/src/components/panel.rs +++ /dev/null @@ -1,142 +0,0 @@ -use std::marker::PhantomData; - -use gpui2::geometry::AbsoluteLength; - -use crate::prelude::*; -use crate::{theme, token, v_stack}; - -#[derive(Default, Debug, PartialEq, Eq, Hash, Clone, Copy)] -pub enum PanelAllowedSides { - LeftOnly, - RightOnly, - BottomOnly, - #[default] - LeftAndRight, - All, -} - -impl PanelAllowedSides { - /// Return a `HashSet` that contains the allowable `PanelSide`s. - pub fn allowed_sides(&self) -> HashSet { - match self { - Self::LeftOnly => HashSet::from_iter([PanelSide::Left]), - Self::RightOnly => HashSet::from_iter([PanelSide::Right]), - Self::BottomOnly => HashSet::from_iter([PanelSide::Bottom]), - Self::LeftAndRight => HashSet::from_iter([PanelSide::Left, PanelSide::Right]), - Self::All => HashSet::from_iter([PanelSide::Left, PanelSide::Right, PanelSide::Bottom]), - } - } -} - -#[derive(Default, Debug, PartialEq, Eq, Hash, Clone, Copy)] -pub enum PanelSide { - #[default] - Left, - Right, - Bottom, -} - -use std::collections::HashSet; - -#[derive(Element)] -pub struct Panel { - view_type: PhantomData, - scroll_state: ScrollState, - current_side: PanelSide, - /// Defaults to PanelAllowedSides::LeftAndRight - allowed_sides: PanelAllowedSides, - initial_width: AbsoluteLength, - width: Option, - children: HackyChildren, - payload: HackyChildrenPayload, -} - -impl Panel { - pub fn new( - scroll_state: ScrollState, - children: HackyChildren, - payload: HackyChildrenPayload, - ) -> Self { - let token = token(); - - Self { - view_type: PhantomData, - scroll_state, - current_side: PanelSide::default(), - allowed_sides: PanelAllowedSides::default(), - initial_width: token.default_panel_size, - width: None, - children, - payload, - } - } - - pub fn initial_width(mut self, initial_width: AbsoluteLength) -> Self { - self.initial_width = initial_width; - self - } - - pub fn width(mut self, width: AbsoluteLength) -> Self { - self.width = Some(width); - self - } - - pub fn allowed_sides(mut self, allowed_sides: PanelAllowedSides) -> Self { - self.allowed_sides = allowed_sides; - self - } - - pub fn side(mut self, side: PanelSide) -> Self { - let allowed_sides = self.allowed_sides.allowed_sides(); - - if allowed_sides.contains(&side) { - self.current_side = side; - } else { - panic!( - "The panel side {:?} was not added as allowed before it was set.", - side - ); - } - self - } - - fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { - let token = token(); - let theme = theme(cx); - - let panel_base; - let current_width = self.width.unwrap_or(self.initial_width); - - match self.current_side { - PanelSide::Left => { - panel_base = v_stack() - .flex_initial() - .h_full() - .w(current_width) - .fill(theme.middle.base.default.background) - .border_r() - .border_color(theme.middle.base.default.border); - } - PanelSide::Right => { - panel_base = v_stack() - .flex_initial() - .h_full() - .w(current_width) - .fill(theme.middle.base.default.background) - .border_l() - .border_color(theme.middle.base.default.border); - } - PanelSide::Bottom => { - panel_base = v_stack() - .flex_initial() - .w_full() - .h(current_width) - .fill(theme.middle.base.default.background) - .border_t() - .border_color(theme.middle.base.default.border); - } - } - - panel_base.children_any((self.children)(cx, self.payload.as_ref())) - } -} diff --git a/crates/ui/src/components/panes.rs b/crates/ui/src/components/panes.rs deleted file mode 100644 index 2518eea8ee..0000000000 --- a/crates/ui/src/components/panes.rs +++ /dev/null @@ -1,132 +0,0 @@ -use std::marker::PhantomData; - -use gpui2::geometry::{Length, Size}; -use gpui2::{hsla, Hsla}; - -use crate::prelude::*; -use crate::theme; - -#[derive(Default, PartialEq)] -pub enum SplitDirection { - #[default] - Horizontal, - Vertical, -} - -#[derive(Element)] -pub struct Pane { - view_type: PhantomData, - scroll_state: ScrollState, - size: Size, - fill: Hsla, - children: HackyChildren, - payload: HackyChildrenPayload, -} - -impl Pane { - pub fn new( - scroll_state: ScrollState, - size: Size, - children: HackyChildren, - payload: HackyChildrenPayload, - ) -> Self { - // Fill is only here for debugging purposes, remove before release - let system_color = SystemColor::new(); - - Self { - view_type: PhantomData, - scroll_state, - size, - fill: hsla(0.3, 0.3, 0.3, 1.), - // fill: system_color.transparent, - children, - payload, - } - } - - pub fn fill(mut self, fill: Hsla) -> Self { - self.fill = fill; - self - } - - fn render(&mut self, view: &mut V, cx: &mut ViewContext) -> impl IntoElement { - let theme = theme(cx); - - div() - .flex() - .flex_initial() - .fill(self.fill) - .w(self.size.width) - .h(self.size.height) - .overflow_y_scroll(self.scroll_state.clone()) - .children_any((self.children)(cx, self.payload.as_ref())) - } -} - -#[derive(Element)] -pub struct PaneGroup { - view_type: PhantomData, - groups: Vec>, - panes: Vec>, - split_direction: SplitDirection, -} - -impl PaneGroup { - pub fn new_groups(groups: Vec>, split_direction: SplitDirection) -> Self { - Self { - view_type: PhantomData, - groups, - panes: Vec::new(), - split_direction, - } - } - - pub fn new_panes(panes: Vec>, split_direction: SplitDirection) -> Self { - Self { - view_type: PhantomData, - groups: Vec::new(), - panes, - split_direction, - } - } - - fn render(&mut self, view: &mut V, cx: &mut ViewContext) -> impl IntoElement { - let theme = theme(cx); - - if !self.panes.is_empty() { - let el = div() - .flex() - .flex_1() - .gap_px() - .w_full() - .h_full() - .fill(theme.lowest.base.default.background) - .children(self.panes.iter_mut().map(|pane| pane.render(view, cx))); - - if self.split_direction == SplitDirection::Horizontal { - return el; - } else { - return el.flex_col(); - } - } - - if !self.groups.is_empty() { - let el = div() - .flex() - .flex_1() - .gap_px() - .w_full() - .h_full() - .fill(theme.lowest.base.default.background) - .children(self.groups.iter_mut().map(|group| group.render(view, cx))); - - if self.split_direction == SplitDirection::Horizontal { - return el; - } else { - return el.flex_col(); - } - } - - unreachable!() - } -} diff --git a/crates/ui/src/components/player_stack.rs b/crates/ui/src/components/player_stack.rs deleted file mode 100644 index 7df6f065fb..0000000000 --- a/crates/ui/src/components/player_stack.rs +++ /dev/null @@ -1,65 +0,0 @@ -use crate::prelude::*; -use crate::{Avatar, Facepile, PlayerWithCallStatus}; - -#[derive(Element)] -pub struct PlayerStack { - player_with_call_status: PlayerWithCallStatus, -} - -impl PlayerStack { - pub fn new(player_with_call_status: PlayerWithCallStatus) -> Self { - Self { - player_with_call_status, - } - } - - fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { - let system_color = SystemColor::new(); - let player = self.player_with_call_status.get_player(); - self.player_with_call_status.get_call_status(); - - let followers = self - .player_with_call_status - .get_call_status() - .followers - .as_ref() - .map(|followers| followers.clone()); - - // if we have no followers return a slightly different element - // if mic_status == muted add a red ring to avatar - - div() - .h_full() - .flex() - .flex_col() - .gap_px() - .justify_center() - .child( - div().flex().justify_center().w_full().child( - div() - .w_4() - .h_0p5() - .rounded_sm() - .fill(player.cursor_color(cx)), - ), - ) - .child( - div() - .flex() - .items_center() - .justify_center() - .h_6() - .pl_1() - .rounded_lg() - .fill(if followers.is_none() { - system_color.transparent - } else { - player.selection_color(cx) - }) - .child(Avatar::new(player.avatar_src().to_string())) - .children(followers.map(|followers| { - div().neg_ml_2().child(Facepile::new(followers.into_iter())) - })), - ) - } -} diff --git a/crates/ui/src/components/project_panel.rs b/crates/ui/src/components/project_panel.rs deleted file mode 100644 index 1f32c698e5..0000000000 --- a/crates/ui/src/components/project_panel.rs +++ /dev/null @@ -1,58 +0,0 @@ -use std::marker::PhantomData; - -use crate::prelude::*; -use crate::{ - static_project_panel_project_items, static_project_panel_single_items, theme, Input, List, - ListHeader, -}; - -#[derive(Element)] -pub struct ProjectPanel { - view_type: PhantomData, - scroll_state: ScrollState, -} - -impl ProjectPanel { - pub fn new(scroll_state: ScrollState) -> Self { - Self { - view_type: PhantomData, - scroll_state, - } - } - - fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { - let theme = theme(cx); - - div() - .flex() - .flex_col() - .w_full() - .h_full() - .px_2() - .fill(theme.middle.base.default.background) - .child( - div() - .w_56() - .flex() - .flex_col() - .overflow_y_scroll(ScrollState::default()) - .child( - List::new(static_project_panel_single_items()) - .header(ListHeader::new("FILES").set_toggle(ToggleState::Toggled)) - .empty_message("No files in directory") - .set_toggle(ToggleState::Toggled), - ) - .child( - List::new(static_project_panel_project_items()) - .header(ListHeader::new("PROJECT").set_toggle(ToggleState::Toggled)) - .empty_message("No folders in directory") - .set_toggle(ToggleState::Toggled), - ), - ) - .child( - Input::new("Find something...") - .value("buffe".to_string()) - .state(InteractionState::Focused), - ) - } -} diff --git a/crates/ui/src/components/recent_projects.rs b/crates/ui/src/components/recent_projects.rs deleted file mode 100644 index 6aca6631b9..0000000000 --- a/crates/ui/src/components/recent_projects.rs +++ /dev/null @@ -1,32 +0,0 @@ -use crate::prelude::*; -use crate::{OrderMethod, Palette, PaletteItem}; - -#[derive(Element)] -pub struct RecentProjects { - scroll_state: ScrollState, -} - -impl RecentProjects { - pub fn new() -> Self { - Self { - scroll_state: ScrollState::default(), - } - } - - fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { - div().child( - Palette::new(self.scroll_state.clone()) - .items(vec![ - PaletteItem::new("zed").sublabel("~/projects/zed"), - PaletteItem::new("saga").sublabel("~/projects/saga"), - PaletteItem::new("journal").sublabel("~/journal"), - PaletteItem::new("dotfiles").sublabel("~/dotfiles"), - PaletteItem::new("zed.dev").sublabel("~/projects/zed.dev"), - PaletteItem::new("laminar").sublabel("~/projects/laminar"), - ]) - .placeholder("Recent Projects...") - .empty_string("No matches") - .default_order(OrderMethod::Ascending), - ) - } -} diff --git a/crates/ui/src/components/status_bar.rs b/crates/ui/src/components/status_bar.rs deleted file mode 100644 index 1970b0bff9..0000000000 --- a/crates/ui/src/components/status_bar.rs +++ /dev/null @@ -1,144 +0,0 @@ -use std::marker::PhantomData; - -use crate::prelude::*; -use crate::theme::{theme, Theme}; -use crate::{Button, Icon, IconButton, IconColor, ToolDivider}; - -#[derive(Default, PartialEq)] -pub enum Tool { - #[default] - ProjectPanel, - CollaborationPanel, - Terminal, - Assistant, - Feedback, - Diagnostics, -} - -struct ToolGroup { - active_index: Option, - tools: Vec, -} - -impl Default for ToolGroup { - fn default() -> Self { - ToolGroup { - active_index: None, - tools: vec![], - } - } -} - -#[derive(Element)] -pub struct StatusBar { - view_type: PhantomData, - left_tools: Option, - right_tools: Option, - bottom_tools: Option, -} - -impl StatusBar { - pub fn new() -> Self { - Self { - view_type: PhantomData, - left_tools: None, - right_tools: None, - bottom_tools: None, - } - } - - pub fn left_tool(mut self, tool: Tool, active_index: Option) -> Self { - self.left_tools = { - let mut tools = vec![tool]; - tools.extend(self.left_tools.take().unwrap_or_default().tools); - Some(ToolGroup { - active_index, - tools, - }) - }; - self - } - - pub fn right_tool(mut self, tool: Tool, active_index: Option) -> Self { - self.right_tools = { - let mut tools = vec![tool]; - tools.extend(self.left_tools.take().unwrap_or_default().tools); - Some(ToolGroup { - active_index, - tools, - }) - }; - self - } - - pub fn bottom_tool(mut self, tool: Tool, active_index: Option) -> Self { - self.bottom_tools = { - let mut tools = vec![tool]; - tools.extend(self.left_tools.take().unwrap_or_default().tools); - Some(ToolGroup { - active_index, - tools, - }) - }; - self - } - - fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { - let theme = theme(cx); - - div() - .py_0p5() - .px_1() - .flex() - .items_center() - .justify_between() - .w_full() - .fill(theme.lowest.base.default.background) - .child(self.left_tools(&theme)) - .child(self.right_tools(&theme)) - } - - fn left_tools(&self, theme: &Theme) -> impl Element { - div() - .flex() - .items_center() - .gap_1() - .child(IconButton::new(Icon::FileTree).color(IconColor::Accent)) - .child(IconButton::new(Icon::Hash)) - .child(ToolDivider::new()) - .child(IconButton::new(Icon::XCircle)) - } - fn right_tools(&self, theme: &Theme) -> impl Element { - div() - .flex() - .items_center() - .gap_2() - .child( - div() - .flex() - .items_center() - .gap_1() - .child(Button::new("116:25")) - .child(Button::new("Rust")), - ) - .child(ToolDivider::new()) - .child( - div() - .flex() - .items_center() - .gap_1() - .child(IconButton::new(Icon::Copilot)) - .child(IconButton::new(Icon::Envelope)), - ) - .child(ToolDivider::new()) - .child( - div() - .flex() - .items_center() - .gap_1() - .child(IconButton::new(Icon::Terminal)) - .child(IconButton::new(Icon::MessageBubbles)) - .child(IconButton::new(Icon::Ai)), - ) - } -} diff --git a/crates/ui/src/components/tab.rs b/crates/ui/src/components/tab.rs deleted file mode 100644 index 9eb1122775..0000000000 --- a/crates/ui/src/components/tab.rs +++ /dev/null @@ -1,131 +0,0 @@ -use crate::prelude::*; -use crate::{theme, Icon, IconColor, IconElement, Label, LabelColor}; - -#[derive(Element, Clone)] -pub struct Tab { - title: String, - icon: Option, - current: bool, - dirty: bool, - fs_status: FileSystemStatus, - git_status: GitStatus, - diagnostic_status: DiagnosticStatus, - close_side: IconSide, -} - -impl Tab { - pub fn new() -> Self { - Self { - title: "untitled".to_string(), - icon: None, - current: false, - dirty: false, - fs_status: FileSystemStatus::None, - git_status: GitStatus::None, - diagnostic_status: DiagnosticStatus::None, - close_side: IconSide::Right, - } - } - - pub fn current(mut self, current: bool) -> Self { - self.current = current; - self - } - - pub fn title(mut self, title: String) -> Self { - self.title = title; - self - } - - pub fn icon(mut self, icon: I) -> Self - where - I: Into>, - { - self.icon = icon.into(); - self - } - - pub fn dirty(mut self, dirty: bool) -> Self { - self.dirty = dirty; - self - } - - pub fn fs_status(mut self, fs_status: FileSystemStatus) -> Self { - self.fs_status = fs_status; - self - } - - pub fn git_status(mut self, git_status: GitStatus) -> Self { - self.git_status = git_status; - self - } - - pub fn diagnostic_status(mut self, diagnostic_status: DiagnosticStatus) -> Self { - self.diagnostic_status = diagnostic_status; - self - } - - pub fn close_side(mut self, close_side: IconSide) -> Self { - self.close_side = close_side; - self - } - - fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { - let theme = theme(cx); - let has_fs_conflict = self.fs_status == FileSystemStatus::Conflict; - let is_deleted = self.fs_status == FileSystemStatus::Deleted; - - let label = match (self.git_status, is_deleted) { - (_, true) | (GitStatus::Deleted, false) => Label::new(self.title.clone()) - .color(LabelColor::Hidden) - .set_strikethrough(true), - (GitStatus::None, false) => Label::new(self.title.clone()), - (GitStatus::Created, false) => { - Label::new(self.title.clone()).color(LabelColor::Created) - } - (GitStatus::Modified, false) => { - Label::new(self.title.clone()).color(LabelColor::Modified) - } - (GitStatus::Renamed, false) => Label::new(self.title.clone()).color(LabelColor::Accent), - (GitStatus::Conflict, false) => Label::new(self.title.clone()), - }; - - let close_icon = IconElement::new(Icon::Close).color(IconColor::Muted); - - div() - .px_2() - .py_0p5() - .flex() - .items_center() - .justify_center() - .fill(if self.current { - theme.highest.base.default.background - } else { - theme.middle.base.default.background - }) - .child( - div() - .px_1() - .flex() - .items_center() - .gap_1() - .children(has_fs_conflict.then(|| { - IconElement::new(Icon::ExclamationTriangle) - .size(crate::IconSize::Small) - .color(IconColor::Warning) - })) - .children(self.icon.map(IconElement::new)) - .children(if self.close_side == IconSide::Left { - Some(close_icon.clone()) - } else { - None - }) - .child(label) - .children(if self.close_side == IconSide::Right { - Some(close_icon) - } else { - None - }), - ) - } -} diff --git a/crates/ui/src/components/tab_bar.rs b/crates/ui/src/components/tab_bar.rs deleted file mode 100644 index 8addcb87b1..0000000000 --- a/crates/ui/src/components/tab_bar.rs +++ /dev/null @@ -1,85 +0,0 @@ -use std::marker::PhantomData; - -use crate::prelude::*; -use crate::{theme, Icon, IconButton, Tab}; - -#[derive(Element)] -pub struct TabBar { - view_type: PhantomData, - scroll_state: ScrollState, - tabs: Vec, -} - -impl TabBar { - pub fn new(tabs: Vec) -> Self { - Self { - view_type: PhantomData, - scroll_state: ScrollState::default(), - tabs, - } - } - - pub fn bind_scroll_state(&mut self, scroll_state: ScrollState) { - self.scroll_state = scroll_state; - } - - fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { - let theme = theme(cx); - let can_navigate_back = true; - let can_navigate_forward = false; - - div() - .w_full() - .flex() - .fill(theme.middle.base.default.background) - // Left Side - .child( - div() - .px_1() - .flex() - .flex_none() - .gap_2() - // Nav Buttons - .child( - div() - .flex() - .items_center() - .gap_px() - .child( - IconButton::new(Icon::ArrowLeft) - .state(InteractionState::Enabled.if_enabled(can_navigate_back)), - ) - .child( - IconButton::new(Icon::ArrowRight).state( - InteractionState::Enabled.if_enabled(can_navigate_forward), - ), - ), - ), - ) - .child( - div().w_0().flex_1().h_full().child( - div() - .flex() - .overflow_x_scroll(self.scroll_state.clone()) - .children(self.tabs.clone()), - ), - ) - // Right Side - .child( - div() - .px_1() - .flex() - .flex_none() - .gap_2() - // Nav Buttons - .child( - div() - .flex() - .items_center() - .gap_px() - .child(IconButton::new(Icon::Plus)) - .child(IconButton::new(Icon::Split)), - ), - ) - } -} diff --git a/crates/ui/src/components/terminal.rs b/crates/ui/src/components/terminal.rs deleted file mode 100644 index 909cb886ce..0000000000 --- a/crates/ui/src/components/terminal.rs +++ /dev/null @@ -1,84 +0,0 @@ -use std::sync::Arc; - -use gpui2::geometry::{relative, rems, Size}; - -use crate::prelude::*; -use crate::{theme, Icon, IconButton, Pane, Tab}; - -#[derive(Element)] -pub struct Terminal {} - -impl Terminal { - pub fn new() -> Self { - Self {} - } - - fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { - let theme = theme(cx); - - let can_navigate_back = true; - let can_navigate_forward = false; - - div() - .flex() - .flex_col() - .w_full() - .child( - // Terminal Tabs. - div() - .w_full() - .flex() - .fill(theme.middle.base.default.background) - .child( - div().px_1().flex().flex_none().gap_2().child( - div() - .flex() - .items_center() - .gap_px() - .child( - IconButton::new(Icon::ArrowLeft).state( - InteractionState::Enabled.if_enabled(can_navigate_back), - ), - ) - .child(IconButton::new(Icon::ArrowRight).state( - InteractionState::Enabled.if_enabled(can_navigate_forward), - )), - ), - ) - .child( - div().w_0().flex_1().h_full().child( - div() - .flex() - .child( - Tab::new() - .title("zed — fish".to_string()) - .icon(Icon::Terminal) - .close_side(IconSide::Right) - .current(true), - ) - .child( - Tab::new() - .title("zed — fish".to_string()) - .icon(Icon::Terminal) - .close_side(IconSide::Right) - .current(false), - ), - ), - ), - ) - // Terminal Pane. - .child(Pane::new( - ScrollState::default(), - Size { - width: relative(1.).into(), - height: rems(36.).into(), - }, - |_, payload| { - let theme = payload.downcast_ref::>().unwrap(); - - vec![crate::static_data::terminal_buffer(&theme).into_any()] - }, - Box::new(theme), - )) - } -} diff --git a/crates/ui/src/components/theme_selector.rs b/crates/ui/src/components/theme_selector.rs deleted file mode 100644 index e6f5237afe..0000000000 --- a/crates/ui/src/components/theme_selector.rs +++ /dev/null @@ -1,37 +0,0 @@ -use crate::prelude::*; -use crate::{OrderMethod, Palette, PaletteItem}; - -#[derive(Element)] -pub struct ThemeSelector { - scroll_state: ScrollState, -} - -impl ThemeSelector { - pub fn new() -> Self { - Self { - scroll_state: ScrollState::default(), - } - } - - fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { - div().child( - Palette::new(self.scroll_state.clone()) - .items(vec![ - PaletteItem::new("One Dark"), - PaletteItem::new("Rosé Pine"), - PaletteItem::new("Rosé Pine Moon"), - PaletteItem::new("Sandcastle"), - PaletteItem::new("Solarized Dark"), - PaletteItem::new("Summercamp"), - PaletteItem::new("Atelier Cave Light"), - PaletteItem::new("Atelier Dune Light"), - PaletteItem::new("Atelier Estuary Light"), - PaletteItem::new("Atelier Forest Light"), - PaletteItem::new("Atelier Heath Light"), - ]) - .placeholder("Select Theme...") - .empty_string("No matches") - .default_order(OrderMethod::Ascending), - ) - } -} diff --git a/crates/ui/src/components/title_bar.rs b/crates/ui/src/components/title_bar.rs deleted file mode 100644 index dd3d6c1fba..0000000000 --- a/crates/ui/src/components/title_bar.rs +++ /dev/null @@ -1,117 +0,0 @@ -use std::marker::PhantomData; -use std::sync::atomic::AtomicBool; -use std::sync::Arc; - -use crate::{prelude::*, PlayerWithCallStatus}; -use crate::{ - theme, Avatar, Button, Icon, IconButton, IconColor, PlayerStack, ToolDivider, TrafficLights, -}; - -#[derive(Clone)] -pub struct Livestream { - pub players: Vec, - pub channel: Option, // projects - // windows -} - -#[derive(Element)] -pub struct TitleBar { - view_type: PhantomData, - /// If the window is active from the OS's perspective. - is_active: Arc, - livestream: Option, -} - -impl TitleBar { - pub fn new(cx: &mut ViewContext) -> Self { - let is_active = Arc::new(AtomicBool::new(true)); - let active = is_active.clone(); - - cx.observe_window_activation(move |_, is_active, cx| { - active.store(is_active, std::sync::atomic::Ordering::SeqCst); - cx.notify(); - }) - .detach(); - - Self { - view_type: PhantomData, - is_active, - livestream: None, - } - } - - pub fn set_livestream(mut self, livestream: Option) -> Self { - self.livestream = livestream; - self - } - - fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { - let theme = theme(cx); - let has_focus = cx.window_is_active(); - - let player_list = if let Some(livestream) = &self.livestream { - livestream.players.clone().into_iter() - } else { - vec![].into_iter() - }; - - div() - .flex() - .items_center() - .justify_between() - .w_full() - .h_8() - .fill(theme.lowest.base.default.background) - .child( - div() - .flex() - .items_center() - .h_full() - .gap_4() - .px_2() - .child(TrafficLights::new().window_has_focus(has_focus)) - // === Project Info === // - .child( - div() - .flex() - .items_center() - .gap_1() - .child(Button::new("zed")) - .child(Button::new("nate/gpui2-ui-components")), - ) - .children(player_list.map(|p| PlayerStack::new(p))) - .child(IconButton::new(Icon::Plus)), - ) - .child( - div() - .flex() - .items_center() - .child( - div() - .px_2() - .flex() - .items_center() - .gap_1() - .child(IconButton::new(Icon::FolderX)) - .child(IconButton::new(Icon::Close)), - ) - .child(ToolDivider::new()) - .child( - div() - .px_2() - .flex() - .items_center() - .gap_1() - .child(IconButton::new(Icon::Mic)) - .child(IconButton::new(Icon::AudioOn)) - .child(IconButton::new(Icon::Screen).color(IconColor::Accent)), - ) - .child( - div().px_2().flex().items_center().child( - Avatar::new("https://avatars.githubusercontent.com/u/1714999?v=4") - .shape(Shape::RoundedRectangle), - ), - ), - ) - } -} diff --git a/crates/ui/src/components/toast.rs b/crates/ui/src/components/toast.rs deleted file mode 100644 index c299cdd6bc..0000000000 --- a/crates/ui/src/components/toast.rs +++ /dev/null @@ -1,66 +0,0 @@ -use crate::prelude::*; - -#[derive(Default, Debug, PartialEq, Eq, Clone, Copy)] -pub enum ToastOrigin { - #[default] - Bottom, - BottomRight, -} - -#[derive(Default, Debug, PartialEq, Eq, Clone, Copy)] -pub enum ToastVariant { - #[default] - Toast, - Status, -} - -/// A toast is a small, temporary window that appears to show a message to the user -/// or indicate a required action. -/// -/// Toasts should not persist on the screen for more than a few seconds unless -/// they are actively showing the a process in progress. -/// -/// Only one toast may be visible at a time. -#[derive(Element)] -pub struct Toast { - origin: ToastOrigin, - children: HackyChildren, - payload: HackyChildrenPayload, -} - -impl Toast { - pub fn new( - origin: ToastOrigin, - children: HackyChildren, - payload: HackyChildrenPayload, - ) -> Self { - Self { - origin, - children, - payload, - } - } - - fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { - let color = ThemeColor::new(cx); - - let mut div = div(); - - if self.origin == ToastOrigin::Bottom { - div = div.right_1_2(); - } else { - div = div.right_4(); - } - - div.absolute() - .bottom_4() - .flex() - .py_2() - .px_1p5() - .min_w_40() - .rounded_md() - .fill(color.elevated_surface) - .max_w_64() - .children_any((self.children)(cx, self.payload.as_ref())) - } -} diff --git a/crates/ui/src/components/toolbar.rs b/crates/ui/src/components/toolbar.rs deleted file mode 100644 index e0953bf3b2..0000000000 --- a/crates/ui/src/components/toolbar.rs +++ /dev/null @@ -1,49 +0,0 @@ -use crate::prelude::*; -use crate::theme; - -#[derive(Clone)] -pub struct ToolbarItem {} - -#[derive(Element)] -pub struct Toolbar { - left_items: HackyChildren, - left_items_payload: HackyChildrenPayload, - right_items: HackyChildren, - right_items_payload: HackyChildrenPayload, -} - -impl Toolbar { - pub fn new( - left_items: HackyChildren, - left_items_payload: HackyChildrenPayload, - right_items: HackyChildren, - right_items_payload: HackyChildrenPayload, - ) -> Self { - Self { - left_items, - left_items_payload, - right_items, - right_items_payload, - } - } - - fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { - let theme = theme(cx); - - div() - .fill(theme.highest.base.default.background) - .p_2() - .flex() - .justify_between() - .child( - div() - .flex() - .children_any((self.left_items)(cx, self.left_items_payload.as_ref())), - ) - .child( - div() - .flex() - .children_any((self.right_items)(cx, self.right_items_payload.as_ref())), - ) - } -} diff --git a/crates/ui/src/components/traffic_lights.rs b/crates/ui/src/components/traffic_lights.rs deleted file mode 100644 index 0d644c49ca..0000000000 --- a/crates/ui/src/components/traffic_lights.rs +++ /dev/null @@ -1,78 +0,0 @@ -use crate::prelude::*; -use crate::{theme, token, SystemColor}; - -#[derive(Clone, Copy)] -enum TrafficLightColor { - Red, - Yellow, - Green, -} - -#[derive(Element)] -struct TrafficLight { - color: TrafficLightColor, - window_has_focus: bool, -} - -impl TrafficLight { - fn new(color: TrafficLightColor, window_has_focus: bool) -> Self { - Self { - color, - window_has_focus, - } - } - - fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { - let theme = theme(cx); - let system_color = SystemColor::new(); - - let fill = match (self.window_has_focus, self.color) { - (true, TrafficLightColor::Red) => system_color.mac_os_traffic_light_red, - (true, TrafficLightColor::Yellow) => system_color.mac_os_traffic_light_yellow, - (true, TrafficLightColor::Green) => system_color.mac_os_traffic_light_green, - (false, _) => theme.lowest.base.active.background, - }; - - div().w_3().h_3().rounded_full().fill(fill) - } -} - -#[derive(Element)] -pub struct TrafficLights { - window_has_focus: bool, -} - -impl TrafficLights { - pub fn new() -> Self { - Self { - window_has_focus: true, - } - } - - pub fn window_has_focus(mut self, window_has_focus: bool) -> Self { - self.window_has_focus = window_has_focus; - self - } - - fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { - let theme = theme(cx); - let token = token(); - - div() - .flex() - .items_center() - .gap_2() - .child(TrafficLight::new( - TrafficLightColor::Red, - self.window_has_focus, - )) - .child(TrafficLight::new( - TrafficLightColor::Yellow, - self.window_has_focus, - )) - .child(TrafficLight::new( - TrafficLightColor::Green, - self.window_has_focus, - )) - } -} diff --git a/crates/ui/src/components/workspace.rs b/crates/ui/src/components/workspace.rs deleted file mode 100644 index b3d375bd64..0000000000 --- a/crates/ui/src/components/workspace.rs +++ /dev/null @@ -1,186 +0,0 @@ -use std::sync::Arc; - -use chrono::DateTime; -use gpui2::geometry::{relative, rems, Size}; - -use crate::{ - hello_world_rust_editor_with_status_example, prelude::*, random_players_with_call_status, - Livestream, -}; -use crate::{ - theme, v_stack, ChatMessage, ChatPanel, EditorPane, Pane, PaneGroup, Panel, PanelAllowedSides, - PanelSide, ProjectPanel, SplitDirection, StatusBar, Terminal, TitleBar, -}; - -#[derive(Element, Default)] -pub struct WorkspaceElement { - left_panel_scroll_state: ScrollState, - right_panel_scroll_state: ScrollState, - tab_bar_scroll_state: ScrollState, - bottom_panel_scroll_state: ScrollState, -} - -impl WorkspaceElement { - fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { - let theme = theme(cx).clone(); - - let temp_size = rems(36.).into(); - - let root_group = PaneGroup::new_groups( - vec![ - PaneGroup::new_panes( - vec![ - Pane::new( - ScrollState::default(), - Size { - width: relative(1.).into(), - height: temp_size, - }, - |_, payload| { - let theme = payload.downcast_ref::>().unwrap(); - - vec![EditorPane::new(hello_world_rust_editor_with_status_example( - &theme, - )) - .into_any()] - }, - Box::new(theme.clone()), - ), - Pane::new( - ScrollState::default(), - Size { - width: relative(1.).into(), - height: temp_size, - }, - |_, _| vec![Terminal::new().into_any()], - Box::new(()), - ), - ], - SplitDirection::Vertical, - ), - PaneGroup::new_panes( - vec![Pane::new( - ScrollState::default(), - Size { - width: relative(1.).into(), - height: relative(1.).into(), - }, - |_, payload| { - let theme = payload.downcast_ref::>().unwrap(); - - vec![EditorPane::new(hello_world_rust_editor_with_status_example( - &theme, - )) - .into_any()] - }, - Box::new(theme.clone()), - )], - SplitDirection::Vertical, - ), - ], - SplitDirection::Horizontal, - ); - - div() - .relative() - .size_full() - .flex() - .flex_col() - .font("Zed Sans Extended") - .gap_0() - .justify_start() - .items_start() - .text_color(theme.lowest.base.default.foreground) - .fill(theme.lowest.base.default.background) - .child(TitleBar::new(cx).set_livestream(Some(Livestream { - players: random_players_with_call_status(7), - channel: Some("gpui2-ui".to_string()), - }))) - .child( - div() - .flex_1() - .w_full() - .flex() - .flex_row() - .overflow_hidden() - .border_t() - .border_b() - .border_color(theme.lowest.base.default.border) - .child( - Panel::new( - self.left_panel_scroll_state.clone(), - |_, payload| vec![ProjectPanel::new(ScrollState::default()).into_any()], - Box::new(()), - ) - .side(PanelSide::Left), - ) - .child( - v_stack() - .flex_1() - .h_full() - .child( - div() - .flex() - .flex_1() - // CSS Hack: Flex 1 has to have a set height to properly fill the space - // Or it will give you a height of 0 - .h_px() - .child(root_group), - ) - .child( - Panel::new( - self.bottom_panel_scroll_state.clone(), - |_, _| vec![Terminal::new().into_any()], - Box::new(()), - ) - .allowed_sides(PanelAllowedSides::BottomOnly) - .side(PanelSide::Bottom), - ), - ) - .child( - Panel::new( - self.right_panel_scroll_state.clone(), - |_, payload| { - vec![ChatPanel::new(ScrollState::default()) - .with_messages(vec![ - ChatMessage::new( - "osiewicz".to_string(), - "is this thing on?".to_string(), - DateTime::parse_from_rfc3339( - "2023-09-27T15:40:52.707Z", - ) - .unwrap() - .naive_local(), - ), - ChatMessage::new( - "maxdeviant".to_string(), - "Reading you loud and clear!".to_string(), - DateTime::parse_from_rfc3339( - "2023-09-28T15:40:52.707Z", - ) - .unwrap() - .naive_local(), - ), - ]) - .into_any()] - }, - Box::new(()), - ) - .side(PanelSide::Right), - ), - ) - .child(StatusBar::new()) - // An example of a toast is below - // Currently because of stacking order this gets obscured by other elements - - // .child(Toast::new( - // ToastOrigin::Bottom, - // |_, payload| { - // let theme = payload.downcast_ref::>().unwrap(); - - // vec![Label::new("label").into_any()] - // }, - // Box::new(theme.clone()), - // )) - } -} diff --git a/crates/ui/src/element_ext.rs b/crates/ui/src/element_ext.rs deleted file mode 100644 index 67352f0779..0000000000 --- a/crates/ui/src/element_ext.rs +++ /dev/null @@ -1,24 +0,0 @@ -use std::marker::PhantomData; - -use gpui2::Element; - -use crate::theme::{Theme, Themed}; - -pub trait ElementExt: Element { - fn themed(self, theme: Theme) -> Themed - where - Self: Sized; -} - -impl> ElementExt for E { - fn themed(self, theme: Theme) -> Themed - where - Self: Sized, - { - Themed { - child: self, - theme, - view_type: PhantomData, - } - } -} diff --git a/crates/ui/src/elements.rs b/crates/ui/src/elements.rs deleted file mode 100644 index c60902ae98..0000000000 --- a/crates/ui/src/elements.rs +++ /dev/null @@ -1,19 +0,0 @@ -mod avatar; -mod button; -mod details; -mod icon; -mod input; -mod label; -mod player; -mod stack; -mod tool_divider; - -pub use avatar::*; -pub use button::*; -pub use details::*; -pub use icon::*; -pub use input::*; -pub use label::*; -pub use player::*; -pub use stack::*; -pub use tool_divider::*; diff --git a/crates/ui/src/elements/avatar.rs b/crates/ui/src/elements/avatar.rs deleted file mode 100644 index 2072b0e501..0000000000 --- a/crates/ui/src/elements/avatar.rs +++ /dev/null @@ -1,41 +0,0 @@ -use gpui2::elements::img; -use gpui2::ArcCow; - -use crate::prelude::*; -use crate::theme; - -#[derive(Element, Clone)] -pub struct Avatar { - src: ArcCow<'static, str>, - shape: Shape, -} - -impl Avatar { - pub fn new(src: impl Into>) -> Self { - Self { - src: src.into(), - shape: Shape::Circle, - } - } - - pub fn shape(mut self, shape: Shape) -> Self { - self.shape = shape; - self - } - - fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { - let theme = theme(cx); - - let mut img = img(); - - if self.shape == Shape::Circle { - img = img.rounded_full(); - } else { - img = img.rounded_md(); - } - - img.uri(self.src.clone()) - .size_4() - .fill(theme.middle.warning.default.foreground) - } -} diff --git a/crates/ui/src/elements/button.rs b/crates/ui/src/elements/button.rs deleted file mode 100644 index c516b27908..0000000000 --- a/crates/ui/src/elements/button.rs +++ /dev/null @@ -1,203 +0,0 @@ -use std::rc::Rc; - -use gpui2::geometry::DefiniteLength; -use gpui2::platform::MouseButton; -use gpui2::{EventContext, Hsla, Interactive, WindowContext}; - -use crate::prelude::*; -use crate::{h_stack, theme, Icon, IconColor, IconElement, Label, LabelColor, LabelSize}; - -#[derive(Default, PartialEq, Clone, Copy)] -pub enum IconPosition { - #[default] - Left, - Right, -} - -#[derive(Default, Copy, Clone, PartialEq)] -pub enum ButtonVariant { - #[default] - Ghost, - Filled, -} - -struct ButtonHandlers { - click: Option)>>, -} - -impl Default for ButtonHandlers { - fn default() -> Self { - Self { click: None } - } -} - -#[derive(Element)] -pub struct Button { - label: String, - variant: ButtonVariant, - state: InteractionState, - icon: Option, - icon_position: Option, - width: Option, - handlers: ButtonHandlers, -} - -impl Button { - pub fn new(label: L) -> Self - where - L: Into, - { - Self { - label: label.into(), - variant: Default::default(), - state: Default::default(), - icon: None, - icon_position: None, - width: Default::default(), - handlers: ButtonHandlers::default(), - } - } - - pub fn ghost(label: L) -> Self - where - L: Into, - { - Self::new(label).variant(ButtonVariant::Ghost) - } - - pub fn variant(mut self, variant: ButtonVariant) -> Self { - self.variant = variant; - self - } - - pub fn state(mut self, state: InteractionState) -> Self { - self.state = state; - self - } - - pub fn icon(mut self, icon: Icon) -> Self { - self.icon = Some(icon); - self - } - - pub fn icon_position(mut self, icon_position: IconPosition) -> Self { - if self.icon.is_none() { - panic!("An icon must be present if an icon_position is provided."); - } - self.icon_position = Some(icon_position); - self - } - - pub fn width(mut self, width: Option) -> Self { - self.width = width; - self - } - - pub fn on_click(mut self, handler: impl Fn(&mut V, &mut EventContext) + 'static) -> Self { - self.handlers.click = Some(Rc::new(handler)); - self - } - - fn background_color(&self, cx: &mut ViewContext) -> Hsla { - let theme = theme(cx); - let system_color = SystemColor::new(); - - match (self.variant, self.state) { - (ButtonVariant::Ghost, InteractionState::Hovered) => { - theme.lowest.base.hovered.background - } - (ButtonVariant::Ghost, InteractionState::Active) => { - theme.lowest.base.pressed.background - } - (ButtonVariant::Filled, InteractionState::Enabled) => { - theme.lowest.on.default.background - } - (ButtonVariant::Filled, InteractionState::Hovered) => { - theme.lowest.on.hovered.background - } - (ButtonVariant::Filled, InteractionState::Active) => theme.lowest.on.pressed.background, - (ButtonVariant::Filled, InteractionState::Disabled) => { - theme.lowest.on.disabled.background - } - _ => system_color.transparent, - } - } - - fn label_color(&self) -> LabelColor { - match self.state { - InteractionState::Disabled => LabelColor::Disabled, - _ => Default::default(), - } - } - - fn icon_color(&self) -> IconColor { - match self.state { - InteractionState::Disabled => IconColor::Disabled, - _ => Default::default(), - } - } - - fn border_color(&self, cx: &WindowContext) -> Hsla { - let theme = theme(cx); - let system_color = SystemColor::new(); - - match self.state { - InteractionState::Focused => theme.lowest.accent.default.border, - _ => system_color.transparent, - } - } - - fn render_label(&self) -> Label { - Label::new(self.label.clone()) - .size(LabelSize::Small) - .color(self.label_color()) - } - - fn render_icon(&self, icon_color: IconColor) -> Option { - self.icon.map(|i| IconElement::new(i).color(icon_color)) - } - - fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { - let theme = theme(cx); - let icon_color = self.icon_color(); - let system_color = SystemColor::new(); - let border_color = self.border_color(cx); - - let mut el = h_stack() - .h_6() - .px_1() - .items_center() - .rounded_md() - .border() - .border_color(border_color) - .fill(self.background_color(cx)); - - match (self.icon, self.icon_position) { - (Some(_), Some(IconPosition::Left)) => { - el = el - .gap_1() - .child(self.render_label()) - .children(self.render_icon(icon_color)) - } - (Some(_), Some(IconPosition::Right)) => { - el = el - .gap_1() - .children(self.render_icon(icon_color)) - .child(self.render_label()) - } - (_, _) => el = el.child(self.render_label()), - } - - if let Some(width) = self.width { - el = el.w(width).justify_center(); - } - - if let Some(click_handler) = self.handlers.click.clone() { - el = el.on_mouse_down(MouseButton::Left, move |view, event, cx| { - click_handler(view, cx); - }); - } - - el - } -} diff --git a/crates/ui/src/elements/details.rs b/crates/ui/src/elements/details.rs deleted file mode 100644 index 9c829bcd41..0000000000 --- a/crates/ui/src/elements/details.rs +++ /dev/null @@ -1,33 +0,0 @@ -use crate::prelude::*; -use crate::theme; - -#[derive(Element, Clone)] -pub struct Details { - text: &'static str, - meta: Option<&'static str>, -} - -impl Details { - pub fn new(text: &'static str) -> Self { - Self { text, meta: None } - } - - pub fn meta_text(mut self, meta: &'static str) -> Self { - self.meta = Some(meta); - self - } - - fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { - let theme = theme(cx); - - div() - // .flex() - // .w_full() - .p_1() - .gap_0p5() - .text_xs() - .text_color(theme.lowest.base.default.foreground) - .child(self.text) - .children(self.meta.map(|m| m)) - } -} diff --git a/crates/ui/src/elements/icon.rs b/crates/ui/src/elements/icon.rs deleted file mode 100644 index 26bf7dab22..0000000000 --- a/crates/ui/src/elements/icon.rs +++ /dev/null @@ -1,185 +0,0 @@ -use std::sync::Arc; - -use gpui2::elements::svg; -use gpui2::Hsla; -use strum::EnumIter; - -use crate::prelude::*; -use crate::theme::theme; -use crate::Theme; - -#[derive(Default, PartialEq, Copy, Clone)] -pub enum IconSize { - Small, - #[default] - Large, -} - -#[derive(Default, PartialEq, Copy, Clone)] -pub enum IconColor { - #[default] - Default, - Muted, - Disabled, - Placeholder, - Accent, - Error, - Warning, - Success, - Info, -} - -impl IconColor { - pub fn color(self, theme: Arc) -> Hsla { - match self { - IconColor::Default => theme.lowest.base.default.foreground, - IconColor::Muted => theme.lowest.variant.default.foreground, - IconColor::Disabled => theme.lowest.base.disabled.foreground, - IconColor::Placeholder => theme.lowest.base.disabled.foreground, - IconColor::Accent => theme.lowest.accent.default.foreground, - IconColor::Error => theme.lowest.negative.default.foreground, - IconColor::Warning => theme.lowest.warning.default.foreground, - IconColor::Success => theme.lowest.positive.default.foreground, - IconColor::Info => theme.lowest.accent.default.foreground, - } - } -} - -#[derive(Default, PartialEq, Copy, Clone, EnumIter)] -pub enum Icon { - Ai, - ArrowLeft, - ArrowRight, - ArrowUpRight, - AudioOff, - AudioOn, - Bolt, - ChevronDown, - ChevronLeft, - ChevronRight, - ChevronUp, - Close, - ExclamationTriangle, - ExternalLink, - File, - FileGeneric, - FileDoc, - FileGit, - FileLock, - FileRust, - FileToml, - FileTree, - Folder, - FolderOpen, - FolderX, - #[default] - Hash, - InlayHint, - MagicWand, - MagnifyingGlass, - Maximize, - Menu, - MessageBubbles, - Mic, - MicMute, - Plus, - Quote, - Screen, - SelectAll, - Split, - SplitMessage, - Terminal, - XCircle, - Copilot, - Envelope, -} - -impl Icon { - pub fn path(self) -> &'static str { - match self { - Icon::Ai => "icons/ai.svg", - Icon::ArrowLeft => "icons/arrow_left.svg", - Icon::ArrowRight => "icons/arrow_right.svg", - Icon::ArrowUpRight => "icons/arrow_up_right.svg", - Icon::AudioOff => "icons/speaker-off.svg", - Icon::AudioOn => "icons/speaker-loud.svg", - Icon::Bolt => "icons/bolt.svg", - Icon::ChevronDown => "icons/chevron_down.svg", - Icon::ChevronLeft => "icons/chevron_left.svg", - Icon::ChevronRight => "icons/chevron_right.svg", - Icon::ChevronUp => "icons/chevron_up.svg", - Icon::Close => "icons/x.svg", - Icon::ExclamationTriangle => "icons/warning.svg", - Icon::ExternalLink => "icons/external_link.svg", - Icon::File => "icons/file.svg", - Icon::FileGeneric => "icons/file_icons/file.svg", - Icon::FileDoc => "icons/file_icons/book.svg", - Icon::FileGit => "icons/file_icons/git.svg", - Icon::FileLock => "icons/file_icons/lock.svg", - Icon::FileRust => "icons/file_icons/rust.svg", - Icon::FileToml => "icons/file_icons/toml.svg", - Icon::FileTree => "icons/project.svg", - Icon::Folder => "icons/file_icons/folder.svg", - Icon::FolderOpen => "icons/file_icons/folder_open.svg", - Icon::FolderX => "icons/stop_sharing.svg", - Icon::Hash => "icons/hash.svg", - Icon::InlayHint => "icons/inlay_hint.svg", - Icon::MagicWand => "icons/magic-wand.svg", - Icon::MagnifyingGlass => "icons/magnifying_glass.svg", - Icon::Maximize => "icons/maximize.svg", - Icon::Menu => "icons/menu.svg", - Icon::MessageBubbles => "icons/conversations.svg", - Icon::Mic => "icons/mic.svg", - Icon::MicMute => "icons/mic-mute.svg", - Icon::Plus => "icons/plus.svg", - Icon::Quote => "icons/quote.svg", - Icon::Screen => "icons/desktop.svg", - Icon::SelectAll => "icons/select-all.svg", - Icon::Split => "icons/split.svg", - Icon::SplitMessage => "icons/split_message.svg", - Icon::Terminal => "icons/terminal.svg", - Icon::XCircle => "icons/error.svg", - Icon::Copilot => "icons/copilot.svg", - Icon::Envelope => "icons/feedback.svg", - } - } -} - -#[derive(Element, Clone)] -pub struct IconElement { - icon: Icon, - color: IconColor, - size: IconSize, -} - -impl IconElement { - pub fn new(icon: Icon) -> Self { - Self { - icon, - color: IconColor::default(), - size: IconSize::default(), - } - } - - pub fn color(mut self, color: IconColor) -> Self { - self.color = color; - self - } - - pub fn size(mut self, size: IconSize) -> Self { - self.size = size; - self - } - - fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { - let theme = theme(cx); - let fill = self.color.color(theme); - - let sized_svg = match self.size { - IconSize::Small => svg().size_3p5(), - IconSize::Large => svg().size_4(), - }; - - sized_svg.flex_none().path(self.icon.path()).fill(fill) - } -} diff --git a/crates/ui/src/elements/input.rs b/crates/ui/src/elements/input.rs deleted file mode 100644 index fd860f30c2..0000000000 --- a/crates/ui/src/elements/input.rs +++ /dev/null @@ -1,106 +0,0 @@ -use crate::prelude::*; -use crate::theme; - -#[derive(Default, PartialEq)] -pub enum InputVariant { - #[default] - Ghost, - Filled, -} - -#[derive(Element)] -pub struct Input { - placeholder: &'static str, - value: String, - state: InteractionState, - variant: InputVariant, -} - -impl Input { - pub fn new(placeholder: &'static str) -> Self { - Self { - placeholder, - value: "".to_string(), - state: InteractionState::default(), - variant: InputVariant::default(), - } - } - - pub fn value(mut self, value: String) -> Self { - self.value = value; - self - } - - pub fn state(mut self, state: InteractionState) -> Self { - self.state = state; - self - } - - pub fn variant(mut self, variant: InputVariant) -> Self { - self.variant = variant; - self - } - - fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { - let theme = theme(cx); - - let text_el; - let text_color; - let background_color_default; - let background_color_active; - - let mut border_color_default = theme.middle.base.default.border; - let mut border_color_hover = theme.middle.base.hovered.border; - let mut border_color_active = theme.middle.base.pressed.border; - let border_color_focus = theme.middle.base.pressed.background; - - match self.variant { - InputVariant::Ghost => { - background_color_default = theme.middle.base.default.background; - background_color_active = theme.middle.base.active.background; - } - InputVariant::Filled => { - background_color_default = theme.middle.on.default.background; - background_color_active = theme.middle.on.active.background; - } - }; - - if self.state == InteractionState::Focused { - border_color_default = theme.players[0].cursor; - border_color_hover = theme.players[0].cursor; - border_color_active = theme.players[0].cursor; - } - - if self.state == InteractionState::Focused || self.state == InteractionState::Active { - text_el = self.value.clone(); - text_color = theme.lowest.base.default.foreground; - } else { - text_el = self.placeholder.to_string().clone(); - text_color = theme.lowest.base.disabled.foreground; - } - - div() - .h_7() - .w_full() - .px_2() - .border() - .border_color(border_color_default) - .fill(background_color_default) - .hover() - .border_color(border_color_hover) - .active() - .border_color(border_color_active) - .fill(background_color_active) - .flex() - .items_center() - .child( - div() - .flex() - .items_center() - .text_sm() - .text_color(text_color) - .child(text_el) - .child(div().text_color(theme.players[0].cursor).child("|")), - ) - } -} diff --git a/crates/ui/src/elements/label.rs b/crates/ui/src/elements/label.rs deleted file mode 100644 index e7b15aaf02..0000000000 --- a/crates/ui/src/elements/label.rs +++ /dev/null @@ -1,161 +0,0 @@ -use gpui2::{Hsla, WindowContext}; -use smallvec::SmallVec; - -use crate::prelude::*; -use crate::theme::theme; - -#[derive(Default, PartialEq, Copy, Clone)] -pub enum LabelColor { - #[default] - Default, - Muted, - Created, - Modified, - Deleted, - Disabled, - Hidden, - Placeholder, - Accent, -} - -impl LabelColor { - pub fn hsla(&self, cx: &WindowContext) -> Hsla { - let theme = theme(cx); - - match self { - Self::Default => theme.middle.base.default.foreground, - Self::Muted => theme.middle.variant.default.foreground, - Self::Created => theme.middle.positive.default.foreground, - Self::Modified => theme.middle.warning.default.foreground, - Self::Deleted => theme.middle.negative.default.foreground, - Self::Disabled => theme.middle.base.disabled.foreground, - Self::Hidden => theme.middle.variant.default.foreground, - Self::Placeholder => theme.middle.base.disabled.foreground, - Self::Accent => theme.middle.accent.default.foreground, - } - } -} - -#[derive(Default, PartialEq, Copy, Clone)] -pub enum LabelSize { - #[default] - Default, - Small, -} - -#[derive(Element, Clone)] -pub struct Label { - label: String, - color: LabelColor, - size: LabelSize, - highlight_indices: Vec, - strikethrough: bool, -} - -impl Label { - pub fn new(label: L) -> Self - where - L: Into, - { - Self { - label: label.into(), - color: LabelColor::Default, - size: LabelSize::Default, - highlight_indices: Vec::new(), - strikethrough: false, - } - } - - pub fn color(mut self, color: LabelColor) -> Self { - self.color = color; - self - } - - pub fn size(mut self, size: LabelSize) -> Self { - self.size = size; - self - } - - pub fn with_highlights(mut self, indices: Vec) -> Self { - self.highlight_indices = indices; - self - } - - pub fn set_strikethrough(mut self, strikethrough: bool) -> Self { - self.strikethrough = strikethrough; - self - } - - fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { - let theme = theme(cx); - - let highlight_color = theme.lowest.accent.default.foreground; - - let mut highlight_indices = self.highlight_indices.iter().copied().peekable(); - - let mut runs: SmallVec<[Run; 8]> = SmallVec::new(); - - for (char_ix, char) in self.label.char_indices() { - let mut color = self.color.hsla(cx); - - if let Some(highlight_ix) = highlight_indices.peek() { - if char_ix == *highlight_ix { - color = highlight_color; - - highlight_indices.next(); - } - } - - let last_run = runs.last_mut(); - - let start_new_run = if let Some(last_run) = last_run { - if color == last_run.color { - last_run.text.push(char); - false - } else { - true - } - } else { - true - }; - - if start_new_run { - runs.push(Run { - text: char.to_string(), - color, - }); - } - } - - div() - .flex() - .when(self.strikethrough, |this| { - this.relative().child( - div() - .absolute() - .top_px() - .my_auto() - .w_full() - .h_px() - .fill(LabelColor::Hidden.hsla(cx)), - ) - }) - .children(runs.into_iter().map(|run| { - let mut div = div(); - - if self.size == LabelSize::Small { - div = div.text_xs(); - } else { - div = div.text_sm(); - } - - div.text_color(run.color).child(run.text) - })) - } -} - -/// A run of text that receives the same style. -struct Run { - pub text: String, - pub color: Hsla, -} diff --git a/crates/ui/src/elements/player.rs b/crates/ui/src/elements/player.rs deleted file mode 100644 index 465542dc7f..0000000000 --- a/crates/ui/src/elements/player.rs +++ /dev/null @@ -1,133 +0,0 @@ -use gpui2::{Hsla, ViewContext}; - -use crate::theme; - -#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] -pub enum PlayerStatus { - #[default] - Offline, - Online, - InCall, - Away, - DoNotDisturb, - Invisible, -} - -#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] -pub enum MicStatus { - Muted, - #[default] - Unmuted, -} - -#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] -pub enum VideoStatus { - On, - #[default] - Off, -} - -#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] -pub enum ScreenShareStatus { - Shared, - #[default] - NotShared, -} - -#[derive(Clone)] -pub struct PlayerCallStatus { - pub mic_status: MicStatus, - /// Indicates if the player is currently speaking - /// And the intensity of the volume coming through - /// - /// 0.0 - 1.0 - pub voice_activity: f32, - pub video_status: VideoStatus, - pub screen_share_status: ScreenShareStatus, - pub in_current_project: bool, - pub disconnected: bool, - pub following: Option>, - pub followers: Option>, -} - -impl PlayerCallStatus { - pub fn new() -> Self { - Self { - mic_status: MicStatus::default(), - voice_activity: 0., - video_status: VideoStatus::default(), - screen_share_status: ScreenShareStatus::default(), - in_current_project: true, - disconnected: false, - following: None, - followers: None, - } - } -} - -#[derive(PartialEq, Clone)] -pub struct Player { - index: usize, - avatar_src: String, - username: String, - status: PlayerStatus, -} - -#[derive(Clone)] -pub struct PlayerWithCallStatus { - player: Player, - call_status: PlayerCallStatus, -} - -impl PlayerWithCallStatus { - pub fn new(player: Player, call_status: PlayerCallStatus) -> Self { - Self { - player, - call_status, - } - } - - pub fn get_player(&self) -> &Player { - &self.player - } - - pub fn get_call_status(&self) -> &PlayerCallStatus { - &self.call_status - } -} - -impl Player { - pub fn new(index: usize, avatar_src: String, username: String) -> Self { - Self { - index, - avatar_src, - username, - status: Default::default(), - } - } - - pub fn set_status(mut self, status: PlayerStatus) -> Self { - self.status = status; - self - } - - pub fn cursor_color(&self, cx: &mut ViewContext) -> Hsla { - let theme = theme(cx); - let index = self.index % 8; - theme.players[self.index].cursor - } - - pub fn selection_color(&self, cx: &mut ViewContext) -> Hsla { - let theme = theme(cx); - let index = self.index % 8; - theme.players[self.index].selection - } - - pub fn avatar_src(&self) -> &str { - &self.avatar_src - } - - pub fn index(&self) -> usize { - self.index - } -} diff --git a/crates/ui/src/elements/stack.rs b/crates/ui/src/elements/stack.rs deleted file mode 100644 index ef186f5ebe..0000000000 --- a/crates/ui/src/elements/stack.rs +++ /dev/null @@ -1,31 +0,0 @@ -use gpui2::elements::div::Div; - -use crate::prelude::*; - -pub trait Stack: StyleHelpers { - /// Horizontally stacks elements. - fn h_stack(self) -> Self { - self.flex().flex_row().items_center() - } - - /// Vertically stacks elements. - fn v_stack(self) -> Self { - self.flex().flex_col() - } -} - -impl Stack for Div {} - -/// Horizontally stacks elements. -/// -/// Sets `flex()`, `flex_row()`, `items_center()` -pub fn h_stack() -> Div { - div().h_stack() -} - -/// Vertically stacks elements. -/// -/// Sets `flex()`, `flex_col()` -pub fn v_stack() -> Div { - div().v_stack() -} diff --git a/crates/ui/src/elements/tool_divider.rs b/crates/ui/src/elements/tool_divider.rs deleted file mode 100644 index 8b5a191445..0000000000 --- a/crates/ui/src/elements/tool_divider.rs +++ /dev/null @@ -1,17 +0,0 @@ -use crate::prelude::*; -use crate::theme; - -#[derive(Element)] -pub struct ToolDivider {} - -impl ToolDivider { - pub fn new() -> Self { - Self {} - } - - fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { - let theme = theme(cx); - - div().w_px().h_3().fill(theme.lowest.base.default.border) - } -} diff --git a/crates/ui/src/lib.rs b/crates/ui/src/lib.rs deleted file mode 100644 index 39156d3ab4..0000000000 --- a/crates/ui/src/lib.rs +++ /dev/null @@ -1,20 +0,0 @@ -#![allow(dead_code, unused_variables)] - -mod children; -mod components; -mod element_ext; -mod elements; -pub mod prelude; -mod static_data; -mod theme; -mod tokens; - -pub use children::*; -pub use components::*; -pub use element_ext::*; -pub use elements::*; -pub use prelude::*; -pub use static_data::*; -pub use tokens::*; - -pub use crate::theme::*; diff --git a/crates/ui/src/prelude.rs b/crates/ui/src/prelude.rs deleted file mode 100644 index e0da3579e2..0000000000 --- a/crates/ui/src/prelude.rs +++ /dev/null @@ -1,274 +0,0 @@ -pub use gpui2::elements::div::{div, ScrollState}; -pub use gpui2::style::{StyleHelpers, Styleable}; -pub use gpui2::{Element, IntoElement, ParentElement, ViewContext}; - -pub use crate::{theme, ButtonVariant, HackyChildren, HackyChildrenPayload, InputVariant, Theme}; - -use gpui2::{hsla, rgb, Hsla, WindowContext}; -use strum::EnumIter; - -#[derive(Default)] -pub struct SystemColor { - pub transparent: Hsla, - pub mac_os_traffic_light_red: Hsla, - pub mac_os_traffic_light_yellow: Hsla, - pub mac_os_traffic_light_green: Hsla, -} - -impl SystemColor { - pub fn new() -> SystemColor { - SystemColor { - transparent: hsla(0.0, 0.0, 0.0, 0.0), - mac_os_traffic_light_red: rgb::(0xEC695E), - mac_os_traffic_light_yellow: rgb::(0xF4BF4F), - mac_os_traffic_light_green: rgb::(0x62C554), - } - } - pub fn color(&self) -> Hsla { - self.transparent - } -} - -#[derive(Clone, Copy)] -pub struct ThemeColor { - pub border: Hsla, - pub border_variant: Hsla, - /// The background color of an elevated surface, like a modal, tooltip or toast. - pub elevated_surface: Hsla, -} - -impl ThemeColor { - pub fn new(cx: &WindowContext) -> Self { - let theme = theme(cx); - - Self { - border: theme.lowest.base.default.border, - border_variant: theme.lowest.variant.default.border, - elevated_surface: theme.middle.base.default.background, - } - } -} - -#[derive(Default, PartialEq, EnumIter, Clone, Copy)] -pub enum HighlightColor { - #[default] - Default, - Comment, - String, - Function, - Keyword, -} - -impl HighlightColor { - pub fn hsla(&self, theme: &Theme) -> Hsla { - let system_color = SystemColor::new(); - - match self { - Self::Default => theme - .syntax - .get("primary") - .expect("no theme.syntax.primary") - .clone(), - Self::Comment => theme - .syntax - .get("comment") - .expect("no theme.syntax.comment") - .clone(), - Self::String => theme - .syntax - .get("string") - .expect("no theme.syntax.string") - .clone(), - Self::Function => theme - .syntax - .get("function") - .expect("no theme.syntax.function") - .clone(), - Self::Keyword => theme - .syntax - .get("keyword") - .expect("no theme.syntax.keyword") - .clone(), - } - } -} - -#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)] -pub enum FileSystemStatus { - #[default] - None, - Conflict, - Deleted, -} - -impl FileSystemStatus { - pub fn to_string(&self) -> String { - match self { - Self::None => "None".to_string(), - Self::Conflict => "Conflict".to_string(), - Self::Deleted => "Deleted".to_string(), - } - } -} - -#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)] -pub enum GitStatus { - #[default] - None, - Created, - Modified, - Deleted, - Conflict, - Renamed, -} - -impl GitStatus { - pub fn to_string(&self) -> String { - match self { - Self::None => "None".to_string(), - Self::Created => "Created".to_string(), - Self::Modified => "Modified".to_string(), - Self::Deleted => "Deleted".to_string(), - Self::Conflict => "Conflict".to_string(), - Self::Renamed => "Renamed".to_string(), - } - } - - pub fn hsla(&self, cx: &WindowContext) -> Hsla { - let theme = theme(cx); - let system_color = SystemColor::new(); - - match self { - Self::None => system_color.transparent, - Self::Created => theme.lowest.positive.default.foreground, - Self::Modified => theme.lowest.warning.default.foreground, - Self::Deleted => theme.lowest.negative.default.foreground, - Self::Conflict => theme.lowest.warning.default.foreground, - Self::Renamed => theme.lowest.accent.default.foreground, - } - } -} - -#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)] -pub enum DiagnosticStatus { - #[default] - None, - Error, - Warning, - Info, -} - -#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)] -pub enum IconSide { - #[default] - Left, - Right, -} - -#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)] -pub enum OrderMethod { - #[default] - Ascending, - Descending, - MostRecent, -} - -#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)] -pub enum Shape { - #[default] - Circle, - RoundedRectangle, -} - -#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)] -pub enum DisclosureControlVisibility { - #[default] - OnHover, - Always, -} - -#[derive(Default, PartialEq, Copy, Clone, EnumIter, strum::Display)] -pub enum InteractionState { - #[default] - Enabled, - Hovered, - Active, - Focused, - Disabled, -} - -impl InteractionState { - pub fn if_enabled(&self, enabled: bool) -> Self { - if enabled { - *self - } else { - InteractionState::Disabled - } - } -} - -#[derive(Default, PartialEq)] -pub enum SelectedState { - #[default] - Unselected, - PartiallySelected, - Selected, -} - -#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] -pub enum Toggleable { - Toggleable(ToggleState), - #[default] - NotToggleable, -} - -impl Toggleable { - pub fn is_toggled(&self) -> bool { - match self { - Self::Toggleable(ToggleState::Toggled) => true, - _ => false, - } - } -} - -impl From for Toggleable { - fn from(state: ToggleState) -> Self { - Self::Toggleable(state) - } -} - -#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] -pub enum ToggleState { - /// The "on" state of a toggleable element. - /// - /// Example: - /// - A collasable list that is currently expanded - /// - A toggle button that is currently on. - Toggled, - /// The "off" state of a toggleable element. - /// - /// Example: - /// - A collasable list that is currently collapsed - /// - A toggle button that is currently off. - #[default] - NotToggled, -} - -impl From for ToggleState { - fn from(toggleable: Toggleable) -> Self { - match toggleable { - Toggleable::Toggleable(state) => state, - Toggleable::NotToggleable => ToggleState::NotToggled, - } - } -} - -impl From for ToggleState { - fn from(toggled: bool) -> Self { - if toggled { - ToggleState::Toggled - } else { - ToggleState::NotToggled - } - } -} diff --git a/crates/ui/src/static_data.rs b/crates/ui/src/static_data.rs deleted file mode 100644 index b8c4e18f14..0000000000 --- a/crates/ui/src/static_data.rs +++ /dev/null @@ -1,966 +0,0 @@ -use std::path::PathBuf; -use std::str::FromStr; - -use rand::Rng; - -use crate::{ - Buffer, BufferRow, BufferRows, Editor, FileSystemStatus, GitStatus, HighlightColor, - HighlightedLine, HighlightedText, Icon, Keybinding, Label, LabelColor, ListEntry, - ListEntrySize, ListItem, Livestream, MicStatus, ModifierKeys, PaletteItem, Player, - PlayerCallStatus, PlayerWithCallStatus, ScreenShareStatus, Symbol, Tab, Theme, ToggleState, - VideoStatus, -}; - -pub fn static_tabs_example() -> Vec { - vec![ - Tab::new() - .title("wip.rs".to_string()) - .icon(Icon::FileRust) - .current(false) - .fs_status(FileSystemStatus::Deleted), - Tab::new() - .title("Cargo.toml".to_string()) - .icon(Icon::FileToml) - .current(false) - .git_status(GitStatus::Modified), - Tab::new() - .title("Channels Panel".to_string()) - .icon(Icon::Hash) - .current(false), - Tab::new() - .title("channels_panel.rs".to_string()) - .icon(Icon::FileRust) - .current(true) - .git_status(GitStatus::Modified), - Tab::new() - .title("workspace.rs".to_string()) - .current(false) - .icon(Icon::FileRust) - .git_status(GitStatus::Modified), - Tab::new() - .title("icon_button.rs".to_string()) - .icon(Icon::FileRust) - .current(false), - Tab::new() - .title("storybook.rs".to_string()) - .icon(Icon::FileRust) - .current(false) - .git_status(GitStatus::Created), - Tab::new() - .title("theme.rs".to_string()) - .icon(Icon::FileRust) - .current(false), - Tab::new() - .title("theme_registry.rs".to_string()) - .icon(Icon::FileRust) - .current(false), - Tab::new() - .title("styleable_helpers.rs".to_string()) - .icon(Icon::FileRust) - .current(false), - ] -} - -pub fn static_tabs_1() -> Vec { - vec![ - Tab::new() - .title("project_panel.rs".to_string()) - .icon(Icon::FileRust) - .current(false) - .fs_status(FileSystemStatus::Deleted), - Tab::new() - .title("tab_bar.rs".to_string()) - .icon(Icon::FileRust) - .current(false) - .git_status(GitStatus::Modified), - Tab::new() - .title("workspace.rs".to_string()) - .icon(Icon::FileRust) - .current(false), - Tab::new() - .title("tab.rs".to_string()) - .icon(Icon::FileRust) - .current(true) - .git_status(GitStatus::Modified), - ] -} - -pub fn static_tabs_2() -> Vec { - vec![ - Tab::new() - .title("tab_bar.rs".to_string()) - .icon(Icon::FileRust) - .current(false) - .fs_status(FileSystemStatus::Deleted), - Tab::new() - .title("static_data.rs".to_string()) - .icon(Icon::FileRust) - .current(true) - .git_status(GitStatus::Modified), - ] -} - -pub fn static_tabs_3() -> Vec { - vec![Tab::new().git_status(GitStatus::Created).current(true)] -} - -pub fn static_players() -> Vec { - vec![ - Player::new( - 0, - "https://avatars.githubusercontent.com/u/1714999?v=4".into(), - "nathansobo".into(), - ), - Player::new( - 1, - "https://avatars.githubusercontent.com/u/326587?v=4".into(), - "maxbrunsfeld".into(), - ), - Player::new( - 2, - "https://avatars.githubusercontent.com/u/482957?v=4".into(), - "as-cii".into(), - ), - Player::new( - 3, - "https://avatars.githubusercontent.com/u/1714999?v=4".into(), - "iamnbutler".into(), - ), - Player::new( - 4, - "https://avatars.githubusercontent.com/u/1486634?v=4".into(), - "maxdeviant".into(), - ), - ] -} - -#[derive(Debug)] -pub struct PlayerData { - pub url: String, - pub name: String, -} -pub fn static_player_data() -> Vec { - vec![ - PlayerData { - url: "https://avatars.githubusercontent.com/u/1714999?v=4".into(), - name: "iamnbutler".into(), - }, - PlayerData { - url: "https://avatars.githubusercontent.com/u/326587?v=4".into(), - name: "maxbrunsfeld".into(), - }, - PlayerData { - url: "https://avatars.githubusercontent.com/u/482957?v=4".into(), - name: "as-cii".into(), - }, - PlayerData { - url: "https://avatars.githubusercontent.com/u/1789?v=4".into(), - name: "nathansobo".into(), - }, - PlayerData { - url: "https://avatars.githubusercontent.com/u/1486634?v=4".into(), - name: "ForLoveOfCats".into(), - }, - PlayerData { - url: "https://avatars.githubusercontent.com/u/2690773?v=4".into(), - name: "SomeoneToIgnore".into(), - }, - PlayerData { - url: "https://avatars.githubusercontent.com/u/19867440?v=4".into(), - name: "JosephTLyons".into(), - }, - PlayerData { - url: "https://avatars.githubusercontent.com/u/24362066?v=4".into(), - name: "osiewicz".into(), - }, - PlayerData { - url: "https://avatars.githubusercontent.com/u/22121886?v=4".into(), - name: "KCaverly".into(), - }, - PlayerData { - url: "https://avatars.githubusercontent.com/u/1486634?v=4".into(), - name: "maxdeviant".into(), - }, - ] -} -pub fn create_static_players(player_data: Vec) -> Vec { - let mut players = Vec::new(); - for data in player_data { - players.push(Player::new(players.len(), data.url, data.name)); - } - players -} -pub fn static_player_1(data: &Vec) -> Player { - Player::new(1, data[0].url.clone(), data[0].name.clone()) -} -pub fn static_player_2(data: &Vec) -> Player { - Player::new(2, data[1].url.clone(), data[1].name.clone()) -} -pub fn static_player_3(data: &Vec) -> Player { - Player::new(3, data[2].url.clone(), data[2].name.clone()) -} -pub fn static_player_4(data: &Vec) -> Player { - Player::new(4, data[3].url.clone(), data[3].name.clone()) -} -pub fn static_player_5(data: &Vec) -> Player { - Player::new(5, data[4].url.clone(), data[4].name.clone()) -} -pub fn static_player_6(data: &Vec) -> Player { - Player::new(6, data[5].url.clone(), data[5].name.clone()) -} -pub fn static_player_7(data: &Vec) -> Player { - Player::new(7, data[6].url.clone(), data[6].name.clone()) -} -pub fn static_player_8(data: &Vec) -> Player { - Player::new(8, data[7].url.clone(), data[7].name.clone()) -} -pub fn static_player_9(data: &Vec) -> Player { - Player::new(9, data[8].url.clone(), data[8].name.clone()) -} -pub fn static_player_10(data: &Vec) -> Player { - Player::new(10, data[9].url.clone(), data[9].name.clone()) -} -pub fn static_livestream() -> Livestream { - Livestream { - players: random_players_with_call_status(7), - channel: Some("gpui2-ui".to_string()), - } -} -pub fn populate_player_call_status( - player: Player, - followers: Option>, -) -> PlayerCallStatus { - let mut rng = rand::thread_rng(); - let in_current_project: bool = rng.gen(); - let disconnected: bool = rng.gen(); - let voice_activity: f32 = rng.gen(); - let mic_status = if rng.gen_bool(0.5) { - MicStatus::Muted - } else { - MicStatus::Unmuted - }; - let video_status = if rng.gen_bool(0.5) { - VideoStatus::On - } else { - VideoStatus::Off - }; - let screen_share_status = if rng.gen_bool(0.5) { - ScreenShareStatus::Shared - } else { - ScreenShareStatus::NotShared - }; - PlayerCallStatus { - mic_status, - voice_activity, - video_status, - screen_share_status, - in_current_project, - disconnected, - following: None, - followers, - } -} -pub fn random_players_with_call_status(number_of_players: usize) -> Vec { - let players = create_static_players(static_player_data()); - let mut player_status = vec![]; - for i in 0..number_of_players { - let followers = if i == 0 { - Some(vec![ - players[1].clone(), - players[3].clone(), - players[5].clone(), - players[6].clone(), - ]) - } else if i == 1 { - Some(vec![players[2].clone(), players[6].clone()]) - } else { - None - }; - let call_status = populate_player_call_status(players[i].clone(), followers); - player_status.push(PlayerWithCallStatus::new(players[i].clone(), call_status)); - } - player_status -} - -pub fn static_players_with_call_status() -> Vec { - let players = static_players(); - let mut player_0_status = PlayerCallStatus::new(); - let player_1_status = PlayerCallStatus::new(); - let player_2_status = PlayerCallStatus::new(); - let mut player_3_status = PlayerCallStatus::new(); - let mut player_4_status = PlayerCallStatus::new(); - - player_0_status.screen_share_status = ScreenShareStatus::Shared; - player_0_status.followers = Some(vec![players[1].clone(), players[3].clone()]); - - player_3_status.voice_activity = 0.5; - player_4_status.mic_status = MicStatus::Muted; - player_4_status.in_current_project = false; - - vec![ - PlayerWithCallStatus::new(players[0].clone(), player_0_status), - PlayerWithCallStatus::new(players[1].clone(), player_1_status), - PlayerWithCallStatus::new(players[2].clone(), player_2_status), - PlayerWithCallStatus::new(players[3].clone(), player_3_status), - PlayerWithCallStatus::new(players[4].clone(), player_4_status), - ] -} - -pub fn static_project_panel_project_items() -> Vec { - vec![ - ListEntry::new(Label::new("zed")) - .left_icon(Icon::FolderOpen.into()) - .indent_level(0) - .set_toggle(ToggleState::Toggled), - ListEntry::new(Label::new(".cargo")) - .left_icon(Icon::Folder.into()) - .indent_level(1), - ListEntry::new(Label::new(".config")) - .left_icon(Icon::Folder.into()) - .indent_level(1), - ListEntry::new(Label::new(".git").color(LabelColor::Hidden)) - .left_icon(Icon::Folder.into()) - .indent_level(1), - ListEntry::new(Label::new(".cargo")) - .left_icon(Icon::Folder.into()) - .indent_level(1), - ListEntry::new(Label::new(".idea").color(LabelColor::Hidden)) - .left_icon(Icon::Folder.into()) - .indent_level(1), - ListEntry::new(Label::new("assets")) - .left_icon(Icon::Folder.into()) - .indent_level(1) - .set_toggle(ToggleState::Toggled), - ListEntry::new(Label::new("cargo-target").color(LabelColor::Hidden)) - .left_icon(Icon::Folder.into()) - .indent_level(1), - ListEntry::new(Label::new("crates")) - .left_icon(Icon::FolderOpen.into()) - .indent_level(1) - .set_toggle(ToggleState::Toggled), - ListEntry::new(Label::new("activity_indicator")) - .left_icon(Icon::Folder.into()) - .indent_level(2), - ListEntry::new(Label::new("ai")) - .left_icon(Icon::Folder.into()) - .indent_level(2), - ListEntry::new(Label::new("audio")) - .left_icon(Icon::Folder.into()) - .indent_level(2), - ListEntry::new(Label::new("auto_update")) - .left_icon(Icon::Folder.into()) - .indent_level(2), - ListEntry::new(Label::new("breadcrumbs")) - .left_icon(Icon::Folder.into()) - .indent_level(2), - ListEntry::new(Label::new("call")) - .left_icon(Icon::Folder.into()) - .indent_level(2), - ListEntry::new(Label::new("sqlez").color(LabelColor::Modified)) - .left_icon(Icon::Folder.into()) - .indent_level(2) - .set_toggle(ToggleState::NotToggled), - ListEntry::new(Label::new("gpui2")) - .left_icon(Icon::FolderOpen.into()) - .indent_level(2) - .set_toggle(ToggleState::Toggled), - ListEntry::new(Label::new("src")) - .left_icon(Icon::FolderOpen.into()) - .indent_level(3) - .set_toggle(ToggleState::Toggled), - ListEntry::new(Label::new("derive_element.rs")) - .left_icon(Icon::FileRust.into()) - .indent_level(4), - ListEntry::new(Label::new("storybook").color(LabelColor::Modified)) - .left_icon(Icon::FolderOpen.into()) - .indent_level(1) - .set_toggle(ToggleState::Toggled), - ListEntry::new(Label::new("docs").color(LabelColor::Default)) - .left_icon(Icon::Folder.into()) - .indent_level(2) - .set_toggle(ToggleState::Toggled), - ListEntry::new(Label::new("src").color(LabelColor::Modified)) - .left_icon(Icon::FolderOpen.into()) - .indent_level(3) - .set_toggle(ToggleState::Toggled), - ListEntry::new(Label::new("ui").color(LabelColor::Modified)) - .left_icon(Icon::FolderOpen.into()) - .indent_level(4) - .set_toggle(ToggleState::Toggled), - ListEntry::new(Label::new("component").color(LabelColor::Created)) - .left_icon(Icon::FolderOpen.into()) - .indent_level(5) - .set_toggle(ToggleState::Toggled), - ListEntry::new(Label::new("facepile.rs").color(LabelColor::Default)) - .left_icon(Icon::FileRust.into()) - .indent_level(6), - ListEntry::new(Label::new("follow_group.rs").color(LabelColor::Default)) - .left_icon(Icon::FileRust.into()) - .indent_level(6), - ListEntry::new(Label::new("list_item.rs").color(LabelColor::Created)) - .left_icon(Icon::FileRust.into()) - .indent_level(6), - ListEntry::new(Label::new("tab.rs").color(LabelColor::Default)) - .left_icon(Icon::FileRust.into()) - .indent_level(6), - ListEntry::new(Label::new("target").color(LabelColor::Hidden)) - .left_icon(Icon::Folder.into()) - .indent_level(1), - ListEntry::new(Label::new(".dockerignore")) - .left_icon(Icon::FileGeneric.into()) - .indent_level(1), - ListEntry::new(Label::new(".DS_Store").color(LabelColor::Hidden)) - .left_icon(Icon::FileGeneric.into()) - .indent_level(1), - ListEntry::new(Label::new("Cargo.lock")) - .left_icon(Icon::FileLock.into()) - .indent_level(1), - ListEntry::new(Label::new("Cargo.toml")) - .left_icon(Icon::FileToml.into()) - .indent_level(1), - ListEntry::new(Label::new("Dockerfile")) - .left_icon(Icon::FileGeneric.into()) - .indent_level(1), - ListEntry::new(Label::new("Procfile")) - .left_icon(Icon::FileGeneric.into()) - .indent_level(1), - ListEntry::new(Label::new("README.md")) - .left_icon(Icon::FileDoc.into()) - .indent_level(1), - ] - .into_iter() - .map(From::from) - .collect() -} - -pub fn static_project_panel_single_items() -> Vec { - vec![ - ListEntry::new(Label::new("todo.md")) - .left_icon(Icon::FileDoc.into()) - .indent_level(0), - ListEntry::new(Label::new("README.md")) - .left_icon(Icon::FileDoc.into()) - .indent_level(0), - ListEntry::new(Label::new("config.json")) - .left_icon(Icon::FileGeneric.into()) - .indent_level(0), - ] - .into_iter() - .map(From::from) - .collect() -} - -pub fn static_collab_panel_current_call() -> Vec { - vec![ - ListEntry::new(Label::new("as-cii")).left_avatar("http://github.com/as-cii.png?s=50"), - ListEntry::new(Label::new("nathansobo")) - .left_avatar("http://github.com/nathansobo.png?s=50"), - ListEntry::new(Label::new("maxbrunsfeld")) - .left_avatar("http://github.com/maxbrunsfeld.png?s=50"), - ] - .into_iter() - .map(From::from) - .collect() -} - -pub fn static_collab_panel_channels() -> Vec { - vec![ - ListEntry::new(Label::new("zed")) - .left_icon(Icon::Hash.into()) - .size(ListEntrySize::Medium) - .indent_level(0), - ListEntry::new(Label::new("community")) - .left_icon(Icon::Hash.into()) - .size(ListEntrySize::Medium) - .indent_level(1), - ListEntry::new(Label::new("dashboards")) - .left_icon(Icon::Hash.into()) - .size(ListEntrySize::Medium) - .indent_level(2), - ListEntry::new(Label::new("feedback")) - .left_icon(Icon::Hash.into()) - .size(ListEntrySize::Medium) - .indent_level(2), - ListEntry::new(Label::new("teams-in-channels-alpha")) - .left_icon(Icon::Hash.into()) - .size(ListEntrySize::Medium) - .indent_level(2), - ListEntry::new(Label::new("current-projects")) - .left_icon(Icon::Hash.into()) - .size(ListEntrySize::Medium) - .indent_level(1), - ListEntry::new(Label::new("codegen")) - .left_icon(Icon::Hash.into()) - .size(ListEntrySize::Medium) - .indent_level(2), - ListEntry::new(Label::new("gpui2")) - .left_icon(Icon::Hash.into()) - .size(ListEntrySize::Medium) - .indent_level(2), - ListEntry::new(Label::new("livestreaming")) - .left_icon(Icon::Hash.into()) - .size(ListEntrySize::Medium) - .indent_level(2), - ListEntry::new(Label::new("open-source")) - .left_icon(Icon::Hash.into()) - .size(ListEntrySize::Medium) - .indent_level(2), - ListEntry::new(Label::new("replace")) - .left_icon(Icon::Hash.into()) - .size(ListEntrySize::Medium) - .indent_level(2), - ListEntry::new(Label::new("semantic-index")) - .left_icon(Icon::Hash.into()) - .size(ListEntrySize::Medium) - .indent_level(2), - ListEntry::new(Label::new("vim")) - .left_icon(Icon::Hash.into()) - .size(ListEntrySize::Medium) - .indent_level(2), - ListEntry::new(Label::new("web-tech")) - .left_icon(Icon::Hash.into()) - .size(ListEntrySize::Medium) - .indent_level(2), - ] - .into_iter() - .map(From::from) - .collect() -} - -pub fn example_editor_actions() -> Vec { - vec![ - PaletteItem::new("New File").keybinding(Keybinding::new( - "N".to_string(), - ModifierKeys::new().control(true), - )), - PaletteItem::new("Open File").keybinding(Keybinding::new( - "O".to_string(), - ModifierKeys::new().control(true), - )), - PaletteItem::new("Save File").keybinding(Keybinding::new( - "S".to_string(), - ModifierKeys::new().control(true), - )), - PaletteItem::new("Cut").keybinding(Keybinding::new( - "X".to_string(), - ModifierKeys::new().control(true), - )), - PaletteItem::new("Copy").keybinding(Keybinding::new( - "C".to_string(), - ModifierKeys::new().control(true), - )), - PaletteItem::new("Paste").keybinding(Keybinding::new( - "V".to_string(), - ModifierKeys::new().control(true), - )), - PaletteItem::new("Undo").keybinding(Keybinding::new( - "Z".to_string(), - ModifierKeys::new().control(true), - )), - PaletteItem::new("Redo").keybinding(Keybinding::new( - "Z".to_string(), - ModifierKeys::new().control(true).shift(true), - )), - PaletteItem::new("Find").keybinding(Keybinding::new( - "F".to_string(), - ModifierKeys::new().control(true), - )), - PaletteItem::new("Replace").keybinding(Keybinding::new( - "R".to_string(), - ModifierKeys::new().control(true), - )), - PaletteItem::new("Jump to Line"), - PaletteItem::new("Select All"), - PaletteItem::new("Deselect All"), - PaletteItem::new("Switch Document"), - PaletteItem::new("Insert Line Below"), - PaletteItem::new("Insert Line Above"), - PaletteItem::new("Move Line Up"), - PaletteItem::new("Move Line Down"), - PaletteItem::new("Toggle Comment"), - PaletteItem::new("Delete Line"), - ] -} - -pub fn empty_editor_example() -> Editor { - Editor { - tabs: static_tabs_example(), - path: PathBuf::from_str("crates/ui/src/static_data.rs").unwrap(), - symbols: vec![], - buffer: empty_buffer_example(), - } -} - -pub fn empty_buffer_example() -> Buffer { - Buffer::new().set_rows(Some(BufferRows::default())) -} - -pub fn hello_world_rust_editor_example(theme: &Theme) -> Editor { - Editor { - tabs: static_tabs_example(), - path: PathBuf::from_str("crates/ui/src/static_data.rs").unwrap(), - symbols: vec![Symbol(vec![ - HighlightedText { - text: "fn ".to_string(), - color: HighlightColor::Keyword.hsla(&theme), - }, - HighlightedText { - text: "main".to_string(), - color: HighlightColor::Function.hsla(&theme), - }, - ])], - buffer: hello_world_rust_buffer_example(theme), - } -} - -pub fn hello_world_rust_buffer_example(theme: &Theme) -> Buffer { - Buffer::new() - .set_title("hello_world.rs".to_string()) - .set_path("src/hello_world.rs".to_string()) - .set_language("rust".to_string()) - .set_rows(Some(BufferRows { - show_line_numbers: true, - rows: hello_world_rust_buffer_rows(theme), - })) -} - -pub fn hello_world_rust_buffer_rows(theme: &Theme) -> Vec { - let show_line_number = true; - - vec![ - BufferRow { - line_number: 1, - code_action: false, - current: true, - line: Some(HighlightedLine { - highlighted_texts: vec![ - HighlightedText { - text: "fn ".to_string(), - color: HighlightColor::Keyword.hsla(&theme), - }, - HighlightedText { - text: "main".to_string(), - color: HighlightColor::Function.hsla(&theme), - }, - HighlightedText { - text: "() {".to_string(), - color: HighlightColor::Default.hsla(&theme), - }, - ], - }), - cursors: None, - status: GitStatus::None, - show_line_number, - }, - BufferRow { - line_number: 2, - code_action: false, - current: false, - line: Some(HighlightedLine { - highlighted_texts: vec![HighlightedText { - text: " // Statements here are executed when the compiled binary is called." - .to_string(), - color: HighlightColor::Comment.hsla(&theme), - }], - }), - cursors: None, - status: GitStatus::None, - show_line_number, - }, - BufferRow { - line_number: 3, - code_action: false, - current: false, - line: None, - cursors: None, - status: GitStatus::None, - show_line_number, - }, - BufferRow { - line_number: 4, - code_action: false, - current: false, - line: Some(HighlightedLine { - highlighted_texts: vec![HighlightedText { - text: " // Print text to the console.".to_string(), - color: HighlightColor::Comment.hsla(&theme), - }], - }), - cursors: None, - status: GitStatus::None, - show_line_number, - }, - BufferRow { - line_number: 5, - code_action: false, - current: false, - line: Some(HighlightedLine { - highlighted_texts: vec![ - HighlightedText { - text: " println!(".to_string(), - color: HighlightColor::Default.hsla(&theme), - }, - HighlightedText { - text: "\"Hello, world!\"".to_string(), - color: HighlightColor::String.hsla(&theme), - }, - HighlightedText { - text: ");".to_string(), - color: HighlightColor::Default.hsla(&theme), - }, - ], - }), - cursors: None, - status: GitStatus::None, - show_line_number, - }, - BufferRow { - line_number: 6, - code_action: false, - current: false, - line: Some(HighlightedLine { - highlighted_texts: vec![HighlightedText { - text: "}".to_string(), - color: HighlightColor::Default.hsla(&theme), - }], - }), - cursors: None, - status: GitStatus::None, - show_line_number, - }, - ] -} - -pub fn hello_world_rust_editor_with_status_example(theme: &Theme) -> Editor { - Editor { - tabs: static_tabs_example(), - path: PathBuf::from_str("crates/ui/src/static_data.rs").unwrap(), - symbols: vec![Symbol(vec![ - HighlightedText { - text: "fn ".to_string(), - color: HighlightColor::Keyword.hsla(&theme), - }, - HighlightedText { - text: "main".to_string(), - color: HighlightColor::Function.hsla(&theme), - }, - ])], - buffer: hello_world_rust_buffer_with_status_example(theme), - } -} - -pub fn hello_world_rust_buffer_with_status_example(theme: &Theme) -> Buffer { - Buffer::new() - .set_title("hello_world.rs".to_string()) - .set_path("src/hello_world.rs".to_string()) - .set_language("rust".to_string()) - .set_rows(Some(BufferRows { - show_line_numbers: true, - rows: hello_world_rust_with_status_buffer_rows(theme), - })) -} - -pub fn hello_world_rust_with_status_buffer_rows(theme: &Theme) -> Vec { - let show_line_number = true; - - vec![ - BufferRow { - line_number: 1, - code_action: false, - current: true, - line: Some(HighlightedLine { - highlighted_texts: vec![ - HighlightedText { - text: "fn ".to_string(), - color: HighlightColor::Keyword.hsla(&theme), - }, - HighlightedText { - text: "main".to_string(), - color: HighlightColor::Function.hsla(&theme), - }, - HighlightedText { - text: "() {".to_string(), - color: HighlightColor::Default.hsla(&theme), - }, - ], - }), - cursors: None, - status: GitStatus::None, - show_line_number, - }, - BufferRow { - line_number: 2, - code_action: false, - current: false, - line: Some(HighlightedLine { - highlighted_texts: vec![HighlightedText { - text: "// Statements here are executed when the compiled binary is called." - .to_string(), - color: HighlightColor::Comment.hsla(&theme), - }], - }), - cursors: None, - status: GitStatus::Modified, - show_line_number, - }, - BufferRow { - line_number: 3, - code_action: false, - current: false, - line: None, - cursors: None, - status: GitStatus::None, - show_line_number, - }, - BufferRow { - line_number: 4, - code_action: false, - current: false, - line: Some(HighlightedLine { - highlighted_texts: vec![HighlightedText { - text: " // Print text to the console.".to_string(), - color: HighlightColor::Comment.hsla(&theme), - }], - }), - cursors: None, - status: GitStatus::None, - show_line_number, - }, - BufferRow { - line_number: 5, - code_action: false, - current: false, - line: Some(HighlightedLine { - highlighted_texts: vec![ - HighlightedText { - text: " println!(".to_string(), - color: HighlightColor::Default.hsla(&theme), - }, - HighlightedText { - text: "\"Hello, world!\"".to_string(), - color: HighlightColor::String.hsla(&theme), - }, - HighlightedText { - text: ");".to_string(), - color: HighlightColor::Default.hsla(&theme), - }, - ], - }), - cursors: None, - status: GitStatus::None, - show_line_number, - }, - BufferRow { - line_number: 6, - code_action: false, - current: false, - line: Some(HighlightedLine { - highlighted_texts: vec![HighlightedText { - text: "}".to_string(), - color: HighlightColor::Default.hsla(&theme), - }], - }), - cursors: None, - status: GitStatus::None, - show_line_number, - }, - BufferRow { - line_number: 7, - code_action: false, - current: false, - line: Some(HighlightedLine { - highlighted_texts: vec![HighlightedText { - text: "".to_string(), - color: HighlightColor::Default.hsla(&theme), - }], - }), - cursors: None, - status: GitStatus::Created, - show_line_number, - }, - BufferRow { - line_number: 8, - code_action: false, - current: false, - line: Some(HighlightedLine { - highlighted_texts: vec![HighlightedText { - text: "// Marshall and Nate were here".to_string(), - color: HighlightColor::Comment.hsla(&theme), - }], - }), - cursors: None, - status: GitStatus::Created, - show_line_number, - }, - ] -} - -pub fn terminal_buffer(theme: &Theme) -> Buffer { - Buffer::new() - .set_title("zed — fish".to_string()) - .set_rows(Some(BufferRows { - show_line_numbers: false, - rows: terminal_buffer_rows(theme), - })) -} - -pub fn terminal_buffer_rows(theme: &Theme) -> Vec { - let show_line_number = false; - - vec![ - BufferRow { - line_number: 1, - code_action: false, - current: false, - line: Some(HighlightedLine { - highlighted_texts: vec![ - HighlightedText { - text: "maxdeviant ".to_string(), - color: HighlightColor::Keyword.hsla(&theme), - }, - HighlightedText { - text: "in ".to_string(), - color: HighlightColor::Default.hsla(&theme), - }, - HighlightedText { - text: "profaned-capital ".to_string(), - color: HighlightColor::Function.hsla(&theme), - }, - HighlightedText { - text: "in ".to_string(), - color: HighlightColor::Default.hsla(&theme), - }, - HighlightedText { - text: "~/p/zed ".to_string(), - color: HighlightColor::Function.hsla(&theme), - }, - HighlightedText { - text: "on ".to_string(), - color: HighlightColor::Default.hsla(&theme), - }, - HighlightedText { - text: "î‚  gpui2-ui ".to_string(), - color: HighlightColor::Keyword.hsla(&theme), - }, - ], - }), - cursors: None, - status: GitStatus::None, - show_line_number, - }, - BufferRow { - line_number: 2, - code_action: false, - current: false, - line: Some(HighlightedLine { - highlighted_texts: vec![HighlightedText { - text: "λ ".to_string(), - color: HighlightColor::String.hsla(&theme), - }], - }), - cursors: None, - status: GitStatus::None, - show_line_number, - }, - ] -} diff --git a/crates/ui/src/theme.rs b/crates/ui/src/theme.rs deleted file mode 100644 index d4c8c26273..0000000000 --- a/crates/ui/src/theme.rs +++ /dev/null @@ -1,196 +0,0 @@ -use std::collections::HashMap; -use std::fmt; -use std::marker::PhantomData; -use std::sync::Arc; - -use gpui2::color::Hsla; -use gpui2::element::Element; -use gpui2::{serde_json, AppContext, IntoElement, Vector2F, ViewContext, WindowContext}; -use serde::de::Visitor; -use serde::{Deserialize, Deserializer}; -use theme::ThemeSettings; - -#[derive(Deserialize, Clone, Default, Debug)] -pub struct Theme { - pub name: String, - pub is_light: bool, - pub lowest: Layer, - pub middle: Layer, - pub highest: Layer, - pub popover_shadow: Shadow, - pub modal_shadow: Shadow, - #[serde(deserialize_with = "deserialize_player_colors")] - pub players: Vec, - #[serde(deserialize_with = "deserialize_syntax_colors")] - pub syntax: HashMap, -} - -#[derive(Deserialize, Clone, Default, Debug)] -pub struct Layer { - pub base: StyleSet, - pub variant: StyleSet, - pub on: StyleSet, - pub accent: StyleSet, - pub positive: StyleSet, - pub warning: StyleSet, - pub negative: StyleSet, -} - -#[derive(Deserialize, Clone, Default, Debug)] -pub struct StyleSet { - #[serde(rename = "default")] - pub default: ContainerColors, - pub hovered: ContainerColors, - pub pressed: ContainerColors, - pub active: ContainerColors, - pub disabled: ContainerColors, - pub inverted: ContainerColors, -} - -#[derive(Deserialize, Clone, Default, Debug)] -pub struct ContainerColors { - pub background: Hsla, - pub foreground: Hsla, - pub border: Hsla, -} - -#[derive(Deserialize, Clone, Default, Debug)] -pub struct PlayerColors { - pub selection: Hsla, - pub cursor: Hsla, -} - -#[derive(Deserialize, Clone, Default, Debug)] -pub struct Shadow { - pub blur: u8, - pub color: Hsla, - pub offset: Vec, -} - -fn deserialize_player_colors<'de, D>(deserializer: D) -> Result, D::Error> -where - D: Deserializer<'de>, -{ - struct PlayerArrayVisitor; - - impl<'de> Visitor<'de> for PlayerArrayVisitor { - type Value = Vec; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("an object with integer keys") - } - - fn visit_map>( - self, - mut map: A, - ) -> Result { - let mut players = Vec::with_capacity(8); - while let Some((key, value)) = map.next_entry::()? { - if key < 8 { - players.push(value); - } else { - return Err(serde::de::Error::invalid_value( - serde::de::Unexpected::Unsigned(key as u64), - &"a key in range 0..7", - )); - } - } - Ok(players) - } - } - - deserializer.deserialize_map(PlayerArrayVisitor) -} - -fn deserialize_syntax_colors<'de, D>(deserializer: D) -> Result, D::Error> -where - D: serde::Deserializer<'de>, -{ - #[derive(Deserialize)] - struct ColorWrapper { - color: Hsla, - } - - struct SyntaxVisitor; - - impl<'de> Visitor<'de> for SyntaxVisitor { - type Value = HashMap; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("a map with keys and objects with a single color field as values") - } - - fn visit_map(self, mut map: M) -> Result, M::Error> - where - M: serde::de::MapAccess<'de>, - { - let mut result = HashMap::new(); - while let Some(key) = map.next_key()? { - let wrapper: ColorWrapper = map.next_value()?; // Deserialize values as Hsla - result.insert(key, wrapper.color); - } - Ok(result) - } - } - deserializer.deserialize_map(SyntaxVisitor) -} - -#[derive(IntoElement)] -pub struct Themed> { - pub(crate) theme: Theme, - pub(crate) child: E, - pub(crate) view_type: PhantomData, -} - -impl> Element for Themed { - type PaintState = E::PaintState; - - fn layout( - &mut self, - view: &mut V, - cx: &mut ViewContext, - ) -> anyhow::Result<(gpui2::LayoutId, Self::PaintState)> - where - Self: Sized, - { - cx.push_theme(self.theme.clone()); - let result = self.child.layout(view, cx); - cx.pop_theme(); - result - } - - fn paint( - &mut self, - view: &mut V, - parent_origin: Vector2F, - layout: &gpui2::Layout, - state: &mut Self::PaintState, - cx: &mut ViewContext, - ) where - Self: Sized, - { - cx.push_theme(self.theme.clone()); - self.child.paint(view, parent_origin, layout, state, cx); - cx.pop_theme(); - } -} - -fn preferred_theme(cx: &AppContext) -> Theme { - settings::get::(cx) - .theme - .deserialized_base_theme - .lock() - .get_or_insert_with(|| { - let theme: Theme = - serde_json::from_value(settings::get::(cx).theme.base_theme.clone()) - .unwrap(); - Box::new(theme) - }) - .downcast_ref::() - .unwrap() - .clone() -} - -pub fn theme(cx: &WindowContext) -> Arc { - cx.theme::() -} diff --git a/crates/ui/src/tokens.rs b/crates/ui/src/tokens.rs deleted file mode 100644 index 5fd5b69a2a..0000000000 --- a/crates/ui/src/tokens.rs +++ /dev/null @@ -1,25 +0,0 @@ -use gpui2::geometry::AbsoluteLength; -use gpui2::{hsla, Hsla}; - -#[derive(Clone, Copy)] -pub struct Token { - pub list_indent_depth: AbsoluteLength, - pub default_panel_size: AbsoluteLength, - pub state_hover_background: Hsla, - pub state_active_background: Hsla, -} - -impl Default for Token { - fn default() -> Self { - Self { - list_indent_depth: AbsoluteLength::Rems(0.5), - default_panel_size: AbsoluteLength::Rems(16.), - state_hover_background: hsla(0.0, 0.0, 0.0, 0.08), - state_active_background: hsla(0.0, 0.0, 0.0, 0.16), - } - } -} - -pub fn token() -> Token { - Token::default() -} diff --git a/crates/ui/tracker.md b/crates/ui/tracker.md deleted file mode 100644 index 11b5804db2..0000000000 --- a/crates/ui/tracker.md +++ /dev/null @@ -1,133 +0,0 @@ -* = Not in the app today - -## Template -- [ ] Workspace -- [ ] Title Bar -- [ ] Project Panel -- [ ] Collab Panel -- [ ] Project Diagnosics -- [ ] Project Search -- [ ] Feedback Editor -- [ ] Terminal -- [ ] Assistant -- [ ] Chat* -- [ ] Notifications* -- [ ] Status Bar -- [ ] Panes -- [ ] Pane -- [ ] Editor -- [ ] Tab Bar -- [ ] Tool Bar -- [ ] Buffer -- [ ] Zoomed Editor (Modal) - -### Palettes -- [ ] Project Files Palette (⌘-P) -- [ ] Command Palette (⌘-SHIFT-P) -- [ ] Recent Projects Palette (⌘-OPT-O) -- [ ] Recent Branches Palette (⌘-OPT-B) -- [ ] Project Symbols (⌘-T) -- [ ] Theme Palette (⌘-K, ⌘-T) -- [ ] Outline View (⌘-SHIFT-O) - -### Debug Views -- [ ] LSP Tool -- [ ] Syntax Tree - -## Modules - -### Title Bar -- [ ] Traffic Lights -- [ ] Host Menu -- [ ] Project Menu -- [ ] Branch Menu -- [ ] Collaborators -- [ ] Add Collaborator* -- [ ] Project Controls -- [ ] Call Controls -- [ ] User Menu - -### Project Panel -- [ ] Open Editors* -- [ ] Open Files (Non-project files) -- [ ] Project Files -- [ ] Root Folder - Context Menu -- [ ] Folder - Context Menu -- [ ] File - Context Menu -- [ ] Project Filter* - -### Collab Panel -- [ ] Current Call -- [ ] Channels -- [ ] Channel - Context Menu -- [ ] Contacts -- [ ] Collab Filter - -### Project Diagnosics -WIP - -### Feedback Editor -- [ ] Feedback Header -- [ ] Editor -- [ ] Feedback Actions - -### Terminal -- [ ] Terminal Toolbar* -- [ ] Terminal Line -- [ ] Terminal Input - -### Assistant -- [ ] Toolbar -- [ ] History / Past Conversations -- [ ] Model Controls / Token Counter -- [ ] Chat Editor - -### Chat -WIP - -### Notifications -WIP - -### Status Bar -- [ ] Status Bar Tool (Icon) -- [ ] Status Bar Tool (Text) -- [ ] Status Bar Tool - Context Menu -- [ ] Status Bar Tool - Popover Palette -- [ ] Status Bar Tool - Popover Menu -- [ ] Diagnostic Message -- [ ] LSP Message -- [ ] Update message (New version available, downloading, etc) - -### Panes/Pane - -- [ ] Editor -- [ ] Split Divider/Control - -### Editor -- [ ] Editor -- [ ] Read-only Editor -- [ ] Rendered Markdown View* - -### Tab Bar -- [ ] Navigation History / Control -- [ ] Tabs -- [ ] Editor Controls (New, Split, Zoom) - -### Tool Bar -- [ ] Breadcrumb -- [ ] Editor Tool (Togglable) -- [ ] Buffer Search - -### Buffer - -### Zoomed Editor (Modal) -- [ ] Modal View - -### Palette -- [ ] Input -- [ ] Section Title -- [ ] List - -## Components - -- [ ] Context Menu diff --git a/crates/util/src/channel.rs b/crates/util/src/channel.rs index 274fd576a0..47c6a570a1 100644 --- a/crates/util/src/channel.rs +++ b/crates/util/src/channel.rs @@ -41,4 +41,36 @@ impl ReleaseChannel { ReleaseChannel::Stable => "stable", } } + + pub fn url_scheme(&self) -> &'static str { + match self { + ReleaseChannel::Dev => "zed-dev://", + ReleaseChannel::Preview => "zed-preview://", + ReleaseChannel::Stable => "zed://", + } + } + + pub fn link_prefix(&self) -> &'static str { + match self { + ReleaseChannel::Dev => "https://zed.dev/dev/", + ReleaseChannel::Preview => "https://zed.dev/preview/", + ReleaseChannel::Stable => "https://zed.dev/", + } + } +} + +pub fn parse_zed_link(link: &str) -> Option<&str> { + for release in [ + ReleaseChannel::Dev, + ReleaseChannel::Preview, + ReleaseChannel::Stable, + ] { + if let Some(stripped) = link.strip_prefix(release.link_prefix()) { + return Some(stripped); + } + if let Some(stripped) = link.strip_prefix(release.url_scheme()) { + return Some(stripped); + } + } + None } diff --git a/crates/util/src/github.rs b/crates/util/src/github.rs index b1e981ae49..a3df4c996b 100644 --- a/crates/util/src/github.rs +++ b/crates/util/src/github.rs @@ -16,6 +16,7 @@ pub struct GithubRelease { pub pre_release: bool, pub assets: Vec, pub tarball_url: String, + pub zipball_url: String, } #[derive(Deserialize, Debug)] diff --git a/crates/util/src/paths.rs b/crates/util/src/paths.rs index 4578ce0bc9..96d77236a9 100644 --- a/crates/util/src/paths.rs +++ b/crates/util/src/paths.rs @@ -11,6 +11,7 @@ lazy_static::lazy_static! { pub static ref SUPPORT_DIR: PathBuf = HOME.join("Library/Application Support/Zed"); pub static ref LANGUAGES_DIR: PathBuf = HOME.join("Library/Application Support/Zed/languages"); pub static ref COPILOT_DIR: PathBuf = HOME.join("Library/Application Support/Zed/copilot"); + pub static ref DEFAULT_PRETTIER_DIR: PathBuf = HOME.join("Library/Application Support/Zed/prettier"); pub static ref DB_DIR: PathBuf = HOME.join("Library/Application Support/Zed/db"); pub static ref SETTINGS: PathBuf = CONFIG_DIR.join("settings.json"); pub static ref KEYMAP: PathBuf = CONFIG_DIR.join("keymap.json"); diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index 1277d58b41..6151b8c041 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -46,6 +46,7 @@ actions!( ChangeToEndOfLine, DeleteToEndOfLine, Yank, + YankLine, ChangeCase, JoinLines, ] @@ -66,6 +67,7 @@ pub fn init(cx: &mut AppContext) { cx.add_action(insert_line_above); cx.add_action(insert_line_below); cx.add_action(change_case); + cx.add_action(yank_line); cx.add_action(|_: &mut Workspace, _: &DeleteLeft, cx| { Vim::update(cx, |vim, cx| { @@ -314,6 +316,13 @@ fn insert_line_below(_: &mut Workspace, _: &InsertLineBelow, cx: &mut ViewContex }); } +fn yank_line(_: &mut Workspace, _: &YankLine, cx: &mut ViewContext) { + Vim::update(cx, |vim, cx| { + let count = vim.take_count(cx); + yank_motion(vim, motion::Motion::CurrentLine, count, cx) + }) +} + pub(crate) fn normal_replace(text: Arc, cx: &mut WindowContext) { Vim::update(cx, |vim, cx| { vim.stop_recording(); diff --git a/crates/vim/src/test.rs b/crates/vim/src/test.rs index 3c48679212..4fb87e70a0 100644 --- a/crates/vim/src/test.rs +++ b/crates/vim/src/test.rs @@ -709,3 +709,28 @@ async fn test_wrapped_motions(cx: &mut gpui::TestAppContext) { }) .await; } + +#[gpui::test] +async fn test_paragraphs_dont_wrap(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + cx.set_shared_state(indoc! {" + one + ˇ + two"}) + .await; + + cx.simulate_shared_keystrokes(["}", "}"]).await; + cx.assert_shared_state(indoc! {" + one + + twˇo"}) + .await; + + cx.simulate_shared_keystrokes(["{", "{", "{"]).await; + cx.assert_shared_state(indoc! {" + ˇone + + two"}) + .await; +} diff --git a/crates/vim/test_data/test_paragraphs_dont_wrap.json b/crates/vim/test_data/test_paragraphs_dont_wrap.json new file mode 100644 index 0000000000..9e729651be --- /dev/null +++ b/crates/vim/test_data/test_paragraphs_dont_wrap.json @@ -0,0 +1,8 @@ +{"Put":{"state":"one\nˇ\ntwo"}} +{"Key":"}"} +{"Key":"}"} +{"Get":{"state":"one\n\ntwˇo","mode":"Normal"}} +{"Key":"{"} +{"Key":"{"} +{"Key":"{"} +{"Get":{"state":"ˇone\n\ntwo","mode":"Normal"}} diff --git a/crates/workspace/Cargo.toml b/crates/workspace/Cargo.toml index d1240a45ce..99f19ed9d0 100644 --- a/crates/workspace/Cargo.toml +++ b/crates/workspace/Cargo.toml @@ -30,6 +30,7 @@ gpui = { path = "../gpui" } install_cli = { path = "../install_cli" } language = { path = "../language" } menu = { path = "../menu" } +node_runtime = { path = "../node_runtime" } project = { path = "../project" } settings = { path = "../settings" } terminal = { path = "../terminal" } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index bf96558f5c..e879b981ef 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -14,7 +14,7 @@ use anyhow::{anyhow, Context, Result}; use call::ActiveCall; use client::{ proto::{self, PeerId}, - Client, TypedEnvelope, UserStore, + Client, Status, TypedEnvelope, UserStore, }; use collections::{hash_map, HashMap, HashSet}; use drag_and_drop::DragAndDrop; @@ -35,13 +35,14 @@ use gpui::{ CursorStyle, ModifiersChangedEvent, MouseButton, PathPromptOptions, Platform, PromptLevel, WindowBounds, WindowOptions, }, - AnyModelHandle, AnyViewHandle, AnyWeakViewHandle, AppContext, AsyncAppContext, Entity, - ModelContext, ModelHandle, SizeConstraint, Subscription, Task, View, ViewContext, ViewHandle, - WeakViewHandle, WindowContext, WindowHandle, + AnyModelHandle, AnyViewHandle, AnyWeakViewHandle, AnyWindowHandle, AppContext, AsyncAppContext, + Entity, ModelContext, ModelHandle, SizeConstraint, Subscription, Task, View, ViewContext, + ViewHandle, WeakViewHandle, WindowContext, WindowHandle, }; use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ProjectItem}; use itertools::Itertools; use language::{LanguageRegistry, Rope}; +use node_runtime::NodeRuntime; use std::{ any::TypeId, borrow::Cow, @@ -288,6 +289,7 @@ pub fn init(app_state: Arc, cx: &mut AppContext) { cx.add_global_action(restart); cx.add_async_action(Workspace::save_all); cx.add_action(Workspace::add_folder_to_project); + cx.add_action( |workspace: &mut Workspace, _: &Unfollow, cx: &mut ViewContext| { let pane = workspace.active_pane().clone(); @@ -456,6 +458,7 @@ pub struct AppState { pub initialize_workspace: fn(WeakViewHandle, bool, Arc, AsyncAppContext) -> Task>, pub background_actions: BackgroundActions, + pub node_runtime: Arc, } pub struct WorkspaceStore { @@ -474,6 +477,7 @@ struct Follower { impl AppState { #[cfg(any(test, feature = "test-support"))] pub fn test(cx: &mut AppContext) -> Arc { + use node_runtime::FakeNodeRuntime; use settings::SettingsStore; if !cx.has_global::() { @@ -498,6 +502,7 @@ impl AppState { user_store, // channel_store, workspace_store, + node_runtime: FakeNodeRuntime::new(), initialize_workspace: |_, _, _, _| Task::ready(Ok(())), build_window_options: |_, _, _| Default::default(), background_actions: || &[], @@ -816,6 +821,7 @@ impl Workspace { )> { let project_handle = Project::local( app_state.client.clone(), + app_state.node_runtime.clone(), app_state.user_store.clone(), app_state.languages.clone(), app_state.fs.clone(), @@ -2878,8 +2884,7 @@ impl Workspace { let cx = &cx; move |item| { let item = item.to_followable_item_handle(cx)?; - if project_id.is_some() - && project_id != follower_project_id + if (project_id.is_none() || project_id != follower_project_id) && item.is_project_item(cx) { return None; @@ -3518,6 +3523,8 @@ impl Workspace { #[cfg(any(test, feature = "test-support"))] pub fn test_new(project: ModelHandle, cx: &mut ViewContext) -> Self { + use node_runtime::FakeNodeRuntime; + let client = project.read(cx).client(); let user_store = project.read(cx).user_store(); @@ -3531,6 +3538,7 @@ impl Workspace { build_window_options: |_, _, _| Default::default(), initialize_workspace: |_, _, _, _| Task::ready(Ok(())), background_actions: || &[], + node_runtime: FakeNodeRuntime::new(), }); Self::new(0, project, app_state, cx) } @@ -4139,6 +4147,188 @@ pub async fn last_opened_workspace_paths() -> Option { DB.last_workspace().await.log_err().flatten() } +async fn join_channel_internal( + channel_id: u64, + app_state: &Arc, + requesting_window: Option>, + active_call: &ModelHandle, + cx: &mut AsyncAppContext, +) -> Result { + let (should_prompt, open_room) = active_call.read_with(cx, |active_call, cx| { + let Some(room) = active_call.room().map(|room| room.read(cx)) else { + return (false, None); + }; + + let already_in_channel = room.channel_id() == Some(channel_id); + let should_prompt = room.is_sharing_project() + && room.remote_participants().len() > 0 + && !already_in_channel; + let open_room = if already_in_channel { + active_call.room().cloned() + } else { + None + }; + (should_prompt, open_room) + }); + + if let Some(room) = open_room { + let task = room.update(cx, |room, cx| { + if let Some((project, host)) = room.most_active_project(cx) { + return Some(join_remote_project(project, host, app_state.clone(), cx)); + } + + None + }); + if let Some(task) = task { + task.await?; + } + return anyhow::Ok(true); + } + + if should_prompt { + if let Some(workspace) = requesting_window { + if let Some(window) = workspace.update(cx, |cx| cx.window()) { + let answer = window.prompt( + PromptLevel::Warning, + "Leaving this call will unshare your current project.\nDo you want to switch channels?", + &["Yes, Join Channel", "Cancel"], + cx, + ); + + if let Some(mut answer) = answer { + if answer.next().await == Some(1) { + return Ok(false); + } + } + } else { + return Ok(false); // unreachable!() hopefully + } + } else { + return Ok(false); // unreachable!() hopefully + } + } + + let client = cx.read(|cx| active_call.read(cx).client()); + + let mut client_status = client.status(); + + // this loop will terminate within client::CONNECTION_TIMEOUT seconds. + 'outer: loop { + let Some(status) = client_status.recv().await else { + return Err(anyhow!("error connecting")); + }; + + match status { + Status::Connecting + | Status::Authenticating + | Status::Reconnecting + | Status::Reauthenticating => continue, + Status::Connected { .. } => break 'outer, + Status::SignedOut => return Err(anyhow!("not signed in")), + Status::UpgradeRequired => return Err(anyhow!("zed is out of date")), + Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => { + return Err(anyhow!("zed is offline")) + } + } + } + + let room = active_call + .update(cx, |active_call, cx| { + active_call.join_channel(channel_id, cx) + }) + .await?; + + room.update(cx, |room, _| room.room_update_completed()) + .await; + + let task = room.update(cx, |room, cx| { + if let Some((project, host)) = room.most_active_project(cx) { + return Some(join_remote_project(project, host, app_state.clone(), cx)); + } + + None + }); + if let Some(task) = task { + task.await?; + return anyhow::Ok(true); + } + anyhow::Ok(false) +} + +pub fn join_channel( + channel_id: u64, + app_state: Arc, + requesting_window: Option>, + cx: &mut AppContext, +) -> Task> { + let active_call = ActiveCall::global(cx); + cx.spawn(|mut cx| async move { + let result = join_channel_internal( + channel_id, + &app_state, + requesting_window, + &active_call, + &mut cx, + ) + .await; + + // join channel succeeded, and opened a window + if matches!(result, Ok(true)) { + return anyhow::Ok(()); + } + + if requesting_window.is_some() { + return anyhow::Ok(()); + } + + // find an existing workspace to focus and show call controls + let mut active_window = activate_any_workspace_window(&mut cx); + if active_window.is_none() { + // no open workspaces, make one to show the error in (blergh) + cx.update(|cx| Workspace::new_local(vec![], app_state.clone(), requesting_window, cx)) + .await; + } + + active_window = activate_any_workspace_window(&mut cx); + if active_window.is_none() { + return result.map(|_| ()); // unreachable!() assuming new_local always opens a window + } + + if let Err(err) = result { + let prompt = active_window.unwrap().prompt( + PromptLevel::Critical, + &format!("Failed to join channel: {}", err), + &["Ok"], + &mut cx, + ); + if let Some(mut prompt) = prompt { + prompt.next().await; + } else { + return Err(err); + } + } + + // return ok, we showed the error to the user. + return anyhow::Ok(()); + }) +} + +pub fn activate_any_workspace_window(cx: &mut AsyncAppContext) -> Option { + for window in cx.windows() { + let found = window.update(cx, |cx| { + let is_workspace = cx.root_view().clone().downcast::().is_some(); + if is_workspace { + cx.activate_window(); + } + is_workspace + }); + if found == Some(true) { + return Some(window); + } + } + None +} + #[allow(clippy::type_complexity)] pub fn open_paths( abs_paths: &[PathBuf], diff --git a/crates/zed-actions/Cargo.toml b/crates/zed-actions/Cargo.toml index b3fe3cbb53..353041264a 100644 --- a/crates/zed-actions/Cargo.toml +++ b/crates/zed-actions/Cargo.toml @@ -8,3 +8,4 @@ publish = false [dependencies] gpui = { path = "../gpui" } +serde.workspace = true diff --git a/crates/zed-actions/src/lib.rs b/crates/zed-actions/src/lib.rs index bcd086924d..df6405a4b1 100644 --- a/crates/zed-actions/src/lib.rs +++ b/crates/zed-actions/src/lib.rs @@ -1,4 +1,7 @@ -use gpui::actions; +use std::sync::Arc; + +use gpui::{actions, impl_actions}; +use serde::Deserialize; actions!( zed, @@ -26,3 +29,13 @@ actions!( ResetDatabase, ] ); + +#[derive(Deserialize, Clone, PartialEq)] +pub struct OpenBrowser { + pub url: Arc, +} +#[derive(Deserialize, Clone, PartialEq)] +pub struct OpenZedURL { + pub url: String, +} +impl_actions!(zed, [OpenBrowser, OpenZedURL]); diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index d4ac972a5d..4e2b97f4a1 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -3,7 +3,7 @@ authors = ["Nathan Sobo "] description = "The fast, collaborative code editor." edition = "2021" name = "zed" -version = "0.108.0" +version = "0.110.0" publish = false [lib] @@ -15,6 +15,9 @@ doctest = false name = "Zed" path = "src/main.rs" +[[example]] +name = "semantic_index_eval" + [dependencies] audio = { path = "../audio" } activity_indicator = { path = "../activity_indicator" } @@ -135,12 +138,14 @@ tree-sitter-yaml.workspace = true tree-sitter-lua.workspace = true tree-sitter-nix.workspace = true tree-sitter-nu.workspace = true +tree-sitter-vue.workspace = true url = "2.2" urlencoding = "2.1.2" uuid.workspace = true [dev-dependencies] +ai = { path = "../ai" } call = { path = "../call", features = ["test-support"] } client = { path = "../client", features = ["test-support"] } editor = { path = "../editor", features = ["test-support"] } @@ -162,6 +167,7 @@ identifier = "dev.zed.Zed-Dev" name = "Zed Dev" osx_minimum_system_version = "10.15.7" osx_info_plist_exts = ["resources/info/*"] +osx_url_schemes = ["zed-dev"] [package.metadata.bundle-preview] icon = ["resources/app-icon-preview@2x.png", "resources/app-icon-preview.png"] @@ -169,6 +175,7 @@ identifier = "dev.zed.Zed-Preview" name = "Zed Preview" osx_minimum_system_version = "10.15.7" osx_info_plist_exts = ["resources/info/*"] +osx_url_schemes = ["zed-preview"] [package.metadata.bundle-stable] @@ -177,3 +184,4 @@ identifier = "dev.zed.Zed" name = "Zed" osx_minimum_system_version = "10.15.7" osx_info_plist_exts = ["resources/info/*"] +osx_url_schemes = ["zed"] diff --git a/crates/zed/contents/dev/embedded.provisionprofile b/crates/zed/contents/dev/embedded.provisionprofile new file mode 100644 index 0000000000..8979e1fb9f Binary files /dev/null and b/crates/zed/contents/dev/embedded.provisionprofile differ diff --git a/crates/zed/contents/preview/embedded.provisionprofile b/crates/zed/contents/preview/embedded.provisionprofile new file mode 100644 index 0000000000..6eea317c37 Binary files /dev/null and b/crates/zed/contents/preview/embedded.provisionprofile differ diff --git a/crates/zed/contents/stable/Zed_Stable_Provisioning_Profile.provisionprofile b/crates/zed/contents/stable/Zed_Stable_Provisioning_Profile.provisionprofile new file mode 100644 index 0000000000..0b2abe1838 Binary files /dev/null and b/crates/zed/contents/stable/Zed_Stable_Provisioning_Profile.provisionprofile differ diff --git a/crates/semantic_index/examples/eval.rs b/crates/zed/examples/semantic_index_eval.rs similarity index 99% rename from crates/semantic_index/examples/eval.rs rename to crates/zed/examples/semantic_index_eval.rs index 573cf73d78..33d6b3689c 100644 --- a/crates/semantic_index/examples/eval.rs +++ b/crates/zed/examples/semantic_index_eval.rs @@ -494,6 +494,7 @@ fn main() { let project = cx.update(|cx| { Project::local( client.clone(), + node_runtime::FakeNodeRuntime::new(), user_store.clone(), languages.clone(), fs.clone(), diff --git a/crates/zed/resources/zed.entitlements b/crates/zed/resources/zed.entitlements index f40a8a253a..27499b62ca 100644 --- a/crates/zed/resources/zed.entitlements +++ b/crates/zed/resources/zed.entitlements @@ -2,6 +2,8 @@ + com.apple.developer.associated-domains + applinks:zed.dev com.apple.security.automation.apple-events com.apple.security.cs.allow-jit @@ -10,14 +12,8 @@ com.apple.security.device.camera - com.apple.security.personal-information.addressbook - - com.apple.security.personal-information.calendars - - com.apple.security.personal-information.location - - com.apple.security.personal-information.photos-library - + com.apple.security.keychain-access-groups + MQ55VZLNZQ.dev.zed.Shared diff --git a/crates/zed/src/languages.rs b/crates/zed/src/languages.rs index 04e5292a7d..caf3cbf7c9 100644 --- a/crates/zed/src/languages.rs +++ b/crates/zed/src/languages.rs @@ -24,6 +24,7 @@ mod rust; mod svelte; mod tailwind; mod typescript; +mod vue; mod yaml; // 1. Add tree-sitter-{language} parser to zed crate @@ -190,13 +191,20 @@ pub fn init( language( "php", tree_sitter_php::language(), - vec![Arc::new(php::IntelephenseLspAdapter::new(node_runtime))], + vec![Arc::new(php::IntelephenseLspAdapter::new( + node_runtime.clone(), + ))], ); language("elm", tree_sitter_elm::language(), vec![]); language("glsl", tree_sitter_glsl::language(), vec![]); language("nix", tree_sitter_nix::language(), vec![]); language("nu", tree_sitter_nu::language(), vec![]); + language( + "vue", + tree_sitter_vue::language(), + vec![Arc::new(vue::VueLspAdapter::new(node_runtime))], + ); } #[cfg(any(test, feature = "test-support"))] diff --git a/crates/zed/src/languages/css.rs b/crates/zed/src/languages/css.rs index fdbc179209..f046437d75 100644 --- a/crates/zed/src/languages/css.rs +++ b/crates/zed/src/languages/css.rs @@ -1,7 +1,7 @@ use anyhow::{anyhow, Result}; use async_trait::async_trait; use futures::StreamExt; -use language::{LanguageServerName, LspAdapter, LspAdapterDelegate}; +use language::{BundledFormatter, LanguageServerName, LspAdapter, LspAdapterDelegate}; use lsp::LanguageServerBinary; use node_runtime::NodeRuntime; use serde_json::json; @@ -96,6 +96,10 @@ impl LspAdapter for CssLspAdapter { "provideFormatter": true })) } + + fn enabled_formatters(&self) -> Vec { + vec![BundledFormatter::prettier("css")] + } } async fn get_cached_server_binary( diff --git a/crates/zed/src/languages/html.rs b/crates/zed/src/languages/html.rs index b8f1c70cce..6f27b7ca8f 100644 --- a/crates/zed/src/languages/html.rs +++ b/crates/zed/src/languages/html.rs @@ -1,7 +1,7 @@ use anyhow::{anyhow, Result}; use async_trait::async_trait; use futures::StreamExt; -use language::{LanguageServerName, LspAdapter, LspAdapterDelegate}; +use language::{BundledFormatter, LanguageServerName, LspAdapter, LspAdapterDelegate}; use lsp::LanguageServerBinary; use node_runtime::NodeRuntime; use serde_json::json; @@ -96,6 +96,10 @@ impl LspAdapter for HtmlLspAdapter { "provideFormatter": true })) } + + fn enabled_formatters(&self) -> Vec { + vec![BundledFormatter::prettier("html")] + } } async fn get_cached_server_binary( diff --git a/crates/zed/src/languages/json.rs b/crates/zed/src/languages/json.rs index 63f909ae2a..f017af0a22 100644 --- a/crates/zed/src/languages/json.rs +++ b/crates/zed/src/languages/json.rs @@ -4,7 +4,9 @@ use collections::HashMap; use feature_flags::FeatureFlagAppExt; use futures::{future::BoxFuture, FutureExt, StreamExt}; use gpui::AppContext; -use language::{LanguageRegistry, LanguageServerName, LspAdapter, LspAdapterDelegate}; +use language::{ + BundledFormatter, LanguageRegistry, LanguageServerName, LspAdapter, LspAdapterDelegate, +}; use lsp::LanguageServerBinary; use node_runtime::NodeRuntime; use serde_json::json; @@ -144,6 +146,10 @@ impl LspAdapter for JsonLspAdapter { async fn language_ids(&self) -> HashMap { [("JSON".into(), "jsonc".into())].into_iter().collect() } + + fn enabled_formatters(&self) -> Vec { + vec![BundledFormatter::prettier("json")] + } } async fn get_cached_server_binary( diff --git a/crates/zed/src/languages/svelte.rs b/crates/zed/src/languages/svelte.rs index 5e42d80e77..2089fe88b1 100644 --- a/crates/zed/src/languages/svelte.rs +++ b/crates/zed/src/languages/svelte.rs @@ -1,7 +1,7 @@ use anyhow::{anyhow, Result}; use async_trait::async_trait; use futures::StreamExt; -use language::{LanguageServerName, LspAdapter, LspAdapterDelegate}; +use language::{BundledFormatter, LanguageServerName, LspAdapter, LspAdapterDelegate}; use lsp::LanguageServerBinary; use node_runtime::NodeRuntime; use serde_json::json; @@ -95,6 +95,13 @@ impl LspAdapter for SvelteLspAdapter { "provideFormatter": true })) } + + fn enabled_formatters(&self) -> Vec { + vec![BundledFormatter::Prettier { + parser_name: Some("svelte"), + plugin_names: vec!["prettier-plugin-svelte"], + }] + } } async fn get_cached_server_binary( diff --git a/crates/zed/src/languages/tailwind.rs b/crates/zed/src/languages/tailwind.rs index cf07fa71c9..8e81f728dc 100644 --- a/crates/zed/src/languages/tailwind.rs +++ b/crates/zed/src/languages/tailwind.rs @@ -6,7 +6,7 @@ use futures::{ FutureExt, StreamExt, }; use gpui::AppContext; -use language::{LanguageServerName, LspAdapter, LspAdapterDelegate}; +use language::{BundledFormatter, LanguageServerName, LspAdapter, LspAdapterDelegate}; use lsp::LanguageServerBinary; use node_runtime::NodeRuntime; use serde_json::{json, Value}; @@ -127,6 +127,13 @@ impl LspAdapter for TailwindLspAdapter { .into_iter(), ) } + + fn enabled_formatters(&self) -> Vec { + vec![BundledFormatter::Prettier { + parser_name: None, + plugin_names: vec!["prettier-plugin-tailwindcss"], + }] + } } async fn get_cached_server_binary( diff --git a/crates/zed/src/languages/typescript.rs b/crates/zed/src/languages/typescript.rs index 676d0fd4c0..f09c964588 100644 --- a/crates/zed/src/languages/typescript.rs +++ b/crates/zed/src/languages/typescript.rs @@ -4,7 +4,7 @@ use async_tar::Archive; use async_trait::async_trait; use futures::{future::BoxFuture, FutureExt}; use gpui::AppContext; -use language::{LanguageServerName, LspAdapter, LspAdapterDelegate}; +use language::{BundledFormatter, LanguageServerName, LspAdapter, LspAdapterDelegate}; use lsp::{CodeActionKind, LanguageServerBinary}; use node_runtime::NodeRuntime; use serde_json::{json, Value}; @@ -161,6 +161,10 @@ impl LspAdapter for TypeScriptLspAdapter { "provideFormatter": true })) } + + fn enabled_formatters(&self) -> Vec { + vec![BundledFormatter::prettier("typescript")] + } } async fn get_cached_ts_server_binary( @@ -309,6 +313,10 @@ impl LspAdapter for EsLintLspAdapter { async fn initialization_options(&self) -> Option { None } + + fn enabled_formatters(&self) -> Vec { + vec![BundledFormatter::prettier("babel")] + } } async fn get_cached_eslint_server_binary( diff --git a/crates/zed/src/languages/vue.rs b/crates/zed/src/languages/vue.rs new file mode 100644 index 0000000000..f0374452df --- /dev/null +++ b/crates/zed/src/languages/vue.rs @@ -0,0 +1,214 @@ +use anyhow::{anyhow, Result}; +use async_trait::async_trait; +use futures::StreamExt; +pub use language::*; +use lsp::{CodeActionKind, LanguageServerBinary}; +use node_runtime::NodeRuntime; +use parking_lot::Mutex; +use serde_json::Value; +use smol::fs::{self}; +use std::{ + any::Any, + ffi::OsString, + path::{Path, PathBuf}, + sync::Arc, +}; +use util::ResultExt; + +pub struct VueLspVersion { + vue_version: String, + ts_version: String, +} + +pub struct VueLspAdapter { + node: Arc, + typescript_install_path: Mutex>, +} + +impl VueLspAdapter { + const SERVER_PATH: &'static str = + "node_modules/@vue/language-server/bin/vue-language-server.js"; + // TODO: this can't be hardcoded, yet we have to figure out how to pass it in initialization_options. + const TYPESCRIPT_PATH: &'static str = "node_modules/typescript/lib"; + pub fn new(node: Arc) -> Self { + let typescript_install_path = Mutex::new(None); + Self { + node, + typescript_install_path, + } + } +} +#[async_trait] +impl super::LspAdapter for VueLspAdapter { + async fn name(&self) -> LanguageServerName { + LanguageServerName("vue-language-server".into()) + } + + fn short_name(&self) -> &'static str { + "vue-language-server" + } + + async fn fetch_latest_server_version( + &self, + _: &dyn LspAdapterDelegate, + ) -> Result> { + Ok(Box::new(VueLspVersion { + vue_version: self + .node + .npm_package_latest_version("@vue/language-server") + .await?, + ts_version: self.node.npm_package_latest_version("typescript").await?, + }) as Box<_>) + } + async fn initialization_options(&self) -> Option { + let typescript_sdk_path = self.typescript_install_path.lock(); + let typescript_sdk_path = typescript_sdk_path + .as_ref() + .expect("initialization_options called without a container_dir for typescript"); + + Some(serde_json::json!({ + "typescript": { + "tsdk": typescript_sdk_path + } + })) + } + fn code_action_kinds(&self) -> Option> { + // REFACTOR is explicitly disabled, as vue-lsp does not adhere to LSP protocol for code actions with these - it + // sends back a CodeAction with neither `command` nor `edits` fields set, which is against the spec. + Some(vec![ + CodeActionKind::EMPTY, + CodeActionKind::QUICKFIX, + CodeActionKind::REFACTOR_REWRITE, + ]) + } + async fn fetch_server_binary( + &self, + version: Box, + container_dir: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Result { + let version = version.downcast::().unwrap(); + let server_path = container_dir.join(Self::SERVER_PATH); + let ts_path = container_dir.join(Self::TYPESCRIPT_PATH); + if fs::metadata(&server_path).await.is_err() { + self.node + .npm_install_packages( + &container_dir, + &[("@vue/language-server", version.vue_version.as_str())], + ) + .await?; + } + assert!(fs::metadata(&server_path).await.is_ok()); + if fs::metadata(&ts_path).await.is_err() { + self.node + .npm_install_packages( + &container_dir, + &[("typescript", version.ts_version.as_str())], + ) + .await?; + } + + assert!(fs::metadata(&ts_path).await.is_ok()); + *self.typescript_install_path.lock() = Some(ts_path); + Ok(LanguageServerBinary { + path: self.node.binary_path().await?, + arguments: vue_server_binary_arguments(&server_path), + }) + } + + async fn cached_server_binary( + &self, + container_dir: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Option { + let (server, ts_path) = get_cached_server_binary(container_dir, self.node.clone()).await?; + *self.typescript_install_path.lock() = Some(ts_path); + Some(server) + } + + async fn installation_test_binary( + &self, + container_dir: PathBuf, + ) -> Option { + let (server, ts_path) = get_cached_server_binary(container_dir, self.node.clone()) + .await + .map(|(mut binary, ts_path)| { + binary.arguments = vec!["--help".into()]; + (binary, ts_path) + })?; + *self.typescript_install_path.lock() = Some(ts_path); + Some(server) + } + + async fn label_for_completion( + &self, + item: &lsp::CompletionItem, + language: &Arc, + ) -> Option { + use lsp::CompletionItemKind as Kind; + let len = item.label.len(); + let grammar = language.grammar()?; + let highlight_id = match item.kind? { + Kind::CLASS | Kind::INTERFACE => grammar.highlight_id_for_name("type"), + Kind::CONSTRUCTOR => grammar.highlight_id_for_name("type"), + Kind::CONSTANT => grammar.highlight_id_for_name("constant"), + Kind::FUNCTION | Kind::METHOD => grammar.highlight_id_for_name("function"), + Kind::PROPERTY | Kind::FIELD => grammar.highlight_id_for_name("tag"), + Kind::VARIABLE => grammar.highlight_id_for_name("type"), + Kind::KEYWORD => grammar.highlight_id_for_name("keyword"), + Kind::VALUE => grammar.highlight_id_for_name("tag"), + _ => None, + }?; + + let text = match &item.detail { + Some(detail) => format!("{} {}", item.label, detail), + None => item.label.clone(), + }; + + Some(language::CodeLabel { + text, + runs: vec![(0..len, highlight_id)], + filter_range: 0..len, + }) + } +} + +fn vue_server_binary_arguments(server_path: &Path) -> Vec { + vec![server_path.into(), "--stdio".into()] +} + +type TypescriptPath = PathBuf; +async fn get_cached_server_binary( + container_dir: PathBuf, + node: Arc, +) -> Option<(LanguageServerBinary, TypescriptPath)> { + (|| async move { + let mut last_version_dir = None; + let mut entries = fs::read_dir(&container_dir).await?; + while let Some(entry) = entries.next().await { + let entry = entry?; + if entry.file_type().await?.is_dir() { + last_version_dir = Some(entry.path()); + } + } + let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?; + let server_path = last_version_dir.join(VueLspAdapter::SERVER_PATH); + let typescript_path = last_version_dir.join(VueLspAdapter::TYPESCRIPT_PATH); + if server_path.exists() && typescript_path.exists() { + Ok(( + LanguageServerBinary { + path: node.binary_path().await?, + arguments: vue_server_binary_arguments(&server_path), + }, + typescript_path, + )) + } else { + Err(anyhow!( + "missing executable in directory {:?}", + last_version_dir + )) + } + })() + .await + .log_err() +} diff --git a/crates/zed/src/languages/vue/brackets.scm b/crates/zed/src/languages/vue/brackets.scm new file mode 100644 index 0000000000..2d12b17daa --- /dev/null +++ b/crates/zed/src/languages/vue/brackets.scm @@ -0,0 +1,2 @@ +("<" @open ">" @close) +("\"" @open "\"" @close) diff --git a/crates/zed/src/languages/vue/config.toml b/crates/zed/src/languages/vue/config.toml new file mode 100644 index 0000000000..c41a667b75 --- /dev/null +++ b/crates/zed/src/languages/vue/config.toml @@ -0,0 +1,14 @@ +name = "Vue.js" +path_suffixes = ["vue"] +block_comment = [""] +autoclose_before = ";:.,=}])>" +brackets = [ + { start = "{", end = "}", close = true, newline = true }, + { start = "[", end = "]", close = true, newline = true }, + { start = "(", end = ")", close = true, newline = true }, + { start = "<", end = ">", close = true, newline = true, not_in = ["string", "comment"] }, + { start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] }, + { start = "'", end = "'", close = true, newline = false, not_in = ["string", "comment"] }, + { start = "`", end = "`", close = true, newline = false, not_in = ["string"] }, +] +word_characters = ["-"] diff --git a/crates/zed/src/languages/vue/highlights.scm b/crates/zed/src/languages/vue/highlights.scm new file mode 100644 index 0000000000..1a80c84f68 --- /dev/null +++ b/crates/zed/src/languages/vue/highlights.scm @@ -0,0 +1,15 @@ +(attribute) @property +(directive_attribute) @property +(quoted_attribute_value) @string +(interpolation) @punctuation.special +(raw_text) @embedded + +((tag_name) @type + (#match? @type "^[A-Z]")) + +((directive_name) @keyword + (#match? @keyword "^v-")) + +(start_tag) @tag +(end_tag) @tag +(self_closing_tag) @tag diff --git a/crates/zed/src/languages/vue/injections.scm b/crates/zed/src/languages/vue/injections.scm new file mode 100644 index 0000000000..9084e373f2 --- /dev/null +++ b/crates/zed/src/languages/vue/injections.scm @@ -0,0 +1,7 @@ +(script_element + (raw_text) @content + (#set! "language" "javascript")) + +(style_element + (raw_text) @content + (#set! "language" "css")) diff --git a/crates/zed/src/languages/yaml.rs b/crates/zed/src/languages/yaml.rs index 8b438d0949..1c1ce18668 100644 --- a/crates/zed/src/languages/yaml.rs +++ b/crates/zed/src/languages/yaml.rs @@ -3,7 +3,8 @@ use async_trait::async_trait; use futures::{future::BoxFuture, FutureExt, StreamExt}; use gpui::AppContext; use language::{ - language_settings::all_language_settings, LanguageServerName, LspAdapter, LspAdapterDelegate, + language_settings::all_language_settings, BundledFormatter, LanguageServerName, LspAdapter, + LspAdapterDelegate, }; use lsp::LanguageServerBinary; use node_runtime::NodeRuntime; @@ -108,6 +109,10 @@ impl LspAdapter for YamlLspAdapter { })) .boxed() } + + fn enabled_formatters(&self) -> Vec { + vec![BundledFormatter::prettier("yaml")] + } } async fn get_cached_server_binary( diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 1a1612e141..dce579e847 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -3,20 +3,16 @@ use anyhow::{anyhow, Context, Result}; use backtrace::Backtrace; -use cli::{ - ipc::{self, IpcSender}, - CliRequest, CliResponse, IpcHandshake, FORCE_CLI_MODE_ENV_VAR_NAME, +use cli::FORCE_CLI_MODE_ENV_VAR_NAME; +use client::{ + self, Client, TelemetrySettings, UserStore, ZED_APP_VERSION, ZED_SECRET_CLIENT_TOKEN, }; -use client::{self, TelemetrySettings, UserStore, ZED_APP_VERSION, ZED_SECRET_CLIENT_TOKEN}; use db::kvp::KEY_VALUE_STORE; -use editor::{scroll::autoscroll::Autoscroll, Editor}; -use futures::{ - channel::{mpsc, oneshot}, - FutureExt, SinkExt, StreamExt, -}; +use editor::Editor; +use futures::StreamExt; use gpui::{Action, App, AppContext, AssetSource, AsyncAppContext, Task}; use isahc::{config::Configurable, Request}; -use language::{LanguageRegistry, Point}; +use language::LanguageRegistry; use log::LevelFilter; use node_runtime::RealNodeRuntime; use parking_lot::Mutex; @@ -26,27 +22,22 @@ use settings::{default_settings, handle_settings_file_changes, watch_config_file use simplelog::ConfigBuilder; use smol::process::Command; use std::{ - collections::HashMap, env, ffi::OsStr, fs::OpenOptions, io::{IsTerminal, Write as _}, - os::unix::prelude::OsStrExt, panic, - path::{Path, PathBuf}, - str, + path::Path, sync::{ - atomic::{AtomicBool, AtomicU32, Ordering}, + atomic::{AtomicU32, Ordering}, Arc, Weak, }, thread, time::{Duration, SystemTime, UNIX_EPOCH}, }; -use sum_tree::Bias; use util::{ - channel::ReleaseChannel, + channel::{parse_zed_link, ReleaseChannel}, http::{self, HttpClient}, - paths::PathLikeWithPosition, }; use uuid::Uuid; use welcome::{show_welcome_experience, FIRST_OPEN}; @@ -58,6 +49,7 @@ use zed::{ assets::Assets, build_window_options, handle_keymap_file_changes, initialize_workspace, languages, menus, only_instance::{ensure_only_instance, IsOnlyInstance}, + open_listener::{handle_cli_connection, OpenListener, OpenRequest}, }; fn main() { @@ -92,32 +84,24 @@ fn main() { }) }; - let (cli_connections_tx, mut cli_connections_rx) = mpsc::unbounded(); - let cli_connections_tx = Arc::new(cli_connections_tx); - let (open_paths_tx, mut open_paths_rx) = mpsc::unbounded(); - let open_paths_tx = Arc::new(open_paths_tx); - let urls_callback_triggered = Arc::new(AtomicBool::new(false)); - - let callback_cli_connections_tx = Arc::clone(&cli_connections_tx); - let callback_open_paths_tx = Arc::clone(&open_paths_tx); - let callback_urls_callback_triggered = Arc::clone(&urls_callback_triggered); - app.on_open_urls(move |urls, _| { - callback_urls_callback_triggered.store(true, Ordering::Release); - open_urls(urls, &callback_cli_connections_tx, &callback_open_paths_tx); - }) - .on_reopen(move |cx| { - if cx.has_global::>() { - if let Some(app_state) = cx.global::>().upgrade() { - workspace::open_new(&app_state, cx, |workspace, cx| { - Editor::new_file(workspace, &Default::default(), cx) - }) - .detach(); + let (listener, mut open_rx) = OpenListener::new(); + let listener = Arc::new(listener); + let callback_listener = listener.clone(); + app.on_open_urls(move |urls, _| callback_listener.open_urls(urls)) + .on_reopen(move |cx| { + if cx.has_global::>() { + if let Some(app_state) = cx.global::>().upgrade() { + workspace::open_new(&app_state, cx, |workspace, cx| { + Editor::new_file(workspace, &Default::default(), cx) + }) + .detach(); + } } - } - }); + }); app.run(move |cx| { cx.set_global(*RELEASE_CHANNEL); + cx.set_global(listener.clone()); let mut store = SettingsStore::default(); store @@ -159,7 +143,12 @@ fn main() { semantic_index::init(fs.clone(), http.clone(), languages.clone(), cx); vim::init(cx); terminal_view::init(cx); - copilot::init(copilot_language_server_id, http.clone(), node_runtime, cx); + copilot::init( + copilot_language_server_id, + http.clone(), + node_runtime.clone(), + cx, + ); assistant::init(cx); component_test::init(cx); @@ -186,6 +175,7 @@ fn main() { initialize_workspace, background_actions, workspace_store, + node_runtime, }); cx.set_global(Arc::downgrade(&app_state)); @@ -210,12 +200,9 @@ fn main() { if stdout_is_a_pty() { cx.platform().activate(true); - let paths = collect_path_args(); - if paths.is_empty() { - cx.spawn(|cx| async move { restore_or_create_workspace(&app_state, cx).await }) - .detach() - } else { - workspace::open_paths(&paths, &app_state, None, cx).detach_and_log_err(cx); + let urls = collect_url_args(); + if !urls.is_empty() { + listener.open_urls(urls) } } else { upload_previous_panics(http.clone(), cx); @@ -223,61 +210,85 @@ fn main() { // TODO Development mode that forces the CLI mode usually runs Zed binary as is instead // of an *app, hence gets no specific callbacks run. Emulate them here, if needed. if std::env::var(FORCE_CLI_MODE_ENV_VAR_NAME).ok().is_some() - && !urls_callback_triggered.load(Ordering::Acquire) + && !listener.triggered.load(Ordering::Acquire) { - open_urls(collect_url_args(), &cli_connections_tx, &open_paths_tx) + listener.open_urls(collect_url_args()) } + } - if let Ok(Some(connection)) = cli_connections_rx.try_next() { - cx.spawn(|cx| handle_cli_connection(connection, app_state.clone(), cx)) - .detach(); - } else if let Ok(Some(paths)) = open_paths_rx.try_next() { + let mut triggered_authentication = false; + + match open_rx.try_next() { + Ok(Some(OpenRequest::Paths { paths })) => { cx.update(|cx| workspace::open_paths(&paths, &app_state, None, cx)) .detach(); - } else { - cx.spawn({ + } + Ok(Some(OpenRequest::CliConnection { connection })) => { + cx.spawn(|cx| handle_cli_connection(connection, app_state.clone(), cx)) + .detach(); + } + Ok(Some(OpenRequest::JoinChannel { channel_id })) => { + triggered_authentication = true; + let app_state = app_state.clone(); + let client = client.clone(); + cx.spawn(|mut cx| async move { + // ignore errors here, we'll show a generic "not signed in" + let _ = authenticate(client, &cx).await; + cx.update(|cx| workspace::join_channel(channel_id, app_state, None, cx)) + .await + }) + .detach_and_log_err(cx) + } + Ok(None) | Err(_) => cx + .spawn({ let app_state = app_state.clone(); |cx| async move { restore_or_create_workspace(&app_state, cx).await } }) - .detach() - } - - cx.spawn(|cx| { - let app_state = app_state.clone(); - async move { - while let Some(connection) = cli_connections_rx.next().await { - handle_cli_connection(connection, app_state.clone(), cx.clone()).await; - } - } - }) - .detach(); - - cx.spawn(|mut cx| { - let app_state = app_state.clone(); - async move { - while let Some(paths) = open_paths_rx.next().await { - cx.update(|cx| workspace::open_paths(&paths, &app_state, None, cx)) - .detach(); - } - } - }) - .detach(); + .detach(), } - cx.spawn(|cx| async move { - if stdout_is_a_pty() { - if client::IMPERSONATE_LOGIN.is_some() { - client.authenticate_and_connect(false, &cx).await?; + cx.spawn(|mut cx| { + let app_state = app_state.clone(); + async move { + while let Some(request) = open_rx.next().await { + match request { + OpenRequest::Paths { paths } => { + cx.update(|cx| workspace::open_paths(&paths, &app_state, None, cx)) + .detach(); + } + OpenRequest::CliConnection { connection } => { + cx.spawn(|cx| handle_cli_connection(connection, app_state.clone(), cx)) + .detach(); + } + OpenRequest::JoinChannel { channel_id } => cx + .update(|cx| { + workspace::join_channel(channel_id, app_state.clone(), None, cx) + }) + .detach(), + } } - } else if client.has_keychain_credentials(&cx) { - client.authenticate_and_connect(true, &cx).await?; } - Ok::<_, anyhow::Error>(()) }) - .detach_and_log_err(cx); + .detach(); + + if !triggered_authentication { + cx.spawn(|cx| async move { authenticate(client, &cx).await }) + .detach_and_log_err(cx); + } }); } +async fn authenticate(client: Arc, cx: &AsyncAppContext) -> Result<()> { + if stdout_is_a_pty() { + if client::IMPERSONATE_LOGIN.is_some() { + client.authenticate_and_connect(false, &cx).await?; + } + } else if client.has_keychain_credentials(&cx) { + client.authenticate_and_connect(true, &cx).await?; + } + Ok::<_, anyhow::Error>(()) +} + async fn installation_id() -> Result { let legacy_key_name = "device_id"; @@ -294,37 +305,6 @@ async fn installation_id() -> Result { } } -fn open_urls( - urls: Vec, - cli_connections_tx: &mpsc::UnboundedSender<( - mpsc::Receiver, - IpcSender, - )>, - open_paths_tx: &mpsc::UnboundedSender>, -) { - if let Some(server_name) = urls.first().and_then(|url| url.strip_prefix("zed-cli://")) { - if let Some(cli_connection) = connect_to_cli(server_name).log_err() { - cli_connections_tx - .unbounded_send(cli_connection) - .map_err(|_| anyhow!("no listener for cli connections")) - .log_err(); - }; - } else { - let paths: Vec<_> = urls - .iter() - .flat_map(|url| url.strip_prefix("file://")) - .map(|url| { - let decoded = urlencoding::decode_binary(url.as_bytes()); - PathBuf::from(OsStr::from_bytes(decoded.as_ref())) - }) - .collect(); - open_paths_tx - .unbounded_send(paths) - .map_err(|_| anyhow!("no listener for open urls requests")) - .log_err(); - } -} - async fn restore_or_create_workspace(app_state: &Arc, mut cx: AsyncAppContext) { if let Some(location) = workspace::last_opened_workspace_paths().await { cx.update(|cx| workspace::open_paths(location.paths().as_ref(), app_state, None, cx)) @@ -634,23 +614,23 @@ fn stdout_is_a_pty() -> bool { std::env::var(FORCE_CLI_MODE_ENV_VAR_NAME).ok().is_none() && std::io::stdout().is_terminal() } -fn collect_path_args() -> Vec { +fn collect_url_args() -> Vec { env::args() .skip(1) - .filter_map(|arg| match std::fs::canonicalize(arg) { - Ok(path) => Some(path), + .filter_map(|arg| match std::fs::canonicalize(Path::new(&arg)) { + Ok(path) => Some(format!("file://{}", path.to_string_lossy())), Err(error) => { - log::error!("error parsing path argument: {}", error); - None + if let Some(_) = parse_zed_link(&arg) { + Some(arg) + } else { + log::error!("error parsing path argument: {}", error); + None + } } }) .collect() } -fn collect_url_args() -> Vec { - env::args().skip(1).collect() -} - fn load_embedded_fonts(app: &App) { let font_paths = Assets.list("fonts"); let embedded_fonts = Mutex::new(Vec::new()); @@ -744,189 +724,6 @@ async fn watch_languages(_: Arc, _: Arc) -> Option<()> #[cfg(not(debug_assertions))] fn watch_file_types(_fs: Arc, _cx: &mut AppContext) {} -fn connect_to_cli( - server_name: &str, -) -> Result<(mpsc::Receiver, IpcSender)> { - let handshake_tx = cli::ipc::IpcSender::::connect(server_name.to_string()) - .context("error connecting to cli")?; - let (request_tx, request_rx) = ipc::channel::()?; - let (response_tx, response_rx) = ipc::channel::()?; - - handshake_tx - .send(IpcHandshake { - requests: request_tx, - responses: response_rx, - }) - .context("error sending ipc handshake")?; - - let (mut async_request_tx, async_request_rx) = - futures::channel::mpsc::channel::(16); - thread::spawn(move || { - while let Ok(cli_request) = request_rx.recv() { - if smol::block_on(async_request_tx.send(cli_request)).is_err() { - break; - } - } - Ok::<_, anyhow::Error>(()) - }); - - Ok((async_request_rx, response_tx)) -} - -async fn handle_cli_connection( - (mut requests, responses): (mpsc::Receiver, IpcSender), - app_state: Arc, - mut cx: AsyncAppContext, -) { - if let Some(request) = requests.next().await { - match request { - CliRequest::Open { paths, wait } => { - let mut caret_positions = HashMap::new(); - - let paths = if paths.is_empty() { - workspace::last_opened_workspace_paths() - .await - .map(|location| location.paths().to_vec()) - .unwrap_or_default() - } else { - paths - .into_iter() - .filter_map(|path_with_position_string| { - let path_with_position = PathLikeWithPosition::parse_str( - &path_with_position_string, - |path_str| { - Ok::<_, std::convert::Infallible>( - Path::new(path_str).to_path_buf(), - ) - }, - ) - .expect("Infallible"); - let path = path_with_position.path_like; - if let Some(row) = path_with_position.row { - if path.is_file() { - let row = row.saturating_sub(1); - let col = - path_with_position.column.unwrap_or(0).saturating_sub(1); - caret_positions.insert(path.clone(), Point::new(row, col)); - } - } - Some(path) - }) - .collect() - }; - - let mut errored = false; - match cx - .update(|cx| workspace::open_paths(&paths, &app_state, None, cx)) - .await - { - Ok((workspace, items)) => { - let mut item_release_futures = Vec::new(); - - for (item, path) in items.into_iter().zip(&paths) { - match item { - Some(Ok(item)) => { - if let Some(point) = caret_positions.remove(path) { - if let Some(active_editor) = item.downcast::() { - active_editor - .downgrade() - .update(&mut cx, |editor, cx| { - let snapshot = - editor.snapshot(cx).display_snapshot; - let point = snapshot - .buffer_snapshot - .clip_point(point, Bias::Left); - editor.change_selections( - Some(Autoscroll::center()), - cx, - |s| s.select_ranges([point..point]), - ); - }) - .log_err(); - } - } - - let released = oneshot::channel(); - cx.update(|cx| { - item.on_release( - cx, - Box::new(move |_| { - let _ = released.0.send(()); - }), - ) - .detach(); - }); - item_release_futures.push(released.1); - } - Some(Err(err)) => { - responses - .send(CliResponse::Stderr { - message: format!("error opening {:?}: {}", path, err), - }) - .log_err(); - errored = true; - } - None => {} - } - } - - if wait { - let background = cx.background(); - let wait = async move { - if paths.is_empty() { - let (done_tx, done_rx) = oneshot::channel(); - if let Some(workspace) = workspace.upgrade(&cx) { - let _subscription = cx.update(|cx| { - cx.observe_release(&workspace, move |_, _| { - let _ = done_tx.send(()); - }) - }); - drop(workspace); - let _ = done_rx.await; - } - } else { - let _ = - futures::future::try_join_all(item_release_futures).await; - }; - } - .fuse(); - futures::pin_mut!(wait); - - loop { - // Repeatedly check if CLI is still open to avoid wasting resources - // waiting for files or workspaces to close. - let mut timer = background.timer(Duration::from_secs(1)).fuse(); - futures::select_biased! { - _ = wait => break, - _ = timer => { - if responses.send(CliResponse::Ping).is_err() { - break; - } - } - } - } - } - } - Err(error) => { - errored = true; - responses - .send(CliResponse::Stderr { - message: format!("error opening {:?}: {}", paths, error), - }) - .log_err(); - } - } - - responses - .send(CliResponse::Exit { - status: i32::from(errored), - }) - .log_err(); - } - } - } -} - pub fn background_actions() -> &'static [(&'static str, &'static dyn Action)] { &[ ("Go to file", &file_finder::Toggle), diff --git a/crates/zed/src/open_listener.rs b/crates/zed/src/open_listener.rs new file mode 100644 index 0000000000..578d8cd69f --- /dev/null +++ b/crates/zed/src/open_listener.rs @@ -0,0 +1,292 @@ +use anyhow::{anyhow, Context, Result}; +use cli::{ipc, IpcHandshake}; +use cli::{ipc::IpcSender, CliRequest, CliResponse}; +use editor::scroll::autoscroll::Autoscroll; +use editor::Editor; +use futures::channel::mpsc::{UnboundedReceiver, UnboundedSender}; +use futures::channel::{mpsc, oneshot}; +use futures::{FutureExt, SinkExt, StreamExt}; +use gpui::AsyncAppContext; +use language::{Bias, Point}; +use std::collections::HashMap; +use std::ffi::OsStr; +use std::os::unix::prelude::OsStrExt; +use std::path::Path; +use std::sync::atomic::Ordering; +use std::sync::Arc; +use std::thread; +use std::time::Duration; +use std::{path::PathBuf, sync::atomic::AtomicBool}; +use util::channel::parse_zed_link; +use util::paths::PathLikeWithPosition; +use util::ResultExt; +use workspace::AppState; + +pub enum OpenRequest { + Paths { + paths: Vec, + }, + CliConnection { + connection: (mpsc::Receiver, IpcSender), + }, + JoinChannel { + channel_id: u64, + }, +} + +pub struct OpenListener { + tx: UnboundedSender, + pub triggered: AtomicBool, +} + +impl OpenListener { + pub fn new() -> (Self, UnboundedReceiver) { + let (tx, rx) = mpsc::unbounded(); + ( + OpenListener { + tx, + triggered: AtomicBool::new(false), + }, + rx, + ) + } + + pub fn open_urls(&self, urls: Vec) { + self.triggered.store(true, Ordering::Release); + let request = if let Some(server_name) = + urls.first().and_then(|url| url.strip_prefix("zed-cli://")) + { + self.handle_cli_connection(server_name) + } else if let Some(request_path) = urls.first().and_then(|url| parse_zed_link(url)) { + self.handle_zed_url_scheme(request_path) + } else { + self.handle_file_urls(urls) + }; + + if let Some(request) = request { + self.tx + .unbounded_send(request) + .map_err(|_| anyhow!("no listener for open requests")) + .log_err(); + } + } + + fn handle_cli_connection(&self, server_name: &str) -> Option { + if let Some(connection) = connect_to_cli(server_name).log_err() { + return Some(OpenRequest::CliConnection { connection }); + } + + None + } + + fn handle_zed_url_scheme(&self, request_path: &str) -> Option { + let mut parts = request_path.split("/"); + if parts.next() == Some("channel") { + if let Some(slug) = parts.next() { + if let Some(id_str) = slug.split("-").last() { + if let Ok(channel_id) = id_str.parse::() { + return Some(OpenRequest::JoinChannel { channel_id }); + } + } + } + } + log::error!("invalid zed url: {}", request_path); + None + } + + fn handle_file_urls(&self, urls: Vec) -> Option { + let paths: Vec<_> = urls + .iter() + .flat_map(|url| url.strip_prefix("file://")) + .map(|url| { + let decoded = urlencoding::decode_binary(url.as_bytes()); + PathBuf::from(OsStr::from_bytes(decoded.as_ref())) + }) + .collect(); + + Some(OpenRequest::Paths { paths }) + } +} + +fn connect_to_cli( + server_name: &str, +) -> Result<(mpsc::Receiver, IpcSender)> { + let handshake_tx = cli::ipc::IpcSender::::connect(server_name.to_string()) + .context("error connecting to cli")?; + let (request_tx, request_rx) = ipc::channel::()?; + let (response_tx, response_rx) = ipc::channel::()?; + + handshake_tx + .send(IpcHandshake { + requests: request_tx, + responses: response_rx, + }) + .context("error sending ipc handshake")?; + + let (mut async_request_tx, async_request_rx) = + futures::channel::mpsc::channel::(16); + thread::spawn(move || { + while let Ok(cli_request) = request_rx.recv() { + if smol::block_on(async_request_tx.send(cli_request)).is_err() { + break; + } + } + Ok::<_, anyhow::Error>(()) + }); + + Ok((async_request_rx, response_tx)) +} + +pub async fn handle_cli_connection( + (mut requests, responses): (mpsc::Receiver, IpcSender), + app_state: Arc, + mut cx: AsyncAppContext, +) { + if let Some(request) = requests.next().await { + match request { + CliRequest::Open { paths, wait } => { + let mut caret_positions = HashMap::new(); + + let paths = if paths.is_empty() { + workspace::last_opened_workspace_paths() + .await + .map(|location| location.paths().to_vec()) + .unwrap_or_default() + } else { + paths + .into_iter() + .filter_map(|path_with_position_string| { + let path_with_position = PathLikeWithPosition::parse_str( + &path_with_position_string, + |path_str| { + Ok::<_, std::convert::Infallible>( + Path::new(path_str).to_path_buf(), + ) + }, + ) + .expect("Infallible"); + let path = path_with_position.path_like; + if let Some(row) = path_with_position.row { + if path.is_file() { + let row = row.saturating_sub(1); + let col = + path_with_position.column.unwrap_or(0).saturating_sub(1); + caret_positions.insert(path.clone(), Point::new(row, col)); + } + } + Some(path) + }) + .collect() + }; + + let mut errored = false; + match cx + .update(|cx| workspace::open_paths(&paths, &app_state, None, cx)) + .await + { + Ok((workspace, items)) => { + let mut item_release_futures = Vec::new(); + + for (item, path) in items.into_iter().zip(&paths) { + match item { + Some(Ok(item)) => { + if let Some(point) = caret_positions.remove(path) { + if let Some(active_editor) = item.downcast::() { + active_editor + .downgrade() + .update(&mut cx, |editor, cx| { + let snapshot = + editor.snapshot(cx).display_snapshot; + let point = snapshot + .buffer_snapshot + .clip_point(point, Bias::Left); + editor.change_selections( + Some(Autoscroll::center()), + cx, + |s| s.select_ranges([point..point]), + ); + }) + .log_err(); + } + } + + let released = oneshot::channel(); + cx.update(|cx| { + item.on_release( + cx, + Box::new(move |_| { + let _ = released.0.send(()); + }), + ) + .detach(); + }); + item_release_futures.push(released.1); + } + Some(Err(err)) => { + responses + .send(CliResponse::Stderr { + message: format!("error opening {:?}: {}", path, err), + }) + .log_err(); + errored = true; + } + None => {} + } + } + + if wait { + let background = cx.background(); + let wait = async move { + if paths.is_empty() { + let (done_tx, done_rx) = oneshot::channel(); + if let Some(workspace) = workspace.upgrade(&cx) { + let _subscription = cx.update(|cx| { + cx.observe_release(&workspace, move |_, _| { + let _ = done_tx.send(()); + }) + }); + drop(workspace); + let _ = done_rx.await; + } + } else { + let _ = + futures::future::try_join_all(item_release_futures).await; + }; + } + .fuse(); + futures::pin_mut!(wait); + + loop { + // Repeatedly check if CLI is still open to avoid wasting resources + // waiting for files or workspaces to close. + let mut timer = background.timer(Duration::from_secs(1)).fuse(); + futures::select_biased! { + _ = wait => break, + _ = timer => { + if responses.send(CliResponse::Ping).is_err() { + break; + } + } + } + } + } + } + Err(error) => { + errored = true; + responses + .send(CliResponse::Stderr { + message: format!("error opening {:?}: {}", paths, error), + }) + .log_err(); + } + } + + responses + .send(CliResponse::Exit { + status: i32::from(errored), + }) + .log_err(); + } + } + } +} diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 4e9a34c269..c2a218acae 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -2,6 +2,7 @@ pub mod assets; pub mod languages; pub mod menus; pub mod only_instance; +pub mod open_listener; #[cfg(any(test, feature = "test-support"))] pub mod test; @@ -28,6 +29,7 @@ use gpui::{ AppContext, AsyncAppContext, Task, ViewContext, WeakViewHandle, }; pub use lsp; +use open_listener::OpenListener; pub use project; use project_panel::ProjectPanel; use quick_action_bar::QuickActionBar; @@ -87,6 +89,10 @@ pub fn init(app_state: &Arc, cx: &mut gpui::AppContext) { }, ); cx.add_global_action(quit); + cx.add_global_action(move |action: &OpenZedURL, cx| { + cx.global::>() + .open_urls(vec![action.url.clone()]) + }); cx.add_global_action(move |action: &OpenBrowser, cx| cx.platform().open_url(&action.url)); cx.add_global_action(move |_: &IncreaseBufferFontSize, cx| { theme::adjust_font_size(cx, |size| *size += 1.0) diff --git a/script/bundle b/script/bundle index 49da1072ce..4775e15837 100755 --- a/script/bundle +++ b/script/bundle @@ -5,6 +5,7 @@ set -e build_flag="--release" target_dir="release" open_result=false +local_arch=false local_only=false overwrite_local_app=false bundle_name="" @@ -16,8 +17,8 @@ Usage: ${0##*/} [options] [bundle_name] Build the application bundle. Options: - -d Compile in debug mode and print the app bundle's path. - -l Compile for local architecture only and copy bundle to /Applications. + -d Compile in debug mode + -l Compile for local architecture and copy bundle to /Applications, implies -d. -o Open the resulting DMG or the app itself in local mode. -f Overwrite the local app bundle if it exists. -h Display this help and exit. @@ -32,10 +33,20 @@ do case "${flag}" in o) open_result=true;; d) + export CARGO_INCREMENTAL=true + export CARGO_BUNDLE_SKIP_BUILD=true build_flag=""; + local_arch=true + target_dir="debug" + ;; + l) + export CARGO_INCREMENTAL=true + export CARGO_BUNDLE_SKIP_BUILD=true + build_flag="" + local_arch=true + local_only=true target_dir="debug" ;; - l) local_only=true;; f) overwrite_local_app=true;; h) help_info @@ -67,7 +78,7 @@ version_info=$(rustc --version --verbose) host_line=$(echo "$version_info" | grep host) local_target_triple=${host_line#*: } -if [ "$local_only" = true ]; then +if [ "$local_arch" = true ]; then echo "Building for local target only." cargo build ${build_flag} --package zed cargo build ${build_flag} --package cli @@ -91,7 +102,7 @@ sed \ "s/package.metadata.bundle-${channel}/package.metadata.bundle/" \ Cargo.toml -if [ "$local_only" = true ]; then +if [ "$local_arch" = true ]; then app_path=$(cargo bundle ${build_flag} --select-workspace-root | xargs) else app_path=$(cargo bundle ${build_flag} --target x86_64-apple-darwin --select-workspace-root | xargs) @@ -101,7 +112,7 @@ mv Cargo.toml.backup Cargo.toml popd echo "Bundled ${app_path}" -if [ "$local_only" = false ]; then +if [ "$local_arch" = false ]; then echo "Creating fat binaries" lipo \ -create \ @@ -117,7 +128,13 @@ fi echo "Copying WebRTC.framework into the frameworks folder" mkdir "${app_path}/Contents/Frameworks" -cp -R target/${local_target_triple}/${target_dir}/WebRTC.framework "${app_path}/Contents/Frameworks/" +if [ "$local_arch" = false ]; then + cp -R target/${local_target_triple}/${target_dir}/WebRTC.framework "${app_path}/Contents/Frameworks/" +else + cp -R target/${target_dir}/WebRTC.framework "${app_path}/Contents/Frameworks/" +fi + +cp crates/zed/contents/$channel/embedded.provisionprofile "${app_path}/Contents/" if [[ -n $MACOS_CERTIFICATE && -n $MACOS_CERTIFICATE_PASSWORD && -n $APPLE_NOTARIZATION_USERNAME && -n $APPLE_NOTARIZATION_PASSWORD ]]; then echo "Signing bundle with Apple-issued certificate" @@ -128,15 +145,35 @@ if [[ -n $MACOS_CERTIFICATE && -n $MACOS_CERTIFICATE_PASSWORD && -n $APPLE_NOTAR security import /tmp/zed-certificate.p12 -k zed.keychain -P "$MACOS_CERTIFICATE_PASSWORD" -T /usr/bin/codesign rm /tmp/zed-certificate.p12 security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$MACOS_CERTIFICATE_PASSWORD" zed.keychain - /usr/bin/codesign --force --deep --timestamp --options runtime --entitlements crates/zed/resources/zed.entitlements --sign "Zed Industries, Inc." "${app_path}" -v + + # sequence of codesign commands modeled after this example: https://developer.apple.com/forums/thread/701514 + /usr/bin/codesign --force --timestamp --sign "Zed Industries, Inc." "${app_path}/Contents/Frameworks/WebRTC.framework" -v + /usr/bin/codesign --force --timestamp --options runtime --sign "Zed Industries, Inc." "${app_path}/Contents/MacOS/cli" -v + /usr/bin/codesign --force --timestamp --options runtime --entitlements crates/zed/resources/zed.entitlements --sign "Zed Industries, Inc." "${app_path}" -v + security default-keychain -s login.keychain else echo "One or more of the following variables are missing: MACOS_CERTIFICATE, MACOS_CERTIFICATE_PASSWORD, APPLE_NOTARIZATION_USERNAME, APPLE_NOTARIZATION_PASSWORD" - echo "Performing an ad-hoc signature, but this bundle should not be distributed" - codesign --force --deep --entitlements crates/zed/resources/zed.entitlements --sign - "${app_path}" -v + if [[ "$local_only" = false ]]; then + echo "To create a self-signed local build use ./scripts/build.sh -ldf" + exit 1 + fi + + echo "====== WARNING ======" + echo "This bundle is being signed without all entitlements, some features (e.g. universal links) will not work" + echo "====== WARNING ======" + + # NOTE: if you need to test universal links you have a few paths forward: + # - create a PR and tag it with the `run-build-dmg` label, and download the .dmg file from there. + # - get a signing key for the MQ55VZLNZQ team from Nathan. + # - create your own signing key, and update references to MQ55VZLNZQ to your own team ID + # then comment out this line. + cat crates/zed/resources/zed.entitlements | sed '/com.apple.developer.associated-domains/,+1d' > "${app_path}/Contents/Resources/zed.entitlements" + + codesign --force --deep --entitlements "${app_path}/Contents/Resources/zed.entitlements" --sign ${MACOS_SIGNING_KEY:- -} "${app_path}" -v fi -if [ "$target_dir" = "debug" ]; then +if [[ "$target_dir" = "debug" && "$local_only" = false ]]; then if [ "$open_result" = true ]; then open "$app_path" else diff --git a/styles/src/style_tree/assistant.ts b/styles/src/style_tree/assistant.ts index cc6ee4b080..08297731bb 100644 --- a/styles/src/style_tree/assistant.ts +++ b/styles/src/style_tree/assistant.ts @@ -79,6 +79,80 @@ export default function assistant(): any { }, }, pending_edit_background: background(theme.highest, "positive"), + context_status: { + error_icon: { + margin: { left: 8, right: 18 }, + color: foreground(theme.highest, "negative"), + width: 12, + }, + in_progress_icon: { + margin: { left: 8, right: 18 }, + color: foreground(theme.highest, "positive"), + width: 12, + }, + complete_icon: { + margin: { left: 8, right: 18 }, + color: foreground(theme.highest, "positive"), + width: 12, + } + }, + retrieve_context: toggleable({ + base: interactive({ + base: { + icon_size: 12, + color: foreground(theme.highest, "variant"), + + button_width: 12, + background: background(theme.highest, "on"), + corner_radius: 2, + border: { + width: 1., color: background(theme.highest, "on") + }, + margin: { left: 2 }, + padding: { + left: 4, + right: 4, + top: 4, + bottom: 4, + }, + }, + state: { + hovered: { + ...text(theme.highest, "mono", "variant", "hovered"), + background: background(theme.highest, "on", "hovered"), + border: { + width: 1., color: background(theme.highest, "on", "hovered") + }, + }, + clicked: { + ...text(theme.highest, "mono", "variant", "pressed"), + background: background(theme.highest, "on", "pressed"), + border: { + width: 1., color: background(theme.highest, "on", "pressed") + }, + }, + }, + }), + state: { + active: { + default: { + icon_size: 12, + button_width: 12, + color: foreground(theme.highest, "variant"), + background: background(theme.highest, "accent"), + border: border(theme.highest, "accent"), + }, + hovered: { + background: background(theme.highest, "accent", "hovered"), + border: border(theme.highest, "accent", "hovered"), + }, + clicked: { + background: background(theme.highest, "accent", "pressed"), + border: border(theme.highest, "accent", "pressed"), + }, + }, + }, + }), include_conversation: toggleable({ base: interactive({ base: { diff --git a/styles/src/style_tree/collab_modals.ts b/styles/src/style_tree/collab_modals.ts index f9b22b6867..6132ce5ff4 100644 --- a/styles/src/style_tree/collab_modals.ts +++ b/styles/src/style_tree/collab_modals.ts @@ -1,10 +1,11 @@ -import { useTheme } from "../theme" +import { StyleSets, useTheme } from "../theme" import { background, border, foreground, text } from "./components" import picker from "./picker" import { input } from "../component/input" import contact_finder from "./contact_finder" import { tab } from "../component/tab" import { icon_button } from "../component/icon_button" +import { interactive } from "../element/interactive" export default function channel_modal(): any { const theme = useTheme() @@ -27,6 +28,24 @@ export default function channel_modal(): any { const picker_input = input() + const interactive_text = (styleset: StyleSets) => + interactive({ + base: { + padding: { + left: 8, + top: 8 + }, + ...text(theme.middle, "sans", styleset, "default"), + }, state: { + hovered: { + ...text(theme.middle, "sans", styleset, "hovered"), + }, + clicked: { + ...text(theme.middle, "sans", styleset, "active"), + } + } + }) + const member_icon_style = icon_button({ variant: "ghost", size: "sm", @@ -88,6 +107,8 @@ export default function channel_modal(): any { left: BUTTON_OFFSET, }, }, + visibility_toggle: interactive_text("base"), + channel_link: interactive_text("accent"), picker: { empty_container: {}, item: { diff --git a/styles/src/style_tree/editor.ts b/styles/src/style_tree/editor.ts index e55a73c365..27a6eaf195 100644 --- a/styles/src/style_tree/editor.ts +++ b/styles/src/style_tree/editor.ts @@ -206,9 +206,13 @@ export default function editor(): any { match_highlight: foreground(theme.middle, "accent", "active"), background: background(theme.middle, "active"), }, - server_name_container: { padding: { left: 40 } }, - server_name_color: text(theme.middle, "sans", "disabled", {}).color, - server_name_size_percent: 0.75, + completion_min_width: 300, + completion_max_width: 700, + inline_docs_container: { padding: { left: 40 } }, + inline_docs_color: text(theme.middle, "sans", "disabled", {}).color, + inline_docs_size_percent: 0.75, + alongside_docs_max_width: 700, + alongside_docs_container: { padding: autocomplete_item.padding } }, diagnostic_header: { background: background(theme.middle),