Compare commits

...

9 Commits

Author SHA1 Message Date
Antonio Scandurra
bdb1768c33 WIP 2025-04-08 16:33:59 -06:00
Kirill Bulatov
f7c3c533a3 Update task defaults (#28368)
Follow-up of https://github.com/zed-industries/zed/pull/28359

Release Notes:

- N/A
2025-04-08 22:20:00 +00:00
Nate Butler
c05bf096f8 Merge Component and ComponentPreview trait (#28365)
- Merge `Component` and `ComponentPreview` trait
- Adds a number of component previews
- Removes a number of stories

Release Notes:

- N/A
2025-04-08 16:09:06 -06:00
João Marcos
b15ee1b1cc Add dedicated actions for LSP completions insertion mode (#28121)
Adds actions so you can have customized keybindings for `insert` and
`replace` modes.

And add `shift-enter` as a default for `replace`, this will override the
default setting
`completions.lsp_insert_mode` which is set to `replace_suffix`, which
tries to "smartly"
decide whether to replace or insert based on the surrounding text.

For those who come from VSCode, if you want to mimic their behavior, you
only have to
set `completions.lsp_insert_mode` to `insert`.

If you want `tab` and `enter` to do different things, you need to remap
them, here is
an example:

```jsonc
[
  // ...
  {
    "context": "Editor && showing_completions",
    "bindings": {
      "enter": "editor::ConfirmCompletionInsert",
      "tab": "editor::ConfirmCompletionReplace"
    }
  },
]
```

Closes #24577

- [x] Make LSP completion insertion mode decision in guest's machine
(host is currently deciding it and not allowing guests to have their own
setting for it)
- [x] Add shift-enter as a hotkey for `replace` by default.
- [x] Test actions.
- [x] Respect the setting being specified per language, instead of using
the "defaults".
- [x] Move `insert_range` of `Completion` to the Lsp variant of
`.source`.
- [x] Fix broken default, forgotten after
https://github.com/zed-industries/zed/pull/27453#pullrequestreview-2736906628,
should be `replace_suffix` and not `insert`.

Release Notes:

- LSP completions: added actions `ConfirmCompletionInsert` and
`ConfirmCompletionReplace` that control how completions are inserted,
these override `completions.lsp_insert_mode`, by default, `shift-enter`
triggers `ConfirmCompletionReplace` which replaces the whole word.
2025-04-08 22:03:03 +00:00
Cole Miller
0459b1d303 Fix panic when a file in a path-based multibuffer excerpt is renamed (#28364)
Closes #ISSUE

Release Notes:

- Fixed a panic that could occur when paths changed in the project diff.

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-04-08 22:01:40 +00:00
5brian
246013cfc2 tab_switcher: Add keybind to close tab tooltip (#27212)
| prev | new |
|--|--|
|<img width="619" alt="image"
src="https://github.com/user-attachments/assets/53b14fd4-17ee-4336-81ca-30324d918e15"
/>|<img width="620" alt="image"
src="https://github.com/user-attachments/assets/316699b3-295b-4f83-9fb1-b799f7c71d7f"
/>|


Release Notes:

- N/A
2025-04-08 15:57:36 -06:00
Bennet Bo Fenner
47eaf274d6 agent: Only require confirmation for batch tool when subset of tool calls require confirmation (#28363)
Release Notes:

- agent: Only require confirmation for batch tool when subset of tool
calls require confirmation
2025-04-08 21:37:10 +00:00
Peter Tripp
ef4b5b0698 script: Ignore feature/meta issues from issue_response nag (#28332)
Release Notes:

- N/A
2025-04-08 17:14:07 -04:00
Kirill Bulatov
39c98ce882 Support tasks from rust-analyzer (#28359)
(and any other LSP server in theory, if it exposes any LSP-ext endpoint
for the same)

Closes https://github.com/zed-industries/zed/issues/16160

* adds a way to disable tree-sitter tasks (the ones from the plugins,
enabled by default) with
```json5
"languages": {
  "Rust": "tasks": {
      "enabled": false
    }
  }
}
```
language settings

* adds a way to disable LSP tasks (the ones from the rust-analyzer
language server, enabled by default) with
```json5
"lsp": {
  "rust-analyzer": {
    "enable_lsp_tasks": false,
  }
}
```

* adds rust-analyzer tasks into tasks modal and gutter:

<img width="1728" alt="modal"
src="https://github.com/user-attachments/assets/22b9cee1-4ffb-4c9e-b1f1-d01e80e72508"
/>

<img width="396" alt="gutter"
src="https://github.com/user-attachments/assets/bd818079-e247-4332-bdb5-1b7cb1cce768"
/>


Release Notes:

- Added tasks from rust-analyzer
2025-04-08 15:07:56 -06:00
114 changed files with 4729 additions and 2309 deletions

52
Cargo.lock generated
View File

@@ -3329,6 +3329,15 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
[[package]]
name = "convert_case"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca"
dependencies = [
"unicode-segmentation",
]
[[package]]
name = "convert_case"
version = "0.8.0"
@@ -4461,6 +4470,32 @@ dependencies = [
"workspace-hack",
]
[[package]]
name = "documented"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc6db32f0995bc4553d2de888999075acd0dbeef75ba923503f6a724263dc6f3"
dependencies = [
"documented-macros",
"phf",
"thiserror 1.0.69",
]
[[package]]
name = "documented-macros"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a394bb35929b58f9a5fd418f7c6b17a4b616efcc1e53e6995ca123948f87e5fa"
dependencies = [
"convert_case 0.6.0",
"itertools 0.13.0",
"optfield",
"proc-macro2",
"quote",
"strum",
"syn 2.0.100",
]
[[package]]
name = "dotenvy"
version = "0.15.7"
@@ -7875,7 +7910,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34"
dependencies = [
"cfg-if",
"windows-targets 0.48.5",
"windows-targets 0.52.6",
]
[[package]]
@@ -9542,6 +9577,17 @@ dependencies = [
"vcpkg",
]
[[package]]
name = "optfield"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa59f025cde9c698fcb4fcb3533db4621795374065bee908215263488f2d2a1d"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.100",
]
[[package]]
name = "option-ext"
version = "0.2.0"
@@ -14127,12 +14173,14 @@ name = "tasks_ui"
version = "0.1.0"
dependencies = [
"anyhow",
"collections",
"debugger_ui",
"editor",
"feature_flags",
"file_icons",
"fuzzy",
"gpui",
"itertools 0.14.0",
"language",
"menu",
"picker",
@@ -15347,6 +15395,7 @@ version = "0.1.0"
dependencies = [
"chrono",
"component",
"documented",
"gpui",
"icons",
"itertools 0.14.0",
@@ -17612,6 +17661,7 @@ dependencies = [
"indexmap",
"inout",
"itertools 0.12.1",
"itertools 0.13.0",
"lazy_static",
"libc",
"libsqlite3-sys",

View File

@@ -532,6 +532,7 @@
"context": "Editor && showing_completions",
"bindings": {
"enter": "editor::ConfirmCompletion",
"shift-enter": "editor::ConfirmCompletionReplace",
"tab": "editor::ComposeCompletion"
}
},

View File

@@ -681,6 +681,7 @@
"use_key_equivalents": true,
"bindings": {
"enter": "editor::ConfirmCompletion",
"shift-enter": "editor::ConfirmCompletionReplace",
"tab": "editor::ComposeCompletion"
}
},

View File

@@ -1136,7 +1136,8 @@
"code_actions_on_format": {},
// Settings related to running tasks.
"tasks": {
"variables": {}
"variables": {},
"enabled": true
},
// An object whose keys are language names, and whose values
// are arrays of filenames or extensions of files that should
@@ -1456,6 +1457,8 @@
"lsp": {
// Specify the LSP name as a key here.
// "rust-analyzer": {
// // A special flag for rust-analyzer integration, to use server-provided tasks
// enable_lsp_tasks": true,
// // These initialization options are merged into Zed's defaults
// "initialization_options": {
// "check": {

View File

@@ -106,7 +106,7 @@ impl ContextPickerCompletionProvider {
.iter()
.map(|mode| {
Completion {
old_range: source_range.clone(),
replace_range: source_range.clone(),
new_text: format!("@{} ", mode.mention_prefix()),
label: CodeLabel::plain(mode.label().to_string(), None),
icon_path: Some(mode.icon().path().into()),
@@ -160,7 +160,7 @@ impl ContextPickerCompletionProvider {
let new_text = MentionLink::for_thread(&thread_entry);
let new_text_len = new_text.len();
Completion {
old_range: source_range.clone(),
replace_range: source_range.clone(),
new_text,
label: CodeLabel::plain(thread_entry.summary.to_string(), None),
documentation: None,
@@ -205,7 +205,7 @@ impl ContextPickerCompletionProvider {
let new_text = MentionLink::for_fetch(&url_to_fetch);
let new_text_len = new_text.len();
Completion {
old_range: source_range.clone(),
replace_range: source_range.clone(),
new_text,
label: CodeLabel::plain(url_to_fetch.to_string(), None),
documentation: None,
@@ -287,7 +287,7 @@ impl ContextPickerCompletionProvider {
let new_text = MentionLink::for_file(&file_name, &full_path);
let new_text_len = new_text.len();
Completion {
old_range: source_range.clone(),
replace_range: source_range.clone(),
new_text,
label,
documentation: None,
@@ -350,7 +350,7 @@ impl ContextPickerCompletionProvider {
let new_text = MentionLink::for_symbol(&symbol.name, &full_path);
let new_text_len = new_text.len();
Some(Completion {
old_range: source_range.clone(),
replace_range: source_range.clone(),
new_text,
label,
documentation: None,

View File

@@ -1414,7 +1414,7 @@ impl Thread {
for tool_use in pending_tool_uses.iter() {
if let Some(tool) = self.tools.tool(&tool_use.name, cx) {
if tool.needs_confirmation()
if tool.needs_confirmation(&tool_use.input, cx)
&& !AssistantSettings::get_global(cx).always_allow_tool_actions
{
self.tool_use.confirm_tool_use(

View File

@@ -201,7 +201,7 @@ impl ToolUseState {
let (icon, needs_confirmation) = if let Some(tool) = self.tools.tool(&tool_use.name, cx)
{
(tool.icon(), tool.needs_confirmation())
(tool.icon(), tool.needs_confirmation(&tool_use.input, cx))
} else {
(IconName::Cog, false)
};

View File

@@ -120,7 +120,7 @@ impl SlashCommandCompletionProvider {
) as Arc<_>
});
Some(project::Completion {
old_range: name_range.clone(),
replace_range: name_range.clone(),
documentation: Some(CompletionDocumentation::SingleLine(
command.description().into(),
)),
@@ -219,7 +219,7 @@ impl SlashCommandCompletionProvider {
}
project::Completion {
old_range: if new_argument.replace_previous_arguments {
replace_range: if new_argument.replace_previous_arguments {
argument_range.clone()
} else {
last_argument_range.clone()

View File

@@ -48,7 +48,7 @@ pub trait Tool: 'static + Send + Sync {
/// Returns true iff the tool needs the users's confirmation
/// before having permission to run.
fn needs_confirmation(&self) -> bool;
fn needs_confirmation(&self, input: &serde_json::Value, cx: &App) -> bool;
/// Returns the JSON schema that describes the tool's input.
fn input_schema(&self, _: LanguageModelToolSchemaFormat) -> serde_json::Value {

View File

@@ -2,7 +2,7 @@ use crate::schema::json_schema_for;
use anyhow::{Context as _, Result, anyhow};
use assistant_tool::{ActionLog, Tool};
use futures::io::BufReader;
use futures::{AsyncBufReadExt, AsyncReadExt};
use futures::{AsyncBufReadExt, AsyncReadExt, FutureExt};
use gpui::{App, Entity, Task};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::Project;
@@ -29,7 +29,7 @@ impl Tool for BashTool {
"bash".to_string()
}
fn needs_confirmation(&self) -> bool {
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
true
}
@@ -123,7 +123,7 @@ impl Tool for BashTool {
worktree.read(cx).abs_path()
};
cx.spawn(async move |_| {
cx.spawn(async move |cx| {
// Add 2>&1 to merge stderr into stdout for proper interleaving.
let command = format!("({}) 2>&1", input.command);
@@ -158,14 +158,33 @@ impl Tool for BashTool {
let mut buffer = vec![0; LIMIT + 1];
let bytes_read = reader.read(&mut buffer).await?;
let mut timer = cx
.background_executor()
.timer(std::time::Duration::from_secs(10))
.fuse();
// Repeatedly fill the output reader's buffer without copying it.
loop {
let skipped_bytes = reader.fill_buf().await?;
if skipped_bytes.is_empty() {
break;
let mut skipped_bytes = reader.fill_buf().fuse();
futures::select! {
skipped_bytes = skipped_bytes => {
let skipped_bytes = skipped_bytes?;
if skipped_bytes.is_empty() {
break;
}
let skipped_bytes_len = skipped_bytes.len();
reader.consume_unpin(skipped_bytes_len);
timer = cx
.background_executor()
.timer(std::time::Duration::from_secs(10))
.fuse();
}
_ = timer => {
return Err(anyhow!("Command timed out. Output so far:\n{}", String::from_utf8_lossy(&buffer[..bytes_read])));
}
}
let skipped_bytes_len = skipped_bytes.len();
reader.consume_unpin(skipped_bytes_len);
}
let output_bytes = &buffer[..bytes_read];

View File

@@ -151,8 +151,17 @@ impl Tool for BatchTool {
"batch_tool".into()
}
fn needs_confirmation(&self) -> bool {
true
fn needs_confirmation(&self, input: &serde_json::Value, cx: &App) -> bool {
serde_json::from_value::<BatchToolInput>(input.clone())
.map(|input| {
let working_set = ToolWorkingSet::default();
input.invocations.iter().any(|invocation| {
working_set
.tool(&invocation.name, cx)
.map_or(false, |tool| tool.needs_confirmation(&invocation.input, cx))
})
})
.unwrap_or(false)
}
fn description(&self) -> String {

View File

@@ -79,7 +79,7 @@ impl Tool for CodeSymbolsTool {
"code_symbols".into()
}
fn needs_confirmation(&self) -> bool {
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
false
}

View File

@@ -43,7 +43,7 @@ impl Tool for CopyPathTool {
"copy_path".into()
}
fn needs_confirmation(&self) -> bool {
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
true
}

View File

@@ -33,7 +33,7 @@ impl Tool for CreateDirectoryTool {
"create_directory".into()
}
fn needs_confirmation(&self) -> bool {
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
true
}

View File

@@ -40,7 +40,7 @@ impl Tool for CreateFileTool {
"create_file".into()
}
fn needs_confirmation(&self) -> bool {
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
false
}

View File

@@ -33,7 +33,7 @@ impl Tool for DeletePathTool {
"delete_path".into()
}
fn needs_confirmation(&self) -> bool {
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
true
}

View File

@@ -46,7 +46,7 @@ impl Tool for DiagnosticsTool {
"diagnostics".into()
}
fn needs_confirmation(&self) -> bool {
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
false
}

View File

@@ -116,7 +116,7 @@ impl Tool for FetchTool {
"fetch".to_string()
}
fn needs_confirmation(&self) -> bool {
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
true
}

View File

@@ -129,7 +129,7 @@ impl Tool for FindReplaceFileTool {
"find_replace_file".into()
}
fn needs_confirmation(&self) -> bool {
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
false
}

View File

@@ -44,7 +44,7 @@ impl Tool for ListDirectoryTool {
"list_directory".into()
}
fn needs_confirmation(&self) -> bool {
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
false
}

View File

@@ -42,7 +42,7 @@ impl Tool for MovePathTool {
"move_path".into()
}
fn needs_confirmation(&self) -> bool {
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
true
}

View File

@@ -33,7 +33,7 @@ impl Tool for NowTool {
"now".into()
}
fn needs_confirmation(&self) -> bool {
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
false
}

View File

@@ -23,7 +23,7 @@ impl Tool for OpenTool {
"open".to_string()
}
fn needs_confirmation(&self) -> bool {
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
true
}

View File

@@ -41,7 +41,7 @@ impl Tool for PathSearchTool {
"path_search".into()
}
fn needs_confirmation(&self) -> bool {
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
false
}

View File

@@ -51,7 +51,7 @@ impl Tool for ReadFileTool {
"read_file".into()
}
fn needs_confirmation(&self) -> bool {
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
false
}

View File

@@ -44,7 +44,7 @@ impl Tool for RegexSearchTool {
"regex_search".into()
}
fn needs_confirmation(&self) -> bool {
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
false
}

View File

@@ -72,7 +72,7 @@ impl Tool for SymbolInfoTool {
"symbol_info".into()
}
fn needs_confirmation(&self) -> bool {
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
false
}

View File

@@ -24,7 +24,7 @@ impl Tool for ThinkingTool {
"thinking".to_string()
}
fn needs_confirmation(&self) -> bool {
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
false
}

View File

@@ -318,6 +318,7 @@ impl Server {
.add_request_handler(forward_read_only_project_request::<proto::OpenUncommittedDiff>)
.add_request_handler(forward_read_only_project_request::<proto::LspExtExpandMacro>)
.add_request_handler(forward_read_only_project_request::<proto::LspExtOpenDocs>)
.add_request_handler(forward_mutating_project_request::<proto::LspExtRunnables>)
.add_request_handler(
forward_read_only_project_request::<proto::LspExtSwitchSourceHeader>,
)

View File

@@ -309,7 +309,7 @@ impl MessageEditor {
.map(|mat| {
let (new_text, label) = completion_fn(&mat);
Completion {
old_range: range.clone(),
replace_range: range.clone(),
new_text,
label,
icon_path: None,

View File

@@ -3,37 +3,62 @@ use std::ops::{Deref, DerefMut};
use std::sync::LazyLock;
use collections::HashMap;
use gpui::{AnyElement, App, IntoElement, RenderOnce, SharedString, Window, div, prelude::*, px};
use gpui::{
AnyElement, App, IntoElement, RenderOnce, SharedString, Window, div, pattern_slash, prelude::*,
px, rems,
};
use linkme::distributed_slice;
use parking_lot::RwLock;
use theme::ActiveTheme;
pub trait Component {
fn scope() -> Option<ComponentScope>;
fn scope() -> ComponentScope {
ComponentScope::None
}
fn name() -> &'static str {
std::any::type_name::<Self>()
}
/// Returns a name that the component should be sorted by.
///
/// Implement this if the component should be sorted in an alternate order than its name.
///
/// Example:
///
/// For example, to group related components together when sorted:
///
/// - Button -> ButtonA
/// - IconButton -> ButtonBIcon
/// - ToggleButton -> ButtonCToggle
///
/// This naming scheme keeps these components together and allows them to /// be sorted in a logical order.
fn sort_name() -> &'static str {
Self::name()
}
fn description() -> Option<&'static str> {
None
}
}
pub trait ComponentPreview: Component {
fn preview(_window: &mut Window, _cx: &mut App) -> AnyElement;
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
None
}
}
#[distributed_slice]
pub static __ALL_COMPONENTS: [fn()] = [..];
#[distributed_slice]
pub static __ALL_PREVIEWS: [fn()] = [..];
pub static COMPONENT_DATA: LazyLock<RwLock<ComponentRegistry>> =
LazyLock::new(|| RwLock::new(ComponentRegistry::new()));
pub struct ComponentRegistry {
components: Vec<(Option<ComponentScope>, &'static str, Option<&'static str>)>,
previews: HashMap<&'static str, fn(&mut Window, &mut App) -> AnyElement>,
components: Vec<(
ComponentScope,
// name
&'static str,
// sort name
&'static str,
// description
Option<&'static str>,
)>,
previews: HashMap<&'static str, fn(&mut Window, &mut App) -> Option<AnyElement>>,
}
impl ComponentRegistry {
@@ -47,30 +72,16 @@ impl ComponentRegistry {
pub fn init() {
let component_fns: Vec<_> = __ALL_COMPONENTS.iter().cloned().collect();
let preview_fns: Vec<_> = __ALL_PREVIEWS.iter().cloned().collect();
for f in component_fns {
f();
}
for f in preview_fns {
f();
}
}
pub fn register_component<T: Component>() {
let component_data = (T::scope(), T::name(), T::description());
COMPONENT_DATA.write().components.push(component_data);
}
pub fn register_preview<T: ComponentPreview>() {
let preview_data = (
T::name(),
T::preview as fn(&mut Window, &mut App) -> AnyElement,
);
COMPONENT_DATA
.write()
.previews
.insert(preview_data.0, preview_data.1);
let component_data = (T::scope(), T::name(), T::sort_name(), T::description());
let mut data = COMPONENT_DATA.write();
data.components.push(component_data);
data.previews.insert(T::name(), T::preview);
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
@@ -80,29 +91,41 @@ pub struct ComponentId(pub &'static str);
pub struct ComponentMetadata {
id: ComponentId,
name: SharedString,
scope: Option<ComponentScope>,
sort_name: SharedString,
scope: ComponentScope,
description: Option<SharedString>,
preview: Option<fn(&mut Window, &mut App) -> AnyElement>,
preview: Option<fn(&mut Window, &mut App) -> Option<AnyElement>>,
}
impl ComponentMetadata {
pub fn id(&self) -> ComponentId {
self.id.clone()
}
pub fn name(&self) -> SharedString {
self.name.clone()
}
pub fn scope(&self) -> Option<ComponentScope> {
self.scope.clone()
pub fn sort_name(&self) -> SharedString {
self.sort_name.clone()
}
pub fn scopeless_name(&self) -> SharedString {
self.name
.clone()
.split("::")
.last()
.unwrap_or(&self.name)
.to_string()
.into()
}
pub fn scope(&self) -> ComponentScope {
self.scope.clone()
}
pub fn description(&self) -> Option<SharedString> {
self.description.clone()
}
pub fn preview(&self) -> Option<fn(&mut Window, &mut App) -> AnyElement> {
pub fn preview(&self) -> Option<fn(&mut Window, &mut App) -> Option<AnyElement>> {
self.preview
}
}
@@ -113,26 +136,18 @@ impl AllComponents {
pub fn new() -> Self {
AllComponents(HashMap::default())
}
/// Returns all components with previews
pub fn all_previews(&self) -> Vec<&ComponentMetadata> {
self.0.values().filter(|c| c.preview.is_some()).collect()
}
/// Returns all components with previews sorted by name
pub fn all_previews_sorted(&self) -> Vec<ComponentMetadata> {
let mut previews: Vec<ComponentMetadata> =
self.all_previews().into_iter().cloned().collect();
previews.sort_by_key(|a| a.name());
previews
}
/// Returns all components
pub fn all(&self) -> Vec<&ComponentMetadata> {
self.0.values().collect()
}
/// Returns all components sorted by name
pub fn all_sorted(&self) -> Vec<ComponentMetadata> {
let mut components: Vec<ComponentMetadata> = self.all().into_iter().cloned().collect();
components.sort_by_key(|a| a.name());
@@ -142,7 +157,6 @@ impl AllComponents {
impl Deref for AllComponents {
type Target = HashMap<ComponentId, ComponentMetadata>;
fn deref(&self) -> &Self::Target {
&self.0
}
@@ -157,139 +171,127 @@ impl DerefMut for AllComponents {
pub fn components() -> AllComponents {
let data = COMPONENT_DATA.read();
let mut all_components = AllComponents::new();
for (scope, name, description) in &data.components {
for (scope, name, sort_name, description) in &data.components {
let preview = data.previews.get(name).cloned();
let component_name = SharedString::new_static(name);
let sort_name = SharedString::new_static(sort_name);
let id = ComponentId(name);
all_components.insert(
id.clone(),
ComponentMetadata {
id,
name: component_name,
sort_name,
scope: scope.clone(),
description: description.map(Into::into),
preview,
},
);
}
all_components
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum ComponentScope {
Layout,
Input,
Notification,
Editor,
Collaboration,
DataDisplay,
Editor,
Images,
Input,
Layout,
Loading,
Navigation,
None,
Notification,
Overlays,
Status,
Typography,
VersionControl,
Unknown(SharedString),
}
impl Display for ComponentScope {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ComponentScope::Layout => write!(f, "Layout"),
ComponentScope::Input => write!(f, "Input"),
ComponentScope::Notification => write!(f, "Notification"),
ComponentScope::Editor => write!(f, "Editor"),
ComponentScope::Collaboration => write!(f, "Collaboration"),
ComponentScope::DataDisplay => write!(f, "Data Display"),
ComponentScope::Editor => write!(f, "Editor"),
ComponentScope::Images => write!(f, "Images & Icons"),
ComponentScope::Input => write!(f, "Forms & Input"),
ComponentScope::Layout => write!(f, "Layout & Structure"),
ComponentScope::Loading => write!(f, "Loading & Progress"),
ComponentScope::Navigation => write!(f, "Navigation"),
ComponentScope::None => write!(f, "Unsorted"),
ComponentScope::Notification => write!(f, "Notification"),
ComponentScope::Overlays => write!(f, "Overlays & Layering"),
ComponentScope::Status => write!(f, "Status"),
ComponentScope::Typography => write!(f, "Typography"),
ComponentScope::VersionControl => write!(f, "Version Control"),
ComponentScope::Unknown(name) => write!(f, "Unknown: {}", name),
}
}
}
impl From<&str> for ComponentScope {
fn from(value: &str) -> Self {
match value {
"Layout" => ComponentScope::Layout,
"Input" => ComponentScope::Input,
"Notification" => ComponentScope::Notification,
"Editor" => ComponentScope::Editor,
"Collaboration" => ComponentScope::Collaboration,
"Version Control" | "VersionControl" => ComponentScope::VersionControl,
_ => ComponentScope::Unknown(SharedString::new(value)),
}
}
}
impl From<String> for ComponentScope {
fn from(value: String) -> Self {
match value.as_str() {
"Layout" => ComponentScope::Layout,
"Input" => ComponentScope::Input,
"Notification" => ComponentScope::Notification,
"Editor" => ComponentScope::Editor,
"Collaboration" => ComponentScope::Collaboration,
"Version Control" | "VersionControl" => ComponentScope::VersionControl,
_ => ComponentScope::Unknown(SharedString::new(value)),
}
}
}
/// Which side of the preview to show labels on
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
pub enum ExampleLabelSide {
/// Left side
Left,
/// Right side
Right,
/// Top side
#[default]
Top,
/// Bottom side
Bottom,
}
/// A single example of a component.
#[derive(IntoElement)]
pub struct ComponentExample {
variant_name: SharedString,
element: AnyElement,
label_side: ExampleLabelSide,
grow: bool,
pub variant_name: SharedString,
pub description: Option<SharedString>,
pub element: AnyElement,
}
impl RenderOnce for ComponentExample {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let base = div().flex();
let base = match self.label_side {
ExampleLabelSide::Right => base.flex_row(),
ExampleLabelSide::Left => base.flex_row_reverse(),
ExampleLabelSide::Bottom => base.flex_col(),
ExampleLabelSide::Top => base.flex_col_reverse(),
};
base.gap_2()
.p_2()
.text_size(px(10.))
.text_color(cx.theme().colors().text_muted)
.when(self.grow, |this| this.flex_1())
.when(!self.grow, |this| this.flex_none())
.child(self.element)
.child(self.variant_name)
div()
.w_full()
.flex()
.flex_col()
.gap_3()
.child(
div()
.child(self.variant_name.clone())
.text_size(rems(1.25))
.text_color(cx.theme().colors().text),
)
.when_some(self.description, |this, description| {
this.child(
div()
.text_size(rems(0.9375))
.text_color(cx.theme().colors().text_muted)
.child(description.clone()),
)
})
.child(
div()
.flex()
.w_full()
.rounded_xl()
.min_h(px(100.))
.justify_center()
.p_8()
.border_1()
.border_color(cx.theme().colors().border)
.bg(pattern_slash(
cx.theme().colors().surface_background.opacity(0.5),
24.0,
24.0,
))
.shadow_sm()
.child(self.element),
)
.into_any_element()
}
}
impl ComponentExample {
/// Create a new example with the given variant name and example value.
pub fn new(variant_name: impl Into<SharedString>, element: AnyElement) -> Self {
Self {
variant_name: variant_name.into(),
element,
label_side: ExampleLabelSide::default(),
grow: false,
description: None,
}
}
/// Set the example to grow to fill the available horizontal space.
pub fn grow(mut self) -> Self {
self.grow = true;
pub fn description(mut self, description: impl Into<SharedString>) -> Self {
self.description = Some(description.into());
self
}
}
@@ -309,7 +311,7 @@ impl RenderOnce for ComponentExampleGroup {
.flex_col()
.text_sm()
.text_color(cx.theme().colors().text_muted)
.when(self.grow, |this| this.w_full().flex_1())
.w_full()
.when_some(self.title, |this, title| {
this.gap_4().child(
div()
@@ -336,7 +338,7 @@ impl RenderOnce for ComponentExampleGroup {
.child(
div()
.flex()
.when(self.vertical, |this| this.flex_col())
.flex_col()
.items_start()
.w_full()
.gap_6()
@@ -348,7 +350,6 @@ impl RenderOnce for ComponentExampleGroup {
}
impl ComponentExampleGroup {
/// Create a new group of examples with the given title.
pub fn new(examples: Vec<ComponentExample>) -> Self {
Self {
title: None,
@@ -357,8 +358,6 @@ impl ComponentExampleGroup {
vertical: false,
}
}
/// Create a new group of examples with the given title.
pub fn with_title(title: impl Into<SharedString>, examples: Vec<ComponentExample>) -> Self {
Self {
title: Some(title.into()),
@@ -367,21 +366,16 @@ impl ComponentExampleGroup {
vertical: false,
}
}
/// Set the group to grow to fill the available horizontal space.
pub fn grow(mut self) -> Self {
self.grow = true;
self
}
/// Lay the group out vertically.
pub fn vertical(mut self) -> Self {
self.vertical = true;
self
}
}
/// Create a single example
pub fn single_example(
variant_name: impl Into<SharedString>,
example: AnyElement,
@@ -389,12 +383,10 @@ pub fn single_example(
ComponentExample::new(variant_name, example)
}
/// Create a group of examples without a title
pub fn example_group(examples: Vec<ComponentExample>) -> ComponentExampleGroup {
ComponentExampleGroup::new(examples)
}
/// Create a group of examples with a title
pub fn example_group_with_title(
title: impl Into<SharedString>,
examples: Vec<ComponentExample>,

View File

@@ -43,6 +43,7 @@ pub fn init(app_state: Arc<AppState>, cx: &mut App) {
language_registry,
user_store,
None,
None,
cx,
)
});
@@ -106,10 +107,12 @@ impl ComponentPreview {
language_registry: Arc<LanguageRegistry>,
user_store: Entity<UserStore>,
selected_index: impl Into<Option<usize>>,
active_page: Option<PreviewPage>,
cx: &mut Context<Self>,
) -> Self {
let sorted_components = components().all_sorted();
let selected_index = selected_index.into().unwrap_or(0);
let active_page = active_page.unwrap_or(PreviewPage::AllComponents);
let component_list = ListState::new(
sorted_components.len(),
@@ -135,7 +138,7 @@ impl ComponentPreview {
language_registry,
user_store,
workspace,
active_page: PreviewPage::AllComponents,
active_page,
component_map: components().0,
components: sorted_components,
component_list,
@@ -169,8 +172,7 @@ impl ComponentPreview {
fn scope_ordered_entries(&self) -> Vec<PreviewEntry> {
use std::collections::HashMap;
let mut scope_groups: HashMap<Option<ComponentScope>, Vec<ComponentMetadata>> =
HashMap::default();
let mut scope_groups: HashMap<ComponentScope, Vec<ComponentMetadata>> = HashMap::default();
for component in &self.components {
scope_groups
@@ -192,6 +194,7 @@ impl ComponentPreview {
ComponentScope::Notification,
ComponentScope::Collaboration,
ComponentScope::VersionControl,
ComponentScope::None,
];
// Always show all components first
@@ -199,38 +202,27 @@ impl ComponentPreview {
entries.push(PreviewEntry::Separator);
for scope in known_scopes.iter() {
let scope_key = Some(scope.clone());
if let Some(components) = scope_groups.remove(&scope_key) {
if let Some(components) = scope_groups.remove(scope) {
if !components.is_empty() {
entries.push(PreviewEntry::SectionHeader(scope.to_string().into()));
let mut sorted_components = components;
sorted_components.sort_by_key(|component| component.sort_name());
for component in components {
for component in sorted_components {
entries.push(PreviewEntry::Component(component));
}
}
}
}
for (scope, components) in &scope_groups {
if let Some(ComponentScope::Unknown(_)) = scope {
if !components.is_empty() {
if let Some(scope_value) = scope {
entries.push(PreviewEntry::SectionHeader(scope_value.to_string().into()));
}
for component in components {
entries.push(PreviewEntry::Component(component.clone()));
}
}
}
}
if let Some(components) = scope_groups.get(&None) {
if let Some(components) = scope_groups.get(&ComponentScope::None) {
if !components.is_empty() {
entries.push(PreviewEntry::Separator);
entries.push(PreviewEntry::SectionHeader("Uncategorized".into()));
let mut sorted_components = components.clone();
sorted_components.sort_by_key(|c| c.sort_name());
for component in components {
for component in sorted_components {
entries.push(PreviewEntry::Component(component.clone()));
}
}
@@ -250,7 +242,10 @@ impl ComponentPreview {
let id = component_metadata.id();
let selected = self.active_page == PreviewPage::Component(id.clone());
ListItem::new(ix)
.child(Label::new(component_metadata.name().clone()).color(Color::Default))
.child(
Label::new(component_metadata.scopeless_name().clone())
.color(Color::Default),
)
.selectable(true)
.toggle_state(selected)
.inset(true)
@@ -333,7 +328,7 @@ impl ComponentPreview {
window: &mut Window,
cx: &mut App,
) -> impl IntoElement {
let name = component.name();
let name = component.scopeless_name();
let scope = component.scope();
let description = component.description();
@@ -354,13 +349,12 @@ impl ComponentPreview {
v_flex()
.gap_1()
.child(
h_flex()
.gap_1()
.text_xl()
.child(div().child(name))
.when_some(scope, |this, scope| {
h_flex().gap_1().text_xl().child(div().child(name)).when(
!matches!(scope, ComponentScope::None),
|this| {
this.child(div().opacity(0.5).child(format!("({})", scope)))
}),
},
),
)
.when_some(description, |this, description| {
this.child(
@@ -373,7 +367,7 @@ impl ComponentPreview {
}),
)
.when_some(component.preview(), |this, preview| {
this.child(preview(window, cx))
this.children(preview(window, cx))
}),
)
.into_any_element()
@@ -395,17 +389,16 @@ impl ComponentPreview {
fn render_component_page(
&mut self,
component_id: &ComponentId,
window: &mut Window,
cx: &mut Context<Self>,
_window: &mut Window,
_cx: &mut Context<Self>,
) -> impl IntoElement {
let component = self.component_map.get(&component_id);
if let Some(component) = component {
v_flex()
.w_full()
.flex_initial()
.min_h_full()
.child(self.render_preview(component, window, cx))
.id("render-component-page")
.size_full()
.child(ComponentPreviewPage::new(component.clone()))
.into_any_element()
} else {
v_flex()
@@ -445,10 +438,11 @@ impl Render for ComponentPreview {
.overflow_hidden()
.size_full()
.track_focus(&self.focus_handle)
.px_2()
.bg(cx.theme().colors().editor_background)
.child(
v_flex()
.border_r_1()
.border_color(cx.theme().colors().border)
.h_full()
.child(
uniform_list(
@@ -465,6 +459,7 @@ impl Render for ComponentPreview {
)
.track_scroll(self.nav_scroll_handle.clone())
.pt_4()
.px_4()
.w(px(240.))
.h_full()
.flex_1(),
@@ -527,6 +522,7 @@ impl Item for ComponentPreview {
let user_store = self.user_store.clone();
let weak_workspace = self.workspace.clone();
let selected_index = self.cursor_index;
let active_page = self.active_page.clone();
Some(cx.new(|cx| {
Self::new(
@@ -534,6 +530,7 @@ impl Item for ComponentPreview {
language_registry,
user_store,
selected_index,
Some(active_page),
cx,
)
}))
@@ -566,7 +563,14 @@ impl SerializableItem for ComponentPreview {
let weak_workspace = workspace.clone();
cx.update(|_, cx| {
Ok(cx.new(|cx| {
ComponentPreview::new(weak_workspace, language_registry, user_store, None, cx)
ComponentPreview::new(
weak_workspace,
language_registry,
user_store,
None,
None,
cx,
)
}))
})?
})
@@ -600,3 +604,76 @@ impl SerializableItem for ComponentPreview {
false
}
}
#[derive(IntoElement)]
pub struct ComponentPreviewPage {
// languages: Arc<LanguageRegistry>,
component: ComponentMetadata,
}
impl ComponentPreviewPage {
pub fn new(
component: ComponentMetadata,
// languages: Arc<LanguageRegistry>
) -> Self {
Self {
// languages,
component,
}
}
fn render_header(&self, _: &Window, cx: &App) -> impl IntoElement {
v_flex()
.px_12()
.pt_16()
.pb_12()
.gap_6()
.bg(cx.theme().colors().surface_background)
.border_b_1()
.border_color(cx.theme().colors().border)
.child(
v_flex()
.gap_0p5()
.child(
Label::new(self.component.scope().to_string())
.size(LabelSize::Small)
.color(Color::Muted),
)
.child(
Headline::new(self.component.scopeless_name()).size(HeadlineSize::XLarge),
),
)
.when_some(self.component.description(), |this, description| {
this.child(div().text_sm().child(description))
})
}
fn render_preview(&self, window: &mut Window, cx: &mut App) -> impl IntoElement {
v_flex()
.flex_1()
.px_12()
.py_6()
.bg(cx.theme().colors().editor_background)
.child(if let Some(preview) = self.component.preview() {
preview(window, cx).unwrap_or_else(|| {
div()
.child("Failed to load preview. This path should be unreachable")
.into_any_element()
})
} else {
div().child("No preview available").into_any_element()
})
}
}
impl RenderOnce for ComponentPreviewPage {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
v_flex()
.id("component-preview-page")
.overflow_y_scroll()
.overflow_x_hidden()
.w_full()
.child(self.render_header(window, cx))
.child(self.render_preview(window, cx))
}
}

View File

@@ -49,7 +49,7 @@ impl Tool for ContextServerTool {
}
}
fn needs_confirmation(&self) -> bool {
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
true
}

View File

@@ -356,7 +356,7 @@ impl ConsoleQueryBarCompletionProvider {
let variable_value = variables.get(&string_match.string)?;
Some(project::Completion {
old_range: buffer_position..buffer_position,
replace_range: buffer_position..buffer_position,
new_text: string_match.string.clone(),
label: CodeLabel {
filter_range: 0..string_match.string.len(),
@@ -428,10 +428,10 @@ impl ConsoleQueryBarCompletionProvider {
let buffer_offset = buffer_position.to_offset(&snapshot);
let start = buffer_offset - word_bytes_length;
let start = snapshot.anchor_before(start);
let old_range = start..buffer_position;
let replace_range = start..buffer_position;
project::Completion {
old_range,
replace_range,
new_text,
label: CodeLabel {
filter_range: 0..completion.label.len(),

View File

@@ -3,6 +3,7 @@ use super::*;
use gpui::{action_as, action_with_deprecated_aliases, actions};
use schemars::JsonSchema;
use util::serde::default_true;
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct SelectNext {
@@ -262,6 +263,8 @@ actions!(
Cancel,
CancelLanguageServerWork,
ConfirmRename,
ConfirmCompletionInsert,
ConfirmCompletionReplace,
ContextMenuFirst,
ContextMenuLast,
ContextMenuNext,

View File

@@ -230,7 +230,7 @@ impl CompletionsMenu {
let completions = choices
.iter()
.map(|choice| Completion {
old_range: selection.start.text_anchor..selection.end.text_anchor,
replace_range: selection.start.text_anchor..selection.end.text_anchor,
new_text: choice.to_string(),
label: CodeLabel {
text: choice.to_string(),

View File

@@ -109,8 +109,8 @@ use language::{
IndentKind, IndentSize, Language, OffsetRangeExt, Point, Selection, SelectionGoal, TextObject,
TransactionId, TreeSitterOptions, WordsQuery,
language_settings::{
self, InlayHintSettings, RewrapBehavior, WordsCompletionMode, all_language_settings,
language_settings,
self, InlayHintSettings, LspInsertMode, RewrapBehavior, WordsCompletionMode,
all_language_settings, language_settings,
},
point_from_lsp, text_diff_with_options,
};
@@ -131,7 +131,7 @@ pub use proposed_changes_editor::{
};
use smallvec::smallvec;
use std::{cell::OnceCell, iter::Peekable};
use task::{ResolvedTask, TaskTemplate, TaskVariables};
use task::{ResolvedTask, RunnableTag, TaskTemplate, TaskVariables};
pub use lsp::CompletionContext;
use lsp::{
@@ -140,6 +140,7 @@ use lsp::{
};
use language::BufferSnapshot;
pub use lsp_ext::lsp_tasks;
use movement::TextLayoutDetails;
pub use multi_buffer::{
Anchor, AnchorRangeExt, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, RowInfo,
@@ -4461,7 +4462,7 @@ impl Editor {
words.remove(&lsp_completion.new_text);
}
completions.extend(words.into_iter().map(|(word, word_range)| Completion {
old_range: old_range.clone(),
replace_range: old_range.clone(),
new_text: word.clone(),
label: CodeLabel::plain(word, None),
icon_path: None,
@@ -4568,6 +4569,26 @@ impl Editor {
self.do_completion(action.item_ix, CompletionIntent::Complete, window, cx)
}
pub fn confirm_completion_insert(
&mut self,
_: &ConfirmCompletionInsert,
window: &mut Window,
cx: &mut Context<Self>,
) -> Option<Task<Result<()>>> {
self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction);
self.do_completion(None, CompletionIntent::CompleteWithInsert, window, cx)
}
pub fn confirm_completion_replace(
&mut self,
_: &ConfirmCompletionReplace,
window: &mut Window,
cx: &mut Context<Self>,
) -> Option<Task<Result<()>>> {
self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction);
self.do_completion(None, CompletionIntent::CompleteWithReplace, window, cx)
}
pub fn compose_completion(
&mut self,
action: &ComposeCompletion,
@@ -4587,12 +4608,10 @@ impl Editor {
) -> Option<Task<Result<()>>> {
use language::ToOffset as _;
let completions_menu =
if let CodeContextMenu::Completions(menu) = self.hide_context_menu(window, cx)? {
menu
} else {
return None;
};
let CodeContextMenu::Completions(completions_menu) = self.hide_context_menu(window, cx)?
else {
return None;
};
let candidate_id = {
let entries = completions_menu.entries.borrow();
@@ -4621,9 +4640,12 @@ impl Editor {
new_text = completion.new_text.clone();
};
let selections = self.selections.all::<usize>(cx);
let replace_range = choose_completion_range(&completion, intent, &buffer_handle, cx);
let buffer = buffer_handle.read(cx);
let old_range = completion.old_range.to_offset(buffer);
let old_text = buffer.text_for_range(old_range.clone()).collect::<String>();
let old_text = buffer
.text_for_range(replace_range.clone())
.collect::<String>();
let newest_selection = self.selections.newest_anchor();
if newest_selection.start.buffer_id != Some(buffer_handle.read(cx).remote_id()) {
@@ -4634,8 +4656,8 @@ impl Editor {
.start
.text_anchor
.to_offset(buffer)
.saturating_sub(old_range.start);
let lookahead = old_range
.saturating_sub(replace_range.start);
let lookahead = replace_range
.end
.saturating_sub(newest_selection.end.text_anchor.to_offset(buffer));
let mut common_prefix_len = 0;
@@ -4664,8 +4686,8 @@ impl Editor {
ranges.clear();
ranges.extend(selections.iter().map(|s| {
if s.id == newest_selection.id {
range_to_replace = Some(old_range.clone());
old_range.clone()
range_to_replace = Some(replace_range.clone());
replace_range.clone()
} else {
s.start..s.end
}
@@ -12449,12 +12471,13 @@ impl Editor {
return Task::ready(());
}
let project = self.project.as_ref().map(Entity::downgrade);
cx.spawn_in(window, async move |this, cx| {
let task_sources = self.lsp_task_sources(cx);
cx.spawn_in(window, async move |editor, cx| {
cx.background_executor().timer(UPDATE_DEBOUNCE).await;
let Some(project) = project.and_then(|p| p.upgrade()) else {
return;
};
let Ok(display_snapshot) = this.update(cx, |this, cx| {
let Ok(display_snapshot) = editor.update(cx, |this, cx| {
this.display_map.update(cx, |map, cx| map.snapshot(cx))
}) else {
return;
@@ -12477,15 +12500,77 @@ impl Editor {
}
})
.await;
let Ok(lsp_tasks) =
cx.update(|_, cx| crate::lsp_tasks(project.clone(), &task_sources, None, cx))
else {
return;
};
let lsp_tasks = lsp_tasks.await;
let Ok(mut lsp_tasks_by_rows) = cx.update(|_, cx| {
lsp_tasks
.into_iter()
.flat_map(|(kind, tasks)| {
tasks.into_iter().filter_map(move |(location, task)| {
Some((kind.clone(), location?, task))
})
})
.fold(HashMap::default(), |mut acc, (kind, location, task)| {
let buffer = location.target.buffer;
let buffer_snapshot = buffer.read(cx).snapshot();
let offset = display_snapshot.buffer_snapshot.excerpts().find_map(
|(excerpt_id, snapshot, _)| {
if snapshot.remote_id() == buffer_snapshot.remote_id() {
display_snapshot
.buffer_snapshot
.anchor_in_excerpt(excerpt_id, location.target.range.start)
} else {
None
}
},
);
if let Some(offset) = offset {
let task_buffer_range =
location.target.range.to_point(&buffer_snapshot);
let context_buffer_range =
task_buffer_range.to_offset(&buffer_snapshot);
let context_range = BufferOffset(context_buffer_range.start)
..BufferOffset(context_buffer_range.end);
acc.entry((buffer_snapshot.remote_id(), task_buffer_range.start.row))
.or_insert_with(|| RunnableTasks {
templates: Vec::new(),
offset,
column: task_buffer_range.start.column,
extra_variables: HashMap::default(),
context_range,
})
.templates
.push((kind, task.original_task().clone()));
}
acc
})
}) else {
return;
};
let rows = Self::runnable_rows(project, display_snapshot, new_rows, cx.clone());
this.update(cx, |this, _| {
this.clear_tasks();
for (key, value) in rows {
this.insert_tasks(key, value);
}
})
.ok();
editor
.update(cx, |editor, _| {
editor.clear_tasks();
for (key, mut value) in rows {
if let Some(lsp_tasks) = lsp_tasks_by_rows.remove(&key) {
value.templates.extend(lsp_tasks.templates);
}
editor.insert_tasks(key, value);
}
for (key, value) in lsp_tasks_by_rows {
editor.insert_tasks(key, value);
}
})
.ok();
})
}
fn fetch_runnable_ranges(
@@ -12500,7 +12585,7 @@ impl Editor {
snapshot: DisplaySnapshot,
runnable_ranges: Vec<RunnableRange>,
mut cx: AsyncWindowContext,
) -> Vec<((BufferId, u32), RunnableTasks)> {
) -> Vec<((BufferId, BufferRow), RunnableTasks)> {
runnable_ranges
.into_iter()
.filter_map(|mut runnable| {
@@ -12557,11 +12642,9 @@ impl Editor {
)
});
let tags = mem::take(&mut runnable.tags);
let mut tags: Vec<_> = tags
let mut templates_with_tags = mem::take(&mut runnable.tags)
.into_iter()
.flat_map(|tag| {
let tag = tag.0.clone();
.flat_map(|RunnableTag(tag)| {
inventory
.as_ref()
.into_iter()
@@ -12578,20 +12661,20 @@ impl Editor {
})
})
.sorted_by_key(|(kind, _)| kind.to_owned())
.collect();
if let Some((leading_tag_source, _)) = tags.first() {
.collect::<Vec<_>>();
if let Some((leading_tag_source, _)) = templates_with_tags.first() {
// Strongest source wins; if we have worktree tag binding, prefer that to
// global and language bindings;
// if we have a global binding, prefer that to language binding.
let first_mismatch = tags
let first_mismatch = templates_with_tags
.iter()
.position(|(tag_source, _)| tag_source != leading_tag_source);
if let Some(index) = first_mismatch {
tags.truncate(index);
templates_with_tags.truncate(index);
}
}
tags
templates_with_tags
}
pub fn move_to_enclosing_bracket(
@@ -17918,6 +18001,81 @@ impl Editor {
}
}
// Consider user intent and default settings
fn choose_completion_range(
completion: &Completion,
intent: CompletionIntent,
buffer: &Entity<Buffer>,
cx: &mut Context<Editor>,
) -> Range<usize> {
fn should_replace(
completion: &Completion,
insert_range: &Range<text::Anchor>,
intent: CompletionIntent,
completion_mode_setting: LspInsertMode,
buffer: &Buffer,
) -> bool {
// specific actions take precedence over settings
match intent {
CompletionIntent::CompleteWithInsert => return false,
CompletionIntent::CompleteWithReplace => return true,
CompletionIntent::Complete | CompletionIntent::Compose => {}
}
match completion_mode_setting {
LspInsertMode::Insert => false,
LspInsertMode::Replace => true,
LspInsertMode::ReplaceSubsequence => {
let mut text_to_replace = buffer.chars_for_range(
buffer.anchor_before(completion.replace_range.start)
..buffer.anchor_after(completion.replace_range.end),
);
let mut completion_text = completion.new_text.chars();
// is `text_to_replace` a subsequence of `completion_text`
text_to_replace
.all(|needle_ch| completion_text.any(|haystack_ch| haystack_ch == needle_ch))
}
LspInsertMode::ReplaceSuffix => {
let range_after_cursor = insert_range.end..completion.replace_range.end;
let text_after_cursor = buffer
.text_for_range(
buffer.anchor_before(range_after_cursor.start)
..buffer.anchor_after(range_after_cursor.end),
)
.collect::<String>();
completion.new_text.ends_with(&text_after_cursor)
}
}
}
let buffer = buffer.read(cx);
if let CompletionSource::Lsp {
insert_range: Some(insert_range),
..
} = &completion.source
{
let completion_mode_setting =
language_settings(buffer.language().map(|l| l.name()), buffer.file(), cx)
.completions
.lsp_insert_mode;
if !should_replace(
completion,
&insert_range,
intent,
completion_mode_setting,
buffer,
) {
return insert_range.to_offset(buffer);
}
}
completion.replace_range.to_offset(buffer)
}
fn insert_extra_newline_brackets(
buffer: &MultiBufferSnapshot,
range: Range<usize>,
@@ -18639,9 +18797,10 @@ fn snippet_completions(
end: lsp_end,
};
Some(Completion {
old_range: range,
replace_range: range,
new_text: snippet.body.clone(),
source: CompletionSource::Lsp {
insert_range: None,
server_id: LanguageServerId(usize::MAX),
resolved: true,
lsp_completion: Box::new(lsp::CompletionItem {

View File

@@ -9218,7 +9218,7 @@ async fn test_completion_mode(cx: &mut TestAppContext) {
initial_state: String,
buffer_marked_text: String,
completion_text: &'static str,
expected_with_insertion_mode: String,
expected_with_insert_mode: String,
expected_with_replace_mode: String,
expected_with_replace_subsequence_mode: String,
expected_with_replace_suffix_mode: String,
@@ -9230,7 +9230,7 @@ async fn test_completion_mode(cx: &mut TestAppContext) {
initial_state: "before ediˇ after".into(),
buffer_marked_text: "before <edi|> after".into(),
completion_text: "editor",
expected_with_insertion_mode: "before editorˇ after".into(),
expected_with_insert_mode: "before editorˇ after".into(),
expected_with_replace_mode: "before editorˇ after".into(),
expected_with_replace_subsequence_mode: "before editorˇ after".into(),
expected_with_replace_suffix_mode: "before editorˇ after".into(),
@@ -9240,7 +9240,7 @@ async fn test_completion_mode(cx: &mut TestAppContext) {
initial_state: "before ediˇtor after".into(),
buffer_marked_text: "before <edi|tor> after".into(),
completion_text: "editor",
expected_with_insertion_mode: "before editorˇtor after".into(),
expected_with_insert_mode: "before editorˇtor after".into(),
expected_with_replace_mode: "before ediˇtor after".into(),
expected_with_replace_subsequence_mode: "before ediˇtor after".into(),
expected_with_replace_suffix_mode: "before ediˇtor after".into(),
@@ -9250,7 +9250,7 @@ async fn test_completion_mode(cx: &mut TestAppContext) {
initial_state: "before torˇ after".into(),
buffer_marked_text: "before <tor|> after".into(),
completion_text: "editor",
expected_with_insertion_mode: "before editorˇ after".into(),
expected_with_insert_mode: "before editorˇ after".into(),
expected_with_replace_mode: "before editorˇ after".into(),
expected_with_replace_subsequence_mode: "before editorˇ after".into(),
expected_with_replace_suffix_mode: "before editorˇ after".into(),
@@ -9260,7 +9260,7 @@ async fn test_completion_mode(cx: &mut TestAppContext) {
initial_state: "before ˇtor after".into(),
buffer_marked_text: "before <|tor> after".into(),
completion_text: "editor",
expected_with_insertion_mode: "before editorˇtor after".into(),
expected_with_insert_mode: "before editorˇtor after".into(),
expected_with_replace_mode: "before editorˇ after".into(),
expected_with_replace_subsequence_mode: "before editorˇ after".into(),
expected_with_replace_suffix_mode: "before editorˇ after".into(),
@@ -9270,7 +9270,7 @@ async fn test_completion_mode(cx: &mut TestAppContext) {
initial_state: "pˇfield: bool".into(),
buffer_marked_text: "<p|field>: bool".into(),
completion_text: "pub ",
expected_with_insertion_mode: "pub ˇfield: bool".into(),
expected_with_insert_mode: "pub ˇfield: bool".into(),
expected_with_replace_mode: "pub ˇ: bool".into(),
expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
@@ -9280,7 +9280,7 @@ async fn test_completion_mode(cx: &mut TestAppContext) {
initial_state: "[element_ˇelement_2]".into(),
buffer_marked_text: "[<element_|element_2>]".into(),
completion_text: "element_1",
expected_with_insertion_mode: "[element_1ˇelement_2]".into(),
expected_with_insert_mode: "[element_1ˇelement_2]".into(),
expected_with_replace_mode: "[element_1ˇ]".into(),
expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
@@ -9290,7 +9290,7 @@ async fn test_completion_mode(cx: &mut TestAppContext) {
initial_state: "[elˇelement]".into(),
buffer_marked_text: "[<el|element>]".into(),
completion_text: "element",
expected_with_insertion_mode: "[elementˇelement]".into(),
expected_with_insert_mode: "[elementˇelement]".into(),
expected_with_replace_mode: "[elˇement]".into(),
expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
expected_with_replace_suffix_mode: "[elˇement]".into(),
@@ -9300,7 +9300,7 @@ async fn test_completion_mode(cx: &mut TestAppContext) {
initial_state: "SubˇError".into(),
buffer_marked_text: "<Sub|Error>".into(),
completion_text: "SubscriptionError",
expected_with_insertion_mode: "SubscriptionErrorˇError".into(),
expected_with_insert_mode: "SubscriptionErrorˇError".into(),
expected_with_replace_mode: "SubscriptionErrorˇ".into(),
expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
@@ -9310,7 +9310,7 @@ async fn test_completion_mode(cx: &mut TestAppContext) {
initial_state: "SubˇErr".into(),
buffer_marked_text: "<Sub|Err>".into(),
completion_text: "SubscriptionError",
expected_with_insertion_mode: "SubscriptionErrorˇErr".into(),
expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
expected_with_replace_mode: "SubscriptionErrorˇ".into(),
expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
@@ -9320,7 +9320,7 @@ async fn test_completion_mode(cx: &mut TestAppContext) {
initial_state: "Suˇscrirr".into(),
buffer_marked_text: "<Su|scrirr>".into(),
completion_text: "SubscriptionError",
expected_with_insertion_mode: "SubscriptionErrorˇscrirr".into(),
expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
expected_with_replace_mode: "SubscriptionErrorˇ".into(),
expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
@@ -9330,7 +9330,7 @@ async fn test_completion_mode(cx: &mut TestAppContext) {
initial_state: "foo(indˇix)".into(),
buffer_marked_text: "foo(<ind|ix>)".into(),
completion_text: "node_index",
expected_with_insertion_mode: "foo(node_indexˇix)".into(),
expected_with_insert_mode: "foo(node_indexˇix)".into(),
expected_with_replace_mode: "foo(node_indexˇ)".into(),
expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
@@ -9339,7 +9339,7 @@ async fn test_completion_mode(cx: &mut TestAppContext) {
for run in runs {
let run_variations = [
(LspInsertMode::Insert, run.expected_with_insertion_mode),
(LspInsertMode::Insert, run.expected_with_insert_mode),
(LspInsertMode::Replace, run.expected_with_replace_mode),
(
LspInsertMode::ReplaceSubsequence,
@@ -9395,6 +9395,98 @@ async fn test_completion_mode(cx: &mut TestAppContext) {
}
}
#[gpui::test]
async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorLspTestContext::new_rust(
lsp::ServerCapabilities {
completion_provider: Some(lsp::CompletionOptions {
resolve_provider: Some(true),
..Default::default()
}),
..Default::default()
},
cx,
)
.await;
let initial_state = "SubˇError";
let buffer_marked_text = "<Sub|Error>";
let completion_text = "SubscriptionError";
let expected_with_insert_mode = "SubscriptionErrorˇError";
let expected_with_replace_mode = "SubscriptionErrorˇ";
update_test_language_settings(&mut cx, |settings| {
settings.defaults.completions = Some(CompletionSettings {
words: WordsCompletionMode::Disabled,
// set the opposite here to ensure that the action is overriding the default behavior
lsp_insert_mode: LspInsertMode::Insert,
lsp: true,
lsp_fetch_timeout_ms: 0,
});
});
cx.set_state(initial_state);
cx.update_editor(|editor, window, cx| {
editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
});
let counter = Arc::new(AtomicUsize::new(0));
handle_completion_request_with_insert_and_replace(
&mut cx,
&buffer_marked_text,
vec![completion_text],
counter.clone(),
)
.await;
cx.condition(|editor, _| editor.context_menu_visible())
.await;
assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
let apply_additional_edits = cx.update_editor(|editor, window, cx| {
editor
.confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
.unwrap()
});
cx.assert_editor_state(&expected_with_replace_mode);
handle_resolve_completion_request(&mut cx, None).await;
apply_additional_edits.await.unwrap();
update_test_language_settings(&mut cx, |settings| {
settings.defaults.completions = Some(CompletionSettings {
words: WordsCompletionMode::Disabled,
// set the opposite here to ensure that the action is overriding the default behavior
lsp_insert_mode: LspInsertMode::Replace,
lsp: true,
lsp_fetch_timeout_ms: 0,
});
});
cx.set_state(initial_state);
cx.update_editor(|editor, window, cx| {
editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
});
handle_completion_request_with_insert_and_replace(
&mut cx,
&buffer_marked_text,
vec![completion_text],
counter.clone(),
)
.await;
cx.condition(|editor, _| editor.context_menu_visible())
.await;
assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
let apply_additional_edits = cx.update_editor(|editor, window, cx| {
editor
.confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
.unwrap()
});
cx.assert_editor_state(&expected_with_insert_mode);
handle_resolve_completion_request(&mut cx, None).await;
apply_additional_edits.await.unwrap();
}
#[gpui::test]
async fn test_completion(cx: &mut TestAppContext) {
init_test(cx, |_| {});
@@ -12539,6 +12631,7 @@ async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppCon
initialization_options: Some(json!({
"some other init value": false
})),
enable_lsp_tasks: false,
},
);
});
@@ -12558,6 +12651,7 @@ async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppCon
initialization_options: Some(json!({
"anotherInitValue": false
})),
enable_lsp_tasks: false,
},
);
});
@@ -12577,6 +12671,7 @@ async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppCon
initialization_options: Some(json!({
"anotherInitValue": false
})),
enable_lsp_tasks: false,
},
);
});
@@ -12594,6 +12689,7 @@ async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppCon
binary: None,
settings: None,
initialization_options: None,
enable_lsp_tasks: false,
},
);
});

View File

@@ -461,6 +461,20 @@ impl EditorElement {
cx.propagate();
}
});
register_action(editor, window, |editor, action, window, cx| {
if let Some(task) = editor.confirm_completion_replace(action, window, cx) {
task.detach_and_notify_err(window, cx);
} else {
cx.propagate();
}
});
register_action(editor, window, |editor, action, window, cx| {
if let Some(task) = editor.confirm_completion_insert(action, window, cx) {
task.detach_and_notify_err(window, cx);
} else {
cx.propagate();
}
});
register_action(editor, window, |editor, action, window, cx| {
if let Some(task) = editor.compose_completion(action, window, cx) {
task.detach_and_notify_err(window, cx);

View File

@@ -1,12 +1,25 @@
use std::sync::Arc;
use crate::Editor;
use collections::HashMap;
use futures::stream::FuturesUnordered;
use gpui::{App, AppContext as _, Entity, Task};
use itertools::Itertools;
use language::Buffer;
use language::Language;
use lsp::LanguageServerId;
use lsp::LanguageServerName;
use multi_buffer::Anchor;
use project::LanguageServerToQuery;
use project::LocationLink;
use project::Project;
use project::TaskSourceKind;
use project::lsp_store::lsp_ext_command::GetLspRunnables;
use smol::stream::StreamExt;
use task::ResolvedTask;
use task::TaskContext;
use text::BufferId;
use util::ResultExt as _;
pub(crate) fn find_specific_language_server_in_selection<F>(
editor: &Editor,
@@ -60,3 +73,83 @@ where
None
})
}
pub fn lsp_tasks(
project: Entity<Project>,
task_sources: &HashMap<LanguageServerName, Vec<BufferId>>,
for_position: Option<text::Anchor>,
cx: &mut App,
) -> Task<Vec<(TaskSourceKind, Vec<(Option<LocationLink>, ResolvedTask)>)>> {
let mut lsp_task_sources = task_sources
.iter()
.map(|(name, buffer_ids)| {
let buffers = buffer_ids
.iter()
.filter_map(|&buffer_id| project.read(cx).buffer_for_id(buffer_id, cx))
.collect::<Vec<_>>();
language_server_for_buffers(project.clone(), name.clone(), buffers, cx)
})
.collect::<FuturesUnordered<_>>();
cx.spawn(async move |cx| {
let mut lsp_tasks = Vec::new();
let lsp_task_context = TaskContext::default();
while let Some(server_to_query) = lsp_task_sources.next().await {
if let Some((server_id, buffers)) = server_to_query {
let source_kind = TaskSourceKind::Lsp(server_id);
let id_base = source_kind.to_id_base();
let mut new_lsp_tasks = Vec::new();
for buffer in buffers {
if let Ok(runnables_task) = project.update(cx, |project, cx| {
let buffer_id = buffer.read(cx).remote_id();
project.request_lsp(
buffer,
LanguageServerToQuery::Other(server_id),
GetLspRunnables {
buffer_id,
position: for_position,
},
cx,
)
}) {
if let Some(new_runnables) = runnables_task.await.log_err() {
new_lsp_tasks.extend(new_runnables.runnables.into_iter().filter_map(
|(location, runnable)| {
let resolved_task =
runnable.resolve_task(&id_base, &lsp_task_context)?;
Some((location, resolved_task))
},
));
}
}
}
lsp_tasks.push((source_kind, new_lsp_tasks));
}
}
lsp_tasks
})
}
fn language_server_for_buffers(
project: Entity<Project>,
name: LanguageServerName,
candidates: Vec<Entity<Buffer>>,
cx: &mut App,
) -> Task<Option<(LanguageServerId, Vec<Entity<Buffer>>)>> {
cx.spawn(async move |cx| {
for buffer in &candidates {
let server_id = buffer
.update(cx, |buffer, cx| {
project.update(cx, |project, cx| {
project.language_server_id_for_name(buffer, &name.0, cx)
})
})
.ok()?
.await;
if let Some(server_id) = server_id {
return Some((server_id, candidates));
}
}
None
})
}

View File

@@ -1,9 +1,12 @@
use crate::Editor;
use collections::HashMap;
use gpui::{App, Task, Window};
use project::Location;
use lsp::LanguageServerName;
use project::{Location, project_settings::ProjectSettings};
use settings::Settings as _;
use task::{TaskContext, TaskVariables, VariableName};
use text::{ToOffset, ToPoint};
use text::{BufferId, ToOffset, ToPoint};
impl Editor {
pub fn task_context(&self, window: &mut Window, cx: &mut App) -> Task<Option<TaskContext>> {
@@ -70,4 +73,38 @@ impl Editor {
})
})
}
pub fn lsp_task_sources(&self, cx: &App) -> HashMap<LanguageServerName, Vec<BufferId>> {
let lsp_settings = &ProjectSettings::get_global(cx).lsp;
self.buffer()
.read(cx)
.all_buffers()
.into_iter()
.filter_map(|buffer| {
let lsp_tasks_source = buffer
.read(cx)
.language()?
.context_provider()?
.lsp_task_source()?;
if lsp_settings
.get(&lsp_tasks_source)
.map_or(true, |s| s.enable_lsp_tasks)
{
let buffer_id = buffer.read(cx).remote_id();
Some((lsp_tasks_source, buffer_id))
} else {
None
}
})
.fold(
HashMap::default(),
|mut acc, (lsp_task_source, buffer_id)| {
acc.entry(lsp_task_source)
.or_insert_with(Vec::new)
.push(buffer_id);
acc
},
)
}
}

View File

@@ -3953,8 +3953,7 @@ impl Render for GitPanelMessageTooltip {
}
}
#[derive(IntoElement, IntoComponent)]
#[component(scope = "Version Control")]
#[derive(IntoElement, RegisterComponent)]
pub struct PanelRepoFooter {
active_repository: SharedString,
branch: Option<Branch>,
@@ -4134,8 +4133,12 @@ impl RenderOnce for PanelRepoFooter {
}
}
impl ComponentPreview for PanelRepoFooter {
fn preview(_window: &mut Window, _cx: &mut App) -> AnyElement {
impl Component for PanelRepoFooter {
fn scope() -> ComponentScope {
ComponentScope::VersionControl
}
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
let unknown_upstream = None;
let no_remote_upstream = Some(UpstreamTracking::Gone);
let ahead_of_upstream = Some(
@@ -4207,192 +4210,180 @@ impl ComponentPreview for PanelRepoFooter {
}
let example_width = px(340.);
v_flex()
.gap_6()
.w_full()
.flex_none()
.children(vec![
example_group_with_title(
"Action Button States",
vec![
single_example(
"No Branch",
div()
.w(example_width)
.overflow_hidden()
.child(PanelRepoFooter::new_preview(
active_repository(1).clone(),
None,
))
.into_any_element(),
)
.grow(),
single_example(
"Remote status unknown",
div()
.w(example_width)
.overflow_hidden()
.child(PanelRepoFooter::new_preview(
active_repository(2).clone(),
Some(branch(unknown_upstream)),
))
.into_any_element(),
)
.grow(),
single_example(
"No Remote Upstream",
div()
.w(example_width)
.overflow_hidden()
.child(PanelRepoFooter::new_preview(
active_repository(3).clone(),
Some(branch(no_remote_upstream)),
))
.into_any_element(),
)
.grow(),
single_example(
"Not Ahead or Behind",
div()
.w(example_width)
.overflow_hidden()
.child(PanelRepoFooter::new_preview(
active_repository(4).clone(),
Some(branch(not_ahead_or_behind_upstream)),
))
.into_any_element(),
)
.grow(),
single_example(
"Behind remote",
div()
.w(example_width)
.overflow_hidden()
.child(PanelRepoFooter::new_preview(
active_repository(5).clone(),
Some(branch(behind_upstream)),
))
.into_any_element(),
)
.grow(),
single_example(
"Ahead of remote",
div()
.w(example_width)
.overflow_hidden()
.child(PanelRepoFooter::new_preview(
active_repository(6).clone(),
Some(branch(ahead_of_upstream)),
))
.into_any_element(),
)
.grow(),
single_example(
"Ahead and behind remote",
div()
.w(example_width)
.overflow_hidden()
.child(PanelRepoFooter::new_preview(
active_repository(7).clone(),
Some(branch(ahead_and_behind_upstream)),
))
.into_any_element(),
)
.grow(),
],
)
.grow()
.vertical(),
])
.children(vec![
example_group_with_title(
"Labels",
vec![
single_example(
"Short Branch & Repo",
div()
.w(example_width)
.overflow_hidden()
.child(PanelRepoFooter::new_preview(
SharedString::from("zed"),
Some(custom("main", behind_upstream)),
))
.into_any_element(),
)
.grow(),
single_example(
"Long Branch",
div()
.w(example_width)
.overflow_hidden()
.child(PanelRepoFooter::new_preview(
SharedString::from("zed"),
Some(custom(
"redesign-and-update-git-ui-list-entry-style",
behind_upstream,
)),
))
.into_any_element(),
)
.grow(),
single_example(
"Long Repo",
div()
.w(example_width)
.overflow_hidden()
.child(PanelRepoFooter::new_preview(
SharedString::from("zed-industries-community-examples"),
Some(custom("gpui", ahead_of_upstream)),
))
.into_any_element(),
)
.grow(),
single_example(
"Long Repo & Branch",
div()
.w(example_width)
.overflow_hidden()
.child(PanelRepoFooter::new_preview(
SharedString::from("zed-industries-community-examples"),
Some(custom(
"redesign-and-update-git-ui-list-entry-style",
behind_upstream,
)),
))
.into_any_element(),
)
.grow(),
single_example(
"Uppercase Repo",
div()
.w(example_width)
.overflow_hidden()
.child(PanelRepoFooter::new_preview(
SharedString::from("LICENSES"),
Some(custom("main", ahead_of_upstream)),
))
.into_any_element(),
)
.grow(),
single_example(
"Uppercase Branch",
div()
.w(example_width)
.overflow_hidden()
.child(PanelRepoFooter::new_preview(
SharedString::from("zed"),
Some(custom("update-README", behind_upstream)),
))
.into_any_element(),
)
.grow(),
],
)
.grow()
.vertical(),
])
.into_any_element()
Some(
v_flex()
.gap_6()
.w_full()
.flex_none()
.children(vec![
example_group_with_title(
"Action Button States",
vec![
single_example(
"No Branch",
div()
.w(example_width)
.overflow_hidden()
.child(PanelRepoFooter::new_preview(
active_repository(1).clone(),
None,
))
.into_any_element(),
),
single_example(
"Remote status unknown",
div()
.w(example_width)
.overflow_hidden()
.child(PanelRepoFooter::new_preview(
active_repository(2).clone(),
Some(branch(unknown_upstream)),
))
.into_any_element(),
),
single_example(
"No Remote Upstream",
div()
.w(example_width)
.overflow_hidden()
.child(PanelRepoFooter::new_preview(
active_repository(3).clone(),
Some(branch(no_remote_upstream)),
))
.into_any_element(),
),
single_example(
"Not Ahead or Behind",
div()
.w(example_width)
.overflow_hidden()
.child(PanelRepoFooter::new_preview(
active_repository(4).clone(),
Some(branch(not_ahead_or_behind_upstream)),
))
.into_any_element(),
),
single_example(
"Behind remote",
div()
.w(example_width)
.overflow_hidden()
.child(PanelRepoFooter::new_preview(
active_repository(5).clone(),
Some(branch(behind_upstream)),
))
.into_any_element(),
),
single_example(
"Ahead of remote",
div()
.w(example_width)
.overflow_hidden()
.child(PanelRepoFooter::new_preview(
active_repository(6).clone(),
Some(branch(ahead_of_upstream)),
))
.into_any_element(),
),
single_example(
"Ahead and behind remote",
div()
.w(example_width)
.overflow_hidden()
.child(PanelRepoFooter::new_preview(
active_repository(7).clone(),
Some(branch(ahead_and_behind_upstream)),
))
.into_any_element(),
),
],
)
.grow()
.vertical(),
])
.children(vec![
example_group_with_title(
"Labels",
vec![
single_example(
"Short Branch & Repo",
div()
.w(example_width)
.overflow_hidden()
.child(PanelRepoFooter::new_preview(
SharedString::from("zed"),
Some(custom("main", behind_upstream)),
))
.into_any_element(),
),
single_example(
"Long Branch",
div()
.w(example_width)
.overflow_hidden()
.child(PanelRepoFooter::new_preview(
SharedString::from("zed"),
Some(custom(
"redesign-and-update-git-ui-list-entry-style",
behind_upstream,
)),
))
.into_any_element(),
),
single_example(
"Long Repo",
div()
.w(example_width)
.overflow_hidden()
.child(PanelRepoFooter::new_preview(
SharedString::from("zed-industries-community-examples"),
Some(custom("gpui", ahead_of_upstream)),
))
.into_any_element(),
),
single_example(
"Long Repo & Branch",
div()
.w(example_width)
.overflow_hidden()
.child(PanelRepoFooter::new_preview(
SharedString::from("zed-industries-community-examples"),
Some(custom(
"redesign-and-update-git-ui-list-entry-style",
behind_upstream,
)),
))
.into_any_element(),
),
single_example(
"Uppercase Repo",
div()
.w(example_width)
.overflow_hidden()
.child(PanelRepoFooter::new_preview(
SharedString::from("LICENSES"),
Some(custom("main", ahead_of_upstream)),
))
.into_any_element(),
),
single_example(
"Uppercase Branch",
div()
.w(example_width)
.overflow_hidden()
.child(PanelRepoFooter::new_preview(
SharedString::from("zed"),
Some(custom("update-README", behind_upstream)),
))
.into_any_element(),
),
],
)
.grow()
.vertical(),
])
.into_any_element(),
)
}
}

View File

@@ -441,8 +441,8 @@ mod remote_button {
}
}
#[derive(IntoElement, IntoComponent)]
#[component(scope = "Version Control")]
/// A visual representation of a file's Git status.
#[derive(IntoElement, RegisterComponent)]
pub struct GitStatusIcon {
status: FileStatus,
}
@@ -484,8 +484,12 @@ impl RenderOnce for GitStatusIcon {
}
// View this component preview using `workspace: open component-preview`
impl ComponentPreview for GitStatusIcon {
fn preview(_window: &mut Window, _cx: &mut App) -> AnyElement {
impl Component for GitStatusIcon {
fn scope() -> ComponentScope {
ComponentScope::VersionControl
}
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
fn tracked_file_status(code: StatusCode) -> FileStatus {
FileStatus::Tracked(git::status::TrackedStatus {
index_status: code,
@@ -502,17 +506,19 @@ impl ComponentPreview for GitStatusIcon {
}
.into();
v_flex()
.gap_6()
.children(vec![example_group(vec![
single_example("Modified", GitStatusIcon::new(modified).into_any_element()),
single_example("Added", GitStatusIcon::new(added).into_any_element()),
single_example("Deleted", GitStatusIcon::new(deleted).into_any_element()),
single_example(
"Conflicted",
GitStatusIcon::new(conflict).into_any_element(),
),
])])
.into_any_element()
Some(
v_flex()
.gap_6()
.children(vec![example_group(vec![
single_example("Modified", GitStatusIcon::new(modified).into_any_element()),
single_example("Added", GitStatusIcon::new(added).into_any_element()),
single_example("Deleted", GitStatusIcon::new(deleted).into_any_element()),
single_example(
"Conflicted",
GitStatusIcon::new(conflict).into_any_element(),
),
])])
.into_any_element(),
)
}
}

View File

@@ -1005,8 +1005,7 @@ impl Render for ProjectDiffToolbar {
}
}
#[derive(IntoElement, IntoComponent)]
#[component(scope = "Version Control")]
#[derive(IntoElement, RegisterComponent)]
pub struct ProjectDiffEmptyState {
pub no_repo: bool,
pub can_push_and_pull: bool,
@@ -1178,8 +1177,12 @@ mod preview {
use super::ProjectDiffEmptyState;
// View this component preview using `workspace: open component-preview`
impl ComponentPreview for ProjectDiffEmptyState {
fn preview(_window: &mut Window, _cx: &mut App) -> AnyElement {
impl Component for ProjectDiffEmptyState {
fn scope() -> ComponentScope {
ComponentScope::VersionControl
}
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
let unknown_upstream: Option<UpstreamTracking> = None;
let ahead_of_upstream: Option<UpstreamTracking> = Some(
UpstreamTrackingStatus {
@@ -1244,46 +1247,48 @@ mod preview {
let (width, height) = (px(480.), px(320.));
v_flex()
.gap_6()
.children(vec![
example_group(vec![
single_example(
"No Repo",
div()
.w(width)
.h(height)
.child(no_repo_state)
.into_any_element(),
),
single_example(
"No Changes",
div()
.w(width)
.h(height)
.child(no_changes_state)
.into_any_element(),
),
single_example(
"Unknown Upstream",
div()
.w(width)
.h(height)
.child(unknown_upstream_state)
.into_any_element(),
),
single_example(
"Ahead of Remote",
div()
.w(width)
.h(height)
.child(ahead_of_upstream_state)
.into_any_element(),
),
Some(
v_flex()
.gap_6()
.children(vec![
example_group(vec![
single_example(
"No Repo",
div()
.w(width)
.h(height)
.child(no_repo_state)
.into_any_element(),
),
single_example(
"No Changes",
div()
.w(width)
.h(height)
.child(no_changes_state)
.into_any_element(),
),
single_example(
"Unknown Upstream",
div()
.w(width)
.h(height)
.child(unknown_upstream_state)
.into_any_element(),
),
single_example(
"Ahead of Remote",
div()
.w(width)
.h(height)
.child(ahead_of_upstream_state)
.into_any_element(),
),
])
.vertical(),
])
.vertical(),
])
.into_any_element()
.into_any_element(),
)
}
}
}

View File

@@ -572,7 +572,11 @@ pub trait LspAdapter: 'static + Send + Sync {
}
/// Support custom initialize params.
fn prepare_initialize_params(&self, original: InitializeParams) -> Result<InitializeParams> {
fn prepare_initialize_params(
&self,
original: InitializeParams,
_: &App,
) -> Result<InitializeParams> {
Ok(original)
}

View File

@@ -370,7 +370,7 @@ fn default_words_completion_mode() -> WordsCompletionMode {
}
fn default_lsp_insert_mode() -> LspInsertMode {
LspInsertMode::Insert
LspInsertMode::ReplaceSuffix
}
fn default_lsp_fetch_timeout_ms() -> u64 {
@@ -1029,7 +1029,10 @@ fn scroll_debounce_ms() -> u64 {
#[derive(Debug, Clone, Deserialize, PartialEq, Serialize, JsonSchema)]
pub struct LanguageTaskConfig {
/// Extra task variables to set for a particular language.
#[serde(default)]
pub variables: HashMap<String, String>,
#[serde(default = "default_true")]
pub enabled: bool,
}
impl InlayHintSettings {

View File

@@ -5,6 +5,7 @@ use crate::{LanguageToolchainStore, Location, Runnable};
use anyhow::Result;
use collections::HashMap;
use gpui::{App, Task};
use lsp::LanguageServerName;
use task::{TaskTemplates, TaskVariables};
use text::BufferId;
@@ -15,6 +16,7 @@ pub struct RunnableRange {
pub runnable: Runnable,
pub extra_captures: HashMap<String, String>,
}
/// Language Contexts are used by Zed tasks to extract information about the source file where the tasks are supposed to be scheduled from.
/// Multiple context providers may be used together: by default, Zed provides a base [`BasicContextProvider`] context that fills all non-custom [`VariableName`] variants.
///
@@ -40,4 +42,9 @@ pub trait ContextProvider: Send + Sync {
) -> Option<TaskTemplates> {
None
}
/// A language server name, that can return tasks using LSP (ext) for this language.
fn lsp_task_source(&self) -> Option<LanguageServerName> {
None
}
}

View File

@@ -1,7 +1,7 @@
use anyhow::{Context, Result, anyhow, bail};
use async_trait::async_trait;
use futures::StreamExt;
use gpui::AsyncApp;
use gpui::{App, AsyncApp};
use http_client::github::{GitHubLspBinaryVersion, latest_github_release};
pub use language::*;
use lsp::{DiagnosticTag, InitializeParams, LanguageServerBinary, LanguageServerName};
@@ -273,6 +273,7 @@ impl super::LspAdapter for CLspAdapter {
fn prepare_initialize_params(
&self,
mut original: InitializeParams,
_: &App,
) -> Result<InitializeParams> {
let experimental = json!({
"textDocument": {

View File

@@ -7,8 +7,11 @@ use gpui::{App, AsyncApp, SharedString, Task};
use http_client::github::AssetKind;
use http_client::github::{GitHubLspBinaryVersion, latest_github_release};
pub use language::*;
use lsp::LanguageServerBinary;
use lsp::{InitializeParams, LanguageServerBinary};
use project::project_settings::ProjectSettings;
use regex::Regex;
use serde_json::json;
use settings::Settings as _;
use smol::fs::{self};
use std::fmt::Display;
use std::{
@@ -18,6 +21,7 @@ use std::{
sync::{Arc, LazyLock},
};
use task::{TaskTemplate, TaskTemplates, TaskType, TaskVariables, VariableName};
use util::merge_json_value_into;
use util::{ResultExt, fs::remove_matching, maybe};
use crate::language_settings::language_settings;
@@ -48,9 +52,9 @@ impl RustLspAdapter {
const ARCH_SERVER_NAME: &str = "pc-windows-msvc";
}
impl RustLspAdapter {
const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("rust-analyzer");
const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("rust-analyzer");
impl RustLspAdapter {
fn build_asset_name() -> String {
let extension = match Self::GITHUB_ASSET_KIND {
AssetKind::TarGz => "tar.gz",
@@ -60,7 +64,7 @@ impl RustLspAdapter {
format!(
"{}-{}-{}.{}",
Self::SERVER_NAME,
SERVER_NAME,
std::env::consts::ARCH,
Self::ARCH_SERVER_NAME,
extension
@@ -98,7 +102,7 @@ impl ManifestProvider for CargoManifestProvider {
#[async_trait(?Send)]
impl LspAdapter for RustLspAdapter {
fn name(&self) -> LanguageServerName {
Self::SERVER_NAME.clone()
SERVER_NAME.clone()
}
fn manifest_name(&self) -> Option<ManifestName> {
@@ -473,6 +477,30 @@ impl LspAdapter for RustLspAdapter {
filter_range,
})
}
fn prepare_initialize_params(
&self,
mut original: InitializeParams,
cx: &App,
) -> Result<InitializeParams> {
let enable_lsp_tasks = ProjectSettings::get_global(cx)
.lsp
.get(&SERVER_NAME)
.map_or(false, |s| s.enable_lsp_tasks);
if enable_lsp_tasks {
let experimental = json!({
"runnables": {
"kinds": [ "cargo", "shell" ],
},
});
if let Some(ref mut original_experimental) = original.capabilities.experimental {
merge_json_value_into(experimental, original_experimental);
} else {
original.capabilities.experimental = Some(experimental);
}
}
Ok(original)
}
}
pub(crate) struct RustContextProvider;
@@ -776,6 +804,10 @@ impl ContextProvider for RustContextProvider {
Some(TaskTemplates(task_templates))
}
fn lsp_task_source(&self) -> Option<LanguageServerName> {
Some(SERVER_NAME)
}
}
/// Part of the data structure of Cargo metadata

View File

@@ -1718,21 +1718,25 @@ impl MultiBuffer {
(None, None) => break,
(None, Some(_)) => {
let existing_id = existing_iter.next().unwrap();
let locator = snapshot.excerpt_locator_for_id(existing_id);
let existing_excerpt = excerpts_cursor.item().unwrap();
excerpts_cursor.seek_forward(&Some(locator), Bias::Left, &());
let existing_end = existing_excerpt
.range
.context
.end
.to_point(&buffer_snapshot);
if let Some((new_id, last)) = to_insert.last() {
if existing_end <= last.context.end {
self.snapshot
.borrow_mut()
.replaced_excerpts
.insert(existing_id, *new_id);
}
let locator = snapshot.excerpt_locator_for_id(existing_id);
excerpts_cursor.seek_forward(&Some(locator), Bias::Left, &());
if let Some(existing_excerpt) = excerpts_cursor
.item()
.filter(|e| e.buffer_id == buffer_snapshot.remote_id())
{
let existing_end = existing_excerpt
.range
.context
.end
.to_point(&buffer_snapshot);
if existing_end <= last.context.end {
self.snapshot
.borrow_mut()
.replaced_excerpts
.insert(existing_id, *new_id);
}
};
}
to_remove.push(existing_id);
continue;
@@ -1745,16 +1749,14 @@ impl MultiBuffer {
};
let locator = snapshot.excerpt_locator_for_id(*existing);
excerpts_cursor.seek_forward(&Some(locator), Bias::Left, &());
let Some(existing_excerpt) = excerpts_cursor.item() else {
let Some(existing_excerpt) = excerpts_cursor
.item()
.filter(|e| e.buffer_id == buffer_snapshot.remote_id())
else {
to_remove.push(existing_iter.next().unwrap());
to_insert.push((next_excerpt_id(), new_iter.next().unwrap()));
continue;
};
if existing_excerpt.buffer_id != buffer_snapshot.remote_id() {
to_remove.push(existing_iter.next().unwrap());
to_insert.push((next_excerpt_id(), new_iter.next().unwrap()));
continue;
}
let existing_start = existing_excerpt
.range

View File

@@ -1798,6 +1798,88 @@ fn test_set_excerpts_for_buffer(cx: &mut TestAppContext) {
});
}
#[gpui::test]
fn test_set_excerpts_for_buffer_rename(cx: &mut TestAppContext) {
let buf1 = cx.new(|cx| {
Buffer::local(
indoc! {
"zero
one
two
three
four
five
six
seven
",
},
cx,
)
});
let path: PathKey = PathKey::namespaced(0, Path::new("/").into());
let buf2 = cx.new(|cx| {
Buffer::local(
indoc! {
"000
111
222
333
"
},
cx,
)
});
let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
multibuffer.update(cx, |multibuffer, cx| {
multibuffer.set_excerpts_for_path(
path.clone(),
buf1.clone(),
vec![Point::row_range(1..1), Point::row_range(4..5)],
1,
cx,
);
});
assert_excerpts_match(
&multibuffer,
cx,
indoc! {
"-----
zero
one
two
-----
three
four
five
six
"
},
);
multibuffer.update(cx, |multibuffer, cx| {
multibuffer.set_excerpts_for_path(
path.clone(),
buf2.clone(),
vec![Point::row_range(0..1)],
2,
cx,
);
});
assert_excerpts_match(
&multibuffer,
cx,
indoc! {"-----
000
111
222
333
"},
);
}
#[gpui::test]
fn test_diff_hunks_with_multiple_excerpts(cx: &mut TestAppContext) {
let base_text_1 = indoc!(

View File

@@ -33,8 +33,7 @@ impl From<IconName> for ToastIcon {
}
}
#[derive(IntoComponent)]
#[component(scope = "Notification")]
#[derive(RegisterComponent)]
pub struct StatusToast {
icon: Option<ToastIcon>,
text: SharedString,
@@ -135,8 +134,12 @@ impl Focusable for StatusToast {
impl EventEmitter<DismissEvent> for StatusToast {}
impl ComponentPreview for StatusToast {
fn preview(_window: &mut Window, cx: &mut App) -> AnyElement {
impl Component for StatusToast {
fn scope() -> ComponentScope {
ComponentScope::Notification
}
fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
let text_example = StatusToast::new("Operation completed", cx, |this, _| this);
let action_example = StatusToast::new("Update ready to install", cx, |this, _cx| {
@@ -175,29 +178,40 @@ impl ComponentPreview for StatusToast {
})
});
v_flex()
.gap_6()
.p_4()
.children(vec![
example_group_with_title(
"Basic Toast",
vec![
single_example("Text", div().child(text_example).into_any_element()),
single_example("Action", div().child(action_example).into_any_element()),
single_example("Icon", div().child(icon_example).into_any_element()),
],
),
example_group_with_title(
"Examples",
vec![
single_example("Success", div().child(success_example).into_any_element()),
single_example("Error", div().child(error_example).into_any_element()),
single_example("Warning", div().child(warning_example).into_any_element()),
single_example("Create PR", div().child(pr_example).into_any_element()),
],
)
.vertical(),
])
.into_any_element()
Some(
v_flex()
.gap_6()
.p_4()
.children(vec![
example_group_with_title(
"Basic Toast",
vec![
single_example("Text", div().child(text_example).into_any_element()),
single_example(
"Action",
div().child(action_example).into_any_element(),
),
single_example("Icon", div().child(icon_example).into_any_element()),
],
),
example_group_with_title(
"Examples",
vec![
single_example(
"Success",
div().child(success_example).into_any_element(),
),
single_example("Error", div().child(error_example).into_any_element()),
single_example(
"Warning",
div().child(warning_example).into_any_element(),
),
single_example("Create PR", div().child(pr_example).into_any_element()),
],
)
.vertical(),
])
.into_any_element(),
)
}
}

View File

@@ -17,9 +17,7 @@ use gpui::{App, AsyncApp, Entity};
use language::{
Anchor, Bias, Buffer, BufferSnapshot, CachedLspAdapter, CharKind, OffsetRangeExt, PointUtf16,
ToOffset, ToPointUtf16, Transaction, Unclipped,
language_settings::{
AllLanguageSettings, InlayHintKind, LanguageSettings, LspInsertMode, language_settings,
},
language_settings::{InlayHintKind, LanguageSettings, language_settings},
point_from_lsp, point_to_lsp,
proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version},
range_from_lsp, range_to_lsp,
@@ -30,7 +28,6 @@ use lsp::{
LanguageServer, LanguageServerId, LinkedEditingRangeServerCapabilities, OneOf, RenameOptions,
ServerCapabilities,
};
use settings::Settings as _;
use signature_help::{lsp_to_proto_signature, proto_to_lsp_signature};
use std::{cmp::Reverse, mem, ops::Range, path::Path, sync::Arc};
use text::{BufferId, LineEnding};
@@ -977,62 +974,69 @@ async fn location_links_from_proto(
let mut links = Vec::new();
for link in proto_links {
let origin = match link.origin {
Some(origin) => {
let buffer_id = BufferId::new(origin.buffer_id)?;
let buffer = lsp_store
.update(&mut cx, |lsp_store, cx| {
lsp_store.wait_for_remote_buffer(buffer_id, cx)
})?
.await?;
let start = origin
.start
.and_then(deserialize_anchor)
.ok_or_else(|| anyhow!("missing origin start"))?;
let end = origin
.end
.and_then(deserialize_anchor)
.ok_or_else(|| anyhow!("missing origin end"))?;
buffer
.update(&mut cx, |buffer, _| buffer.wait_for_anchors([start, end]))?
.await?;
Some(Location {
buffer,
range: start..end,
})
}
None => None,
};
let target = link.target.ok_or_else(|| anyhow!("missing target"))?;
let buffer_id = BufferId::new(target.buffer_id)?;
let buffer = lsp_store
.update(&mut cx, |lsp_store, cx| {
lsp_store.wait_for_remote_buffer(buffer_id, cx)
})?
.await?;
let start = target
.start
.and_then(deserialize_anchor)
.ok_or_else(|| anyhow!("missing target start"))?;
let end = target
.end
.and_then(deserialize_anchor)
.ok_or_else(|| anyhow!("missing target end"))?;
buffer
.update(&mut cx, |buffer, _| buffer.wait_for_anchors([start, end]))?
.await?;
let target = Location {
buffer,
range: start..end,
};
links.push(LocationLink { origin, target })
links.push(location_link_from_proto(link, &lsp_store, &mut cx).await?)
}
Ok(links)
}
pub async fn location_link_from_proto(
link: proto::LocationLink,
lsp_store: &Entity<LspStore>,
cx: &mut AsyncApp,
) -> Result<LocationLink> {
let origin = match link.origin {
Some(origin) => {
let buffer_id = BufferId::new(origin.buffer_id)?;
let buffer = lsp_store
.update(cx, |lsp_store, cx| {
lsp_store.wait_for_remote_buffer(buffer_id, cx)
})?
.await?;
let start = origin
.start
.and_then(deserialize_anchor)
.ok_or_else(|| anyhow!("missing origin start"))?;
let end = origin
.end
.and_then(deserialize_anchor)
.ok_or_else(|| anyhow!("missing origin end"))?;
buffer
.update(cx, |buffer, _| buffer.wait_for_anchors([start, end]))?
.await?;
Some(Location {
buffer,
range: start..end,
})
}
None => None,
};
let target = link.target.ok_or_else(|| anyhow!("missing target"))?;
let buffer_id = BufferId::new(target.buffer_id)?;
let buffer = lsp_store
.update(cx, |lsp_store, cx| {
lsp_store.wait_for_remote_buffer(buffer_id, cx)
})?
.await?;
let start = target
.start
.and_then(deserialize_anchor)
.ok_or_else(|| anyhow!("missing target start"))?;
let end = target
.end
.and_then(deserialize_anchor)
.ok_or_else(|| anyhow!("missing target end"))?;
buffer
.update(cx, |buffer, _| buffer.wait_for_anchors([start, end]))?
.await?;
let target = Location {
buffer,
range: start..end,
};
Ok(LocationLink { origin, target })
}
async fn location_links_from_lsp(
message: Option<lsp::GotoDefinitionResponse>,
lsp_store: Entity<LspStore>,
@@ -1115,6 +1119,65 @@ async fn location_links_from_lsp(
Ok(definitions)
}
pub async fn location_link_from_lsp(
link: lsp::LocationLink,
lsp_store: &Entity<LspStore>,
buffer: &Entity<Buffer>,
server_id: LanguageServerId,
cx: &mut AsyncApp,
) -> Result<LocationLink> {
let (lsp_adapter, language_server) =
language_server_for_buffer(&lsp_store, &buffer, server_id, cx)?;
let (origin_range, target_uri, target_range) = (
link.origin_selection_range,
link.target_uri,
link.target_selection_range,
);
let target_buffer_handle = lsp_store
.update(cx, |lsp_store, cx| {
lsp_store.open_local_buffer_via_lsp(
target_uri,
language_server.server_id(),
lsp_adapter.name.clone(),
cx,
)
})?
.await?;
cx.update(|cx| {
let origin_location = origin_range.map(|origin_range| {
let origin_buffer = buffer.read(cx);
let origin_start =
origin_buffer.clip_point_utf16(point_from_lsp(origin_range.start), Bias::Left);
let origin_end =
origin_buffer.clip_point_utf16(point_from_lsp(origin_range.end), Bias::Left);
Location {
buffer: buffer.clone(),
range: origin_buffer.anchor_after(origin_start)
..origin_buffer.anchor_before(origin_end),
}
});
let target_buffer = target_buffer_handle.read(cx);
let target_start =
target_buffer.clip_point_utf16(point_from_lsp(target_range.start), Bias::Left);
let target_end =
target_buffer.clip_point_utf16(point_from_lsp(target_range.end), Bias::Left);
let target_location = Location {
buffer: target_buffer_handle,
range: target_buffer.anchor_after(target_start)
..target_buffer.anchor_before(target_end),
};
LocationLink {
origin: origin_location,
target: target_location,
}
})
}
fn location_links_to_proto(
links: Vec<LocationLink>,
lsp_store: &mut LspStore,
@@ -1123,45 +1186,52 @@ fn location_links_to_proto(
) -> Vec<proto::LocationLink> {
links
.into_iter()
.map(|definition| {
let origin = definition.origin.map(|origin| {
lsp_store
.buffer_store()
.update(cx, |buffer_store, cx| {
buffer_store.create_buffer_for_peer(&origin.buffer, peer_id, cx)
})
.detach_and_log_err(cx);
let buffer_id = origin.buffer.read(cx).remote_id().into();
proto::Location {
start: Some(serialize_anchor(&origin.range.start)),
end: Some(serialize_anchor(&origin.range.end)),
buffer_id,
}
});
lsp_store
.buffer_store()
.update(cx, |buffer_store, cx| {
buffer_store.create_buffer_for_peer(&definition.target.buffer, peer_id, cx)
})
.detach_and_log_err(cx);
let buffer_id = definition.target.buffer.read(cx).remote_id().into();
let target = proto::Location {
start: Some(serialize_anchor(&definition.target.range.start)),
end: Some(serialize_anchor(&definition.target.range.end)),
buffer_id,
};
proto::LocationLink {
origin,
target: Some(target),
}
})
.map(|definition| location_link_to_proto(definition, lsp_store, peer_id, cx))
.collect()
}
pub fn location_link_to_proto(
location: LocationLink,
lsp_store: &mut LspStore,
peer_id: PeerId,
cx: &mut App,
) -> proto::LocationLink {
let origin = location.origin.map(|origin| {
lsp_store
.buffer_store()
.update(cx, |buffer_store, cx| {
buffer_store.create_buffer_for_peer(&origin.buffer, peer_id, cx)
})
.detach_and_log_err(cx);
let buffer_id = origin.buffer.read(cx).remote_id().into();
proto::Location {
start: Some(serialize_anchor(&origin.range.start)),
end: Some(serialize_anchor(&origin.range.end)),
buffer_id,
}
});
lsp_store
.buffer_store()
.update(cx, |buffer_store, cx| {
buffer_store.create_buffer_for_peer(&location.target.buffer, peer_id, cx)
})
.detach_and_log_err(cx);
let buffer_id = location.target.buffer.read(cx).remote_id().into();
let target = proto::Location {
start: Some(serialize_anchor(&location.target.range.start)),
end: Some(serialize_anchor(&location.target.range.end)),
buffer_id,
};
proto::LocationLink {
origin,
target: Some(target),
}
}
#[async_trait(?Send)]
impl LspCommand for GetReferences {
type Response = Vec<Location>;
@@ -2088,7 +2158,7 @@ impl LspCommand for GetCompletions {
.map(Arc::new);
let mut completion_edits = Vec::new();
buffer.update(&mut cx, |buffer, cx| {
buffer.update(&mut cx, |buffer, _cx| {
let snapshot = buffer.snapshot();
let clipped_position = buffer.clip_point_utf16(Unclipped(self.position), Bias::Left);
@@ -2125,21 +2195,11 @@ impl LspCommand for GetCompletions {
// If the language server provides a range to overwrite, then
// check that the range is valid.
Some(completion_text_edit) => {
let completion_mode = AllLanguageSettings::get_global(cx)
.defaults
.completions
.lsp_insert_mode;
match parse_completion_text_edit(
&completion_text_edit,
&snapshot,
completion_mode,
) {
match parse_completion_text_edit(&completion_text_edit, &snapshot) {
Some(edit) => edit,
None => return false,
}
}
// If the language server does not provide a range, then infer
// the range based on the syntax tree.
None => {
@@ -2191,7 +2251,12 @@ impl LspCommand for GetCompletions {
.as_ref()
.unwrap_or(&lsp_completion.label)
.clone();
(range, text)
ParsedCompletionEdit {
replace_range: range,
insert_range: None,
new_text: text,
}
}
};
@@ -2207,8 +2272,8 @@ impl LspCommand for GetCompletions {
Ok(completions
.into_iter()
.zip(completion_edits)
.map(|(mut lsp_completion, (old_range, mut new_text))| {
LineEnding::normalize(&mut new_text);
.map(|(mut lsp_completion, mut edit)| {
LineEnding::normalize(&mut edit.new_text);
if lsp_completion.data.is_none() {
if let Some(default_data) = lsp_defaults
.as_ref()
@@ -2220,9 +2285,10 @@ impl LspCommand for GetCompletions {
}
}
CoreCompletion {
old_range,
new_text,
replace_range: edit.replace_range,
new_text: edit.new_text,
source: CompletionSource::Lsp {
insert_range: edit.insert_range,
server_id,
lsp_completion: Box::new(lsp_completion),
lsp_defaults: lsp_defaults.clone(),
@@ -2312,91 +2378,53 @@ impl LspCommand for GetCompletions {
}
}
pub struct ParsedCompletionEdit {
pub replace_range: Range<Anchor>,
pub insert_range: Option<Range<Anchor>>,
pub new_text: String,
}
pub(crate) fn parse_completion_text_edit(
edit: &lsp::CompletionTextEdit,
snapshot: &BufferSnapshot,
completion_mode: LspInsertMode,
) -> Option<(Range<Anchor>, String)> {
match edit {
lsp::CompletionTextEdit::Edit(edit) => {
let range = range_from_lsp(edit.range);
let start = snapshot.clip_point_utf16(range.start, Bias::Left);
let end = snapshot.clip_point_utf16(range.end, Bias::Left);
if start != range.start.0 || end != range.end.0 {
log::info!("completion out of expected range");
None
} else {
Some((
snapshot.anchor_before(start)..snapshot.anchor_after(end),
edit.new_text.clone(),
))
}
}
) -> Option<ParsedCompletionEdit> {
let (replace_range, insert_range, new_text) = match edit {
lsp::CompletionTextEdit::Edit(edit) => (edit.range, None, &edit.new_text),
lsp::CompletionTextEdit::InsertAndReplace(edit) => {
let replace = match completion_mode {
LspInsertMode::Insert => false,
LspInsertMode::Replace => true,
LspInsertMode::ReplaceSubsequence => {
let range_to_replace = range_from_lsp(edit.replace);
(edit.replace, Some(edit.insert), &edit.new_text)
}
};
let start = snapshot.clip_point_utf16(range_to_replace.start, Bias::Left);
let end = snapshot.clip_point_utf16(range_to_replace.end, Bias::Left);
if start != range_to_replace.start.0 || end != range_to_replace.end.0 {
false
} else {
let mut completion_text = edit.new_text.chars();
let mut text_to_replace = snapshot.chars_for_range(
snapshot.anchor_before(start)..snapshot.anchor_after(end),
);
// is `text_to_replace` a subsequence of `completion_text`
text_to_replace.all(|needle_ch| {
completion_text.any(|haystack_ch| haystack_ch == needle_ch)
})
}
}
LspInsertMode::ReplaceSuffix => {
let range_after_cursor = lsp::Range {
start: edit.insert.end,
end: edit.replace.end,
};
let range_after_cursor = range_from_lsp(range_after_cursor);
let start = snapshot.clip_point_utf16(range_after_cursor.start, Bias::Left);
let end = snapshot.clip_point_utf16(range_after_cursor.end, Bias::Left);
if start != range_after_cursor.start.0 || end != range_after_cursor.end.0 {
false
} else {
let text_after_cursor = snapshot
.text_for_range(
snapshot.anchor_before(start)..snapshot.anchor_after(end),
)
.collect::<String>();
edit.new_text.ends_with(&text_after_cursor)
}
}
};
let range = range_from_lsp(match replace {
true => edit.replace,
false => edit.insert,
});
let replace_range = {
let range = range_from_lsp(replace_range);
let start = snapshot.clip_point_utf16(range.start, Bias::Left);
let end = snapshot.clip_point_utf16(range.end, Bias::Left);
if start != range.start.0 || end != range.end.0 {
log::info!("completion out of expected range");
return None;
}
snapshot.anchor_before(start)..snapshot.anchor_after(end)
};
let insert_range = match insert_range {
None => None,
Some(insert_range) => {
let range = range_from_lsp(insert_range);
let start = snapshot.clip_point_utf16(range.start, Bias::Left);
let end = snapshot.clip_point_utf16(range.end, Bias::Left);
if start != range.start.0 || end != range.end.0 {
log::info!("completion out of expected range");
None
} else {
Some((
snapshot.anchor_before(start)..snapshot.anchor_after(end),
edit.new_text.clone(),
))
log::info!("completion (insert) out of expected range");
return None;
}
Some(snapshot.anchor_before(start)..snapshot.anchor_after(end))
}
}
};
Some(ParsedCompletionEdit {
insert_range: insert_range,
replace_range: replace_range,
new_text: new_text.clone(),
})
}
#[async_trait(?Send)]

View File

@@ -39,8 +39,7 @@ use language::{
LanguageToolchainStore, LocalFile, LspAdapter, LspAdapterDelegate, Patch, PointUtf16,
TextBufferSnapshot, ToOffset, ToPointUtf16, Transaction, Unclipped,
language_settings::{
AllLanguageSettings, FormatOnSave, Formatter, LanguageSettings, LspInsertMode,
SelectedFormatter, language_settings,
FormatOnSave, Formatter, LanguageSettings, SelectedFormatter, language_settings,
},
point_to_lsp,
proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version},
@@ -280,7 +279,7 @@ impl LocalLspStore {
let initialization_params = cx.update(|cx| {
let mut params = language_server.default_initialize_params(cx);
params.initialization_options = initialization_options;
adapter.adapter.prepare_initialize_params(params)
adapter.adapter.prepare_initialize_params(params, cx)
})??;
Self::setup_lsp_messages(
@@ -3428,6 +3427,9 @@ impl LspStore {
client.add_entity_request_handler(Self::handle_lsp_command::<lsp_ext_command::ExpandMacro>);
client.add_entity_request_handler(Self::handle_lsp_command::<lsp_ext_command::OpenDocs>);
client.add_entity_request_handler(
Self::handle_lsp_command::<lsp_ext_command::GetLspRunnables>,
);
client.add_entity_request_handler(
Self::handle_lsp_command::<lsp_ext_command::SwitchSourceHeader>,
);
@@ -5133,7 +5135,6 @@ impl LspStore {
&buffer_snapshot,
completions.clone(),
completion_index,
cx,
)
.await
.log_err()
@@ -5167,7 +5168,6 @@ impl LspStore {
snapshot: &BufferSnapshot,
completions: Rc<RefCell<Box<[Completion]>>>,
completion_index: usize,
cx: &mut AsyncApp,
) -> Result<()> {
let server_id = server.server_id();
let can_resolve = server
@@ -5205,41 +5205,38 @@ impl LspStore {
};
let resolved_completion = request.await?;
let mut updated_insert_range = None;
if let Some(text_edit) = resolved_completion.text_edit.as_ref() {
// Technically we don't have to parse the whole `text_edit`, since the only
// language server we currently use that does update `text_edit` in `completionItem/resolve`
// is `typescript-language-server` and they only update `text_edit.new_text`.
// But we should not rely on that.
let completion_mode = cx
.read_global(|_: &SettingsStore, cx| {
AllLanguageSettings::get_global(cx)
.defaults
.completions
.lsp_insert_mode
})
.unwrap_or(LspInsertMode::Insert);
let edit = parse_completion_text_edit(text_edit, snapshot, completion_mode);
let edit = parse_completion_text_edit(text_edit, snapshot);
if let Some((old_range, mut new_text)) = edit {
LineEnding::normalize(&mut new_text);
if let Some(mut parsed_edit) = edit {
LineEnding::normalize(&mut parsed_edit.new_text);
let mut completions = completions.borrow_mut();
let completion = &mut completions[completion_index];
completion.new_text = new_text;
completion.old_range = old_range;
completion.new_text = parsed_edit.new_text;
completion.replace_range = parsed_edit.replace_range;
updated_insert_range = parsed_edit.insert_range;
}
}
let mut completions = completions.borrow_mut();
let completion = &mut completions[completion_index];
if let CompletionSource::Lsp {
insert_range,
lsp_completion,
resolved,
server_id: completion_server_id,
..
} = &mut completion.source
{
*insert_range = updated_insert_range;
if *resolved {
return Ok(());
}
@@ -5381,12 +5378,19 @@ impl LspStore {
let completion = &mut completions[completion_index];
completion.documentation = Some(documentation);
if let CompletionSource::Lsp {
insert_range,
lsp_completion,
resolved,
server_id: completion_server_id,
lsp_defaults: _,
} = &mut completion.source
{
let completion_insert_range = response
.old_insert_start
.and_then(deserialize_anchor)
.zip(response.old_insert_end.and_then(deserialize_anchor));
*insert_range = completion_insert_range.map(|(start, end)| start..end);
if *resolved {
return Ok(());
}
@@ -5398,14 +5402,14 @@ impl LspStore {
*resolved = true;
}
let old_range = response
.old_start
let replace_range = response
.old_replace_start
.and_then(deserialize_anchor)
.zip(response.old_end.and_then(deserialize_anchor));
if let Some((old_start, old_end)) = old_range {
.zip(response.old_replace_end.and_then(deserialize_anchor));
if let Some((old_replace_start, old_replace_end)) = replace_range {
if !response.new_text.is_empty() {
completion.new_text = response.new_text;
completion.old_range = old_start..old_end;
completion.replace_range = old_replace_start..old_replace_end;
}
}
@@ -5430,7 +5434,7 @@ impl LspStore {
project_id,
buffer_id: buffer_id.into(),
completion: Some(Self::serialize_completion(&CoreCompletion {
old_range: completion.old_range,
replace_range: completion.replace_range,
new_text: completion.new_text,
source: completion.source,
})),
@@ -5474,7 +5478,6 @@ impl LspStore {
&snapshot,
completions.clone(),
completion_index,
cx,
)
.await
.context("resolving completion")?;
@@ -5502,7 +5505,7 @@ impl LspStore {
buffer.start_transaction();
for (range, text) in edits {
let primary = &completion.old_range;
let primary = &completion.replace_range;
let start_within = primary.start.cmp(&range.start, buffer).is_le()
&& primary.end.cmp(&range.start, buffer).is_ge();
let end_within = range.start.cmp(&primary.end, buffer).is_le()
@@ -7706,8 +7709,10 @@ impl LspStore {
// If we have a new buffer_id, that means we're talking to a new client
// and want to check for new text_edits in the completion too.
let mut old_start = None;
let mut old_end = None;
let mut old_replace_start = None;
let mut old_replace_end = None;
let mut old_insert_start = None;
let mut old_insert_end = None;
let mut new_text = String::default();
if let Ok(buffer_id) = BufferId::new(envelope.payload.buffer_id) {
let buffer_snapshot = this.update(&mut cx, |this, cx| {
@@ -7716,23 +7721,18 @@ impl LspStore {
})??;
if let Some(text_edit) = completion.text_edit.as_ref() {
let completion_mode = cx
.read_global(|_: &SettingsStore, cx| {
AllLanguageSettings::get_global(cx)
.defaults
.completions
.lsp_insert_mode
})
.unwrap_or(LspInsertMode::Insert);
let edit = parse_completion_text_edit(text_edit, &buffer_snapshot);
let edit = parse_completion_text_edit(text_edit, &buffer_snapshot, completion_mode);
if let Some(mut edit) = edit {
LineEnding::normalize(&mut edit.new_text);
if let Some((old_range, mut text_edit_new_text)) = edit {
LineEnding::normalize(&mut text_edit_new_text);
new_text = text_edit_new_text;
old_start = Some(serialize_anchor(&old_range.start));
old_end = Some(serialize_anchor(&old_range.end));
new_text = edit.new_text;
old_replace_start = Some(serialize_anchor(&edit.replace_range.start));
old_replace_end = Some(serialize_anchor(&edit.replace_range.end));
if let Some(insert_range) = edit.insert_range {
old_insert_start = Some(serialize_anchor(&insert_range.start));
old_insert_end = Some(serialize_anchor(&insert_range.end));
}
}
}
}
@@ -7740,10 +7740,12 @@ impl LspStore {
Ok(proto::ResolveCompletionDocumentationResponse {
documentation,
documentation_is_markdown,
old_start,
old_end,
old_replace_start,
old_replace_end,
new_text,
lsp_completion,
old_insert_start,
old_insert_end,
})
}
@@ -8045,7 +8047,7 @@ impl LspStore {
this.apply_additional_edits_for_completion(
buffer,
Rc::new(RefCell::new(Box::new([Completion {
old_range: completion.old_range,
replace_range: completion.replace_range,
new_text: completion.new_text,
source: completion.source,
documentation: None,
@@ -8368,7 +8370,6 @@ impl LspStore {
self.buffer_store.update(cx, |buffer_store, cx| {
for buffer in buffer_store.buffers() {
buffer.update(cx, |buffer, cx| {
// TODO kb clean inlays
buffer.update_diagnostics(server_id, DiagnosticSet::new([], buffer), cx);
buffer.set_completion_triggers(server_id, Default::default(), cx);
});
@@ -9101,18 +9102,26 @@ impl LspStore {
pub(crate) fn serialize_completion(completion: &CoreCompletion) -> proto::Completion {
let mut serialized_completion = proto::Completion {
old_start: Some(serialize_anchor(&completion.old_range.start)),
old_end: Some(serialize_anchor(&completion.old_range.end)),
old_replace_start: Some(serialize_anchor(&completion.replace_range.start)),
old_replace_end: Some(serialize_anchor(&completion.replace_range.end)),
new_text: completion.new_text.clone(),
..proto::Completion::default()
};
match &completion.source {
CompletionSource::Lsp {
insert_range,
server_id,
lsp_completion,
lsp_defaults,
resolved,
} => {
let (old_insert_start, old_insert_end) = insert_range
.as_ref()
.map(|range| (serialize_anchor(&range.start), serialize_anchor(&range.end)))
.unzip();
serialized_completion.old_insert_start = old_insert_start;
serialized_completion.old_insert_end = old_insert_end;
serialized_completion.source = proto::completion::Source::Lsp as i32;
serialized_completion.server_id = server_id.0 as u64;
serialized_completion.lsp_completion = serde_json::to_vec(lsp_completion).unwrap();
@@ -9140,20 +9149,31 @@ impl LspStore {
}
pub(crate) fn deserialize_completion(completion: proto::Completion) -> Result<CoreCompletion> {
let old_start = completion
.old_start
let old_replace_start = completion
.old_replace_start
.and_then(deserialize_anchor)
.context("invalid old start")?;
let old_end = completion
.old_end
let old_replace_end = completion
.old_replace_end
.and_then(deserialize_anchor)
.context("invalid old end")?;
let insert_range = {
match completion.old_insert_start.zip(completion.old_insert_end) {
Some((start, end)) => {
let start = deserialize_anchor(start).context("invalid insert old start")?;
let end = deserialize_anchor(end).context("invalid insert old end")?;
Some(start..end)
}
None => None,
}
};
Ok(CoreCompletion {
old_range: old_start..old_end,
replace_range: old_replace_start..old_replace_end,
new_text: completion.new_text,
source: match proto::completion::Source::from_i32(completion.source) {
Some(proto::completion::Source::Custom) => CompletionSource::Custom,
Some(proto::completion::Source::Lsp) => CompletionSource::Lsp {
insert_range,
server_id: LanguageServerId::from_proto(completion.server_id),
lsp_completion: serde_json::from_slice(&completion.lsp_completion)?,
lsp_defaults: completion
@@ -9342,7 +9362,7 @@ async fn populate_labels_for_completions(
completions.push(Completion {
label,
documentation,
old_range: completion.old_range,
replace_range: completion.replace_range,
new_text: completion.new_text,
insert_text_mode: lsp_completion.insert_text_mode,
source: completion.source,
@@ -9356,7 +9376,7 @@ async fn populate_labels_for_completions(
completions.push(Completion {
label,
documentation: None,
old_range: completion.old_range,
replace_range: completion.replace_range,
new_text: completion.new_text,
source: completion.source,
insert_text_mode: None,

View File

@@ -1,12 +1,27 @@
use crate::{lsp_command::LspCommand, lsp_store::LspStore, make_text_document_identifier};
use crate::{
LocationLink,
lsp_command::{
LspCommand, location_link_from_lsp, location_link_from_proto, location_link_to_proto,
},
lsp_store::LspStore,
make_text_document_identifier,
};
use anyhow::{Context as _, Result};
use async_trait::async_trait;
use collections::HashMap;
use gpui::{App, AsyncApp, Entity};
use language::{Buffer, point_to_lsp, proto::deserialize_anchor};
use language::{
Buffer, point_to_lsp,
proto::{deserialize_anchor, serialize_anchor},
};
use lsp::{LanguageServer, LanguageServerId};
use rpc::proto::{self, PeerId};
use serde::{Deserialize, Serialize};
use std::{path::Path, sync::Arc};
use std::{
path::{Path, PathBuf},
sync::Arc,
};
use task::TaskTemplate;
use text::{BufferId, PointUtf16, ToPointUtf16};
pub enum LspExpandMacro {}
@@ -363,3 +378,245 @@ impl LspCommand for SwitchSourceHeader {
BufferId::new(message.buffer_id)
}
}
// https://rust-analyzer.github.io/book/contributing/lsp-extensions.html#runnables
// Taken from https://github.com/rust-lang/rust-analyzer/blob/a73a37a757a58b43a796d3eb86a1f7dfd0036659/crates/rust-analyzer/src/lsp/ext.rs#L425-L489
pub enum Runnables {}
impl lsp::request::Request for Runnables {
type Params = RunnablesParams;
type Result = Vec<Runnable>;
const METHOD: &'static str = "experimental/runnables";
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct RunnablesParams {
pub text_document: lsp::TextDocumentIdentifier,
pub position: Option<lsp::Position>,
}
#[derive(Deserialize, Serialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct Runnable {
pub label: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub location: Option<lsp::LocationLink>,
pub kind: RunnableKind,
pub args: RunnableArgs,
}
#[derive(Deserialize, Serialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
#[serde(untagged)]
pub enum RunnableArgs {
Cargo(CargoRunnableArgs),
Shell(ShellRunnableArgs),
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "lowercase")]
pub enum RunnableKind {
Cargo,
Shell,
}
#[derive(Deserialize, Serialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct CargoRunnableArgs {
#[serde(skip_serializing_if = "HashMap::is_empty")]
pub environment: HashMap<String, String>,
pub cwd: PathBuf,
/// Command to be executed instead of cargo
pub override_cargo: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub workspace_root: Option<PathBuf>,
// command, --package and --lib stuff
pub cargo_args: Vec<String>,
// stuff after --
pub executable_args: Vec<String>,
}
#[derive(Deserialize, Serialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct ShellRunnableArgs {
#[serde(skip_serializing_if = "HashMap::is_empty")]
pub environment: HashMap<String, String>,
pub cwd: PathBuf,
pub program: String,
pub args: Vec<String>,
}
#[derive(Debug)]
pub struct GetLspRunnables {
pub buffer_id: BufferId,
pub position: Option<text::Anchor>,
}
#[derive(Debug, Default)]
pub struct LspRunnables {
pub runnables: Vec<(Option<LocationLink>, TaskTemplate)>,
}
#[async_trait(?Send)]
impl LspCommand for GetLspRunnables {
type Response = LspRunnables;
type LspRequest = Runnables;
type ProtoRequest = proto::LspExtRunnables;
fn display_name(&self) -> &str {
"LSP Runnables"
}
fn to_lsp(
&self,
path: &Path,
buffer: &Buffer,
_: &Arc<LanguageServer>,
_: &App,
) -> Result<RunnablesParams> {
let url = match lsp::Url::from_file_path(path) {
Ok(url) => url,
Err(()) => anyhow::bail!("Failed to parse path {path:?} as lsp::Url"),
};
Ok(RunnablesParams {
text_document: lsp::TextDocumentIdentifier::new(url),
position: self
.position
.map(|anchor| point_to_lsp(anchor.to_point_utf16(&buffer.snapshot()))),
})
}
async fn response_from_lsp(
self,
lsp_runnables: Vec<Runnable>,
lsp_store: Entity<LspStore>,
buffer: Entity<Buffer>,
server_id: LanguageServerId,
mut cx: AsyncApp,
) -> Result<LspRunnables> {
let mut runnables = Vec::with_capacity(lsp_runnables.len());
for runnable in lsp_runnables {
let location = match runnable.location {
Some(location) => Some(
location_link_from_lsp(location, &lsp_store, &buffer, server_id, &mut cx)
.await?,
),
None => None,
};
let mut task_template = TaskTemplate::default();
task_template.label = runnable.label;
match runnable.args {
RunnableArgs::Cargo(cargo) => {
match cargo.override_cargo {
Some(override_cargo) => {
let mut override_parts =
override_cargo.split(" ").map(|s| s.to_string());
task_template.command = override_parts
.next()
.unwrap_or_else(|| override_cargo.clone());
task_template.args.extend(override_parts);
}
None => task_template.command = "cargo".to_string(),
};
task_template.env = cargo.environment;
task_template.cwd = Some(
cargo
.workspace_root
.unwrap_or(cargo.cwd)
.to_string_lossy()
.to_string(),
);
task_template.args.extend(cargo.cargo_args);
if !cargo.executable_args.is_empty() {
task_template.args.push("--".to_string());
task_template.args.extend(cargo.executable_args);
}
}
RunnableArgs::Shell(shell) => {
task_template.command = shell.program;
task_template.args = shell.args;
task_template.env = shell.environment;
task_template.cwd = Some(shell.cwd.to_string_lossy().to_string());
}
}
runnables.push((location, task_template));
}
Ok(LspRunnables { runnables })
}
fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::LspExtRunnables {
proto::LspExtRunnables {
project_id,
buffer_id: buffer.remote_id().to_proto(),
position: self.position.as_ref().map(serialize_anchor),
}
}
async fn from_proto(
message: proto::LspExtRunnables,
_: Entity<LspStore>,
_: Entity<Buffer>,
_: AsyncApp,
) -> Result<Self> {
let buffer_id = Self::buffer_id_from_proto(&message)?;
let position = message.position.and_then(deserialize_anchor);
Ok(Self {
buffer_id,
position,
})
}
fn response_to_proto(
response: LspRunnables,
lsp_store: &mut LspStore,
peer_id: PeerId,
_: &clock::Global,
cx: &mut App,
) -> proto::LspExtRunnablesResponse {
proto::LspExtRunnablesResponse {
runnables: response
.runnables
.into_iter()
.map(|(location, task_template)| proto::LspRunnable {
location: location
.map(|location| location_link_to_proto(location, lsp_store, peer_id, cx)),
task_template: serde_json::to_vec(&task_template).unwrap(),
})
.collect(),
}
}
async fn response_from_proto(
self,
message: proto::LspExtRunnablesResponse,
lsp_store: Entity<LspStore>,
_: Entity<Buffer>,
mut cx: AsyncApp,
) -> Result<LspRunnables> {
let mut runnables = LspRunnables {
runnables: Vec::new(),
};
for lsp_runnable in message.runnables {
let location = match lsp_runnable.location {
Some(location) => {
Some(location_link_from_proto(location, &lsp_store, &mut cx).await?)
}
None => None,
};
let task_template = serde_json::from_slice(&lsp_runnable.task_template)
.context("deserializing task template from proto")?;
runnables.runnables.push((location, task_template));
}
Ok(runnables)
}
fn buffer_id_from_proto(message: &proto::LspExtRunnables) -> Result<BufferId> {
BufferId::new(message.buffer_id)
}
}

View File

@@ -8,7 +8,7 @@ pub const RUST_ANALYZER_NAME: &str = "rust-analyzer";
/// Experimental: Informs the end user about the state of the server
///
/// [Rust Analyzer Specification](https://github.com/rust-lang/rust-analyzer/blob/master/docs/dev/lsp-extensions.md#server-status)
/// [Rust Analyzer Specification](https://rust-analyzer.github.io/book/contributing/lsp-extensions.html#server-status)
#[derive(Debug)]
enum ServerStatus {}
@@ -38,13 +38,10 @@ pub fn register_notifications(lsp_store: WeakEntity<LspStore>, language_server:
let name = language_server.name();
let server_id = language_server.server_id();
let this = lsp_store;
language_server
.on_notification::<ServerStatus, _>({
let name = name.to_string();
move |params, cx| {
let this = this.clone();
let name = name.to_string();
if let Some(ref message) = params.message {
let message = message.trim();
@@ -53,10 +50,10 @@ pub fn register_notifications(lsp_store: WeakEntity<LspStore>, language_server:
"Language server {name} (id {server_id}) status update: {message}"
);
match params.health {
ServerHealthStatus::Ok => log::info!("{}", formatted_message),
ServerHealthStatus::Warning => log::warn!("{}", formatted_message),
ServerHealthStatus::Ok => log::info!("{formatted_message}"),
ServerHealthStatus::Warning => log::warn!("{formatted_message}"),
ServerHealthStatus::Error => {
log::error!("{}", formatted_message);
log::error!("{formatted_message}");
let (tx, _rx) = smol::channel::bounded(1);
let request = LanguageServerPromptRequest {
level: PromptLevel::Critical,
@@ -65,7 +62,7 @@ pub fn register_notifications(lsp_store: WeakEntity<LspStore>, language_server:
response_channel: tx,
lsp_name: name.clone(),
};
let _ = this
lsp_store
.update(cx, |_, cx| {
cx.emit(LspStoreEvent::LanguageServerPrompt(request));
})

View File

@@ -359,8 +359,14 @@ pub struct InlayHint {
#[derive(PartialEq, Eq, Hash, Debug, Clone, Copy)]
pub enum CompletionIntent {
/// The user intends to 'commit' this result, if possible
/// completion confirmations should run side effects
/// completion confirmations should run side effects.
///
/// For LSP completions, will respect the setting `completions.lsp_insert_mode`.
Complete,
/// Similar to [Self::Complete], but behaves like `lsp_insert_mode` is set to `insert`.
CompleteWithInsert,
/// Similar to [Self::Complete], but behaves like `lsp_insert_mode` is set to `replace`.
CompleteWithReplace,
/// The user intends to continue 'composing' this completion
/// completion confirmations should not run side effects and
/// let the user continue composing their action
@@ -377,11 +383,11 @@ impl CompletionIntent {
}
}
/// A completion provided by a language server
/// Similar to `CoreCompletion`, but with extra metadata attached.
#[derive(Clone)]
pub struct Completion {
/// The range of the buffer that will be replaced.
pub old_range: Range<Anchor>,
/// The range of text that will be replaced by this completion.
pub replace_range: Range<Anchor>,
/// The new text that will be inserted.
pub new_text: String,
/// A label for this completion that is shown in the menu.
@@ -404,6 +410,8 @@ pub struct Completion {
#[derive(Debug, Clone)]
pub enum CompletionSource {
Lsp {
/// The alternate `insert` range, if provided by the LSP server.
insert_range: Option<Range<Anchor>>,
/// The id of the language server that produced this completion.
server_id: LanguageServerId,
/// The raw completion provided by the language server.
@@ -508,7 +516,7 @@ impl CompletionSource {
impl std::fmt::Debug for Completion {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Completion")
.field("old_range", &self.old_range)
.field("replace_range", &self.replace_range)
.field("new_text", &self.new_text)
.field("label", &self.label)
.field("documentation", &self.documentation)
@@ -517,10 +525,10 @@ impl std::fmt::Debug for Completion {
}
}
/// A completion provided by a language server
/// A generic completion that can come from different sources.
#[derive(Clone, Debug)]
pub(crate) struct CoreCompletion {
old_range: Range<Anchor>,
replace_range: Range<Anchor>,
new_text: String,
source: CompletionSource,
}

View File

@@ -25,7 +25,7 @@ use std::{
time::Duration,
};
use task::{TaskTemplates, VsCodeTaskFile};
use util::ResultExt;
use util::{ResultExt, serde::default_true};
use worktree::{PathChange, UpdatedEntriesSet, Worktree, WorktreeId};
use crate::{
@@ -278,12 +278,28 @@ pub struct BinarySettings {
pub ignore_system_version: Option<bool>,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub struct LspSettings {
pub binary: Option<BinarySettings>,
pub initialization_options: Option<serde_json::Value>,
pub settings: Option<serde_json::Value>,
/// If the server supports sending tasks over LSP extensions,
/// this setting can be used to enable or disable them in Zed.
/// Default: true
#[serde(default = "default_true")]
pub enable_lsp_tasks: bool,
}
impl Default for LspSettings {
fn default() -> Self {
Self {
binary: None,
initialization_options: None,
settings: None,
enable_lsp_tasks: true,
}
}
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]

View File

@@ -459,6 +459,8 @@ async fn test_fallback_to_single_worktree_tasks(cx: &mut gpui::TestAppContext) {
active_item_context: Some((Some(worktree_id), None, TaskContext::default())),
active_worktree_context: None,
other_worktree_contexts: Vec::new(),
lsp_task_sources: HashMap::default(),
latest_selection: None,
},
cx,
)
@@ -481,6 +483,8 @@ async fn test_fallback_to_single_worktree_tasks(cx: &mut gpui::TestAppContext) {
worktree_context
})),
other_worktree_contexts: Vec::new(),
lsp_task_sources: HashMap::default(),
latest_selection: None,
},
cx,
)
@@ -797,7 +801,7 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) {
.receive_notification::<lsp::notification::DidCloseTextDocument>()
.await
.text_document,
lsp::TextDocumentIdentifier::new(lsp::Url::from_file_path(path!("/dir/test3.rs")).unwrap(),),
lsp::TextDocumentIdentifier::new(lsp::Url::from_file_path(path!("/dir/test3.rs")).unwrap()),
);
assert_eq!(
fake_json_server
@@ -3014,7 +3018,7 @@ async fn test_completions_with_text_edit(cx: &mut gpui::TestAppContext) {
assert_eq!(completions.len(), 1);
assert_eq!(completions[0].new_text, "textEditText");
assert_eq!(
completions[0].old_range.to_offset(&snapshot),
completions[0].replace_range.to_offset(&snapshot),
text.len() - 3..text.len()
);
}
@@ -3097,7 +3101,7 @@ async fn test_completions_with_edit_ranges(cx: &mut gpui::TestAppContext) {
assert_eq!(completions.len(), 1);
assert_eq!(completions[0].new_text, "insertText");
assert_eq!(
completions[0].old_range.to_offset(&snapshot),
completions[0].replace_range.to_offset(&snapshot),
text.len() - 3..text.len()
);
}
@@ -3139,7 +3143,7 @@ async fn test_completions_with_edit_ranges(cx: &mut gpui::TestAppContext) {
assert_eq!(completions.len(), 1);
assert_eq!(completions[0].new_text, "labelText");
assert_eq!(
completions[0].old_range.to_offset(&snapshot),
completions[0].replace_range.to_offset(&snapshot),
text.len() - 3..text.len()
);
}
@@ -3209,7 +3213,7 @@ async fn test_completions_without_edit_ranges(cx: &mut gpui::TestAppContext) {
assert_eq!(completions.len(), 1);
assert_eq!(completions[0].new_text, "fullyQualifiedName");
assert_eq!(
completions[0].old_range.to_offset(&snapshot),
completions[0].replace_range.to_offset(&snapshot),
text.len() - 3..text.len()
);
@@ -3236,7 +3240,7 @@ async fn test_completions_without_edit_ranges(cx: &mut gpui::TestAppContext) {
assert_eq!(completions.len(), 1);
assert_eq!(completions[0].new_text, "component");
assert_eq!(
completions[0].old_range.to_offset(&snapshot),
completions[0].replace_range.to_offset(&snapshot),
text.len() - 4..text.len() - 1
);
}

View File

@@ -12,13 +12,17 @@ use anyhow::Result;
use collections::{HashMap, HashSet, VecDeque};
use gpui::{App, AppContext as _, Entity, SharedString, Task};
use itertools::Itertools;
use language::{ContextProvider, File, Language, LanguageToolchainStore, Location};
use language::{
ContextProvider, File, Language, LanguageToolchainStore, Location,
language_settings::language_settings,
};
use lsp::{LanguageServerId, LanguageServerName};
use settings::{InvalidSettingsError, TaskKind, parse_json_with_comments};
use task::{
DebugTaskDefinition, ResolvedTask, TaskContext, TaskId, TaskTemplate, TaskTemplates,
TaskVariables, VariableName,
};
use text::{Point, ToPoint};
use text::{BufferId, Point, ToPoint};
use util::{NumericPrefixWithSuffix, ResultExt as _, paths::PathExt as _, post_inc};
use worktree::WorktreeId;
@@ -55,6 +59,8 @@ pub enum TaskSourceKind {
},
/// Languages-specific tasks coming from extensions.
Language { name: SharedString },
/// Language-specific tasks coming from LSP servers.
Lsp(LanguageServerId),
}
/// A collection of task contexts, derived from the current state of the workspace.
@@ -68,6 +74,8 @@ pub struct TaskContexts {
pub active_worktree_context: Option<(WorktreeId, TaskContext)>,
/// If there are multiple worktrees in the workspace, all non-active ones are included here.
pub other_worktree_contexts: Vec<(WorktreeId, TaskContext)>,
pub lsp_task_sources: HashMap<LanguageServerName, Vec<BufferId>>,
pub latest_selection: Option<text::Anchor>,
}
impl TaskContexts {
@@ -104,18 +112,19 @@ impl TaskContexts {
impl TaskSourceKind {
pub fn to_id_base(&self) -> String {
match self {
TaskSourceKind::UserInput => "oneshot".to_string(),
TaskSourceKind::AbsPath { id_base, abs_path } => {
Self::UserInput => "oneshot".to_string(),
Self::AbsPath { id_base, abs_path } => {
format!("{id_base}_{}", abs_path.display())
}
TaskSourceKind::Worktree {
Self::Worktree {
id,
id_base,
directory_in_worktree,
} => {
format!("{id_base}_{id}_{}", directory_in_worktree.display())
}
TaskSourceKind::Language { name } => format!("language_{name}"),
Self::Language { name } => format!("language_{name}"),
Self::Lsp(server_id) => format!("lsp_{server_id}"),
}
}
}
@@ -156,6 +165,11 @@ impl Inventory {
});
let global_tasks = self.global_templates_from_settings();
let language_tasks = language
.filter(|language| {
language_settings(Some(language.name()), file.as_ref(), cx)
.tasks
.enabled
})
.and_then(|language| language.context_provider()?.associated_tasks(file, cx))
.into_iter()
.flat_map(|tasks| tasks.0.into_iter())
@@ -171,10 +185,10 @@ impl Inventory {
/// Joins the new resolutions with the resolved tasks that were used (spawned) before,
/// orders them so that the most recently used come first, all equally used ones are ordered so that the most specific tasks come first.
/// Deduplicates the tasks by their labels and context and splits the ordered list into two: used tasks and the rest, newly resolved tasks.
pub fn used_and_current_resolved_tasks(
&self,
task_contexts: &TaskContexts,
cx: &App,
pub fn used_and_current_resolved_tasks<'a>(
&'a self,
task_contexts: &'a TaskContexts,
cx: &'a App,
) -> (
Vec<(TaskSourceKind, ResolvedTask)>,
Vec<(TaskSourceKind, ResolvedTask)>,
@@ -227,7 +241,13 @@ impl Inventory {
let not_used_score = post_inc(&mut lru_score);
let global_tasks = self.global_templates_from_settings();
let language_tasks = language
.filter(|language| {
language_settings(Some(language.name()), file.as_ref(), cx)
.tasks
.enabled
})
.and_then(|language| language.context_provider()?.associated_tasks(file, cx))
.into_iter()
.flat_map(|tasks| tasks.0.into_iter())
@@ -475,6 +495,7 @@ fn task_lru_comparator(
fn task_source_kind_preference(kind: &TaskSourceKind) -> u32 {
match kind {
TaskSourceKind::Lsp(..) => 0,
TaskSourceKind::Language { .. } => 1,
TaskSourceKind::UserInput => 2,
TaskSourceKind::Worktree { .. } => 3,
@@ -698,7 +719,7 @@ mod tests {
async fn test_task_list_sorting(cx: &mut TestAppContext) {
init_test(cx);
let inventory = cx.update(Inventory::new);
let initial_tasks = resolved_task_names(&inventory, None, cx).await;
let initial_tasks = resolved_task_names(&inventory, None, cx);
assert!(
initial_tasks.is_empty(),
"No tasks expected for empty inventory, but got {initial_tasks:?}"
@@ -732,7 +753,7 @@ mod tests {
&expected_initial_state,
);
assert_eq!(
resolved_task_names(&inventory, None, cx).await,
resolved_task_names(&inventory, None, cx),
&expected_initial_state,
"Tasks with equal amount of usages should be sorted alphanumerically"
);
@@ -743,7 +764,7 @@ mod tests {
&expected_initial_state,
);
assert_eq!(
resolved_task_names(&inventory, None, cx).await,
resolved_task_names(&inventory, None, cx),
vec![
"2_task".to_string(),
"1_a_task".to_string(),
@@ -761,7 +782,7 @@ mod tests {
&expected_initial_state,
);
assert_eq!(
resolved_task_names(&inventory, None, cx).await,
resolved_task_names(&inventory, None, cx),
vec![
"3_task".to_string(),
"1_task".to_string(),
@@ -797,7 +818,7 @@ mod tests {
&expected_updated_state,
);
assert_eq!(
resolved_task_names(&inventory, None, cx).await,
resolved_task_names(&inventory, None, cx),
vec![
"3_task".to_string(),
"1_task".to_string(),
@@ -814,7 +835,7 @@ mod tests {
&expected_updated_state,
);
assert_eq!(
resolved_task_names(&inventory, None, cx).await,
resolved_task_names(&inventory, None, cx),
vec![
"11_hello".to_string(),
"3_task".to_string(),
@@ -987,21 +1008,21 @@ mod tests {
TaskStore::init(None);
}
async fn resolved_task_names(
fn resolved_task_names(
inventory: &Entity<Inventory>,
worktree: Option<WorktreeId>,
cx: &mut TestAppContext,
) -> Vec<String> {
let (used, current) = inventory.update(cx, |inventory, cx| {
inventory.update(cx, |inventory, cx| {
let mut task_contexts = TaskContexts::default();
task_contexts.active_worktree_context =
worktree.map(|worktree| (worktree, TaskContext::default()));
inventory.used_and_current_resolved_tasks(&task_contexts, cx)
});
used.into_iter()
.chain(current)
.map(|(_, task)| task.original_task().label.clone())
.collect()
let (used, current) = inventory.used_and_current_resolved_tasks(&task_contexts, cx);
used.into_iter()
.chain(current)
.map(|(_, task)| task.original_task().label.clone())
.collect()
})
}
fn mock_tasks_from_names<'a>(task_names: impl Iterator<Item = &'a str> + 'a) -> String {
@@ -1024,17 +1045,17 @@ mod tests {
worktree: Option<WorktreeId>,
cx: &mut TestAppContext,
) -> Vec<(TaskSourceKind, String)> {
let (used, current) = inventory.update(cx, |inventory, cx| {
inventory.update(cx, |inventory, cx| {
let mut task_contexts = TaskContexts::default();
task_contexts.active_worktree_context =
worktree.map(|worktree| (worktree, TaskContext::default()));
inventory.used_and_current_resolved_tasks(&task_contexts, cx)
});
let mut all = used;
all.extend(current);
all.into_iter()
.map(|(source_kind, task)| (source_kind, task.resolved_label))
.sorted_by_key(|(kind, label)| (task_source_kind_preference(kind), label.clone()))
.collect()
let (used, current) = inventory.used_and_current_resolved_tasks(&task_contexts, cx);
let mut all = used;
all.extend(current);
all.into_iter()
.map(|(source_kind, task)| (source_kind, task.resolved_label))
.sorted_by_key(|(kind, label)| (task_source_kind_preference(kind), label.clone()))
.collect()
})
}
}

View File

@@ -198,8 +198,8 @@ message ApplyCompletionAdditionalEditsResponse {
}
message Completion {
Anchor old_start = 1;
Anchor old_end = 2;
Anchor old_replace_start = 1;
Anchor old_replace_end = 2;
string new_text = 3;
uint64 server_id = 4;
bytes lsp_completion = 5;
@@ -208,6 +208,8 @@ message Completion {
optional bytes lsp_defaults = 8;
optional Anchor buffer_word_start = 9;
optional Anchor buffer_word_end = 10;
Anchor old_insert_start = 11;
Anchor old_insert_end = 12;
enum Source {
Lsp = 0;
@@ -428,10 +430,12 @@ message ResolveCompletionDocumentation {
message ResolveCompletionDocumentationResponse {
string documentation = 1;
bool documentation_is_markdown = 2;
Anchor old_start = 3;
Anchor old_end = 4;
Anchor old_replace_start = 3;
Anchor old_replace_end = 4;
string new_text = 5;
bytes lsp_completion = 6;
Anchor old_insert_start = 7;
Anchor old_insert_end = 8;
}
message ResolveInlayHint {
@@ -699,3 +703,18 @@ message LanguageServerIdForName {
message LanguageServerIdForNameResponse {
optional uint64 server_id = 1;
}
message LspExtRunnables {
uint64 project_id = 1;
uint64 buffer_id = 2;
optional Anchor position = 3;
}
message LspExtRunnablesResponse {
repeated LspRunnable runnables = 1;
}
message LspRunnable {
bytes task_template = 1;
optional LocationLink location = 2;
}

View File

@@ -372,12 +372,15 @@ message Envelope {
GetDocumentSymbolsResponse get_document_symbols_response = 331;
LanguageServerIdForName language_server_id_for_name = 332;
LanguageServerIdForNameResponse language_server_id_for_name_response = 333; // current max
LanguageServerIdForNameResponse language_server_id_for_name_response = 333;
LoadCommitDiff load_commit_diff = 334;
LoadCommitDiffResponse load_commit_diff_response = 335;
StopLanguageServers stop_language_servers = 336; // current max
StopLanguageServers stop_language_servers = 336;
LspExtRunnables lsp_ext_runnables = 337;
LspExtRunnablesResponse lsp_ext_runnables_response = 338; // current max
}
reserved 87 to 88;

View File

@@ -171,6 +171,8 @@ messages!(
(LspExtExpandMacroResponse, Background),
(LspExtOpenDocs, Background),
(LspExtOpenDocsResponse, Background),
(LspExtRunnables, Background),
(LspExtRunnablesResponse, Background),
(LspExtSwitchSourceHeader, Background),
(LspExtSwitchSourceHeaderResponse, Background),
(MarkNotificationRead, Foreground),
@@ -414,6 +416,7 @@ request_messages!(
(LanguageServerIdForName, LanguageServerIdForNameResponse),
(LspExtExpandMacro, LspExtExpandMacroResponse),
(LspExtOpenDocs, LspExtOpenDocsResponse),
(LspExtRunnables, LspExtRunnablesResponse),
(SetRoomParticipantRole, Ack),
(BlameBuffer, BlameBufferResponse),
(RejoinRemoteProjects, RejoinRemoteProjectsResponse),
@@ -537,6 +540,7 @@ entity_messages!(
UpdateWorktreeSettings,
LspExtExpandMacro,
LspExtOpenDocs,
LspExtRunnables,
AdvertiseContexts,
OpenContext,
CreateContext,

View File

@@ -18,9 +18,7 @@ pub enum ComponentStory {
ContextMenu,
Cursor,
DefaultColors,
Disclosure,
Focus,
Icon,
IconButton,
Keybinding,
List,
@@ -35,7 +33,6 @@ pub enum ComponentStory {
ToggleButton,
ViewportUnits,
WithRemSize,
Vector,
}
impl ComponentStory {
@@ -51,9 +48,7 @@ impl ComponentStory {
Self::ContextMenu => cx.new(|_| ui::ContextMenuStory).into(),
Self::Cursor => cx.new(|_| crate::stories::CursorStory).into(),
Self::DefaultColors => DefaultColorsStory::model(cx).into(),
Self::Disclosure => cx.new(|_| ui::DisclosureStory).into(),
Self::Focus => FocusStory::model(window, cx).into(),
Self::Icon => cx.new(|_| ui::IconStory).into(),
Self::IconButton => cx.new(|_| ui::IconButtonStory).into(),
Self::Keybinding => cx.new(|_| ui::KeybindingStory).into(),
Self::List => cx.new(|_| ui::ListStory).into(),
@@ -68,7 +63,6 @@ impl ComponentStory {
Self::ToggleButton => cx.new(|_| ui::ToggleButtonStory).into(),
Self::ViewportUnits => cx.new(|_| crate::stories::ViewportUnitsStory).into(),
Self::WithRemSize => cx.new(|_| crate::stories::WithRemSizeStory).into(),
Self::Vector => cx.new(|_| ui::VectorStory).into(),
}
}
}

View File

@@ -450,7 +450,7 @@ impl PickerDelegate for TabSwitcherDelegate {
IconButton::new("close_tab", IconName::Close)
.icon_size(IconSize::Small)
.icon_color(indicator_color)
.tooltip(Tooltip::text("Close"))
.tooltip(Tooltip::for_action_title("Close", &CloseSelectedItem))
.on_click(cx.listener(move |picker, _, window, cx| {
cx.stop_propagation();
picker.delegate.close_item_at(ix, window, cx);

View File

@@ -13,11 +13,13 @@ path = "src/tasks_ui.rs"
[dependencies]
anyhow.workspace = true
collections.workspace = true
debugger_ui.workspace = true
editor.workspace = true
file_icons.workspace = true
fuzzy.workspace = true
feature_flags.workspace = true
itertools.workspace = true
gpui.workspace = true
menu.workspace = true
picker.workspace = true

View File

@@ -7,6 +7,7 @@ use gpui::{
Focusable, InteractiveElement, ParentElement, Render, SharedString, Styled, Subscription, Task,
WeakEntity, Window, rems,
};
use itertools::Itertools;
use picker::{Picker, PickerDelegate, highlighted_match_with_paths::HighlightedMatch};
use project::{TaskSourceKind, task_store::TaskStore};
use task::{
@@ -221,42 +222,66 @@ impl PickerDelegate for TasksModalDelegate {
cx: &mut Context<picker::Picker<Self>>,
) -> Task<()> {
let task_type = self.task_modal_type.clone();
cx.spawn_in(window, async move |picker, cx| {
let Some(candidates) = picker
.update(cx, |picker, cx| match &mut picker.delegate.candidates {
Some(candidates) => string_match_candidates(candidates.iter(), task_type),
None => {
let Some(task_inventory) = picker
.delegate
.task_store
.read(cx)
.task_inventory()
.cloned()
else {
let candidates = match &self.candidates {
Some(candidates) => Task::ready(string_match_candidates(candidates, task_type)),
None => {
if let Some(task_inventory) = self.task_store.read(cx).task_inventory().cloned() {
let (used, current) = task_inventory
.read(cx)
.used_and_current_resolved_tasks(&self.task_contexts, cx);
let workspace = self.workspace.clone();
let lsp_task_sources = self.task_contexts.lsp_task_sources.clone();
let task_position = self.task_contexts.latest_selection;
cx.spawn(async move |picker, cx| {
let Ok(lsp_tasks) = workspace.update(cx, |workspace, cx| {
editor::lsp_tasks(
workspace.project().clone(),
&lsp_task_sources,
task_position,
cx,
)
}) else {
return Vec::new();
};
let (used, current) = task_inventory
.read(cx)
.used_and_current_resolved_tasks(&picker.delegate.task_contexts, cx);
picker.delegate.last_used_candidate_index = if used.is_empty() {
None
} else {
Some(used.len() - 1)
};
let lsp_tasks = lsp_tasks.await;
picker
.update(cx, |picker, _| {
picker.delegate.last_used_candidate_index = if used.is_empty() {
None
} else {
Some(used.len() - 1)
};
let mut new_candidates = used;
new_candidates.extend(current);
let match_candidates =
string_match_candidates(new_candidates.iter(), task_type);
let _ = picker.delegate.candidates.insert(new_candidates);
match_candidates
}
})
.ok()
else {
return;
};
let mut new_candidates = used;
new_candidates.extend(lsp_tasks.into_iter().flat_map(
|(kind, tasks_with_locations)| {
tasks_with_locations
.into_iter()
.sorted_by_key(|(location, task)| {
(location.is_none(), task.resolved_label.clone())
})
.map(move |(_, task)| (kind.clone(), task))
},
));
new_candidates.extend(current);
let match_candidates =
string_match_candidates(&new_candidates, task_type);
let _ = picker.delegate.candidates.insert(new_candidates);
match_candidates
})
.ok()
.unwrap_or_default()
})
} else {
Task::ready(Vec::new())
}
}
};
cx.spawn_in(window, async move |picker, cx| {
let candidates = candidates.await;
let matches = fuzzy::match_strings(
&candidates,
&query,
@@ -426,6 +451,7 @@ impl PickerDelegate for TasksModalDelegate {
color: Color::Default,
};
let icon = match source_kind {
TaskSourceKind::Lsp(..) => Some(Icon::new(IconName::Bolt)),
TaskSourceKind::UserInput => Some(Icon::new(IconName::Terminal)),
TaskSourceKind::AbsPath { .. } => Some(Icon::new(IconName::Settings)),
TaskSourceKind::Worktree { .. } => Some(Icon::new(IconName::FileTree)),
@@ -697,10 +723,11 @@ impl PickerDelegate for TasksModalDelegate {
}
fn string_match_candidates<'a>(
candidates: impl Iterator<Item = &'a (TaskSourceKind, ResolvedTask)> + 'a,
candidates: impl IntoIterator<Item = &'a (TaskSourceKind, ResolvedTask)> + 'a,
task_modal_type: TaskModal,
) -> Vec<StringMatchCandidate> {
candidates
.into_iter()
.enumerate()
.filter(|(_, (_, candidate))| match candidate.task_type() {
TaskType::Script => task_modal_type == TaskModal::ScriptModal,

View File

@@ -1,6 +1,6 @@
use std::collections::HashMap;
use std::path::Path;
use collections::HashMap;
use debugger_ui::Start;
use editor::Editor;
use feature_flags::{Debugger, FeatureFlagViewExt};
@@ -313,6 +313,17 @@ fn task_contexts(workspace: &Workspace, window: &mut Window, cx: &mut App) -> Ta
})
});
let lsp_task_sources = active_editor
.as_ref()
.map(|active_editor| active_editor.update(cx, |editor, cx| editor.lsp_task_sources(cx)))
.unwrap_or_default();
let latest_selection = active_editor.as_ref().map(|active_editor| {
active_editor.update(cx, |editor, _| {
editor.selections.newest_anchor().head().text_anchor
})
});
let mut worktree_abs_paths = workspace
.worktrees(cx)
.filter(|worktree| is_visible_directory(worktree, cx))
@@ -325,6 +336,9 @@ fn task_contexts(workspace: &Workspace, window: &mut Window, cx: &mut App) -> Ta
cx.background_spawn(async move {
let mut task_contexts = TaskContexts::default();
task_contexts.lsp_task_sources = lsp_task_sources;
task_contexts.latest_selection = latest_selection;
if let Some(editor_context_task) = editor_context_task {
if let Some(editor_context) = editor_context_task.await {
task_contexts.active_item_context =

View File

@@ -2231,6 +2231,7 @@ impl BufferSnapshot {
} else if *anchor == Anchor::MAX {
self.visible_text.len()
} else {
debug_assert!(anchor.buffer_id == Some(self.remote_id));
let anchor_key = InsertionFragmentKey {
timestamp: anchor.timestamp,
split_offset: anchor.offset,

View File

@@ -28,6 +28,7 @@ strum.workspace = true
theme.workspace = true
ui_macros.workspace = true
util.workspace = true
documented = "0.9.1"
workspace-hack.workspace = true
[target.'cfg(windows)'.dependencies]

View File

@@ -0,0 +1,5 @@
pub use component::{
Component, ComponentScope, example_group, example_group_with_title, single_example,
};
pub use documented::Documented;
pub use ui_macros::RegisterComponent;

View File

@@ -73,7 +73,5 @@ pub use table::*;
pub use toggle::*;
pub use tooltip::*;
#[cfg(feature = "stories")]
pub use image::story::*;
#[cfg(feature = "stories")]
pub use stories::*;

View File

@@ -1,5 +1,6 @@
use crate::prelude::*;
use documented::Documented;
use gpui::{AnyElement, Hsla, ImageSource, Img, IntoElement, Styled, img};
/// An element that renders a user avatar with customizable appearance options.
@@ -14,7 +15,7 @@ use gpui::{AnyElement, Hsla, ImageSource, Img, IntoElement, Styled, img};
/// .grayscale(true)
/// .border_color(gpui::red());
/// ```
#[derive(IntoElement, IntoComponent)]
#[derive(IntoElement, Documented, RegisterComponent)]
pub struct Avatar {
image: Img,
size: Option<AbsoluteLength>,
@@ -219,84 +220,102 @@ impl RenderOnce for AvatarAvailabilityIndicator {
}
// View this component preview using `workspace: open component-preview`
impl ComponentPreview for Avatar {
fn preview(_window: &mut Window, cx: &mut App) -> AnyElement {
impl Component for Avatar {
fn scope() -> ComponentScope {
ComponentScope::Collaboration
}
fn description() -> Option<&'static str> {
Some(Avatar::DOCS)
}
fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
let example_avatar = "https://avatars.githubusercontent.com/u/1714999?v=4";
v_flex()
.gap_6()
.children(vec![
example_group_with_title(
"Sizes",
vec![
single_example("Default", Avatar::new(example_avatar).into_any_element()),
single_example(
"Small",
Avatar::new(example_avatar).size(px(24.)).into_any_element(),
),
single_example(
"Large",
Avatar::new(example_avatar).size(px(48.)).into_any_element(),
),
],
),
example_group_with_title(
"Styles",
vec![
single_example("Default", Avatar::new(example_avatar).into_any_element()),
single_example(
"Grayscale",
Avatar::new(example_avatar)
.grayscale(true)
.into_any_element(),
),
single_example(
"With Border",
Avatar::new(example_avatar)
.border_color(cx.theme().colors().border)
.into_any_element(),
),
],
),
example_group_with_title(
"Audio Status",
vec![
single_example(
"Muted",
Avatar::new(example_avatar)
.indicator(AvatarAudioStatusIndicator::new(AudioStatus::Muted))
.into_any_element(),
),
single_example(
"Deafened",
Avatar::new(example_avatar)
.indicator(AvatarAudioStatusIndicator::new(AudioStatus::Deafened))
.into_any_element(),
),
],
),
example_group_with_title(
"Availability",
vec![
single_example(
"Free",
Avatar::new(example_avatar)
.indicator(AvatarAvailabilityIndicator::new(
CollaboratorAvailability::Free,
))
.into_any_element(),
),
single_example(
"Busy",
Avatar::new(example_avatar)
.indicator(AvatarAvailabilityIndicator::new(
CollaboratorAvailability::Busy,
))
.into_any_element(),
),
],
),
])
.into_any_element()
Some(
v_flex()
.gap_6()
.children(vec![
example_group_with_title(
"Sizes",
vec![
single_example(
"Default",
Avatar::new(example_avatar).into_any_element(),
),
single_example(
"Small",
Avatar::new(example_avatar).size(px(24.)).into_any_element(),
),
single_example(
"Large",
Avatar::new(example_avatar).size(px(48.)).into_any_element(),
),
],
),
example_group_with_title(
"Styles",
vec![
single_example(
"Default",
Avatar::new(example_avatar).into_any_element(),
),
single_example(
"Grayscale",
Avatar::new(example_avatar)
.grayscale(true)
.into_any_element(),
),
single_example(
"With Border",
Avatar::new(example_avatar)
.border_color(cx.theme().colors().border)
.into_any_element(),
),
],
),
example_group_with_title(
"Audio Status",
vec![
single_example(
"Muted",
Avatar::new(example_avatar)
.indicator(AvatarAudioStatusIndicator::new(AudioStatus::Muted))
.into_any_element(),
),
single_example(
"Deafened",
Avatar::new(example_avatar)
.indicator(AvatarAudioStatusIndicator::new(
AudioStatus::Deafened,
))
.into_any_element(),
),
],
),
example_group_with_title(
"Availability",
vec![
single_example(
"Free",
Avatar::new(example_avatar)
.indicator(AvatarAvailabilityIndicator::new(
CollaboratorAvailability::Free,
))
.into_any_element(),
),
single_example(
"Busy",
Avatar::new(example_avatar)
.indicator(AvatarAvailabilityIndicator::new(
CollaboratorAvailability::Busy,
))
.into_any_element(),
),
],
),
])
.into_any_element(),
)
}
}

View File

@@ -28,8 +28,7 @@ pub enum Severity {
/// .icon_position(IconPosition::End),
/// )
/// ```
#[derive(IntoElement, IntoComponent)]
#[component(scope = "Notification")]
#[derive(IntoElement, RegisterComponent)]
pub struct Banner {
severity: Severity,
children: Option<AnyElement>,
@@ -137,8 +136,12 @@ impl RenderOnce for Banner {
}
}
impl ComponentPreview for Banner {
fn preview(_window: &mut Window, _cx: &mut App) -> AnyElement {
impl Component for Banner {
fn scope() -> ComponentScope {
ComponentScope::Notification
}
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
let severity_examples = vec![
single_example(
"Default",
@@ -185,8 +188,10 @@ impl ComponentPreview for Banner {
),
];
example_group(severity_examples)
.vertical()
.into_any_element()
Some(
example_group(severity_examples)
.vertical()
.into_any_element(),
)
}
}

View File

@@ -1,6 +1,6 @@
use component::{ComponentPreview, example_group_with_title, single_example};
use crate::component_prelude::*;
use gpui::{AnyElement, AnyView, DefiniteLength};
use ui_macros::IntoComponent;
use ui_macros::RegisterComponent;
use crate::{ButtonCommon, ButtonLike, ButtonSize, ButtonStyle, IconName, IconSize, Label};
use crate::{
@@ -77,8 +77,7 @@ use super::button_icon::ButtonIcon;
/// });
/// ```
///
#[derive(IntoElement, IntoComponent)]
#[component(scope = "Input")]
#[derive(IntoElement, Documented, RegisterComponent)]
pub struct Button {
base: ButtonLike,
label: SharedString,
@@ -466,121 +465,135 @@ impl RenderOnce for Button {
}
// View this component preview using `workspace: open component-preview`
impl ComponentPreview for Button {
fn preview(_window: &mut Window, _cx: &mut App) -> AnyElement {
v_flex()
.gap_6()
.children(vec![
example_group_with_title(
"Button Styles",
vec![
single_example(
"Default",
Button::new("default", "Default").into_any_element(),
),
single_example(
"Filled",
Button::new("filled", "Filled")
.style(ButtonStyle::Filled)
.into_any_element(),
),
single_example(
"Subtle",
Button::new("outline", "Subtle")
.style(ButtonStyle::Subtle)
.into_any_element(),
),
single_example(
"Tinted",
Button::new("tinted_accent_style", "Accent")
.style(ButtonStyle::Tinted(TintColor::Accent))
.into_any_element(),
),
single_example(
"Transparent",
Button::new("transparent", "Transparent")
.style(ButtonStyle::Transparent)
.into_any_element(),
),
],
),
example_group_with_title(
"Tint Styles",
vec![
single_example(
"Accent",
Button::new("tinted_accent", "Accent")
.style(ButtonStyle::Tinted(TintColor::Accent))
.into_any_element(),
),
single_example(
"Error",
Button::new("tinted_negative", "Error")
.style(ButtonStyle::Tinted(TintColor::Error))
.into_any_element(),
),
single_example(
"Warning",
Button::new("tinted_warning", "Warning")
.style(ButtonStyle::Tinted(TintColor::Warning))
.into_any_element(),
),
single_example(
"Success",
Button::new("tinted_positive", "Success")
.style(ButtonStyle::Tinted(TintColor::Success))
.into_any_element(),
),
],
),
example_group_with_title(
"Special States",
vec![
single_example(
"Default",
Button::new("default_state", "Default").into_any_element(),
),
single_example(
"Disabled",
Button::new("disabled", "Disabled")
.disabled(true)
.into_any_element(),
),
single_example(
"Selected",
Button::new("selected", "Selected")
.toggle_state(true)
.into_any_element(),
),
],
),
example_group_with_title(
"Buttons with Icons",
vec![
single_example(
"Icon Start",
Button::new("icon_start", "Icon Start")
.icon(IconName::Check)
.icon_position(IconPosition::Start)
.into_any_element(),
),
single_example(
"Icon End",
Button::new("icon_end", "Icon End")
.icon(IconName::Check)
.icon_position(IconPosition::End)
.into_any_element(),
),
single_example(
"Icon Color",
Button::new("icon_color", "Icon Color")
.icon(IconName::Check)
.icon_color(Color::Accent)
.into_any_element(),
),
],
),
])
.into_any_element()
impl Component for Button {
fn scope() -> ComponentScope {
ComponentScope::Input
}
fn sort_name() -> &'static str {
"ButtonA"
}
fn description() -> Option<&'static str> {
Some("A button triggers an event or action.")
}
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
Some(
v_flex()
.gap_6()
.children(vec![
example_group_with_title(
"Button Styles",
vec![
single_example(
"Default",
Button::new("default", "Default").into_any_element(),
),
single_example(
"Filled",
Button::new("filled", "Filled")
.style(ButtonStyle::Filled)
.into_any_element(),
),
single_example(
"Subtle",
Button::new("outline", "Subtle")
.style(ButtonStyle::Subtle)
.into_any_element(),
),
single_example(
"Tinted",
Button::new("tinted_accent_style", "Accent")
.style(ButtonStyle::Tinted(TintColor::Accent))
.into_any_element(),
),
single_example(
"Transparent",
Button::new("transparent", "Transparent")
.style(ButtonStyle::Transparent)
.into_any_element(),
),
],
),
example_group_with_title(
"Tint Styles",
vec![
single_example(
"Accent",
Button::new("tinted_accent", "Accent")
.style(ButtonStyle::Tinted(TintColor::Accent))
.into_any_element(),
),
single_example(
"Error",
Button::new("tinted_negative", "Error")
.style(ButtonStyle::Tinted(TintColor::Error))
.into_any_element(),
),
single_example(
"Warning",
Button::new("tinted_warning", "Warning")
.style(ButtonStyle::Tinted(TintColor::Warning))
.into_any_element(),
),
single_example(
"Success",
Button::new("tinted_positive", "Success")
.style(ButtonStyle::Tinted(TintColor::Success))
.into_any_element(),
),
],
),
example_group_with_title(
"Special States",
vec![
single_example(
"Default",
Button::new("default_state", "Default").into_any_element(),
),
single_example(
"Disabled",
Button::new("disabled", "Disabled")
.disabled(true)
.into_any_element(),
),
single_example(
"Selected",
Button::new("selected", "Selected")
.toggle_state(true)
.into_any_element(),
),
],
),
example_group_with_title(
"Buttons with Icons",
vec![
single_example(
"Icon Start",
Button::new("icon_start", "Icon Start")
.icon(IconName::Check)
.icon_position(IconPosition::Start)
.into_any_element(),
),
single_example(
"Icon End",
Button::new("icon_end", "Icon End")
.icon(IconName::Check)
.icon_position(IconPosition::End)
.into_any_element(),
),
single_example(
"Icon Color",
Button::new("icon_color", "Icon Color")
.icon(IconName::Check)
.icon_color(Color::Accent)
.into_any_element(),
),
],
),
])
.into_any_element(),
)
}
}

View File

@@ -5,7 +5,7 @@ use gpui::Hsla;
///
/// Can be used as either an icon alongside a label, like in [`Button`](crate::Button),
/// or as a standalone icon, like in [`IconButton`](crate::IconButton).
#[derive(IntoElement)]
#[derive(IntoElement, RegisterComponent)]
pub(super) struct ButtonIcon {
icon: IconName,
size: IconSize,
@@ -39,7 +39,6 @@ impl ButtonIcon {
if let Some(size) = size.into() {
self.size = size;
}
self
}
@@ -47,7 +46,6 @@ impl ButtonIcon {
if let Some(color) = color.into() {
self.color = color;
}
self
}
@@ -120,3 +118,82 @@ impl RenderOnce for ButtonIcon {
}
}
}
impl Component for ButtonIcon {
fn scope() -> ComponentScope {
ComponentScope::Input
}
fn name() -> &'static str {
"ButtonIcon"
}
fn description() -> Option<&'static str> {
Some("An icon component specifically designed for use within buttons.")
}
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
Some(
v_flex()
.gap_6()
.children(vec![
example_group_with_title(
"Basic Usage",
vec![
single_example(
"Default",
ButtonIcon::new(IconName::Star).into_any_element(),
),
single_example(
"Custom Size",
ButtonIcon::new(IconName::Star)
.size(IconSize::Medium)
.into_any_element(),
),
single_example(
"Custom Color",
ButtonIcon::new(IconName::Star)
.color(Color::Accent)
.into_any_element(),
),
],
),
example_group_with_title(
"States",
vec![
single_example(
"Selected",
ButtonIcon::new(IconName::Star)
.toggle_state(true)
.into_any_element(),
),
single_example(
"Disabled",
ButtonIcon::new(IconName::Star)
.disabled(true)
.into_any_element(),
),
],
),
example_group_with_title(
"With Indicator",
vec![
single_example(
"Default Indicator",
ButtonIcon::new(IconName::Star)
.indicator(Indicator::dot())
.into_any_element(),
),
single_example(
"Custom Indicator",
ButtonIcon::new(IconName::Star)
.indicator(Indicator::dot().color(Color::Error))
.into_any_element(),
),
],
),
])
.into_any_element(),
)
}
}

View File

@@ -1,5 +1,8 @@
use gpui::{AnyElement, AnyView, ClickEvent, Hsla, Rems, transparent_black};
use gpui::{CursorStyle, DefiniteLength, MouseButton, MouseDownEvent, MouseUpEvent, relative};
use documented::Documented;
use gpui::{
AnyElement, AnyView, ClickEvent, CursorStyle, DefiniteLength, Hsla, MouseButton,
MouseDownEvent, MouseUpEvent, Rems, relative, transparent_black,
};
use smallvec::SmallVec;
use crate::{DynamicSpacing, ElevationIndex, prelude::*};
@@ -343,7 +346,7 @@ impl ButtonSize {
/// unconstrained and may make the UI feel less consistent.
///
/// This is also used to build the prebuilt buttons.
#[derive(IntoElement)]
#[derive(IntoElement, Documented, RegisterComponent)]
pub struct ButtonLike {
pub(super) base: Div,
id: ElementId,
@@ -585,3 +588,99 @@ impl RenderOnce for ButtonLike {
.children(self.children)
}
}
impl Component for ButtonLike {
fn scope() -> ComponentScope {
ComponentScope::Input
}
fn sort_name() -> &'static str {
// ButtonLike should be at the bottom of the button list
"ButtonZ"
}
fn description() -> Option<&'static str> {
Some(ButtonLike::DOCS)
}
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
Some(
v_flex()
.gap_6()
.children(vec![
example_group(vec![
single_example(
"Default",
ButtonLike::new("default")
.child(Label::new("Default"))
.into_any_element(),
),
single_example(
"Filled",
ButtonLike::new("filled")
.style(ButtonStyle::Filled)
.child(Label::new("Filled"))
.into_any_element(),
),
single_example(
"Subtle",
ButtonLike::new("outline")
.style(ButtonStyle::Subtle)
.child(Label::new("Subtle"))
.into_any_element(),
),
single_example(
"Tinted",
ButtonLike::new("tinted_accent_style")
.style(ButtonStyle::Tinted(TintColor::Accent))
.child(Label::new("Accent"))
.into_any_element(),
),
single_example(
"Transparent",
ButtonLike::new("transparent")
.style(ButtonStyle::Transparent)
.child(Label::new("Transparent"))
.into_any_element(),
),
]),
example_group_with_title(
"Button Group Constructors",
vec![
single_example(
"Left Rounded",
ButtonLike::new_rounded_left("left_rounded")
.child(Label::new("Left Rounded"))
.style(ButtonStyle::Filled)
.into_any_element(),
),
single_example(
"Right Rounded",
ButtonLike::new_rounded_right("right_rounded")
.child(Label::new("Right Rounded"))
.style(ButtonStyle::Filled)
.into_any_element(),
),
single_example(
"Button Group",
h_flex()
.gap_px()
.child(
ButtonLike::new_rounded_left("bg_left")
.child(Label::new("Left"))
.style(ButtonStyle::Filled),
)
.child(
ButtonLike::new_rounded_right("bg_right")
.child(Label::new("Right"))
.style(ButtonStyle::Filled),
)
.into_any_element(),
),
],
),
])
.into_any_element(),
)
}
}

View File

@@ -13,8 +13,7 @@ pub enum IconButtonShape {
Wide,
}
#[derive(IntoElement, IntoComponent)]
#[component(scope = "Input")]
#[derive(IntoElement, RegisterComponent)]
pub struct IconButton {
base: ButtonLike,
shape: IconButtonShape,
@@ -210,159 +209,169 @@ impl RenderOnce for IconButton {
}
}
impl ComponentPreview for IconButton {
fn preview(_window: &mut Window, _cx: &mut App) -> AnyElement {
v_flex()
.gap_6()
.children(vec![
example_group_with_title(
"Icon Button Styles",
vec![
single_example(
"Default",
IconButton::new("default", IconName::Check)
.layer(ElevationIndex::Background)
.into_any_element(),
),
single_example(
"Filled",
IconButton::new("filled", IconName::Check)
.layer(ElevationIndex::Background)
.style(ButtonStyle::Filled)
.into_any_element(),
),
single_example(
"Subtle",
IconButton::new("subtle", IconName::Check)
.layer(ElevationIndex::Background)
.style(ButtonStyle::Subtle)
.into_any_element(),
),
single_example(
"Tinted",
IconButton::new("tinted", IconName::Check)
.layer(ElevationIndex::Background)
.style(ButtonStyle::Tinted(TintColor::Accent))
.into_any_element(),
),
single_example(
"Transparent",
IconButton::new("transparent", IconName::Check)
.layer(ElevationIndex::Background)
.style(ButtonStyle::Transparent)
.into_any_element(),
),
],
),
example_group_with_title(
"Icon Button Shapes",
vec![
single_example(
"Square",
IconButton::new("square", IconName::Check)
.shape(IconButtonShape::Square)
.style(ButtonStyle::Filled)
.layer(ElevationIndex::Background)
.into_any_element(),
),
single_example(
"Wide",
IconButton::new("wide", IconName::Check)
.shape(IconButtonShape::Wide)
.style(ButtonStyle::Filled)
.layer(ElevationIndex::Background)
.into_any_element(),
),
],
),
example_group_with_title(
"Icon Button Sizes",
vec![
single_example(
"Small",
IconButton::new("small", IconName::Check)
.icon_size(IconSize::XSmall)
.style(ButtonStyle::Filled)
.layer(ElevationIndex::Background)
.into_any_element(),
),
single_example(
"Small",
IconButton::new("small", IconName::Check)
.icon_size(IconSize::Small)
.style(ButtonStyle::Filled)
.layer(ElevationIndex::Background)
.into_any_element(),
),
single_example(
"Medium",
IconButton::new("medium", IconName::Check)
.icon_size(IconSize::Medium)
.style(ButtonStyle::Filled)
.layer(ElevationIndex::Background)
.into_any_element(),
),
single_example(
"XLarge",
IconButton::new("xlarge", IconName::Check)
.icon_size(IconSize::XLarge)
.style(ButtonStyle::Filled)
.layer(ElevationIndex::Background)
.into_any_element(),
),
],
),
example_group_with_title(
"Special States",
vec![
single_example(
"Disabled",
IconButton::new("disabled", IconName::Check)
.disabled(true)
.style(ButtonStyle::Filled)
.layer(ElevationIndex::Background)
.into_any_element(),
),
single_example(
"Selected",
IconButton::new("selected", IconName::Check)
.toggle_state(true)
.style(ButtonStyle::Filled)
.layer(ElevationIndex::Background)
.into_any_element(),
),
single_example(
"With Indicator",
IconButton::new("indicator", IconName::Check)
.indicator(Indicator::dot().color(Color::Success))
.style(ButtonStyle::Filled)
.layer(ElevationIndex::Background)
.into_any_element(),
),
],
),
example_group_with_title(
"Custom Colors",
vec![
single_example(
"Custom Icon Color",
IconButton::new("custom_color", IconName::Check)
.icon_color(Color::Accent)
.style(ButtonStyle::Filled)
.layer(ElevationIndex::Background)
.into_any_element(),
),
single_example(
"With Alpha",
IconButton::new("alpha", IconName::Check)
.alpha(0.5)
.style(ButtonStyle::Filled)
.layer(ElevationIndex::Background)
.into_any_element(),
),
],
),
])
.into_any_element()
impl Component for IconButton {
fn scope() -> ComponentScope {
ComponentScope::Input
}
fn sort_name() -> &'static str {
"ButtonB"
}
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
Some(
v_flex()
.gap_6()
.children(vec![
example_group_with_title(
"Icon Button Styles",
vec![
single_example(
"Default",
IconButton::new("default", IconName::Check)
.layer(ElevationIndex::Background)
.into_any_element(),
),
single_example(
"Filled",
IconButton::new("filled", IconName::Check)
.layer(ElevationIndex::Background)
.style(ButtonStyle::Filled)
.into_any_element(),
),
single_example(
"Subtle",
IconButton::new("subtle", IconName::Check)
.layer(ElevationIndex::Background)
.style(ButtonStyle::Subtle)
.into_any_element(),
),
single_example(
"Tinted",
IconButton::new("tinted", IconName::Check)
.layer(ElevationIndex::Background)
.style(ButtonStyle::Tinted(TintColor::Accent))
.into_any_element(),
),
single_example(
"Transparent",
IconButton::new("transparent", IconName::Check)
.layer(ElevationIndex::Background)
.style(ButtonStyle::Transparent)
.into_any_element(),
),
],
),
example_group_with_title(
"Icon Button Shapes",
vec![
single_example(
"Square",
IconButton::new("square", IconName::Check)
.shape(IconButtonShape::Square)
.style(ButtonStyle::Filled)
.layer(ElevationIndex::Background)
.into_any_element(),
),
single_example(
"Wide",
IconButton::new("wide", IconName::Check)
.shape(IconButtonShape::Wide)
.style(ButtonStyle::Filled)
.layer(ElevationIndex::Background)
.into_any_element(),
),
],
),
example_group_with_title(
"Icon Button Sizes",
vec![
single_example(
"XSmall",
IconButton::new("xsmall", IconName::Check)
.icon_size(IconSize::XSmall)
.style(ButtonStyle::Filled)
.layer(ElevationIndex::Background)
.into_any_element(),
),
single_example(
"Small",
IconButton::new("small", IconName::Check)
.icon_size(IconSize::Small)
.style(ButtonStyle::Filled)
.layer(ElevationIndex::Background)
.into_any_element(),
),
single_example(
"Medium",
IconButton::new("medium", IconName::Check)
.icon_size(IconSize::Medium)
.style(ButtonStyle::Filled)
.layer(ElevationIndex::Background)
.into_any_element(),
),
single_example(
"XLarge",
IconButton::new("xlarge", IconName::Check)
.icon_size(IconSize::XLarge)
.style(ButtonStyle::Filled)
.layer(ElevationIndex::Background)
.into_any_element(),
),
],
),
example_group_with_title(
"Special States",
vec![
single_example(
"Disabled",
IconButton::new("disabled", IconName::Check)
.disabled(true)
.style(ButtonStyle::Filled)
.layer(ElevationIndex::Background)
.into_any_element(),
),
single_example(
"Selected",
IconButton::new("selected", IconName::Check)
.toggle_state(true)
.style(ButtonStyle::Filled)
.layer(ElevationIndex::Background)
.into_any_element(),
),
single_example(
"With Indicator",
IconButton::new("indicator", IconName::Check)
.indicator(Indicator::dot().color(Color::Success))
.style(ButtonStyle::Filled)
.layer(ElevationIndex::Background)
.into_any_element(),
),
],
),
example_group_with_title(
"Custom Colors",
vec![
single_example(
"Custom Icon Color",
IconButton::new("custom_color", IconName::Check)
.icon_color(Color::Accent)
.style(ButtonStyle::Filled)
.layer(ElevationIndex::Background)
.into_any_element(),
),
single_example(
"With Alpha",
IconButton::new("alpha", IconName::Check)
.alpha(0.5)
.style(ButtonStyle::Filled)
.layer(ElevationIndex::Background)
.into_any_element(),
),
],
),
])
.into_any_element(),
)
}
}

View File

@@ -15,8 +15,7 @@ pub enum ToggleButtonPosition {
Last,
}
#[derive(IntoElement, IntoComponent)]
#[component(scope = "Input")]
#[derive(IntoElement, RegisterComponent)]
pub struct ToggleButton {
base: ButtonLike,
position_in_group: Option<ToggleButtonPosition>,
@@ -155,129 +154,139 @@ impl RenderOnce for ToggleButton {
}
}
impl ComponentPreview for ToggleButton {
fn preview(_window: &mut Window, _cx: &mut App) -> AnyElement {
v_flex()
.gap_6()
.children(vec![
example_group_with_title(
"Button Styles",
vec![
single_example(
"Off",
ToggleButton::new("off", "Off")
.layer(ElevationIndex::Background)
.style(ButtonStyle::Filled)
.into_any_element(),
),
single_example(
"On",
ToggleButton::new("on", "On")
.layer(ElevationIndex::Background)
.toggle_state(true)
.style(ButtonStyle::Filled)
.into_any_element(),
),
single_example(
"Off Disabled",
ToggleButton::new("disabled_off", "Disabled Off")
.layer(ElevationIndex::Background)
.disabled(true)
.style(ButtonStyle::Filled)
.into_any_element(),
),
single_example(
"On Disabled",
ToggleButton::new("disabled_on", "Disabled On")
.layer(ElevationIndex::Background)
.disabled(true)
.toggle_state(true)
.style(ButtonStyle::Filled)
.into_any_element(),
),
],
),
example_group_with_title(
"Button Group",
vec![
single_example(
"Three Buttons",
h_flex()
.child(
ToggleButton::new("three_btn_first", "First")
.layer(ElevationIndex::Background)
.style(ButtonStyle::Filled)
.first()
.into_any_element(),
)
.child(
ToggleButton::new("three_btn_middle", "Middle")
.layer(ElevationIndex::Background)
.style(ButtonStyle::Filled)
.middle()
.toggle_state(true)
.into_any_element(),
)
.child(
ToggleButton::new("three_btn_last", "Last")
.layer(ElevationIndex::Background)
.style(ButtonStyle::Filled)
.last()
.into_any_element(),
)
.into_any_element(),
),
single_example(
"Two Buttons",
h_flex()
.child(
ToggleButton::new("two_btn_first", "First")
.layer(ElevationIndex::Background)
.style(ButtonStyle::Filled)
.first()
.into_any_element(),
)
.child(
ToggleButton::new("two_btn_last", "Last")
.layer(ElevationIndex::Background)
.style(ButtonStyle::Filled)
.last()
.into_any_element(),
)
.into_any_element(),
),
],
),
example_group_with_title(
"Alternate Sizes",
vec![
single_example(
"None",
ToggleButton::new("none", "None")
.layer(ElevationIndex::Background)
.style(ButtonStyle::Filled)
.size(ButtonSize::None)
.into_any_element(),
),
single_example(
"Compact",
ToggleButton::new("compact", "Compact")
.layer(ElevationIndex::Background)
.style(ButtonStyle::Filled)
.size(ButtonSize::Compact)
.into_any_element(),
),
single_example(
"Large",
ToggleButton::new("large", "Large")
.layer(ElevationIndex::Background)
.style(ButtonStyle::Filled)
.size(ButtonSize::Large)
.into_any_element(),
),
],
),
])
.into_any_element()
impl Component for ToggleButton {
fn scope() -> ComponentScope {
ComponentScope::Input
}
fn sort_name() -> &'static str {
"ButtonC"
}
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
Some(
v_flex()
.gap_6()
.children(vec![
example_group_with_title(
"Button Styles",
vec![
single_example(
"Off",
ToggleButton::new("off", "Off")
.layer(ElevationIndex::Background)
.style(ButtonStyle::Filled)
.into_any_element(),
),
single_example(
"On",
ToggleButton::new("on", "On")
.layer(ElevationIndex::Background)
.toggle_state(true)
.style(ButtonStyle::Filled)
.into_any_element(),
),
single_example(
"Off Disabled",
ToggleButton::new("disabled_off", "Disabled Off")
.layer(ElevationIndex::Background)
.disabled(true)
.style(ButtonStyle::Filled)
.into_any_element(),
),
single_example(
"On Disabled",
ToggleButton::new("disabled_on", "Disabled On")
.layer(ElevationIndex::Background)
.disabled(true)
.toggle_state(true)
.style(ButtonStyle::Filled)
.into_any_element(),
),
],
),
example_group_with_title(
"Button Group",
vec![
single_example(
"Three Buttons",
h_flex()
.child(
ToggleButton::new("three_btn_first", "First")
.layer(ElevationIndex::Background)
.style(ButtonStyle::Filled)
.first()
.into_any_element(),
)
.child(
ToggleButton::new("three_btn_middle", "Middle")
.layer(ElevationIndex::Background)
.style(ButtonStyle::Filled)
.middle()
.toggle_state(true)
.into_any_element(),
)
.child(
ToggleButton::new("three_btn_last", "Last")
.layer(ElevationIndex::Background)
.style(ButtonStyle::Filled)
.last()
.into_any_element(),
)
.into_any_element(),
),
single_example(
"Two Buttons",
h_flex()
.child(
ToggleButton::new("two_btn_first", "First")
.layer(ElevationIndex::Background)
.style(ButtonStyle::Filled)
.first()
.into_any_element(),
)
.child(
ToggleButton::new("two_btn_last", "Last")
.layer(ElevationIndex::Background)
.style(ButtonStyle::Filled)
.last()
.into_any_element(),
)
.into_any_element(),
),
],
),
example_group_with_title(
"Alternate Sizes",
vec![
single_example(
"None",
ToggleButton::new("none", "None")
.layer(ElevationIndex::Background)
.style(ButtonStyle::Filled)
.size(ButtonSize::None)
.into_any_element(),
),
single_example(
"Compact",
ToggleButton::new("compact", "Compact")
.layer(ElevationIndex::Background)
.style(ButtonStyle::Filled)
.size(ButtonSize::Compact)
.into_any_element(),
),
single_example(
"Large",
ToggleButton::new("large", "Large")
.layer(ElevationIndex::Background)
.style(ButtonStyle::Filled)
.size(ButtonSize::Large)
.into_any_element(),
),
],
),
])
.into_any_element(),
)
}
}

View File

@@ -1,5 +1,5 @@
use crate::component_prelude::*;
use crate::prelude::*;
use component::{ComponentPreview, example_group, single_example};
use gpui::{AnyElement, IntoElement, ParentElement, StyleRefinement, Styled};
use smallvec::SmallVec;
@@ -23,8 +23,7 @@ pub fn h_container() -> ContentGroup {
}
/// A flexible container component that can hold other elements.
#[derive(IntoElement, IntoComponent)]
#[component(scope = "Layout")]
#[derive(IntoElement, Documented, RegisterComponent)]
pub struct ContentGroup {
base: Div,
border: bool,
@@ -83,51 +82,56 @@ impl RenderOnce for ContentGroup {
this.border_1().border_color(cx.theme().colors().border)
})
.rounded_sm()
.p_2()
.children(self.children)
}
}
// View this component preview using `workspace: open component-preview`
impl ComponentPreview for ContentGroup {
fn preview(_window: &mut Window, _cx: &mut App) -> AnyElement {
example_group(vec![
single_example(
"Default",
ContentGroup::new()
.flex_1()
.items_center()
.justify_center()
.h_48()
.child(Label::new("Default ContentBox"))
.into_any_element(),
)
.grow(),
single_example(
"Without Border",
ContentGroup::new()
.flex_1()
.items_center()
.justify_center()
.h_48()
.borderless()
.child(Label::new("Borderless ContentBox"))
.into_any_element(),
)
.grow(),
single_example(
"Without Fill",
ContentGroup::new()
.flex_1()
.items_center()
.justify_center()
.h_48()
.unfilled()
.child(Label::new("Unfilled ContentBox"))
.into_any_element(),
)
.grow(),
])
.into_any_element()
impl Component for ContentGroup {
fn scope() -> ComponentScope {
ComponentScope::Layout
}
fn description() -> Option<&'static str> {
Some(ContentGroup::DOCS)
}
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
Some(
example_group(vec![
single_example(
"Default",
ContentGroup::new()
.flex_1()
.items_center()
.justify_center()
.h_48()
.child(Label::new("Default ContentGroup"))
.into_any_element(),
).description("A contained style for laying out groups of content. Has a default background and border color."),
single_example(
"Without Border",
ContentGroup::new()
.flex_1()
.items_center()
.justify_center()
.h_48()
.borderless()
.child(Label::new("Borderless ContentGroup"))
.into_any_element(),
),
single_example(
"Without Fill",
ContentGroup::new()
.flex_1()
.items_center()
.justify_center()
.h_48()
.unfilled()
.child(Label::new("Unfilled ContentGroup"))
.into_any_element(),
),
])
.into_any_element(),
)
}
}

View File

@@ -4,7 +4,7 @@ use gpui::{ClickEvent, CursorStyle};
use crate::{Color, IconButton, IconButtonShape, IconName, IconSize, prelude::*};
#[derive(IntoElement)]
#[derive(IntoElement, RegisterComponent)]
pub struct Disclosure {
id: ElementId,
is_open: bool,
@@ -84,3 +84,55 @@ impl RenderOnce for Disclosure {
})
}
}
impl Component for Disclosure {
fn scope() -> ComponentScope {
ComponentScope::Navigation
}
fn description() -> Option<&'static str> {
Some(
"An interactive element used to show or hide content, typically used in expandable sections or tree-like structures.",
)
}
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
Some(
v_flex()
.gap_6()
.children(vec![
example_group_with_title(
"Disclosure States",
vec![
single_example(
"Closed",
Disclosure::new("closed", false).into_any_element(),
),
single_example(
"Open",
Disclosure::new("open", true).into_any_element(),
),
],
),
example_group_with_title(
"Interactive Example",
vec![single_example(
"Toggleable",
v_flex()
.gap_2()
.child(
Disclosure::new("interactive", false)
// .on_toggle(Some(Arc::new(|_, _, cx| {
// cx.refresh();
// })))
.into_any_element(),
)
.child(Label::new("Click to toggle"))
.into_any_element(),
)],
),
])
.into_any_element(),
)
}
}

View File

@@ -49,7 +49,7 @@ impl DividerColor {
}
}
#[derive(IntoElement)]
#[derive(IntoElement, RegisterComponent)]
pub struct Divider {
style: DividerStyle,
direction: DividerDirection,
@@ -158,3 +158,90 @@ impl Divider {
)
}
}
impl Component for Divider {
fn scope() -> ComponentScope {
ComponentScope::Layout
}
fn description() -> Option<&'static str> {
Some(
"Visual separator used to create divisions between groups of content or sections in a layout.",
)
}
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
Some(
v_flex()
.gap_6()
.children(vec![
example_group_with_title(
"Horizontal Dividers",
vec![
single_example("Default", Divider::horizontal().into_any_element()),
single_example(
"Border Color",
Divider::horizontal()
.color(DividerColor::Border)
.into_any_element(),
),
single_example(
"Inset",
Divider::horizontal().inset().into_any_element(),
),
single_example(
"Dashed",
Divider::horizontal_dashed().into_any_element(),
),
],
),
example_group_with_title(
"Vertical Dividers",
vec![
single_example(
"Default",
div().h_16().child(Divider::vertical()).into_any_element(),
),
single_example(
"Border Color",
div()
.h_16()
.child(Divider::vertical().color(DividerColor::Border))
.into_any_element(),
),
single_example(
"Inset",
div()
.h_16()
.child(Divider::vertical().inset())
.into_any_element(),
),
single_example(
"Dashed",
div()
.h_16()
.child(Divider::vertical_dashed())
.into_any_element(),
),
],
),
example_group_with_title(
"Example Usage",
vec![single_example(
"Between Content",
v_flex()
.gap_4()
.px_4()
.child(Label::new("Section One"))
.child(Divider::horizontal())
.child(Label::new("Section Two"))
.child(Divider::horizontal_dashed())
.child(Label::new("Section Three"))
.into_any_element(),
)],
),
])
.into_any_element(),
)
}
}

View File

@@ -7,7 +7,7 @@ enum LabelKind {
Element(AnyElement),
}
#[derive(IntoElement)]
#[derive(IntoElement, RegisterComponent)]
pub struct DropdownMenu {
id: ElementId,
label: LabelKind,
@@ -72,6 +72,69 @@ impl RenderOnce for DropdownMenu {
}
}
impl Component for DropdownMenu {
fn scope() -> ComponentScope {
ComponentScope::Input
}
fn name() -> &'static str {
"DropdownMenu"
}
fn description() -> Option<&'static str> {
Some(
"A dropdown menu displays a list of actions or options. A dropdown menu is always activated by clicking a trigger (or via a keybinding).",
)
}
fn preview(window: &mut Window, cx: &mut App) -> Option<AnyElement> {
let menu = ContextMenu::build(window, cx, |this, _, _| {
this.entry("Option 1", None, |_, _| {})
.entry("Option 2", None, |_, _| {})
.entry("Option 3", None, |_, _| {})
.separator()
.entry("Option 4", None, |_, _| {})
});
Some(
v_flex()
.gap_6()
.children(vec![
example_group_with_title(
"Basic Usage",
vec![
single_example(
"Default",
DropdownMenu::new("default", "Select an option", menu.clone())
.into_any_element(),
),
single_example(
"Full Width",
DropdownMenu::new(
"full-width",
"Full Width Dropdown",
menu.clone(),
)
.full_width(true)
.into_any_element(),
),
],
),
example_group_with_title(
"States",
vec![single_example(
"Disabled",
DropdownMenu::new("disabled", "Disabled Dropdown", menu.clone())
.disabled(true)
.into_any_element(),
)],
),
])
.into_any_element(),
)
}
}
#[derive(IntoElement)]
struct DropdownMenuTrigger {
label: LabelKind,

View File

@@ -1,13 +1,31 @@
use crate::{Avatar, prelude::*};
use crate::component_prelude::*;
use crate::prelude::*;
use gpui::{AnyElement, StyleRefinement};
use smallvec::SmallVec;
/// A facepile is a collection of faces stacked horizontally
/// always with the leftmost face on top and descending in z-index
use super::Avatar;
/// An element that displays a collection of (usually) faces stacked
/// horizontally, with the left-most face on top, visually descending
/// from left to right.
///
/// Facepiles are used to display a group of people or things,
/// such as a list of participants in a collaboration session.
#[derive(IntoElement, IntoComponent)]
///
/// # Examples
///
/// ## Default
///
/// A default, horizontal facepile.
///
/// ```
/// use ui::{Avatar, Facepile, EXAMPLE_FACES};
///
/// Facepile::new(
/// EXAMPLE_FACES.iter().take(3).iter().map(|&url|
/// Avatar::new(url).into_any_element()).collect())
/// ```
#[derive(IntoElement, Documented, RegisterComponent)]
pub struct Facepile {
base: Div,
faces: SmallVec<[AnyElement; 2]>,
@@ -60,27 +78,37 @@ impl RenderOnce for Facepile {
}
}
impl ComponentPreview for Facepile {
fn preview(_window: &mut Window, _cx: &mut App) -> AnyElement {
let faces: [&'static str; 6] = [
"https://avatars.githubusercontent.com/u/326587?s=60&v=4",
"https://avatars.githubusercontent.com/u/2280405?s=60&v=4",
"https://avatars.githubusercontent.com/u/1789?s=60&v=4",
"https://avatars.githubusercontent.com/u/67129314?s=60&v=4",
"https://avatars.githubusercontent.com/u/482957?s=60&v=4",
"https://avatars.githubusercontent.com/u/1714999?s=60&v=4",
];
pub const EXAMPLE_FACES: [&'static str; 6] = [
"https://avatars.githubusercontent.com/u/326587?s=60&v=4",
"https://avatars.githubusercontent.com/u/2280405?s=60&v=4",
"https://avatars.githubusercontent.com/u/1789?s=60&v=4",
"https://avatars.githubusercontent.com/u/67129314?s=60&v=4",
"https://avatars.githubusercontent.com/u/482957?s=60&v=4",
"https://avatars.githubusercontent.com/u/1714999?s=60&v=4",
];
v_flex()
.gap_6()
.children(vec![
example_group_with_title(
impl Component for Facepile {
fn scope() -> ComponentScope {
ComponentScope::Collaboration
}
fn description() -> Option<&'static str> {
Some(
"Displays a collection of avatars or initials in a compact format. Often used to represent active collaborators or a subset of contributors.",
)
}
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
Some(
v_flex()
.gap_6()
.children(vec![example_group_with_title(
"Facepile Examples",
vec![
single_example(
"Default",
Facepile::new(
faces
EXAMPLE_FACES
.iter()
.map(|&url| Avatar::new(url).into_any_element())
.collect(),
@@ -90,7 +118,7 @@ impl ComponentPreview for Facepile {
single_example(
"Custom Size",
Facepile::new(
faces
EXAMPLE_FACES
.iter()
.map(|&url| Avatar::new(url).size(px(24.)).into_any_element())
.collect(),
@@ -98,19 +126,8 @@ impl ComponentPreview for Facepile {
.into_any_element(),
),
],
),
example_group_with_title(
"Special Cases",
vec![
single_example("Empty Facepile", Facepile::empty().into_any_element()),
single_example(
"Single Face",
Facepile::new(vec![Avatar::new(faces[0]).into_any_element()].into())
.into_any_element(),
),
],
),
])
.into_any_element()
)])
.into_any_element(),
)
}
}

View File

@@ -136,7 +136,7 @@ impl IconSource {
}
}
#[derive(IntoElement, IntoComponent)]
#[derive(IntoElement, RegisterComponent)]
pub struct Icon {
source: IconSource,
color: Color,
@@ -265,43 +265,54 @@ impl RenderOnce for IconWithIndicator {
}
}
// View this component preview using `workspace: open component-preview`
impl ComponentPreview for Icon {
fn preview(_window: &mut Window, _cx: &mut App) -> AnyElement {
v_flex()
.gap_6()
.children(vec![
example_group_with_title(
"Sizes",
vec![
single_example("Default", Icon::new(IconName::Star).into_any_element()),
single_example(
"Small",
Icon::new(IconName::Star)
.size(IconSize::Small)
.into_any_element(),
),
single_example(
"Large",
Icon::new(IconName::Star)
.size(IconSize::XLarge)
.into_any_element(),
),
],
),
example_group_with_title(
"Colors",
vec![
single_example("Default", Icon::new(IconName::Bell).into_any_element()),
single_example(
"Custom Color",
Icon::new(IconName::Bell)
.color(Color::Error)
.into_any_element(),
),
],
),
])
.into_any_element()
impl Component for Icon {
fn scope() -> ComponentScope {
ComponentScope::None
}
fn description() -> Option<&'static str> {
Some(
"A versatile icon component that supports SVG and image-based icons with customizable size, color, and transformations.",
)
}
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
Some(
v_flex()
.gap_6()
.children(vec![
example_group_with_title(
"Sizes",
vec![
single_example("Default", Icon::new(IconName::Star).into_any_element()),
single_example(
"Small",
Icon::new(IconName::Star)
.size(IconSize::Small)
.into_any_element(),
),
single_example(
"Large",
Icon::new(IconName::Star)
.size(IconSize::XLarge)
.into_any_element(),
),
],
),
example_group_with_title(
"Colors",
vec![
single_example("Default", Icon::new(IconName::Bell).into_any_element()),
single_example(
"Custom Color",
Icon::new(IconName::Bell)
.color(Color::Error)
.into_any_element(),
),
],
),
])
.into_any_element(),
)
}
}

View File

@@ -2,7 +2,7 @@ use gpui::{AnyElement, IntoElement, Point};
use crate::{IconDecoration, IconDecorationKind, prelude::*};
#[derive(IntoElement, IntoComponent)]
#[derive(IntoElement, RegisterComponent)]
pub struct DecoratedIcon {
icon: Icon,
decoration: Option<IconDecoration>,
@@ -24,9 +24,18 @@ impl RenderOnce for DecoratedIcon {
}
}
// View this component preview using `workspace: open component-preview`
impl ComponentPreview for DecoratedIcon {
fn preview(_window: &mut Window, cx: &mut App) -> AnyElement {
impl Component for DecoratedIcon {
fn scope() -> ComponentScope {
ComponentScope::None
}
fn description() -> Option<&'static str> {
Some(
"An icon with an optional decoration overlay (like an X, triangle, or dot) that can be positioned relative to the icon",
)
}
fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
let decoration_x = IconDecoration::new(
IconDecorationKind::X,
cx.theme().colors().surface_background,
@@ -60,32 +69,38 @@ impl ComponentPreview for DecoratedIcon {
y: px(-2.),
});
v_flex()
.gap_6()
.children(vec![example_group_with_title(
"Decorations",
vec![
single_example(
"No Decoration",
DecoratedIcon::new(Icon::new(IconName::FileDoc), None).into_any_element(),
),
single_example(
"X Decoration",
DecoratedIcon::new(Icon::new(IconName::FileDoc), Some(decoration_x))
Some(
v_flex()
.gap_6()
.children(vec![example_group_with_title(
"Decorations",
vec![
single_example(
"No Decoration",
DecoratedIcon::new(Icon::new(IconName::FileDoc), None)
.into_any_element(),
),
single_example(
"X Decoration",
DecoratedIcon::new(Icon::new(IconName::FileDoc), Some(decoration_x))
.into_any_element(),
),
single_example(
"Triangle Decoration",
DecoratedIcon::new(
Icon::new(IconName::FileDoc),
Some(decoration_triangle),
)
.into_any_element(),
),
single_example(
"Triangle Decoration",
DecoratedIcon::new(Icon::new(IconName::FileDoc), Some(decoration_triangle))
.into_any_element(),
),
single_example(
"Dot Decoration",
DecoratedIcon::new(Icon::new(IconName::FileDoc), Some(decoration_dot))
.into_any_element(),
),
],
)])
.into_any_element()
),
single_example(
"Dot Decoration",
DecoratedIcon::new(Icon::new(IconName::FileDoc), Some(decoration_dot))
.into_any_element(),
),
],
)])
.into_any_element(),
)
}
}

View File

@@ -4,6 +4,7 @@ use strum::{EnumIter, EnumString, IntoStaticStr};
use ui_macros::{DerivePathStr, path_str};
use crate::Color;
use crate::prelude::*;
#[derive(
Debug,
@@ -30,7 +31,7 @@ pub enum VectorName {
/// A [`Vector`] is different from an [`crate::Icon`] in that it is intended
/// to be displayed at a specific size, or series of sizes, rather
/// than conforming to the standard size of an icon.
#[derive(IntoElement)]
#[derive(IntoElement, RegisterComponent)]
pub struct Vector {
path: &'static str,
color: Color,
@@ -61,7 +62,6 @@ impl Vector {
/// Sets the vector size.
pub fn size(mut self, size: impl Into<Size<Rems>>) -> Self {
let size = size.into();
self.size = size;
self
}
@@ -83,24 +83,72 @@ impl RenderOnce for Vector {
}
}
#[cfg(feature = "stories")]
pub mod story {
use gpui::Render;
use story::{Story, StoryItem, StorySection};
use strum::IntoEnumIterator;
impl Component for Vector {
fn scope() -> ComponentScope {
ComponentScope::Images
}
use crate::prelude::*;
fn name() -> &'static str {
"Vector"
}
use super::{Vector, VectorName};
fn description() -> Option<&'static str> {
Some("A vector image component that can be displayed at specific sizes.")
}
pub struct VectorStory;
impl Render for VectorStory {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
Story::container().child(StorySection::new().children(VectorName::iter().map(
|vector| StoryItem::new(format!("{:?}", vector), Vector::square(vector, rems(8.))),
)))
}
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
Some(
v_flex()
.gap_6()
.children(vec![
example_group_with_title(
"Basic Usage",
vec![
single_example(
"Default",
Vector::square(VectorName::ZedLogo, rems(8.)).into_any_element(),
),
single_example(
"Custom Size",
Vector::new(VectorName::ZedLogo, rems(12.), rems(6.))
.into_any_element(),
),
],
),
example_group_with_title(
"Colored",
vec![
single_example(
"Accent Color",
Vector::square(VectorName::ZedLogo, rems(8.))
.color(Color::Accent)
.into_any_element(),
),
single_example(
"Error Color",
Vector::square(VectorName::ZedLogo, rems(8.))
.color(Color::Error)
.into_any_element(),
),
],
),
example_group_with_title(
"Different Vectors",
vec![
single_example(
"Zed Logo",
Vector::square(VectorName::ZedLogo, rems(8.)).into_any_element(),
),
single_example(
"Zed X Copilot",
Vector::square(VectorName::ZedXCopilot, rems(8.))
.into_any_element(),
),
],
),
])
.into_any_element(),
)
}
}

View File

@@ -1,4 +1,5 @@
use crate::{AnyIcon, prelude::*};
use super::AnyIcon;
use crate::prelude::*;
#[derive(Default)]
enum IndicatorKind {
@@ -8,7 +9,7 @@ enum IndicatorKind {
Icon(AnyIcon),
}
#[derive(IntoElement)]
#[derive(IntoElement, RegisterComponent)]
pub struct Indicator {
kind: IndicatorKind,
border_color: Option<Color>,
@@ -82,3 +83,95 @@ impl RenderOnce for Indicator {
}
}
}
impl Component for Indicator {
fn scope() -> ComponentScope {
ComponentScope::Status
}
fn description() -> Option<&'static str> {
Some(
"Visual indicators used to represent status, notifications, or draw attention to specific elements.",
)
}
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
Some(
v_flex()
.gap_6()
.children(vec![
example_group_with_title(
"Dot Indicators",
vec![
single_example("Default", Indicator::dot().into_any_element()),
single_example(
"Success",
Indicator::dot().color(Color::Success).into_any_element(),
),
single_example(
"Warning",
Indicator::dot().color(Color::Warning).into_any_element(),
),
single_example(
"Error",
Indicator::dot().color(Color::Error).into_any_element(),
),
single_example(
"With Border",
Indicator::dot()
.color(Color::Accent)
.border_color(Color::Default)
.into_any_element(),
),
],
),
example_group_with_title(
"Bar Indicators",
vec![
single_example("Default", Indicator::bar().into_any_element()),
single_example(
"Success",
Indicator::bar().color(Color::Success).into_any_element(),
),
single_example(
"Warning",
Indicator::bar().color(Color::Warning).into_any_element(),
),
single_example(
"Error",
Indicator::bar().color(Color::Error).into_any_element(),
),
],
),
example_group_with_title(
"Icon Indicators",
vec![
single_example(
"Default",
Indicator::icon(Icon::new(IconName::Circle)).into_any_element(),
),
single_example(
"Success",
Indicator::icon(Icon::new(IconName::Check))
.color(Color::Success)
.into_any_element(),
),
single_example(
"Warning",
Indicator::icon(Icon::new(IconName::Warning))
.color(Color::Warning)
.into_any_element(),
),
single_example(
"Error",
Indicator::icon(Icon::new(IconName::X))
.color(Color::Error)
.into_any_element(),
),
],
),
])
.into_any_element(),
)
}
}

View File

@@ -6,7 +6,7 @@ use gpui::{
};
use itertools::Itertools;
#[derive(Debug, IntoElement, Clone)]
#[derive(Debug, IntoElement, Clone, RegisterComponent)]
pub struct KeyBinding {
/// A keybinding consists of a key and a set of modifier keys.
/// More then one keybinding produces a chord.
@@ -449,6 +449,93 @@ fn keystroke_text(keystroke: &Keystroke, platform_style: PlatformStyle, vim_mode
text
}
impl Component for KeyBinding {
fn scope() -> ComponentScope {
ComponentScope::Input
}
fn name() -> &'static str {
"KeyBinding"
}
fn description() -> Option<&'static str> {
Some(
"A component that displays a key binding, supporting different platform styles and vim mode.",
)
}
fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
Some(
v_flex()
.gap_6()
.children(vec![
example_group_with_title(
"Basic Usage",
vec![
single_example(
"Default",
KeyBinding::new(
gpui::KeyBinding::new("ctrl-s", gpui::NoAction, None),
cx,
)
.into_any_element(),
),
single_example(
"Mac Style",
KeyBinding::new(
gpui::KeyBinding::new("cmd-s", gpui::NoAction, None),
cx,
)
.platform_style(PlatformStyle::Mac)
.into_any_element(),
),
single_example(
"Windows Style",
KeyBinding::new(
gpui::KeyBinding::new("ctrl-s", gpui::NoAction, None),
cx,
)
.platform_style(PlatformStyle::Windows)
.into_any_element(),
),
],
),
example_group_with_title(
"Vim Mode",
vec![single_example(
"Vim Mode Enabled",
KeyBinding::new(gpui::KeyBinding::new("dd", gpui::NoAction, None), cx)
.vim_mode(true)
.into_any_element(),
)],
),
example_group_with_title(
"Complex Bindings",
vec![
single_example(
"Multiple Keys",
KeyBinding::new(
gpui::KeyBinding::new("ctrl-k ctrl-b", gpui::NoAction, None),
cx,
)
.into_any_element(),
),
single_example(
"With Shift",
KeyBinding::new(
gpui::KeyBinding::new("shift-cmd-p", gpui::NoAction, None),
cx,
)
.into_any_element(),
),
],
),
])
.into_any_element(),
)
}
}
#[cfg(test)]
mod tests {
use super::*;

View File

@@ -18,7 +18,7 @@ use theme::Appearance;
/// .prefix("Save:")
/// .size(Pixels::from(14.0));
/// ```
#[derive(Debug, IntoElement, IntoComponent)]
#[derive(Debug, IntoElement, RegisterComponent)]
pub struct KeybindingHint {
prefix: Option<SharedString>,
suffix: Option<SharedString>,
@@ -205,68 +205,81 @@ impl RenderOnce for KeybindingHint {
}
}
// View this component preview using `workspace: open component-preview`
impl ComponentPreview for KeybindingHint {
fn preview(window: &mut Window, cx: &mut App) -> AnyElement {
impl Component for KeybindingHint {
fn scope() -> ComponentScope {
ComponentScope::None
}
fn description() -> Option<&'static str> {
Some("Displays a keyboard shortcut hint with optional prefix and suffix text")
}
fn preview(window: &mut Window, cx: &mut App) -> Option<AnyElement> {
let enter_fallback = gpui::KeyBinding::new("enter", menu::Confirm, None);
let enter = KeyBinding::for_action(&menu::Confirm, window, cx)
.unwrap_or(KeyBinding::new(enter_fallback, cx));
let bg_color = cx.theme().colors().surface_background;
v_flex()
.gap_6()
.children(vec![
example_group_with_title(
"Basic",
vec![
single_example(
"With Prefix",
KeybindingHint::with_prefix("Go to Start:", enter.clone(), bg_color)
Some(
v_flex()
.gap_6()
.children(vec![
example_group_with_title(
"Basic",
vec![
single_example(
"With Prefix",
KeybindingHint::with_prefix(
"Go to Start:",
enter.clone(),
bg_color,
)
.into_any_element(),
),
single_example(
"With Suffix",
KeybindingHint::with_suffix(enter.clone(), "Go to End", bg_color)
.into_any_element(),
),
single_example(
"With Prefix and Suffix",
KeybindingHint::new(enter.clone(), bg_color)
.prefix("Confirm:")
.suffix("Execute selected action")
.into_any_element(),
),
],
),
example_group_with_title(
"Sizes",
vec![
single_example(
"Small",
KeybindingHint::new(enter.clone(), bg_color)
.size(Pixels::from(12.0))
.prefix("Small:")
.into_any_element(),
),
single_example(
"Medium",
KeybindingHint::new(enter.clone(), bg_color)
.size(Pixels::from(16.0))
.suffix("Medium")
.into_any_element(),
),
single_example(
"Large",
KeybindingHint::new(enter.clone(), bg_color)
.size(Pixels::from(20.0))
.prefix("Large:")
.suffix("Size")
.into_any_element(),
),
],
),
])
.into_any_element()
),
single_example(
"With Suffix",
KeybindingHint::with_suffix(enter.clone(), "Go to End", bg_color)
.into_any_element(),
),
single_example(
"With Prefix and Suffix",
KeybindingHint::new(enter.clone(), bg_color)
.prefix("Confirm:")
.suffix("Execute selected action")
.into_any_element(),
),
],
),
example_group_with_title(
"Sizes",
vec![
single_example(
"Small",
KeybindingHint::new(enter.clone(), bg_color)
.size(Pixels::from(12.0))
.prefix("Small:")
.into_any_element(),
),
single_example(
"Medium",
KeybindingHint::new(enter.clone(), bg_color)
.size(Pixels::from(16.0))
.suffix("Medium")
.into_any_element(),
),
single_example(
"Large",
KeybindingHint::new(enter.clone(), bg_color)
.size(Pixels::from(20.0))
.prefix("Large:")
.suffix("Size")
.into_any_element(),
),
],
),
])
.into_any_element(),
)
}
}

View File

@@ -4,7 +4,7 @@ use gpui::{FontWeight, HighlightStyle, StyledText};
use crate::{LabelCommon, LabelLike, LabelSize, LineHeightStyle, prelude::*};
#[derive(IntoElement)]
#[derive(IntoElement, RegisterComponent)]
pub struct HighlightedLabel {
base: LabelLike,
label: SharedString,
@@ -129,3 +129,99 @@ impl RenderOnce for HighlightedLabel {
.child(StyledText::new(self.label).with_default_highlights(&text_style, highlights))
}
}
impl Component for HighlightedLabel {
fn scope() -> ComponentScope {
ComponentScope::Typography
}
fn name() -> &'static str {
"HighlightedLabel"
}
fn description() -> Option<&'static str> {
Some("A label with highlighted characters based on specified indices.")
}
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
Some(
v_flex()
.gap_6()
.children(vec![
example_group_with_title(
"Basic Usage",
vec![
single_example(
"Default",
HighlightedLabel::new("Highlighted Text", vec![0, 1, 2, 3]).into_any_element(),
),
single_example(
"Custom Color",
HighlightedLabel::new("Colored Highlight", vec![0, 1, 7, 8, 9])
.color(Color::Accent)
.into_any_element(),
),
],
),
example_group_with_title(
"Styles",
vec![
single_example(
"Bold",
HighlightedLabel::new("Bold Highlight", vec![0, 1, 2, 3])
.weight(FontWeight::BOLD)
.into_any_element(),
),
single_example(
"Italic",
HighlightedLabel::new("Italic Highlight", vec![0, 1, 6, 7, 8])
.italic()
.into_any_element(),
),
single_example(
"Underline",
HighlightedLabel::new("Underlined Highlight", vec![0, 1, 10, 11, 12])
.underline()
.into_any_element(),
),
],
),
example_group_with_title(
"Sizes",
vec![
single_example(
"Small",
HighlightedLabel::new("Small Highlight", vec![0, 1, 5, 6, 7])
.size(LabelSize::Small)
.into_any_element(),
),
single_example(
"Large",
HighlightedLabel::new("Large Highlight", vec![0, 1, 5, 6, 7])
.size(LabelSize::Large)
.into_any_element(),
),
],
),
example_group_with_title(
"Special Cases",
vec![
single_example(
"Single Line",
HighlightedLabel::new("Single Line Highlight\nWith Newline", vec![0, 1, 7, 8, 9])
.single_line()
.into_any_element(),
),
single_example(
"Truncate",
HighlightedLabel::new("This is a very long text that should be truncated with highlights", vec![0, 1, 2, 3, 4, 5])
.truncate()
.into_any_element(),
),
],
),
])
.into_any_element()
)
}
}

View File

@@ -29,7 +29,7 @@ use gpui::StyleRefinement;
///
/// let my_label = Label::new("Deleted").strikethrough(true);
/// ```
#[derive(IntoElement, IntoComponent)]
#[derive(IntoElement, RegisterComponent)]
pub struct Label {
base: LabelLike,
label: SharedString,
@@ -58,9 +58,6 @@ impl Label {
}
}
// nate: If we are going to do this, we might as well just
// impl Styled for Label and not constrain styles
// Style methods.
impl Label {
fn style(&mut self) -> &mut StyleRefinement {
@@ -200,12 +197,17 @@ impl RenderOnce for Label {
}
}
mod label_preview {
use crate::prelude::*;
impl Component for Label {
fn scope() -> ComponentScope {
ComponentScope::None
}
// View this component preview using `workspace: open component-preview`
impl ComponentPreview for Label {
fn preview(_window: &mut Window, _cx: &mut App) -> AnyElement {
fn description() -> Option<&'static str> {
Some("A text label component that supports various styles, sizes, and formatting options.")
}
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
Some(
v_flex()
.gap_6()
.children(vec![
@@ -251,6 +253,6 @@ mod label_preview {
),
])
.into_any_element()
}
)
}
}

View File

@@ -232,3 +232,70 @@ impl RenderOnce for LabelLike {
.children(self.children)
}
}
impl Component for LabelLike {
fn scope() -> ComponentScope {
ComponentScope::Typography
}
fn name() -> &'static str {
"LabelLike"
}
fn description() -> Option<&'static str> {
Some(
"A flexible, customizable label-like component that serves as a base for other label types.",
)
}
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
Some(
v_flex()
.gap_6()
.children(vec![
example_group_with_title(
"Sizes",
vec![
single_example("Default", LabelLike::new().child("Default size").into_any_element()),
single_example("Large", LabelLike::new().size(LabelSize::Large).child("Large size").into_any_element()),
single_example("Small", LabelLike::new().size(LabelSize::Small).child("Small size").into_any_element()),
single_example("XSmall", LabelLike::new().size(LabelSize::XSmall).child("Extra small size").into_any_element()),
],
),
example_group_with_title(
"Styles",
vec![
single_example("Bold", LabelLike::new().weight(FontWeight::BOLD).child("Bold text").into_any_element()),
single_example("Italic", LabelLike::new().italic().child("Italic text").into_any_element()),
single_example("Underline", LabelLike::new().underline().child("Underlined text").into_any_element()),
single_example("Strikethrough", LabelLike::new().strikethrough().child("Strikethrough text").into_any_element()),
],
),
example_group_with_title(
"Colors",
vec![
single_example("Default", LabelLike::new().child("Default color").into_any_element()),
single_example("Accent", LabelLike::new().color(Color::Accent).child("Accent color").into_any_element()),
single_example("Error", LabelLike::new().color(Color::Error).child("Error color").into_any_element()),
single_example("Alpha", LabelLike::new().alpha(0.5).child("50% opacity").into_any_element()),
],
),
example_group_with_title(
"Line Height",
vec![
single_example("Default", LabelLike::new().child("Default line height\nMulti-line text").into_any_element()),
single_example("UI Label", LabelLike::new().line_height_style(LineHeightStyle::UiLabel).child("UI label line height\nMulti-line text").into_any_element()),
],
),
example_group_with_title(
"Special Cases",
vec![
single_example("Single Line", LabelLike::new().single_line().child("This is a very long text that should be displayed in a single line").into_any_element()),
single_example("Truncate", LabelLike::new().truncate().child("This is a very long text that should be truncated with an ellipsis").into_any_element()),
],
),
])
.into_any_element()
)
}
}

View File

@@ -2,8 +2,7 @@ use crate::prelude::*;
use gpui::IntoElement;
use smallvec::{SmallVec, smallvec};
#[derive(IntoElement, IntoComponent)]
#[component(scope = "Notification")]
#[derive(IntoElement, RegisterComponent)]
pub struct AlertModal {
id: ElementId,
children: SmallVec<[AnyElement; 2]>,
@@ -77,23 +76,33 @@ impl ParentElement for AlertModal {
}
}
impl ComponentPreview for AlertModal {
fn preview(_window: &mut Window, _cx: &mut App) -> AnyElement {
v_flex()
.gap_6()
.p_4()
.children(vec![example_group(
vec![
single_example(
"Basic Alert",
AlertModal::new("simple-modal", "Do you want to leave the current call?")
.child("The current window will be closed, and connections to any shared projects will be terminated."
)
.primary_action("Leave Call")
.into_any_element(),
)
],
)])
.into_any_element()
impl Component for AlertModal {
fn scope() -> ComponentScope {
ComponentScope::Notification
}
fn description() -> Option<&'static str> {
Some("A modal dialog that presents an alert message with primary and dismiss actions.")
}
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
Some(
v_flex()
.gap_6()
.p_4()
.children(vec![example_group(
vec![
single_example(
"Basic Alert",
AlertModal::new("simple-modal", "Do you want to leave the current call?")
.child("The current window will be closed, and connections to any shared projects will be terminated."
)
.primary_action("Leave Call")
.into_any_element(),
)
],
)])
.into_any_element()
)
}
}

View File

@@ -3,7 +3,9 @@ use smallvec::SmallVec;
use crate::prelude::*;
#[derive(IntoElement)]
use super::Checkbox;
#[derive(IntoElement, RegisterComponent)]
pub struct SettingsContainer {
children: SmallVec<[AnyElement; 2]>,
}
@@ -33,3 +35,55 @@ impl RenderOnce for SettingsContainer {
v_flex().px_2().gap_1().children(self.children)
}
}
impl Component for SettingsContainer {
fn scope() -> ComponentScope {
ComponentScope::Layout
}
fn name() -> &'static str {
"SettingsContainer"
}
fn description() -> Option<&'static str> {
Some("A container for organizing and displaying settings in a structured manner.")
}
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
Some(
v_flex()
.gap_6()
.children(vec![
example_group_with_title(
"Basic Usage",
vec![
single_example(
"Empty Container",
SettingsContainer::new().into_any_element(),
),
single_example(
"With Content",
SettingsContainer::new()
.child(Label::new("Setting 1"))
.child(Label::new("Setting 2"))
.child(Label::new("Setting 3"))
.into_any_element(),
),
],
),
example_group_with_title(
"With Different Elements",
vec![single_example(
"Mixed Content",
SettingsContainer::new()
.child(Label::new("Text Setting"))
.child(Checkbox::new("checkbox", ToggleState::Unselected))
.child(Button::new("button", "Click me"))
.into_any_element(),
)],
),
])
.into_any_element(),
)
}
}

View File

@@ -3,8 +3,10 @@ use smallvec::SmallVec;
use crate::{ListHeader, prelude::*};
use super::Checkbox;
/// A group of settings.
#[derive(IntoElement)]
#[derive(IntoElement, RegisterComponent)]
pub struct SettingsGroup {
header: SharedString,
children: SmallVec<[AnyElement; 2]>,
@@ -34,3 +36,75 @@ impl RenderOnce for SettingsGroup {
.children(self.children)
}
}
impl Component for SettingsGroup {
fn scope() -> ComponentScope {
ComponentScope::Layout
}
fn name() -> &'static str {
"SettingsGroup"
}
fn description() -> Option<&'static str> {
Some("A group of settings with a header, used to organize related settings.")
}
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
Some(
v_flex()
.gap_6()
.children(vec![
example_group_with_title(
"Basic Usage",
vec![
single_example(
"Empty Group",
SettingsGroup::new("General Settings").into_any_element(),
),
single_example(
"With Children",
SettingsGroup::new("Appearance")
.child(
Checkbox::new("dark_mode", ToggleState::Unselected)
.label("Dark Mode"),
)
.child(
Checkbox::new("high_contrast", ToggleState::Unselected)
.label("High Contrast"),
)
.into_any_element(),
),
],
),
example_group_with_title(
"Multiple Groups",
vec![single_example(
"Two Groups",
v_flex()
.gap_4()
.child(
SettingsGroup::new("General").child(
Checkbox::new("auto_update", ToggleState::Selected)
.label("Auto Update"),
),
)
.child(
SettingsGroup::new("Editor")
.child(
Checkbox::new("line_numbers", ToggleState::Selected)
.label("Show Line Numbers"),
)
.child(
Checkbox::new("word_wrap", ToggleState::Unselected)
.label("Word Wrap"),
),
)
.into_any_element(),
)],
),
])
.into_any_element(),
)
}
}

View File

@@ -1,6 +1,4 @@
mod context_menu;
mod disclosure;
mod icon;
mod icon_button;
mod keybinding;
mod list;
@@ -11,8 +9,6 @@ mod tab_bar;
mod toggle_button;
pub use context_menu::*;
pub use disclosure::*;
pub use icon::*;
pub use icon_button::*;
pub use keybinding::*;
pub use list::*;

View File

@@ -1,20 +0,0 @@
use gpui::Render;
use story::Story;
use strum::IntoEnumIterator;
use crate::prelude::*;
use crate::{Icon, IconName};
pub struct IconStory;
impl Render for IconStory {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
let icons = IconName::iter();
Story::container()
.child(Story::title_for::<Icon>())
.child(Story::label("DecoratedIcon"))
.child(Story::label("All Icons"))
.child(div().flex().gap_3().children(icons.map(Icon::new)))
}
}

View File

@@ -26,7 +26,7 @@ pub enum TabCloseSide {
End,
}
#[derive(IntoElement, IntoComponent)]
#[derive(IntoElement, RegisterComponent)]
pub struct Tab {
div: Stateful<Div>,
selected: bool,
@@ -171,48 +171,59 @@ impl RenderOnce for Tab {
}
}
// View this component preview using `workspace: open component-preview`
impl ComponentPreview for Tab {
fn preview(_window: &mut Window, _cx: &mut App) -> AnyElement {
v_flex()
.gap_6()
.children(vec![example_group_with_title(
"Variations",
vec![
single_example(
"Default",
Tab::new("default").child("Default Tab").into_any_element(),
),
single_example(
"Selected",
Tab::new("selected")
.toggle_state(true)
.child("Selected Tab")
.into_any_element(),
),
single_example(
"First",
Tab::new("first")
.position(TabPosition::First)
.child("First Tab")
.into_any_element(),
),
single_example(
"Middle",
Tab::new("middle")
.position(TabPosition::Middle(Ordering::Equal))
.child("Middle Tab")
.into_any_element(),
),
single_example(
"Last",
Tab::new("last")
.position(TabPosition::Last)
.child("Last Tab")
.into_any_element(),
),
],
)])
.into_any_element()
impl Component for Tab {
fn scope() -> ComponentScope {
ComponentScope::None
}
fn description() -> Option<&'static str> {
Some(
"A tab component that can be used in a tabbed interface, supporting different positions and states.",
)
}
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
Some(
v_flex()
.gap_6()
.children(vec![example_group_with_title(
"Variations",
vec![
single_example(
"Default",
Tab::new("default").child("Default Tab").into_any_element(),
),
single_example(
"Selected",
Tab::new("selected")
.toggle_state(true)
.child("Selected Tab")
.into_any_element(),
),
single_example(
"First",
Tab::new("first")
.position(TabPosition::First)
.child("First Tab")
.into_any_element(),
),
single_example(
"Middle",
Tab::new("middle")
.position(TabPosition::Middle(Ordering::Equal))
.child("Middle Tab")
.into_any_element(),
),
single_example(
"Last",
Tab::new("last")
.position(TabPosition::Last)
.child("Last Tab")
.into_any_element(),
),
],
)])
.into_any_element(),
)
}
}

View File

@@ -4,7 +4,7 @@ use smallvec::SmallVec;
use crate::Tab;
use crate::prelude::*;
#[derive(IntoElement)]
#[derive(IntoElement, RegisterComponent)]
pub struct TabBar {
id: ElementId,
start_children: SmallVec<[AnyElement; 2]>,
@@ -151,3 +151,57 @@ impl RenderOnce for TabBar {
})
}
}
impl Component for TabBar {
fn scope() -> ComponentScope {
ComponentScope::Navigation
}
fn name() -> &'static str {
"TabBar"
}
fn description() -> Option<&'static str> {
Some("A horizontal bar containing tabs for navigation between different views or sections.")
}
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
Some(
v_flex()
.gap_6()
.children(vec![
example_group_with_title(
"Basic Usage",
vec![
single_example(
"Empty TabBar",
TabBar::new("empty_tab_bar").into_any_element(),
),
single_example(
"With Tabs",
TabBar::new("tab_bar_with_tabs")
.child(Tab::new("tab1"))
.child(Tab::new("tab2"))
.child(Tab::new("tab3"))
.into_any_element(),
),
],
),
example_group_with_title(
"With Start and End Children",
vec![single_example(
"Full TabBar",
TabBar::new("full_tab_bar")
.start_child(Button::new("start_button", "Start"))
.child(Tab::new("tab1"))
.child(Tab::new("tab2"))
.child(Tab::new("tab3"))
.end_child(Button::new("end_button", "End"))
.into_any_element(),
)],
),
])
.into_any_element(),
)
}
}

Some files were not shown because too many files have changed in this diff Show More