Detailed changes
@@ -2,11 +2,4 @@
Release Notes:
-- N/A
-
-or
-
- (Added|Fixed|Improved) ... ([#<public_issue_number_if_exists>](https://github.com/zed-industries/community/issues/<public_issue_number_if_exists>)).
-
-If the release notes are only intended for a specific release channel only, add `(<release_channel>-only)` to the end of the release note line.
-These will be removed by the person making the release.
@@ -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,19 @@ jobs:
URL="https://zed.dev/releases/stable/latest"
fi
echo "::set-output name=URL::$URL"
-
- - name: Discord Webhook Action
- uses: tsickert/discord-webhook@v5.3.0
+ - name: Get content
+ uses: 2428392/gh-truncate-string-action@v1.2.0
+ id: get-content
with:
- webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
- content: |
+ stringToTruncate: |
📣 Zed ${{ github.event.release.tag_name }} was just released!
- Restart your Zed or head to ${{ steps.get-appropriate-url.outputs.URL }} to grab it.
+ Restart your Zed or head to ${{ steps.get-release-url.outputs.URL }} to grab it.
${{ 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: ${{ steps.get-content.outputs.string }}
@@ -1083,7 +1083,6 @@ dependencies = [
"anyhow",
"async-broadcast",
"audio",
- "channel",
"client",
"collections",
"fs",
@@ -1468,7 +1467,7 @@ dependencies = [
[[package]]
name = "collab"
-version = "0.23.2"
+version = "0.24.0"
dependencies = [
"anyhow",
"async-trait",
@@ -1503,6 +1502,7 @@ dependencies = [
"log",
"lsp",
"nanoid",
+ "node_runtime",
"parking_lot 0.11.2",
"pretty_assertions",
"project",
@@ -2080,9 +2080,9 @@ dependencies = [
[[package]]
name = "curl-sys"
-version = "0.4.66+curl-8.3.0"
+version = "0.4.67+curl-8.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "70c44a72e830f0e40ad90dda8a6ab6ed6314d39776599a58a2e5e37fbc6db5b9"
+checksum = "3cc35d066510b197a0f72de863736641539957628c8a42e70e27c66849e77c34"
dependencies = [
"cc",
"libc",
@@ -2405,7 +2405,6 @@ dependencies = [
"parking_lot 0.11.2",
"postage",
"project",
- "pulldown-cmark",
"rand 0.8.5",
"rich_text",
"rpc",
@@ -2833,7 +2832,6 @@ dependencies = [
"parking_lot 0.11.2",
"regex",
"rope",
- "rpc",
"serde",
"serde_derive",
"serde_json",
@@ -3991,6 +3989,7 @@ dependencies = [
"lsp",
"parking_lot 0.11.2",
"postage",
+ "pulldown-cmark",
"rand 0.8.5",
"regex",
"rpc",
@@ -5520,6 +5519,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"
@@ -5632,8 +5651,10 @@ dependencies = [
"lazy_static",
"log",
"lsp",
+ "node_runtime",
"parking_lot 0.11.2",
"postage",
+ "prettier",
"pretty_assertions",
"rand 0.8.5",
"regex",
@@ -6603,12 +6624,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"
@@ -6942,7 +6957,6 @@ dependencies = [
"unindent",
"util",
"workspace",
- "zed",
]
[[package]]
@@ -7662,28 +7676,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"
@@ -7706,22 +7698,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"
@@ -8802,6 +8778,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"
@@ -8873,21 +8858,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"
@@ -9651,6 +9621,7 @@ dependencies = [
"theme",
"theme_selector",
"util",
+ "vim",
"workspace",
]
@@ -9957,7 +9928,6 @@ dependencies = [
"async-recursion 1.0.5",
"bincode",
"call",
- "channel",
"client",
"collections",
"context_menu",
@@ -9974,6 +9944,7 @@ dependencies = [
"lazy_static",
"log",
"menu",
+ "node_runtime",
"parking_lot 0.11.2",
"postage",
"project",
@@ -10069,9 +10040,10 @@ dependencies = [
[[package]]
name = "zed"
-version = "0.108.0"
+version = "0.109.0"
dependencies = [
"activity_indicator",
+ "ai",
"anyhow",
"assistant",
"async-compression",
@@ -10184,6 +10156,7 @@ dependencies = [
"tree-sitter-svelte",
"tree-sitter-toml",
"tree-sitter-typescript",
+ "tree-sitter-vue",
"tree-sitter-yaml",
"unindent",
"url",
@@ -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" }
@@ -83,9 +83,7 @@ foreman start
If you want to run Zed pointed at the local servers, you can run:
```
-script/zed-with-local-servers
-# or...
-script/zed-with-local-servers --release
+script/zed-local
```
### Dump element JSON
@@ -408,6 +408,7 @@
"vim::PushOperator",
"Yank"
],
+ "shift-y": "vim::YankLine",
"i": "vim::InsertBefore",
"shift-i": "vim::InsertFirstNonWhitespace",
"a": "vim::InsertAfter",
@@ -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.
@@ -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;
@@ -296,22 +296,36 @@ impl AssistantPanel {
if selection.start.excerpt_id() != selection.end.excerpt_id() {
return;
}
-
- 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::<usize>(cx).is_empty() {
+
+ // 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: selection.start,
+ position: snapshot.anchor_after(point_selection.start),
}
} else {
CodegenKind::Transform {
- range: selection.start..selection.end,
+ 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 provider = Arc::new(OpenAICompletionProvider::new(
+ api_key,
+ cx.background().clone(),
+ ));
+
let codegen = cx.add_model(|cx| {
Codegen::new(editor.read(cx).buffer().clone(), codegen_kind, provider, cx)
});
@@ -361,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();
@@ -654,12 +668,14 @@ impl AssistantPanel {
let snippets = cx.spawn(|_, cx| async move {
let mut snippets = Vec::new();
for result in search_results.await {
- snippets.push(result.buffer.read_with(&cx, |buffer, _| {
- buffer
- .snapshot()
- .text_for_range(result.range)
- .collect::<String>()
- }));
+ snippets.push(PromptCodeSnippet::new(result, &cx));
+
+ // snippets.push(result.buffer.read_with(&cx, |buffer, _| {
+ // buffer
+ // .snapshot()
+ // .text_for_range(result.range)
+ // .collect::<String>()
+ // }));
}
snippets
});
@@ -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<MultiBuffer>,
- mut kind: CodegenKind,
+ kind: CodegenKind,
provider: Arc<dyn CompletionProvider>,
cx: &mut ModelContext<Self>,
) -> 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| {
@@ -1,10 +1,65 @@
use crate::codegen::CodegenKind;
-use language::{BufferSnapshot, OffsetRangeExt, ToOffset};
+use gpui::{AppContext, AsyncAppContext};
+use language::{BufferSnapshot, Language, OffsetRangeExt, ToOffset};
+use semantic_index::SearchResult;
+use std::borrow::Cow;
use std::cmp::{self, Reverse};
use std::fmt::Write;
use std::ops::Range;
+use std::path::PathBuf;
+use std::sync::Arc;
use tiktoken_rs::ChatCompletionRequestMessage;
+pub struct PromptCodeSnippet {
+ path: Option<PathBuf>,
+ language_name: Option<String>,
+ 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, cx| {
+ let snapshot = buffer.snapshot();
+ let content = snapshot
+ .text_for_range(search_result.range.clone())
+ .collect::<String>();
+
+ let language_name = buffer
+ .language()
+ .and_then(|language| Some(language.name().to_string()));
+
+ let language = buffer.language();
+ 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<impl ToOffset>) -> String {
#[derive(Debug)]
struct Match {
@@ -121,13 +176,14 @@ pub fn generate_content_prompt(
buffer: &BufferSnapshot,
range: Range<impl ToOffset>,
kind: CodegenKind,
- search_results: Vec<String>,
+ search_results: Vec<PromptCodeSnippet>,
model: &str,
) -> String {
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 {
@@ -139,13 +195,26 @@ pub fn generate_content_prompt(
// Snippets
let mut snippet_position = prompts.len() - 1;
- let outline = summarize(buffer, range);
- prompts.push("The file you are currently working on has the following outline:".to_string());
+ 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();
- prompts.push(format!("```{language_name}\n{outline}\n```"));
+ prompts.push(format!("```{language_name}\n{content}\n```"));
} else {
- prompts.push(format!("```\n{outline}\n```"));
+ prompts.push(format!("```\n{content}\n```"));
}
match kind {
@@ -164,17 +233,20 @@ pub fn generate_content_prompt(
CodegenKind::Transform { range: _ } => {
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}"
+ "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 {
- prompts.push(format!("Your answer MUST always be valid {language_name}"));
+ prompts.push(format!(
+ "Your answer MUST always and only be valid {language_name}"
+ ));
}
- prompts.push("Always wrap your response in a Markdown codeblock".to_string());
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("DO NOT wrap your text in a Markdown block".to_string());
let current_messages = [ChatCompletionRequestMessage {
role: "user".to_string(),
@@ -208,7 +280,8 @@ pub fn generate_content_prompt(
for search_result in search_results {
let mut snippet_prompt = template.to_string();
- writeln!(snippet_prompt, "```\n{search_result}\n```").unwrap();
+ 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())
@@ -20,7 +20,6 @@ test-support = [
[dependencies]
audio = { path = "../audio" }
-channel = { path = "../channel" }
client = { path = "../client" }
collections = { path = "../collections" }
gpui = { path = "../gpui" }
@@ -5,7 +5,6 @@ pub mod room;
use anyhow::{anyhow, Result};
use audio::Audio;
use call_settings::CallSettings;
-use channel::ChannelId;
use client::{
proto, ClickhouseEvent, Client, TelemetrySettings, TypedEnvelope, User, UserStore,
ZED_ALWAYS_ACTIVE,
@@ -79,7 +78,7 @@ impl ActiveCall {
}
}
- pub fn channel_id(&self, cx: &AppContext) -> Option<ChannelId> {
+ pub fn channel_id(&self, cx: &AppContext) -> Option<u64> {
self.room()?.read(cx).channel_id()
}
@@ -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<UserStore>,
follows_by_leader_id_project_id: HashMap<(PeerId, u64), Vec<PeerId>>,
subscriptions: Vec<client::Subscription>,
+ room_update_completed_tx: watch::Sender<Option<()>>,
+ room_update_completed_rx: watch::Receiver<Option<()>>,
pending_room_update: Option<Task<()>>,
maintain_connection: Option<Task<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,28 +605,40 @@ impl Room {
}
/// Returns the most 'active' projects, defined as most people in the project
- pub fn most_active_project(&self) -> Option<(u64, u64)> {
- let mut projects = HashMap::default();
- let mut hosts = HashMap::default();
+ pub fn most_active_project(&self, cx: &AppContext) -> Option<(u64, u64)> {
+ let mut project_hosts_and_guest_counts = HashMap::<u64, (Option<u64>, u32)>::default();
for participant in self.remote_participants.values() {
match participant.location {
ParticipantLocation::SharedProject { project_id } => {
- *projects.entry(project_id).or_insert(0) += 1;
+ project_hosts_and_guest_counts
+ .entry(project_id)
+ .or_default()
+ .1 += 1;
}
ParticipantLocation::External | ParticipantLocation::UnsharedProject => {}
}
for project in &participant.projects {
- *projects.entry(project.id).or_insert(0) += 1;
- hosts.insert(project.id, participant.user.id);
+ project_hosts_and_guest_counts
+ .entry(project.id)
+ .or_default()
+ .0 = Some(participant.user.id);
}
}
- let mut pairs: Vec<(u64, usize)> = projects.into_iter().collect();
- pairs.sort_by_key(|(_, count)| *count as i32);
+ 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);
+ }
+ }
- pairs
- .first()
- .map(|(project_id, _)| (*project_id, hosts[&project_id]))
+ project_hosts_and_guest_counts
+ .into_iter()
+ .filter_map(|(id, (host, guest_count))| Some((id, host?, guest_count)))
+ .max_by_key(|(_, _, guest_count)| *guest_count)
+ .map(|(id, host, _)| (id, host))
}
async fn handle_room_updated(
@@ -686,6 +704,7 @@ impl Room {
let Some(peer_id) = participant.peer_id else {
continue;
};
+ let participant_index = ParticipantIndex(participant.participant_index);
this.participant_user_ids.insert(participant.user_id);
let old_projects = this
@@ -736,8 +755,9 @@ impl Room {
if let Some(remote_participant) =
this.remote_participants.get_mut(&participant.user_id)
{
- remote_participant.projects = participant.projects;
remote_participant.peer_id = peer_id;
+ remote_participant.projects = participant.projects;
+ remote_participant.participant_index = participant_index;
if location != remote_participant.location {
remote_participant.location = location;
cx.emit(Event::ParticipantLocationChanged {
@@ -749,9 +769,7 @@ impl Room {
participant.user_id,
RemoteParticipant {
user: user.clone(),
- participant_index: ParticipantIndex(
- participant.participant_index,
- ),
+ participant_index,
peer_id,
projects: participant.projects,
location,
@@ -855,6 +873,7 @@ impl Room {
});
this.check_invariants();
+ this.room_update_completed_tx.try_send(Some(())).ok();
cx.notify();
});
}));
@@ -863,6 +882,17 @@ impl Room {
Ok(())
}
+ pub fn room_update_completed(&mut self) -> impl Future<Output = ()> {
+ 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,
@@ -2,19 +2,21 @@ mod channel_buffer;
mod channel_chat;
mod channel_store;
+use client::{Client, UserStore};
+use gpui::{AppContext, ModelHandle};
+use std::sync::Arc;
+
pub use channel_buffer::{ChannelBuffer, ChannelBufferEvent, ACKNOWLEDGE_DEBOUNCE_INTERVAL};
pub use channel_chat::{ChannelChat, ChannelChatEvent, ChannelMessage, ChannelMessageId};
pub use channel_store::{
Channel, ChannelData, ChannelEvent, ChannelId, ChannelMembership, ChannelPath, ChannelStore,
};
-use client::Client;
-use std::sync::Arc;
-
#[cfg(test)]
mod channel_store_tests;
-pub fn init(client: &Arc<Client>) {
+pub fn init(client: &Arc<Client>, user_store: ModelHandle<UserStore>, cx: &mut AppContext) {
+ channel_store::init(client, user_store, cx);
channel_buffer::init(client);
channel_chat::init(client);
}
@@ -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<UserStore> {
&self.user_store
}
@@ -2,8 +2,10 @@ mod channel_index;
use crate::{channel_buffer::ChannelBuffer, channel_chat::ChannelChat};
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::{
@@ -14,7 +16,11 @@ use serde_derive::{Deserialize, Serialize};
use std::{borrow::Cow, hash::Hash, mem, ops::Deref, sync::Arc, time::Duration};
use util::ResultExt;
-use self::channel_index::ChannelIndex;
+pub fn init(client: &Arc<Client>, user_store: ModelHandle<UserStore>, cx: &mut AppContext) {
+ let channel_store =
+ cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx));
+ cx.set_global(channel_store);
+}
pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30);
@@ -47,6 +53,26 @@ pub struct Channel {
pub unseen_message_id: Option<u64>,
}
+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]>);
@@ -71,6 +97,10 @@ enum OpenedModelHandle<E: Entity> {
}
impl ChannelStore {
+ pub fn global(cx: &AppContext) -> ModelHandle<Self> {
+ cx.global::<ModelHandle<Self>>().clone()
+ }
+
pub fn new(
client: Arc<Client>,
user_store: ModelHandle<UserStore>,
@@ -84,12 +114,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(())
@@ -793,7 +832,7 @@ impl ChannelStore {
})
}
- fn handle_disconnect(&mut self, cx: &mut ModelContext<Self>) {
+ fn handle_disconnect(&mut self, wait_for_reconnect: bool, cx: &mut ModelContext<Self>) {
self.channel_index.clear();
self.channel_invitations.clear();
self.channel_participants.clear();
@@ -804,7 +843,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() {
@@ -340,10 +340,10 @@ fn init_test(cx: &mut AppContext) -> ModelHandle<ChannelStore> {
cx.foreground().forbid_parking();
cx.set_global(SettingsStore::test(cx));
- crate::init(&client);
client::init(&client, cx);
+ crate::init(&client, user_store, cx);
- cx.add_model(|cx| ChannelStore::new(client, user_store, cx))
+ ChannelStore::global(cx)
}
fn update_channels(
@@ -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 {
@@ -70,7 +70,7 @@ pub const ZED_SECRET_CLIENT_TOKEN: &str = "618033988749894";
pub const INITIAL_RECONNECTION_DELAY: Duration = Duration::from_millis(100);
pub const CONNECTION_TIMEOUT: Duration = Duration::from_secs(5);
-actions!(client, [SignIn, SignOut]);
+actions!(client, [SignIn, SignOut, Reconnect]);
pub fn init_settings(cx: &mut AppContext) {
settings::register::<TelemetrySettings>(cx);
@@ -102,6 +102,17 @@ pub fn init(client: &Arc<Client>, cx: &mut AppContext) {
}
}
});
+ cx.add_global_action({
+ let client = client.clone();
+ move |_: &Reconnect, cx| {
+ if let Some(client) = client.upgrade() {
+ cx.spawn(|cx| async move {
+ client.reconnect(&cx);
+ })
+ .detach();
+ }
+ }
+ });
}
pub struct Client {
@@ -1212,6 +1223,11 @@ impl Client {
self.set_status(Status::SignedOut, cx);
}
+ pub fn reconnect(self: &Arc<Self>, cx: &AsyncAppContext) {
+ self.peer.teardown();
+ self.set_status(Status::ConnectionLost, cx);
+ }
+
fn connection_id(&self) -> Result<ConnectionId> {
if let Status::Connected { connection_id, .. } = *self.status().borrow() {
Ok(connection_id)
@@ -8,7 +8,6 @@ use sysinfo::{Pid, PidExt, ProcessExt, System, SystemExt};
use tempfile::NamedTempFile;
use util::http::HttpClient;
use util::{channel::ReleaseChannel, TryFutureExt};
-use uuid::Uuid;
pub struct Telemetry {
http_client: Arc<dyn HttpClient>,
@@ -20,7 +19,7 @@ pub struct Telemetry {
struct TelemetryState {
metrics_id: Option<Arc<str>>, // Per logged-in user
installation_id: Option<Arc<str>>, // Per app installation (different for dev, preview, and stable)
- session_id: String, // Per app launch
+ session_id: Option<Arc<str>>, // Per app launch
app_version: Option<Arc<str>>,
release_channel: Option<&'static str>,
os_name: &'static str,
@@ -43,7 +42,7 @@ lazy_static! {
struct ClickhouseEventRequestBody {
token: &'static str,
installation_id: Option<Arc<str>>,
- session_id: String,
+ session_id: Option<Arc<str>>,
is_staff: Option<bool>,
app_version: Option<Arc<str>>,
os_name: &'static str,
@@ -134,7 +133,7 @@ impl Telemetry {
release_channel,
installation_id: None,
metrics_id: None,
- session_id: Uuid::new_v4().to_string(),
+ session_id: None,
clickhouse_events_queue: Default::default(),
flush_clickhouse_events_task: Default::default(),
log_file: None,
@@ -149,9 +148,15 @@ impl Telemetry {
Some(self.state.lock().log_file.as_ref()?.path().to_path_buf())
}
- pub fn start(self: &Arc<Self>, installation_id: Option<String>, cx: &mut AppContext) {
+ pub fn start(
+ self: &Arc<Self>,
+ installation_id: Option<String>,
+ session_id: String,
+ cx: &mut AppContext,
+ ) {
let mut state = self.state.lock();
state.installation_id = installation_id.map(|id| id.into());
+ state.session_id = Some(session_id.into());
let has_clickhouse_events = !state.clickhouse_events_queue.is_empty();
drop(state);
@@ -283,23 +288,21 @@ impl Telemetry {
{
let state = this.state.lock();
+ let request_body = ClickhouseEventRequestBody {
+ token: ZED_SECRET_CLIENT_TOKEN,
+ installation_id: state.installation_id.clone(),
+ session_id: state.session_id.clone(),
+ is_staff: state.is_staff.clone(),
+ app_version: state.app_version.clone(),
+ os_name: state.os_name,
+ os_version: state.os_version.clone(),
+ architecture: state.architecture,
+
+ release_channel: state.release_channel,
+ events,
+ };
json_bytes.clear();
- serde_json::to_writer(
- &mut json_bytes,
- &ClickhouseEventRequestBody {
- token: ZED_SECRET_CLIENT_TOKEN,
- installation_id: state.installation_id.clone(),
- session_id: state.session_id.clone(),
- is_staff: state.is_staff.clone(),
- app_version: state.app_version.clone(),
- os_name: state.os_name,
- os_version: state.os_version.clone(),
- architecture: state.architecture,
-
- release_channel: state.release_channel,
- events,
- },
- )?;
+ serde_json::to_writer(&mut json_bytes, &request_body)?;
}
this.http_client
@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"]
default-run = "collab"
edition = "2021"
name = "collab"
-version = "0.23.2"
+version = "0.24.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"] }
@@ -37,8 +37,10 @@ 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,
@@ -0,0 +1 @@
+ALTER TABLE rooms ADD COLUMN enviroment TEXT;
@@ -0,0 +1 @@
+CREATE UNIQUE INDEX "index_rooms_on_channel_id" ON "rooms" ("channel_id");
@@ -19,21 +19,14 @@ impl Database {
.await
}
- pub async fn create_root_channel(
- &self,
- name: &str,
- live_kit_room: &str,
- creator_id: UserId,
- ) -> Result<ChannelId> {
- self.create_channel(name, None, live_kit_room, creator_id)
- .await
+ pub async fn create_root_channel(&self, name: &str, creator_id: UserId) -> Result<ChannelId> {
+ self.create_channel(name, None, creator_id).await
}
pub async fn create_channel(
&self,
name: &str,
parent: Option<ChannelId>,
- live_kit_room: &str,
creator_id: UserId,
) -> Result<ChannelId> {
let name = Self::sanitize_channel_name(name)?;
@@ -90,14 +83,6 @@ impl Database {
.insert(&*tx)
.await?;
- room::ActiveModel {
- channel_id: ActiveValue::Set(Some(channel.id)),
- live_kit_room: ActiveValue::Set(live_kit_room.to_string()),
- ..Default::default()
- }
- .insert(&*tx)
- .await?;
-
Ok(channel.id)
})
.await
@@ -797,18 +782,36 @@ impl Database {
.await
}
- pub async fn room_id_for_channel(&self, channel_id: ChannelId) -> Result<RoomId> {
+ pub async fn get_or_create_channel_room(
+ &self,
+ channel_id: ChannelId,
+ live_kit_room: &str,
+ enviroment: &str,
+ ) -> Result<RoomId> {
self.transaction(|tx| async move {
let tx = tx;
- let room = channel::Model {
- id: channel_id,
- ..Default::default()
- }
- .find_related(room::Entity)
- .one(&*tx)
- .await?
- .ok_or_else(|| anyhow!("invalid channel"))?;
- Ok(room.id)
+
+ let room = room::Entity::find()
+ .filter(room::Column::ChannelId.eq(channel_id))
+ .one(&*tx)
+ .await?;
+
+ let room_id = if let Some(room) = room {
+ 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(enviroment.to_string())),
+ ..Default::default()
+ })
+ .exec(&*tx)
+ .await?;
+
+ result.last_insert_id
+ };
+
+ Ok(room_id)
})
.await
}
@@ -89,7 +89,7 @@ impl Database {
let mut rows = channel_message::Entity::find()
.filter(condition)
- .order_by_asc(channel_message::Column::Id)
+ .order_by_desc(channel_message::Column::Id)
.limit(count as u64)
.stream(&*tx)
.await?;
@@ -110,6 +110,7 @@ impl Database {
});
}
drop(rows);
+ messages.reverse();
Ok(messages)
})
.await
@@ -107,10 +107,12 @@ impl Database {
user_id: UserId,
connection: ConnectionId,
live_kit_room: &str,
+ release_channel: &str,
) -> Result<proto::Room> {
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,20 +272,31 @@ impl Database {
room_id: RoomId,
user_id: UserId,
connection: ConnectionId,
+ enviroment: &str,
) -> Result<RoomGuard<JoinRoom>> {
self.room_transaction(room_id, |tx| async move {
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
- enum QueryChannelId {
+ enum QueryChannelIdAndEnviroment {
ChannelId,
+ Enviroment,
+ }
+
+ let (channel_id, release_channel): (Option<ChannelId>, Option<String>) =
+ 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 channel_id: Option<ChannelId> = 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 {
@@ -300,6 +313,7 @@ impl Database {
.into_values::<_, QueryParticipantIndices>()
.all(&*tx)
.await?;
+
let mut participant_index = 0;
while existing_participant_indices.contains(&participant_index) {
participant_index += 1;
@@ -818,10 +832,7 @@ 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
@@ -8,6 +8,7 @@ pub struct Model {
pub id: RoomId,
pub live_kit_room: String,
pub channel_id: Option<ChannelId>,
+ pub enviroment: Option<String>,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
@@ -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<Arc<Database>>,
pub connection: Option<sqlx::AnyConnection>,
@@ -54,7 +54,7 @@ async fn test_channel_buffers(db: &Arc<Database>) {
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)
.await
@@ -141,7 +141,7 @@ async fn test_channel_buffers(db: &Arc<Database>) {
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,7 +207,7 @@ 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();
@@ -5,7 +5,11 @@ use rpc::{
};
use crate::{
- db::{queries::channels::ChannelGraph, tests::graph, ChannelId, Database, NewUserParams},
+ db::{
+ queries::channels::ChannelGraph,
+ tests::{graph, TEST_RELEASE_CHANNEL},
+ ChannelId, Database, NewUserParams,
+ },
test_both_dbs,
};
use std::sync::Arc;
@@ -41,7 +45,7 @@ async fn test_channels(db: &Arc<Database>) {
.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());
@@ -54,16 +58,13 @@ async fn test_channels(db: &Arc<Database>) {
.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 +72,14 @@ async fn test_channels(db: &Arc<Database>) {
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();
@@ -198,15 +199,20 @@ async fn test_joining_channels(db: &Arc<Database>) {
.unwrap()
.user_id;
- let channel_1 = db
- .create_root_channel("channel_1", "1", user_1)
+ let channel_1 = db.create_root_channel("channel_1", user_1).await.unwrap();
+ let room_1 = db
+ .get_or_create_channel_room(channel_1, "1", TEST_RELEASE_CHANNEL)
.await
.unwrap();
- let room_1 = db.room_id_for_channel(channel_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 })
+ .join_room(
+ room_1,
+ user_1,
+ ConnectionId { owner_id, id: 1 },
+ TEST_RELEASE_CHANNEL,
+ )
.await
.unwrap();
assert_eq!(joined_room.room.participants.len(), 1);
@@ -214,7 +220,12 @@ async fn test_joining_channels(db: &Arc<Database>) {
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_1,
+ user_2,
+ ConnectionId { owner_id, id: 1 },
+ TEST_RELEASE_CHANNEL
+ )
.await
.is_err());
}
@@ -269,15 +280,9 @@ async fn test_channel_invites(db: &Arc<Database>) {
.unwrap()
.user_id;
- let channel_1_1 = db
- .create_root_channel("channel_1", "1", user_1)
- .await
- .unwrap();
+ let channel_1_1 = db.create_root_channel("channel_1", user_1).await.unwrap();
- let channel_1_2 = db
- .create_root_channel("channel_2", "2", user_1)
- .await
- .unwrap();
+ 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, false)
.await
@@ -339,7 +344,7 @@ async fn test_channel_invites(db: &Arc<Database>) {
.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();
@@ -401,7 +406,7 @@ async fn test_channel_renames(db: &Arc<Database>) {
.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
@@ -446,25 +451,22 @@ async fn test_db_channel_moving(db: &Arc<Database>) {
.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 +519,7 @@ async fn test_db_channel_moving(db: &Arc<Database>) {
// ========================================================================
// 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 +809,15 @@ async fn test_db_channel_moving_bugs(db: &Arc<Database>) {
.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();
@@ -479,7 +479,7 @@ async fn test_project_count(db: &Arc<Database>) {
.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<Database>) {
)
.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<Database>) {
+ 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<Background> {
Deterministic::new(0).build_background()
}
@@ -1,10 +1,72 @@
use crate::{
- db::{Database, NewUserParams},
+ db::{Database, MessageId, NewUserParams},
test_both_dbs,
};
use std::sync::Arc;
use time::OffsetDateTime;
+test_both_dbs!(
+ test_channel_message_retrieval,
+ test_channel_message_retrieval_postgres,
+ test_channel_message_retrieval_sqlite
+);
+
+async fn test_channel_message_retrieval(db: &Arc<Database>) {
+ let user = db
+ .create_user(
+ "user@example.com",
+ false,
+ NewUserParams {
+ github_login: "user".into(),
+ github_user_id: 1,
+ invite_count: 0,
+ },
+ )
+ .await
+ .unwrap()
+ .user_id;
+ 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)
+ .await
+ .unwrap();
+
+ let mut all_messages = Vec::new();
+ for i in 0..10 {
+ all_messages.push(
+ db.create_channel_message(channel, user, &i.to_string(), OffsetDateTime::now_utc(), i)
+ .await
+ .unwrap()
+ .0
+ .to_proto(),
+ );
+ }
+
+ let messages = db
+ .get_channel_messages(channel, user, 3, None)
+ .await
+ .unwrap()
+ .into_iter()
+ .map(|message| message.id)
+ .collect::<Vec<_>>();
+ assert_eq!(messages, &all_messages[7..10]);
+
+ let messages = db
+ .get_channel_messages(
+ channel,
+ user,
+ 4,
+ Some(MessageId::from_proto(all_messages[6])),
+ )
+ .await
+ .unwrap()
+ .into_iter()
+ .map(|message| message.id)
+ .collect::<Vec<_>>();
+ assert_eq!(messages, &all_messages[2..6]);
+}
+
test_both_dbs!(
test_channel_message_nonces,
test_channel_message_nonces_postgres,
@@ -25,10 +87,7 @@ async fn test_channel_message_nonces(db: &Arc<Database>) {
.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;
@@ -92,15 +151,9 @@ async fn test_channel_message_new_notification(db: &Arc<Database>) {
.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)
.await
@@ -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::<proto::OpenBufferByPath>)
.add_request_handler(forward_project_request::<proto::GetCompletions>)
.add_request_handler(forward_project_request::<proto::ApplyCompletionAdditionalEdits>)
+ .add_request_handler(forward_project_request::<proto::ResolveCompletionDocumentation>)
.add_request_handler(forward_project_request::<proto::GetCodeActions>)
.add_request_handler(forward_project_request::<proto::ApplyCodeAction>)
.add_request_handler(forward_project_request::<proto::PrepareRename>)
@@ -937,11 +939,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 +954,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 {
@@ -979,7 +981,12 @@ async fn join_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()
@@ -2195,15 +2202,10 @@ 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 {
@@ -2608,15 +2610,23 @@ async fn join_channel(
session: Session,
) -> Result<()> {
let channel_id = ChannelId::from_proto(request.channel_id);
+ let live_kit_room = format!("channel-{}", nanoid::nanoid!(30));
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 room_id = db
+ .get_or_create_channel_room(channel_id, &live_kit_room, &*RELEASE_CHANNEL_NAME)
+ .await?;
let joined_room = db
- .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?;
let live_kit_connection_info = session.live_kit_client.as_ref().and_then(|live_kit| {
@@ -380,6 +380,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(
@@ -184,20 +184,12 @@ async fn test_basic_following(
// All clients see that clients B and C are following client A.
cx_c.foreground().run_until_parked();
- for (name, active_call, cx) in [
- ("A", &active_call_a, &cx_a),
- ("B", &active_call_b, &cx_b),
- ("C", &active_call_c, &cx_c),
- ("D", &active_call_d, &cx_d),
- ] {
- active_call.read_with(*cx, |call, cx| {
- let room = call.room().unwrap().read(cx);
- assert_eq!(
- room.followers_for(peer_id_a, project_id),
- &[peer_id_b, peer_id_c],
- "checking followers for A as {name}"
- );
- });
+ for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
+ assert_eq!(
+ followers_by_leader(project_id, cx),
+ &[(peer_id_a, vec![peer_id_b, peer_id_c])],
+ "followers seen by {name}"
+ );
}
// Client C unfollows client A.
@@ -207,46 +199,39 @@ async fn test_basic_following(
// All clients see that clients B is following client A.
cx_c.foreground().run_until_parked();
- for (name, active_call, cx) in [
- ("A", &active_call_a, &cx_a),
- ("B", &active_call_b, &cx_b),
- ("C", &active_call_c, &cx_c),
- ("D", &active_call_d, &cx_d),
- ] {
- active_call.read_with(*cx, |call, cx| {
- let room = call.room().unwrap().read(cx);
- assert_eq!(
- room.followers_for(peer_id_a, project_id),
- &[peer_id_b],
- "checking followers for A as {name}"
- );
- });
+ for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
+ assert_eq!(
+ followers_by_leader(project_id, cx),
+ &[(peer_id_a, vec![peer_id_b])],
+ "followers seen by {name}"
+ );
}
// Client C re-follows client A.
- workspace_c.update(cx_c, |workspace, cx| {
- workspace.follow(peer_id_a, cx);
- });
+ workspace_c
+ .update(cx_c, |workspace, cx| {
+ workspace.follow(peer_id_a, cx).unwrap()
+ })
+ .await
+ .unwrap();
// All clients see that clients B and C are following client A.
cx_c.foreground().run_until_parked();
- for (name, active_call, cx) in [
- ("A", &active_call_a, &cx_a),
- ("B", &active_call_b, &cx_b),
- ("C", &active_call_c, &cx_c),
- ("D", &active_call_d, &cx_d),
- ] {
- active_call.read_with(*cx, |call, cx| {
- let room = call.room().unwrap().read(cx);
- assert_eq!(
- room.followers_for(peer_id_a, project_id),
- &[peer_id_b, peer_id_c],
- "checking followers for A as {name}"
- );
- });
+ for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
+ assert_eq!(
+ followers_by_leader(project_id, cx),
+ &[(peer_id_a, vec![peer_id_b, peer_id_c])],
+ "followers seen by {name}"
+ );
}
- // Client D follows client C.
+ // Client D follows client B, then switches to following client C.
+ workspace_d
+ .update(cx_d, |workspace, cx| {
+ workspace.follow(peer_id_b, cx).unwrap()
+ })
+ .await
+ .unwrap();
workspace_d
.update(cx_d, |workspace, cx| {
workspace.follow(peer_id_c, cx).unwrap()
@@ -256,20 +241,15 @@ async fn test_basic_following(
// All clients see that D is following C
cx_d.foreground().run_until_parked();
- for (name, active_call, cx) in [
- ("A", &active_call_a, &cx_a),
- ("B", &active_call_b, &cx_b),
- ("C", &active_call_c, &cx_c),
- ("D", &active_call_d, &cx_d),
- ] {
- active_call.read_with(*cx, |call, cx| {
- let room = call.room().unwrap().read(cx);
- assert_eq!(
- room.followers_for(peer_id_c, project_id),
- &[peer_id_d],
- "checking followers for C as {name}"
- );
- });
+ for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
+ assert_eq!(
+ followers_by_leader(project_id, cx),
+ &[
+ (peer_id_a, vec![peer_id_b, peer_id_c]),
+ (peer_id_c, vec![peer_id_d])
+ ],
+ "followers seen by {name}"
+ );
}
// Client C closes the project.
@@ -278,32 +258,12 @@ async fn test_basic_following(
// Clients A and B see that client B is following A, and client C is not present in the followers.
cx_c.foreground().run_until_parked();
- for (name, active_call, cx) in [("A", &active_call_a, &cx_a), ("B", &active_call_b, &cx_b)] {
- active_call.read_with(*cx, |call, cx| {
- let room = call.room().unwrap().read(cx);
- assert_eq!(
- room.followers_for(peer_id_a, project_id),
- &[peer_id_b],
- "checking followers for A as {name}"
- );
- });
- }
-
- // All clients see that no-one is following C
- for (name, active_call, cx) in [
- ("A", &active_call_a, &cx_a),
- ("B", &active_call_b, &cx_b),
- ("C", &active_call_c, &cx_c),
- ("D", &active_call_d, &cx_d),
- ] {
- active_call.read_with(*cx, |call, cx| {
- let room = call.room().unwrap().read(cx);
- assert_eq!(
- room.followers_for(peer_id_c, project_id),
- &[],
- "checking followers for C as {name}"
- );
- });
+ for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
+ assert_eq!(
+ followers_by_leader(project_id, cx),
+ &[(peer_id_a, vec![peer_id_b]),],
+ "followers seen by {name}"
+ );
}
// When client A activates a different editor, client B does so as well.
@@ -1667,6 +1627,30 @@ struct PaneSummary {
items: Vec<(bool, String)>,
}
+fn followers_by_leader(project_id: u64, cx: &TestAppContext) -> Vec<(PeerId, Vec<PeerId>)> {
+ cx.read(|cx| {
+ let active_call = ActiveCall::global(cx).read(cx);
+ let peer_id = active_call.client().peer_id();
+ let room = active_call.room().unwrap().read(cx);
+ let mut result = room
+ .remote_participants()
+ .values()
+ .map(|participant| participant.peer_id)
+ .chain(peer_id)
+ .filter_map(|peer_id| {
+ let followers = room.followers_for(peer_id, project_id);
+ if followers.is_empty() {
+ None
+ } else {
+ Some((peer_id, followers.to_vec()))
+ }
+ })
+ .collect::<Vec<_>>();
+ result.sort_by_key(|e| e.0);
+ result
+ })
+}
+
fn pane_summaries(workspace: &ViewHandle<Workspace>, cx: &mut TestAppContext) -> Vec<PaneSummary> {
workspace.read_with(cx, |workspace, cx| {
let active_pane = workspace.active_pane();
@@ -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<Deterministic>,
+ 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::<AllLanguageSettings>(cx, |file| {
+ file.defaults.formatter = Some(Formatter::Auto);
+ });
+ });
+ });
+ cx_b.update(|cx| {
+ cx.update_global(|store: &mut SettingsStore, cx| {
+ store.update_user_settings::<AllLanguageSettings>(cx, |file| {
+ file.defaults.formatter = Some(Formatter::LanguageServer);
+ });
+ });
+ });
+ let fake_language_server = fake_language_servers.next().await.unwrap();
+ fake_language_server.handle_request::<lsp::request::Formatting, _, _>(|_, _| 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<Deterministic>,
@@ -46,12 +46,7 @@ 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..] {
@@ -15,6 +15,7 @@ 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;
@@ -44,6 +45,7 @@ pub struct TestServer {
pub struct TestClient {
pub username: String,
pub app_state: Arc<workspace::AppState>,
+ channel_store: ModelHandle<ChannelStore>,
state: RefCell<TestClientState>,
}
@@ -206,20 +208,18 @@ impl TestServer {
let fs = FakeFs::new(cx.background());
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx));
let workspace_store = cx.add_model(|cx| WorkspaceStore::new(client.clone(), cx));
- let channel_store =
- cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx));
let mut language_registry = LanguageRegistry::test();
language_registry.set_executor(cx.background());
let app_state = Arc::new(workspace::AppState {
client: client.clone(),
user_store: user_store.clone(),
workspace_store,
- channel_store: channel_store.clone(),
languages: Arc::new(language_registry),
fs: fs.clone(),
build_window_options: |_, _, _| Default::default(),
initialize_workspace: |_, _, _, _| Task::ready(Ok(())),
background_actions: || &[],
+ node_runtime: FakeNodeRuntime::new(),
});
cx.update(|cx| {
@@ -231,7 +231,7 @@ impl TestServer {
workspace::init(app_state.clone(), cx);
audio::init((), cx);
call::init(client.clone(), user_store.clone(), cx);
- channel::init(&client);
+ channel::init(&client, user_store, cx);
});
client
@@ -242,6 +242,7 @@ impl TestServer {
let client = TestClient {
app_state,
username: name.to_string(),
+ channel_store: cx.read(ChannelStore::global).clone(),
state: Default::default(),
};
client.wait_for_current_user(cx).await;
@@ -310,10 +311,9 @@ impl TestServer {
admin: (&TestClient, &mut TestAppContext),
members: &mut [(&TestClient, &mut TestAppContext)],
) -> u64 {
- let (admin_client, admin_cx) = admin;
- let channel_id = admin_client
- .app_state
- .channel_store
+ let (_, admin_cx) = admin;
+ let channel_id = admin_cx
+ .read(ChannelStore::global)
.update(admin_cx, |channel_store, cx| {
channel_store.create_channel(channel, parent, cx)
})
@@ -321,9 +321,8 @@ impl TestServer {
.unwrap();
for (member_client, member_cx) in members {
- admin_client
- .app_state
- .channel_store
+ admin_cx
+ .read(ChannelStore::global)
.update(admin_cx, |channel_store, cx| {
channel_store.invite_member(
channel_id,
@@ -337,9 +336,8 @@ impl TestServer {
admin_cx.foreground().run_until_parked();
- member_client
- .app_state
- .channel_store
+ member_cx
+ .read(ChannelStore::global)
.update(*member_cx, |channels, _| {
channels.respond_to_channel_invite(channel_id, true)
})
@@ -447,7 +445,7 @@ impl TestClient {
}
pub fn channel_store(&self) -> &ModelHandle<ChannelStore> {
- &self.app_state.channel_store
+ &self.channel_store
}
pub fn user_store(&self) -> &ModelHandle<UserStore> {
@@ -571,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(),
@@ -614,8 +613,8 @@ impl TestClient {
) {
let (other_client, other_cx) = user;
- self.app_state
- .channel_store
+ cx_self
+ .read(ChannelStore::global)
.update(cx_self, |channel_store, cx| {
channel_store.invite_member(channel, other_client.user_id().unwrap(), true, cx)
})
@@ -624,11 +623,10 @@ impl TestClient {
cx_self.foreground().run_until_parked();
- other_client
- .app_state
- .channel_store
- .update(other_cx, |channels, _| {
- channels.respond_to_channel_invite(channel, true)
+ other_cx
+ .read(ChannelStore::global)
+ .update(other_cx, |channel_store, _| {
+ channel_store.respond_to_channel_invite(channel, true)
})
.await
.unwrap();
@@ -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]);
@@ -73,7 +73,7 @@ impl ChannelView {
) -> Task<Result<ViewHandle<Self>>> {
let workspace = workspace.read(cx);
let project = workspace.project().to_owned();
- let channel_store = workspace.app_state().channel_store.clone();
+ let channel_store = ChannelStore::global(cx);
let markdown = workspace
.app_state()
.languages
@@ -93,15 +93,36 @@ impl ChannelView {
}
pane.update(&mut cx, |pane, cx| {
- pane.items_of_type::<Self>()
- .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::<Self>()
+ .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<proto::view::Variant> {
- 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)
{
@@ -81,7 +81,7 @@ impl ChatPanel {
pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> ViewHandle<Self> {
let fs = workspace.app_state().fs.clone();
let client = workspace.app_state().client.clone();
- let channel_store = workspace.app_state().channel_store.clone();
+ let channel_store = ChannelStore::global(cx);
let languages = workspace.app_state().languages.clone();
let input_editor = cx.add_view(|cx| {
@@ -34,8 +34,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 +100,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 +162,7 @@ impl_actions!(
OpenChannelNotes,
JoinChannelCall,
JoinChannelChat,
+ CopyChannelLink,
LinkChannel,
StartMoveChannelFor,
StartLinkChannelFor,
@@ -205,6 +211,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<CollabPanel>| {
@@ -648,7 +655,7 @@ impl CollabPanel {
channel_editing_state: None,
selection: None,
user_store: workspace.user_store().clone(),
- channel_store: workspace.app_state().channel_store.clone(),
+ channel_store: ChannelStore::global(cx),
project: workspace.project().clone(),
subscriptions: Vec::default(),
match_candidates: Vec::default(),
@@ -2568,6 +2575,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();
@@ -3187,49 +3201,19 @@ impl CollabPanel {
}
fn join_channel(&self, channel_id: u64, cx: &mut ViewContext<Self>) {
- 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::<Workspace>() 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<Self>) {
@@ -3246,6 +3230,15 @@ impl CollabPanel {
});
}
}
+
+ fn copy_channel_link(&mut self, action: &CopyChannelLink, cx: &mut ViewContext<Self>) {
+ 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(
@@ -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::<ConnectionStatusButton, _>(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::<ConnectionStatusButton, _>(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,
}
}
@@ -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();
@@ -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
@@ -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,
@@ -77,9 +77,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,
};
@@ -118,6 +119,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<Tag: 'static>(
+ parsed: &language::ParsedMarkdown,
+ editor_style: &EditorStyle,
+ workspace: Option<WeakViewHandle<Workspace>>,
+ cx: &mut ViewContext<Editor>,
+) -> 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::<Vec<_>>(),
+ )
+ .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::<Editor, _>(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)]
@@ -594,7 +656,7 @@ pub struct Editor {
background_highlights: BTreeMap<TypeId, BackgroundHighlight>,
inlay_background_highlights: TreeMap<Option<TypeId>, InlayBackgroundHighlight>,
nav_history: Option<ItemNavHistory>,
- context_menu: Option<ContextMenu>,
+ context_menu: RwLock<Option<ContextMenu>>,
mouse_context_menu: ViewHandle<context_menu::ContextMenu>,
completion_tasks: Vec<(CompletionId, Task<Option<()>>)>,
next_completion_id: CompletionId,
@@ -787,10 +849,14 @@ enum ContextMenu {
}
impl ContextMenu {
- fn select_first(&mut self, cx: &mut ViewContext<Editor>) -> bool {
+ fn select_first(
+ &mut self,
+ project: Option<&ModelHandle<Project>>,
+ cx: &mut ViewContext<Editor>,
+ ) -> 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
@@ -799,10 +865,14 @@ impl ContextMenu {
}
}
- fn select_prev(&mut self, cx: &mut ViewContext<Editor>) -> bool {
+ fn select_prev(
+ &mut self,
+ project: Option<&ModelHandle<Project>>,
+ cx: &mut ViewContext<Editor>,
+ ) -> 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
@@ -811,10 +881,14 @@ impl ContextMenu {
}
}
- fn select_next(&mut self, cx: &mut ViewContext<Editor>) -> bool {
+ fn select_next(
+ &mut self,
+ project: Option<&ModelHandle<Project>>,
+ cx: &mut ViewContext<Editor>,
+ ) -> 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
@@ -823,10 +897,14 @@ impl ContextMenu {
}
}
- fn select_last(&mut self, cx: &mut ViewContext<Editor>) -> bool {
+ fn select_last(
+ &mut self,
+ project: Option<&ModelHandle<Project>>,
+ cx: &mut ViewContext<Editor>,
+ ) -> 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
@@ -846,99 +924,350 @@ impl ContextMenu {
&self,
cursor_position: DisplayPoint,
style: EditorStyle,
+ workspace: Option<WeakViewHandle<Workspace>>,
cx: &mut ViewContext<Editor>,
) -> (DisplayPoint, AnyElement<Editor>) {
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<Buffer>,
- project: Option<ModelHandle<Project>>,
- completions: Arc<[Completion]>,
- match_candidates: Vec<StringMatchCandidate>,
+ completions: Arc<RwLock<Box<[Completion]>>>,
+ match_candidates: Arc<[StringMatchCandidate]>,
matches: Arc<[StringMatch]>,
selected_item: usize,
list: UniformListState,
}
impl CompletionsMenu {
- fn select_first(&mut self, cx: &mut ViewContext<Editor>) {
+ fn select_first(
+ &mut self,
+ project: Option<&ModelHandle<Project>>,
+ cx: &mut ViewContext<Editor>,
+ ) {
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<Editor>) {
+ fn select_prev(
+ &mut self,
+ project: Option<&ModelHandle<Project>>,
+ cx: &mut ViewContext<Editor>,
+ ) {
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<Editor>) {
+ fn select_next(
+ &mut self,
+ project: Option<&ModelHandle<Project>>,
+ cx: &mut ViewContext<Editor>,
+ ) {
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<Editor>) {
+ fn select_last(
+ &mut self,
+ project: Option<&ModelHandle<Project>>,
+ cx: &mut ViewContext<Editor>,
+ ) {
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<ModelHandle<Project>>,
+ cx: &mut ViewContext<Editor>,
+ ) {
+ let settings = settings::get::<EditorSettings>(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<Project>>,
+ cx: &mut ViewContext<Editor>,
+ ) {
+ let settings = settings::get::<EditorSettings>(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<RwLock<Box<[Completion]>>>,
+ completion_index: usize,
+ completion: lsp::CompletionItem,
+ client: Arc<Client>,
+ language_registry: Arc<LanguageRegistry>,
+ ) {
+ 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<lsp::LanguageServer>,
+ completions: Arc<RwLock<Box<[Completion]>>>,
+ completion_index: usize,
+ completion: lsp::CompletionItem,
+ language_registry: Arc<LanguageRegistry>,
+ ) {
+ 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::<lsp::request::ResolveCompletionItem>(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<Editor>) -> AnyElement<Editor> {
+ fn render(
+ &self,
+ style: EditorStyle,
+ workspace: Option<WeakViewHandle<Workspace>>,
+ cx: &mut ViewContext<Editor>,
+ ) -> AnyElement<Editor> {
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::<Vec<_>>()
- });
- 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::<EditorSettings>(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
@@ -948,16 +1277,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::<CompletionTag, _>(
mat.candidate_id,
@@ -986,22 +1323,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, _, _| {
@@ -1021,7 +1354,7 @@ impl CompletionsMenu {
.with_style(
style
.autocomplete
- .server_name_container,
+ .inline_docs_container,
)
.into_any(),
)
@@ -1060,15 +1393,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::<MultiLineDocumentation>(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::<MultiLineDocumentation>(0, None, cx)
+ .with_child(render_parsed_markdown::<MultiLineDocumentation>(
+ 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<executor::Background>) {
@@ -1095,13 +1472,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())
@@ -1111,23 +1488,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;
}
}
@@ -1563,7 +1944,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(),
@@ -1858,10 +2239,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
}
};
@@ -1873,13 +2256,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);
@@ -2912,6 +3321,7 @@ impl Editor {
false
});
}
+
fn completion_query(buffer: &MultiBufferSnapshot, position: impl ToOffset) -> Option<String> {
let offset = position.to_offset(buffer);
let (word_range, kind) = buffer.surrounding_word(offset);
@@ -3150,7 +3560,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() {
@@ -3169,8 +3578,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(),
@@ -3179,6 +3587,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 {
@@ -3188,23 +3599,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);
}
@@ -3235,7 +3653,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;
@@ -3348,14 +3767,13 @@ impl Editor {
}
pub fn toggle_code_actions(&mut self, action: &ToggleCodeActions, cx: &mut ViewContext<Self>) {
- 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();
@@ -3368,16 +3786,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,
- );
+ }));
}
}
})?;
@@ -3740,7 +4158,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();
+ }
}
}
@@ -3752,7 +4173,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();
+ }
}
}
@@ -3841,7 +4265,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
{
@@ -3975,6 +4399,7 @@ impl Editor {
pub fn context_menu_visible(&self) -> bool {
self.context_menu
+ .read()
.as_ref()
.map_or(false, |menu| menu.visible())
}
@@ -3985,24 +4410,20 @@ impl Editor {
style: EditorStyle,
cx: &mut ViewContext<Editor>,
) -> Option<(DisplayPoint, AnyElement<Editor>)> {
- self.context_menu
- .as_ref()
- .map(|menu| menu.render(cursor_position, style, cx))
- }
-
- fn show_context_menu(&mut self, menu: ContextMenu, cx: &mut ViewContext<Self>) {
- 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<Self>) -> Option<ContextMenu> {
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);
}
@@ -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<bool>,
pub hover_popover_enabled: Option<bool>,
pub show_completions_on_input: Option<bool>,
+ pub show_completion_documentation: Option<bool>,
pub use_on_type_format: Option<bool>,
pub scrollbar: Option<ScrollbarContent>,
pub relative_line_numbers: Option<bool>,
@@ -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};
@@ -1333,7 +1333,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
@@ -1344,54 +1344,41 @@ 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
+ &r#"one
two
-
+ ˇ
three
four
five
- sixˇ"#
+ six"#
.unindent(),
);
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
- four
- five
- ˇ
six"#
.unindent(),
);
@@ -5089,7 +5076,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 {
@@ -5105,6 +5094,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;
@@ -5113,7 +5108,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
@@ -5231,7 +5229,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 {
@@ -5430,9 +5430,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
@@ -5494,12 +5494,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);
});
@@ -7788,7 +7788,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::<Vec<_>>(),
&["bg-red", "bg-blue", "bg-yellow"]
@@ -7801,7 +7801,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::<Vec<_>>(),
&["bg-blue", "bg-yellow"]
@@ -7817,7 +7817,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::<Vec<_>>(),
&["bg-yellow"]
@@ -7828,6 +7828,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<DisplayPoint> {
let point = DisplayPoint::new(row as u32, column as u32);
point..point
@@ -2428,7 +2428,7 @@ impl Element<Editor> for EditorElement {
}
let active = matches!(
- editor.context_menu,
+ editor.context_menu.read().as_ref(),
Some(crate::ContextMenu::CodeActions(_))
);
@@ -2439,9 +2439,13 @@ impl Element<Editor> 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(
@@ -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<LanguageRegistry>,
- language: Option<&Arc<Language>>,
-) -> RichText {
- let mut data = RichText {
- text: Default::default(),
- highlights: Default::default(),
- region_ranges: Default::default(),
- regions: Default::default(),
- };
+ language: Option<Arc<Language>>,
+) -> 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<u32>,
+ workspace: Option<WeakViewHandle<Workspace>>,
cx: &mut ViewContext<Editor>,
) -> Option<(DisplayPoint, Vec<AnyElement<Editor>>)> {
// 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<Project>,
symbol_range: RangeInEditor,
pub blocks: Vec<HoverBlock>,
- language: Option<Arc<Language>>,
- rendered_content: Option<RichText>,
+ parsed_content: ParsedMarkdown,
}
impl InfoPopover {
pub fn render(
&mut self,
style: &EditorStyle,
+ workspace: Option<WeakViewHandle<Workspace>>,
cx: &mut ViewContext<Editor>,
) -> AnyElement<Editor> {
- 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::<InfoPopover, _>(0, cx, move |_, cx| {
- let code_span_background_color = style.document_highlight_read_background;
+ MouseEventHandler::new::<InfoPopover, _>(0, cx, |_, cx| {
Flex::column()
- .scrollable::<HoverBlock>(1, None, cx)
- .with_child(rendered_content.element(
- style.syntax.clone(),
- style.text.clone(),
- code_span_background_color,
+ .scrollable::<HoverBlock>(0, None, cx)
+ .with_child(crate::render_parsed_markdown::<HoverBlock>(
+ &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 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"
);
@@ -234,7 +234,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;
@@ -261,7 +261,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;
@@ -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<str> = "".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<u64, Vec<BufferEdit>>,
+ autoindent_mode: Option<AutoindentMode>,
+ edited_excerpt_ids: Vec<ExcerptId>,
+ cx: &mut ModelContext<MultiBuffer>,
+ ) {
+ 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<str> = "".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<Self>) -> Option<TransactionId> {
@@ -107,13 +107,23 @@ fn matching_history_item_paths(
) -> HashMap<Arc<Path>, PathMatch> {
let history_items_by_worktrees = history_items
.iter()
- .map(|found_path| {
- let path = &found_path.project.path;
+ .filter_map(|found_path| {
let candidate = PathMatchCandidate {
- path,
- char_bag: CharBag::from_iter(path.to_string_lossy().to_lowercase().chars()),
+ path: &found_path.project.path,
+ // Only match history items names, otherwise their paths may match too many queries, producing false positives.
+ // E.g. `foo` would match both `something/foo/bar.rs` and `something/foo/foo.rs` and if the former is a history item,
+ // it would be shown first always, despite the latter being a better match.
+ char_bag: CharBag::from_iter(
+ found_path
+ .project
+ .path
+ .file_name()?
+ .to_string_lossy()
+ .to_lowercase()
+ .chars(),
+ ),
};
- (found_path.project.worktree_id, candidate)
+ Some((found_path.project.worktree_id, candidate))
})
.fold(
HashMap::default(),
@@ -212,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();
@@ -236,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,
@@ -505,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))
@@ -1803,6 +1822,202 @@ mod tests {
});
}
+ #[gpui::test]
+ async fn test_history_items_vs_very_good_external_match(
+ deterministic: Arc<gpui::executor::Deterministic>,
+ cx: &mut gpui::TestAppContext,
+ ) {
+ let app_state = init_test(cx);
+
+ app_state
+ .fs
+ .as_fake()
+ .insert_tree(
+ "/src",
+ json!({
+ "collab_ui": {
+ "first.rs": "// First Rust file",
+ "second.rs": "// Second Rust file",
+ "third.rs": "// Third Rust file",
+ "collab_ui.rs": "// Fourth 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(
+ "sec",
+ 1,
+ "second.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(
+ "sec",
+ 1,
+ "second.rs",
+ window.into(),
+ &workspace,
+ &deterministic,
+ cx,
+ )
+ .await;
+
+ cx.dispatch_action(window.into(), Toggle);
+ let query = "collab_ui";
+ let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().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();
+ assert!(
+ delegate.matches.history.is_empty(),
+ "History items should not math query {query}, they should be matched by name only"
+ );
+
+ let search_entries = delegate
+ .matches
+ .search
+ .iter()
+ .map(|path_match| path_match.path.to_path_buf())
+ .collect::<Vec<_>>();
+ assert_eq!(
+ search_entries,
+ vec![
+ PathBuf::from("collab_ui/collab_ui.rs"),
+ PathBuf::from("collab_ui/third.rs"),
+ PathBuf::from("collab_ui/first.rs"),
+ PathBuf::from("collab_ui/second.rs"),
+ ],
+ "Despite all search results having the same directory name, the most matching one should be on top"
+ );
+ });
+ }
+
+ #[gpui::test]
+ async fn test_nonexistent_history_items_not_shown(
+ deterministic: Arc<gpui::executor::Deterministic>,
+ 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::<FileFinder>().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::<Vec<_>>();
+ 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,
@@ -13,7 +13,6 @@ rope = { path = "../rope" }
text = { path = "../text" }
util = { path = "../util" }
sum_tree = { path = "../sum_tree" }
-rpc = { path = "../rpc" }
anyhow.workspace = true
async-trait.workspace = true
@@ -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,
@@ -2,7 +2,6 @@ use anyhow::Result;
use collections::HashMap;
use git2::{BranchType, StatusShow};
use parking_lot::Mutex;
-use rpc::proto;
use serde_derive::{Deserialize, Serialize};
use std::{
cmp::Ordering,
@@ -23,6 +22,7 @@ pub struct Branch {
/// Timestamp of most recent commit, normalized to Unix Epoch format.
pub unix_timestamp: Option<i64>,
}
+
#[async_trait::async_trait]
pub trait GitRepository: Send {
fn reload_index(&self);
@@ -358,24 +358,6 @@ impl GitFileStatus {
}
}
}
-
- pub fn from_proto(git_status: Option<i32>) -> Option<GitFileStatus> {
- git_status.and_then(|status| {
- proto::GitStatus::from_i32(status).map(|status| match status {
- proto::GitStatus::Added => GitFileStatus::Added,
- proto::GitStatus::Modified => GitFileStatus::Modified,
- proto::GitStatus::Conflict => GitFileStatus::Conflict,
- })
- })
- }
-
- pub fn to_proto(self) -> i32 {
- match self {
- GitFileStatus::Added => proto::GitStatus::Added as i32,
- GitFileStatus::Modified => proto::GitStatus::Modified as i32,
- GitFileStatus::Conflict => proto::GitStatus::Conflict as i32,
- }
- }
}
#[derive(Clone, Debug, Ord, Hash, PartialOrd, Eq, PartialEq)]
@@ -441,7 +441,7 @@ mod tests {
score,
worktree_id: 0,
positions: Vec::new(),
- path: candidate.path.clone(),
+ path: Arc::from(candidate.path),
path_prefix: "".into(),
distance_to_relative_ancestor: usize::MAX,
},
@@ -14,7 +14,7 @@ use crate::{
#[derive(Clone, Debug)]
pub struct PathMatchCandidate<'a> {
- pub path: &'a Arc<Path>,
+ pub path: &'a Path,
pub char_bag: CharBag,
}
@@ -120,7 +120,7 @@ pub fn match_fixed_path_set(
score,
worktree_id,
positions: Vec::new(),
- path: candidate.path.clone(),
+ path: Arc::from(candidate.path),
path_prefix: Arc::from(""),
distance_to_relative_ancestor: usize::MAX,
},
@@ -195,7 +195,7 @@ pub async fn match_path_sets<'a, Set: PathMatchCandidateSet<'a>>(
score,
worktree_id,
positions: Vec::new(),
- path: candidate.path.clone(),
+ path: Arc::from(candidate.path),
path_prefix: candidate_set.prefix(),
distance_to_relative_ancestor: relative_to.as_ref().map_or(
usize::MAX,
@@ -71,7 +71,7 @@ pub struct Window {
pub(crate) hovered_region_ids: Vec<MouseRegionId>,
pub(crate) clicked_region_ids: Vec<MouseRegionId>,
pub(crate) clicked_region: Option<(MouseRegionId, MouseButton)>,
- text_layout_cache: TextLayoutCache,
+ text_layout_cache: Arc<TextLayoutCache>,
refreshing: bool,
}
@@ -107,7 +107,7 @@ impl Window {
cursor_regions: Default::default(),
mouse_regions: Default::default(),
event_handlers: Default::default(),
- text_layout_cache: TextLayoutCache::new(cx.font_system.clone()),
+ text_layout_cache: Arc::new(TextLayoutCache::new(cx.font_system.clone())),
last_mouse_moved_event: None,
last_mouse_position: Vector2F::zero(),
pressed_buttons: Default::default(),
@@ -303,7 +303,7 @@ impl<'a> WindowContext<'a> {
self.window.refreshing
}
- pub fn text_layout_cache(&self) -> &TextLayoutCache {
+ pub fn text_layout_cache(&self) -> &Arc<TextLayoutCache> {
&self.window.text_layout_cache
}
@@ -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<Option<usize>>,
scroll_position: Cell<f32>,
+ type_tag: TypeTag,
}
pub struct Flex<V> {
@@ -66,8 +67,14 @@ impl<V: 'static> Flex<V> {
where
Tag: 'static,
{
- let scroll_state = cx.default_element_state::<Tag, Rc<ScrollState>>(element_id);
- scroll_state.read(cx).scroll_to.set(scroll_to);
+ let scroll_state = cx.element_state::<Tag, Rc<ScrollState>>(
+ element_id,
+ Rc::new(ScrollState {
+ scroll_to: Cell::new(scroll_to),
+ scroll_position: Default::default(),
+ type_tag: TypeTag::new::<Tag>(),
+ }),
+ );
self.scroll_state = Some((scroll_state, cx.handle().id()));
self
}
@@ -276,38 +283,44 @@ impl<V: 'static> Element<V> for Flex<V> {
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::<Self>(*id, 0, bounds)
- .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()
- }
+ 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()
}
- Axis::Vertical => scroll_delta.y(),
- };
- if !e.delta.precise() {
- delta *= 20.;
}
+ Axis::Vertical => scroll_delta.y(),
+ };
+ if !e.delta.precise() {
+ delta *= 20.;
+ }
- scroll_state
- .scroll_position
- .set(scroll_state.scroll_position.get() - delta);
+ scroll_state
+ .scroll_position
+ .set(scroll_state.scroll_position.get() - delta);
- cx.notify();
- } else {
- cx.propagate_event();
- }
+ cx.notify();
+ } else {
+ cx.propagate_event();
}
- })
- .on_move(|_, _: &mut V, _| { /* Capture move events */ }),
+ }
+ })
+ .on_move(|_, _: &mut V, _| { /* Capture move events */ }),
)
}
@@ -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);
@@ -5,7 +5,7 @@ use crate::{
use anyhow::Result;
use gpui::{
geometry::{vector::Vector2F, Size},
- text_layout::LineLayout,
+ text_layout::Line,
LayoutId,
};
use parking_lot::Mutex;
@@ -32,7 +32,7 @@ impl<V: 'static> Element<V> for Text {
_view: &mut V,
cx: &mut ViewContext<V>,
) -> Result<(LayoutId, Self::PaintState)> {
- let fonts = cx.platform().fonts();
+ let layout_cache = cx.text_layout_cache().clone();
let text_style = cx.text_style();
let line_height = cx.font_cache().line_height(text_style.font_size);
let text = self.text.clone();
@@ -41,14 +41,14 @@ impl<V: 'static> Element<V> for Text {
let layout_id = cx.add_measured_layout_node(Default::default(), {
let paint_state = paint_state.clone();
move |_params| {
- let line_layout = fonts.layout_line(
+ let line_layout = layout_cache.layout_str(
text.as_ref(),
text_style.font_size,
&[(text.len(), text_style.to_run())],
);
let size = Size {
- width: line_layout.width,
+ width: line_layout.width(),
height: line_height,
};
@@ -85,13 +85,9 @@ impl<V: 'static> Element<V> for Text {
line_height = paint_state.line_height;
}
- let text_style = cx.text_style();
- let line =
- gpui::text_layout::Line::new(line_layout, &[(self.text.len(), text_style.to_run())]);
-
// TODO: We haven't added visible bounds to the new element system yet, so this is a placeholder.
let visible_bounds = bounds;
- line.paint(bounds.origin(), visible_bounds, line_height, cx.legacy_cx);
+ line_layout.paint(bounds.origin(), visible_bounds, line_height, cx.legacy_cx);
}
}
@@ -104,6 +100,6 @@ impl<V: 'static> IntoElement<V> for Text {
}
pub struct TextLayout {
- line_layout: Arc<LineLayout>,
+ line_layout: Arc<Line>,
line_height: f32,
}
@@ -22,7 +22,6 @@ test-support = [
]
[dependencies]
-client = { path = "../client" }
clock = { path = "../clock" }
collections = { path = "../collections" }
fuzzy = { path = "../fuzzy" }
@@ -46,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
@@ -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<LanguageRegistry>,
+ language: Option<Arc<Language>>,
+) -> 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<Anchor>,
pub new_text: String,
pub label: CodeLabel,
+ pub documentation: Option<Documentation>,
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)));
-
- let edit_operation = self.text.edit(edits.iter().cloned());
- let edit_id = edit_operation.timestamp();
+ // Non-generic part hoisted out to reduce LLVM IR size.
+ fn tail(
+ this: &mut Buffer,
+ edits: Vec<(Range<usize>, Arc<str>)>,
+ autoindent_mode: Option<AutoindentMode>,
+ cx: &mut ModelContext<Buffer>,
+ ) -> Option<clock::Lamport> {
+ 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 = 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);
+
+ 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;
+ }
- 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;
-
- // 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;
+ }
- // 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;
+ }
+ }
- // 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();
- 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();
+ this.autoindent_requests.push(Arc::new(AutoindentRequest {
+ before_edit,
+ entries,
+ is_block_mode: matches!(mode, AutoindentMode::Block { .. }),
+ }));
+ }
- self.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(
@@ -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<str>);
pub struct CachedLspAdapter {
pub name: LanguageServerName,
pub short_name: &'static str,
- pub initialization_options: Option<Value>,
pub disk_based_diagnostic_sources: Vec<String>,
pub disk_based_diagnostics_progress_token: Option<String>,
pub language_ids: HashMap<String, String>,
@@ -121,7 +121,6 @@ impl CachedLspAdapter {
pub async fn new(adapter: Arc<dyn LspAdapter>) -> Arc<Self> {
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<CodeLabel> {
self.adapter.label_for_symbol(name, kind, language).await
}
+
+ pub fn enabled_formatters(&self) -> Vec<BundledFormatter> {
+ 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<String, String> {
Default::default()
}
+
+ fn enabled_formatters(&self) -> Vec<BundledFormatter> {
+ 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<Box<dyn 'static + Send + Sync + Fn(&mut lsp::FakeLanguageServer)>>,
pub disk_based_diagnostics_progress_token: Option<String>,
pub disk_based_diagnostics_sources: Vec<String>,
+ pub enabled_formatters: Vec<BundledFormatter>,
}
#[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<FakeLspAdapter> {
async fn initialization_options(&self) -> Option<Value> {
self.initialization_options.clone()
}
+
+ fn enabled_formatters(&self) -> Vec<BundledFormatter> {
+ self.enabled_formatters.clone()
+ }
}
fn get_capture_indices(query: &Query, captures: &mut [(&str, &mut Option<u32>)]) {
@@ -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<String, serde_json::Value>,
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<Formatter>,
#[serde(default)]
+ pub prettier: Option<HashMap<String, serde_json::Value>>,
+ #[serde(default)]
pub enable_language_server: Option<bool>,
#[serde(default)]
pub show_copilot_suggestions: Option<bool>,
@@ -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<str>,
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,
@@ -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<usize>, MarkdownHighlight)>,
+ pub region_ranges: Vec<Range<usize>>,
+ pub regions: Vec<ParsedRegion>,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum MarkdownHighlight {
+ Style(MarkdownHighlightStyle),
+ Code(HighlightId),
+}
+
+impl MarkdownHighlight {
+ pub fn to_highlight_style(&self, theme: &theme::SyntaxTheme) -> Option<HighlightStyle> {
+ 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<Link>,
+}
+
+#[derive(Debug, Clone)]
+pub enum Link {
+ Web { url: String },
+ Path { path: PathBuf },
+}
+
+impl Link {
+ fn identify(text: String) -> Option<Link> {
+ 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<LanguageRegistry>,
+ language: Option<Arc<Language>>,
+) -> 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<LanguageRegistry>,
+ language: Option<Arc<Language>>,
+ text: &mut String,
+ highlights: &mut Vec<(Range<usize>, MarkdownHighlight)>,
+ region_ranges: &mut Vec<Range<usize>>,
+ regions: &mut Vec<ParsedRegion>,
+) {
+ 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<usize>, MarkdownHighlight)>,
+ content: &str,
+ language: &Arc<Language>,
+) {
+ 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<u64>, 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(" ");
+ }
+}
@@ -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,
})
@@ -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",
@@ -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
+
+ // <Foo className="bar" />
+ // <Foo className={`bar`} />
+ // <Foo className={something + "bar"} />
+ // <Foo className={something + "bar"} />
+ // const classes = "awesome ";
+ // <Foo className={classes} />
+
fn request_internal<T: request::Request>(
next_id: &AtomicUsize,
response_handlers: &Mutex<Option<HashMap<usize, ResponseHandler>>>,
@@ -220,29 +220,129 @@ impl NodeRuntime for RealNodeRuntime {
}
}
-pub struct FakeNodeRuntime;
+pub struct FakeNodeRuntime(Option<PrettierSupport>);
+
+struct PrettierSupport {
+ plugins: Vec<&'static str>,
+}
impl FakeNodeRuntime {
pub fn new() -> Arc<dyn NodeRuntime> {
- Arc::new(FakeNodeRuntime)
+ Arc::new(FakeNodeRuntime(None))
+ }
+
+ pub fn with_prettier_support(plugins: &[&'static str]) -> Arc<dyn NodeRuntime> {
+ Arc::new(FakeNodeRuntime(Some(PrettierSupport::new(plugins))))
}
}
#[async_trait::async_trait]
impl NodeRuntime for FakeNodeRuntime {
- async fn binary_path(&self) -> Result<PathBuf> {
- unreachable!()
+ async fn binary_path(&self) -> anyhow::Result<PathBuf> {
+ 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<Output> {
+ 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<String> {
+ 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<PathBuf> {
+ Ok(PathBuf::from("prettier_fake_node"))
}
async fn run_npm_subcommand(&self, _: Option<&Path>, _: &str, _: &[&str]) -> Result<Output> {
unreachable!()
}
- async fn npm_package_latest_version(&self, _: &str) -> Result<String> {
- unreachable!()
+ async fn npm_package_latest_version(&self, name: &str) -> anyhow::Result<String> {
+ 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(())
}
}
@@ -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"] }
@@ -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<usize>,
+ default: bool,
+ prettier_dir: PathBuf,
+ server: Arc<LanguageServer>,
+}
+
+#[cfg(any(test, feature = "test-support"))]
+pub struct TestPrettier {
+ worktree_id: Option<usize>,
+ prettier_dir: PathBuf,
+ default: bool,
+}
+
+#[derive(Debug)]
+pub struct LocateStart {
+ pub worktree_root_path: Arc<Path>,
+ pub starting_path: Arc<Path>,
+}
+
+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<LocateStart>,
+ fs: Arc<dyn Fs>,
+ ) -> anyhow::Result<PathBuf> {
+ 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::<PathBuf>();
+
+ 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<usize>,
+ _: LanguageServerId,
+ prettier_dir: PathBuf,
+ _: Arc<dyn NodeRuntime>,
+ _: AsyncAppContext,
+ ) -> anyhow::Result<Self> {
+ 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<usize>,
+ server_id: LanguageServerId,
+ prettier_dir: PathBuf,
+ node: Arc<dyn NodeRuntime>,
+ cx: AsyncAppContext,
+ ) -> anyhow::Result<Self> {
+ 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>,
+ buffer_path: Option<PathBuf>,
+ cx: &AsyncAppContext,
+ ) -> anyhow::Result<Diff> {
+ 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::<Vec<_>>();
+ 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::<Format>(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::<ClearCache>(())
+ .await
+ .context("prettier clear cache"),
+ #[cfg(any(test, feature = "test-support"))]
+ Self::Test(_) => Ok(()),
+ }
+ }
+
+ pub fn server(&self) -> Option<&Arc<LanguageServer>> {
+ 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<usize> {
+ 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<PathBuf>,
+ fs: &dyn Fs,
+) -> anyhow::Result<Option<PathBuf>> {
+ 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::<HashMap<String, serde_json::Value>>(
+ &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<PathBuf>,
+ parser: Option<String>,
+ #[serde(rename = "filepath")]
+ path: Option<PathBuf>,
+ prettier_options: Option<HashMap<String, serde_json::Value>>,
+}
+
+#[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";
+}
@@ -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}`);
+ }
+ }
+ });
+ });
+}
@@ -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
@@ -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<lsp::CompletionResponse>,
- _: ModelHandle<Project>,
+ project: ModelHandle<Project>,
buffer: ModelHandle<Buffer>,
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,
}
@@ -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<gpui::Subscription>,
copilot_log_subscription: Option<lsp::Subscription>,
current_lsp_settings: HashMap<Arc<str>, LspSettings>,
+ node: Option<Arc<dyn NodeRuntime>>,
+ prettier_instances: HashMap<
+ (Option<WorktreeId>, PathBuf),
+ Shared<Task<Result<Arc<Prettier>, Arc<anyhow::Error>>>>,
+ >,
}
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<Client>,
+ node: Arc<dyn NodeRuntime>,
user_store: ModelHandle<UserStore>,
languages: Arc<LanguageRegistry>,
fs: Arc<dyn Fs>,
@@ -660,6 +674,8 @@ impl Project {
copilot_lsp_subscription,
copilot_log_subscription: None,
current_lsp_settings: settings::get::<ProjectSettings>(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::<ProjectSettings>(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<Self>) {
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<Self>,
- initialization_options: Option<serde_json::Value>,
+ override_initialization_options: Option<serde_json::Value>,
pending_server: PendingLanguageServer,
adapter: Arc<CachedLspAdapter>,
language: Arc<Language>,
@@ -2817,7 +2876,7 @@ impl Project {
) -> Result<Option<Arc<LanguageServer>>> {
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<Self>,
- initialization_options: Option<serde_json::Value>,
+ override_options: Option<serde_json::Value>,
pending_server: PendingLanguageServer,
adapter: Arc<CachedLspAdapter>,
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<Project>,
- ) -> Task<Result<ProjectTransaction>> {
+ ) -> Task<anyhow::Result<ProjectTransaction>> {
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<Anchor>, 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<Worktree>,
+ changes: &[(Arc<Path>, ProjectEntryId, PathChange)],
+ cx: &mut ModelContext<'_, Project>,
+ ) {
+ let prettier_config_files = Prettier::CONFIG_FILE_NAMES
+ .iter()
+ .map(Path::new)
+ .collect::<HashSet<_>>();
+
+ 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::<Vec<_>>();
+
+ 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<ProjectPath>, cx: &mut ModelContext<Self>) {
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<Self>,
+ envelope: TypedEnvelope<proto::ResolveCompletionDocumentation>,
+ _: Arc<Client>,
+ mut cx: AsyncAppContext,
+ ) -> Result<proto::ResolveCompletionDocumentationResponse> {
+ 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::request::ResolveCompletionItem>(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<Self>,
envelope: TypedEnvelope<proto::ApplyCodeAction>,
@@ -8109,6 +8341,236 @@ impl Project {
Vec::new()
}
}
+
+ fn prettier_instance_for_buffer(
+ &mut self,
+ buffer: &ModelHandle<Buffer>,
+ cx: &mut ModelContext<Self>,
+ ) -> Task<Option<Shared<Task<Result<Arc<Prettier>, Arc<anyhow::Error>>>>>> {
+ 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<WorktreeId>,
+ new_language: &Language,
+ language_settings: &LanguageSettings,
+ cx: &mut ModelContext<Self>,
+ ) -> Task<anyhow::Result<()>> {
+ 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::<Vec<_>>();
+ 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(
@@ -4310,7 +4310,7 @@ impl<'a> From<&'a Entry> for proto::Entry {
is_symlink: entry.is_symlink,
is_ignored: entry.is_ignored,
is_external: entry.is_external,
- git_status: entry.git_status.map(|status| status.to_proto()),
+ git_status: entry.git_status.map(git_status_to_proto),
}
}
}
@@ -4337,7 +4337,7 @@ impl<'a> TryFrom<(&'a CharBag, proto::Entry)> for Entry {
is_symlink: entry.is_symlink,
is_ignored: entry.is_ignored,
is_external: entry.is_external,
- git_status: GitFileStatus::from_proto(entry.git_status),
+ git_status: git_status_from_proto(entry.git_status),
})
} else {
Err(anyhow!(
@@ -4366,3 +4366,21 @@ fn combine_git_statuses(
unstaged
}
}
+
+fn git_status_from_proto(git_status: Option<i32>) -> Option<GitFileStatus> {
+ git_status.and_then(|status| {
+ proto::GitStatus::from_i32(status).map(|status| match status {
+ proto::GitStatus::Added => GitFileStatus::Added,
+ proto::GitStatus::Modified => GitFileStatus::Modified,
+ proto::GitStatus::Conflict => GitFileStatus::Conflict,
+ })
+ })
+}
+
+fn git_status_to_proto(status: GitFileStatus) -> i32 {
+ match status {
+ GitFileStatus::Added => proto::GitStatus::Added as i32,
+ GitFileStatus::Modified => proto::GitStatus::Modified as i32,
+ GitFileStatus::Conflict => proto::GitStatus::Conflict as i32,
+ }
+}
@@ -89,88 +89,90 @@ 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;
-
- UpdateContacts update_contacts = 87;
- UpdateInviteInfo update_invite_info = 88;
- ShowContacts show_contacts = 89;
-
- 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;
-
- 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;
-
- OnTypeFormatting on_type_formatting = 103;
- OnTypeFormattingResponse on_type_formatting_response = 104;
-
- UpdateWorktreeSettings update_worktree_settings = 105;
-
- 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;
-
- 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;
-
- 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;
-
- 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;
-
- LinkChannel link_channel = 140;
- UnlinkChannel unlink_channel = 141;
- MoveChannel move_channel = 142; // current max: 144
+ 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 = 89;
+ UpdateInviteInfo update_invite_info = 90;
+ ShowContacts show_contacts = 91;
+
+ 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 = 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 = 105;
+ OnTypeFormattingResponse on_type_formatting_response = 106;
+
+ UpdateWorktreeSettings update_worktree_settings = 107;
+
+ 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 = 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;
+ SetChannelMemberAdmin set_channel_member_admin = 123;
+ RenameChannel rename_channel = 124;
+ RenameChannelResponse rename_channel_response = 125;
+
+ 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 = 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 = 142;
+ UnlinkChannel unlink_channel = 143;
+ MoveChannel move_channel = 144; // current max: 146
}
}
@@ -832,6 +834,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;
@@ -205,6 +205,8 @@ messages!(
(OnTypeFormattingResponse, Background),
(InlayHints, Background),
(InlayHintsResponse, Background),
+ (ResolveCompletionDocumentation, Background),
+ (ResolveCompletionDocumentationResponse, Background),
(ResolveInlayHint, Background),
(ResolveInlayHintResponse, Background),
(RefreshInlayHints, Foreground),
@@ -318,6 +320,10 @@ request_messages!(
(PrepareRename, PrepareRenameResponse),
(OnTypeFormatting, OnTypeFormattingResponse),
(InlayHints, InlayHintsResponse),
+ (
+ ResolveCompletionDocumentation,
+ ResolveCompletionDocumentationResponse
+ ),
(ResolveInlayHint, ResolveInlayHintResponse),
(RefreshInlayHints, Ack),
(ReloadBuffers, ReloadBuffersResponse),
@@ -381,6 +387,7 @@ entity_messages!(
PerformRename,
OnTypeFormatting,
InlayHints,
+ ResolveCompletionDocumentation,
ResolveInlayHint,
RefreshInlayHints,
PrepareRename,
@@ -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;
@@ -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"
@@ -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",
-]
@@ -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"] }
@@ -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<E: Element> {
-
-}
-
-impl<V> Element<V> 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<V>(layout: Layout, cx: &mut PaintContext<V>);
- pub fn paint_foreground<V>(layout: Layout, cx: &mut PaintContext<V>);
-}
-
-pub trait Interactive<V> {
- fn interactions(&mut self) -> &mut Interactions<V>;
-
- fn on_click(self, )
-}
-
-struct Interactions<V> {
- click: SmallVec<[<Rc<dyn Fn(&mut V, &dyn Any, )>; 1]>,
-}
-
-
-```
-
-
-```rs
-
-
-trait Stylable {
- type Style;
-
- fn with_style(self, style: Self::Style) -> Self;
-}
-
-
-
-
-
-
-```
@@ -1,3 +0,0 @@
-pub mod components;
-pub mod elements;
-pub mod kitchen_sink;
@@ -1,18 +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 palette;
-pub mod panel;
-pub mod project_panel;
-pub mod status_bar;
-pub mod tab;
-pub mod tab_bar;
-pub mod terminal;
-pub mod title_bar;
-pub mod toolbar;
-pub mod traffic_lights;
@@ -1,16 +0,0 @@
-use ui::prelude::*;
-use ui::AssistantPanel;
-
-use crate::story::Story;
-
-#[derive(Element, Default)]
-pub struct AssistantPanelStory {}
-
-impl AssistantPanelStory {
- fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
- Story::container(cx)
- .child(Story::title_for::<_, AssistantPanel<V>>(cx))
- .child(Story::label(cx, "Default"))
- .child(AssistantPanel::new())
- }
-}
@@ -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<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
- 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),
- },
- ]),
- ],
- ))
- }
-}
@@ -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<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
- 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)),
- )
- }
-}
@@ -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<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
- Story::container(cx)
- .child(Story::title_for::<_, ChatPanel<V>>(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(()),
- ))
- }
-}
@@ -1,16 +0,0 @@
-use ui::prelude::*;
-use ui::CollabPanel;
-
-use crate::story::Story;
-
-#[derive(Element, Default)]
-pub struct CollabPanelStory {}
-
-impl CollabPanelStory {
- fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
- Story::container(cx)
- .child(Story::title_for::<_, CollabPanel<V>>(cx))
- .child(Story::label(cx, "Default"))
- .child(CollabPanel::new(ScrollState::default()))
- }
-}
@@ -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<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
- 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")),
- ]))
- }
-}
@@ -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<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
- 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))),
- )
- }
-}
@@ -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<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
- 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)),
- ))
- }
-}
@@ -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<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
- Story::container(cx)
- .child(Story::title_for::<_, Palette<V>>(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())),
- ]),
- )
- }
-}
@@ -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<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
- Story::container(cx)
- .child(Story::title_for::<_, Panel<V>>(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(()),
- ))
- }
-}
@@ -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<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
- Story::container(cx)
- .child(Story::title_for::<_, ProjectPanel<V>>(cx))
- .child(Story::label(cx, "Default"))
- .child(Panel::new(
- ScrollState::default(),
- |_, _| vec![ProjectPanel::new(ScrollState::default()).into_any()],
- Box::new(()),
- ))
- }
-}
@@ -1,16 +0,0 @@
-use ui::prelude::*;
-use ui::StatusBar;
-
-use crate::story::Story;
-
-#[derive(Element, Default)]
-pub struct StatusBarStory {}
-
-impl StatusBarStory {
- fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
- Story::container(cx)
- .child(Story::title_for::<_, StatusBar<V>>(cx))
- .child(Story::label(cx, "Default"))
- .child(StatusBar::new())
- }
-}
@@ -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<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
- 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)
- }))),
- )
- }
-}
@@ -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<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
- Story::container(cx)
- .child(Story::title_for::<_, TabBar<V>>(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),
- ]))
- }
-}
@@ -1,16 +0,0 @@
-use ui::prelude::*;
-use ui::Terminal;
-
-use crate::story::Story;
-
-#[derive(Element, Default)]
-pub struct TerminalStory {}
-
-impl TerminalStory {
- fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
- Story::container(cx)
- .child(Story::title_for::<_, Terminal>(cx))
- .child(Story::label(cx, "Default"))
- .child(Terminal::new())
- }
-}
@@ -1,16 +0,0 @@
-use ui::prelude::*;
-use ui::TitleBar;
-
-use crate::story::Story;
-
-#[derive(Element, Default)]
-pub struct TitleBarStory {}
-
-impl TitleBarStory {
- fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
- Story::container(cx)
- .child(Story::title_for::<_, TitleBar<V>>(cx))
- .child(Story::label(cx, "Default"))
- .child(TitleBar::new(cx))
- }
-}
@@ -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<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
- let theme = theme(cx);
-
- struct LeftItemsPayload {
- pub theme: Arc<Theme>,
- }
-
- Story::container(cx)
- .child(Story::title_for::<_, Toolbar<V>>(cx))
- .child(Story::label(cx, "Default"))
- .child(Toolbar::new(
- |_, payload| {
- let payload = payload.downcast_ref::<LeftItemsPayload>().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(()),
- ))
- }
-}
@@ -1,18 +0,0 @@
-use ui::prelude::*;
-use ui::TrafficLights;
-
-use crate::story::Story;
-
-#[derive(Element, Default)]
-pub struct TrafficLightsStory {}
-
-impl TrafficLightsStory {
- fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
- 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))
- }
-}
@@ -1,5 +0,0 @@
-pub mod avatar;
-pub mod button;
-pub mod icon;
-pub mod input;
-pub mod label;
@@ -1,23 +0,0 @@
-use ui::prelude::*;
-use ui::Avatar;
-
-use crate::story::Story;
-
-#[derive(Element, Default)]
-pub struct AvatarStory {}
-
-impl AvatarStory {
- fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
- 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),
- )
- }
-}
@@ -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<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
- let states = InteractionState::iter();
-
- Story::container(cx)
- .child(Story::title_for::<_, Button<V>>(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.")),
- )
- }
-}
@@ -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<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
- 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)))
- }
-}
@@ -1,16 +0,0 @@
-use ui::prelude::*;
-use ui::Input;
-
-use crate::story::Story;
-
-#[derive(Element, Default)]
-pub struct InputStory {}
-
-impl InputStory {
- fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
- Story::container(cx)
- .child(Story::title_for::<_, Input>(cx))
- .child(Story::label(cx, "Default"))
- .child(div().flex().child(Input::new("Search")))
- }
-}
@@ -1,18 +0,0 @@
-use ui::prelude::*;
-use ui::Label;
-
-use crate::story::Story;
-
-#[derive(Element, Default)]
-pub struct LabelStory {}
-
-impl LabelStory {
- fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
- 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]))
- }
-}
@@ -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<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
- 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())
- }
-}
@@ -1,44 +0,0 @@
-use gpui2::elements::div::Div;
-use ui::prelude::*;
-use ui::theme;
-
-pub struct Story {}
-
-impl Story {
- pub fn container<V: 'static>(cx: &mut ViewContext<V>) -> Div<V> {
- 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<V: 'static>(cx: &mut ViewContext<V>, title: &str) -> impl Element<V> {
- let theme = theme(cx);
-
- div()
- .text_xl()
- .text_color(theme.lowest.base.default.foreground)
- .child(title.to_owned())
- }
-
- pub fn title_for<V: 'static, T>(cx: &mut ViewContext<V>) -> impl Element<V> {
- Self::title(cx, std::any::type_name::<T>())
- }
-
- pub fn label<V: 'static>(cx: &mut ViewContext<V>, label: &str) -> impl Element<V> {
- let theme = theme(cx);
-
- div()
- .mt_4()
- .mb_2()
- .text_xs()
- .text_color(theme.lowest.base.default.foreground)
- .child(label.to_owned())
- }
-}
@@ -1,164 +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<V: 'static>(&self) -> AnyElement<V> {
- 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,
- Palette,
- Panel,
- ProjectPanel,
- StatusBar,
- Tab,
- TabBar,
- Terminal,
- TitleBar,
- Toolbar,
- TrafficLights,
-}
-
-impl ComponentStory {
- pub fn story<V: 'static>(&self) -> AnyElement<V> {
- 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::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::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::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<Self, Self::Err> {
- 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<V: 'static>(&self) -> AnyElement<V> {
- 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<Vec<StorySelector>> = 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::<Vec<_>>()
- });
-
- stories
- }
-
- fn to_possible_value(&self) -> Option<clap::builder::PossibleValue> {
- 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))
- }
-}
@@ -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<StorySelector>,
-
- /// The name of the theme to use in the storybook.
- ///
- /// If not provided, the default theme will be used.
- #[arg(long)]
- theme: Option<String>,
-}
-
-async fn watch_zed_changes(fs: Arc<dyn fs::Fs>) -> 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<std::process::Child> = 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::<Arc<ThemeRegistry>>();
-
- 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<V: 'static, S: IntoElement<V>>(
- cx: &mut ViewContext<V>,
- theme_override: Option<Arc<legacy_theme::Theme>>,
- story: S,
-) -> impl Element<V> {
- let theme = current_theme(cx, theme_override);
-
- story.into_element().themed(theme)
-}
-
-fn current_theme<V: 'static>(
- cx: &mut ViewContext<V>,
- theme_override: Option<Arc<legacy_theme::Theme>>,
-) -> Theme {
- let legacy_theme =
- theme_override.unwrap_or_else(|| settings::get::<ThemeSettings>(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::<Theme>()
- .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<std::borrow::Cow<[u8]>> {
- Self::get(path)
- .map(|f| f.data)
- .ok_or_else(|| anyhow!("could not find asset at path \"{}\"", path))
- }
-
- fn list(&self, path: &str) -> Vec<std::borrow::Cow<'static, str>> {
- 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();
-// }
@@ -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();
@@ -867,9 +867,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)]
@@ -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"
@@ -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.
@@ -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
@@ -1,7 +0,0 @@
-use std::any::Any;
-
-use gpui2::{AnyElement, ViewContext};
-
-pub type HackyChildren<V> = fn(&mut ViewContext<V>, &dyn Any) -> Vec<AnyElement<V>>;
-
-pub type HackyChildrenPayload = Box<dyn Any>;
@@ -1,153 +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 list;
-mod palette;
-mod panel;
-mod panes;
-mod player_stack;
-mod project_panel;
-mod status_bar;
-mod tab;
-mod tab_bar;
-mod terminal;
-mod title_bar;
-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 list::*;
-pub use palette::*;
-pub use panel::*;
-pub use panes::*;
-pub use player_stack::*;
-pub use project_panel::*;
-pub use status_bar::*;
-pub use tab::*;
-pub use tab_bar::*;
-pub use terminal::*;
-pub use title_bar::*;
-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<V, D> {
-// click: Option<Rc<dyn Fn(&mut V, &D, &mut EventContext<V>)>>,
-// }
-
-// impl<V, D> Default for ButtonHandlers<V, D> {
-// fn default() -> Self {
-// Self { click: None }
-// }
-// }
-
-// #[derive(Element)]
-// pub struct Button<V: 'static, D: 'static> {
-// handlers: ButtonHandlers<V, D>,
-// label: Option<ArcCow<'static, str>>,
-// icon: Option<ArcCow<'static, str>>,
-// data: Rc<D>,
-// view_type: PhantomData<V>,
-// }
-
-// // Impl block for buttons without data.
-// // See below for an impl block for any button.
-// impl<V: 'static> Button<V, ()> {
-// fn new() -> Self {
-// Self {
-// handlers: ButtonHandlers::default(),
-// label: None,
-// icon: None,
-// data: Rc::new(()),
-// view_type: PhantomData,
-// }
-// }
-
-// pub fn data<D: 'static>(self, data: D) -> Button<V, D> {
-// 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<V: 'static, D: 'static> Button<V, D> {
-// pub fn label(mut self, label: impl Into<ArcCow<'static, str>>) -> Self {
-// self.label = Some(label.into());
-// self
-// }
-
-// pub fn icon(mut self, icon: impl Into<ArcCow<'static, str>>) -> Self {
-// self.icon = Some(icon.into());
-// self
-// }
-
-// pub fn on_click(
-// mut self,
-// handler: impl Fn(&mut V, &D, &mut EventContext<V>) + 'static,
-// ) -> Self {
-// self.handlers.click = Some(Rc::new(handler));
-// self
-// }
-// }
-
-// pub fn button<V>() -> Button<V, ()> {
-// Button::new()
-// }
-
-// impl<V: 'static, D: 'static> Button<V, D> {
-// fn render(
-// &mut self,
-// view: &mut V,
-// cx: &mut ViewContext<V>,
-// ) -> impl IntoElement<V> + Interactive<V> {
-// // let colors = &cx.theme::<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
-// }
-// }
-// }
@@ -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<V: 'static> {
- view_type: PhantomData<V>,
- scroll_state: ScrollState,
- current_side: PanelSide,
-}
-
-impl<V: 'static> AssistantPanel<V> {
- 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<V>) -> impl IntoElement<V> {
- let theme = theme(cx);
-
- struct PanelPayload {
- pub scroll_state: ScrollState,
- }
-
- Panel::new(
- self.scroll_state.clone(),
- |_, payload| {
- let payload = payload.downcast_ref::<PanelPayload>().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.))
- }
-}
@@ -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<HighlightedText>);
-
-#[derive(Element)]
-pub struct Breadcrumb {
- path: PathBuf,
- symbols: Vec<Symbol>,
-}
-
-impl Breadcrumb {
- pub fn new(path: PathBuf, symbols: Vec<Symbol>) -> Self {
- Self { path, symbols }
- }
-
- fn render_separator<V: 'static>(&self, theme: &Theme) -> Div<V> {
- div()
- .child(" › ")
- .text_color(HighlightColor::Default.hsla(theme))
- }
-
- fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
- 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::<Vec<_>>(),
- ),
- )
- }
-}
@@ -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<HighlightedText>,
-}
-
-#[derive(Default, PartialEq, Clone)]
-pub struct BufferRow {
- pub line_number: usize,
- pub code_action: bool,
- pub current: bool,
- pub line: Option<HighlightedLine>,
- pub cursors: Option<Vec<PlayerCursor>>,
- pub status: GitStatus,
- pub show_line_number: bool,
-}
-
-#[derive(Clone)]
-pub struct BufferRows {
- pub show_line_numbers: bool,
- pub rows: Vec<BufferRow>,
-}
-
-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<HighlightedLine>) -> Self {
- self.line = line;
- self
- }
-
- pub fn set_cursors(mut self, cursors: Option<Vec<PlayerCursor>>) -> 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<BufferRows>,
- readonly: bool,
- language: Option<String>,
- title: Option<String>,
- path: Option<String>,
-}
-
-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<T: Into<Option<String>>>(mut self, title: T) -> Self {
- self.title = title.into();
- self
- }
-
- pub fn set_path<P: Into<Option<String>>>(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<R: Into<Option<BufferRows>>>(mut self, rows: R) -> Self {
- self.rows = rows.into();
- self
- }
-
- pub fn set_language<L: Into<Option<String>>>(mut self, language: L) -> Self {
- self.language = language.into();
- self
- }
-
- fn render_row<V: 'static>(row: BufferRow, cx: &WindowContext) -> impl IntoElement<V> {
- 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<V: 'static>(&self, cx: &WindowContext) -> Vec<impl IntoElement<V>> {
- match &self.rows {
- Some(rows) => rows
- .rows
- .iter()
- .map(|row| Self::render_row(row.clone(), cx))
- .collect(),
- None => vec![],
- }
- }
-
- fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
- 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)
- }
-}
@@ -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<V: 'static> {
- view_type: PhantomData<V>,
- scroll_state: ScrollState,
- messages: Vec<ChatMessage>,
-}
-
-impl<V: 'static> ChatPanel<V> {
- pub fn new(scroll_state: ScrollState) -> Self {
- Self {
- view_type: PhantomData,
- scroll_state,
- messages: Vec::new(),
- }
- }
-
- pub fn with_messages(mut self, messages: Vec<ChatMessage>) -> Self {
- self.messages = messages;
- self
- }
-
- fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
- 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<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
- 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())))
- }
-}
@@ -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<V: 'static> {
- view_type: PhantomData<V>,
- scroll_state: ScrollState,
-}
-
-impl<V: 'static> CollabPanel<V> {
- pub fn new(scroll_state: ScrollState) -> Self {
- Self {
- view_type: PhantomData,
- scroll_state,
- }
- }
-
- fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
- 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<ArcCow<'static, str>>,
- expanded: bool,
- theme: &Theme,
- ) -> impl Element<V> {
- 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<ArcCow<'static, str>>,
- label: impl Into<ArcCow<'static, str>>,
- theme: &Theme,
- ) -> impl Element<V> {
- 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),
- )
- }
-}
@@ -1,29 +0,0 @@
-use std::marker::PhantomData;
-
-use crate::prelude::*;
-use crate::{example_editor_actions, OrderMethod, Palette};
-
-#[derive(Element)]
-pub struct CommandPalette<V: 'static> {
- view_type: PhantomData<V>,
- scroll_state: ScrollState,
-}
-
-impl<V: 'static> CommandPalette<V> {
- pub fn new(scroll_state: ScrollState) -> Self {
- Self {
- view_type: PhantomData,
- scroll_state,
- }
- }
-
- fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
- 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),
- )
- }
-}
@@ -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<ContextMenuItem>,
-}
-
-impl ContextMenu {
- pub fn new(items: impl IntoIterator<Item = ContextMenuItem>) -> Self {
- Self {
- items: items.into_iter().collect(),
- }
- }
- fn render<V: 'static>(&mut self, view: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
- 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())
- }
-}
@@ -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<Tab>,
- pub path: PathBuf,
- pub symbols: Vec<Symbol>,
- pub buffer: Buffer,
-}
-
-#[derive(Element)]
-pub struct EditorPane<V: 'static> {
- view_type: PhantomData<V>,
- editor: Editor,
-}
-
-impl<V: 'static> EditorPane<V> {
- pub fn new(editor: Editor) -> Self {
- Self {
- view_type: PhantomData,
- editor,
- }
- }
-
- fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
- struct LeftItemsPayload {
- path: PathBuf,
- symbols: Vec<Symbol>,
- }
-
- v_stack()
- .w_full()
- .h_full()
- .flex_1()
- .child(TabBar::new(self.editor.tabs.clone()))
- .child(Toolbar::new(
- |_, payload| {
- let payload = payload.downcast_ref::<LeftItemsPayload>().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())
- }
-}
@@ -1,28 +0,0 @@
-use crate::prelude::*;
-use crate::{theme, Avatar, Player};
-
-#[derive(Element)]
-pub struct Facepile {
- players: Vec<Player>,
-}
-
-impl Facepile {
- pub fn new<P: Iterator<Item = Player>>(players: P) -> Self {
- Self {
- players: players.collect(),
- }
- }
-
- fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
- 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)
- }
-}
@@ -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<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
- 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))
- }
-}
@@ -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<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
- 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<K>(key: K) -> Self
- where
- K: Into<String>,
- {
- Self { key: key.into() }
- }
-
- fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
- 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<ModifierKey>);
-
-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
- }
-}
@@ -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<Icon>,
- 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<Icon>) -> Self {
- self.left_icon = left_icon;
- self
- }
-
- pub fn state(mut self, state: InteractionState) -> Self {
- self.state = state;
- self
- }
-
- fn disclosure_control<V: 'static>(&self) -> Div<V> {
- 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<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
- 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<Icon>,
- 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<Icon>) -> Self {
- self.left_icon = left_icon;
- self
- }
-
- fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
- 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<ListEntry> for ListItem {
- fn from(entry: ListEntry) -> Self {
- Self::Entry(entry)
- }
-}
-
-impl From<ListSeparator> for ListItem {
- fn from(entry: ListSeparator) -> Self {
- Self::Separator(entry)
- }
-}
-
-impl From<ListSubHeader> for ListItem {
- fn from(entry: ListSubHeader) -> Self {
- Self::Header(entry)
- }
-}
-
-impl ListItem {
- fn render<V: 'static>(&mut self, v: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
- 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<LeftContent>,
- variant: ListItemVariant,
- size: ListEntrySize,
- state: InteractionState,
- toggle: Option<ToggleState>,
-}
-
-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<V: 'static>(
- &mut self,
- cx: &mut ViewContext<V>,
- ) -> Option<impl IntoElement<V>> {
- 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<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
- 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<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
- let theme = theme(cx);
-
- div().h_px().w_full().fill(theme.lowest.base.default.border)
- }
-}
-
-#[derive(Element)]
-pub struct List {
- items: Vec<ListItem>,
- empty_message: &'static str,
- header: Option<ListHeader>,
- toggleable: Toggleable,
-}
-
-impl List {
- pub fn new(items: Vec<ListItem>) -> 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<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
- 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)
- }
-}
@@ -1,144 +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<V: 'static> {
- view_type: PhantomData<V>,
- scroll_state: ScrollState,
- input_placeholder: &'static str,
- empty_string: &'static str,
- items: Vec<PaletteItem>,
- default_order: OrderMethod,
-}
-
-impl<V: 'static> Palette<V> {
- 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<PaletteItem>) -> 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<V>) -> impl IntoElement<V> {
- 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(
- PaletteItem::new(item.label)
- .keybinding(item.keybinding.clone()),
- )
- })),
- ),
- )
- }
-}
-
-#[derive(Element)]
-pub struct PaletteItem {
- pub label: &'static str,
- pub keybinding: Option<Keybinding>,
-}
-
-impl PaletteItem {
- pub fn new(label: &'static str) -> Self {
- Self {
- label,
- keybinding: None,
- }
- }
-
- pub fn label(mut self, label: &'static str) -> Self {
- self.label = label;
- self
- }
-
- pub fn keybinding<K>(mut self, keybinding: K) -> Self
- where
- K: Into<Option<Keybinding>>,
- {
- self.keybinding = keybinding.into();
- self
- }
-
- fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
- let theme = theme(cx);
-
- div()
- .flex()
- .flex_row()
- .grow()
- .justify_between()
- .child(Label::new(self.label))
- .children(self.keybinding.clone())
- }
-}
@@ -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<PanelSide> {
- 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<V: 'static> {
- view_type: PhantomData<V>,
- scroll_state: ScrollState,
- current_side: PanelSide,
- /// Defaults to PanelAllowedSides::LeftAndRight
- allowed_sides: PanelAllowedSides,
- initial_width: AbsoluteLength,
- width: Option<AbsoluteLength>,
- children: HackyChildren<V>,
- payload: HackyChildrenPayload,
-}
-
-impl<V: 'static> Panel<V> {
- pub fn new(
- scroll_state: ScrollState,
- children: HackyChildren<V>,
- 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<V>) -> impl IntoElement<V> {
- 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()))
- }
-}
@@ -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<V: 'static> {
- view_type: PhantomData<V>,
- scroll_state: ScrollState,
- size: Size<Length>,
- fill: Hsla,
- children: HackyChildren<V>,
- payload: HackyChildrenPayload,
-}
-
-impl<V: 'static> Pane<V> {
- pub fn new(
- scroll_state: ScrollState,
- size: Size<Length>,
- children: HackyChildren<V>,
- 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<V>) -> impl IntoElement<V> {
- 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<V: 'static> {
- view_type: PhantomData<V>,
- groups: Vec<PaneGroup<V>>,
- panes: Vec<Pane<V>>,
- split_direction: SplitDirection,
-}
-
-impl<V: 'static> PaneGroup<V> {
- pub fn new_groups(groups: Vec<PaneGroup<V>>, split_direction: SplitDirection) -> Self {
- Self {
- view_type: PhantomData,
- groups,
- panes: Vec::new(),
- split_direction,
- }
- }
-
- pub fn new_panes(panes: Vec<Pane<V>>, split_direction: SplitDirection) -> Self {
- Self {
- view_type: PhantomData,
- groups: Vec::new(),
- panes,
- split_direction,
- }
- }
-
- fn render(&mut self, view: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
- 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!()
- }
-}
@@ -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<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
- 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()))
- })),
- )
- }
-}
@@ -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<V: 'static> {
- view_type: PhantomData<V>,
- scroll_state: ScrollState,
-}
-
-impl<V: 'static> ProjectPanel<V> {
- pub fn new(scroll_state: ScrollState) -> Self {
- Self {
- view_type: PhantomData,
- scroll_state,
- }
- }
-
- fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
- 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),
- )
- }
-}
@@ -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<usize>,
- tools: Vec<Tool>,
-}
-
-impl Default for ToolGroup {
- fn default() -> Self {
- ToolGroup {
- active_index: None,
- tools: vec![],
- }
- }
-}
-
-#[derive(Element)]
-pub struct StatusBar<V: 'static> {
- view_type: PhantomData<V>,
- left_tools: Option<ToolGroup>,
- right_tools: Option<ToolGroup>,
- bottom_tools: Option<ToolGroup>,
-}
-
-impl<V: 'static> StatusBar<V> {
- 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<usize>) -> 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<usize>) -> 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<usize>) -> 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<V>) -> impl IntoElement<V> {
- 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<V> {
- 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<V> {
- 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)),
- )
- }
-}
@@ -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<Icon>,
- 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<I>(mut self, icon: I) -> Self
- where
- I: Into<Option<Icon>>,
- {
- 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<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
- 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
- }),
- )
- }
-}
@@ -1,85 +0,0 @@
-use std::marker::PhantomData;
-
-use crate::prelude::*;
-use crate::{theme, Icon, IconButton, Tab};
-
-#[derive(Element)]
-pub struct TabBar<V: 'static> {
- view_type: PhantomData<V>,
- scroll_state: ScrollState,
- tabs: Vec<Tab>,
-}
-
-impl<V: 'static> TabBar<V> {
- pub fn new(tabs: Vec<Tab>) -> 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<V>) -> impl IntoElement<V> {
- 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)),
- ),
- )
- }
-}
@@ -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<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
- 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::<Arc<Theme>>().unwrap();
-
- vec![crate::static_data::terminal_buffer(&theme).into_any()]
- },
- Box::new(theme),
- ))
- }
-}
@@ -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<PlayerWithCallStatus>,
- pub channel: Option<String>, // projects
- // windows
-}
-
-#[derive(Element)]
-pub struct TitleBar<V: 'static> {
- view_type: PhantomData<V>,
- /// If the window is active from the OS's perspective.
- is_active: Arc<AtomicBool>,
- livestream: Option<Livestream>,
-}
-
-impl<V: 'static> TitleBar<V> {
- pub fn new(cx: &mut ViewContext<V>) -> 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<Livestream>) -> Self {
- self.livestream = livestream;
- self
- }
-
- fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
- 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),
- ),
- ),
- )
- }
-}
@@ -1,49 +0,0 @@
-use crate::prelude::*;
-use crate::theme;
-
-#[derive(Clone)]
-pub struct ToolbarItem {}
-
-#[derive(Element)]
-pub struct Toolbar<V: 'static> {
- left_items: HackyChildren<V>,
- left_items_payload: HackyChildrenPayload,
- right_items: HackyChildren<V>,
- right_items_payload: HackyChildrenPayload,
-}
-
-impl<V: 'static> Toolbar<V> {
- pub fn new(
- left_items: HackyChildren<V>,
- left_items_payload: HackyChildrenPayload,
- right_items: HackyChildren<V>,
- 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<V>) -> impl IntoElement<V> {
- 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())),
- )
- }
-}
@@ -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<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
- 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<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
- 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,
- ))
- }
-}
@@ -1,173 +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<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
- 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::<Arc<Theme>>().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::<Arc<Theme>>().unwrap();
-
- vec![EditorPane::new(hello_world_rust_editor_with_status_example(
- &theme,
- ))
- .into_any()]
- },
- Box::new(theme.clone()),
- )],
- SplitDirection::Vertical,
- ),
- ],
- SplitDirection::Horizontal,
- );
-
- div()
- .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())
- }
-}
@@ -1,24 +0,0 @@
-use std::marker::PhantomData;
-
-use gpui2::Element;
-
-use crate::theme::{Theme, Themed};
-
-pub trait ElementExt<V: 'static>: Element<V> {
- fn themed(self, theme: Theme) -> Themed<V, Self>
- where
- Self: Sized;
-}
-
-impl<V: 'static, E: Element<V>> ElementExt<V> for E {
- fn themed(self, theme: Theme) -> Themed<V, Self>
- where
- Self: Sized,
- {
- Themed {
- child: self,
- theme,
- view_type: PhantomData,
- }
- }
-}
@@ -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::*;
@@ -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<ArcCow<'static, str>>) -> Self {
- Self {
- src: src.into(),
- shape: Shape::Circle,
- }
- }
-
- pub fn shape(mut self, shape: Shape) -> Self {
- self.shape = shape;
- self
- }
-
- fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
- 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)
- }
-}
@@ -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<V> {
- click: Option<Rc<dyn Fn(&mut V, &mut EventContext<V>)>>,
-}
-
-impl<V> Default for ButtonHandlers<V> {
- fn default() -> Self {
- Self { click: None }
- }
-}
-
-#[derive(Element)]
-pub struct Button<V: 'static> {
- label: String,
- variant: ButtonVariant,
- state: InteractionState,
- icon: Option<Icon>,
- icon_position: Option<IconPosition>,
- width: Option<DefiniteLength>,
- handlers: ButtonHandlers<V>,
-}
-
-impl<V: 'static> Button<V> {
- pub fn new<L>(label: L) -> Self
- where
- L: Into<String>,
- {
- Self {
- label: label.into(),
- variant: Default::default(),
- state: Default::default(),
- icon: None,
- icon_position: None,
- width: Default::default(),
- handlers: ButtonHandlers::default(),
- }
- }
-
- pub fn ghost<L>(label: L) -> Self
- where
- L: Into<String>,
- {
- 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<DefiniteLength>) -> Self {
- self.width = width;
- self
- }
-
- pub fn on_click(mut self, handler: impl Fn(&mut V, &mut EventContext<V>) + 'static) -> Self {
- self.handlers.click = Some(Rc::new(handler));
- self
- }
-
- fn background_color(&self, cx: &mut ViewContext<V>) -> 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<IconElement> {
- self.icon.map(|i| IconElement::new(i).color(icon_color))
- }
-
- fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
- 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
- }
-}
@@ -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<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
- 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))
- }
-}
@@ -1,183 +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<Theme>) -> 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,
- 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::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<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
- 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)
- }
-}
@@ -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<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
- 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("|")),
- )
- }
-}
@@ -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<usize>,
- strikethrough: bool,
-}
-
-impl Label {
- pub fn new<L>(label: L) -> Self
- where
- L: Into<String>,
- {
- 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<usize>) -> Self {
- self.highlight_indices = indices;
- self
- }
-
- pub fn set_strikethrough(mut self, strikethrough: bool) -> Self {
- self.strikethrough = strikethrough;
- self
- }
-
- fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
- 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,
-}
@@ -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<Vec<Player>>,
- pub followers: Option<Vec<Player>>,
-}
-
-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<V>(&self, cx: &mut ViewContext<V>) -> Hsla {
- let theme = theme(cx);
- let index = self.index % 8;
- theme.players[self.index].cursor
- }
-
- pub fn selection_color<V>(&self, cx: &mut ViewContext<V>) -> 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
- }
-}
@@ -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<V> Stack for Div<V> {}
-
-/// Horizontally stacks elements.
-///
-/// Sets `flex()`, `flex_row()`, `items_center()`
-pub fn h_stack<V: 'static>() -> Div<V> {
- div().h_stack()
-}
-
-/// Vertically stacks elements.
-///
-/// Sets `flex()`, `flex_col()`
-pub fn v_stack<V: 'static>() -> Div<V> {
- div().v_stack()
-}
@@ -1,17 +0,0 @@
-use crate::prelude::*;
-use crate::theme;
-
-#[derive(Element)]
-pub struct ToolDivider {}
-
-impl ToolDivider {
- pub fn new() -> Self {
- Self {}
- }
-
- fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
- let theme = theme(cx);
-
- div().w_px().h_3().fill(theme.lowest.base.default.border)
- }
-}
@@ -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::*;
@@ -1,254 +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::<Hsla>(0xEC695E),
- mac_os_traffic_light_yellow: rgb::<Hsla>(0xF4BF4F),
- mac_os_traffic_light_green: rgb::<Hsla>(0x62C554),
- }
- }
- pub fn color(&self) -> Hsla {
- self.transparent
- }
-}
-
-#[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<ToggleState> 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<Toggleable> for ToggleState {
- fn from(toggleable: Toggleable) -> Self {
- match toggleable {
- Toggleable::Toggleable(state) => state,
- Toggleable::NotToggleable => ToggleState::NotToggled,
- }
- }
-}
-
-impl From<bool> for ToggleState {
- fn from(toggled: bool) -> Self {
- if toggled {
- ToggleState::Toggled
- } else {
- ToggleState::NotToggled
- }
- }
-}
@@ -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<Tab> {
- 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<Tab> {
- 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<Tab> {
- 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<Tab> {
- vec![Tab::new().git_status(GitStatus::Created).current(true)]
-}
-
-pub fn static_players() -> Vec<Player> {
- 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<PlayerData> {
- 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<PlayerData>) -> Vec<Player> {
- 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<PlayerData>) -> Player {
- Player::new(1, data[0].url.clone(), data[0].name.clone())
-}
-pub fn static_player_2(data: &Vec<PlayerData>) -> Player {
- Player::new(2, data[1].url.clone(), data[1].name.clone())
-}
-pub fn static_player_3(data: &Vec<PlayerData>) -> Player {
- Player::new(3, data[2].url.clone(), data[2].name.clone())
-}
-pub fn static_player_4(data: &Vec<PlayerData>) -> Player {
- Player::new(4, data[3].url.clone(), data[3].name.clone())
-}
-pub fn static_player_5(data: &Vec<PlayerData>) -> Player {
- Player::new(5, data[4].url.clone(), data[4].name.clone())
-}
-pub fn static_player_6(data: &Vec<PlayerData>) -> Player {
- Player::new(6, data[5].url.clone(), data[5].name.clone())
-}
-pub fn static_player_7(data: &Vec<PlayerData>) -> Player {
- Player::new(7, data[6].url.clone(), data[6].name.clone())
-}
-pub fn static_player_8(data: &Vec<PlayerData>) -> Player {
- Player::new(8, data[7].url.clone(), data[7].name.clone())
-}
-pub fn static_player_9(data: &Vec<PlayerData>) -> Player {
- Player::new(9, data[8].url.clone(), data[8].name.clone())
-}
-pub fn static_player_10(data: &Vec<PlayerData>) -> 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<Vec<Player>>,
-) -> 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<PlayerWithCallStatus> {
- 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<PlayerWithCallStatus> {
- 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<ListItem> {
- 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<ListItem> {
- 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<ListItem> {
- 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<ListItem> {
- 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<PaletteItem> {
- 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<BufferRow> {
- 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<BufferRow> {
- 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<BufferRow> {
- 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,
- },
- ]
-}
@@ -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<PlayerColors>,
- #[serde(deserialize_with = "deserialize_syntax_colors")]
- pub syntax: HashMap<String, Hsla>,
-}
-
-#[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<u8>,
-}
-
-fn deserialize_player_colors<'de, D>(deserializer: D) -> Result<Vec<PlayerColors>, D::Error>
-where
- D: Deserializer<'de>,
-{
- struct PlayerArrayVisitor;
-
- impl<'de> Visitor<'de> for PlayerArrayVisitor {
- type Value = Vec<PlayerColors>;
-
- fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
- formatter.write_str("an object with integer keys")
- }
-
- fn visit_map<A: serde::de::MapAccess<'de>>(
- self,
- mut map: A,
- ) -> Result<Self::Value, A::Error> {
- let mut players = Vec::with_capacity(8);
- while let Some((key, value)) = map.next_entry::<usize, PlayerColors>()? {
- 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<HashMap<String, Hsla>, D::Error>
-where
- D: serde::Deserializer<'de>,
-{
- #[derive(Deserialize)]
- struct ColorWrapper {
- color: Hsla,
- }
-
- struct SyntaxVisitor;
-
- impl<'de> Visitor<'de> for SyntaxVisitor {
- type Value = HashMap<String, Hsla>;
-
- 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<M>(self, mut map: M) -> Result<HashMap<String, Hsla>, 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<V: 'static, E: Element<V>> {
- pub(crate) theme: Theme,
- pub(crate) child: E,
- pub(crate) view_type: PhantomData<V>,
-}
-
-impl<V: 'static, E: Element<V>> Element<V> for Themed<V, E> {
- type PaintState = E::PaintState;
-
- fn layout(
- &mut self,
- view: &mut V,
- cx: &mut ViewContext<V>,
- ) -> 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<V>,
- ) where
- Self: Sized,
- {
- cx.push_theme(self.theme.clone());
- self.child.paint(view, parent_origin, layout, state, cx);
- cx.pop_theme();
- }
-}
-
-fn preferred_theme<V: 'static>(cx: &AppContext) -> Theme {
- settings::get::<ThemeSettings>(cx)
- .theme
- .deserialized_base_theme
- .lock()
- .get_or_insert_with(|| {
- let theme: Theme =
- serde_json::from_value(settings::get::<ThemeSettings>(cx).theme.base_theme.clone())
- .unwrap();
- Box::new(theme)
- })
- .downcast_ref::<Theme>()
- .unwrap()
- .clone()
-}
-
-pub fn theme(cx: &WindowContext) -> Arc<Theme> {
- cx.theme::<Theme>()
-}
@@ -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()
-}
@@ -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
@@ -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
}
@@ -16,6 +16,7 @@ pub struct GithubRelease {
pub pre_release: bool,
pub assets: Vec<GithubReleaseAsset>,
pub tarball_url: String,
+ pub zipball_url: String,
}
#[derive(Deserialize, Debug)]
@@ -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");
@@ -139,6 +140,12 @@ impl<P> PathLikeWithPosition<P> {
column: None,
})
} else {
+ let maybe_col_str =
+ if maybe_col_str.ends_with(FILE_ROW_COLUMN_DELIMITER) {
+ &maybe_col_str[..maybe_col_str.len() - 1]
+ } else {
+ maybe_col_str
+ };
match maybe_col_str.parse::<u32>() {
Ok(col) => Ok(Self {
path_like: parse_path_like_str(path_like_str)?,
@@ -241,7 +248,6 @@ mod tests {
"test_file.rs:1::",
"test_file.rs::1:2",
"test_file.rs:1::2",
- "test_file.rs:1:2:",
"test_file.rs:1:2:3",
] {
let actual = parse_str(input);
@@ -277,6 +283,14 @@ mod tests {
column: None,
},
),
+ (
+ "crates/file_finder/src/file_finder.rs:1902:13:",
+ PathLikeWithPosition {
+ path_like: "crates/file_finder/src/file_finder.rs".to_string(),
+ row: Some(1902),
+ column: Some(13),
+ },
+ ),
];
for (input, expected) in input_and_expected {
@@ -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| {
@@ -308,6 +310,13 @@ fn insert_line_below(_: &mut Workspace, _: &InsertLineBelow, cx: &mut ViewContex
});
}
+fn yank_line(_: &mut Workspace, _: &YankLine, cx: &mut ViewContext<Workspace>) {
+ 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<str>, cx: &mut WindowContext) {
Vim::update(cx, |vim, cx| {
vim.stop_recording();
@@ -652,3 +652,28 @@ async fn test_selection_goal(cx: &mut gpui::TestAppContext) {
Lorem Ipsum"})
.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;
+}
@@ -33,7 +33,7 @@ use workspace::{self, Workspace};
use crate::state::ReplayableAction;
-struct VimModeSetting(bool);
+pub struct VimModeSetting(pub bool);
#[derive(Clone, Deserialize, PartialEq)]
pub struct SwitchMode(pub Mode);
@@ -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"}}
@@ -25,6 +25,7 @@ theme_selector = { path = "../theme_selector" }
util = { path = "../util" }
picker = { path = "../picker" }
workspace = { path = "../workspace" }
+vim = { path = "../vim" }
anyhow.workspace = true
log.workspace = true
@@ -10,6 +10,7 @@ use gpui::{
};
use settings::{update_settings_file, SettingsStore};
use std::{borrow::Cow, sync::Arc};
+use vim::VimModeSetting;
use workspace::{
dock::DockPosition, item::Item, open_new, AppState, PaneBackdrop, Welcome, Workspace,
WorkspaceId,
@@ -65,6 +66,7 @@ impl View for WelcomePage {
let width = theme.welcome.page_width;
let telemetry_settings = *settings::get::<TelemetrySettings>(cx);
+ let vim_mode_setting = settings::get::<VimModeSetting>(cx).0;
enum Metrics {}
enum Diagnostics {}
@@ -144,6 +146,27 @@ impl View for WelcomePage {
)
.with_child(
Flex::column()
+ .with_child(
+ theme::ui::checkbox::<Diagnostics, Self, _>(
+ "Enable vim mode",
+ &theme.welcome.checkbox,
+ vim_mode_setting,
+ 0,
+ cx,
+ |this, checked, cx| {
+ if let Some(workspace) = this.workspace.upgrade(cx) {
+ let fs = workspace.read(cx).app_state().fs.clone();
+ update_settings_file::<VimModeSetting>(
+ fs,
+ cx,
+ move |setting| *setting = Some(checked),
+ )
+ }
+ },
+ )
+ .contained()
+ .with_style(theme.welcome.checkbox_container),
+ )
.with_child(
theme::ui::checkbox_with_label::<Metrics, _, Self, _>(
Flex::column()
@@ -186,7 +209,7 @@ impl View for WelcomePage {
"Send crash reports",
&theme.welcome.checkbox,
telemetry_settings.diagnostics,
- 0,
+ 1,
cx,
|this, checked, cx| {
if let Some(workspace) = this.workspace.upgrade(cx) {
@@ -22,7 +22,6 @@ test-support = [
db = { path = "../db" }
call = { path = "../call" }
client = { path = "../client" }
-channel = { path = "../channel" }
collections = { path = "../collections" }
context_menu = { path = "../context_menu" }
drag_and_drop = { path = "../drag_and_drop" }
@@ -31,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" }
@@ -1,10 +1,7 @@
-use std::{cell::RefCell, rc::Rc, sync::Arc};
-
-use crate::{
- pane_group::element::PaneAxisElement, AppState, FollowerStatesByLeader, Pane, Workspace,
-};
+use crate::{pane_group::element::PaneAxisElement, AppState, FollowerState, Pane, Workspace};
use anyhow::{anyhow, Result};
use call::{ActiveCall, ParticipantLocation};
+use collections::HashMap;
use gpui::{
elements::*,
geometry::{rect::RectF, vector::Vector2F},
@@ -13,6 +10,7 @@ use gpui::{
};
use project::Project;
use serde::Deserialize;
+use std::{cell::RefCell, rc::Rc, sync::Arc};
use theme::Theme;
const HANDLE_HITBOX_SIZE: f32 = 4.0;
@@ -95,7 +93,7 @@ impl PaneGroup {
&self,
project: &ModelHandle<Project>,
theme: &Theme,
- follower_states: &FollowerStatesByLeader,
+ follower_states: &HashMap<ViewHandle<Pane>, FollowerState>,
active_call: Option<&ModelHandle<ActiveCall>>,
active_pane: &ViewHandle<Pane>,
zoomed: Option<&AnyViewHandle>,
@@ -162,7 +160,7 @@ impl Member {
project: &ModelHandle<Project>,
basis: usize,
theme: &Theme,
- follower_states: &FollowerStatesByLeader,
+ follower_states: &HashMap<ViewHandle<Pane>, FollowerState>,
active_call: Option<&ModelHandle<ActiveCall>>,
active_pane: &ViewHandle<Pane>,
zoomed: Option<&AnyViewHandle>,
@@ -179,19 +177,10 @@ impl Member {
ChildView::new(pane, cx).into_any()
};
- let leader = follower_states
- .iter()
- .find_map(|(leader_id, follower_states)| {
- if follower_states.contains_key(pane) {
- Some(leader_id)
- } else {
- None
- }
- })
- .and_then(|leader_id| {
- let room = active_call?.read(cx).room()?.read(cx);
- room.remote_participant_for_peer_id(*leader_id)
- });
+ let leader = follower_states.get(pane).and_then(|state| {
+ let room = active_call?.read(cx).room()?.read(cx);
+ room.remote_participant_for_peer_id(state.leader_id)
+ });
let mut leader_border = Border::default();
let mut leader_status_box = None;
@@ -486,7 +475,7 @@ impl PaneAxis {
project: &ModelHandle<Project>,
basis: usize,
theme: &Theme,
- follower_state: &FollowerStatesByLeader,
+ follower_states: &HashMap<ViewHandle<Pane>, FollowerState>,
active_call: Option<&ModelHandle<ActiveCall>>,
active_pane: &ViewHandle<Pane>,
zoomed: Option<&AnyViewHandle>,
@@ -515,7 +504,7 @@ impl PaneAxis {
project,
(basis + ix) * 10,
theme,
- follower_state,
+ follower_states,
active_call,
active_pane,
zoomed,
@@ -12,10 +12,9 @@ mod workspace_settings;
use anyhow::{anyhow, Context, Result};
use call::ActiveCall;
-use channel::ChannelStore;
use client::{
proto::{self, PeerId},
- Client, TypedEnvelope, UserStore,
+ Client, Status, TypedEnvelope, UserStore,
};
use collections::{hash_map, HashMap, HashSet};
use drag_and_drop::DragAndDrop;
@@ -36,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,
@@ -450,7 +450,6 @@ pub struct AppState {
pub languages: Arc<LanguageRegistry>,
pub client: Arc<Client>,
pub user_store: ModelHandle<UserStore>,
- pub channel_store: ModelHandle<ChannelStore>,
pub workspace_store: ModelHandle<WorkspaceStore>,
pub fs: Arc<dyn fs::Fs>,
pub build_window_options:
@@ -458,6 +457,7 @@ pub struct AppState {
pub initialize_workspace:
fn(WeakViewHandle<Workspace>, bool, Arc<AppState>, AsyncAppContext) -> Task<Result<()>>,
pub background_actions: BackgroundActions,
+ pub node_runtime: Arc<dyn NodeRuntime>,
}
pub struct WorkspaceStore {
@@ -476,6 +476,7 @@ struct Follower {
impl AppState {
#[cfg(any(test, feature = "test-support"))]
pub fn test(cx: &mut AppContext) -> Arc<Self> {
+ use node_runtime::FakeNodeRuntime;
use settings::SettingsStore;
if !cx.has_global::<SettingsStore>() {
@@ -487,8 +488,6 @@ impl AppState {
let http_client = util::http::FakeHttpClient::with_404_response();
let client = Client::new(http_client.clone(), cx);
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
- let channel_store =
- cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx));
let workspace_store = cx.add_model(|cx| WorkspaceStore::new(client.clone(), cx));
theme::init((), cx);
@@ -500,8 +499,9 @@ impl AppState {
fs,
languages,
user_store,
- channel_store,
+ // channel_store,
workspace_store,
+ node_runtime: FakeNodeRuntime::new(),
initialize_workspace: |_, _, _, _| Task::ready(Ok(())),
build_window_options: |_, _, _| Default::default(),
background_actions: || &[],
@@ -578,7 +578,7 @@ pub struct Workspace {
titlebar_item: Option<AnyViewHandle>,
notifications: Vec<(TypeId, usize, Box<dyn NotificationHandle>)>,
project: ModelHandle<Project>,
- follower_states_by_leader: FollowerStatesByLeader,
+ follower_states: HashMap<ViewHandle<Pane>, FollowerState>,
last_leaders_by_pane: HashMap<WeakViewHandle<Pane>, PeerId>,
window_edited: bool,
active_call: Option<(ModelHandle<ActiveCall>, Vec<Subscription>)>,
@@ -603,10 +603,9 @@ pub struct ViewId {
pub id: u64,
}
-type FollowerStatesByLeader = HashMap<PeerId, HashMap<ViewHandle<Pane>, FollowerState>>;
-
#[derive(Default)]
struct FollowerState {
+ leader_id: PeerId,
active_view_id: Option<ViewId>,
items_by_leader_view_id: HashMap<ViewId, Box<dyn FollowableItemHandle>>,
}
@@ -795,7 +794,7 @@ impl Workspace {
bottom_dock,
right_dock,
project: project.clone(),
- follower_states_by_leader: Default::default(),
+ follower_states: Default::default(),
last_leaders_by_pane: Default::default(),
window_edited: false,
active_call,
@@ -821,6 +820,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(),
@@ -2513,13 +2513,16 @@ impl Workspace {
}
fn collaborator_left(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
- if let Some(states_by_pane) = self.follower_states_by_leader.remove(&peer_id) {
- for state in states_by_pane.into_values() {
- for item in state.items_by_leader_view_id.into_values() {
+ self.follower_states.retain(|_, state| {
+ if state.leader_id == peer_id {
+ for item in state.items_by_leader_view_id.values() {
item.set_leader_peer_id(None, cx);
}
+ false
+ } else {
+ true
}
- }
+ });
cx.notify();
}
@@ -2532,10 +2535,15 @@ impl Workspace {
self.last_leaders_by_pane
.insert(pane.downgrade(), leader_id);
- self.follower_states_by_leader
- .entry(leader_id)
- .or_default()
- .insert(pane.clone(), Default::default());
+ self.unfollow(&pane, cx);
+ self.follower_states.insert(
+ pane.clone(),
+ FollowerState {
+ leader_id,
+ active_view_id: None,
+ items_by_leader_view_id: Default::default(),
+ },
+ );
cx.notify();
let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
@@ -2550,9 +2558,8 @@ impl Workspace {
let response = request.await?;
this.update(&mut cx, |this, _| {
let state = this
- .follower_states_by_leader
- .get_mut(&leader_id)
- .and_then(|states_by_pane| states_by_pane.get_mut(&pane))
+ .follower_states
+ .get_mut(&pane)
.ok_or_else(|| anyhow!("following interrupted"))?;
state.active_view_id = if let Some(active_view_id) = response.active_view_id {
Some(ViewId::from_proto(active_view_id)?)
@@ -2647,12 +2654,10 @@ impl Workspace {
}
// if you're already following, find the right pane and focus it.
- for (existing_leader_id, states_by_pane) in &mut self.follower_states_by_leader {
- if leader_id == *existing_leader_id {
- for (pane, _) in states_by_pane {
- cx.focus(pane);
- return None;
- }
+ for (pane, state) in &self.follower_states {
+ if leader_id == state.leader_id {
+ cx.focus(pane);
+ return None;
}
}
@@ -2665,36 +2670,37 @@ impl Workspace {
pane: &ViewHandle<Pane>,
cx: &mut ViewContext<Self>,
) -> Option<PeerId> {
- for (leader_id, states_by_pane) in &mut self.follower_states_by_leader {
- let leader_id = *leader_id;
- if let Some(state) = states_by_pane.remove(pane) {
- for (_, item) in state.items_by_leader_view_id {
- item.set_leader_peer_id(None, cx);
- }
-
- if states_by_pane.is_empty() {
- self.follower_states_by_leader.remove(&leader_id);
- let project_id = self.project.read(cx).remote_id();
- let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
- self.app_state
- .client
- .send(proto::Unfollow {
- room_id,
- project_id,
- leader_id: Some(leader_id),
- })
- .log_err();
- }
+ let state = self.follower_states.remove(pane)?;
+ let leader_id = state.leader_id;
+ for (_, item) in state.items_by_leader_view_id {
+ item.set_leader_peer_id(None, cx);
+ }
- cx.notify();
- return Some(leader_id);
- }
+ if self
+ .follower_states
+ .values()
+ .all(|state| state.leader_id != state.leader_id)
+ {
+ let project_id = self.project.read(cx).remote_id();
+ let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
+ self.app_state
+ .client
+ .send(proto::Unfollow {
+ room_id,
+ project_id,
+ leader_id: Some(leader_id),
+ })
+ .log_err();
}
- None
+
+ cx.notify();
+ Some(leader_id)
}
pub fn is_being_followed(&self, peer_id: PeerId) -> bool {
- self.follower_states_by_leader.contains_key(&peer_id)
+ self.follower_states
+ .values()
+ .any(|state| state.leader_id == peer_id)
}
fn render_titlebar(&self, theme: &Theme, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
@@ -2877,8 +2883,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;
@@ -2917,8 +2922,8 @@ impl Workspace {
match update.variant.ok_or_else(|| anyhow!("invalid update"))? {
proto::update_followers::Variant::UpdateActiveView(update_active_view) => {
this.update(cx, |this, _| {
- if let Some(state) = this.follower_states_by_leader.get_mut(&leader_id) {
- for state in state.values_mut() {
+ for (_, state) in &mut this.follower_states {
+ if state.leader_id == leader_id {
state.active_view_id =
if let Some(active_view_id) = update_active_view.id.clone() {
Some(ViewId::from_proto(active_view_id)?)
@@ -2940,8 +2945,8 @@ impl Workspace {
let mut tasks = Vec::new();
this.update(cx, |this, cx| {
let project = this.project.clone();
- if let Some(state) = this.follower_states_by_leader.get_mut(&leader_id) {
- for state in state.values_mut() {
+ for (_, state) in &mut this.follower_states {
+ if state.leader_id == leader_id {
let view_id = ViewId::from_proto(id.clone())?;
if let Some(item) = state.items_by_leader_view_id.get(&view_id) {
tasks.push(item.apply_update_proto(&project, variant.clone(), cx));
@@ -2954,10 +2959,9 @@ impl Workspace {
}
proto::update_followers::Variant::CreateView(view) => {
let panes = this.read_with(cx, |this, _| {
- this.follower_states_by_leader
- .get(&leader_id)
- .into_iter()
- .flat_map(|states_by_pane| states_by_pane.keys())
+ this.follower_states
+ .iter()
+ .filter_map(|(pane, state)| (state.leader_id == leader_id).then_some(pane))
.cloned()
.collect()
})?;
@@ -3016,11 +3020,7 @@ impl Workspace {
for (pane, (item_tasks, leader_view_ids)) in item_tasks_by_pane {
let items = futures::future::try_join_all(item_tasks).await?;
this.update(cx, |this, cx| {
- let state = this
- .follower_states_by_leader
- .get_mut(&leader_id)?
- .get_mut(&pane)?;
-
+ let state = this.follower_states.get_mut(&pane)?;
for (id, item) in leader_view_ids.into_iter().zip(items) {
item.set_leader_peer_id(Some(leader_id), cx);
state.items_by_leader_view_id.insert(id, item);
@@ -3077,15 +3077,7 @@ impl Workspace {
}
pub fn leader_for_pane(&self, pane: &ViewHandle<Pane>) -> Option<PeerId> {
- self.follower_states_by_leader
- .iter()
- .find_map(|(leader_id, state)| {
- if state.contains_key(pane) {
- Some(*leader_id)
- } else {
- None
- }
- })
+ self.follower_states.get(pane).map(|state| state.leader_id)
}
fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) -> Option<()> {
@@ -3113,17 +3105,23 @@ impl Workspace {
}
};
- for (pane, state) in self.follower_states_by_leader.get(&leader_id)? {
- if leader_in_this_app {
- let item = state
- .active_view_id
- .and_then(|id| state.items_by_leader_view_id.get(&id));
- if let Some(item) = item {
+ for (pane, state) in &self.follower_states {
+ if state.leader_id != leader_id {
+ continue;
+ }
+ if let (Some(active_view_id), true) = (state.active_view_id, leader_in_this_app) {
+ if let Some(item) = state.items_by_leader_view_id.get(&active_view_id) {
if leader_in_this_project || !item.is_project_item(cx) {
items_to_activate.push((pane.clone(), item.boxed_clone()));
}
- continue;
+ } else {
+ log::warn!(
+ "unknown view id {:?} for leader {:?}",
+ active_view_id,
+ leader_id
+ );
}
+ continue;
}
if let Some(shared_screen) = self.shared_screen_for_peer(leader_id, pane, cx) {
items_to_activate.push((pane.clone(), Box::new(shared_screen)));
@@ -3524,22 +3522,22 @@ impl Workspace {
#[cfg(any(test, feature = "test-support"))]
pub fn test_new(project: ModelHandle<Project>, cx: &mut ViewContext<Self>) -> Self {
+ use node_runtime::FakeNodeRuntime;
+
let client = project.read(cx).client();
let user_store = project.read(cx).user_store();
- let channel_store =
- cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx));
let workspace_store = cx.add_model(|cx| WorkspaceStore::new(client.clone(), cx));
let app_state = Arc::new(AppState {
languages: project.read(cx).languages().clone(),
workspace_store,
client,
user_store,
- channel_store,
fs: project.read(cx).fs().clone(),
build_window_options: |_, _, _| Default::default(),
initialize_workspace: |_, _, _, _| Task::ready(Ok(())),
background_actions: || &[],
+ node_runtime: FakeNodeRuntime::new(),
});
Self::new(0, project, app_state, cx)
}
@@ -3811,7 +3809,7 @@ impl View for Workspace {
self.center.render(
&project,
&theme,
- &self.follower_states_by_leader,
+ &self.follower_states,
self.active_call(),
self.active_pane(),
self.zoomed
@@ -4148,6 +4146,188 @@ pub async fn last_opened_workspace_paths() -> Option<WorkspaceLocation> {
DB.last_workspace().await.log_err().flatten()
}
+async fn join_channel_internal(
+ channel_id: u64,
+ app_state: &Arc<AppState>,
+ requesting_window: Option<WindowHandle<Workspace>>,
+ active_call: &ModelHandle<ActiveCall>,
+ cx: &mut AsyncAppContext,
+) -> Result<bool> {
+ 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<AppState>,
+ requesting_window: Option<WindowHandle<Workspace>>,
+ cx: &mut AppContext,
+) -> Task<Result<()>> {
+ 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<AnyWindowHandle> {
+ for window in cx.windows() {
+ let found = window.update(cx, |cx| {
+ let is_workspace = cx.root_view().clone().downcast::<Workspace>().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],
@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathansobo@gmail.com>"]
description = "The fast, collaborative code editor."
edition = "2021"
name = "zed"
-version = "0.108.0"
+version = "0.109.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"]
@@ -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(),
@@ -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"))]
@@ -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<BundledFormatter> {
+ vec![BundledFormatter::prettier("css")]
+ }
}
async fn get_cached_server_binary(
@@ -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<BundledFormatter> {
+ vec![BundledFormatter::prettier("html")]
+ }
}
async fn get_cached_server_binary(
@@ -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<String, String> {
[("JSON".into(), "jsonc".into())].into_iter().collect()
}
+
+ fn enabled_formatters(&self) -> Vec<BundledFormatter> {
+ vec![BundledFormatter::prettier("json")]
+ }
}
async fn get_cached_server_binary(
@@ -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<BundledFormatter> {
+ vec![BundledFormatter::Prettier {
+ parser_name: Some("svelte"),
+ plugin_names: vec!["prettier-plugin-svelte"],
+ }]
+ }
}
async fn get_cached_server_binary(
@@ -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<BundledFormatter> {
+ vec![BundledFormatter::Prettier {
+ parser_name: None,
+ plugin_names: vec!["prettier-plugin-tailwindcss"],
+ }]
+ }
}
async fn get_cached_server_binary(
@@ -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<BundledFormatter> {
+ 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<serde_json::Value> {
None
}
+
+ fn enabled_formatters(&self) -> Vec<BundledFormatter> {
+ vec![BundledFormatter::prettier("babel")]
+ }
}
async fn get_cached_eslint_server_binary(
@@ -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<dyn NodeRuntime>,
+ typescript_install_path: Mutex<Option<PathBuf>>,
+}
+
+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<dyn NodeRuntime>) -> 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<Box<dyn 'static + Send + Any>> {
+ 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<Value> {
+ 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<Vec<CodeActionKind>> {
+ // 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<dyn 'static + Send + Any>,
+ container_dir: PathBuf,
+ _: &dyn LspAdapterDelegate,
+ ) -> Result<LanguageServerBinary> {
+ let version = version.downcast::<VueLspVersion>().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<LanguageServerBinary> {
+ 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<LanguageServerBinary> {
+ 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<language::Language>,
+ ) -> Option<language::CodeLabel> {
+ 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<OsString> {
+ vec![server_path.into(), "--stdio".into()]
+}
+
+type TypescriptPath = PathBuf;
+async fn get_cached_server_binary(
+ container_dir: PathBuf,
+ node: Arc<dyn NodeRuntime>,
+) -> 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()
+}
@@ -0,0 +1,2 @@
+("<" @open ">" @close)
+("\"" @open "\"" @close)
@@ -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 = ["-"]
@@ -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
@@ -0,0 +1,7 @@
+(script_element
+ (raw_text) @content
+ (#set! "language" "javascript"))
+
+(style_element
+ (raw_text) @content
+ (#set! "language" "css"))
@@ -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<BundledFormatter> {
+ vec![BundledFormatter::prettier("yaml")]
+ }
}
async fn get_cached_server_binary(
@@ -3,12 +3,13 @@
use anyhow::{anyhow, Context, Result};
use backtrace::Backtrace;
-use channel::ChannelStore;
use cli::{
ipc::{self, IpcSender},
CliRequest, CliResponse, IpcHandshake, FORCE_CLI_MODE_ENV_VAR_NAME,
};
-use client::{self, TelemetrySettings, UserStore, ZED_APP_VERSION, ZED_SECRET_CLIENT_TOKEN};
+use client::{
+ self, Client, TelemetrySettings, UserStore, ZED_APP_VERSION, ZED_SECRET_CLIENT_TOKEN,
+};
use db::kvp::KEY_VALUE_STORE;
use editor::{scroll::autoscroll::Autoscroll, Editor};
use futures::{
@@ -32,12 +33,10 @@ use std::{
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,
@@ -45,7 +44,7 @@ use std::{
};
use sum_tree::Bias;
use util::{
- channel::ReleaseChannel,
+ channel::{parse_zed_link, ReleaseChannel},
http::{self, HttpClient},
paths::PathLikeWithPosition,
};
@@ -61,6 +60,10 @@ use zed::{
only_instance::{ensure_only_instance, IsOnlyInstance},
};
+use crate::open_listener::{OpenListener, OpenRequest};
+
+mod open_listener;
+
fn main() {
let http = http::client();
init_paths();
@@ -74,7 +77,8 @@ fn main() {
let mut app = gpui::App::new(Assets).unwrap();
let installation_id = app.background().block(installation_id()).ok();
- init_panic_hook(&app, installation_id.clone());
+ let session_id = Uuid::new_v4().to_string();
+ init_panic_hook(&app, installation_id.clone(), session_id.clone());
load_embedded_fonts(&app);
@@ -92,29 +96,20 @@ 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::<Weak<AppState>>() {
- if let Some(app_state) = cx.global::<Weak<AppState>>().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::<Weak<AppState>>() {
+ if let Some(app_state) = cx.global::<Weak<AppState>>().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);
@@ -137,8 +132,6 @@ fn main() {
languages::init(languages.clone(), node_runtime.clone(), cx);
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http.clone(), cx));
- let channel_store =
- cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx));
let workspace_store = cx.add_model(|cx| WorkspaceStore::new(client.clone(), cx));
cx.set_global(client.clone());
@@ -155,13 +148,18 @@ fn main() {
outline::init(cx);
project_symbols::init(cx);
project_panel::init(Assets, cx);
- channel::init(&client);
+ channel::init(&client, user_store.clone(), cx);
diagnostics::init(cx);
search::init(cx);
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);
@@ -177,18 +175,18 @@ fn main() {
})
.detach();
- client.telemetry().start(installation_id, cx);
+ client.telemetry().start(installation_id, session_id, cx);
let app_state = Arc::new(AppState {
languages,
client: client.clone(),
user_store,
- channel_store,
fs,
build_window_options,
initialize_workspace,
background_actions,
workspace_store,
+ node_runtime,
});
cx.set_global(Arc::downgrade(&app_state));
@@ -213,12 +211,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);
@@ -226,61 +221,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<Client>, 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<String> {
let legacy_key_name = "device_id";
@@ -297,37 +316,6 @@ async fn installation_id() -> Result<String> {
}
}
-fn open_urls(
- urls: Vec<String>,
- cli_connections_tx: &mpsc::UnboundedSender<(
- mpsc::Receiver<CliRequest>,
- IpcSender<CliResponse>,
- )>,
- open_paths_tx: &mpsc::UnboundedSender<Vec<PathBuf>>,
-) {
- 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<AppState>, 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))
@@ -402,6 +390,7 @@ struct Panic {
panicked_on: u128,
#[serde(skip_serializing_if = "Option::is_none")]
installation_id: Option<String>,
+ session_id: String,
}
#[derive(Serialize)]
@@ -412,7 +401,7 @@ struct PanicRequest {
static PANIC_COUNT: AtomicU32 = AtomicU32::new(0);
-fn init_panic_hook(app: &App, installation_id: Option<String>) {
+fn init_panic_hook(app: &App, installation_id: Option<String>, session_id: String) {
let is_pty = stdout_is_a_pty();
let platform = app.platform();
@@ -477,7 +466,7 @@ fn init_panic_hook(app: &App, installation_id: Option<String>) {
line: location.line(),
}),
app_version: app_version.clone(),
- release_channel: RELEASE_CHANNEL.dev_name().into(),
+ release_channel: RELEASE_CHANNEL.display_name().into(),
os_name: platform.os_name().into(),
os_version: platform
.os_version()
@@ -490,13 +479,14 @@ fn init_panic_hook(app: &App, installation_id: Option<String>) {
.as_millis(),
backtrace,
installation_id: installation_id.clone(),
+ session_id: session_id.clone(),
};
- if is_pty {
- if let Some(panic_data_json) = serde_json::to_string_pretty(&panic_data).log_err() {
- eprintln!("{}", panic_data_json);
- }
- } else {
+ if let Some(panic_data_json) = serde_json::to_string_pretty(&panic_data).log_err() {
+ log::error!("{}", panic_data_json);
+ }
+
+ if !is_pty {
if let Some(panic_data_json) = serde_json::to_string(&panic_data).log_err() {
let timestamp = chrono::Utc::now().format("%Y_%m_%d %H_%M_%S").to_string();
let panic_file_path = paths::LOGS_DIR.join(format!("zed-{}.panic", timestamp));
@@ -635,23 +625,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<PathBuf> {
+fn collect_url_args() -> Vec<String> {
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<String> {
- env::args().skip(1).collect()
-}
-
fn load_embedded_fonts(app: &App) {
let font_paths = Assets.list("fonts");
let embedded_fonts = Mutex::new(Vec::new());
@@ -0,0 +1,98 @@
+use anyhow::anyhow;
+use cli::{ipc::IpcSender, CliRequest, CliResponse};
+use futures::channel::mpsc;
+use futures::channel::mpsc::{UnboundedReceiver, UnboundedSender};
+use std::ffi::OsStr;
+use std::os::unix::prelude::OsStrExt;
+use std::sync::atomic::Ordering;
+use std::{path::PathBuf, sync::atomic::AtomicBool};
+use util::channel::parse_zed_link;
+use util::ResultExt;
+
+use crate::connect_to_cli;
+
+pub enum OpenRequest {
+ Paths {
+ paths: Vec<PathBuf>,
+ },
+ CliConnection {
+ connection: (mpsc::Receiver<CliRequest>, IpcSender<CliResponse>),
+ },
+ JoinChannel {
+ channel_id: u64,
+ },
+}
+
+pub struct OpenListener {
+ tx: UnboundedSender<OpenRequest>,
+ pub triggered: AtomicBool,
+}
+
+impl OpenListener {
+ pub fn new() -> (Self, UnboundedReceiver<OpenRequest>) {
+ let (tx, rx) = mpsc::unbounded();
+ (
+ OpenListener {
+ tx,
+ triggered: AtomicBool::new(false),
+ },
+ rx,
+ )
+ }
+
+ pub fn open_urls(&self, urls: Vec<String>) {
+ 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<OpenRequest> {
+ 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<OpenRequest> {
+ 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::<u64>() {
+ return Some(OpenRequest::JoinChannel { channel_id });
+ }
+ }
+ }
+ }
+ log::error!("invalid zed url: {}", request_path);
+ None
+ }
+
+ fn handle_file_urls(&self, urls: Vec<String>) -> Option<OpenRequest> {
+ 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 })
+ }
+}
@@ -2424,6 +2424,7 @@ mod tests {
state.build_window_options = build_window_options;
theme::init((), cx);
audio::init((), cx);
+ channel::init(&app_state.client, app_state.user_store.clone(), cx);
call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
workspace::init(app_state.clone(), cx);
Project::init_settings(cx);
@@ -75,8 +75,7 @@ Expect this to take 30min to an hour! Some of these steps will take quite a whil
- If you are just using the latest version, but not working on zed:
- `cargo run --release`
- If you need to run the collaboration server locally:
- - `script/zed-with-local-servers`
- - If you need to test collaboration with mutl
+ - `script/zed-local`
## Troubleshooting
@@ -17,6 +17,6 @@
## Testing collab locally
1. Run `foreman start` from the root of the repo.
-1. In another terminal run `script/start-local-collaboration`.
+1. In another terminal run `script/zed-local -2`.
1. Two copies of Zed will open. Add yourself as a contact in the one that is not you.
1. Start a collaboration session as normal with any open project.
@@ -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,11 @@ 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
if [[ -n $MACOS_CERTIFICATE && -n $MACOS_CERTIFICATE_PASSWORD && -n $APPLE_NOTARIZATION_USERNAME && -n $APPLE_NOTARIZATION_PASSWORD ]]; then
echo "Signing bundle with Apple-issued certificate"
@@ -133,10 +148,12 @@ if [[ -n $MACOS_CERTIFICATE && -n $MACOS_CERTIFICATE_PASSWORD && -n $APPLE_NOTAR
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
+ echo "If you see 'The application cannot be opened for an unexpected reason,' you likely don't have the necessary entitlements to run the application in your signing keychain"
+ echo "You will need to download a new signing key from developer.apple.com, add it to keychain, and export MACOS_SIGNING_KEY=<email address of signing key>"
+ codesign --force --deep --entitlements crates/zed/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
@@ -0,0 +1,19 @@
+#!/bin/bash
+
+set -e
+
+if [[ -x cargo-depgraph ]]; then
+ cargo install cargo-depgraph
+fi
+
+graph_file=target/crate-graph.html
+
+cargo depgraph \
+ --workspace-only \
+ --offline \
+ --root=zed,cli,collab \
+ --dedup-transitive-deps \
+ | dot -Tsvg > $graph_file
+
+echo "open $graph_file"
+open $graph_file
@@ -1,59 +0,0 @@
-#!/bin/bash
-
-set -e
-
-if [[ -z "$GITHUB_TOKEN" ]]; then
- cat <<-MESSAGE
-Missing \`GITHUB_TOKEN\` environment variable. This token is needed
-for fetching your GitHub identity from the command-line.
-
-Create an access token here: https://github.com/settings/tokens
-Then edit your \`~/.zshrc\` (or other shell initialization script),
-adding a line like this:
-
- export GITHUB_TOKEN="(the token)"
-
-MESSAGE
- exit 1
-fi
-
-# Install jq if it's not installed
-if ! command -v jq &> /dev/null; then
- echo "Installing jq..."
- brew install jq
-fi
-
-# Start one Zed instance as the current user and a second instance with a different user.
-username_1=$(curl -sH "Authorization: bearer $GITHUB_TOKEN" https://api.github.com/user | jq -r .login)
-username_2=nathansobo
-if [[ $username_1 == $username_2 ]]; then
- username_2=as-cii
-fi
-
-# Make each Zed instance take up half of the screen.
-output=$(system_profiler SPDisplaysDataType -json)
-main_display=$(echo "$output" | jq '.SPDisplaysDataType[].spdisplays_ndrvs[] | select(.spdisplays_main == "spdisplays_yes")')
-resolution=$(echo "$main_display" | jq -r '._spdisplays_resolution')
-width=$(echo "$resolution" | jq -Rr 'match("(\\d+) x (\\d+)").captures[0].string')
-half_width=$(($width / 2))
-height=$(echo "$resolution" | jq -Rr 'match("(\\d+) x (\\d+)").captures[1].string')
-y=0
-
-position_1=0,${y}
-position_2=${half_width},${y}
-
-# Authenticate using the collab server's admin secret.
-export ZED_STATELESS=1
-export ZED_ALWAYS_ACTIVE=1
-export ZED_ADMIN_API_TOKEN=secret
-export ZED_SERVER_URL=http://localhost:8080
-export ZED_WINDOW_SIZE=${half_width},${height}
-
-cargo build
-sleep 0.5
-
-# Start the two Zed child processes. Open the given paths with the first instance.
-trap "trap - SIGTERM && kill -- -$$" SIGINT SIGTERM EXIT
-ZED_IMPERSONATE=${ZED_IMPERSONATE:=${username_1}} ZED_WINDOW_POSITION=${position_1} target/debug/Zed $@ &
-SECOND=true ZED_IMPERSONATE=${username_2} ZED_WINDOW_POSITION=${position_2} target/debug/Zed &
-wait
@@ -0,0 +1,88 @@
+#!/usr/bin/env node
+
+const {spawn, execFileSync} = require('child_process')
+
+const RESOLUTION_REGEX = /(\d+) x (\d+)/
+const DIGIT_FLAG_REGEX = /^--?(\d+)$/
+
+const args = process.argv.slice(2)
+
+// Parse the number of Zed instances to spawn.
+let instanceCount = 1
+const digitMatch = args[0]?.match(DIGIT_FLAG_REGEX)
+if (digitMatch) {
+ instanceCount = parseInt(digitMatch[1])
+ args.shift()
+}
+if (instanceCount > 4) {
+ throw new Error('Cannot spawn more than 4 instances')
+}
+
+// Parse the resolution of the main screen
+const displayInfo = JSON.parse(
+ execFileSync(
+ 'system_profiler',
+ ['SPDisplaysDataType', '-json'],
+ {encoding: 'utf8'}
+ )
+)
+const mainDisplayResolution = displayInfo
+ ?.SPDisplaysDataType[0]
+ ?.spdisplays_ndrvs
+ ?.find(entry => entry.spdisplays_main === "spdisplays_yes")
+ ?._spdisplays_resolution
+ ?.match(RESOLUTION_REGEX)
+if (!mainDisplayResolution) {
+ throw new Error('Could not parse screen resolution')
+}
+const screenWidth = parseInt(mainDisplayResolution[1])
+const screenHeight = parseInt(mainDisplayResolution[2])
+
+// Determine the window size for each instance
+let instanceWidth = screenWidth
+let instanceHeight = screenHeight
+if (instanceCount > 1) {
+ instanceWidth = Math.floor(screenWidth / 2)
+ if (instanceCount > 2) {
+ instanceHeight = Math.floor(screenHeight / 2)
+ }
+}
+
+let users = [
+ 'nathansobo',
+ 'as-cii',
+ 'maxbrunsfeld',
+ 'iamnbutler'
+]
+
+// If a user is specified, make sure it's first in the list
+const user = process.env.ZED_IMPERSONATE
+if (user) {
+ users = [user].concat(users.filter(u => u !== user))
+}
+
+const positions = [
+ '0,0',
+ `${instanceWidth},0`,
+ `0,${instanceHeight}`,
+ `${instanceWidth},${instanceHeight}`
+]
+
+execFileSync('cargo', ['build'], {stdio: 'inherit'})
+
+setTimeout(() => {
+ for (let i = 0; i < instanceCount; i++) {
+ spawn('target/debug/Zed', i == 0 ? args : [], {
+ stdio: 'inherit',
+ env: {
+ ZED_IMPERSONATE: users[i],
+ ZED_WINDOW_POSITION: positions[i],
+ ZED_STATELESS: '1',
+ ZED_ALWAYS_ACTIVE: '1',
+ ZED_SERVER_URL: 'http://localhost:8080',
+ ZED_ADMIN_API_TOKEN: 'secret',
+ ZED_WINDOW_SIZE: `${instanceWidth},${instanceHeight}`
+ }
+ })
+ }
+}, 0.1)
@@ -1,6 +0,0 @@
-#!/bin/bash
-
-: "${ZED_IMPERSONATE:=as-cii}"
-export ZED_IMPERSONATE
-
-ZED_ADMIN_API_TOKEN=secret ZED_SERVER_URL=http://localhost:8080 cargo run $@
@@ -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),