Compare commits

..

5 Commits

Author SHA1 Message Date
Peter Tripp
d5e93c08d1 vim: Support keyboard navigation of context menus and code actions 2025-06-25 12:42:40 -04:00
Anthony Eid
fc1fc264ec debugger: Generate inline values based on debugger.scm file (#33081)
## Context

To support inline values a language will have to implement their own
provider trait that walks through tree sitter nodes. This is overly
complicated, hard to accurately implement for each language, and lacks
proper extension support.

This PR switches to a singular inline provider that uses a language's
`debugger.scm` query field to capture variables and scopes. The inline
provider is able to use this information to generate inlays that take
scope into account and work with any language that defines a debugger
query file.

### Todos
- [x] Implement a utility test function to easily test inline values
- [x] Generate inline values based on captures
- [x] Reimplement Python, Rust, and Go support
- [x] Take scope into account when iterating through variable captures
- [x] Add tests for Go inline values
- [x] Remove old inline provider code and trait implementations

Release Notes:

- debugger: Generate inline values based on a language debugger.scm file
2025-06-24 18:24:43 +00:00
Peter Tripp
800b925fd7 Improve Atom keymap (#33329)
Closes: https://github.com/zed-industries/zed/issues/33256

Move some Editor keymap entries into `Editor && mode == full`

Release Notes:

- N/A
2025-06-24 18:02:07 +00:00
fantacell
95cf153ad7 Simulate helix line wrapping (#32763)
In helix the `f`, `F`, `t`, `T`, left and right motions wrap lines. I
added that by default.

Release Notes:

- vim: The `use_multiline_find` setting is replaced by binding to the
correct action in the keymap:
    ```
"f": ["vim::PushFindForward", { "before": false, "multiline": true }],
"t": ["vim::PushFindForward", { "before": true, "multiline": true }],
"shift-f": ["vim::PushFindBackward", { "after": false, "multiline": true
}],
"shift-t": ["vim::PushFindBackward", { "after": true, "multiline": true
}],
    ```
- helix: `f`/`t`/`shift-f`/`shift-t`/`h`/`l`/`left`/`right` are now
multiline by default (like helix)
2025-06-24 10:51:41 -06:00
Bennet Bo Fenner
7be57baef0 agent: Fix issue with Anthropic thinking models (#33317)
cc @osyvokon 

We were seeing a bunch of errors in our backend when people were using
Claude models with thinking enabled.

In the logs we would see
> an error occurred while interacting with the Anthropic API:
invalid_request_error: messages.x.content.0.type: Expected `thinking` or
`redacted_thinking`, but found `text`. When `thinking` is enabled, a
final `assistant` message must start with a thinking block (preceeding
the lastmost set of `tool_use` and `tool_result` blocks). We recommend
you include thinking blocks from previous turns. To avoid this
requirement, disable `thinking`. Please consult our documentation at
https://docs.anthropic.com/en/docs/build-with-claude/extended-thinking

However, this issue did not occur frequently and was not easily
reproducible. Turns out it was triggered by us not correctly handling
[Redacted Thinking
Blocks](https://docs.anthropic.com/en/docs/build-with-claude/extended-thinking#thinking-redaction).

I could constantly reproduce this issue by including this magic string:
`ANTHROPIC_MAGIC_STRING_TRIGGER_REDACTED_THINKING_46C9A13E193C177646C7398A98432ECCCE4C1253D5E2D82641AC0E52CC2876CB
` in the request, which forces `claude-3-7-sonnet` to emit redacted
thinking blocks (confusingly the magic string does not seem to be
working for `claude-sonnet-4`). As soon as we hit a tool call Anthropic
would return an error.

Thanks to @osyvokon for pointing me in the right direction 😄!


Release Notes:

- agent: Fixed an issue where Anthropic models would sometimes return an
error when thinking was enabled
2025-06-24 16:23:59 +00:00
59 changed files with 1391 additions and 3139 deletions

11
Cargo.lock generated
View File

@@ -4348,6 +4348,7 @@ dependencies = [
"terminal_view",
"theme",
"tree-sitter",
"tree-sitter-go",
"tree-sitter-json",
"ui",
"unindent",
@@ -14566,22 +14567,13 @@ dependencies = [
name = "settings_ui"
version = "0.1.0"
dependencies = [
"collections",
"command_palette",
"command_palette_hooks",
"component",
"db",
"editor",
"feature_flags",
"fs",
"fuzzy",
"gpui",
"log",
"menu",
"paths",
"project",
"schemars",
"search",
"serde",
"settings",
"theme",
@@ -17057,7 +17049,6 @@ dependencies = [
"gpui_macros",
"icons",
"itertools 0.14.0",
"log",
"menu",
"serde",
"settings",

View File

@@ -1050,12 +1050,5 @@
"ctrl-tab": "pane::ActivateNextItem",
"ctrl-shift-tab": "pane::ActivatePreviousItem"
}
},
{
"context": "KeymapEditor",
"use_key_equivalents": true,
"bindings": {
"ctrl-f": "search::FocusSearch"
}
}
]

View File

@@ -611,7 +611,7 @@
"cmd-shift-f": "pane::DeploySearch",
"cmd-shift-h": ["pane::DeploySearch", { "replace_enabled": true }],
"cmd-shift-t": "pane::ReopenClosedItem",
"cmd-k cmd-s": "zed::OpenKeymapEditor",
"cmd-k cmd-s": "zed::OpenKeymap",
"cmd-k cmd-t": "theme_selector::Toggle",
"cmd-t": "project_symbols::Toggle",
"cmd-p": "file_finder::Toggle",
@@ -1149,12 +1149,5 @@
"ctrl-tab": "pane::ActivateNextItem",
"ctrl-shift-tab": "pane::ActivatePreviousItem"
}
},
{
"context": "KeymapEditor",
"use_key_equivalents": true,
"bindings": {
"cmd-f": "search::FocusSearch"
}
}
]

View File

@@ -9,6 +9,13 @@
},
{
"context": "Editor",
"bindings": {
"ctrl-k ctrl-u": "editor::ConvertToUpperCase", // editor:upper-case
"ctrl-k ctrl-l": "editor::ConvertToLowerCase" // editor:lower-case
}
},
{
"context": "Editor && mode == full",
"bindings": {
"ctrl-shift-l": "language_selector::Toggle", // grammar-selector:show
"ctrl-|": "pane::RevealInProjectPanel", // tree-view:reveal-active-file
@@ -19,25 +26,20 @@
"shift-f3": ["editor::SelectPrevious", { "replace_newest": true }], //find-and-replace:find-previous
"alt-shift-down": "editor::AddSelectionBelow", // editor:add-selection-below
"alt-shift-up": "editor::AddSelectionAbove", // editor:add-selection-above
"ctrl-k ctrl-u": "editor::ConvertToUpperCase", // editor:upper-case
"ctrl-k ctrl-l": "editor::ConvertToLowerCase", // editor:lower-case
"ctrl-j": "editor::JoinLines", // editor:join-lines
"ctrl-shift-d": "editor::DuplicateLineDown", // editor:duplicate-lines
"ctrl-up": "editor::MoveLineUp", // editor:move-line-up
"ctrl-down": "editor::MoveLineDown", // editor:move-line-down
"ctrl-\\": "workspace::ToggleLeftDock", // tree-view:toggle
"ctrl-shift-m": "markdown::OpenPreviewToTheSide" // markdown-preview:toggle
}
},
{
"context": "Editor && mode == full",
"bindings": {
"ctrl-shift-m": "markdown::OpenPreviewToTheSide", // markdown-preview:toggle
"ctrl-r": "outline::Toggle" // symbols-view:toggle-project-symbols
}
},
{
"context": "BufferSearchBar",
"bindings": {
"f3": ["editor::SelectNext", { "replace_newest": true }], // find-and-replace:find-next
"shift-f3": ["editor::SelectPrevious", { "replace_newest": true }], //find-and-replace:find-previous
"ctrl-f3": "search::SelectNextMatch", // find-and-replace:find-next-selected
"ctrl-shift-f3": "search::SelectPreviousMatch" // find-and-replace:find-previous-selected
}

View File

@@ -9,6 +9,14 @@
},
{
"context": "Editor",
"bindings": {
"cmd-shift-backspace": "editor::DeleteToBeginningOfLine",
"cmd-k cmd-u": "editor::ConvertToUpperCase",
"cmd-k cmd-l": "editor::ConvertToLowerCase"
}
},
{
"context": "Editor && mode == full",
"bindings": {
"ctrl-shift-l": "language_selector::Toggle",
"cmd-|": "pane::RevealInProjectPanel",
@@ -19,26 +27,20 @@
"cmd-shift-g": ["editor::SelectPrevious", { "replace_newest": true }],
"ctrl-shift-down": "editor::AddSelectionBelow",
"ctrl-shift-up": "editor::AddSelectionAbove",
"cmd-shift-backspace": "editor::DeleteToBeginningOfLine",
"cmd-k cmd-u": "editor::ConvertToUpperCase",
"cmd-k cmd-l": "editor::ConvertToLowerCase",
"alt-enter": "editor::Newline",
"cmd-shift-d": "editor::DuplicateLineDown",
"ctrl-cmd-up": "editor::MoveLineUp",
"ctrl-cmd-down": "editor::MoveLineDown",
"cmd-\\": "workspace::ToggleLeftDock",
"ctrl-shift-m": "markdown::OpenPreviewToTheSide"
}
},
{
"context": "Editor && mode == full",
"bindings": {
"ctrl-shift-m": "markdown::OpenPreviewToTheSide",
"cmd-r": "outline::Toggle"
}
},
{
"context": "BufferSearchBar",
"bindings": {
"cmd-g": ["editor::SelectNext", { "replace_newest": true }],
"cmd-shift-g": ["editor::SelectPrevious", { "replace_newest": true }],
"cmd-f3": "search::SelectNextMatch",
"cmd-shift-f3": "search::SelectPreviousMatch"
}

View File

@@ -85,10 +85,10 @@
"[ {": ["vim::UnmatchedBackward", { "char": "{" }],
"] )": ["vim::UnmatchedForward", { "char": ")" }],
"[ (": ["vim::UnmatchedBackward", { "char": "(" }],
"f": ["vim::PushFindForward", { "before": false }],
"t": ["vim::PushFindForward", { "before": true }],
"shift-f": ["vim::PushFindBackward", { "after": false }],
"shift-t": ["vim::PushFindBackward", { "after": true }],
"f": ["vim::PushFindForward", { "before": false, "multiline": false }],
"t": ["vim::PushFindForward", { "before": true, "multiline": false }],
"shift-f": ["vim::PushFindBackward", { "after": false, "multiline": false }],
"shift-t": ["vim::PushFindBackward", { "after": true, "multiline": false }],
"m": "vim::PushMark",
"'": ["vim::PushJump", { "line": true }],
"`": ["vim::PushJump", { "line": false }],
@@ -368,6 +368,10 @@
"escape": "editor::Cancel",
"ctrl-[": "editor::Cancel",
":": "command_palette::Toggle",
"left": "vim::WrappingLeft",
"right": "vim::WrappingRight",
"h": "vim::WrappingLeft",
"l": "vim::WrappingRight",
"shift-d": "vim::DeleteToEndOfLine",
"shift-j": "vim::JoinLines",
"y": "editor::Copy",
@@ -385,6 +389,10 @@
"shift-p": ["vim::Paste", { "before": true }],
"u": "vim::Undo",
"ctrl-r": "vim::Redo",
"f": ["vim::PushFindForward", { "before": false, "multiline": true }],
"t": ["vim::PushFindForward", { "before": true, "multiline": true }],
"shift-f": ["vim::PushFindBackward", { "after": false, "multiline": true }],
"shift-t": ["vim::PushFindBackward", { "after": true, "multiline": true }],
"r": "vim::PushReplace",
"s": "vim::Substitute",
"shift-s": "vim::SubstituteLine",
@@ -826,6 +834,24 @@
"g g": "menu::SelectFirst"
}
},
{
"context": "menu",
"bindings": {
"j": "menu::SelectNext",
"k": "menu::SelectPrevious",
"shift-g": "menu::SelectLast",
"g g": "menu::SelectFirst"
}
},
{
"context": "Editor && showing_code_actions",
"bindings": {
"j": "editor::ContextMenuNext",
"k": "editor::ContextMenuPrevious",
"shift-g": "editor::ContextMenuLast",
"g g": "editor::ContextMenuFirst"
}
},
{
"context": "GitPanel && ChangesList",
"use_key_equivalents": true,

View File

@@ -1734,7 +1734,6 @@
"default_mode": "normal",
"toggle_relative_line_numbers": false,
"use_system_clipboard": "always",
"use_multiline_find": false,
"use_smartcase_find": false,
"highlight_on_yank_duration": 200,
"custom_digraphs": {},

View File

@@ -145,6 +145,10 @@ impl Message {
}
}
pub fn push_redacted_thinking(&mut self, data: String) {
self.segments.push(MessageSegment::RedactedThinking(data));
}
pub fn push_text(&mut self, text: &str) {
if let Some(MessageSegment::Text(segment)) = self.segments.last_mut() {
segment.push_str(text);
@@ -183,7 +187,7 @@ pub enum MessageSegment {
text: String,
signature: Option<String>,
},
RedactedThinking(Vec<u8>),
RedactedThinking(String),
}
impl MessageSegment {
@@ -1643,6 +1647,25 @@ impl Thread {
};
}
}
LanguageModelCompletionEvent::RedactedThinking {
data
} => {
thread.received_chunk();
if let Some(last_message) = thread.messages.last_mut() {
if last_message.role == Role::Assistant
&& !thread.tool_use.has_tool_results(last_message.id)
{
last_message.push_redacted_thinking(data);
} else {
request_assistant_message_id =
Some(thread.insert_assistant_message(
vec![MessageSegment::RedactedThinking(data)],
cx,
));
};
}
}
LanguageModelCompletionEvent::ToolUse(tool_use) => {
let last_assistant_message_id = request_assistant_message_id
.unwrap_or_else(|| {

View File

@@ -731,7 +731,7 @@ pub enum SerializedMessageSegment {
signature: Option<String>,
},
RedactedThinking {
data: Vec<u8>,
data: String,
},
}

View File

@@ -2117,6 +2117,7 @@ impl AssistantContext {
);
}
}
LanguageModelCompletionEvent::RedactedThinking { .. } => {},
LanguageModelCompletionEvent::Text(mut chunk) => {
if let Some(start) = thought_process_stack.pop() {
let end = buffer.anchor_before(message_old_end_offset);

View File

@@ -41,7 +41,7 @@ pub struct CommandPalette {
/// Removes subsequent whitespace characters and double colons from the query.
///
/// This improves the likelihood of a match by either humanized name or keymap-style name.
pub fn normalize_action_query(input: &str) -> String {
fn normalize_query(input: &str) -> String {
let mut result = String::with_capacity(input.len());
let mut last_char = None;
@@ -297,7 +297,7 @@ impl PickerDelegate for CommandPaletteDelegate {
let mut commands = self.all_commands.clone();
let hit_counts = self.hit_counts();
let executor = cx.background_executor().clone();
let query = normalize_action_query(query.as_str());
let query = normalize_query(query.as_str());
async move {
commands.sort_by_key(|action| {
(
@@ -311,17 +311,29 @@ impl PickerDelegate for CommandPaletteDelegate {
.enumerate()
.map(|(ix, command)| StringMatchCandidate::new(ix, &command.name))
.collect::<Vec<_>>();
let matches = fuzzy::match_strings(
&candidates,
&query,
true,
true,
10000,
&Default::default(),
executor,
)
.await;
let matches = if query.is_empty() {
candidates
.into_iter()
.enumerate()
.map(|(index, candidate)| StringMatch {
candidate_id: index,
string: candidate.string,
positions: Vec::new(),
score: 0.0,
})
.collect()
} else {
fuzzy::match_strings(
&candidates,
&query,
true,
true,
10000,
&Default::default(),
executor,
)
.await
};
tx.send((commands, matches)).await.log_err();
}
@@ -410,8 +422,8 @@ impl PickerDelegate for CommandPaletteDelegate {
window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Option<Self::ListItem> {
let matching_command = self.matches.get(ix)?;
let command = self.commands.get(matching_command.candidate_id)?;
let r#match = self.matches.get(ix)?;
let command = self.commands.get(r#match.candidate_id)?;
Some(
ListItem::new(ix)
.inset(true)
@@ -424,7 +436,7 @@ impl PickerDelegate for CommandPaletteDelegate {
.justify_between()
.child(HighlightedLabel::new(
command.name.clone(),
matching_command.positions.clone(),
r#match.positions.clone(),
))
.children(KeyBinding::for_action_in(
&*command.action,
@@ -500,28 +512,19 @@ mod tests {
#[test]
fn test_normalize_query() {
assert_eq!(normalize_query("editor: backspace"), "editor: backspace");
assert_eq!(normalize_query("editor: backspace"), "editor: backspace");
assert_eq!(normalize_query("editor: backspace"), "editor: backspace");
assert_eq!(
normalize_action_query("editor: backspace"),
"editor: backspace"
);
assert_eq!(
normalize_action_query("editor: backspace"),
"editor: backspace"
);
assert_eq!(
normalize_action_query("editor: backspace"),
"editor: backspace"
);
assert_eq!(
normalize_action_query("editor::GoToDefinition"),
normalize_query("editor::GoToDefinition"),
"editor:GoToDefinition"
);
assert_eq!(
normalize_action_query("editor::::GoToDefinition"),
normalize_query("editor::::GoToDefinition"),
"editor:GoToDefinition"
);
assert_eq!(
normalize_action_query("editor: :GoToDefinition"),
normalize_query("editor: :GoToDefinition"),
"editor: :GoToDefinition"
);
}

View File

@@ -1,5 +1,3 @@
use std::collections::{HashMap, HashSet};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum VariableLookupKind {
Variable,
@@ -20,641 +18,3 @@ pub struct InlineValueLocation {
pub row: usize,
pub column: usize,
}
/// A trait for providing inline values for debugging purposes.
///
/// Implementors of this trait are responsible for analyzing a given node in the
/// source code and extracting variable information, including their names,
/// scopes, and positions. This information is used to display inline values
/// during debugging sessions. Implementors must also handle variable scoping
/// themselves by traversing the syntax tree upwards to determine whether a
/// variable is local or global.
pub trait InlineValueProvider: 'static + Send + Sync {
/// Provides a list of inline value locations based on the given node and source code.
///
/// # Parameters
/// - `node`: The root node of the active debug line. Implementors should traverse
/// upwards from this node to gather variable information and determine their scope.
/// - `source`: The source code as a string slice, used to extract variable names.
/// - `max_row`: The maximum row to consider when collecting variables. Variables
/// declared beyond this row should be ignored.
///
/// # Returns
/// A vector of `InlineValueLocation` instances, each representing a variable's
/// name, scope, and the position of the inline value should be shown.
fn provide(
&self,
node: language::Node,
source: &str,
max_row: usize,
) -> Vec<InlineValueLocation>;
}
pub struct RustInlineValueProvider;
impl InlineValueProvider for RustInlineValueProvider {
fn provide(
&self,
mut node: language::Node,
source: &str,
max_row: usize,
) -> Vec<InlineValueLocation> {
let mut variables = Vec::new();
let mut variable_names = HashSet::new();
let mut scope = VariableScope::Local;
loop {
let mut variable_names_in_scope = HashMap::new();
for child in node.named_children(&mut node.walk()) {
if child.start_position().row >= max_row {
break;
}
if scope == VariableScope::Local && child.kind() == "let_declaration" {
if let Some(identifier) = child.child_by_field_name("pattern") {
let variable_name = source[identifier.byte_range()].to_string();
if variable_names.contains(&variable_name) {
continue;
}
if let Some(index) = variable_names_in_scope.get(&variable_name) {
variables.remove(*index);
}
variable_names_in_scope.insert(variable_name.clone(), variables.len());
variables.push(InlineValueLocation {
variable_name,
scope: VariableScope::Local,
lookup: VariableLookupKind::Variable,
row: identifier.end_position().row,
column: identifier.end_position().column,
});
}
} else if child.kind() == "static_item" {
if let Some(name) = child.child_by_field_name("name") {
let variable_name = source[name.byte_range()].to_string();
variables.push(InlineValueLocation {
variable_name,
scope: scope.clone(),
lookup: VariableLookupKind::Expression,
row: name.end_position().row,
column: name.end_position().column,
});
}
}
}
variable_names.extend(variable_names_in_scope.keys().cloned());
if matches!(node.kind(), "function_item" | "closure_expression") {
scope = VariableScope::Global;
}
if let Some(parent) = node.parent() {
node = parent;
} else {
break;
}
}
variables
}
}
pub struct PythonInlineValueProvider;
impl InlineValueProvider for PythonInlineValueProvider {
fn provide(
&self,
mut node: language::Node,
source: &str,
max_row: usize,
) -> Vec<InlineValueLocation> {
let mut variables = Vec::new();
let mut variable_names = HashSet::new();
let mut scope = VariableScope::Local;
loop {
let mut variable_names_in_scope = HashMap::new();
for child in node.named_children(&mut node.walk()) {
if child.start_position().row >= max_row {
break;
}
if scope == VariableScope::Local {
match child.kind() {
"expression_statement" => {
if let Some(expr) = child.child(0) {
if expr.kind() == "assignment" {
if let Some(param) = expr.child(0) {
let param_identifier = if param.kind() == "identifier" {
Some(param)
} else if param.kind() == "typed_parameter" {
param.child(0)
} else {
None
};
if let Some(identifier) = param_identifier {
if identifier.kind() == "identifier" {
let variable_name =
source[identifier.byte_range()].to_string();
if variable_names.contains(&variable_name) {
continue;
}
if let Some(index) =
variable_names_in_scope.get(&variable_name)
{
variables.remove(*index);
}
variable_names_in_scope
.insert(variable_name.clone(), variables.len());
variables.push(InlineValueLocation {
variable_name,
scope: VariableScope::Local,
lookup: VariableLookupKind::Variable,
row: identifier.end_position().row,
column: identifier.end_position().column,
});
}
}
}
}
}
}
"function_definition" => {
if let Some(params) = child.child_by_field_name("parameters") {
for param in params.named_children(&mut params.walk()) {
let param_identifier = if param.kind() == "identifier" {
Some(param)
} else if param.kind() == "typed_parameter" {
param.child(0)
} else {
None
};
if let Some(identifier) = param_identifier {
if identifier.kind() == "identifier" {
let variable_name =
source[identifier.byte_range()].to_string();
if variable_names.contains(&variable_name) {
continue;
}
if let Some(index) =
variable_names_in_scope.get(&variable_name)
{
variables.remove(*index);
}
variable_names_in_scope
.insert(variable_name.clone(), variables.len());
variables.push(InlineValueLocation {
variable_name,
scope: VariableScope::Local,
lookup: VariableLookupKind::Variable,
row: identifier.end_position().row,
column: identifier.end_position().column,
});
}
}
}
}
}
"for_statement" => {
if let Some(target) = child.child_by_field_name("left") {
if target.kind() == "identifier" {
let variable_name = source[target.byte_range()].to_string();
if variable_names.contains(&variable_name) {
continue;
}
if let Some(index) = variable_names_in_scope.get(&variable_name)
{
variables.remove(*index);
}
variable_names_in_scope
.insert(variable_name.clone(), variables.len());
variables.push(InlineValueLocation {
variable_name,
scope: VariableScope::Local,
lookup: VariableLookupKind::Variable,
row: target.end_position().row,
column: target.end_position().column,
});
}
}
}
_ => {}
}
}
}
variable_names.extend(variable_names_in_scope.keys().cloned());
if matches!(node.kind(), "function_definition" | "module")
&& node.range().end_point.row < max_row
{
scope = VariableScope::Global;
}
if let Some(parent) = node.parent() {
node = parent;
} else {
break;
}
}
variables
}
}
pub struct GoInlineValueProvider;
impl InlineValueProvider for GoInlineValueProvider {
fn provide(
&self,
mut node: language::Node,
source: &str,
max_row: usize,
) -> Vec<InlineValueLocation> {
let mut variables = Vec::new();
let mut variable_names = HashSet::new();
let mut scope = VariableScope::Local;
loop {
let mut variable_names_in_scope = HashMap::new();
for child in node.named_children(&mut node.walk()) {
if child.start_position().row >= max_row {
break;
}
if scope == VariableScope::Local {
match child.kind() {
"var_declaration" => {
for var_spec in child.named_children(&mut child.walk()) {
if var_spec.kind() == "var_spec" {
if let Some(name_node) = var_spec.child_by_field_name("name") {
let variable_name =
source[name_node.byte_range()].to_string();
if variable_names.contains(&variable_name) {
continue;
}
if let Some(index) =
variable_names_in_scope.get(&variable_name)
{
variables.remove(*index);
}
variable_names_in_scope
.insert(variable_name.clone(), variables.len());
variables.push(InlineValueLocation {
variable_name,
scope: VariableScope::Local,
lookup: VariableLookupKind::Variable,
row: name_node.end_position().row,
column: name_node.end_position().column,
});
}
}
}
}
"short_var_declaration" => {
if let Some(left_side) = child.child_by_field_name("left") {
for identifier in left_side.named_children(&mut left_side.walk()) {
if identifier.kind() == "identifier" {
let variable_name =
source[identifier.byte_range()].to_string();
if variable_names.contains(&variable_name) {
continue;
}
if let Some(index) =
variable_names_in_scope.get(&variable_name)
{
variables.remove(*index);
}
variable_names_in_scope
.insert(variable_name.clone(), variables.len());
variables.push(InlineValueLocation {
variable_name,
scope: VariableScope::Local,
lookup: VariableLookupKind::Variable,
row: identifier.end_position().row,
column: identifier.end_position().column,
});
}
}
}
}
"assignment_statement" => {
if let Some(left_side) = child.child_by_field_name("left") {
for identifier in left_side.named_children(&mut left_side.walk()) {
if identifier.kind() == "identifier" {
let variable_name =
source[identifier.byte_range()].to_string();
if variable_names.contains(&variable_name) {
continue;
}
if let Some(index) =
variable_names_in_scope.get(&variable_name)
{
variables.remove(*index);
}
variable_names_in_scope
.insert(variable_name.clone(), variables.len());
variables.push(InlineValueLocation {
variable_name,
scope: VariableScope::Local,
lookup: VariableLookupKind::Variable,
row: identifier.end_position().row,
column: identifier.end_position().column,
});
}
}
}
}
"function_declaration" | "method_declaration" => {
if let Some(params) = child.child_by_field_name("parameters") {
for param in params.named_children(&mut params.walk()) {
if param.kind() == "parameter_declaration" {
if let Some(name_node) = param.child_by_field_name("name") {
let variable_name =
source[name_node.byte_range()].to_string();
if variable_names.contains(&variable_name) {
continue;
}
if let Some(index) =
variable_names_in_scope.get(&variable_name)
{
variables.remove(*index);
}
variable_names_in_scope
.insert(variable_name.clone(), variables.len());
variables.push(InlineValueLocation {
variable_name,
scope: VariableScope::Local,
lookup: VariableLookupKind::Variable,
row: name_node.end_position().row,
column: name_node.end_position().column,
});
}
}
}
}
}
"for_statement" => {
if let Some(clause) = child.named_child(0) {
if clause.kind() == "for_clause" {
if let Some(init) = clause.named_child(0) {
if init.kind() == "short_var_declaration" {
if let Some(left_side) =
init.child_by_field_name("left")
{
if left_side.kind() == "expression_list" {
for identifier in left_side
.named_children(&mut left_side.walk())
{
if identifier.kind() == "identifier" {
let variable_name = source
[identifier.byte_range()]
.to_string();
if variable_names
.contains(&variable_name)
{
continue;
}
if let Some(index) =
variable_names_in_scope
.get(&variable_name)
{
variables.remove(*index);
}
variable_names_in_scope.insert(
variable_name.clone(),
variables.len(),
);
variables.push(InlineValueLocation {
variable_name,
scope: VariableScope::Local,
lookup:
VariableLookupKind::Variable,
row: identifier.end_position().row,
column: identifier
.end_position()
.column,
});
}
}
}
}
}
}
} else if clause.kind() == "range_clause" {
if let Some(left) = clause.child_by_field_name("left") {
if left.kind() == "expression_list" {
for identifier in left.named_children(&mut left.walk())
{
if identifier.kind() == "identifier" {
let variable_name =
source[identifier.byte_range()].to_string();
if variable_name == "_" {
continue;
}
if variable_names.contains(&variable_name) {
continue;
}
if let Some(index) =
variable_names_in_scope.get(&variable_name)
{
variables.remove(*index);
}
variable_names_in_scope.insert(
variable_name.clone(),
variables.len(),
);
variables.push(InlineValueLocation {
variable_name,
scope: VariableScope::Local,
lookup: VariableLookupKind::Variable,
row: identifier.end_position().row,
column: identifier.end_position().column,
});
}
}
}
}
}
}
}
_ => {}
}
} else if child.kind() == "var_declaration" {
for var_spec in child.named_children(&mut child.walk()) {
if var_spec.kind() == "var_spec" {
if let Some(name_node) = var_spec.child_by_field_name("name") {
let variable_name = source[name_node.byte_range()].to_string();
variables.push(InlineValueLocation {
variable_name,
scope: VariableScope::Global,
lookup: VariableLookupKind::Expression,
row: name_node.end_position().row,
column: name_node.end_position().column,
});
}
}
}
}
}
variable_names.extend(variable_names_in_scope.keys().cloned());
if matches!(node.kind(), "function_declaration" | "method_declaration") {
scope = VariableScope::Global;
}
if let Some(parent) = node.parent() {
node = parent;
} else {
break;
}
}
variables
}
}
#[cfg(test)]
mod tests {
use super::*;
use tree_sitter::Parser;
#[test]
fn test_go_inline_value_provider() {
let provider = GoInlineValueProvider;
let source = r#"
package main
func main() {
items := []int{1, 2, 3, 4, 5}
for i, v := range items {
println(i, v)
}
for j := 0; j < 10; j++ {
println(j)
}
}
"#;
let mut parser = Parser::new();
if parser
.set_language(&tree_sitter_go::LANGUAGE.into())
.is_err()
{
return;
}
let Some(tree) = parser.parse(source, None) else {
return;
};
let root_node = tree.root_node();
let mut main_body = None;
for child in root_node.named_children(&mut root_node.walk()) {
if child.kind() == "function_declaration" {
if let Some(name) = child.child_by_field_name("name") {
if &source[name.byte_range()] == "main" {
if let Some(body) = child.child_by_field_name("body") {
main_body = Some(body);
break;
}
}
}
}
}
let Some(main_body) = main_body else {
return;
};
let variables = provider.provide(main_body, source, 100);
assert!(variables.len() >= 2);
let variable_names: Vec<&str> =
variables.iter().map(|v| v.variable_name.as_str()).collect();
assert!(variable_names.contains(&"items"));
assert!(variable_names.contains(&"j"));
}
#[test]
fn test_go_inline_value_provider_counter_pattern() {
let provider = GoInlineValueProvider;
let source = r#"
package main
func main() {
N := 10
for i := range N {
println(i)
}
}
"#;
let mut parser = Parser::new();
if parser
.set_language(&tree_sitter_go::LANGUAGE.into())
.is_err()
{
return;
}
let Some(tree) = parser.parse(source, None) else {
return;
};
let root_node = tree.root_node();
let mut main_body = None;
for child in root_node.named_children(&mut root_node.walk()) {
if child.kind() == "function_declaration" {
if let Some(name) = child.child_by_field_name("name") {
if &source[name.byte_range()] == "main" {
if let Some(body) = child.child_by_field_name("body") {
main_body = Some(body);
break;
}
}
}
}
}
let Some(main_body) = main_body else {
return;
};
let variables = provider.provide(main_body, source, 100);
let variable_names: Vec<&str> =
variables.iter().map(|v| v.variable_name.as_str()).collect();
assert!(variable_names.contains(&"N"));
assert!(variable_names.contains(&"i"));
}
}

View File

@@ -8,10 +8,7 @@ use task::{
AdapterSchema, AdapterSchemas, DebugRequest, DebugScenario, SpawnInTerminal, TaskTemplate,
};
use crate::{
adapters::{DebugAdapter, DebugAdapterName},
inline_value::InlineValueProvider,
};
use crate::adapters::{DebugAdapter, DebugAdapterName};
use std::{collections::BTreeMap, sync::Arc};
/// Given a user build configuration, locator creates a fill-in debug target ([DebugScenario]) on behalf of the user.
@@ -33,7 +30,6 @@ pub trait DapLocator: Send + Sync {
struct DapRegistryState {
adapters: BTreeMap<DebugAdapterName, Arc<dyn DebugAdapter>>,
locators: FxHashMap<SharedString, Arc<dyn DapLocator>>,
inline_value_providers: FxHashMap<String, Arc<dyn InlineValueProvider>>,
}
#[derive(Clone, Default)]
@@ -82,22 +78,6 @@ impl DapRegistry {
schemas
}
pub fn add_inline_value_provider(
&self,
language: String,
provider: Arc<dyn InlineValueProvider>,
) {
let _previous_value = self
.0
.write()
.inline_value_providers
.insert(language, provider);
debug_assert!(
_previous_value.is_none(),
"Attempted to insert a new inline value provider when one is already registered"
);
}
pub fn locators(&self) -> FxHashMap<SharedString, Arc<dyn DapLocator>> {
self.0.read().locators.clone()
}
@@ -106,10 +86,6 @@ impl DapRegistry {
self.0.read().adapters.get(name).cloned()
}
pub fn inline_value_provider(&self, language: &str) -> Option<Arc<dyn InlineValueProvider>> {
self.0.read().inline_value_providers.get(language).cloned()
}
pub fn enumerate_adapters(&self) -> Vec<DebugAdapterName> {
self.0.read().adapters.keys().cloned().collect()
}

View File

@@ -18,7 +18,6 @@ use dap::{
GithubRepo,
},
configure_tcp_connection,
inline_value::{GoInlineValueProvider, PythonInlineValueProvider, RustInlineValueProvider},
};
use gdb::GdbDebugAdapter;
use go::GoDebugAdapter;
@@ -44,10 +43,5 @@ pub fn init(cx: &mut App) {
{
registry.add_adapter(Arc::from(dap::FakeAdapter {}));
}
registry.add_inline_value_provider("Rust".to_string(), Arc::from(RustInlineValueProvider));
registry
.add_inline_value_provider("Python".to_string(), Arc::from(PythonInlineValueProvider));
registry.add_inline_value_provider("Go".to_string(), Arc::from(GoInlineValueProvider));
})
}

View File

@@ -81,3 +81,4 @@ unindent.workspace = true
util = { workspace = true, features = ["test-support"] }
workspace = { workspace = true, features = ["test-support"] }
zlog.workspace = true
tree-sitter-go.workspace = true

View File

@@ -246,10 +246,10 @@ fn main() {
editor.update_in(cx, |editor, window, cx| {
pretty_assertions::assert_eq!(
r#"
static mut GLOBAL: 1: usize = 1;
static mut GLOBAL: usize = 1;
fn main() {
let x = 10;
let x: 10 = 10;
let value = 42;
let y = 4;
let tester = {
@@ -303,11 +303,11 @@ fn main() {
editor.update_in(cx, |editor, window, cx| {
pretty_assertions::assert_eq!(
r#"
static mut GLOBAL: 1: usize = 1;
static mut GLOBAL: usize = 1;
fn main() {
let x: 10 = 10;
let value = 42;
let value: 42 = 42;
let y = 4;
let tester = {
let y = 10;
@@ -360,12 +360,12 @@ fn main() {
editor.update_in(cx, |editor, window, cx| {
pretty_assertions::assert_eq!(
r#"
static mut GLOBAL: 1: usize = 1;
static mut GLOBAL: usize = 1;
fn main() {
let x: 10 = 10;
let value: 42 = 42;
let y = 4;
let y: 4 = 4;
let tester = {
let y = 10;
let y = 5;
@@ -417,7 +417,7 @@ fn main() {
editor.update_in(cx, |editor, window, cx| {
pretty_assertions::assert_eq!(
r#"
static mut GLOBAL: 1: usize = 1;
static mut GLOBAL: usize = 1;
fn main() {
let x: 10 = 10;
@@ -474,14 +474,14 @@ fn main() {
editor.update_in(cx, |editor, window, cx| {
pretty_assertions::assert_eq!(
r#"
static mut GLOBAL: 1: usize = 1;
static mut GLOBAL: usize = 1;
fn main() {
let x: 10 = 10;
let value: 42 = 42;
let y: 4 = 4;
let tester = {
let y = 10;
let y: 4 = 10;
let y = 5;
let b = 3;
vec![y, 20, 30]
@@ -581,15 +581,15 @@ fn main() {
editor.update_in(cx, |editor, window, cx| {
pretty_assertions::assert_eq!(
r#"
static mut GLOBAL: 1: usize = 1;
static mut GLOBAL: usize = 1;
fn main() {
let x: 10 = 10;
let value: 42 = 42;
let y = 4;
let y: 10 = 4;
let tester = {
let y: 10 = 10;
let y = 5;
let y: 10 = 5;
let b = 3;
vec![y, 20, 30]
};
@@ -688,14 +688,14 @@ fn main() {
editor.update_in(cx, |editor, window, cx| {
pretty_assertions::assert_eq!(
r#"
static mut GLOBAL: 1: usize = 1;
static mut GLOBAL: usize = 1;
fn main() {
let x: 10 = 10;
let value: 42 = 42;
let y = 4;
let y: 5 = 4;
let tester = {
let y = 10;
let y: 5 = 10;
let y: 5 = 5;
let b = 3;
vec![y, 20, 30]
@@ -807,17 +807,17 @@ fn main() {
editor.update_in(cx, |editor, window, cx| {
pretty_assertions::assert_eq!(
r#"
static mut GLOBAL: 1: usize = 1;
static mut GLOBAL: usize = 1;
fn main() {
let x: 10 = 10;
let value: 42 = 42;
let y = 4;
let y: 5 = 4;
let tester = {
let y = 10;
let y: 5 = 10;
let y: 5 = 5;
let b: 3 = 3;
vec![y, 20, 30]
vec![y: 5, 20, 30]
};
let caller = || {
@@ -926,7 +926,7 @@ fn main() {
editor.update_in(cx, |editor, window, cx| {
pretty_assertions::assert_eq!(
r#"
static mut GLOBAL: 1: usize = 1;
static mut GLOBAL: usize = 1;
fn main() {
let x: 10 = 10;
@@ -1058,7 +1058,7 @@ fn main() {
editor.update_in(cx, |editor, window, cx| {
pretty_assertions::assert_eq!(
r#"
static mut GLOBAL: 1: usize = 1;
static mut GLOBAL: usize = 1;
fn main() {
let x: 10 = 10;
@@ -1115,21 +1115,21 @@ fn main() {
editor.update_in(cx, |editor, window, cx| {
pretty_assertions::assert_eq!(
r#"
static mut GLOBAL: 1: usize = 1;
static mut GLOBAL: usize = 1;
fn main() {
let x = 10;
let value = 42;
let y = 4;
let tester = {
let x: 10 = 10;
let value: 42 = 42;
let y: 4 = 4;
let tester: size=3 = {
let y = 10;
let y = 5;
let b = 3;
vec![y, 20, 30]
};
let caller = || {
let x = 3;
let caller: <not available> = || {
let x: 10 = 3;
println!("x={}", x);
};
@@ -1193,10 +1193,10 @@ fn main() {
editor.update_in(cx, |editor, window, cx| {
pretty_assertions::assert_eq!(
r#"
static mut GLOBAL: 1: usize = 1;
static mut GLOBAL: usize = 1;
fn main() {
let x = 10;
let x: 3 = 10;
let value = 42;
let y = 4;
let tester = {
@@ -1208,7 +1208,7 @@ fn main() {
let caller = || {
let x: 3 = 3;
println!("x={}", x);
println!("x={}", x: 3);
};
caller();
@@ -1338,7 +1338,7 @@ fn main() {
editor.update_in(cx, |editor, window, cx| {
pretty_assertions::assert_eq!(
r#"
static mut GLOBAL: 2: usize = 1;
static mut GLOBAL: usize = 1;
fn main() {
let x: 10 = 10;
@@ -1362,7 +1362,7 @@ fn main() {
GLOBAL = 2;
}
let result = value * 2 * x;
let result = value: 42 * 2 * x: 10;
println!("Simple test executed: value={}, result={}", value, result);
assert!(true);
}
@@ -1483,7 +1483,7 @@ fn main() {
editor.update_in(cx, |editor, window, cx| {
pretty_assertions::assert_eq!(
r#"
static mut GLOBAL: 2: usize = 1;
static mut GLOBAL: usize = 1;
fn main() {
let x: 10 = 10;
@@ -1507,8 +1507,8 @@ fn main() {
GLOBAL = 2;
}
let result: 840 = value * 2 * x;
println!("Simple test executed: value={}, result={}", value, result);
let result: 840 = value: 42 * 2 * x: 10;
println!("Simple test executed: value={}, result={}", value: 42, result: 840);
assert!(true);
}
"#
@@ -1519,6 +1519,7 @@ fn main() {
}
fn rust_lang() -> Language {
let debug_variables_query = include_str!("../../../languages/src/rust/debugger.scm");
Language::new(
LanguageConfig {
name: "Rust".into(),
@@ -1530,6 +1531,8 @@ fn rust_lang() -> Language {
},
Some(tree_sitter_rust::LANGUAGE.into()),
)
.with_debug_variables_query(debug_variables_query)
.unwrap()
}
#[gpui::test]
@@ -1818,8 +1821,8 @@ def process_data(untyped_param, typed_param: int, another_typed: str):
def process_data(untyped_param: test_value, typed_param: 42: int, another_typed: world: str):
# Local variables
x: 10 = 10
result: 84 = typed_param * 2
text: Hello, world = "Hello, " + another_typed
result: 84 = typed_param: 42 * 2
text: Hello, world = "Hello, " + another_typed: world
# For loop with range
sum_value: 10 = 0
@@ -1837,6 +1840,7 @@ def process_data(untyped_param, typed_param: int, another_typed: str):
}
fn python_lang() -> Language {
let debug_variables_query = include_str!("../../../languages/src/python/debugger.scm");
Language::new(
LanguageConfig {
name: "Python".into(),
@@ -1848,4 +1852,392 @@ fn python_lang() -> Language {
},
Some(tree_sitter_python::LANGUAGE.into()),
)
.with_debug_variables_query(debug_variables_query)
.unwrap()
}
fn go_lang() -> Language {
let debug_variables_query = include_str!("../../../languages/src/go/debugger.scm");
Language::new(
LanguageConfig {
name: "Go".into(),
matcher: LanguageMatcher {
path_suffixes: vec!["go".to_string()],
..Default::default()
},
..Default::default()
},
Some(tree_sitter_go::LANGUAGE.into()),
)
.with_debug_variables_query(debug_variables_query)
.unwrap()
}
/// Test utility function for inline values testing
///
/// # Arguments
/// * `variables` - List of tuples containing (variable_name, variable_value)
/// * `before` - Source code before inline values are applied
/// * `after` - Expected source code after inline values are applied
/// * `language` - Language configuration to use for parsing
/// * `executor` - Background executor for async operations
/// * `cx` - Test app context
async fn test_inline_values_util(
local_variables: &[(&str, &str)],
global_variables: &[(&str, &str)],
before: &str,
after: &str,
active_debug_line: Option<usize>,
language: Language,
executor: BackgroundExecutor,
cx: &mut TestAppContext,
) {
init_test(cx);
let lines_count = before.lines().count();
let stop_line =
active_debug_line.unwrap_or_else(|| if lines_count > 6 { 6 } else { lines_count - 1 });
let fs = FakeFs::new(executor.clone());
fs.insert_tree(path!("/project"), json!({ "main.rs": before.to_string() }))
.await;
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
let workspace = init_test_workspace(&project, cx).await;
workspace
.update(cx, |workspace, window, cx| {
workspace.focus_panel::<DebugPanel>(window, cx);
})
.unwrap();
let cx = &mut VisualTestContext::from_window(*workspace, cx);
let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
let client = session.update(cx, |session, _| session.adapter_client().unwrap());
client.on_request::<dap::requests::Threads, _>(|_, _| {
Ok(dap::ThreadsResponse {
threads: vec![dap::Thread {
id: 1,
name: "main".into(),
}],
})
});
client.on_request::<dap::requests::StackTrace, _>(move |_, _| {
Ok(dap::StackTraceResponse {
stack_frames: vec![dap::StackFrame {
id: 1,
name: "main".into(),
source: Some(dap::Source {
name: Some("main.rs".into()),
path: Some(path!("/project/main.rs").into()),
source_reference: None,
presentation_hint: None,
origin: None,
sources: None,
adapter_data: None,
checksums: None,
}),
line: stop_line as u64,
column: 1,
end_line: None,
end_column: None,
can_restart: None,
instruction_pointer_reference: None,
module_id: None,
presentation_hint: None,
}],
total_frames: None,
})
});
let local_vars: Vec<Variable> = local_variables
.iter()
.map(|(name, value)| Variable {
name: (*name).into(),
value: (*value).into(),
type_: None,
presentation_hint: None,
evaluate_name: None,
variables_reference: 0,
named_variables: None,
indexed_variables: None,
memory_reference: None,
declaration_location_reference: None,
value_location_reference: None,
})
.collect();
let global_vars: Vec<Variable> = global_variables
.iter()
.map(|(name, value)| Variable {
name: (*name).into(),
value: (*value).into(),
type_: None,
presentation_hint: None,
evaluate_name: None,
variables_reference: 0,
named_variables: None,
indexed_variables: None,
memory_reference: None,
declaration_location_reference: None,
value_location_reference: None,
})
.collect();
client.on_request::<Variables, _>({
let local_vars = Arc::new(local_vars.clone());
let global_vars = Arc::new(global_vars.clone());
move |_, args| {
let variables = match args.variables_reference {
2 => (*local_vars).clone(),
3 => (*global_vars).clone(),
_ => vec![],
};
Ok(dap::VariablesResponse { variables })
}
});
client.on_request::<dap::requests::Scopes, _>(move |_, _| {
Ok(dap::ScopesResponse {
scopes: vec![
Scope {
name: "Local".into(),
presentation_hint: None,
variables_reference: 2,
named_variables: None,
indexed_variables: None,
expensive: false,
source: None,
line: None,
column: None,
end_line: None,
end_column: None,
},
Scope {
name: "Global".into(),
presentation_hint: None,
variables_reference: 3,
named_variables: None,
indexed_variables: None,
expensive: false,
source: None,
line: None,
column: None,
end_line: None,
end_column: None,
},
],
})
});
if !global_variables.is_empty() {
let global_evaluate_map: std::collections::HashMap<String, String> = global_variables
.iter()
.map(|(name, value)| (name.to_string(), value.to_string()))
.collect();
client.on_request::<dap::requests::Evaluate, _>(move |_, args| {
let value = global_evaluate_map
.get(&args.expression)
.unwrap_or(&"undefined".to_string())
.clone();
Ok(dap::EvaluateResponse {
result: value,
type_: None,
presentation_hint: None,
variables_reference: 0,
named_variables: None,
indexed_variables: None,
memory_reference: None,
value_location_reference: None,
})
});
}
client
.fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
reason: dap::StoppedEventReason::Pause,
description: None,
thread_id: Some(1),
preserve_focus_hint: None,
text: None,
all_threads_stopped: None,
hit_breakpoint_ids: None,
}))
.await;
cx.run_until_parked();
let project_path = Path::new(path!("/project"));
let worktree = project
.update(cx, |project, cx| project.find_worktree(project_path, cx))
.expect("This worktree should exist in project")
.0;
let worktree_id = workspace
.update(cx, |_, _, cx| worktree.read(cx).id())
.unwrap();
let buffer = project
.update(cx, |project, cx| {
project.open_buffer((worktree_id, "main.rs"), cx)
})
.await
.unwrap();
buffer.update(cx, |buffer, cx| {
buffer.set_language(Some(Arc::new(language)), cx);
});
let (editor, cx) = cx.add_window_view(|window, cx| {
Editor::new(
EditorMode::full(),
MultiBuffer::build_from_buffer(buffer, cx),
Some(project),
window,
cx,
)
});
active_debug_session_panel(workspace, cx).update_in(cx, |_, window, cx| {
cx.focus_self(window);
});
cx.run_until_parked();
editor.update(cx, |editor, cx| editor.refresh_inline_values(cx));
cx.run_until_parked();
editor.update_in(cx, |editor, window, cx| {
pretty_assertions::assert_eq!(after, editor.snapshot(window, cx).text());
});
}
#[gpui::test]
async fn test_inline_values_example(executor: BackgroundExecutor, cx: &mut TestAppContext) {
let variables = [("x", "10"), ("y", "20"), ("result", "30")];
let before = r#"
fn main() {
let x = 10;
let y = 20;
let result = x + y;
println!("Result: {}", result);
}
"#
.unindent();
let after = r#"
fn main() {
let x: 10 = 10;
let y: 20 = 20;
let result: 30 = x: 10 + y: 20;
println!("Result: {}", result: 30);
}
"#
.unindent();
test_inline_values_util(
&variables,
&[],
&before,
&after,
None,
rust_lang(),
executor,
cx,
)
.await;
}
#[gpui::test]
async fn test_inline_values_with_globals(executor: BackgroundExecutor, cx: &mut TestAppContext) {
let variables = [("x", "5"), ("y", "10")];
let before = r#"
static mut GLOBAL_COUNTER: usize = 42;
fn main() {
let x = 5;
let y = 10;
unsafe {
GLOBAL_COUNTER += 1;
}
println!("x={}, y={}, global={}", x, y, unsafe { GLOBAL_COUNTER });
}
"#
.unindent();
let after = r#"
static mut GLOBAL_COUNTER: 42: usize = 42;
fn main() {
let x: 5 = 5;
let y: 10 = 10;
unsafe {
GLOBAL_COUNTER += 1;
}
println!("x={}, y={}, global={}", x, y, unsafe { GLOBAL_COUNTER });
}
"#
.unindent();
test_inline_values_util(
&variables,
&[("GLOBAL_COUNTER", "42")],
&before,
&after,
None,
rust_lang(),
executor,
cx,
)
.await;
}
#[gpui::test]
async fn test_go_inline_values(executor: BackgroundExecutor, cx: &mut TestAppContext) {
let variables = [("x", "42"), ("y", "hello")];
let before = r#"
package main
var globalCounter int = 100
func main() {
x := 42
y := "hello"
z := x + 10
println(x, y, z)
}
"#
.unindent();
let after = r#"
package main
var globalCounter: 100 int = 100
func main() {
x: 42 := 42
y := "hello"
z := x + 10
println(x, y, z)
}
"#
.unindent();
test_inline_values_util(
&variables,
&[("globalCounter", "100")],
&before,
&after,
None,
go_lang(),
executor,
cx,
)
.await;
}

View File

@@ -19167,7 +19167,7 @@ impl Editor {
let current_execution_position = self
.highlighted_rows
.get(&TypeId::of::<ActiveDebugLine>())
.and_then(|lines| lines.last().map(|line| line.range.start));
.and_then(|lines| lines.last().map(|line| line.range.end));
self.inline_value_cache.refresh_task = cx.spawn(async move |editor, cx| {
let inline_values = editor
@@ -21553,7 +21553,6 @@ impl SemanticsProvider for Entity<Project> {
fn inline_values(
&self,
buffer_handle: Entity<Buffer>,
range: Range<text::Anchor>,
cx: &mut App,
) -> Option<Task<anyhow::Result<Vec<InlayHint>>>> {

View File

@@ -1030,6 +1030,7 @@ pub fn response_events_to_markdown(
Ok(LanguageModelCompletionEvent::Thinking { text, .. }) => {
thinking_buffer.push_str(text);
}
Ok(LanguageModelCompletionEvent::RedactedThinking { .. }) => {}
Ok(LanguageModelCompletionEvent::Stop(reason)) => {
flush_buffers(&mut response, &mut text_buffer, &mut thinking_buffer);
response.push_str(&format!("**Stop**: {:?}\n\n", reason));
@@ -1126,6 +1127,7 @@ impl ThreadDialog {
// Skip these
Ok(LanguageModelCompletionEvent::UsageUpdate(_))
| Ok(LanguageModelCompletionEvent::RedactedThinking { .. })
| Ok(LanguageModelCompletionEvent::StatusUpdate { .. })
| Ok(LanguageModelCompletionEvent::StartMessage { .. })
| Ok(LanguageModelCompletionEvent::Stop(_)) => {}

View File

@@ -298,7 +298,3 @@ path = "examples/uniform_list.rs"
[[example]]
name = "window_shadow"
path = "examples/window_shadow.rs"
[[example]]
name = "uniform_table"
path = "examples/uniform_table.rs"

View File

@@ -1,6 +1,6 @@
use gpui::{
App, Application, Bounds, Context, ListSizingBehavior, Window, WindowBounds, WindowOptions,
div, prelude::*, px, rgb, size, uniform_list,
App, Application, Bounds, Context, Window, WindowBounds, WindowOptions, div, prelude::*, px,
rgb, size, uniform_list,
};
struct UniformListExample {}
@@ -12,7 +12,6 @@ impl Render for UniformListExample {
"entries",
50,
cx.processor(|_this, range, _window, _cx| {
dbg!(&range);
let mut items = Vec::new();
for ix in range {
let item = ix + 1;
@@ -31,7 +30,6 @@ impl Render for UniformListExample {
items
}),
)
.with_sizing_behavior(ListSizingBehavior::Infer)
.h_full(),
)
}

View File

@@ -1,54 +0,0 @@
use gpui::{
App, Application, Bounds, Context, Window, WindowBounds, WindowOptions, div, prelude::*, px,
rgb, size,
};
struct UniformTableExample {}
impl Render for UniformTableExample {
fn render(&mut self, _window: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
const COLS: usize = 24;
const ROWS: usize = 100;
let mut headers = [0; COLS];
for column in 0..COLS {
headers[column] = column;
}
div().bg(rgb(0xffffff)).size_full().child(
gpui::uniform_table("simple table", ROWS, move |range, _, _| {
dbg!(&range);
range
.map(|row_index| {
let mut row = [0; COLS];
for col in 0..COLS {
row[col] = (row_index + 1) * (col + 1);
}
row.map(|cell| ToString::to_string(&cell))
.map(|cell| div().flex().flex_row().child(cell))
.map(IntoElement::into_any_element)
})
.collect()
})
.with_width_from_item(Some(ROWS - 1))
// todo! without this, the AvailableSpace passed in window.request_measured_layout is a Definite(2600px) on Anthony's machine
// this doesn't make sense, and results in the full range of elements getting rendered. This also occurs on uniform_list
// This is resulting from windows.bounds() being called
.h_full(),
)
}
}
fn main() {
Application::new().run(|cx: &mut App| {
let bounds = Bounds::centered(None, size(px(300.0), px(300.0)), cx);
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
..Default::default()
},
|_, cx| cx.new(|_| UniformTableExample {}),
)
.unwrap();
});
}

View File

@@ -1334,11 +1334,6 @@ impl App {
self.pending_effects.push_back(Effect::RefreshWindows);
}
/// Get all key bindings in the app.
pub fn key_bindings(&self) -> Rc<RefCell<Keymap>> {
self.keymap.clone()
}
/// Register a global listener for actions invoked via the keyboard.
pub fn on_action<A: Action>(&mut self, listener: impl Fn(&A, &mut Self) + 'static) {
self.global_action_listeners

View File

@@ -613,10 +613,10 @@ pub trait InteractiveElement: Sized {
/// Track the focus state of the given focus handle on this element.
/// If the focus handle is focused by the application, this element will
/// apply its focused styles.
fn track_focus(mut self, focus_handle: &FocusHandle) -> Self {
fn track_focus(mut self, focus_handle: &FocusHandle) -> FocusableWrapper<Self> {
self.interactivity().focusable = true;
self.interactivity().tracked_focus_handle = Some(focus_handle.clone());
self
FocusableWrapper { element: self }
}
/// Set the keymap context for this element. This will be used to determine
@@ -980,35 +980,15 @@ pub trait InteractiveElement: Sized {
self.interactivity().block_mouse_except_scroll();
self
}
/// Set the given styles to be applied when this element, specifically, is focused.
/// Requires that the element is focusable. Elements can be made focusable using [`InteractiveElement::track_focus`].
fn focus(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self
where
Self: Sized,
{
self.interactivity().focus_style = Some(Box::new(f(StyleRefinement::default())));
self
}
/// Set the given styles to be applied when this element is inside another element that is focused.
/// Requires that the element is focusable. Elements can be made focusable using [`InteractiveElement::track_focus`].
fn in_focus(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self
where
Self: Sized,
{
self.interactivity().in_focus_style = Some(Box::new(f(StyleRefinement::default())));
self
}
}
/// A trait for elements that want to use the standard GPUI interactivity features
/// that require state.
pub trait StatefulInteractiveElement: InteractiveElement {
/// Set this element to focusable.
fn focusable(mut self) -> Self {
fn focusable(mut self) -> FocusableWrapper<Self> {
self.interactivity().focusable = true;
self
FocusableWrapper { element: self }
}
/// Set the overflow x and y to scroll.
@@ -1138,6 +1118,27 @@ pub trait StatefulInteractiveElement: InteractiveElement {
}
}
/// A trait for providing focus related APIs to interactive elements
pub trait FocusableElement: InteractiveElement {
/// Set the given styles to be applied when this element, specifically, is focused.
fn focus(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self
where
Self: Sized,
{
self.interactivity().focus_style = Some(Box::new(f(StyleRefinement::default())));
self
}
/// Set the given styles to be applied when this element is inside another element that is focused.
fn in_focus(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self
where
Self: Sized,
{
self.interactivity().in_focus_style = Some(Box::new(f(StyleRefinement::default())));
self
}
}
pub(crate) type MouseDownListener =
Box<dyn Fn(&MouseDownEvent, DispatchPhase, &Hitbox, &mut Window, &mut App) + 'static>;
pub(crate) type MouseUpListener =
@@ -2776,6 +2777,126 @@ impl GroupHitboxes {
}
}
/// A wrapper around an element that can be focused.
pub struct FocusableWrapper<E> {
/// The element that is focusable
pub element: E,
}
impl<E: InteractiveElement> FocusableElement for FocusableWrapper<E> {}
impl<E> InteractiveElement for FocusableWrapper<E>
where
E: InteractiveElement,
{
fn interactivity(&mut self) -> &mut Interactivity {
self.element.interactivity()
}
}
impl<E: StatefulInteractiveElement> StatefulInteractiveElement for FocusableWrapper<E> {}
impl<E> Styled for FocusableWrapper<E>
where
E: Styled,
{
fn style(&mut self) -> &mut StyleRefinement {
self.element.style()
}
}
impl FocusableWrapper<Div> {
/// Add a listener to be called when the children of this `Div` are prepainted.
/// This allows you to store the [`Bounds`] of the children for later use.
pub fn on_children_prepainted(
mut self,
listener: impl Fn(Vec<Bounds<Pixels>>, &mut Window, &mut App) + 'static,
) -> Self {
self.element = self.element.on_children_prepainted(listener);
self
}
}
impl<E> Element for FocusableWrapper<E>
where
E: Element,
{
type RequestLayoutState = E::RequestLayoutState;
type PrepaintState = E::PrepaintState;
fn id(&self) -> Option<ElementId> {
self.element.id()
}
fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
self.element.source_location()
}
fn request_layout(
&mut self,
id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
window: &mut Window,
cx: &mut App,
) -> (LayoutId, Self::RequestLayoutState) {
self.element.request_layout(id, inspector_id, window, cx)
}
fn prepaint(
&mut self,
id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
state: &mut Self::RequestLayoutState,
window: &mut Window,
cx: &mut App,
) -> E::PrepaintState {
self.element
.prepaint(id, inspector_id, bounds, state, window, cx)
}
fn paint(
&mut self,
id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
request_layout: &mut Self::RequestLayoutState,
prepaint: &mut Self::PrepaintState,
window: &mut Window,
cx: &mut App,
) {
self.element.paint(
id,
inspector_id,
bounds,
request_layout,
prepaint,
window,
cx,
)
}
}
impl<E> IntoElement for FocusableWrapper<E>
where
E: IntoElement,
{
type Element = E::Element;
fn into_element(self) -> Self::Element {
self.element.into_element()
}
}
impl<E> ParentElement for FocusableWrapper<E>
where
E: ParentElement,
{
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
self.element.extend(elements)
}
}
/// A wrapper around an element that can store state, produced after assigning an ElementId.
pub struct Stateful<E> {
pub(crate) element: E,
@@ -2806,6 +2927,8 @@ where
}
}
impl<E: FocusableElement> FocusableElement for Stateful<E> {}
impl<E> Element for Stateful<E>
where
E: Element,

View File

@@ -25,7 +25,7 @@ use std::{
use thiserror::Error;
use util::ResultExt;
use super::{Stateful, StatefulInteractiveElement};
use super::{FocusableElement, Stateful, StatefulInteractiveElement};
/// The delay before showing the loading state.
pub const LOADING_DELAY: Duration = Duration::from_millis(200);
@@ -509,6 +509,8 @@ impl IntoElement for Img {
}
}
impl FocusableElement for Img {}
impl StatefulInteractiveElement for Img {}
impl ImageSource {

View File

@@ -10,7 +10,6 @@ mod surface;
mod svg;
mod text;
mod uniform_list;
mod uniform_table;
pub use anchored::*;
pub use animation::*;
@@ -24,4 +23,3 @@ pub use surface::*;
pub use svg::*;
pub use text::*;
pub use uniform_list::*;
pub use uniform_table::*;

View File

@@ -1,516 +0,0 @@
use std::{cell::RefCell, cmp, ops::Range, rc::Rc};
use smallvec::SmallVec;
use crate::{
AnyElement, App, AvailableSpace, Bounds, ContentMask, Div, Element, ElementId, GlobalElementId,
Hitbox, InspectorElementId, Interactivity, IntoElement, IsZero as _, LayoutId, Length,
Overflow, Pixels, ScrollHandle, Size, StyleRefinement, Styled, Window, point, px, size,
};
/// todo!
pub struct UniformTable<const COLS: usize> {
id: ElementId,
row_count: usize,
render_rows:
Rc<dyn Fn(Range<usize>, &mut Window, &mut App) -> Vec<[AnyElement; COLS]> + 'static>,
interactivity: Interactivity,
source_location: &'static std::panic::Location<'static>,
item_to_measure_index: usize,
scroll_handle: Option<UniformTableScrollHandle>, // todo! we either want to make our own or make a shared scroll handle between list and table
sizings: [Length; COLS],
}
/// TODO
#[track_caller]
pub fn uniform_table<const COLS: usize, F>(
id: impl Into<ElementId>,
row_count: usize,
render_rows: F,
) -> UniformTable<COLS>
where
F: 'static + Fn(Range<usize>, &mut Window, &mut App) -> Vec<[AnyElement; COLS]>,
{
let mut base_style = StyleRefinement::default();
base_style.overflow.y = Some(Overflow::Scroll);
let id = id.into();
let mut interactivity = Interactivity::new();
interactivity.element_id = Some(id.clone());
UniformTable {
id: id.clone(),
row_count,
render_rows: Rc::new(render_rows),
interactivity: Interactivity {
element_id: Some(id),
base_style: Box::new(base_style),
..Interactivity::new()
},
source_location: core::panic::Location::caller(),
item_to_measure_index: 0,
scroll_handle: None,
sizings: [Length::Auto; COLS],
}
}
impl<const COLS: usize> UniformTable<COLS> {
/// todo!
pub fn with_width_from_item(mut self, item_index: Option<usize>) -> Self {
self.item_to_measure_index = item_index.unwrap_or(0);
self
}
}
impl<const COLS: usize> IntoElement for UniformTable<COLS> {
type Element = Self;
fn into_element(self) -> Self::Element {
self
}
}
impl<const COLS: usize> Styled for UniformTable<COLS> {
fn style(&mut self) -> &mut StyleRefinement {
&mut self.interactivity.base_style
}
}
impl<const COLS: usize> Element for UniformTable<COLS> {
type RequestLayoutState = ();
type PrepaintState = (Option<Hitbox>, SmallVec<[AnyElement; 32]>);
fn id(&self) -> Option<ElementId> {
Some(self.id.clone())
}
fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
Some(self.source_location)
}
fn request_layout(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
window: &mut Window,
cx: &mut App,
) -> (LayoutId, Self::RequestLayoutState) {
let measure_cx = MeasureContext::new(self);
let item_size = measure_cx.measure_item(AvailableSpace::MinContent, None, window, cx);
let layout_id =
self.interactivity.request_layout(
global_id,
inspector_id,
window,
cx,
|style, window, _cx| {
window.with_text_style(style.text_style().cloned(), |window| {
window.request_measured_layout(
style,
move |known_dimensions, available_space, window, cx| {
let desired_height = item_size.height * measure_cx.row_count;
let width = known_dimensions.width.unwrap_or(match available_space
.width
{
AvailableSpace::Definite(x) => x,
AvailableSpace::MinContent | AvailableSpace::MaxContent => {
item_size.width
}
});
let height =
known_dimensions.height.unwrap_or(
match available_space.height {
AvailableSpace::Definite(height) => desired_height
.min(dbg!(window.bounds()).size.height),
AvailableSpace::MinContent
| AvailableSpace::MaxContent => desired_height,
},
);
size(width, height)
},
)
})
},
);
(layout_id, ())
}
fn prepaint(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
_request_layout: &mut Self::RequestLayoutState,
window: &mut Window,
cx: &mut App,
) -> Self::PrepaintState {
let style = self
.interactivity
.compute_style(global_id, None, window, cx);
let border = style.border_widths.to_pixels(window.rem_size());
let padding = style
.padding
.to_pixels(bounds.size.into(), window.rem_size());
let padded_bounds = Bounds::from_corners(
bounds.origin + point(border.left + padding.left, border.top + padding.top),
bounds.bottom_right()
- point(border.right + padding.right, border.bottom + padding.bottom),
);
let can_scroll_horizontally = true;
let mut column_widths = [Pixels::default(); COLS];
let longest_row_size = MeasureContext::new(self).measure_item(
AvailableSpace::Definite(bounds.size.width),
Some(&mut column_widths),
window,
cx,
);
// We need to run this for each column:
let content_width = padded_bounds.size.width.max(longest_row_size.width);
let content_size = Size {
width: content_width,
height: longest_row_size.height * self.row_count + padding.top + padding.bottom,
};
let shared_scroll_offset = self.interactivity.scroll_offset.clone().unwrap();
let row_height = longest_row_size.height;
let shared_scroll_to_item = self.scroll_handle.as_mut().and_then(|handle| {
let mut handle = handle.0.borrow_mut();
handle.last_row_size = Some(RowSize {
row: padded_bounds.size,
contents: content_size,
});
handle.deferred_scroll_to_item.take()
});
let mut rendered_rows = SmallVec::default();
let hitbox = self.interactivity.prepaint(
global_id,
inspector_id,
bounds,
content_size,
window,
cx,
|style, mut scroll_offset, hitbox, window, cx| {
dbg!(bounds, window.bounds());
let border = style.border_widths.to_pixels(window.rem_size());
let padding = style
.padding
.to_pixels(bounds.size.into(), window.rem_size());
let padded_bounds = Bounds::from_corners(
bounds.origin + point(border.left + padding.left, border.top),
bounds.bottom_right() - point(border.right + padding.right, border.bottom),
);
let y_flipped = if let Some(scroll_handle) = self.scroll_handle.as_mut() {
let mut scroll_state = scroll_handle.0.borrow_mut();
scroll_state.base_handle.set_bounds(bounds);
scroll_state.y_flipped
} else {
false
};
if self.row_count > 0 {
let content_height = row_height * self.row_count + padding.top + padding.bottom;
let is_scrolled_vertically = !scroll_offset.y.is_zero();
let min_vertical_scroll_offset = padded_bounds.size.height - content_height;
if is_scrolled_vertically && scroll_offset.y < min_vertical_scroll_offset {
shared_scroll_offset.borrow_mut().y = min_vertical_scroll_offset;
scroll_offset.y = min_vertical_scroll_offset;
}
let content_width = content_size.width + padding.left + padding.right;
let is_scrolled_horizontally =
can_scroll_horizontally && !scroll_offset.x.is_zero();
if is_scrolled_horizontally && content_width <= padded_bounds.size.width {
shared_scroll_offset.borrow_mut().x = Pixels::ZERO;
scroll_offset.x = Pixels::ZERO;
}
if let Some((mut ix, scroll_strategy)) = shared_scroll_to_item {
if y_flipped {
ix = self.row_count.saturating_sub(ix + 1);
}
let list_height = dbg!(padded_bounds.size.height);
let mut updated_scroll_offset = shared_scroll_offset.borrow_mut();
let item_top = row_height * ix + padding.top;
let item_bottom = item_top + row_height;
let scroll_top = -updated_scroll_offset.y;
let mut scrolled_to_top = false;
if item_top < scroll_top + padding.top {
scrolled_to_top = true;
updated_scroll_offset.y = -(item_top) + padding.top;
} else if item_bottom > scroll_top + list_height - padding.bottom {
scrolled_to_top = true;
updated_scroll_offset.y = -(item_bottom - list_height) - padding.bottom;
}
match scroll_strategy {
ScrollStrategy::Top => {}
ScrollStrategy::Center => {
if scrolled_to_top {
let item_center = item_top + row_height / 2.0;
let target_scroll_top = item_center - list_height / 2.0;
if item_top < scroll_top
|| item_bottom > scroll_top + list_height
{
updated_scroll_offset.y = -target_scroll_top
.max(Pixels::ZERO)
.min(content_height - list_height)
.max(Pixels::ZERO);
}
}
}
}
scroll_offset = *updated_scroll_offset
}
let first_visible_element_ix =
(-(scroll_offset.y + padding.top) / row_height).floor() as usize;
let last_visible_element_ix = ((-scroll_offset.y + padded_bounds.size.height)
/ row_height)
.ceil() as usize;
let visible_range =
first_visible_element_ix..cmp::min(last_visible_element_ix, self.row_count);
let rows = if y_flipped {
let flipped_range = self.row_count.saturating_sub(visible_range.end)
..self.row_count.saturating_sub(visible_range.start);
let mut items = (self.render_rows)(flipped_range, window, cx);
items.reverse();
items
} else {
(self.render_rows)(visible_range.clone(), window, cx)
};
let content_mask = ContentMask { bounds };
window.with_content_mask(Some(content_mask), |window| {
let available_width = if can_scroll_horizontally {
padded_bounds.size.width + scroll_offset.x.abs()
} else {
padded_bounds.size.width
};
let available_space = size(
AvailableSpace::Definite(available_width),
AvailableSpace::Definite(row_height),
);
for (mut row, ix) in rows.into_iter().zip(visible_range.clone()) {
let row_origin = padded_bounds.origin
+ point(
if can_scroll_horizontally {
scroll_offset.x + padding.left
} else {
scroll_offset.x
},
row_height * ix + scroll_offset.y + padding.top,
);
let mut item = render_row(row, column_widths, row_height).into_any();
item.layout_as_root(available_space, window, cx);
item.prepaint_at(row_origin, window, cx);
rendered_rows.push(item);
}
});
}
hitbox
},
);
return (hitbox, rendered_rows);
}
fn paint(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
_: &mut Self::RequestLayoutState,
(hitbox, rendered_rows): &mut Self::PrepaintState,
window: &mut Window,
cx: &mut App,
) {
self.interactivity.paint(
global_id,
inspector_id,
bounds,
hitbox.as_ref(),
window,
cx,
|_, window, cx| {
for item in rendered_rows {
item.paint(window, cx);
}
},
)
}
}
const DIVIDER_PADDING_PX: Pixels = px(2.0);
fn render_row<const COLS: usize>(
row: [AnyElement; COLS],
column_widths: [Pixels; COLS],
row_height: Pixels,
) -> Div {
use crate::ParentElement;
let mut div = crate::div().flex().flex_row().gap(DIVIDER_PADDING_PX);
for (ix, cell) in row.into_iter().enumerate() {
div = div.child(
crate::div()
.w(column_widths[ix])
.h(row_height)
.overflow_hidden()
.child(cell),
)
}
div
}
struct MeasureContext<const COLS: usize> {
row_count: usize,
item_to_measure_index: usize,
render_rows:
Rc<dyn Fn(Range<usize>, &mut Window, &mut App) -> Vec<[AnyElement; COLS]> + 'static>,
sizings: [Length; COLS],
}
impl<const COLS: usize> MeasureContext<COLS> {
fn new(table: &UniformTable<COLS>) -> Self {
Self {
row_count: table.row_count,
item_to_measure_index: table.item_to_measure_index,
render_rows: table.render_rows.clone(),
sizings: table.sizings,
}
}
fn measure_item(
&self,
table_width: AvailableSpace,
column_sizes: Option<&mut [Pixels; COLS]>,
window: &mut Window,
cx: &mut App,
) -> Size<Pixels> {
if self.row_count == 0 {
return Size::default();
}
let item_ix = cmp::min(self.item_to_measure_index, self.row_count - 1);
let mut items = (self.render_rows)(item_ix..item_ix + 1, window, cx);
let Some(mut item_to_measure) = items.pop() else {
return Size::default();
};
let mut default_column_sizes = [Pixels::default(); COLS];
let column_sizes = column_sizes.unwrap_or(&mut default_column_sizes);
let mut row_height = px(0.0);
for i in 0..COLS {
let column_available_width = match self.sizings[i] {
Length::Definite(definite_length) => match table_width {
AvailableSpace::Definite(pixels) => AvailableSpace::Definite(
definite_length.to_pixels(pixels.into(), window.rem_size()),
),
AvailableSpace::MinContent => AvailableSpace::MinContent,
AvailableSpace::MaxContent => AvailableSpace::MaxContent,
},
Length::Auto => AvailableSpace::MaxContent,
};
let column_available_space = size(column_available_width, AvailableSpace::MinContent);
// todo!: Adjust row sizing to account for inter-column spacing
let cell_size = item_to_measure[i].layout_as_root(column_available_space, window, cx);
column_sizes[i] = cell_size.width;
row_height = row_height.max(cell_size.height);
}
let mut width = Pixels::ZERO;
for size in *column_sizes {
width += size;
}
Size::new(width + (COLS - 1) * DIVIDER_PADDING_PX, row_height)
}
}
impl<const COLS: usize> UniformTable<COLS> {}
/// A handle for controlling the scroll position of a uniform list.
/// This should be stored in your view and passed to the uniform_list on each frame.
#[derive(Clone, Debug, Default)]
pub struct UniformTableScrollHandle(pub Rc<RefCell<UniformTableScrollState>>);
/// Where to place the element scrolled to.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ScrollStrategy {
/// Place the element at the top of the list's viewport.
Top,
/// Attempt to place the element in the middle of the list's viewport.
/// May not be possible if there's not enough list items above the item scrolled to:
/// in this case, the element will be placed at the closest possible position.
Center,
}
#[derive(Copy, Clone, Debug, Default)]
/// The size of the item and its contents.
pub struct RowSize {
/// The size of the item.
pub row: Size<Pixels>,
/// The size of the item's contents, which may be larger than the item itself,
/// if the item was bounded by a parent element.
pub contents: Size<Pixels>,
}
#[derive(Clone, Debug, Default)]
#[allow(missing_docs)]
pub struct UniformTableScrollState {
pub base_handle: ScrollHandle,
pub deferred_scroll_to_item: Option<(usize, ScrollStrategy)>,
/// Size of the item, captured during last layout.
pub last_row_size: Option<RowSize>,
/// Whether the list was vertically flipped during last layout.
pub y_flipped: bool,
}
impl UniformTableScrollHandle {
/// Create a new scroll handle to bind to a uniform list.
pub fn new() -> Self {
Self(Rc::new(RefCell::new(UniformTableScrollState {
base_handle: ScrollHandle::new(),
deferred_scroll_to_item: None,
last_row_size: None,
y_flipped: false,
})))
}
/// Scroll the list to the given item index.
pub fn scroll_to_item(&self, ix: usize, strategy: ScrollStrategy) {
self.0.borrow_mut().deferred_scroll_to_item = Some((ix, strategy));
}
/// Check if the list is flipped vertically.
pub fn y_flipped(&self) -> bool {
self.0.borrow().y_flipped
}
/// Get the index of the topmost visible child.
#[cfg(any(test, feature = "test-support"))]
pub fn logical_scroll_top_index(&self) -> usize {
let this = self.0.borrow();
this.deferred_scroll_to_item
.map(|(ix, _)| ix)
.unwrap_or_else(|| this.base_handle.logical_scroll_top().0)
}
}

View File

@@ -2,7 +2,7 @@ use std::rc::Rc;
use collections::HashMap;
use crate::{Action, InvalidKeystrokeError, KeyBindingContextPredicate, Keystroke, SharedString};
use crate::{Action, InvalidKeystrokeError, KeyBindingContextPredicate, Keystroke};
use smallvec::SmallVec;
/// A keybinding and its associated metadata, from the keymap.
@@ -11,8 +11,6 @@ pub struct KeyBinding {
pub(crate) keystrokes: SmallVec<[Keystroke; 2]>,
pub(crate) context_predicate: Option<Rc<KeyBindingContextPredicate>>,
pub(crate) meta: Option<KeyBindingMetaIndex>,
/// The json input string used when building the keybinding, if any
pub(crate) action_input: Option<SharedString>,
}
impl Clone for KeyBinding {
@@ -22,7 +20,6 @@ impl Clone for KeyBinding {
keystrokes: self.keystrokes.clone(),
context_predicate: self.context_predicate.clone(),
meta: self.meta,
action_input: self.action_input.clone(),
}
}
}
@@ -35,7 +32,7 @@ impl KeyBinding {
} else {
None
};
Self::load(keystrokes, Box::new(action), context_predicate, None, None).unwrap()
Self::load(keystrokes, Box::new(action), context_predicate, None).unwrap()
}
/// Load a keybinding from the given raw data.
@@ -44,7 +41,6 @@ impl KeyBinding {
action: Box<dyn Action>,
context_predicate: Option<Rc<KeyBindingContextPredicate>>,
key_equivalents: Option<&HashMap<char, char>>,
action_input: Option<SharedString>,
) -> std::result::Result<Self, InvalidKeystrokeError> {
let mut keystrokes: SmallVec<[Keystroke; 2]> = keystrokes
.split_whitespace()
@@ -66,7 +62,6 @@ impl KeyBinding {
action,
context_predicate,
meta: None,
action_input,
})
}
@@ -115,11 +110,6 @@ impl KeyBinding {
pub fn meta(&self) -> Option<KeyBindingMetaIndex> {
self.meta
}
/// Get the action input associated with the action for this binding
pub fn action_input(&self) -> Option<SharedString> {
self.action_input.clone()
}
}
impl std::fmt::Debug for KeyBinding {

View File

@@ -3,7 +3,7 @@
//! application to avoid having to import each trait individually.
pub use crate::{
AppContext as _, BorrowAppContext, Context, Element, InteractiveElement, IntoElement,
ParentElement, Refineable, Render, RenderOnce, StatefulInteractiveElement, Styled, StyledImage,
VisualContext, util::FluentBuilder,
AppContext as _, BorrowAppContext, Context, Element, FocusableElement, InteractiveElement,
IntoElement, ParentElement, Refineable, Render, RenderOnce, StatefulInteractiveElement, Styled,
StyledImage, VisualContext, util::FluentBuilder,
};

View File

@@ -20,6 +20,7 @@ test-support = [
"text/test-support",
"tree-sitter-rust",
"tree-sitter-python",
"tree-sitter-rust",
"tree-sitter-typescript",
"settings/test-support",
"util/test-support",

View File

@@ -1,12 +1,6 @@
pub use crate::{
Grammar, Language, LanguageRegistry,
diagnostic_set::DiagnosticSet,
highlight_map::{HighlightId, HighlightMap},
proto,
};
use crate::{
LanguageScope, Outline, OutlineConfig, RunnableCapture, RunnableTag, TextObject,
TreeSitterOptions,
DebuggerTextObject, LanguageScope, Outline, OutlineConfig, RunnableCapture, RunnableTag,
TextObject, TreeSitterOptions,
diagnostic_set::{DiagnosticEntry, DiagnosticGroup},
language_settings::{LanguageSettings, language_settings},
outline::OutlineItem,
@@ -17,6 +11,12 @@ use crate::{
task_context::RunnableRange,
text_diff::text_diff,
};
pub use crate::{
Grammar, Language, LanguageRegistry,
diagnostic_set::DiagnosticSet,
highlight_map::{HighlightId, HighlightMap},
proto,
};
use anyhow::{Context as _, Result};
pub use clock::ReplicaId;
use clock::{AGENT_REPLICA_ID, Lamport};
@@ -3848,6 +3848,74 @@ impl BufferSnapshot {
.filter(|pair| !pair.newline_only)
}
pub fn debug_variables_query<T: ToOffset>(
&self,
range: Range<T>,
) -> impl Iterator<Item = (Range<usize>, DebuggerTextObject)> + '_ {
let range = range.start.to_offset(self).saturating_sub(1)
..self.len().min(range.end.to_offset(self) + 1);
let mut matches = self.syntax.matches_with_options(
range.clone(),
&self.text,
TreeSitterOptions::default(),
|grammar| grammar.debug_variables_config.as_ref().map(|c| &c.query),
);
let configs = matches
.grammars()
.iter()
.map(|grammar| grammar.debug_variables_config.as_ref())
.collect::<Vec<_>>();
let mut captures = Vec::<(Range<usize>, DebuggerTextObject)>::new();
iter::from_fn(move || {
loop {
while let Some(capture) = captures.pop() {
if capture.0.overlaps(&range) {
return Some(capture);
}
}
let mat = matches.peek()?;
let Some(config) = configs[mat.grammar_index].as_ref() else {
matches.advance();
continue;
};
for capture in mat.captures {
let Some(ix) = config
.objects_by_capture_ix
.binary_search_by_key(&capture.index, |e| e.0)
.ok()
else {
continue;
};
let text_object = config.objects_by_capture_ix[ix].1;
let byte_range = capture.node.byte_range();
let mut found = false;
for (range, existing) in captures.iter_mut() {
if existing == &text_object {
range.start = range.start.min(byte_range.start);
range.end = range.end.max(byte_range.end);
found = true;
break;
}
}
if !found {
captures.push((byte_range, text_object));
}
}
matches.advance();
}
})
}
pub fn text_object_ranges<T: ToOffset>(
&self,
range: Range<T>,

View File

@@ -1082,6 +1082,7 @@ pub struct Grammar {
pub embedding_config: Option<EmbeddingConfig>,
pub(crate) injection_config: Option<InjectionConfig>,
pub(crate) override_config: Option<OverrideConfig>,
pub(crate) debug_variables_config: Option<DebugVariablesConfig>,
pub(crate) highlight_map: Mutex<HighlightMap>,
}
@@ -1104,6 +1105,22 @@ pub struct OutlineConfig {
pub annotation_capture_ix: Option<u32>,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum DebuggerTextObject {
Variable,
Scope,
}
impl DebuggerTextObject {
pub fn from_capture_name(name: &str) -> Option<DebuggerTextObject> {
match name {
"debug-variable" => Some(DebuggerTextObject::Variable),
"debug-scope" => Some(DebuggerTextObject::Scope),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum TextObject {
InsideFunction,
@@ -1206,6 +1223,11 @@ struct BracketsPatternConfig {
newline_only: bool,
}
pub struct DebugVariablesConfig {
pub query: Query,
pub objects_by_capture_ix: Vec<(u32, DebuggerTextObject)>,
}
impl Language {
pub fn new(config: LanguageConfig, ts_language: Option<tree_sitter::Language>) -> Self {
Self::new_with_id(LanguageId::new(), config, ts_language)
@@ -1237,6 +1259,7 @@ impl Language {
redactions_config: None,
runnable_config: None,
error_query: Query::new(&ts_language, "(ERROR) @error").ok(),
debug_variables_config: None,
ts_language,
highlight_map: Default::default(),
})
@@ -1307,6 +1330,11 @@ impl Language {
.with_text_object_query(query.as_ref())
.context("Error loading textobject query")?;
}
if let Some(query) = queries.debugger {
self = self
.with_debug_variables_query(query.as_ref())
.context("Error loading debug variables query")?;
}
Ok(self)
}
@@ -1425,6 +1453,24 @@ impl Language {
Ok(self)
}
pub fn with_debug_variables_query(mut self, source: &str) -> Result<Self> {
let grammar = self.grammar_mut().context("cannot mutate grammar")?;
let query = Query::new(&grammar.ts_language, source)?;
let mut objects_by_capture_ix = Vec::new();
for (ix, name) in query.capture_names().iter().enumerate() {
if let Some(text_object) = DebuggerTextObject::from_capture_name(name) {
objects_by_capture_ix.push((ix as u32, text_object));
}
}
grammar.debug_variables_config = Some(DebugVariablesConfig {
query,
objects_by_capture_ix,
});
Ok(self)
}
pub fn with_brackets_query(mut self, source: &str) -> Result<Self> {
let grammar = self.grammar_mut().context("cannot mutate grammar")?;
let query = Query::new(&grammar.ts_language, source)?;
@@ -1930,6 +1976,10 @@ impl Grammar {
.capture_index_for_name(name)?;
Some(self.highlight_map.lock().get(capture_id))
}
pub fn debug_variables_config(&self) -> Option<&DebugVariablesConfig> {
self.debug_variables_config.as_ref()
}
}
impl CodeLabel {

View File

@@ -226,7 +226,7 @@ pub const QUERY_FILENAME_PREFIXES: &[(
("overrides", |q| &mut q.overrides),
("redactions", |q| &mut q.redactions),
("runnables", |q| &mut q.runnables),
("debug_variables", |q| &mut q.debug_variables),
("debugger", |q| &mut q.debugger),
("textobjects", |q| &mut q.text_objects),
];
@@ -243,7 +243,7 @@ pub struct LanguageQueries {
pub redactions: Option<Cow<'static, str>>,
pub runnables: Option<Cow<'static, str>>,
pub text_objects: Option<Cow<'static, str>>,
pub debug_variables: Option<Cow<'static, str>>,
pub debugger: Option<Cow<'static, str>>,
}
#[derive(Clone, Default)]

View File

@@ -67,6 +67,9 @@ pub enum LanguageModelCompletionEvent {
text: String,
signature: Option<String>,
},
RedactedThinking {
data: String,
},
ToolUse(LanguageModelToolUse),
StartMessage {
message_id: String,
@@ -359,6 +362,7 @@ pub trait LanguageModel: Send + Sync {
Ok(LanguageModelCompletionEvent::StartMessage { .. }) => None,
Ok(LanguageModelCompletionEvent::Text(text)) => Some(Ok(text)),
Ok(LanguageModelCompletionEvent::Thinking { .. }) => None,
Ok(LanguageModelCompletionEvent::RedactedThinking { .. }) => None,
Ok(LanguageModelCompletionEvent::Stop(_)) => None,
Ok(LanguageModelCompletionEvent::ToolUse(_)) => None,
Ok(LanguageModelCompletionEvent::UsageUpdate(token_usage)) => {

View File

@@ -303,7 +303,7 @@ pub enum MessageContent {
text: String,
signature: Option<String>,
},
RedactedThinking(Vec<u8>),
RedactedThinking(String),
Image(LanguageModelImage),
ToolUse(LanguageModelToolUse),
ToolResult(LanguageModelToolResult),

View File

@@ -554,9 +554,7 @@ pub fn into_anthropic(
}
MessageContent::RedactedThinking(data) => {
if !data.is_empty() {
Some(anthropic::RequestContent::RedactedThinking {
data: String::from_utf8(data).ok()?,
})
Some(anthropic::RequestContent::RedactedThinking { data })
} else {
None
}
@@ -730,10 +728,8 @@ impl AnthropicEventMapper {
signature: None,
})]
}
ResponseContent::RedactedThinking { .. } => {
// Redacted thinking is encrypted and not accessible to the user, see:
// https://docs.anthropic.com/en/docs/build-with-claude/extended-thinking#suggestions-for-handling-redacted-thinking-in-production
Vec::new()
ResponseContent::RedactedThinking { data } => {
vec![Ok(LanguageModelCompletionEvent::RedactedThinking { data })]
}
ResponseContent::ToolUse { id, name, .. } => {
self.tool_uses_by_index.insert(

View File

@@ -0,0 +1,26 @@
(parameter_declaration (identifier) @debug-variable)
(short_var_declaration (expression_list (identifier) @debug-variable))
(var_declaration (var_spec (identifier) @debug-variable))
(const_declaration (const_spec (identifier) @debug-variable))
(assignment_statement (expression_list (identifier) @debug-variable))
(binary_expression (identifier) @debug-variable
(#not-match? @debug-variable "^[A-Z]"))
(call_expression (argument_list (identifier) @debug-variable
(#not-match? @debug-variable "^[A-Z]")))
(return_statement (expression_list (identifier) @debug-variable
(#not-match? @debug-variable "^[A-Z]")))
(range_clause (expression_list (identifier) @debug-variable))
(parenthesized_expression (identifier) @debug-variable
(#not-match? @debug-variable "^[A-Z]"))
(block) @debug-scope
(function_declaration) @debug-scope

View File

@@ -0,0 +1,43 @@
(identifier) @debug-variable
(#eq? @debug-variable "self")
(assignment left: (identifier) @debug-variable)
(assignment left: (pattern_list (identifier) @debug-variable))
(assignment left: (tuple_pattern (identifier) @debug-variable))
(augmented_assignment left: (identifier) @debug-variable)
(for_statement left: (identifier) @debug-variable)
(for_statement left: (pattern_list (identifier) @debug-variable))
(for_statement left: (tuple_pattern (identifier) @debug-variable))
(for_in_clause left: (identifier) @debug-variable)
(for_in_clause left: (pattern_list (identifier) @debug-variable))
(for_in_clause left: (tuple_pattern (identifier) @debug-variable))
(as_pattern (identifier) @debug-variable)
(binary_operator left: (identifier) @debug-variable (#not-match? @debug-variable "^[A-Z]"))
(binary_operator right: (identifier) @debug-variable (#not-match? @debug-variable "^[A-Z]"))
(comparison_operator (identifier) @debug-variable (#not-match? @debug-variable "^[A-Z]"))
(list (identifier) @debug-variable (#not-match? @debug-variable "^[A-Z]"))
(tuple (identifier) @debug-variable (#not-match? @debug-variable "^[A-Z]"))
(set (identifier) @debug-variable (#not-match? @debug-variable "^[A-Z]"))
(subscript value: (identifier) @debug-variable (#not-match? @debug-variable "^[A-Z]"))
(attribute object: (identifier) @debug-variable (#not-match? @debug-variable "^[A-Z]"))
(return_statement (identifier) @debug-variable (#not-match? @debug-variable "^[A-Z]"))
(parenthesized_expression (identifier) @debug-variable (#not-match? @debug-variable "^[A-Z]"))
(argument_list (identifier) @debug-variable (#not-match? @debug-variable "^[A-Z]"))
(if_statement condition: (identifier) @debug-variable (#not-match? @debug-variable "^[A-Z]"))
(while_statement condition: (identifier) @debug-variable (#not-match? @debug-variable "^[A-Z]"))
(block) @debug-scope
(module) @debug-scope

View File

@@ -0,0 +1,50 @@
(metavariable) @debug-variable
(parameter (identifier) @debug-variable)
(self) @debug-variable
(static_item (identifier) @debug-variable)
(const_item (identifier) @debug-variable)
(let_declaration pattern: (identifier) @debug-variable)
(let_condition (identifier) @debug-variable)
(match_arm (identifier) @debug-variable)
(for_expression (identifier) @debug-variable)
(closure_parameters (identifier) @debug-variable)
(assignment_expression (identifier) @debug-variable)
(field_expression (identifier) @debug-variable)
(binary_expression (identifier) @debug-variable
(#not-match? @debug-variable "^[A-Z]"))
(reference_expression (identifier) @debug-variable
(#not-match? @debug-variable "^[A-Z]"))
(array_expression (identifier) @debug-variable)
(tuple_expression (identifier) @debug-variable)
(return_expression (identifier) @debug-variable)
(await_expression (identifier) @debug-variable)
(try_expression (identifier) @debug-variable)
(index_expression (identifier) @debug-variable)
(range_expression (identifier) @debug-variable)
(unary_expression (identifier) @debug-variable)
(if_expression (identifier) @debug-variable)
(while_expression (identifier) @debug-variable)
(parenthesized_expression (identifier) @debug-variable)
(arguments (identifier) @debug-variable
(#not-match? @debug-variable "^[A-Z]"))
(macro_invocation (token_tree (identifier) @debug-variable
(#not-match? @debug-variable "^[A-Z]")))
(block) @debug-scope

View File

@@ -588,7 +588,14 @@ impl DapStore {
cx: &mut Context<Self>,
) -> Task<Result<Vec<InlayHint>>> {
let snapshot = buffer_handle.read(cx).snapshot();
let all_variables = session.read(cx).variables_by_stack_frame_id(stack_frame_id);
let local_variables =
session
.read(cx)
.variables_by_stack_frame_id(stack_frame_id, false, true);
let global_variables =
session
.read(cx)
.variables_by_stack_frame_id(stack_frame_id, true, false);
fn format_value(mut value: String) -> String {
const LIMIT: usize = 100;
@@ -617,10 +624,20 @@ impl DapStore {
match inline_value_location.lookup {
VariableLookupKind::Variable => {
let Some(variable) = all_variables
.iter()
.find(|variable| variable.name == inline_value_location.variable_name)
else {
let variable_search =
if inline_value_location.scope
== dap::inline_value::VariableScope::Local
{
local_variables.iter().chain(global_variables.iter()).find(
|variable| variable.name == inline_value_location.variable_name,
)
} else {
global_variables.iter().find(|variable| {
variable.name == inline_value_location.variable_name
})
};
let Some(variable) = variable_search else {
continue;
};

View File

@@ -2171,7 +2171,12 @@ impl Session {
.unwrap_or_default()
}
pub fn variables_by_stack_frame_id(&self, stack_frame_id: StackFrameId) -> Vec<dap::Variable> {
pub fn variables_by_stack_frame_id(
&self,
stack_frame_id: StackFrameId,
globals: bool,
locals: bool,
) -> Vec<dap::Variable> {
let Some(stack_frame) = self.stack_frames.get(&stack_frame_id) else {
return Vec::new();
};
@@ -2179,6 +2184,10 @@ impl Session {
stack_frame
.scopes
.iter()
.filter(|scope| {
(scope.name.to_lowercase().contains("local") && locals)
|| (scope.name.to_lowercase().contains("global") && globals)
})
.filter_map(|scope| self.variables.get(&scope.variables_reference))
.flatten()
.cloned()

View File

@@ -31,6 +31,8 @@ use git_store::{Repository, RepositoryId};
pub mod search_history;
mod yarn;
use dap::inline_value::{InlineValueLocation, VariableLookupKind, VariableScope};
use crate::git_store::GitStore;
pub use git_store::{
ConflictRegion, ConflictSet, ConflictSetSnapshot, ConflictSetUpdate,
@@ -45,7 +47,7 @@ use client::{
};
use clock::ReplicaId;
use dap::{DapRegistry, client::DebugAdapterClient};
use dap::client::DebugAdapterClient;
use collections::{BTreeSet, HashMap, HashSet};
use debounced_delay::DebouncedDelay;
@@ -111,7 +113,7 @@ use std::{
use task_store::TaskStore;
use terminals::Terminals;
use text::{Anchor, BufferId};
use text::{Anchor, BufferId, Point};
use toolchain_store::EmptyToolchainStore;
use util::{
ResultExt as _,
@@ -3667,35 +3669,15 @@ impl Project {
range: Range<text::Anchor>,
cx: &mut Context<Self>,
) -> Task<anyhow::Result<Vec<InlayHint>>> {
let language_name = buffer_handle
.read(cx)
.language()
.map(|language| language.name().to_string());
let Some(inline_value_provider) = language_name
.and_then(|language| DapRegistry::global(cx).inline_value_provider(&language))
else {
return Task::ready(Err(anyhow::anyhow!("Inline value provider not found")));
};
let snapshot = buffer_handle.read(cx).snapshot();
let Some(root_node) = snapshot.syntax_root_ancestor(range.end) else {
return Task::ready(Ok(vec![]));
};
let captures = snapshot.debug_variables_query(Anchor::MIN..range.end);
let row = snapshot
.summary_for_anchor::<text::PointUtf16>(&range.end)
.row as usize;
let inline_value_locations = inline_value_provider.provide(
root_node,
snapshot
.text_for_range(Anchor::MIN..range.end)
.collect::<String>()
.as_str(),
row,
);
let inline_value_locations = provide_inline_values(captures, &snapshot, row);
let stack_frame_id = active_stack_frame.stack_frame_id;
cx.spawn(async move |this, cx| {
@@ -5377,3 +5359,69 @@ fn proto_to_prompt(level: proto::language_server_prompt_request::Level) -> gpui:
proto::language_server_prompt_request::Level::Critical(_) => gpui::PromptLevel::Critical,
}
}
fn provide_inline_values(
captures: impl Iterator<Item = (Range<usize>, language::DebuggerTextObject)>,
snapshot: &language::BufferSnapshot,
max_row: usize,
) -> Vec<InlineValueLocation> {
let mut variables = Vec::new();
let mut variable_position = HashSet::default();
let mut scopes = Vec::new();
let active_debug_line_offset = snapshot.point_to_offset(Point::new(max_row as u32, 0));
for (capture_range, capture_kind) in captures {
match capture_kind {
language::DebuggerTextObject::Variable => {
let variable_name = snapshot
.text_for_range(capture_range.clone())
.collect::<String>();
let point = snapshot.offset_to_point(capture_range.end);
while scopes.last().map_or(false, |scope: &Range<_>| {
!scope.contains(&capture_range.start)
}) {
scopes.pop();
}
if point.row as usize > max_row {
break;
}
let scope = if scopes
.last()
.map_or(true, |scope| !scope.contains(&active_debug_line_offset))
{
VariableScope::Global
} else {
VariableScope::Local
};
if variable_position.insert(capture_range.end) {
variables.push(InlineValueLocation {
variable_name,
scope,
lookup: VariableLookupKind::Variable,
row: point.row as usize,
column: point.column as usize,
});
}
}
language::DebuggerTextObject::Scope => {
while scopes.last().map_or_else(
|| false,
|scope: &Range<usize>| {
!(scope.contains(&capture_range.start)
&& scope.contains(&capture_range.end))
},
) {
scopes.pop();
}
scopes.push(capture_range);
}
}
}
variables
}

View File

@@ -3,7 +3,7 @@ use collections::{BTreeMap, HashMap, IndexMap};
use fs::Fs;
use gpui::{
Action, ActionBuildError, App, InvalidKeystrokeError, KEYSTROKE_PARSE_EXPECTED_MESSAGE,
KeyBinding, KeyBindingContextPredicate, KeyBindingMetaIndex, NoAction, SharedString,
KeyBinding, KeyBindingContextPredicate, KeyBindingMetaIndex, NoAction,
};
use schemars::{
JsonSchema,
@@ -399,13 +399,7 @@ impl KeymapFile {
},
};
let key_binding = match KeyBinding::load(
keystrokes,
action,
context,
key_equivalents,
action_input_string.map(SharedString::from),
) {
let key_binding = match KeyBinding::load(keystrokes, action, context, key_equivalents) {
Ok(key_binding) => key_binding,
Err(InvalidKeystrokeError { keystroke }) => {
return Err(format!(

View File

@@ -12,21 +12,12 @@ workspace = true
path = "src/settings_ui.rs"
[dependencies]
command_palette.workspace = true
command_palette_hooks.workspace = true
component.workspace = true
collections.workspace = true
db.workspace = true
editor.workspace = true
feature_flags.workspace = true
fs.workspace = true
fuzzy.workspace = true
gpui.workspace = true
log.workspace = true
menu.workspace = true
paths.workspace = true
project.workspace = true
search.workspace = true
schemars.workspace = true
serde.workspace = true
settings.workspace = true

View File

@@ -1,612 +0,0 @@
use std::{fmt::Write as _, ops::Range, sync::Arc};
use collections::HashSet;
use db::anyhow::anyhow;
use editor::{Editor, EditorEvent};
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
AppContext as _, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
FontWeight, Global, KeyContext, ScrollStrategy, Subscription, WeakEntity, actions, div,
};
use util::ResultExt;
use ui::{
ActiveTheme as _, App, BorrowAppContext, ParentElement as _, Render, SharedString, Styled as _,
Window, prelude::*,
};
use workspace::{Item, ModalView, SerializableItem, Workspace, register_serializable_item};
use crate::{
keybindings::persistence::KEYBINDING_EDITORS,
ui_components::table::{Table, TableInteractionState},
};
actions!(zed, [OpenKeymapEditor]);
pub fn init(cx: &mut App) {
let keymap_event_channel = KeymapEventChannel::new();
cx.set_global(keymap_event_channel);
cx.observe_new(|workspace: &mut Workspace, _window, _cx| {
workspace.register_action(|workspace, _: &OpenKeymapEditor, window, cx| {
let open_keymap_editor =
cx.new(|cx| KeymapEditor::new(workspace.weak_handle(), window, cx));
workspace.add_item_to_center(Box::new(open_keymap_editor), window, cx);
});
})
.detach();
register_serializable_item::<KeymapEditor>(cx);
}
pub struct KeymapEventChannel {}
impl Global for KeymapEventChannel {}
impl KeymapEventChannel {
fn new() -> Self {
Self {}
}
pub fn trigger_keymap_changed(cx: &mut App) {
cx.update_global(|_event_channel: &mut Self, _| {
/* triggers observers in KeymapEditors */
});
}
}
struct KeymapEditor {
workspace: WeakEntity<Workspace>,
focus_handle: FocusHandle,
_keymap_subscription: Subscription,
keybindings: Vec<ProcessedKeybinding>,
// corresponds 1 to 1 with keybindings
string_match_candidates: Arc<Vec<StringMatchCandidate>>,
matches: Vec<StringMatch>,
table_interaction_state: Entity<TableInteractionState>,
filter_editor: Entity<Editor>,
selected_index: Option<usize>,
}
impl EventEmitter<()> for KeymapEditor {}
impl Focusable for KeymapEditor {
fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
return self.filter_editor.focus_handle(cx);
}
}
impl KeymapEditor {
fn new(workspace: WeakEntity<Workspace>, window: &mut Window, cx: &mut Context<Self>) -> Self {
let focus_handle = cx.focus_handle();
let _keymap_subscription =
cx.observe_global::<KeymapEventChannel>(Self::update_keybindings);
let table_interaction_state = TableInteractionState::new(window, cx);
let filter_editor = cx.new(|cx| {
let mut editor = Editor::single_line(window, cx);
editor.set_placeholder_text("Filter action names...", cx);
editor
});
cx.subscribe(&filter_editor, |this, _, e: &EditorEvent, cx| {
if !matches!(e, EditorEvent::BufferEdited) {
return;
}
this.update_matches(cx);
})
.detach();
let mut this = Self {
workspace,
keybindings: vec![],
string_match_candidates: Arc::new(vec![]),
matches: vec![],
focus_handle: focus_handle.clone(),
_keymap_subscription,
table_interaction_state,
filter_editor,
selected_index: None,
};
this.update_keybindings(cx);
this
}
fn update_matches(&mut self, cx: &mut Context<Self>) {
let query = self.filter_editor.read(cx).text(cx);
let string_match_candidates = self.string_match_candidates.clone();
let executor = cx.background_executor().clone();
let keybind_count = self.keybindings.len();
let query = command_palette::normalize_action_query(&query);
let fuzzy_match = cx.background_spawn(async move {
fuzzy::match_strings(
&string_match_candidates,
&query,
true,
true,
keybind_count,
&Default::default(),
executor,
)
.await
});
cx.spawn(async move |this, cx| {
let matches = fuzzy_match.await;
this.update(cx, |this, cx| {
this.selected_index.take();
this.scroll_to_item(0, ScrollStrategy::Top, cx);
this.matches = matches;
cx.notify();
})
})
.detach();
}
fn process_bindings(
cx: &mut Context<Self>,
) -> (Vec<ProcessedKeybinding>, Vec<StringMatchCandidate>) {
let key_bindings_ptr = cx.key_bindings();
let lock = key_bindings_ptr.borrow();
let key_bindings = lock.bindings();
let mut unmapped_action_names = HashSet::from_iter(cx.all_action_names());
let mut processed_bindings = Vec::new();
let mut string_match_candidates = Vec::new();
for key_binding in key_bindings {
let mut keystroke_text = String::new();
for keystroke in key_binding.keystrokes() {
write!(&mut keystroke_text, "{} ", keystroke.unparse()).ok();
}
let keystroke_text = keystroke_text.trim().to_string();
let context = key_binding
.predicate()
.map(|predicate| predicate.to_string())
.unwrap_or_else(|| "<global>".to_string());
let source = key_binding
.meta()
.map(|meta| settings::KeybindSource::from_meta(meta).name().into());
let action_name = key_binding.action().name();
unmapped_action_names.remove(&action_name);
let index = processed_bindings.len();
let string_match_candidate = StringMatchCandidate::new(index, &action_name);
processed_bindings.push(ProcessedKeybinding {
keystroke_text: keystroke_text.into(),
action: action_name.into(),
action_input: key_binding.action_input(),
context: context.into(),
source,
});
string_match_candidates.push(string_match_candidate);
}
let empty = SharedString::new_static("");
for action_name in unmapped_action_names.into_iter() {
let index = processed_bindings.len();
let string_match_candidate = StringMatchCandidate::new(index, &action_name);
processed_bindings.push(ProcessedKeybinding {
keystroke_text: empty.clone(),
action: (*action_name).into(),
action_input: None,
context: empty.clone(),
source: None,
});
string_match_candidates.push(string_match_candidate);
}
(processed_bindings, string_match_candidates)
}
fn update_keybindings(self: &mut KeymapEditor, cx: &mut Context<KeymapEditor>) {
let (key_bindings, string_match_candidates) = Self::process_bindings(cx);
self.keybindings = key_bindings;
self.string_match_candidates = Arc::new(string_match_candidates);
self.matches = self
.string_match_candidates
.iter()
.enumerate()
.map(|(ix, candidate)| StringMatch {
candidate_id: ix,
score: 0.0,
positions: vec![],
string: candidate.string.clone(),
})
.collect();
self.update_matches(cx);
cx.notify();
}
fn dispatch_context(&self, _window: &Window, _cx: &Context<Self>) -> KeyContext {
let mut dispatch_context = KeyContext::new_with_defaults();
dispatch_context.add("KeymapEditor");
dispatch_context.add("menu");
// todo! track key context in keybind edit modal
// let identifier = if self.keymap_editor.focus_handle(cx).is_focused(window) {
// "editing"
// } else {
// "not_editing"
// };
// dispatch_context.add(identifier);
dispatch_context
}
fn scroll_to_item(&self, index: usize, strategy: ScrollStrategy, cx: &mut App) {
let index = usize::min(index, self.matches.len().saturating_sub(1));
self.table_interaction_state.update(cx, |this, _cx| {
this.scroll_handle.scroll_to_item(index, strategy);
});
}
fn select_next(&mut self, _: &menu::SelectNext, window: &mut Window, cx: &mut Context<Self>) {
if let Some(selected) = self.selected_index {
let selected = selected + 1;
if selected >= self.matches.len() {
self.select_last(&Default::default(), window, cx);
} else {
self.selected_index = Some(selected);
self.scroll_to_item(selected, ScrollStrategy::Center, cx);
cx.notify();
}
} else {
self.select_first(&Default::default(), window, cx);
}
}
fn select_previous(
&mut self,
_: &menu::SelectPrevious,
window: &mut Window,
cx: &mut Context<Self>,
) {
if let Some(selected) = self.selected_index {
if selected == 0 {
return;
}
let selected = selected - 1;
if selected >= self.matches.len() {
self.select_last(&Default::default(), window, cx);
} else {
self.selected_index = Some(selected);
self.scroll_to_item(selected, ScrollStrategy::Center, cx);
cx.notify();
}
} else {
self.select_last(&Default::default(), window, cx);
}
}
fn select_first(
&mut self,
_: &menu::SelectFirst,
_window: &mut Window,
cx: &mut Context<Self>,
) {
if self.matches.get(0).is_some() {
self.selected_index = Some(0);
self.scroll_to_item(0, ScrollStrategy::Center, cx);
cx.notify();
}
}
fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) {
if self.matches.last().is_some() {
let index = self.matches.len() - 1;
self.selected_index = Some(index);
self.scroll_to_item(index, ScrollStrategy::Center, cx);
cx.notify();
}
}
fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
let Some(index) = self.selected_index else {
return;
};
let keybind = self.keybindings[self.matches[index].candidate_id].clone();
self.edit_keybinding(keybind, window, cx);
}
fn edit_keybinding(
&mut self,
keybind: ProcessedKeybinding,
window: &mut Window,
cx: &mut Context<Self>,
) {
// todo! how to map keybinds to how to update/edit them
_ = keybind;
self.workspace
.update(cx, |workspace, cx| {
workspace.toggle_modal(window, cx, |window, cx| {
let modal = KeybindingEditorModal::new(window, cx);
window.focus(&modal.focus_handle(cx));
modal
});
})
.log_err();
}
fn focus_search(
&mut self,
_: &search::FocusSearch,
window: &mut Window,
cx: &mut Context<Self>,
) {
if !self
.filter_editor
.focus_handle(cx)
.contains_focused(window, cx)
{
window.focus(&self.filter_editor.focus_handle(cx));
} else {
self.filter_editor.update(cx, |editor, cx| {
editor.select_all(&Default::default(), window, cx);
});
}
self.selected_index.take();
}
}
#[derive(Clone)]
struct ProcessedKeybinding {
keystroke_text: SharedString,
action: SharedString,
action_input: Option<SharedString>,
context: SharedString,
source: Option<SharedString>,
}
impl Item for KeymapEditor {
type Event = ();
fn tab_content_text(&self, _detail: usize, _cx: &App) -> ui::SharedString {
"Keymap Editor".into()
}
}
impl Render for KeymapEditor {
fn render(&mut self, window: &mut Window, cx: &mut ui::Context<Self>) -> impl ui::IntoElement {
let row_count = self.matches.len();
let theme = cx.theme();
div()
.key_context(self.dispatch_context(window, cx))
.on_action(cx.listener(Self::select_next))
.on_action(cx.listener(Self::select_previous))
.on_action(cx.listener(Self::select_first))
.on_action(cx.listener(Self::select_last))
.on_action(cx.listener(Self::focus_search))
.on_action(cx.listener(Self::confirm))
.size_full()
.bg(theme.colors().editor_background)
.id("keymap-editor")
.track_focus(&self.focus_handle)
.px_4()
.v_flex()
.pb_4()
.child(
h_flex()
.key_context({
let mut context = KeyContext::new_with_defaults();
context.add("BufferSearchBar");
context
})
.w_full()
.h_12()
.px_4()
.my_4()
.border_2()
.border_color(theme.colors().border)
.child(self.filter_editor.clone()),
)
.child(
Table::new()
.interactable(&self.table_interaction_state)
.striped()
.column_widths([rems(24.), rems(16.), rems(32.), rems(8.)])
.header(["Command", "Keystrokes", "Context", "Source"])
.selected_item_index(self.selected_index.clone())
.on_click_row(cx.processor(|this, row_index, _window, _cx| {
this.selected_index = Some(row_index);
}))
.uniform_list(
"keymap-editor-table",
row_count,
cx.processor(move |this, range: Range<usize>, _window, _cx| {
range
.filter_map(|index| {
let candidate_id = this.matches.get(index)?.candidate_id;
let binding = &this.keybindings[candidate_id];
let action = h_flex()
.items_start()
.gap_1()
.child(binding.action.clone())
.when_some(
binding.action_input.clone(),
|this, binding_input| this.child(binding_input),
);
let keystrokes = binding.keystroke_text.clone();
let context = binding.context.clone();
let source = binding.source.clone().unwrap_or_default();
Some([
action.into_any_element(),
keystrokes.into_any_element(),
context.into_any_element(),
source.into_any_element(),
])
})
.collect()
}),
),
)
}
}
struct KeybindingEditorModal {
keybind_editor: Entity<Editor>,
}
impl ModalView for KeybindingEditorModal {}
impl EventEmitter<DismissEvent> for KeybindingEditorModal {}
impl Focusable for KeybindingEditorModal {
fn focus_handle(&self, cx: &App) -> FocusHandle {
self.keybind_editor.focus_handle(cx)
}
}
impl KeybindingEditorModal {
pub fn new(window: &mut Window, cx: &mut App) -> Self {
let keybind_editor = cx.new(|cx| {
let editor = Editor::single_line(window, cx);
editor
});
Self { keybind_editor }
}
}
impl Render for KeybindingEditorModal {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let theme = cx.theme().colors();
return v_flex()
.items_center()
.text_center()
.bg(theme.background)
.border_color(theme.border)
.border_2()
.px_4()
.py_2()
.w(rems(36.))
.child(div().text_lg().font_weight(FontWeight::BOLD).child(
// todo! better text
"Input desired keybinding, then hit Enter to save",
))
.child(
h_flex()
.w_full()
.h_12()
.px_4()
.my_4()
.border_2()
.border_color(theme.border)
.child(self.keybind_editor.clone()),
);
}
}
impl SerializableItem for KeymapEditor {
fn serialized_item_kind() -> &'static str {
"KeymapEditor"
}
fn cleanup(
workspace_id: workspace::WorkspaceId,
alive_items: Vec<workspace::ItemId>,
_window: &mut Window,
cx: &mut App,
) -> gpui::Task<gpui::Result<()>> {
workspace::delete_unloaded_items(
alive_items,
workspace_id,
"keybinding_editors",
&KEYBINDING_EDITORS,
cx,
)
}
fn deserialize(
_project: Entity<project::Project>,
workspace: WeakEntity<Workspace>,
workspace_id: workspace::WorkspaceId,
item_id: workspace::ItemId,
window: &mut Window,
cx: &mut App,
) -> gpui::Task<gpui::Result<Entity<Self>>> {
window.spawn(cx, async move |cx| {
if KEYBINDING_EDITORS
.get_keybinding_editor(item_id, workspace_id)?
.is_some()
{
cx.update(|window, cx| cx.new(|cx| KeymapEditor::new(workspace, window, cx)))
} else {
Err(anyhow!("No keybinding editor to deserialize"))
}
})
}
fn serialize(
&mut self,
workspace: &mut Workspace,
item_id: workspace::ItemId,
_closing: bool,
_window: &mut Window,
cx: &mut ui::Context<Self>,
) -> Option<gpui::Task<gpui::Result<()>>> {
let workspace_id = workspace.database_id()?;
Some(cx.background_spawn(async move {
KEYBINDING_EDITORS
.save_keybinding_editor(item_id, workspace_id)
.await
}))
}
fn should_serialize(&self, _event: &Self::Event) -> bool {
false
}
}
mod persistence {
use db::{define_connection, query, sqlez_macros::sql};
use workspace::WorkspaceDb;
define_connection! {
pub static ref KEYBINDING_EDITORS: KeybindingEditorDb<WorkspaceDb> =
&[sql!(
CREATE TABLE keybinding_editors (
workspace_id INTEGER,
item_id INTEGER UNIQUE,
PRIMARY KEY(workspace_id, item_id),
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
ON DELETE CASCADE
) STRICT;
)];
}
impl KeybindingEditorDb {
query! {
pub async fn save_keybinding_editor(
item_id: workspace::ItemId,
workspace_id: workspace::WorkspaceId
) -> Result<()> {
INSERT OR REPLACE INTO keybinding_editors(item_id, workspace_id)
VALUES (?, ?)
}
}
query! {
pub fn get_keybinding_editor(
item_id: workspace::ItemId,
workspace_id: workspace::WorkspaceId
) -> Result<Option<workspace::ItemId>> {
SELECT item_id
FROM keybinding_editors
WHERE item_id = ? AND workspace_id = ?
}
}
}
}

View File

@@ -20,9 +20,6 @@ use workspace::{Workspace, with_active_or_new_workspace};
use crate::appearance_settings_controls::AppearanceSettingsControls;
pub mod keybindings;
pub mod ui_components;
pub struct SettingsUiFeatureFlag;
impl FeatureFlag for SettingsUiFeatureFlag {
@@ -124,8 +121,6 @@ pub fn init(cx: &mut App) {
.detach();
})
.detach();
keybindings::init(cx);
}
async fn handle_import_vscode_settings(

View File

@@ -1 +0,0 @@
pub mod table;

View File

@@ -1,884 +0,0 @@
use std::{ops::Range, rc::Rc, time::Duration};
use editor::{EditorSettings, ShowScrollbar, scroll::ScrollbarAutoHide};
use gpui::{
AppContext, Axis, Context, Entity, FocusHandle, FontWeight, Length,
ListHorizontalSizingBehavior, ListSizingBehavior, MouseButton, Stateful, Task,
UniformListScrollHandle, WeakEntity, transparent_black, uniform_list,
};
use settings::Settings as _;
use ui::{
ActiveTheme as _, AnyElement, App, Button, ButtonCommon as _, ButtonStyle, Color, Component,
ComponentScope, Div, ElementId, FixedWidth as _, FluentBuilder as _, Indicator,
InteractiveElement as _, IntoElement, ParentElement, Pixels, RegisterComponent, RenderOnce,
Scrollbar, ScrollbarState, StatefulInteractiveElement as _, Styled, StyledExt as _,
StyledTypography, Window, div, example_group_with_title, h_flex, px, single_example, v_flex,
};
struct UniformListData<const COLS: usize> {
render_item_fn: Box<dyn Fn(Range<usize>, &mut Window, &mut App) -> Vec<[AnyElement; COLS]>>,
element_id: ElementId,
row_count: usize,
}
enum TableContents<const COLS: usize> {
Vec(Vec<[AnyElement; COLS]>),
UniformList(UniformListData<COLS>),
}
impl<const COLS: usize> TableContents<COLS> {
fn rows_mut(&mut self) -> Option<&mut Vec<[AnyElement; COLS]>> {
match self {
TableContents::Vec(rows) => Some(rows),
TableContents::UniformList(_) => None,
}
}
fn len(&self) -> usize {
match self {
TableContents::Vec(rows) => rows.len(),
TableContents::UniformList(data) => data.row_count,
}
}
}
pub struct TableInteractionState {
pub focus_handle: FocusHandle,
pub scroll_handle: UniformListScrollHandle,
pub horizontal_scrollbar: ScrollbarProperties,
pub vertical_scrollbar: ScrollbarProperties,
}
impl TableInteractionState {
pub fn new(window: &mut Window, cx: &mut App) -> Entity<Self> {
cx.new(|cx| {
let focus_handle = cx.focus_handle();
cx.on_focus_out(&focus_handle, window, |this: &mut Self, _, window, cx| {
this.hide_scrollbars(window, cx);
})
.detach();
let scroll_handle = UniformListScrollHandle::new();
let vertical_scrollbar = ScrollbarProperties {
axis: Axis::Vertical,
state: ScrollbarState::new(scroll_handle.clone()).parent_entity(&cx.entity()),
show_scrollbar: false,
show_track: false,
auto_hide: false,
hide_task: None,
};
let horizontal_scrollbar = ScrollbarProperties {
axis: Axis::Horizontal,
state: ScrollbarState::new(scroll_handle.clone()).parent_entity(&cx.entity()),
show_scrollbar: false,
show_track: false,
auto_hide: false,
hide_task: None,
};
let mut this = Self {
focus_handle,
scroll_handle,
horizontal_scrollbar,
vertical_scrollbar,
};
this.update_scrollbar_visibility(cx);
this
})
}
fn update_scrollbar_visibility(&mut self, cx: &mut Context<Self>) {
let show_setting = EditorSettings::get_global(cx).scrollbar.show;
let scroll_handle = self.scroll_handle.0.borrow();
let autohide = |show: ShowScrollbar, cx: &mut Context<Self>| match show {
ShowScrollbar::Auto => true,
ShowScrollbar::System => cx
.try_global::<ScrollbarAutoHide>()
.map_or_else(|| cx.should_auto_hide_scrollbars(), |autohide| autohide.0),
ShowScrollbar::Always => false,
ShowScrollbar::Never => false,
};
let longest_item_width = scroll_handle.last_item_size.and_then(|size| {
(size.contents.width > size.item.width).then_some(size.contents.width)
});
// is there an item long enough that we should show a horizontal scrollbar?
let item_wider_than_container = if let Some(longest_item_width) = longest_item_width {
longest_item_width > px(scroll_handle.base_handle.bounds().size.width.0)
} else {
true
};
let show_scrollbar = match show_setting {
ShowScrollbar::Auto | ShowScrollbar::System | ShowScrollbar::Always => true,
ShowScrollbar::Never => false,
};
let show_vertical = show_scrollbar;
let show_horizontal = item_wider_than_container && show_scrollbar;
let show_horizontal_track =
show_horizontal && matches!(show_setting, ShowScrollbar::Always);
// TODO: we probably should hide the scroll track when the list doesn't need to scroll
let show_vertical_track = show_vertical && matches!(show_setting, ShowScrollbar::Always);
self.vertical_scrollbar = ScrollbarProperties {
axis: self.vertical_scrollbar.axis,
state: self.vertical_scrollbar.state.clone(),
show_scrollbar: show_vertical,
show_track: show_vertical_track,
auto_hide: autohide(show_setting, cx),
hide_task: None,
};
self.horizontal_scrollbar = ScrollbarProperties {
axis: self.horizontal_scrollbar.axis,
state: self.horizontal_scrollbar.state.clone(),
show_scrollbar: show_horizontal,
show_track: show_horizontal_track,
auto_hide: autohide(show_setting, cx),
hide_task: None,
};
cx.notify();
}
fn hide_scrollbars(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.horizontal_scrollbar.hide(window, cx);
self.vertical_scrollbar.hide(window, cx);
}
// fn listener(this: Entity<Self>, fn: F) ->
pub fn listener<E: ?Sized>(
this: &Entity<Self>,
f: impl Fn(&mut Self, &E, &mut Window, &mut Context<Self>) + 'static,
) -> impl Fn(&E, &mut Window, &mut App) + 'static {
let view = this.downgrade();
move |e: &E, window: &mut Window, cx: &mut App| {
view.update(cx, |view, cx| f(view, e, window, cx)).ok();
}
}
fn render_vertical_scrollbar_track(
this: &Entity<Self>,
parent: Div,
scroll_track_size: Pixels,
cx: &mut App,
) -> Div {
if !this.read(cx).vertical_scrollbar.show_track {
return parent;
}
let child = v_flex()
.h_full()
.flex_none()
.w(scroll_track_size)
.bg(cx.theme().colors().background)
.child(
div()
.size_full()
.flex_1()
.border_l_1()
.border_color(cx.theme().colors().border),
);
parent.child(child)
}
fn render_vertical_scrollbar(this: &Entity<Self>, parent: Div, cx: &mut App) -> Div {
if !this.read(cx).vertical_scrollbar.show_scrollbar {
return parent;
}
let child = div()
.id(("table-vertical-scrollbar", this.entity_id()))
.occlude()
.flex_none()
.h_full()
.cursor_default()
.absolute()
.right_0()
.top_0()
.bottom_0()
.w(px(12.))
.on_mouse_move(Self::listener(this, |_, _, _, cx| {
cx.notify();
cx.stop_propagation()
}))
.on_hover(|_, _, cx| {
cx.stop_propagation();
})
.on_mouse_up(
MouseButton::Left,
Self::listener(this, |this, _, window, cx| {
if !this.vertical_scrollbar.state.is_dragging()
&& !this.focus_handle.contains_focused(window, cx)
{
this.vertical_scrollbar.hide(window, cx);
cx.notify();
}
cx.stop_propagation();
}),
)
.on_any_mouse_down(|_, _, cx| {
cx.stop_propagation();
})
.on_scroll_wheel(Self::listener(&this, |_, _, _, cx| {
cx.notify();
}))
.children(Scrollbar::vertical(
this.read(cx).vertical_scrollbar.state.clone(),
));
parent.child(child)
}
/// Renders the horizontal scrollbar.
///
/// The right offset is used to determine how far to the right the
/// scrollbar should extend to, useful for ensuring it doesn't collide
/// with the vertical scrollbar when visible.
fn render_horizontal_scrollbar(
this: &Entity<Self>,
parent: Div,
right_offset: Pixels,
cx: &mut App,
) -> Div {
if !this.read(cx).horizontal_scrollbar.show_scrollbar {
return parent;
}
let child = div()
.id(("table-horizontal-scrollbar", this.entity_id()))
.occlude()
.flex_none()
.w_full()
.cursor_default()
.absolute()
.bottom_neg_px()
.left_0()
.right_0()
.pr(right_offset)
.on_mouse_move(Self::listener(this, |_, _, _, cx| {
cx.notify();
cx.stop_propagation()
}))
.on_hover(|_, _, cx| {
cx.stop_propagation();
})
.on_any_mouse_down(|_, _, cx| {
cx.stop_propagation();
})
.on_mouse_up(
MouseButton::Left,
Self::listener(this, |this, _, window, cx| {
if !this.horizontal_scrollbar.state.is_dragging()
&& !this.focus_handle.contains_focused(window, cx)
{
this.horizontal_scrollbar.hide(window, cx);
cx.notify();
}
cx.stop_propagation();
}),
)
.on_scroll_wheel(Self::listener(this, |_, _, _, cx| {
cx.notify();
}))
.children(Scrollbar::horizontal(
// percentage as f32..end_offset as f32,
this.read(cx).horizontal_scrollbar.state.clone(),
));
parent.child(child)
}
fn render_horizantal_scrollbar_track(
this: &Entity<Self>,
parent: Div,
scroll_track_size: Pixels,
cx: &mut App,
) -> Div {
if !this.read(cx).horizontal_scrollbar.show_track {
return parent;
}
let child = h_flex()
.w_full()
.h(scroll_track_size)
.flex_none()
.relative()
.child(
div()
.w_full()
.flex_1()
// for some reason the horizontal scrollbar is 1px
// taller than the vertical scrollbar??
.h(scroll_track_size - px(1.))
.bg(cx.theme().colors().background)
.border_t_1()
.border_color(cx.theme().colors().border),
)
.when(this.read(cx).vertical_scrollbar.show_track, |parent| {
parent
.child(
div()
.flex_none()
// -1px prevents a missing pixel between the two container borders
.w(scroll_track_size - px(1.))
.h_full(),
)
.child(
// HACK: Fill the missing 1px 🥲
div()
.absolute()
.right(scroll_track_size - px(1.))
.bottom(scroll_track_size - px(1.))
.size_px()
.bg(cx.theme().colors().border),
)
});
parent.child(child)
}
}
/// A table component
#[derive(RegisterComponent, IntoElement)]
pub struct Table<const COLS: usize = 3> {
striped: bool,
width: Option<Length>,
headers: Option<[AnyElement; COLS]>,
rows: TableContents<COLS>,
interaction_state: Option<WeakEntity<TableInteractionState>>,
selected_item_index: Option<usize>,
column_widths: Option<[Length; COLS]>,
on_click_row: Option<Rc<dyn Fn(usize, &mut Window, &mut App)>>,
}
impl<const COLS: usize> Table<COLS> {
/// number of headers provided.
pub fn new() -> Self {
Table {
striped: false,
width: None,
headers: None,
rows: TableContents::Vec(Vec::new()),
interaction_state: None,
selected_item_index: None,
column_widths: None,
on_click_row: None,
}
}
/// Enables uniform list rendering.
/// The provided function will be passed directly to the `uniform_list` element.
/// Therefore, if this method is called, any calls to [`Table::row`] before or after
/// this method is called will be ignored.
pub fn uniform_list(
mut self,
id: impl Into<ElementId>,
row_count: usize,
render_item_fn: impl Fn(Range<usize>, &mut Window, &mut App) -> Vec<[AnyElement; COLS]>
+ 'static,
) -> Self {
self.rows = TableContents::UniformList(UniformListData {
element_id: id.into(),
row_count: row_count,
render_item_fn: Box::new(render_item_fn),
});
self
}
/// Enables row striping.
pub fn striped(mut self) -> Self {
self.striped = true;
self
}
/// Sets the width of the table.
/// Will enable horizontal scrolling if [`Self::interactable`] is also called.
pub fn width(mut self, width: impl Into<Length>) -> Self {
self.width = Some(width.into());
self
}
/// Enables interaction (primarily scrolling) with the table.
///
/// Vertical scrolling will be enabled by default if the table is taller than its container.
///
/// Horizontal scrolling will only be enabled if [`Self::width`] is also called, otherwise
/// the list will always shrink the table columns to fit their contents I.e. If [`Self::uniform_list`]
/// is used without a width and with [`Self::interactable`], the [`ListHorizontalSizingBehavior`] will
/// be set to [`ListHorizontalSizingBehavior::FitList`].
pub fn interactable(mut self, interaction_state: &Entity<TableInteractionState>) -> Self {
self.interaction_state = Some(interaction_state.downgrade());
self
}
pub fn selected_item_index(mut self, selected_item_index: Option<usize>) -> Self {
self.selected_item_index = selected_item_index;
self
}
pub fn header(mut self, headers: [impl IntoElement; COLS]) -> Self {
self.headers = Some(headers.map(IntoElement::into_any_element));
self
}
pub fn row(mut self, items: [impl IntoElement; COLS]) -> Self {
if let Some(rows) = self.rows.rows_mut() {
rows.push(items.map(IntoElement::into_any_element));
}
self
}
pub fn column_widths(mut self, widths: [impl Into<Length>; COLS]) -> Self {
self.column_widths = Some(widths.map(Into::into));
self
}
pub fn on_click_row(
mut self,
callback: impl Fn(usize, &mut Window, &mut App) + 'static,
) -> Self {
self.on_click_row = Some(Rc::new(callback));
self
}
}
fn base_cell_style(width: Option<Length>, cx: &App) -> Div {
div()
.px_1p5()
.when_some(width, |this, width| this.w(width))
.when(width.is_none(), |this| this.flex_1())
.justify_start()
.text_ui(cx)
.whitespace_nowrap()
.text_ellipsis()
.overflow_hidden()
}
pub fn render_row<const COLS: usize>(
row_index: usize,
items: [impl IntoElement; COLS],
table_context: TableRenderContext<COLS>,
cx: &App,
) -> AnyElement {
let is_striped = table_context.striped;
let is_last = row_index == table_context.total_row_count - 1;
let bg = if row_index % 2 == 1 && is_striped {
Some(cx.theme().colors().text.opacity(0.05))
} else {
None
};
let column_widths = table_context
.column_widths
.map_or([None; COLS], |widths| widths.map(|width| Some(width)));
let is_selected = table_context.selected_item_index == Some(row_index);
let row = div()
.w_full()
.border_2()
.border_color(transparent_black())
.when(is_selected, |row| {
row.border_color(cx.theme().colors().panel_focused_border)
})
.child(
div()
.w_full()
.flex()
.flex_row()
.items_center()
.justify_between()
.px_1p5()
.py_1()
.when_some(bg, |row, bg| row.bg(bg))
.when(!is_striped, |row| {
row.border_b_1()
.border_color(transparent_black())
.when(!is_last, |row| row.border_color(cx.theme().colors().border))
})
.children(
items
.map(IntoElement::into_any_element)
.into_iter()
.zip(column_widths)
.map(|(cell, width)| base_cell_style(width, cx).child(cell)),
),
);
if let Some(on_click) = table_context.on_click_row {
row.id(("table-row", row_index))
.on_click(move |_, window, cx| on_click(row_index, window, cx))
.into_any_element()
} else {
row.into_any_element()
}
}
pub fn render_header<const COLS: usize>(
headers: [impl IntoElement; COLS],
table_context: TableRenderContext<COLS>,
cx: &mut App,
) -> impl IntoElement {
let column_widths = table_context
.column_widths
.map_or([None; COLS], |widths| widths.map(|width| Some(width)));
div()
.flex()
.flex_row()
.items_center()
.justify_between()
.w_full()
.p_2()
.border_b_1()
.border_color(cx.theme().colors().border)
.children(headers.into_iter().zip(column_widths).map(|(h, width)| {
base_cell_style(width, cx)
.font_weight(FontWeight::SEMIBOLD)
.child(h)
}))
}
#[derive(Clone)]
pub struct TableRenderContext<const COLS: usize> {
pub striped: bool,
pub total_row_count: usize,
pub selected_item_index: Option<usize>,
pub column_widths: Option<[Length; COLS]>,
pub on_click_row: Option<Rc<dyn Fn(usize, &mut Window, &mut App)>>,
}
impl<const COLS: usize> TableRenderContext<COLS> {
fn new(table: &Table<COLS>) -> Self {
Self {
striped: table.striped,
total_row_count: table.rows.len(),
column_widths: table.column_widths,
selected_item_index: table.selected_item_index.clone(),
on_click_row: table.on_click_row.clone(),
}
}
}
impl<const COLS: usize> RenderOnce for Table<COLS> {
fn render(mut self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let table_context = TableRenderContext::new(&self);
let interaction_state = self.interaction_state.and_then(|state| state.upgrade());
let scroll_track_size = px(16.);
let h_scroll_offset = if interaction_state
.as_ref()
.is_some_and(|state| state.read(cx).vertical_scrollbar.show_scrollbar)
{
// magic number
px(3.)
} else {
px(0.)
};
let width = self.width;
let table = div()
.when_some(width, |this, width| this.w(width))
.h_full()
.v_flex()
.when_some(self.headers.take(), |this, headers| {
this.child(render_header(headers, table_context.clone(), cx))
})
.child(
div()
.flex_grow()
.w_full()
.relative()
.overflow_hidden()
.map(|parent| match self.rows {
TableContents::Vec(items) => {
parent.children(items.into_iter().enumerate().map(|(index, row)| {
render_row(index, row, table_context.clone(), cx)
}))
}
TableContents::UniformList(uniform_list_data) => parent.child(
uniform_list(
uniform_list_data.element_id,
uniform_list_data.row_count,
{
let render_item_fn = uniform_list_data.render_item_fn;
move |range: Range<usize>, window, cx| {
let elements = render_item_fn(range.clone(), window, cx);
elements
.into_iter()
.zip(range)
.map(|(row, row_index)| {
render_row(
row_index,
row,
table_context.clone(),
cx,
)
})
.collect()
}
},
)
.size_full()
.flex_grow()
.with_sizing_behavior(ListSizingBehavior::Auto)
.with_horizontal_sizing_behavior(if width.is_some() {
ListHorizontalSizingBehavior::Unconstrained
} else {
ListHorizontalSizingBehavior::FitList
})
.when_some(
interaction_state.as_ref(),
|this, state| {
this.track_scroll(
state.read_with(cx, |s, _| s.scroll_handle.clone()),
)
},
),
),
})
.when_some(interaction_state.as_ref(), |this, interaction_state| {
this.map(|this| {
TableInteractionState::render_vertical_scrollbar_track(
interaction_state,
this,
scroll_track_size,
cx,
)
})
.map(|this| {
TableInteractionState::render_vertical_scrollbar(
interaction_state,
this,
cx,
)
})
}),
)
.when_some(
width.and(interaction_state.as_ref()),
|this, interaction_state| {
this.map(|this| {
TableInteractionState::render_horizantal_scrollbar_track(
interaction_state,
this,
scroll_track_size,
cx,
)
})
.map(|this| {
TableInteractionState::render_horizontal_scrollbar(
interaction_state,
this,
h_scroll_offset,
cx,
)
})
},
);
if let Some(interaction_state) = interaction_state.as_ref() {
table
.track_focus(&interaction_state.read(cx).focus_handle)
.id(("table", interaction_state.entity_id()))
.on_hover({
let interaction_state = interaction_state.downgrade();
move |hovered, window, cx| {
interaction_state
.update(cx, |interaction_state, cx| {
if *hovered {
interaction_state.horizontal_scrollbar.show(cx);
interaction_state.vertical_scrollbar.show(cx);
cx.notify();
} else if !interaction_state
.focus_handle
.contains_focused(window, cx)
{
interaction_state.hide_scrollbars(window, cx);
}
})
.ok(); // todo! handle error?
}
})
.into_any_element()
} else {
table.into_any_element()
}
}
}
// computed state related to how to render scrollbars
// one per axis
// on render we just read this off the keymap editor
// we update it when
// - settings change
// - on focus in, on focus out, on hover, etc.
#[derive(Debug)]
pub struct ScrollbarProperties {
axis: Axis,
show_scrollbar: bool,
show_track: bool,
auto_hide: bool,
hide_task: Option<Task<()>>,
state: ScrollbarState,
}
impl ScrollbarProperties {
// Shows the scrollbar and cancels any pending hide task
fn show(&mut self, cx: &mut Context<TableInteractionState>) {
if !self.auto_hide {
return;
}
self.show_scrollbar = true;
self.hide_task.take();
cx.notify();
}
fn hide(&mut self, window: &mut Window, cx: &mut Context<TableInteractionState>) {
const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
if !self.auto_hide {
return;
}
let axis = self.axis;
self.hide_task = Some(cx.spawn_in(window, async move |keymap_editor, cx| {
cx.background_executor()
.timer(SCROLLBAR_SHOW_INTERVAL)
.await;
if let Some(keymap_editor) = keymap_editor.upgrade() {
keymap_editor
.update(cx, |keymap_editor, cx| {
match axis {
Axis::Vertical => {
keymap_editor.vertical_scrollbar.show_scrollbar = false
}
Axis::Horizontal => {
keymap_editor.horizontal_scrollbar.show_scrollbar = false
}
}
cx.notify();
})
.ok();
}
}));
}
}
impl Component for Table<3> {
fn scope() -> ComponentScope {
ComponentScope::Layout
}
fn description() -> Option<&'static str> {
Some("A table component for displaying data in rows and columns with optional styling.")
}
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
Some(
v_flex()
.gap_6()
.children(vec![
example_group_with_title(
"Basic Tables",
vec![
single_example(
"Simple Table",
Table::new()
.width(px(400.))
.header(["Name", "Age", "City"])
.row(["Alice", "28", "New York"])
.row(["Bob", "32", "San Francisco"])
.row(["Charlie", "25", "London"])
.into_any_element(),
),
single_example(
"Two Column Table",
Table::new()
.header(["Category", "Value"])
.width(px(300.))
.row(["Revenue", "$100,000"])
.row(["Expenses", "$75,000"])
.row(["Profit", "$25,000"])
.into_any_element(),
),
],
),
example_group_with_title(
"Styled Tables",
vec![
single_example(
"Default",
Table::new()
.width(px(400.))
.header(["Product", "Price", "Stock"])
.row(["Laptop", "$999", "In Stock"])
.row(["Phone", "$599", "Low Stock"])
.row(["Tablet", "$399", "Out of Stock"])
.into_any_element(),
),
single_example(
"Striped",
Table::new()
.width(px(400.))
.striped()
.header(["Product", "Price", "Stock"])
.row(["Laptop", "$999", "In Stock"])
.row(["Phone", "$599", "Low Stock"])
.row(["Tablet", "$399", "Out of Stock"])
.row(["Headphones", "$199", "In Stock"])
.into_any_element(),
),
],
),
example_group_with_title(
"Mixed Content Table",
vec![single_example(
"Table with Elements",
Table::new()
.width(px(840.))
.header(["Status", "Name", "Priority", "Deadline", "Action"])
.row([
Indicator::dot().color(Color::Success).into_any_element(),
"Project A".into_any_element(),
"High".into_any_element(),
"2023-12-31".into_any_element(),
Button::new("view_a", "View")
.style(ButtonStyle::Filled)
.full_width()
.into_any_element(),
])
.row([
Indicator::dot().color(Color::Warning).into_any_element(),
"Project B".into_any_element(),
"Medium".into_any_element(),
"2024-03-15".into_any_element(),
Button::new("view_b", "View")
.style(ButtonStyle::Filled)
.full_width()
.into_any_element(),
])
.row([
Indicator::dot().color(Color::Error).into_any_element(),
"Project C".into_any_element(),
"Low".into_any_element(),
"2024-06-30".into_any_element(),
Button::new("view_c", "View")
.style(ButtonStyle::Filled)
.full_width()
.into_any_element(),
])
.into_any_element(),
)],
),
])
.into_any_element(),
)
}
}

View File

@@ -196,6 +196,7 @@ impl TerminalElement {
interactivity: Default::default(),
}
.track_focus(&focus)
.element
}
//Vec<Range<AlacPoint>> -> Clip out the parts of the ranges

View File

@@ -20,7 +20,6 @@ gpui.workspace = true
gpui_macros.workspace = true
icons.workspace = true
itertools.workspace = true
log.workspace = true
menu.workspace = true
serde.workspace = true
settings.workspace = true

View File

@@ -32,9 +32,9 @@ mod settings_group;
mod stack;
mod tab;
mod tab_bar;
mod table;
mod toggle;
mod tooltip;
mod uniform_table;
#[cfg(feature = "stories")]
mod stories;
@@ -73,9 +73,9 @@ pub use settings_group::*;
pub use stack::*;
pub use tab::*;
pub use tab_bar::*;
pub use table::*;
pub use toggle::*;
pub use tooltip::*;
pub use uniform_table::*;
#[cfg(feature = "stories")]
pub use stories::*;

View File

@@ -0,0 +1,271 @@
use crate::{Indicator, prelude::*};
use gpui::{AnyElement, FontWeight, IntoElement, Length, div};
/// A table component
#[derive(IntoElement, RegisterComponent)]
pub struct Table {
column_headers: Vec<SharedString>,
rows: Vec<Vec<TableCell>>,
column_count: usize,
striped: bool,
width: Length,
}
impl Table {
/// Create a new table with a column count equal to the
/// number of headers provided.
pub fn new(headers: Vec<impl Into<SharedString>>) -> Self {
let column_count = headers.len();
Table {
column_headers: headers.into_iter().map(Into::into).collect(),
column_count,
rows: Vec::new(),
striped: false,
width: Length::Auto,
}
}
/// Adds a row to the table.
///
/// The row must have the same number of columns as the table.
pub fn row(mut self, items: Vec<impl Into<TableCell>>) -> Self {
if items.len() == self.column_count {
self.rows.push(items.into_iter().map(Into::into).collect());
} else {
// TODO: Log error: Row length mismatch
}
self
}
/// Adds multiple rows to the table.
///
/// Each row must have the same number of columns as the table.
/// Rows that don't match the column count are ignored.
pub fn rows(mut self, rows: Vec<Vec<impl Into<TableCell>>>) -> Self {
for row in rows {
self = self.row(row);
}
self
}
fn base_cell_style(cx: &mut App) -> Div {
div()
.px_1p5()
.flex_1()
.justify_start()
.text_ui(cx)
.whitespace_nowrap()
.text_ellipsis()
.overflow_hidden()
}
/// Enables row striping.
pub fn striped(mut self) -> Self {
self.striped = true;
self
}
/// Sets the width of the table.
pub fn width(mut self, width: impl Into<Length>) -> Self {
self.width = width.into();
self
}
}
impl RenderOnce for Table {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
let header = div()
.flex()
.flex_row()
.items_center()
.justify_between()
.w_full()
.p_2()
.border_b_1()
.border_color(cx.theme().colors().border)
.children(self.column_headers.into_iter().map(|h| {
Self::base_cell_style(cx)
.font_weight(FontWeight::SEMIBOLD)
.child(h)
}));
let row_count = self.rows.len();
let rows = self.rows.into_iter().enumerate().map(|(ix, row)| {
let is_last = ix == row_count - 1;
let bg = if ix % 2 == 1 && self.striped {
Some(cx.theme().colors().text.opacity(0.05))
} else {
None
};
div()
.w_full()
.flex()
.flex_row()
.items_center()
.justify_between()
.px_1p5()
.py_1()
.when_some(bg, |row, bg| row.bg(bg))
.when(!is_last, |row| {
row.border_b_1().border_color(cx.theme().colors().border)
})
.children(row.into_iter().map(|cell| match cell {
TableCell::String(s) => Self::base_cell_style(cx).child(s),
TableCell::Element(e) => Self::base_cell_style(cx).child(e),
}))
});
div()
.w(self.width)
.overflow_hidden()
.child(header)
.children(rows)
}
}
/// Represents a cell in a table.
pub enum TableCell {
/// A cell containing a string value.
String(SharedString),
/// A cell containing a UI element.
Element(AnyElement),
}
/// Creates a `TableCell` containing a string value.
pub fn string_cell(s: impl Into<SharedString>) -> TableCell {
TableCell::String(s.into())
}
/// Creates a `TableCell` containing an element.
pub fn element_cell(e: impl Into<AnyElement>) -> TableCell {
TableCell::Element(e.into())
}
impl<E> From<E> for TableCell
where
E: Into<SharedString>,
{
fn from(e: E) -> Self {
TableCell::String(e.into())
}
}
impl Component for Table {
fn scope() -> ComponentScope {
ComponentScope::Layout
}
fn description() -> Option<&'static str> {
Some("A table component for displaying data in rows and columns with optional styling.")
}
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
Some(
v_flex()
.gap_6()
.children(vec![
example_group_with_title(
"Basic Tables",
vec![
single_example(
"Simple Table",
Table::new(vec!["Name", "Age", "City"])
.width(px(400.))
.row(vec!["Alice", "28", "New York"])
.row(vec!["Bob", "32", "San Francisco"])
.row(vec!["Charlie", "25", "London"])
.into_any_element(),
),
single_example(
"Two Column Table",
Table::new(vec!["Category", "Value"])
.width(px(300.))
.row(vec!["Revenue", "$100,000"])
.row(vec!["Expenses", "$75,000"])
.row(vec!["Profit", "$25,000"])
.into_any_element(),
),
],
),
example_group_with_title(
"Styled Tables",
vec![
single_example(
"Default",
Table::new(vec!["Product", "Price", "Stock"])
.width(px(400.))
.row(vec!["Laptop", "$999", "In Stock"])
.row(vec!["Phone", "$599", "Low Stock"])
.row(vec!["Tablet", "$399", "Out of Stock"])
.into_any_element(),
),
single_example(
"Striped",
Table::new(vec!["Product", "Price", "Stock"])
.width(px(400.))
.striped()
.row(vec!["Laptop", "$999", "In Stock"])
.row(vec!["Phone", "$599", "Low Stock"])
.row(vec!["Tablet", "$399", "Out of Stock"])
.row(vec!["Headphones", "$199", "In Stock"])
.into_any_element(),
),
],
),
example_group_with_title(
"Mixed Content Table",
vec![single_example(
"Table with Elements",
Table::new(vec!["Status", "Name", "Priority", "Deadline", "Action"])
.width(px(840.))
.row(vec![
element_cell(
Indicator::dot().color(Color::Success).into_any_element(),
),
string_cell("Project A"),
string_cell("High"),
string_cell("2023-12-31"),
element_cell(
Button::new("view_a", "View")
.style(ButtonStyle::Filled)
.full_width()
.into_any_element(),
),
])
.row(vec![
element_cell(
Indicator::dot().color(Color::Warning).into_any_element(),
),
string_cell("Project B"),
string_cell("Medium"),
string_cell("2024-03-15"),
element_cell(
Button::new("view_b", "View")
.style(ButtonStyle::Filled)
.full_width()
.into_any_element(),
),
])
.row(vec![
element_cell(
Indicator::dot().color(Color::Error).into_any_element(),
),
string_cell("Project C"),
string_cell("Low"),
string_cell("2024-06-30"),
element_cell(
Button::new("view_c", "View")
.style(ButtonStyle::Filled)
.full_width()
.into_any_element(),
),
])
.into_any_element(),
)],
),
])
.into_any_element(),
)
}
}

View File

@@ -1,50 +0,0 @@
use component::{Component, example_group_with_title, single_example};
use gpui::{AnyElement, App, IntoElement, ParentElement as _, Styled as _, Window};
use ui_macros::RegisterComponent;
use crate::v_flex;
#[derive(RegisterComponent)]
struct Table;
impl Component for Table {
fn name() -> &'static str {
"Uniform Table"
}
fn scope() -> component::ComponentScope {
component::ComponentScope::Layout
}
fn description() -> Option<&'static str> {
Some("A table with uniform rows")
}
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
let data = vec![
["Alice", "25", "New York"],
["Bob", "30", "Los Angeles"],
["Charlie", "35", "Chicago"],
["Sam", "27", "Detroit"],
];
Some(
v_flex()
.gap_6()
.children([example_group_with_title(
"Basic",
vec![single_example(
"Simple Table",
gpui::uniform_table("simple table", 4, move |range, _, _| {
data[range]
.iter()
.cloned()
.map(|arr| arr.map(IntoElement::into_any_element))
.collect()
})
.into_any_element(),
)],
)])
.into_any_element(),
)
}
}

View File

@@ -435,4 +435,37 @@ mod test {
// Mode::HelixNormal,
// );
// }
#[gpui::test]
async fn test_f_and_t(cx: &mut gpui::TestAppContext) {
let mut cx = VimTestContext::new(cx, true).await;
cx.set_state(
indoc! {"
The quˇick brown
fox jumps over
the lazy dog."},
Mode::HelixNormal,
);
cx.simulate_keystrokes("f z");
cx.assert_state(
indoc! {"
The qu«ick brown
fox jumps over
the lazˇ»y dog."},
Mode::HelixNormal,
);
cx.simulate_keystrokes("2 T r");
cx.assert_state(
indoc! {"
The quick br«ˇown
fox jumps over
the laz»y dog."},
Mode::HelixNormal,
);
}
}

View File

@@ -1532,90 +1532,6 @@ mod test {
}
}
#[gpui::test]
async fn test_f_and_t_multiline(cx: &mut gpui::TestAppContext) {
let mut cx = VimTestContext::new(cx, true).await;
cx.update_global(|store: &mut SettingsStore, cx| {
store.update_user_settings::<VimSettings>(cx, |s| {
s.use_multiline_find = Some(true);
});
});
cx.assert_binding(
"f l",
indoc! {"
ˇfunction print() {
console.log('ok')
}
"},
Mode::Normal,
indoc! {"
function print() {
consoˇle.log('ok')
}
"},
Mode::Normal,
);
cx.assert_binding(
"t l",
indoc! {"
ˇfunction print() {
console.log('ok')
}
"},
Mode::Normal,
indoc! {"
function print() {
consˇole.log('ok')
}
"},
Mode::Normal,
);
}
#[gpui::test]
async fn test_capital_f_and_capital_t_multiline(cx: &mut gpui::TestAppContext) {
let mut cx = VimTestContext::new(cx, true).await;
cx.update_global(|store: &mut SettingsStore, cx| {
store.update_user_settings::<VimSettings>(cx, |s| {
s.use_multiline_find = Some(true);
});
});
cx.assert_binding(
"shift-f p",
indoc! {"
function print() {
console.ˇlog('ok')
}
"},
Mode::Normal,
indoc! {"
function ˇprint() {
console.log('ok')
}
"},
Mode::Normal,
);
cx.assert_binding(
"shift-t p",
indoc! {"
function print() {
console.ˇlog('ok')
}
"},
Mode::Normal,
indoc! {"
function pˇrint() {
console.log('ok')
}
"},
Mode::Normal,
);
}
#[gpui::test]
async fn test_f_and_t_smartcase(cx: &mut gpui::TestAppContext) {
let mut cx = VimTestContext::new(cx, true).await;

View File

@@ -86,9 +86,11 @@ pub enum Operator {
},
FindForward {
before: bool,
multiline: bool,
},
FindBackward {
after: bool,
multiline: bool,
},
Sneak {
first_char: Option<char>,
@@ -994,12 +996,12 @@ impl Operator {
Operator::Replace => "r",
Operator::Digraph { .. } => "^K",
Operator::Literal { .. } => "^V",
Operator::FindForward { before: false } => "f",
Operator::FindForward { before: true } => "t",
Operator::FindForward { before: false, .. } => "f",
Operator::FindForward { before: true, .. } => "t",
Operator::Sneak { .. } => "s",
Operator::SneakBackward { .. } => "S",
Operator::FindBackward { after: false } => "F",
Operator::FindBackward { after: true } => "T",
Operator::FindBackward { after: false, .. } => "F",
Operator::FindBackward { after: true, .. } => "T",
Operator::AddSurrounds { .. } => "ys",
Operator::ChangeSurrounds { .. } => "cs",
Operator::DeleteSurrounds => "ds",

View File

@@ -72,6 +72,7 @@ struct PushObject {
#[serde(deny_unknown_fields)]
struct PushFindForward {
before: bool,
multiline: bool,
}
#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
@@ -79,6 +80,7 @@ struct PushFindForward {
#[serde(deny_unknown_fields)]
struct PushFindBackward {
after: bool,
multiline: bool,
}
#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
@@ -500,6 +502,7 @@ impl Vim {
vim.push_operator(
Operator::FindForward {
before: action.before,
multiline: action.multiline,
},
window,
cx,
@@ -510,6 +513,7 @@ impl Vim {
vim.push_operator(
Operator::FindBackward {
after: action.after,
multiline: action.multiline,
},
window,
cx,
@@ -1513,11 +1517,11 @@ impl Vim {
}
match self.active_operator() {
Some(Operator::FindForward { before }) => {
Some(Operator::FindForward { before, multiline }) => {
let find = Motion::FindForward {
before,
char: text.chars().next().unwrap(),
mode: if VimSettings::get_global(cx).use_multiline_find {
mode: if multiline {
FindRange::MultiLine
} else {
FindRange::SingleLine
@@ -1527,11 +1531,11 @@ impl Vim {
Vim::globals(cx).last_find = Some(find.clone());
self.motion(find, window, cx)
}
Some(Operator::FindBackward { after }) => {
Some(Operator::FindBackward { after, multiline }) => {
let find = Motion::FindBackward {
after,
char: text.chars().next().unwrap(),
mode: if VimSettings::get_global(cx).use_multiline_find {
mode: if multiline {
FindRange::MultiLine
} else {
FindRange::SingleLine
@@ -1729,7 +1733,6 @@ struct VimSettings {
pub default_mode: Mode,
pub toggle_relative_line_numbers: bool,
pub use_system_clipboard: UseSystemClipboard,
pub use_multiline_find: bool,
pub use_smartcase_find: bool,
pub custom_digraphs: HashMap<String, Arc<str>>,
pub highlight_on_yank_duration: u64,
@@ -1741,7 +1744,6 @@ struct VimSettingsContent {
pub default_mode: Option<ModeContent>,
pub toggle_relative_line_numbers: Option<bool>,
pub use_system_clipboard: Option<UseSystemClipboard>,
pub use_multiline_find: Option<bool>,
pub use_smartcase_find: Option<bool>,
pub custom_digraphs: Option<HashMap<String, Arc<str>>>,
pub highlight_on_yank_duration: Option<u64>,
@@ -1794,9 +1796,6 @@ impl Settings for VimSettings {
use_system_clipboard: settings
.use_system_clipboard
.ok_or_else(Self::missing_default)?,
use_multiline_find: settings
.use_multiline_find
.ok_or_else(Self::missing_default)?,
use_smartcase_find: settings
.use_smartcase_find
.ok_or_else(Self::missing_default)?,

View File

@@ -5,8 +5,8 @@ use theme::all_theme_colors;
use ui::{
AudioStatus, Avatar, AvatarAudioStatusIndicator, AvatarAvailabilityIndicator, ButtonLike,
Checkbox, CheckboxWithLabel, CollaboratorAvailability, ContentGroup, DecoratedIcon,
ElevationIndex, Facepile, IconDecoration, Indicator, KeybindingHint, Switch, TintColor,
Tooltip, prelude::*, utils::calculate_contrast_ratio,
ElevationIndex, Facepile, IconDecoration, Indicator, KeybindingHint, Switch, Table, TintColor,
Tooltip, element_cell, prelude::*, string_cell, utils::calculate_contrast_ratio,
};
use crate::{Item, Workspace};

View File

@@ -1419,8 +1419,6 @@ fn reload_keymaps(cx: &mut App, mut user_key_bindings: Vec<KeyBinding>) {
"New Window",
workspace::NewWindow,
)]);
// todo! nicer api here
settings_ui::keybindings::KeymapEventChannel::trigger_keymap_changed(cx);
}
pub fn load_default_keymap(cx: &mut App) {

View File

@@ -561,7 +561,7 @@ You can change the following settings to modify vim mode's behavior:
| ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- |
| default_mode | The default mode to start in. One of "normal", "insert", "replace", "visual", "visual_line", "visual_block", "helix_normal". | "normal" |
| use_system_clipboard | Determines how system clipboard is used:<br><ul><li>"always": use for all operations</li><li>"never": only use when explicitly specified</li><li>"on_yank": use for yank operations</li></ul> | "always" |
| use_multiline_find | If `true`, `f` and `t` motions extend across multiple lines. | false |
| use_multiline_find | deprecated |
| use_smartcase_find | If `true`, `f` and `t` motions are case-insensitive when the target letter is lowercase. | false |
| toggle_relative_line_numbers | If `true`, line numbers are relative in normal mode and absolute in insert mode, giving you the best of both options. | false |
| custom_digraphs | An object that allows you to add custom digraphs. Read below for an example. | {} |
@@ -586,7 +586,6 @@ Here's an example of these settings changed:
"vim": {
"default_mode": "insert",
"use_system_clipboard": "never",
"use_multiline_find": true,
"use_smartcase_find": true,
"toggle_relative_line_numbers": true,
"highlight_on_yank_duration": 50,