Compare commits

..

1 Commits

Author SHA1 Message Date
Jason Mancuso
26d754a168 Add FindReplace workflow suggestion toolkind 2024-08-30 11:47:27 -04:00
212 changed files with 11793 additions and 17021 deletions

View File

@@ -11,12 +11,8 @@
Alex Viscreanu <alexviscreanu@gmail.com>
Alex Viscreanu <alexviscreanu@gmail.com> <alexandru.viscreanu@kiwi.com>
Alexander Mankuta <alex@pointless.one>
Alexander Mankuta <alex@pointless.one> <alex+github@pointless.one>
amtoaer <amtoaer@gmail.com>
amtoaer <amtoaer@gmail.com> <amtoaer@outlook.com>
Andrei Zvonimir Crnković <andrei@0x7f.dev>
Andrei Zvonimir Crnković <andrei@0x7f.dev> <andreicek@0x7f.dev>
Antonio Scandurra <me@as-cii.com>
Antonio Scandurra <me@as-cii.com> <antonio@zed.dev>
Bennet Bo Fenner <bennet@zed.dev>
@@ -28,9 +24,8 @@ Conrad Irwin <conrad@zed.dev>
Conrad Irwin <conrad@zed.dev> <conrad.irwin@gmail.com>
Danilo Leal <danilo@zed.dev>
Danilo Leal <danilo@zed.dev> <67129314+danilo-leal@users.noreply.github.com>
Evren Sen <nervenes@icloud.com>
Evren Sen <nervenes@icloud.com> <146845123+evrensen467@users.noreply.github.com>
Evren Sen <nervenes@icloud.com> <146845123+evrsen@users.noreply.github.com>
Evren Sen <146845123+evrensen467@users.noreply.github.com>
Evren Sen <146845123+evrensen467@users.noreply.github.com> <146845123+evrsen@users.noreply.github.com>
Fernando Tagawa <tagawafernando@gmail.com>
Fernando Tagawa <tagawafernando@gmail.com> <fernando.tagawa.gamail.com@gmail.com>
Greg Morenz <greg-morenz@droid.cafe>
@@ -54,12 +49,8 @@ LoganDark <contact@logandark.mozmail.com> <git@logandark.mozmail.com>
LoganDark <contact@logandark.mozmail.com> <github@logandark.mozmail.com>
Marshall Bowers <elliott.codes@gmail.com>
Marshall Bowers <elliott.codes@gmail.com> <marshall@zed.dev>
Matt Fellenz <matt@felle.nz>
Matt Fellenz <matt@felle.nz> <matt+github@felle.nz>
Max Brunsfeld <maxbrunsfeld@gmail.com>
Max Brunsfeld <maxbrunsfeld@gmail.com> <max@zed.dev>
Max Linke <maxlinke88@gmail.com>
Max Linke <maxlinke88@gmail.com> <kain88-de@users.noreply.github.com>
Mikayla Maki <mikayla@zed.dev>
Mikayla Maki <mikayla@zed.dev> <mikayla.c.maki@gmail.com>
Mikayla Maki <mikayla@zed.dev> <mikayla.c.maki@icloud.com>
@@ -84,18 +75,10 @@ Richard Feldman <oss@rtfeldman.com>
Richard Feldman <oss@rtfeldman.com> <richard@zed.dev>
Robert Clover <git@clo4.net>
Robert Clover <git@clo4.net> <robert@clover.gdn>
Roy Williams <roy.williams.iii@gmail.com>
Roy Williams <roy.williams.iii@gmail.com> <roy@anthropic.com>
Sergey Onufrienko <sergey@onufrienko.com>
Thorben Kröger <dev@thorben.net>
Thorben Kröger <dev@thorben.net> <thorben.kroeger@hexagon.com>
Thorsten Ball <thorsten@zed.dev>
Thorsten Ball <thorsten@zed.dev> <me@thorstenball.com>
Thorsten Ball <thorsten@zed.dev> <mrnugget@gmail.com>
Tristan Hume <tris.hume@gmail.com>
Tristan Hume <tris.hume@gmail.com> <tristan@anthropic.com>
Uladzislau Kaminski <i@uladkaminski.com>
Uladzislau Kaminski <i@uladkaminski.com> <uladzislau_kaminski@epam.com>
Vitaly Slobodin <vitaliy.slobodin@gmail.com>
Vitaly Slobodin <vitaliy.slobodin@gmail.com> <vitaly_slobodin@fastmail.com>
WindSoilder <WindSoilder@outlook.com>

View File

@@ -38,7 +38,6 @@
}
}
},
"hard_tabs": false,
"formatter": "auto",
"remove_trailing_whitespace_on_save": true,
"ensure_final_newline_on_save": true

626
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,6 @@ members = [
"crates/assets",
"crates/assistant",
"crates/assistant_slash_command",
"crates/assistant_tool",
"crates/audio",
"crates/auto_update",
"crates/breadcrumbs",
@@ -182,7 +181,6 @@ anthropic = { path = "crates/anthropic" }
assets = { path = "crates/assets" }
assistant = { path = "crates/assistant" }
assistant_slash_command = { path = "crates/assistant_slash_command" }
assistant_tool = { path = "crates/assistant_tool" }
audio = { path = "crates/audio" }
auto_update = { path = "crates/auto_update" }
breadcrumbs = { path = "crates/breadcrumbs" }
@@ -317,7 +315,7 @@ async-dispatcher = "0.1"
async-fs = "1.6"
async-pipe = { git = "https://github.com/zed-industries/async-pipe-rs", rev = "82d00a04211cf4e1236029aa03e6b6ce2a74c553" }
async-recursion = "1.0.0"
async-tar = "0.5.0"
async-tar = "0.4.2"
async-trait = "0.1"
async-tungstenite = "0.23"
async-watch = "0.3.1"

View File

@@ -1,3 +1 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5 4L12 8L5 12V4Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M3.24182 2.32181C3.3919 2.23132 3.5784 2.22601 3.73338 2.30781L12.7334 7.05781C12.8974 7.14436 13 7.31457 13 7.5C13 7.68543 12.8974 7.85564 12.7334 7.94219L3.73338 12.6922C3.5784 12.774 3.3919 12.7687 3.24182 12.6782C3.09175 12.5877 3 12.4252 3 12.25V2.75C3 2.57476 3.09175 2.4123 3.24182 2.32181ZM4 3.57925V11.4207L11.4288 7.5L4 3.57925Z" fill="currentColor" fill-rule="evenodd" clip-rule="evenodd"></path></svg>

Before

Width:  |  Height:  |  Size: 218 B

After

Width:  |  Height:  |  Size: 518 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-pocket-knife"><path d="M3 2v1c0 1 2 1 2 2S3 6 3 7s2 1 2 2-2 1-2 2 2 1 2 2"/><path d="M18 6h.01"/><path d="M6 18h.01"/><path d="M20.83 8.83a4 4 0 0 0-5.66-5.66l-12 12a4 4 0 1 0 5.66 5.66Z"/><path d="M18 11.66V22a4 4 0 0 0 4-4V6"/></svg>

Before

Width:  |  Height:  |  Size: 438 B

View File

@@ -59,8 +59,11 @@
"ctrl-backspace": "editor::DeleteToPreviousWordStart",
"ctrl-delete": "editor::DeleteToNextWordEnd",
"shift-delete": "editor::Cut",
"ctrl-x": "editor::Cut",
"ctrl-insert": "editor::Copy",
"ctrl-c": "editor::Copy",
"shift-insert": "editor::Paste",
"ctrl-v": "editor::Paste",
"ctrl-y": "editor::Redo",
"ctrl-z": "editor::Undo",
"ctrl-shift-z": "editor::Redo",
@@ -109,15 +112,6 @@
"alt-g b": "editor::ToggleGitBlame"
}
},
{
// Separate block with same context so these display in context menus
"context": "Editor",
"bindings": {
"ctrl-x": "editor::Cut",
"ctrl-c": "editor::Copy",
"ctrl-v": "editor::Paste"
}
},
{
"context": "Editor && mode == full",
"bindings": {
@@ -521,7 +515,7 @@
}
},
{
"context": "OutlinePanel && not_editing",
"context": "OutlinePanel",
"bindings": {
"escape": "menu::Cancel",
"left": "outline_panel::CollapseSelectedEntry",
@@ -541,13 +535,18 @@
"right": "project_panel::ExpandSelectedEntry",
"ctrl-n": "project_panel::NewFile",
"alt-ctrl-n": "project_panel::NewDirectory",
"ctrl-x": "project_panel::Cut",
"ctrl-c": "project_panel::Copy",
"ctrl-insert": "project_panel::Copy",
"ctrl-v": "project_panel::Paste",
"shift-insert": "project_panel::Paste",
"ctrl-alt-c": "project_panel::CopyPath",
"alt-ctrl-shift-c": "project_panel::CopyRelativePath",
"f2": "project_panel::Rename",
"enter": "project_panel::Rename",
"backspace": ["project_panel::Trash", { "skip_prompt": false }],
"shift-delete": ["project_panel::Delete", { "skip_prompt": false }],
"delete": ["project_panel::Trash", { "skip_prompt": false }],
"ctrl-backspace": ["project_panel::Delete", { "skip_prompt": false }],
"ctrl-delete": ["project_panel::Delete", { "skip_prompt": false }],
"alt-ctrl-r": "project_panel::RevealInFileManager",
@@ -557,17 +556,6 @@
"escape": "menu::Cancel"
}
},
{
// Separate block with same context so these display in context menus
"context": "ProjectPanel",
"bindings": {
"f2": "project_panel::Rename",
"ctrl-c": "project_panel::Copy",
"ctrl-x": "project_panel::Cut",
"ctrl-v": "project_panel::Paste",
"delete": ["project_panel::Trash", { "skip_prompt": false }]
}
},
{
"context": "ProjectPanel && not_editing",
"bindings": {
@@ -623,7 +611,9 @@
"context": "Terminal",
"bindings": {
"ctrl-alt-space": "terminal::ShowCharacterPalette",
"ctrl-shift-c": "terminal::Copy",
"ctrl-insert": "terminal::Copy",
"ctrl-shift-v": "terminal::Paste",
"shift-insert": "terminal::Paste",
"ctrl-enter": "assistant::InlineAssist",
// Overrides for conflicting keybindings
@@ -647,13 +637,5 @@
"shift-home": "terminal::ScrollToTop",
"shift-end": "terminal::ScrollToBottom"
}
},
{
// Separate block with same context so these display in context menus
"context": "Terminal",
"bindings": {
"ctrl-shift-c": "terminal::Copy",
"ctrl-shift-v": "terminal::Paste"
}
}
]

View File

@@ -528,7 +528,7 @@
}
},
{
"context": "OutlinePanel && not_editing",
"context": "OutlinePanel",
"bindings": {
"escape": "menu::Cancel",
"left": "outline_panel::CollapseSelectedEntry",

View File

@@ -489,7 +489,7 @@
}
},
{
"context": "OutlinePanel && not_editing",
"context": "OutlinePanel",
"bindings": {
"j": "menu::SelectNext",
"k": "menu::SelectPrev",

View File

@@ -1,312 +1,302 @@
<task_description>
<workflow>
Guide the user through code changes in numbered steps that focus on individual functions, type definitions, etc.
Surround each distinct step in a <step></step> XML tag. The user will be performing these steps in a code editor
named Zed, which is where they will have entered this prompt and will be seeing the response.
# Code Change Workflow
<instructions>
- Use the language of the file for code fence blocks unless otherwise specified.
- Include a code or file action in each step.
- Only put code in separate steps if it should either go in separate files, or in different (non-contiguous) places in the same file.
- Provide error handling and input validation where appropriate.
- Adapt explanations based on the user's perceived level of expertise.
- Include comments in code examples to enhance understanding.
- Consider more complex programming scenarios when relevant.
- Avoid using the terminal to perform filesystem operations such as creating, deleting, renaming, and editing files. Assume the user prefers to use Zed for these operations, unless the prompt suggests that the user is expecting terminal commands.
- When creating a new file, Zed will automatically create any necessary directories in that file's path. So when a step will create a new file in Zed, don't mention needing to create the directory that it will go in.
- Assume the user is performing these actions in the Zed code editor, so avoid redundancies like "In your code editor, ..." or "In Zed, ..."
- Be concise without leaving out important information.
</instructions>
Your task is to guide the user through code changes using a series of steps. Each step should describe a high-level change, which can consist of multiple edits to distinct locations in the codebase.
Examples of user-assistant dialogs with step numbers and user follow-up requests:
## Output Example
Provide output as XML, with the following format:
<step>
Update the Person struct to store an age
<example language="rust">
<turn number="1">
<user>Create a Library struct with a vector of Book structs. Include methods to add books and find books by author. Use generics for the book ID.</user>
<assistant>Certainly! Let's create a Library struct with a vector of Book structs, including methods to add books and find books by author, using generics for the book ID:
<step>Define the Book and Library structs
```rust
struct Person {
// existing fields...
age: u8,
height: f32,
// existing fields...
use std::fmt::Display;
struct Book<T>
where
T: Display + Clone
{
id: T,
title: String,
author: String,
}
impl Person {
fn age(&self) -> u8 {
self.age
struct Library<T>
where
T: Display + Clone
{
books: Vec<Book<T>>,
}
```
</step>
<step>Implement methods for the Library struct
```rust
impl<T> Library<T>
where
T: Display + Clone
{
fn new() -> Self {
Library { books: Vec::new() }
}
fn add_book(&mut self, id: T, title: String, author: String) {
let book = Book { id, title, author };
self.books.push(book);
}
fn find_books_by_author(&self, author: &str) -> Vec<&Book<T>> {
self.books
.iter()
.filter(|book| book.author.to_lowercase() == author.to_lowercase())
.collect()
}
}
```
<edit>
<path>src/person.rs</path>
<operation>insert_before</operation>
<search>height: f32,</search>
<description>Add the age field</description>
</edit>
<edit>
<path>src/person.rs</path>
<operation>insert_after</operation>
<search>impl Person {</search>
<description>Add the age getter</description>
</edit>
</step>
## Output Format
This code defines:
1. A generic Book struct with an ID of type T, title, and author
2. A Library struct containing a vector of Books
3. Methods to create a new Library, add books, and find books by author
The use of generics allows flexibility in the type of ID used for books.</assistant>
</turn>
First, each `<step>` must contain a written description of the change that should be made. The description should begin with a high-level overview, and can contain markdown code blocks as well. The description should be self-contained and actionable.
After the description, each `<step>` must contain one or more `<edit>` tags, each of which refer to a specific range in a source file. Each `<edit>` tag must contain the following child tags:
### `<path>` (required)
This tag contains the path to the file that will be changed. It can be an existing path, or a path that should be created.
### `<search>` (optional)
This tag contains a search string to locate in the source file, e.g. `pub fn baz() {`. If not provided, the new content will be inserted at the top of the file. Make sure to produce a string that exists in the source file and that isn't ambiguous. When there's ambiguity, add more lines to the search to eliminate it.
### `<description>` (required)
This tag contains a single-line description of the edit that should be made at the given location.
### `<operation>` (required)
This tag indicates what type of change should be made, relative to the given location. It can be one of the following:
- `update`: Rewrites the specified string entirely based on the given description.
- `create`: Creates a new file with the given path based on the provided description.
- `insert_before`: Inserts new text based on the given description before the specified search string.
- `insert_after`: Inserts new text based on the given description after the specified search string.
- `delete`: Deletes the specified string from the containing file.
<guidelines>
- There's no need to describe *what* to do, just *where* to do it.
- Only reference locations that actually exist (unless you're creating a file).
- If creating a file, assume any subsequent updates are included at the time of creation.
- Don't create and then update a file. Always create new files in one hot.
- Prefer multiple edits to smaller regions, as opposed to one big edit to a larger region.
- Don't produce edits that intersect each other. In that case, merge them into a bigger edit.
- Never nest an edit with another edit. Never include CDATA. All edits are leaf nodes.
- Descriptions are required for all edits except delete.
- When generating multiple edits, ensure the descriptions are specific to each individual operation.
- Avoid referring to the search string in the description. Focus on the change to be made, not the location where it's made. That's implicit with the `search` string you provide.
- Don't generate multiple edits at the same location. Instead, combine them together in a single edit with a succinct combined description.
- Always ensure imports are added if you're referencing symbols that are not in scope.
</guidelines>
Here are some concrete examples.
<example>
<message role="user">
```rs src/shapes.rs
pub mod rectangle;
pub mod circle;
```
```rs src/shapes/rectangle.rs
pub struct Rectangle {
width: f64,
height: f64,
}
impl Rectangle {
pub fn new(width: f64, height: f64) -> Self {
Rectangle { width, height }
}
}
```
```rs src/shapes/circle.rs
pub struct Circle {
radius: f64,
}
impl Circle {
pub fn new(radius: f64) -> Self {
Circle { radius }
}
}
```
Update all shapes to store their origin as an (x, y) tuple and implement Display.
</message>
<message role="assistant">
We'll need to update both the rectangle and circle modules.
<step>
Add origin fields to both shape types.
<turn number="2">
<user>How can I use this Library struct in a main function?</user>
<assistant>Here's how you can use the Library struct in a main function:
<step>Create a main function to demonstrate Library usage
```rust
struct Rectangle {
// existing fields ...
origin: (f64, f64),
fn main() {
// Create a new Library with string IDs
let mut library = Library::new();
// Add some books
library.add_book(String::from("001"), String::from("The Rust Programming Language"), String::from("Steve Klabnik"));
library.add_book(String::from("002"), String::from("Programming Rust"), String::from("Jim Blandy"));
library.add_book(String::from("003"), String::from("Rust in Action"), String::from("Tim McNamara"));
// Find books by author
let author = "Steve Klabnik";
let books = library.find_books_by_author(author);
println!("Books by {}:", author);
for book in books {
println!("ID: {}, Title: {}", book.id, book.title);
}
}
```
```rust
struct Circle {
// existing fields ...
origin: (f64, f64),
}
```
<edit>
<path>src/shapes/rectangle.rs</path>
<operation>insert_before</operation>
<search>
width: f64,
height: f64,
</search>
<description>Add the origin field to Rectangle</description>
</edit>
<edit>
<path>src/shapes/circle.rs</path>
<operation>insert_before</operation>
<search>
radius: f64,
</search>
<description>Add the origin field to Circle</description>
</edit>
<step>
Update both shape's constructors to take an origin.
<edit>
<path>src/shapes/rectangle.rs</path>
<operation>update</operation>
<search>
fn new(width: f64, height: f64) -> Self {
Rectangle { width, height }
}
</search>
<description>Update the Rectangle new function to take an origin</description>
</edit>
<edit>
<path>src/shapes/circle.rs</path>
<operation>update</operation>
<search>
fn new(radius: f64) -> Self {
Circle { radius }
}
</search>
<description>Update the Circle new function to take an origin</description>
</edit>
</step>
<step>
Implement Display for both shapes
This main function demonstrates:
1. Creating a new Library instance
2. Adding books with string IDs
3. Finding and displaying books by a specific author</assistant>
</turn>
<edit>
<path>src/shapes/rectangle.rs</path>
<operation>insert_before</operation>
<search>
struct Rectangle {
</search>
<description>Add an import for the `std::fmt` module</description>
</edit>
<turn number="3">
<user>Can you add a method to remove a book by ID and handle potential errors?</user>
<assistant>Certainly! Let's add a method to remove a book by ID and handle potential errors:
<edit>
<path>src/shapes/rectangle.rs</path>
<operation>insert_after</operation>
<search>
Rectangle { width, height }
<step>Add a remove_book method to the Library struct
```rust
use std::fmt::Display;
use std::error::Error;
use std::fmt;
#[derive(Debug)]
struct BookNotFoundError;
impl Error for BookNotFoundError {}
impl fmt::Display for BookNotFoundError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Book not found")
}
}
</search>
<description>Add a Display implementation for Rectangle</description>
</edit>
<edit>
<path>src/shapes/circle.rs</path>
<operation>insert_before</operation>
<search>
struct Circle {
</search>
<description>Add an import for the `std::fmt` module</description>
</edit>
impl<T> Library<T>
where
T: Display + Clone + PartialEq
{
// ... (previous methods remain the same)
<edit>
<path>src/shapes/circle.rs</path>
<operation>insert_after</operation>
<search>
Circle { radius }
fn remove_book(&mut self, id: &T) -> Result<Book<T>, BookNotFoundError> {
if let Some(index) = self.books.iter().position(|book| &book.id == id) {
Ok(self.books.remove(index))
} else {
Err(BookNotFoundError)
}
}
}
</search>
<description>Add a Display implementation for Circle</description>
</edit>
```
</step>
</message>
<step>Update the main function to demonstrate book removal
```rust
fn main() {
// ... (previous code remains the same)
// Attempt to remove a book
let id_to_remove = String::from("002");
match library.remove_book(&id_to_remove) {
Ok(removed_book) => println!("Removed book: {} by {}", removed_book.title, removed_book.author),
Err(e) => println!("Error removing book: {}", e),
}
// Attempt to remove a non-existent book
let non_existent_id = String::from("999");
match library.remove_book(&non_existent_id) {
Ok(_) => println!("Book removed successfully"),
Err(e) => println!("Error removing book: {}", e),
}
// Display remaining books
println!("\nRemaining books:");
for book in &library.books {
println!("ID: {}, Title: {} by {}", book.id, book.title, book.author);
}
}
```
</step>
This update includes:
1. A new remove_book method that returns a Result type
2. A custom BookNotFoundError for error handling
3. Demonstration of successful book removal and error handling for non-existent books
4. Display of remaining books after removal attempts
The remove_book method uses the PartialEq trait to compare book IDs, allowing for flexible ID types while maintaining type safety.</assistant>
</turn>
</example>
<example number=2>
<message role="user">
<example language="go">
<turn number="1">
<user>Create a Person struct with name, age, and email fields, and a method to greet. Include error handling for age.</user>
<assistant>Certainly! Let's create a Person struct with the specified fields and a method to greet in Go:
```rs src/user.rs
struct User {
pub name: String,
age: u32,
email: String,
<step>Define the Person struct and NewPerson function with error handling
```go
type Person struct {
Name string
Age int
Email string
}
impl User {
fn new(name: String, age: u32, email: String) -> Self {
User { name, age, email }
func NewPerson(name string, age int, email string) (*Person, error) {
if age < 0 || age > 150 {
return nil, fmt.Errorf("invalid age: %d", age)
}
return &Person{Name: name, Age: age, Email: email}, nil
}
```
</step>
<step>Implement the Greet method for the Person struct
```go
func (p *Person) Greet() string {
return fmt.Sprintf("Hello, I'm %s, %d years old. You can reach me at %s.", p.Name, p.Age, p.Email)
}
```
</step>
This code defines a Person struct, a NewPerson function with age validation, and a Greet method.</assistant>
</turn>
<turn number="2">
<user>How can I use these in a main function?</user>
<assistant>Here's how to use the Person struct and its methods in a main function:
<step>Create a main function with error handling
```go
func main() {
// Create a new Person instance
person, err := NewPerson("Alice", 30, "alice@example.com")
if err != nil {
fmt.Println("Error creating person:", err)
return
}
pub fn print_info(&self) {
todo!()
// Use the Greet method
fmt.Println(person.Greet())
// Demonstrate error handling with invalid age
invalidPerson, err := NewPerson("Bob", 200, "bob@example.com")
if err != nil {
fmt.Println("Error creating person with invalid age:", err)
}
}
```
</step>
Let's print all the user information and delete the email field.
</message>
This main function demonstrates:
1. Creating a valid Person instance
2. Using the Greet method
3. Handling errors when creating a Person with an invalid age</assistant>
</turn>
<message role="assistant">
<step>
Update the 'print_info' method to print user information
<turn number="3">
<user>Can you add a method to update the email with validation?</user>
<assistant>Certainly! Let's add a method to update the email with validation:
```rust
impl User {
// ... other methods ...
<step>Add UpdateEmail method with validation
```go
import (
"fmt"
"regexp"
)
pub fn print_info(&self) {
println!("Name: {name}, Age: {age}", name = self.name, age = self.age);
func (p *Person) UpdateEmail(newEmail string) error {
// Simple email validation using regex
emailRegex := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
if !emailRegex.MatchString(newEmail) {
return fmt.Errorf("invalid email format: %s", newEmail)
}
p.Email = newEmail
return nil
}
```
</step>
<step>Update the main function to demonstrate email update
```go
func main() {
// ... (previous code remains the same)
// Demonstrate email update
err = person.UpdateEmail("alice.new@example.com")
if err != nil {
fmt.Println("Error updating email:", err)
} else {
fmt.Println("Updated greeting:", person.Greet())
}
// Demonstrate email update with invalid email
err = person.UpdateEmail("invalid-email")
if err != nil {
fmt.Println("Error updating email:", err)
}
}
```
<edit>
<path>src/user.rs</path>
<operation>update</operation>
<search>
pub fn print_info(&self) {
todo!()
}
</search>
<description>Print all the user information</description>
</edit>
</step>
<step>
Remove the 'email' field from the User struct
<edit>
<path>src/user.rs</path>
<operation>delete</operation>
<search>
email: String,
</search>
</edit>
<edit>
<path>src/user.rs</path>
<operation>update</operation>
<symbol>
fn new(name: String, age: u32, email: String) -> Self {
User { name, age, email }
}
</symbol>
<description>Remove email parameter from new method</description>
</edit>
</step>
</message>
This update includes:
1. An UpdateEmail method with email format validation
2. Demonstration of successful email update in the main function
3. Handling of invalid email update attempt</assistant>
</turn>
</example>
You should think step by step. When possible, produce smaller, coherent logical steps as opposed to one big step that combines lots of heterogeneous edits.
</task_description>
</workflow>

View File

@@ -227,8 +227,6 @@
// Whether to show diagnostic indicators in the scrollbar.
"diagnostics": true
},
// Enable middle-click paste on Linux.
"middle_click_paste": true,
// What to do when multibuffer is double clicked in some of its excerpts
// (parts of singleton buffers).
// May take 2 values:
@@ -603,7 +601,7 @@
// "shell": {
// "with_arguments": {
// "program": "/bin/bash",
// "args": ["--login"]
// "arguments": ["--login"]
// }
// }
"shell": "system",

View File

@@ -35,7 +35,7 @@
// "shell": {
// "with_arguments": {
// "program": "/bin/bash",
// "args": ["--login"]
// "arguments": ["--login"]
// }
// }
"shell": "system"

View File

@@ -1,5 +1,4 @@
{
"$schema": "https://zed.dev/schema/themes/v0.1.0.json",
"name": "Andromeda",
"author": "Zed Industries",
"themes": [

View File

@@ -1,5 +1,4 @@
{
"$schema": "https://zed.dev/schema/themes/v0.1.0.json",
"name": "Atelier",
"author": "Zed Industries",
"themes": [

View File

@@ -1,5 +1,4 @@
{
"$schema": "https://zed.dev/schema/themes/v0.1.0.json",
"name": "Ayu",
"author": "Zed Industries",
"themes": [

View File

@@ -1,5 +1,4 @@
{
"$schema": "https://zed.dev/schema/themes/v0.1.0.json",
"name": "Gruvbox",
"author": "Zed Industries",
"themes": [

View File

@@ -1,5 +1,4 @@
{
"$schema": "https://zed.dev/schema/themes/v0.1.0.json",
"name": "One",
"author": "Zed Industries",
"themes": [

View File

@@ -1,5 +1,4 @@
{
"$schema": "https://zed.dev/schema/themes/v0.1.0.json",
"name": "Rosé Pine",
"author": "Zed Industries",
"themes": [

View File

@@ -1,5 +1,4 @@
{
"$schema": "https://zed.dev/schema/themes/v0.1.0.json",
"name": "Sandcastle",
"author": "Zed Industries",
"themes": [

View File

@@ -1,5 +1,4 @@
{
"$schema": "https://zed.dev/schema/themes/v0.1.0.json",
"name": "Solarized",
"author": "Zed Industries",
"themes": [

View File

@@ -1,5 +1,4 @@
{
"$schema": "https://zed.dev/schema/themes/v0.1.0.json",
"name": "Summercamp",
"author": "Zed Industries",
"themes": [

View File

@@ -150,7 +150,7 @@ impl ActivityIndicator {
) -> impl Iterator<Item = PendingWork<'a>> {
self.project
.read(cx)
.language_server_statuses(cx)
.language_server_statuses()
.rev()
.filter_map(|(server_id, status)| {
if status.pending_work.is_empty() {

View File

@@ -1,8 +1,5 @@
mod supported_countries;
use std::time::Duration;
use std::{pin::Pin, str::FromStr};
use anyhow::{anyhow, Context, Result};
use chrono::{DateTime, Utc};
use futures::{io::BufReader, stream::BoxStream, AsyncBufReadExt, AsyncReadExt, Stream, StreamExt};
@@ -10,6 +7,8 @@ use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest};
use isahc::config::Configurable;
use isahc::http::{HeaderMap, HeaderValue};
use serde::{Deserialize, Serialize};
use std::time::Duration;
use std::{pin::Pin, str::FromStr};
use strum::{EnumIter, EnumString};
use thiserror::Error;
use util::ResultExt as _;
@@ -331,6 +330,28 @@ pub async fn stream_completion_with_rate_limit_info(
}
}
pub fn extract_text_from_events(
response: impl Stream<Item = Result<Event, AnthropicError>>,
) -> impl Stream<Item = Result<String, AnthropicError>> {
response.filter_map(|response| async move {
match response {
Ok(response) => match response {
Event::ContentBlockStart { content_block, .. } => match content_block {
Content::Text { text, .. } => Some(Ok(text)),
_ => None,
},
Event::ContentBlockDelta { delta, .. } => match delta {
ContentDelta::TextDelta { text } => Some(Ok(text)),
_ => None,
},
Event::Error { error } => Some(Err(AnthropicError::ApiError(error))),
_ => None,
},
Err(error) => Some(Err(error)),
}
})
}
pub async fn extract_tool_args_from_events(
tool_name: String,
mut events: Pin<Box<dyn Send + Stream<Item = Result<Event>>>>,
@@ -342,7 +363,7 @@ pub async fn extract_tool_args_from_events(
content_block,
} = event?
{
if let ResponseContent::ToolUse { name, .. } = content_block {
if let Content::ToolUse { name, .. } = content_block {
if name == tool_name {
tool_use_index = Some(index);
break;
@@ -390,7 +411,7 @@ pub struct CacheControl {
#[derive(Debug, Serialize, Deserialize)]
pub struct Message {
pub role: Role,
pub content: Vec<RequestContent>,
pub content: Vec<Content>,
}
#[derive(Debug, Serialize, Deserialize, Eq, PartialEq, Hash)]
@@ -402,7 +423,7 @@ pub enum Role {
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum RequestContent {
pub enum Content {
#[serde(rename = "text")]
Text {
text: String,
@@ -426,26 +447,12 @@ pub enum RequestContent {
#[serde(rename = "tool_result")]
ToolResult {
tool_use_id: String,
is_error: bool,
content: String,
#[serde(skip_serializing_if = "Option::is_none")]
cache_control: Option<CacheControl>,
},
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum ResponseContent {
#[serde(rename = "text")]
Text { text: String },
#[serde(rename = "tool_use")]
ToolUse {
id: String,
name: String,
input: serde_json::Value,
},
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ImageSource {
#[serde(rename = "type")]
@@ -518,7 +525,7 @@ pub struct Response {
#[serde(rename = "type")]
pub response_type: String,
pub role: Role,
pub content: Vec<ResponseContent>,
pub content: Vec<Content>,
pub model: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub stop_reason: Option<String>,
@@ -535,7 +542,7 @@ pub enum Event {
#[serde(rename = "content_block_start")]
ContentBlockStart {
index: usize,
content_block: ResponseContent,
content_block: Content,
},
#[serde(rename = "content_block_delta")]
ContentBlockDelta { index: usize, delta: ContentDelta },

View File

@@ -25,7 +25,6 @@ anthropic = { workspace = true, features = ["schemars"] }
anyhow.workspace = true
assets.workspace = true
assistant_slash_command.workspace = true
assistant_tool.workspace = true
async-watch.workspace = true
cargo_toml.workspace = true
chrono.workspace = true
@@ -74,7 +73,6 @@ settings.workspace = true
similar.workspace = true
smallvec.workspace = true
smol.workspace = true
strum.workspace = true
telemetry_events.workspace = true
terminal.workspace = true
terminal_view.workspace = true

View File

@@ -13,13 +13,11 @@ pub(crate) mod slash_command_picker;
pub mod slash_command_settings;
mod streaming_diff;
mod terminal_inline_assistant;
mod tools;
mod workflow;
pub use assistant_panel::{AssistantPanel, AssistantPanelEvent};
use assistant_settings::AssistantSettings;
use assistant_slash_command::SlashCommandRegistry;
use assistant_tool::ToolRegistry;
use client::{proto, Client};
use command_palette_hooks::CommandPaletteFilter;
pub use context::*;
@@ -65,7 +63,6 @@ actions!(
DeployHistory,
DeployPromptLibrary,
ConfirmCommand,
NewContext,
ToggleModelSelector,
]
);
@@ -212,11 +209,10 @@ pub fn init(
})
.detach();
context_store::init(&client.clone().into());
context_store::init(&client);
prompt_library::init(cx);
init_language_model_settings(cx);
assistant_slash_command::init(cx);
assistant_tool::init(cx);
assistant_panel::init(cx);
context_servers::init(cx);
@@ -231,7 +227,6 @@ pub fn init(
.map(Arc::new)
.unwrap_or_else(|| Arc::new(prompts::PromptBuilder::new(None).unwrap()));
register_slash_commands(Some(prompt_builder.clone()), cx);
register_tools(cx);
inline_assistant::init(
fs.clone(),
prompt_builder.clone(),
@@ -367,7 +362,7 @@ fn register_slash_commands(prompt_builder: Option<Arc<PromptBuilder>>, cx: &mut
if let Some(prompt_builder) = prompt_builder {
slash_command_registry.register_command(
workflow_command::WorkflowSlashCommand::new(prompt_builder.clone()),
workflow_command::WorkflowSlashCommand::new(prompt_builder),
true,
);
}
@@ -405,11 +400,6 @@ fn update_slash_commands_from_settings(cx: &mut AppContext) {
}
}
fn register_tools(cx: &mut AppContext) {
let tool_registry = ToolRegistry::global(cx);
tool_registry.register_tool(tools::now_tool::NowTool);
}
pub fn humanize_token_count(count: usize) -> String {
match count {
0..=999 => count.to_string(),

File diff suppressed because it is too large Load Diff

View File

@@ -1,17 +1,11 @@
use std::sync::Arc;
use ::open_ai::Model as OpenAiModel;
use anthropic::Model as AnthropicModel;
use fs::Fs;
use gpui::{AppContext, Pixels};
use language_model::provider::open_ai;
use language_model::settings::{
AnthropicSettingsContent, AnthropicSettingsContentV1, OllamaSettingsContent,
OpenAiSettingsContent, OpenAiSettingsContentV1, VersionedAnthropicSettingsContent,
VersionedOpenAiSettingsContent,
};
use language_model::{settings::AllLanguageModelSettings, CloudModel, LanguageModel};
use ollama::Model as OllamaModel;
use open_ai::Model as OpenAiModel;
use schemars::{schema::Schema, JsonSchema};
use serde::{Deserialize, Serialize};
use settings::{update_settings_file, Settings, SettingsSources};
@@ -115,15 +109,16 @@ impl AssistantSettingsContent {
cx,
move |content, _| {
if content.anthropic.is_none() {
content.anthropic = Some(AnthropicSettingsContent::Versioned(
VersionedAnthropicSettingsContent::V1(
AnthropicSettingsContentV1 {
api_url,
low_speed_timeout_in_seconds,
available_models: None,
},
),
));
content.anthropic =
Some(language_model::settings::AnthropicSettingsContent::Versioned(
language_model::settings::VersionedAnthropicSettingsContent::V1(
language_model::settings::AnthropicSettingsContentV1 {
api_url,
low_speed_timeout_in_seconds,
available_models: None
}
)
));
}
},
),
@@ -136,11 +131,11 @@ impl AssistantSettingsContent {
cx,
move |content, _| {
if content.ollama.is_none() {
content.ollama = Some(OllamaSettingsContent {
api_url,
low_speed_timeout_in_seconds,
available_models: None,
});
content.ollama =
Some(language_model::settings::OllamaSettingsContent {
api_url,
low_speed_timeout_in_seconds,
});
}
},
),
@@ -158,28 +153,23 @@ impl AssistantSettingsContent {
models
.into_iter()
.filter_map(|model| match model {
OpenAiModel::Custom {
name,
max_tokens,
max_output_tokens,
} => Some(open_ai::AvailableModel {
name,
max_tokens,
max_output_tokens,
}),
open_ai::Model::Custom { name, max_tokens,max_output_tokens } => {
Some(language_model::provider::open_ai::AvailableModel { name, max_tokens,max_output_tokens })
}
_ => None,
})
.collect::<Vec<_>>()
});
content.openai = Some(OpenAiSettingsContent::Versioned(
VersionedOpenAiSettingsContent::V1(
OpenAiSettingsContentV1 {
api_url,
low_speed_timeout_in_seconds,
available_models,
},
),
));
content.openai =
Some(language_model::settings::OpenAiSettingsContent::Versioned(
language_model::settings::VersionedOpenAiSettingsContent::V1(
language_model::settings::OpenAiSettingsContentV1 {
api_url,
low_speed_timeout_in_seconds,
available_models
}
)
));
}
},
),
@@ -305,7 +295,7 @@ impl AssistantSettingsContent {
_ => (None, None),
};
settings.provider = Some(AssistantProviderContentV1::Ollama {
default_model: Some(ollama::Model::new(&model, None, None)),
default_model: Some(ollama::Model::new(&model)),
api_url,
low_speed_timeout_in_seconds,
});
@@ -326,7 +316,7 @@ impl AssistantSettingsContent {
_ => (None, None, None),
};
settings.provider = Some(AssistantProviderContentV1::OpenAi {
default_model: OpenAiModel::from_id(&model).ok(),
default_model: open_ai::Model::from_id(&model).ok(),
api_url,
low_speed_timeout_in_seconds,
available_models,
@@ -339,7 +329,7 @@ impl AssistantSettingsContent {
}
},
AssistantSettingsContent::Legacy(settings) => {
if let Ok(model) = OpenAiModel::from_id(&language_model.id().0) {
if let Ok(model) = open_ai::Model::from_id(&language_model.id().0) {
settings.default_open_ai_model = Some(model);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,6 @@
use super::{MessageCacheMetadata, WorkflowStepEdit};
use crate::{
assistant_panel, prompt_library, slash_command::file_command, CacheStatus, Context,
ContextEvent, ContextId, ContextOperation, MessageId, MessageStatus, PromptBuilder,
WorkflowStepEditKind,
assistant_panel, prompt_library, slash_command::file_command, workflow::tool, CacheStatus,
Context, ContextEvent, ContextId, ContextOperation, MessageId, MessageStatus, PromptBuilder,
};
use anyhow::Result;
use assistant_slash_command::{
@@ -10,13 +8,15 @@ use assistant_slash_command::{
SlashCommandRegistry,
};
use collections::HashSet;
use fs::FakeFs;
use fs::{FakeFs, Fs as _};
use gpui::{AppContext, Model, SharedString, Task, TestAppContext, WeakView};
use indoc::indoc;
use language::{Buffer, LanguageRegistry, LspAdapterDelegate};
use language_model::{LanguageModelCacheConfiguration, LanguageModelRegistry, Role};
use parking_lot::Mutex;
use project::Project;
use rand::prelude::*;
use rope::Point;
use serde_json::json;
use settings::SettingsStore;
use std::{
@@ -27,15 +27,14 @@ use std::{
rc::Rc,
sync::{atomic::AtomicBool, Arc},
};
use text::{network::Network, OffsetRangeExt as _, ReplicaId};
use text::{network::Network, OffsetRangeExt as _, ReplicaId, ToPoint as _};
use ui::{Context as _, WindowContext};
use unindent::Unindent;
use util::{
test::{generate_marked_text, marked_text_ranges},
RandomCharIter,
};
use util::{test::marked_text_ranges, RandomCharIter};
use workspace::Workspace;
use super::MessageCacheMetadata;
#[gpui::test]
fn test_inserting_and_removing_messages(cx: &mut AppContext) {
let settings_store = SettingsStore::test(cx);
@@ -480,12 +479,28 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
cx.update(prompt_library::init);
let settings_store = cx.update(SettingsStore::test);
cx.set_global(settings_store);
cx.update(language::init);
cx.update(Project::init_settings);
let fs = FakeFs::new(cx.executor());
fs.as_fake()
.insert_tree(
"/root",
json!({
"hello.rs": r#"
fn hello() {
println!("Hello, World!");
}
"#.unindent()
}),
)
.await;
let project = Project::test(fs, [Path::new("/root")], cx).await;
cx.update(LanguageModelRegistry::test);
let model = cx.read(|cx| {
LanguageModelRegistry::read_global(cx)
.active_model()
.unwrap()
});
cx.update(assistant_panel::init);
let registry = Arc::new(LanguageRegistry::test(cx.executor()));
@@ -500,382 +515,151 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
cx,
)
});
let buffer = context.read_with(cx, |context, _| context.buffer.clone());
// Insert an assistant message to simulate a response.
let assistant_message_id = context.update(cx, |context, cx| {
let user_message_id = context.messages(cx).next().unwrap().id;
context
.insert_message_after(user_message_id, Role::Assistant, MessageStatus::Done, cx)
.unwrap()
.id
// Simulate user input
let user_message = indoc! {r#"
Please add unnecessary complexity to this code:
```hello.rs
fn main() {
println!("Hello, World!");
}
```
"#};
buffer.update(cx, |buffer, cx| {
buffer.edit([(0..0, user_message)], None, cx);
});
// No edit tags
edit(
&context,
"
«one
two
»",
cx,
);
expect_steps(
&context,
"
one
two
",
&[],
cx,
);
// Partial edit step tag is added
edit(
&context,
"
one
two
«
<step»",
cx,
);
expect_steps(
&context,
"
one
two
<step",
&[],
cx,
);
// The rest of the step tag is added. The unclosed
// step is treated as incomplete.
edit(
&context,
"
one
two
<step«>
Add a second function
```rust
fn two() {}
```
<edit>»",
cx,
);
expect_steps(
&context,
"
one
two
«<step>
Add a second function
```rust
fn two() {}
```
<edit>»",
&[&[]],
cx,
);
// The full suggestion is added
edit(
&context,
"
one
two
// Simulate LLM response with edit steps
let llm_response = indoc! {r#"
Sure, I can help you with that. Here's a step-by-step process:
<step>
Add a second function
First, let's extract the greeting into a separate function:
```rust
fn two() {}
```
fn greet() {
println!("Hello, World!");
}
<edit>«
<path>src/lib.rs</path>
<operation>insert_after</operation>
<search>fn one</search>
<description>add a `two` function</description>
</edit>
fn main() {
greet();
}
```
</step>
also,»",
cx,
);
expect_steps(
&context,
"
one
two
«<step>
Add a second function
```rust
fn two() {}
```
<edit>
<path>src/lib.rs</path>
<operation>insert_after</operation>
<search>fn one</search>
<description>add a `two` function</description>
</edit>
</step>»
also,",
&[&[WorkflowStepEdit {
path: "src/lib.rs".into(),
kind: WorkflowStepEditKind::InsertAfter {
search: "fn one".into(),
description: "add a `two` function".into(),
},
}]],
cx,
);
// The step is manually edited.
edit(
&context,
"
one
two
<step>
Add a second function
Now, let's make the greeting customizable:
```rust
fn two() {}
```
fn greet(name: &str) {
println!("Hello, {}!", name);
}
<edit>
<path>src/lib.rs</path>
<operation>insert_after</operation>
<search>«fn zero»</search>
<description>add a `two` function</description>
</edit>
fn main() {
greet("World");
}
```
</step>
also,",
cx,
);
expect_steps(
&context,
"
These changes make the code more modular and flexible.
"#};
one
two
// Simulate the assist method to trigger the LLM response
context.update(cx, |context, cx| context.assist(cx));
cx.run_until_parked();
«<step>
Add a second function
```rust
fn two() {}
```
<edit>
<path>src/lib.rs</path>
<operation>insert_after</operation>
<search>fn zero</search>
<description>add a `two` function</description>
</edit>
</step>»
also,",
&[&[WorkflowStepEdit {
path: "src/lib.rs".into(),
kind: WorkflowStepEditKind::InsertAfter {
search: "fn zero".into(),
description: "add a `two` function".into(),
},
}]],
cx,
);
// When setting the message role to User, the steps are cleared.
context.update(cx, |context, cx| {
context.cycle_message_roles(HashSet::from_iter([assistant_message_id]), cx);
context.cycle_message_roles(HashSet::from_iter([assistant_message_id]), cx);
// Retrieve the assistant response message's start from the context
let response_start_row = context.read_with(cx, |context, cx| {
let buffer = context.buffer.read(cx);
context.message_anchors[1].start.to_point(buffer).row
});
expect_steps(
&context,
"
one
two
// Simulate the LLM completion
model
.as_fake()
.stream_last_completion_response(llm_response.to_string());
model.as_fake().end_last_completion_stream();
<step>
Add a second function
// Wait for the completion to be processed
cx.run_until_parked();
```rust
fn two() {}
```
<edit>
<path>src/lib.rs</path>
<operation>insert_after</operation>
<search>fn zero</search>
<description>add a `two` function</description>
</edit>
</step>
also,",
&[],
cx,
);
// When setting the message role back to Assistant, the steps are reparsed.
context.update(cx, |context, cx| {
context.cycle_message_roles(HashSet::from_iter([assistant_message_id]), cx);
// Verify that the edit steps were parsed correctly
context.read_with(cx, |context, cx| {
assert_eq!(
workflow_steps(context, cx),
vec![
(
Point::new(response_start_row + 2, 0)..Point::new(response_start_row + 12, 3),
WorkflowStepTestStatus::Pending
),
(
Point::new(response_start_row + 14, 0)..Point::new(response_start_row + 24, 3),
WorkflowStepTestStatus::Pending
),
]
);
});
expect_steps(
&context,
"
one
two
«<step>
Add a second function
```rust
fn two() {}
```
<edit>
<path>src/lib.rs</path>
<operation>insert_after</operation>
<search>fn zero</search>
<description>add a `two` function</description>
</edit>
</step>»
also,",
&[&[WorkflowStepEdit {
path: "src/lib.rs".into(),
kind: WorkflowStepEditKind::InsertAfter {
search: "fn zero".into(),
description: "add a `two` function".into(),
},
}]],
cx,
);
// Ensure steps are re-parsed when deserializing.
let serialized_context = context.read_with(cx, |context, cx| context.serialize(cx));
let deserialized_context = cx.new_model(|cx| {
Context::deserialize(
serialized_context,
Default::default(),
registry.clone(),
prompt_builder.clone(),
None,
None,
cx,
)
});
expect_steps(
&deserialized_context,
"
one
two
«<step>
Add a second function
```rust
fn two() {}
```
<edit>
<path>src/lib.rs</path>
<operation>insert_after</operation>
<search>fn zero</search>
<description>add a `two` function</description>
</edit>
</step>»
also,",
&[&[WorkflowStepEdit {
path: "src/lib.rs".into(),
kind: WorkflowStepEditKind::InsertAfter {
search: "fn zero".into(),
description: "add a `two` function".into(),
},
}]],
cx,
);
fn edit(context: &Model<Context>, new_text_marked_with_edits: &str, cx: &mut TestAppContext) {
context.update(cx, |context, cx| {
context.buffer.update(cx, |buffer, cx| {
buffer.edit_via_marked_text(&new_text_marked_with_edits.unindent(), None, cx);
});
model
.as_fake()
.respond_to_last_tool_use(tool::WorkflowStepResolutionTool {
step_title: "Title".into(),
suggestions: vec![tool::WorkflowSuggestionTool {
path: "/root/hello.rs".into(),
// Simulate a symbol name that's slightly different than our outline query
kind: tool::WorkflowSuggestionToolKind::Update {
symbol: "fn main()".into(),
description: "Extract a greeting function".into(),
},
}],
});
cx.executor().run_until_parked();
// Wait for tool use to be processed.
cx.run_until_parked();
// Verify that the first edit step is not pending anymore.
context.read_with(cx, |context, cx| {
assert_eq!(
workflow_steps(context, cx),
vec![
(
Point::new(response_start_row + 2, 0)..Point::new(response_start_row + 12, 3),
WorkflowStepTestStatus::Resolved
),
(
Point::new(response_start_row + 14, 0)..Point::new(response_start_row + 24, 3),
WorkflowStepTestStatus::Pending
),
]
);
});
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
enum WorkflowStepTestStatus {
Pending,
Resolved,
Error,
}
fn expect_steps(
context: &Model<Context>,
expected_marked_text: &str,
expected_suggestions: &[&[WorkflowStepEdit]],
cx: &mut TestAppContext,
) {
context.update(cx, |context, cx| {
let expected_marked_text = expected_marked_text.unindent();
let (expected_text, expected_ranges) = marked_text_ranges(&expected_marked_text, false);
context.buffer.read_with(cx, |buffer, _| {
assert_eq!(buffer.text(), expected_text);
let ranges = context
.workflow_steps
.iter()
.map(|entry| entry.range.to_offset(buffer))
.collect::<Vec<_>>();
let marked = generate_marked_text(&expected_text, &ranges, false);
assert_eq!(
marked,
expected_marked_text,
"unexpected suggestion ranges. actual: {ranges:?}, expected: {expected_ranges:?}"
);
let suggestions = context
.workflow_steps
.iter()
.map(|step| {
step.edits
.iter()
.map(|edit| {
let edit = edit.as_ref().unwrap();
WorkflowStepEdit {
path: edit.path.clone(),
kind: edit.kind.clone(),
}
})
.collect::<Vec<_>>()
})
.collect::<Vec<_>>();
assert_eq!(suggestions, expected_suggestions);
});
});
fn workflow_steps(
context: &Context,
cx: &AppContext,
) -> Vec<(Range<Point>, WorkflowStepTestStatus)> {
context
.workflow_steps
.iter()
.map(|step| {
let buffer = context.buffer.read(cx);
let status = match &step.step.read(cx).resolution {
None => WorkflowStepTestStatus::Pending,
Some(Ok(_)) => WorkflowStepTestStatus::Resolved,
Some(Err(_)) => WorkflowStepTestStatus::Error,
};
(step.range.to_point(buffer), status)
})
.collect()
}
}

View File

@@ -2,7 +2,6 @@ use crate::{
prompts::PromptBuilder, Context, ContextEvent, ContextId, ContextOperation, ContextVersion,
SavedContext, SavedContextMetadata,
};
use ::proto::AnyProtoClient;
use anyhow::{anyhow, Context as _, Result};
use client::{proto, telemetry::Telemetry, Client, TypedEnvelope};
use clock::ReplicaId;
@@ -26,7 +25,7 @@ use std::{
};
use util::{ResultExt, TryFutureExt};
pub fn init(client: &AnyProtoClient) {
pub fn init(client: &Arc<Client>) {
client.add_model_message_handler(ContextStore::handle_advertise_contexts);
client.add_model_request_handler(ContextStore::handle_open_context);
client.add_model_request_handler(ContextStore::handle_create_context);

View File

@@ -1200,11 +1200,9 @@ impl InlineAssistStatus {
pub(crate) fn is_pending(&self) -> bool {
matches!(self, Self::Pending)
}
pub(crate) fn is_confirmed(&self) -> bool {
matches!(self, Self::Confirmed)
}
pub(crate) fn is_done(&self) -> bool {
matches!(self, Self::Done)
}
@@ -1794,16 +1792,13 @@ impl PromptEditor {
CodegenStatus::Pending => {
cx.emit(PromptEditorEvent::DismissRequested);
}
CodegenStatus::Done => {
CodegenStatus::Done | CodegenStatus::Error(_) => {
if self.edited_since_done {
cx.emit(PromptEditorEvent::StartRequested);
} else {
cx.emit(PromptEditorEvent::ConfirmRequested);
}
}
CodegenStatus::Error(_) => {
cx.emit(PromptEditorEvent::StartRequested);
}
}
}
@@ -2344,7 +2339,7 @@ impl Codegen {
self.build_request(user_prompt, assistant_panel_context, edit_range.clone(), cx)?;
let chunks =
cx.spawn(|_, cx| async move { model.stream_completion_text(request, &cx).await });
cx.spawn(|_, cx| async move { model.stream_completion(request, &cx).await });
async move { Ok(chunks.await?.boxed()) }.boxed_local()
};
self.handle_stream(telemetry_id, edit_range, chunks, cx);
@@ -2416,7 +2411,6 @@ impl Codegen {
Ok(LanguageModelRequest {
messages,
tools: Vec::new(),
stop: vec!["|END|>".to_string()],
temperature,
})

View File

@@ -794,7 +794,6 @@ impl PromptLibrary {
content: vec![body.to_string().into()],
cache: false,
}],
tools: Vec::new(),
stop: Vec::new(),
temperature: 1.,
},

View File

@@ -297,4 +297,11 @@ impl PromptBuilder {
pub fn generate_workflow_prompt(&self) -> Result<String, RenderError> {
self.handlebars.lock().render("edit_workflow", &())
}
pub fn generate_step_resolution_prompt(
&self,
context: &StepResolutionContext,
) -> Result<String, RenderError> {
self.handlebars.lock().render("step_resolution", context)
}
}

View File

@@ -1,7 +1,6 @@
use anyhow::{anyhow, Result};
use assistant_slash_command::{
AfterCompletion, ArgumentCompletion, SlashCommand, SlashCommandOutput,
SlashCommandOutputSection,
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
};
use collections::HashMap;
use context_servers::{
@@ -9,10 +8,9 @@ use context_servers::{
protocol::PromptInfo,
};
use gpui::{Task, WeakView, WindowContext};
use language::{CodeLabel, LspAdapterDelegate};
use language::LspAdapterDelegate;
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use text::LineEnding;
use ui::{IconName, SharedString};
use workspace::Workspace;
@@ -52,57 +50,12 @@ impl SlashCommand for ContextServerSlashCommand {
fn complete_argument(
self: Arc<Self>,
arguments: &[String],
_arguments: &[String],
_cancel: Arc<AtomicBool>,
_workspace: Option<WeakView<Workspace>>,
cx: &mut WindowContext,
_cx: &mut WindowContext,
) -> Task<Result<Vec<ArgumentCompletion>>> {
let server_id = self.server_id.clone();
let prompt_name = self.prompt.name.clone();
let manager = ContextServerManager::global(cx);
let manager = manager.read(cx);
let (arg_name, arg_val) = match completion_argument(&self.prompt, arguments) {
Ok(tp) => tp,
Err(e) => {
return Task::ready(Err(e));
}
};
if let Some(server) = manager.get_server(&server_id) {
cx.foreground_executor().spawn(async move {
let Some(protocol) = server.client.read().clone() else {
return Err(anyhow!("Context server not initialized"));
};
let completion_result = protocol
.completion(
context_servers::types::CompletionReference::Prompt(
context_servers::types::PromptReference {
r#type: context_servers::types::PromptReferenceType::Prompt,
name: prompt_name,
},
),
arg_name,
arg_val,
)
.await?;
let completions = completion_result
.values
.into_iter()
.map(|value| ArgumentCompletion {
label: CodeLabel::plain(value.clone(), None),
new_text: value,
after_completion: AfterCompletion::Continue,
replace_previous_arguments: false,
})
.collect();
Ok(completions)
})
} else {
Task::ready(Err(anyhow!("Context server not found")))
}
Task::ready(Ok(Vec::new()))
}
fn run(
@@ -128,14 +81,10 @@ impl SlashCommand for ContextServerSlashCommand {
return Err(anyhow!("Context server not initialized"));
};
let result = protocol.run_prompt(&prompt_name, prompt_args).await?;
let mut prompt = result.prompt;
// We must normalize the line endings here, since servers might return CR characters.
LineEnding::normalize(&mut prompt);
Ok(SlashCommandOutput {
sections: vec![SlashCommandOutputSection {
range: 0..(prompt.len()),
range: 0..(result.prompt.len()),
icon: IconName::ZedAssistant,
label: SharedString::from(
result
@@ -143,7 +92,7 @@ impl SlashCommand for ContextServerSlashCommand {
.unwrap_or(format!("Result from {}", prompt_name)),
),
}],
text: prompt,
text: result.prompt,
run_commands_in_text: false,
})
})
@@ -153,22 +102,6 @@ impl SlashCommand for ContextServerSlashCommand {
}
}
fn completion_argument(prompt: &PromptInfo, arguments: &[String]) -> Result<(String, String)> {
if arguments.is_empty() {
return Err(anyhow!("No arguments given"));
}
match &prompt.arguments {
Some(args) if args.len() == 1 => {
let arg_name = args[0].name.clone();
let arg_value = arguments.join(" ");
Ok((arg_name, arg_value))
}
Some(_) => Err(anyhow!("Prompt must have exactly one argument")),
None => Err(anyhow!("Prompt has no arguments")),
}
}
fn prompt_arguments(prompt: &PromptInfo, arguments: &[String]) -> Result<HashMap<String, String>> {
match &prompt.arguments {
Some(args) if args.len() > 1 => Err(anyhow!(

View File

@@ -9,13 +9,13 @@ use collections::{HashMap, HashSet};
use editor::Editor;
use futures::future::join_all;
use gpui::{Entity, Task, WeakView};
use language::{BufferSnapshot, CodeLabel, HighlightId, LspAdapterDelegate};
use language::{BufferSnapshot, LspAdapterDelegate};
use std::{
fmt::Write,
path::PathBuf,
sync::{atomic::AtomicBool, Arc},
};
use ui::{ActiveTheme, WindowContext};
use ui::WindowContext;
use workspace::Workspace;
pub(crate) struct TabSlashCommand;
@@ -79,8 +79,6 @@ impl SlashCommand for TabSlashCommand {
let current_query = arguments.last().cloned().unwrap_or_default();
let tab_items_search =
tab_items_for_queries(workspace, &[current_query], cancel, false, cx);
let comment_id = cx.theme().syntax().highlight_id("comment").map(HighlightId);
cx.spawn(|_| async move {
let tab_items = tab_items_search.await?;
let run_command = tab_items.len() == 1;
@@ -92,9 +90,8 @@ impl SlashCommand for TabSlashCommand {
if active_item_path.is_some() && active_item_path == path {
return None;
}
let label = create_tab_completion_label(path.as_ref()?, comment_id);
Some(ArgumentCompletion {
label,
label: path_string.clone().into(),
new_text: path_string,
replace_previous_arguments: false,
after_completion: run_command.into(),
@@ -103,17 +100,14 @@ impl SlashCommand for TabSlashCommand {
let active_item_completion = active_item_path
.as_deref()
.map(|active_item_path| {
let path_string = active_item_path.to_string_lossy().to_string();
let label = create_tab_completion_label(active_item_path, comment_id);
ArgumentCompletion {
label,
new_text: path_string,
replace_previous_arguments: false,
after_completion: run_command.into(),
}
})
.filter(|completion| !argument_set.contains(&completion.new_text));
.map(|active_item_path| active_item_path.to_string_lossy().to_string())
.filter(|path_string| !argument_set.contains(path_string))
.map(|path_string| ArgumentCompletion {
label: path_string.clone().into(),
new_text: path_string,
replace_previous_arguments: false,
after_completion: run_command.into(),
});
Ok(active_item_completion
.into_iter()
@@ -325,23 +319,3 @@ fn active_item_buffer(
.snapshot();
Ok(snapshot)
}
fn create_tab_completion_label(
path: &std::path::Path,
comment_id: Option<HighlightId>,
) -> CodeLabel {
let file_name = path
.file_name()
.map(|f| f.to_string_lossy())
.unwrap_or_default();
let parent_path = path
.parent()
.map(|p| p.to_string_lossy())
.unwrap_or_default();
let mut label = CodeLabel::default();
label.push_str(&file_name, None);
label.push_str(" ", None);
label.push_str(&parent_path, comment_id);
label.filter_range = 0..file_name.len();
label
}

View File

@@ -157,11 +157,11 @@ impl TerminalInlineAssistant {
PromptEditorEvent::StopRequested => {
self.stop_assist(assist_id, cx);
}
PromptEditorEvent::ConfirmRequested { execute } => {
self.finish_assist(assist_id, false, *execute, cx);
PromptEditorEvent::ConfirmRequested => {
self.finish_assist(assist_id, false, cx);
}
PromptEditorEvent::CancelRequested => {
self.finish_assist(assist_id, true, false, cx);
self.finish_assist(assist_id, true, cx);
}
PromptEditorEvent::DismissRequested => {
self.dismiss_assist(assist_id, cx);
@@ -282,7 +282,6 @@ impl TerminalInlineAssistant {
Ok(LanguageModelRequest {
messages,
tools: Vec::new(),
stop: Vec::new(),
temperature: 1.0,
})
@@ -292,7 +291,6 @@ impl TerminalInlineAssistant {
&mut self,
assist_id: TerminalInlineAssistId,
undo: bool,
execute: bool,
cx: &mut WindowContext,
) {
self.dismiss_assist(assist_id, cx);
@@ -308,7 +306,7 @@ impl TerminalInlineAssistant {
assist.codegen.update(cx, |codegen, cx| {
if undo {
codegen.undo(cx);
} else if execute {
} else {
codegen.complete(cx);
}
});
@@ -424,7 +422,7 @@ impl TerminalInlineAssist {
}
if assist.prompt_editor.is_none() {
this.finish_assist(assist_id, false, false, cx);
this.finish_assist(assist_id, false, cx);
}
}
})
@@ -437,7 +435,7 @@ impl TerminalInlineAssist {
enum PromptEditorEvent {
StartRequested,
StopRequested,
ConfirmRequested { execute: bool },
ConfirmRequested,
CancelRequested,
DismissRequested,
Resized { height_in_lines: u8 },
@@ -510,15 +508,15 @@ impl Render for PromptEditor {
]
}
CodegenStatus::Error(_) | CodegenStatus::Done => {
let cancel = IconButton::new("cancel", IconName::Close)
.icon_color(Color::Muted)
.shape(IconButtonShape::Square)
.tooltip(|cx| Tooltip::for_action("Cancel Assist", &menu::Cancel, cx))
.on_click(cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::CancelRequested)));
if self.edited_since_done {
vec![
cancel,
vec![
IconButton::new("cancel", IconName::Close)
.icon_color(Color::Muted)
.shape(IconButtonShape::Square)
.tooltip(|cx| Tooltip::for_action("Cancel Assist", &menu::Cancel, cx))
.on_click(
cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::CancelRequested)),
),
if self.edited_since_done {
IconButton::new("restart", IconName::RotateCw)
.icon_color(Color::Info)
.shape(IconButtonShape::Square)
@@ -532,35 +530,19 @@ impl Render for PromptEditor {
})
.on_click(cx.listener(|_, _, cx| {
cx.emit(PromptEditorEvent::StartRequested);
})),
]
} else {
vec![
cancel,
IconButton::new("accept", IconName::Check)
.icon_color(Color::Info)
.shape(IconButtonShape::Square)
.tooltip(|cx| {
Tooltip::for_action("Accept Generated Command", &menu::Confirm, cx)
})
.on_click(cx.listener(|_, _, cx| {
cx.emit(PromptEditorEvent::ConfirmRequested { execute: false });
})),
}))
} else {
IconButton::new("confirm", IconName::Play)
.icon_color(Color::Info)
.shape(IconButtonShape::Square)
.tooltip(|cx| {
Tooltip::for_action(
"Execute Generated Command",
&menu::SecondaryConfirm,
cx,
)
Tooltip::for_action("Execute generated command", &menu::Confirm, cx)
})
.on_click(cx.listener(|_, _, cx| {
cx.emit(PromptEditorEvent::ConfirmRequested { execute: true });
})),
]
}
cx.emit(PromptEditorEvent::ConfirmRequested);
}))
},
]
}
};
@@ -572,7 +554,6 @@ impl Render for PromptEditor {
.h_full()
.w_full()
.on_action(cx.listener(Self::confirm))
.on_action(cx.listener(Self::secondary_confirm))
.on_action(cx.listener(Self::cancel))
.on_action(cx.listener(Self::move_up))
.on_action(cx.listener(Self::move_down))
@@ -623,7 +604,7 @@ impl Render for PromptEditor {
.child(div().flex_1().child(self.render_prompt_editor(cx)))
.child(
h_flex()
.gap_1()
.gap_2()
.pr_4()
.children(self.render_token_count(cx))
.children(buttons),
@@ -823,22 +804,13 @@ impl PromptEditor {
CodegenStatus::Pending => {
cx.emit(PromptEditorEvent::DismissRequested);
}
CodegenStatus::Done => {
CodegenStatus::Done | CodegenStatus::Error(_) => {
if self.edited_since_done {
cx.emit(PromptEditorEvent::StartRequested);
} else {
cx.emit(PromptEditorEvent::ConfirmRequested { execute: false });
cx.emit(PromptEditorEvent::ConfirmRequested);
}
}
CodegenStatus::Error(_) => {
cx.emit(PromptEditorEvent::StartRequested);
}
}
}
fn secondary_confirm(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext<Self>) {
if matches!(self.codegen.read(cx).status, CodegenStatus::Done) {
cx.emit(PromptEditorEvent::ConfirmRequested { execute: true });
}
}
@@ -1034,7 +1006,7 @@ impl Codegen {
self.transaction = Some(TerminalTransaction::start(self.terminal.clone()));
self.generation = cx.spawn(|this, mut cx| async move {
let model_telemetry_id = model.telemetry_id();
let response = model.stream_completion_text(prompt, &cx).await;
let response = model.stream_completion(prompt, &cx).await;
let generate = async {
let (mut hunks_tx, mut hunks_rx) = mpsc::channel(1);

View File

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

View File

@@ -1,60 +0,0 @@
use std::sync::Arc;
use anyhow::{anyhow, Result};
use assistant_tool::Tool;
use chrono::{Local, Utc};
use gpui::{Task, WeakView, WindowContext};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum Timezone {
/// Use UTC for the datetime.
Utc,
/// Use local time for the datetime.
Local,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct FileToolInput {
/// The timezone to use for the datetime.
timezone: Timezone,
}
pub struct NowTool;
impl Tool for NowTool {
fn name(&self) -> String {
"now".into()
}
fn description(&self) -> String {
"Returns the current datetime in RFC 3339 format.".into()
}
fn input_schema(&self) -> serde_json::Value {
let schema = schemars::schema_for!(FileToolInput);
serde_json::to_value(&schema).unwrap()
}
fn run(
self: Arc<Self>,
input: serde_json::Value,
_workspace: WeakView<workspace::Workspace>,
_cx: &mut WindowContext,
) -> Task<Result<String>> {
let input: FileToolInput = match serde_json::from_value(input) {
Ok(input) => input,
Err(err) => return Task::ready(Err(anyhow!(err))),
};
let now = match input.timezone {
Timezone::Utc => Utc::now().to_rfc3339(),
Timezone::Local => Local::now().to_rfc3339(),
};
let text = format!("The current datetime is {now}.");
Task::ready(Ok(text))
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,315 @@
use super::WorkflowStep;
use crate::{Assist, Context};
use editor::{
display_map::{BlockDisposition, BlockProperties, BlockStyle},
Editor, EditorEvent, ExcerptRange, MultiBuffer,
};
use gpui::{
div, AnyElement, AppContext, Context as _, Empty, EventEmitter, FocusableView, IntoElement,
Model, ParentElement as _, Render, SharedString, Styled as _, View, ViewContext,
VisualContext as _, WeakModel, WindowContext,
};
use language::{language_settings::SoftWrap, Anchor, Buffer, LanguageRegistry};
use std::{ops::DerefMut, sync::Arc};
use text::OffsetRangeExt;
use theme::ActiveTheme as _;
use ui::{
h_flex, v_flex, ButtonCommon as _, ButtonLike, ButtonStyle, Color, Icon, IconName,
InteractiveElement as _, Label, LabelCommon as _,
};
use workspace::{
item::{self, Item},
pane,
searchable::SearchableItemHandle,
};
pub struct WorkflowStepView {
step: WeakModel<WorkflowStep>,
tool_output_buffer: Model<Buffer>,
editor: View<Editor>,
}
impl WorkflowStepView {
pub fn new(
context: Model<Context>,
step: Model<WorkflowStep>,
language_registry: Arc<LanguageRegistry>,
cx: &mut ViewContext<Self>,
) -> Self {
let tool_output_buffer =
cx.new_model(|cx| Buffer::local(step.read(cx).tool_output.clone(), cx));
let buffer = cx.new_model(|cx| {
let mut buffer = MultiBuffer::without_headers(0, language::Capability::ReadWrite);
buffer.push_excerpts(
context.read(cx).buffer().clone(),
[ExcerptRange {
context: step.read(cx).context_buffer_range.clone(),
primary: None,
}],
cx,
);
buffer.push_excerpts(
tool_output_buffer.clone(),
[ExcerptRange {
context: Anchor::MIN..Anchor::MAX,
primary: None,
}],
cx,
);
buffer
});
let buffer_snapshot = buffer.read(cx).snapshot(cx);
let output_excerpt = buffer_snapshot.excerpts().skip(1).next().unwrap().0;
let input_start_anchor = multi_buffer::Anchor::min();
let output_start_anchor = buffer_snapshot
.anchor_in_excerpt(output_excerpt, Anchor::MIN)
.unwrap();
let output_end_anchor = multi_buffer::Anchor::max();
let handle = cx.view().downgrade();
let editor = cx.new_view(|cx| {
let mut editor = Editor::for_multibuffer(buffer.clone(), None, false, cx);
editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
editor.set_show_line_numbers(false, cx);
editor.set_show_git_diff_gutter(false, cx);
editor.set_show_code_actions(false, cx);
editor.set_show_runnables(false, cx);
editor.set_show_wrap_guides(false, cx);
editor.set_show_indent_guides(false, cx);
editor.set_read_only(true);
editor.set_show_inline_completions(Some(false), cx);
editor.insert_blocks(
[
BlockProperties {
position: input_start_anchor,
height: 1,
style: BlockStyle::Fixed,
render: Box::new(|cx| section_header("Step Input", cx)),
disposition: BlockDisposition::Above,
priority: 0,
},
BlockProperties {
position: output_start_anchor,
height: 1,
style: BlockStyle::Fixed,
render: Box::new(|cx| section_header("Tool Output", cx)),
disposition: BlockDisposition::Above,
priority: 0,
},
BlockProperties {
position: output_end_anchor,
height: 1,
style: BlockStyle::Fixed,
render: Box::new(move |cx| {
if let Some(result) = handle.upgrade().and_then(|this| {
this.update(cx.deref_mut(), |this, cx| this.render_result(cx))
}) {
v_flex()
.child(section_header("Output", cx))
.child(
div().pl(cx.gutter_dimensions.full_width()).child(result),
)
.into_any_element()
} else {
Empty.into_any_element()
}
}),
disposition: BlockDisposition::Below,
priority: 0,
},
],
None,
cx,
);
editor
});
cx.observe(&step, Self::step_updated).detach();
cx.observe_release(&step, Self::step_released).detach();
cx.spawn(|this, mut cx| async move {
if let Ok(language) = language_registry.language_for_name("JSON").await {
this.update(&mut cx, |this, cx| {
this.tool_output_buffer.update(cx, |buffer, cx| {
buffer.set_language(Some(language), cx);
});
})
.ok();
}
})
.detach();
Self {
tool_output_buffer,
step: step.downgrade(),
editor,
}
}
pub fn step(&self) -> &WeakModel<WorkflowStep> {
&self.step
}
fn render_result(&mut self, cx: &mut ViewContext<Self>) -> Option<AnyElement> {
let step = self.step.upgrade()?;
let result = step.read(cx).resolution.as_ref()?;
match result {
Ok(result) => {
Some(
v_flex()
.child(result.title.clone())
.children(result.suggestion_groups.iter().filter_map(
|(buffer, suggestion_groups)| {
let buffer = buffer.read(cx);
let path = buffer.file().map(|f| f.path());
let snapshot = buffer.snapshot();
v_flex()
.mb_2()
.border_b_1()
.children(path.map(|path| format!("path: {}", path.display())))
.children(suggestion_groups.iter().map(|group| {
v_flex().pt_2().pl_2().children(
group.suggestions.iter().map(|suggestion| {
let range = suggestion.range().to_point(&snapshot);
v_flex()
.children(
suggestion.description().map(|desc| {
format!("description: {desc}")
}),
)
.child(format!("kind: {}", suggestion.kind()))
.children(suggestion.symbol_path().map(
|path| format!("symbol path: {}", path.0),
))
.child(format!(
"lines: {} - {}",
range.start.row + 1,
range.end.row + 1
))
}),
)
}))
.into()
},
))
.into_any_element(),
)
}
Err(error) => Some(format!("{:?}", error).into_any_element()),
}
}
fn step_updated(&mut self, step: Model<WorkflowStep>, cx: &mut ViewContext<Self>) {
self.tool_output_buffer.update(cx, |buffer, cx| {
let text = step.read(cx).tool_output.clone();
buffer.set_text(text, cx);
});
cx.notify();
}
fn step_released(&mut self, _: &mut WorkflowStep, cx: &mut ViewContext<Self>) {
cx.emit(EditorEvent::Closed);
}
fn resolve(&mut self, _: &Assist, cx: &mut ViewContext<Self>) {
self.step
.update(cx, |step, cx| {
step.resolve(cx);
})
.ok();
}
}
fn section_header(
name: &'static str,
cx: &mut editor::display_map::BlockContext,
) -> gpui::AnyElement {
h_flex()
.pl(cx.gutter_dimensions.full_width())
.h_11()
.w_full()
.relative()
.gap_1()
.child(
ButtonLike::new("role")
.style(ButtonStyle::Filled)
.child(Label::new(name).color(Color::Default)),
)
.into_any_element()
}
impl Render for WorkflowStepView {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
div()
.key_context("ContextEditor")
.on_action(cx.listener(Self::resolve))
.flex_grow()
.bg(cx.theme().colors().editor_background)
.child(self.editor.clone())
}
}
impl EventEmitter<EditorEvent> for WorkflowStepView {}
impl FocusableView for WorkflowStepView {
fn focus_handle(&self, cx: &gpui::AppContext) -> gpui::FocusHandle {
self.editor.read(cx).focus_handle(cx)
}
}
impl Item for WorkflowStepView {
type Event = EditorEvent;
fn tab_content_text(&self, cx: &WindowContext) -> Option<SharedString> {
let step = self.step.upgrade()?.read(cx);
let context = step.context.upgrade()?.read(cx);
let buffer = context.buffer().read(cx);
let index = context
.workflow_step_index_for_range(&step.context_buffer_range, buffer)
.ok()?
+ 1;
Some(format!("Step {index}").into())
}
fn tab_icon(&self, _cx: &WindowContext) -> Option<ui::Icon> {
Some(Icon::new(IconName::SearchCode))
}
fn to_item_events(event: &Self::Event, mut f: impl FnMut(item::ItemEvent)) {
match event {
EditorEvent::Edited { .. } => {
f(item::ItemEvent::Edit);
}
EditorEvent::TitleChanged => {
f(item::ItemEvent::UpdateTab);
}
EditorEvent::Closed => f(item::ItemEvent::CloseItem),
_ => {}
}
}
fn tab_tooltip_text(&self, _cx: &AppContext) -> Option<SharedString> {
None
}
fn as_searchable(&self, _handle: &View<Self>) -> Option<Box<dyn SearchableItemHandle>> {
None
}
fn set_nav_history(&mut self, nav_history: pane::ItemNavHistory, cx: &mut ViewContext<Self>) {
self.editor.update(cx, |editor, cx| {
Item::set_nav_history(editor, nav_history, cx)
})
}
fn navigate(&mut self, data: Box<dyn std::any::Any>, cx: &mut ViewContext<Self>) -> bool {
self.editor
.update(cx, |editor, cx| Item::navigate(editor, data, cx))
}
fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
self.editor
.update(cx, |editor, cx| Item::deactivated(editor, cx))
}
}

View File

@@ -1,22 +0,0 @@
[package]
name = "assistant_tool"
version = "0.1.0"
edition = "2021"
publish = false
license = "GPL-3.0-or-later"
[lints]
workspace = true
[lib]
path = "src/assistant_tool.rs"
[dependencies]
anyhow.workspace = true
collections.workspace = true
derive_more.workspace = true
gpui.workspace = true
parking_lot.workspace = true
serde.workspace = true
serde_json.workspace = true
workspace.workspace = true

View File

@@ -1 +0,0 @@
../../LICENSE-GPL

View File

@@ -1,35 +0,0 @@
mod tool_registry;
use std::sync::Arc;
use anyhow::Result;
use gpui::{AppContext, Task, WeakView, WindowContext};
use workspace::Workspace;
pub use tool_registry::*;
pub fn init(cx: &mut AppContext) {
ToolRegistry::default_global(cx);
}
/// A tool that can be used by a language model.
pub trait Tool: 'static + Send + Sync {
/// Returns the name of the tool.
fn name(&self) -> String;
/// Returns the description of the tool.
fn description(&self) -> String;
/// Returns the JSON schema that describes the tool's input.
fn input_schema(&self) -> serde_json::Value {
serde_json::Value::Object(serde_json::Map::default())
}
/// Runs the tool with the provided input.
fn run(
self: Arc<Self>,
input: serde_json::Value,
workspace: WeakView<Workspace>,
cx: &mut WindowContext,
) -> Task<Result<String>>;
}

View File

@@ -1,74 +0,0 @@
use std::sync::Arc;
use collections::HashMap;
use derive_more::{Deref, DerefMut};
use gpui::Global;
use gpui::{AppContext, ReadGlobal};
use parking_lot::RwLock;
use crate::Tool;
#[derive(Default, Deref, DerefMut)]
struct GlobalToolRegistry(Arc<ToolRegistry>);
impl Global for GlobalToolRegistry {}
#[derive(Default)]
struct ToolRegistryState {
tools: HashMap<Arc<str>, Arc<dyn Tool>>,
}
#[derive(Default)]
pub struct ToolRegistry {
state: RwLock<ToolRegistryState>,
}
impl ToolRegistry {
/// Returns the global [`ToolRegistry`].
pub fn global(cx: &AppContext) -> Arc<Self> {
GlobalToolRegistry::global(cx).0.clone()
}
/// Returns the global [`ToolRegistry`].
///
/// Inserts a default [`ToolRegistry`] if one does not yet exist.
pub fn default_global(cx: &mut AppContext) -> Arc<Self> {
cx.default_global::<GlobalToolRegistry>().0.clone()
}
pub fn new() -> Arc<Self> {
Arc::new(Self {
state: RwLock::new(ToolRegistryState {
tools: HashMap::default(),
}),
})
}
/// Registers the provided [`Tool`].
pub fn register_tool(&self, tool: impl Tool) {
let mut state = self.state.write();
let tool_name: Arc<str> = tool.name().into();
state.tools.insert(tool_name, Arc::new(tool));
}
/// Unregisters the provided [`Tool`].
pub fn unregister_tool(&self, tool: impl Tool) {
self.unregister_tool_by_name(tool.name().as_str())
}
/// Unregisters the tool with the given name.
pub fn unregister_tool_by_name(&self, tool_name: &str) {
let mut state = self.state.write();
state.tools.remove(tool_name);
}
/// Returns the list of tools in the registry.
pub fn tools(&self) -> Vec<Arc<dyn Tool>> {
self.state.read().tools.values().cloned().collect()
}
/// Returns the [`Tool`] with the given name.
pub fn tool(&self, name: &str) -> Option<Arc<dyn Tool>> {
self.state.read().tools.get(name).cloned()
}
}

View File

@@ -18,6 +18,6 @@ mod channel_store_tests;
pub fn init(client: &Arc<Client>, user_store: Model<UserStore>, cx: &mut AppContext) {
channel_store::init(client, user_store, cx);
channel_buffer::init(&client.clone().into());
channel_chat::init(&client.clone().into());
channel_buffer::init(client);
channel_chat::init(client);
}

View File

@@ -5,7 +5,7 @@ use collections::HashMap;
use gpui::{AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Task};
use language::proto::serialize_version;
use rpc::{
proto::{self, AnyProtoClient, PeerId},
proto::{self, PeerId},
TypedEnvelope,
};
use std::{sync::Arc, time::Duration};
@@ -14,7 +14,7 @@ use util::ResultExt;
pub const ACKNOWLEDGE_DEBOUNCE_INTERVAL: Duration = Duration::from_millis(250);
pub(crate) fn init(client: &AnyProtoClient) {
pub(crate) fn init(client: &Arc<Client>) {
client.add_model_message_handler(ChannelBuffer::handle_update_channel_buffer);
client.add_model_message_handler(ChannelBuffer::handle_update_channel_buffer_collaborators);
}

View File

@@ -11,7 +11,6 @@ use gpui::{
AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Task, WeakModel,
};
use rand::prelude::*;
use rpc::proto::AnyProtoClient;
use std::{
ops::{ControlFlow, Range},
sync::Arc,
@@ -96,7 +95,7 @@ pub enum ChannelChatEvent {
}
impl EventEmitter<ChannelChatEvent> for ChannelChat {}
pub fn init(client: &AnyProtoClient) {
pub fn init(client: &Arc<Client>) {
client.add_model_message_handler(ChannelChat::handle_message_sent);
client.add_model_message_handler(ChannelChat::handle_message_removed);
client.add_model_message_handler(ChannelChat::handle_message_updated);

View File

@@ -12,20 +12,23 @@ use async_tungstenite::tungstenite::{
error::Error as WebsocketError,
http::{HeaderValue, Request, StatusCode},
};
use chrono::{DateTime, Utc};
use clock::SystemClock;
use collections::HashMap;
use futures::{
channel::oneshot, future::BoxFuture, AsyncReadExt, FutureExt, SinkExt, Stream, StreamExt,
TryFutureExt as _, TryStreamExt,
channel::oneshot,
future::{BoxFuture, LocalBoxFuture},
AsyncReadExt, FutureExt, SinkExt, Stream, StreamExt, TryFutureExt as _, TryStreamExt,
};
use gpui::{
actions, AnyModel, AnyWeakModel, AppContext, AsyncAppContext, Global, Model, Task, WeakModel,
};
use gpui::{actions, AppContext, AsyncAppContext, Global, Model, Task, WeakModel};
use http_client::{AsyncBody, HttpClient, HttpClientWithUrl};
use parking_lot::RwLock;
use postage::watch;
use proto::{AnyProtoClient, EntityMessageSubscriber, ProtoClient, ProtoMessageHandlerSet};
use proto::ProtoClient;
use rand::prelude::*;
use release_channel::{AppVersion, ReleaseChannel};
use rpc::proto::{AnyTypedEnvelope, EnvelopedMessage, PeerId, RequestMessage};
use rpc::proto::{AnyTypedEnvelope, EntityMessage, EnvelopedMessage, PeerId, RequestMessage};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources};
@@ -204,7 +207,6 @@ pub struct Client {
telemetry: Arc<Telemetry>,
credentials_provider: Arc<dyn CredentialsProvider + Send + Sync + 'static>,
state: RwLock<ClientState>,
handler_set: parking_lot::Mutex<ProtoMessageHandlerSet>,
#[allow(clippy::type_complexity)]
#[cfg(any(test, feature = "test-support"))]
@@ -301,7 +303,30 @@ impl Status {
struct ClientState {
credentials: Option<Credentials>,
status: (watch::Sender<Status>, watch::Receiver<Status>),
entity_id_extractors: HashMap<TypeId, fn(&dyn AnyTypedEnvelope) -> u64>,
_reconnect_task: Option<Task<()>>,
entities_by_type_and_remote_id: HashMap<(TypeId, u64), WeakSubscriber>,
models_by_message_type: HashMap<TypeId, AnyWeakModel>,
entity_types_by_message_type: HashMap<TypeId, TypeId>,
#[allow(clippy::type_complexity)]
message_handlers: HashMap<
TypeId,
Arc<
dyn Send
+ Sync
+ Fn(
AnyModel,
Box<dyn AnyTypedEnvelope>,
&Arc<Client>,
AsyncAppContext,
) -> LocalBoxFuture<'static, Result<()>>,
>,
>,
}
enum WeakSubscriber {
Entity { handle: AnyWeakModel },
Pending(Vec<Box<dyn AnyTypedEnvelope>>),
}
#[derive(Clone, Debug, Eq, PartialEq)]
@@ -353,7 +378,12 @@ impl Default for ClientState {
Self {
credentials: None,
status: watch::channel_with(Status::SignedOut),
entity_id_extractors: Default::default(),
_reconnect_task: None,
models_by_message_type: Default::default(),
entities_by_type_and_remote_id: Default::default(),
entity_types_by_message_type: Default::default(),
message_handlers: Default::default(),
}
}
}
@@ -374,13 +404,13 @@ impl Drop for Subscription {
match self {
Subscription::Entity { client, id } => {
if let Some(client) = client.upgrade() {
let mut state = client.handler_set.lock();
let mut state = client.state.write();
let _ = state.entities_by_type_and_remote_id.remove(id);
}
}
Subscription::Message { client, id } => {
if let Some(client) = client.upgrade() {
let mut state = client.handler_set.lock();
let mut state = client.state.write();
let _ = state.entity_types_by_message_type.remove(id);
let _ = state.message_handlers.remove(id);
}
@@ -399,31 +429,22 @@ pub struct PendingEntitySubscription<T: 'static> {
impl<T: 'static> PendingEntitySubscription<T> {
pub fn set_model(mut self, model: &Model<T>, cx: &mut AsyncAppContext) -> Subscription {
self.consumed = true;
let mut handlers = self.client.handler_set.lock();
let mut state = self.client.state.write();
let id = (TypeId::of::<T>(), self.remote_id);
let Some(EntityMessageSubscriber::Pending(messages)) =
handlers.entities_by_type_and_remote_id.remove(&id)
let Some(WeakSubscriber::Pending(messages)) =
state.entities_by_type_and_remote_id.remove(&id)
else {
unreachable!()
};
handlers.entities_by_type_and_remote_id.insert(
state.entities_by_type_and_remote_id.insert(
id,
EntityMessageSubscriber::Entity {
WeakSubscriber::Entity {
handle: model.downgrade().into(),
},
);
drop(handlers);
drop(state);
for message in messages {
let client_id = self.client.id();
let type_name = message.payload_type_name();
let sender_id = message.original_sender_id();
log::debug!(
"handling queued rpc message. client_id:{}, sender_id:{:?}, type:{}",
client_id,
sender_id,
type_name
);
self.client.handle_message(message, cx);
}
Subscription::Entity {
@@ -436,8 +457,8 @@ impl<T: 'static> PendingEntitySubscription<T> {
impl<T: 'static> Drop for PendingEntitySubscription<T> {
fn drop(&mut self) {
if !self.consumed {
let mut state = self.client.handler_set.lock();
if let Some(EntityMessageSubscriber::Pending(messages)) = state
let mut state = self.client.state.write();
if let Some(WeakSubscriber::Pending(messages)) = state
.entities_by_type_and_remote_id
.remove(&(TypeId::of::<T>(), self.remote_id))
{
@@ -518,7 +539,6 @@ impl Client {
http,
credentials_provider,
state: Default::default(),
handler_set: Default::default(),
#[cfg(any(test, feature = "test-support"))]
authenticate: Default::default(),
@@ -562,7 +582,10 @@ impl Client {
pub fn teardown(&self) {
let mut state = self.state.write();
state._reconnect_task.take();
self.handler_set.lock().clear();
state.message_handlers.clear();
state.models_by_message_type.clear();
state.entities_by_type_and_remote_id.clear();
state.entity_id_extractors.clear();
self.peer.teardown();
}
@@ -675,14 +698,14 @@ impl Client {
{
let id = (TypeId::of::<T>(), remote_id);
let mut state = self.handler_set.lock();
let mut state = self.state.write();
if state.entities_by_type_and_remote_id.contains_key(&id) {
return Err(anyhow!("already subscribed to entity"));
}
state
.entities_by_type_and_remote_id
.insert(id, EntityMessageSubscriber::Pending(Default::default()));
.insert(id, WeakSubscriber::Pending(Default::default()));
Ok(PendingEntitySubscription {
client: self.clone(),
@@ -719,13 +742,13 @@ impl Client {
E: 'static,
H: 'static
+ Sync
+ Fn(Model<E>, TypedEnvelope<M>, AnyProtoClient, AsyncAppContext) -> F
+ Fn(Model<E>, TypedEnvelope<M>, Arc<Self>, AsyncAppContext) -> F
+ Send
+ Sync,
F: 'static + Future<Output = Result<()>>,
{
let message_type_id = TypeId::of::<M>();
let mut state = self.handler_set.lock();
let mut state = self.state.write();
state
.models_by_message_type
.insert(message_type_id, entity.into());
@@ -770,18 +793,85 @@ impl Client {
})
}
pub fn add_model_message_handler<M, E, H, F>(self: &Arc<Self>, handler: H)
where
M: EntityMessage,
E: 'static,
H: 'static + Fn(Model<E>, TypedEnvelope<M>, AsyncAppContext) -> F + Send + Sync,
F: 'static + Future<Output = Result<()>>,
{
self.add_entity_message_handler::<M, E, _, _>(move |subscriber, message, _, cx| {
handler(subscriber.downcast::<E>().unwrap(), message, cx)
})
}
fn add_entity_message_handler<M, E, H, F>(self: &Arc<Self>, handler: H)
where
M: EntityMessage,
E: 'static,
H: 'static + Fn(AnyModel, TypedEnvelope<M>, Arc<Self>, AsyncAppContext) -> F + Send + Sync,
F: 'static + Future<Output = Result<()>>,
{
let model_type_id = TypeId::of::<E>();
let message_type_id = TypeId::of::<M>();
let mut state = self.state.write();
state
.entity_types_by_message_type
.insert(message_type_id, model_type_id);
state
.entity_id_extractors
.entry(message_type_id)
.or_insert_with(|| {
|envelope| {
envelope
.as_any()
.downcast_ref::<TypedEnvelope<M>>()
.unwrap()
.payload
.remote_entity_id()
}
});
let prev_handler = state.message_handlers.insert(
message_type_id,
Arc::new(move |handle, envelope, client, cx| {
let envelope = envelope.into_any().downcast::<TypedEnvelope<M>>().unwrap();
handler(handle, *envelope, client.clone(), cx).boxed_local()
}),
);
if prev_handler.is_some() {
panic!("registered handler for the same message twice");
}
}
pub fn add_model_request_handler<M, E, H, F>(self: &Arc<Self>, handler: H)
where
M: EntityMessage + RequestMessage,
E: 'static,
H: 'static + Fn(Model<E>, TypedEnvelope<M>, AsyncAppContext) -> F + Send + Sync,
F: 'static + Future<Output = Result<M::Response>>,
{
self.add_entity_message_handler::<M, E, _, _>(move |entity, envelope, client, cx| {
Self::respond_to_request::<M, _>(
envelope.receipt(),
handler(entity.downcast::<E>().unwrap(), envelope, cx),
client,
)
})
}
async fn respond_to_request<T: RequestMessage, F: Future<Output = Result<T::Response>>>(
receipt: Receipt<T>,
response: F,
client: AnyProtoClient,
client: Arc<Self>,
) -> Result<()> {
match response.await {
Ok(response) => {
client.send_response(receipt.message_id, response)?;
client.respond(receipt, response)?;
Ok(())
}
Err(error) => {
client.send_response(receipt.message_id, error.to_proto())?;
client.respond_with_error(receipt, error.to_proto())?;
Err(error)
}
}
@@ -1310,7 +1400,6 @@ impl Client {
struct GithubUser {
id: i32,
login: String,
created_at: DateTime<Utc>,
}
let request = {
@@ -1356,32 +1445,14 @@ impl Client {
user
};
let query_params = [
("github_login", &github_user.login),
("github_user_id", &github_user.id.to_string()),
(
"github_user_created_at",
&github_user.created_at.to_rfc3339(),
),
];
// Use the collab server's admin API to retrieve the ID
// Use the collab server's admin API to retrieve the id
// of the impersonated user.
let mut url = self.rpc_url(http.clone(), None).await?;
url.set_path("/user");
url.set_query(Some(
&query_params
.iter()
.map(|(key, value)| {
format!(
"{}={}",
key,
url::form_urlencoded::byte_serialize(value.as_bytes()).collect::<String>()
)
})
.collect::<Vec<String>>()
.join("&"),
));
url.set_query(Some(&format!(
"github_login={}&github_user_id={}",
github_user.login, github_user.id
)));
let request: http_client::Request<AsyncBody> = Request::get(url.as_str())
.header("Authorization", format!("token {api_token}"))
.body("".into())?;
@@ -1441,6 +1512,11 @@ impl Client {
self.peer.send(self.connection_id()?, message)
}
pub fn send_dynamic(&self, envelope: proto::Envelope) -> Result<()> {
let connection_id = self.connection_id()?;
self.peer.send_dynamic(connection_id, envelope)
}
pub fn request<T: RequestMessage>(
&self,
request: T,
@@ -1522,56 +1598,115 @@ impl Client {
}
}
fn respond<T: RequestMessage>(&self, receipt: Receipt<T>, response: T::Response) -> Result<()> {
log::debug!("rpc respond. client_id:{}. name:{}", self.id(), T::NAME);
self.peer.respond(receipt, response)
}
fn respond_with_error<T: RequestMessage>(
&self,
receipt: Receipt<T>,
error: proto::Error,
) -> Result<()> {
log::debug!("rpc respond. client_id:{}. name:{}", self.id(), T::NAME);
self.peer.respond_with_error(receipt, error)
}
fn handle_message(
self: &Arc<Client>,
message: Box<dyn AnyTypedEnvelope>,
cx: &AsyncAppContext,
) {
let sender_id = message.sender_id();
let request_id = message.message_id();
let mut state = self.state.write();
let type_name = message.payload_type_name();
let original_sender_id = message.original_sender_id();
let payload_type_id = message.payload_type_id();
let sender_id = message.original_sender_id();
if let Some(future) = ProtoMessageHandlerSet::handle_message(
&self.handler_set,
message,
self.clone().into(),
cx.clone(),
) {
let mut subscriber = None;
if let Some(handle) = state
.models_by_message_type
.get(&payload_type_id)
.and_then(|handle| handle.upgrade())
{
subscriber = Some(handle);
} else if let Some((extract_entity_id, entity_type_id)) =
state.entity_id_extractors.get(&payload_type_id).zip(
state
.entity_types_by_message_type
.get(&payload_type_id)
.copied(),
)
{
let entity_id = (extract_entity_id)(message.as_ref());
match state
.entities_by_type_and_remote_id
.get_mut(&(entity_type_id, entity_id))
{
Some(WeakSubscriber::Pending(pending)) => {
pending.push(message);
return;
}
Some(weak_subscriber) => match weak_subscriber {
WeakSubscriber::Entity { handle } => {
subscriber = handle.upgrade();
}
WeakSubscriber::Pending(_) => {}
},
_ => {}
}
}
let subscriber = if let Some(subscriber) = subscriber {
subscriber
} else {
log::info!("unhandled message {}", type_name);
self.peer.respond_with_unhandled_message(message).log_err();
return;
};
let handler = state.message_handlers.get(&payload_type_id).cloned();
// Dropping the state prevents deadlocks if the handler interacts with rpc::Client.
// It also ensures we don't hold the lock while yielding back to the executor, as
// that might cause the executor thread driving this future to block indefinitely.
drop(state);
if let Some(handler) = handler {
let future = handler(subscriber, message, self, cx.clone());
let client_id = self.id();
log::debug!(
"rpc message received. client_id:{}, sender_id:{:?}, type:{}",
client_id,
original_sender_id,
sender_id,
type_name
);
cx.spawn(move |_| async move {
match future.await {
Ok(()) => {
log::debug!(
"rpc message handled. client_id:{}, sender_id:{:?}, type:{}",
client_id,
original_sender_id,
type_name
);
match future.await {
Ok(()) => {
log::debug!(
"rpc message handled. client_id:{}, sender_id:{:?}, type:{}",
client_id,
sender_id,
type_name
);
}
Err(error) => {
log::error!(
"error handling message. client_id:{}, sender_id:{:?}, type:{}, error:{:?}",
client_id,
sender_id,
type_name,
error
);
}
}
Err(error) => {
log::error!(
"error handling message. client_id:{}, sender_id:{:?}, type:{}, error:{:?}",
client_id,
original_sender_id,
type_name,
error
);
}
}
})
.detach();
})
.detach();
} else {
log::info!("unhandled message {}", type_name);
self.peer
.respond_with_unhandled_message(sender_id.into(), request_id, type_name)
.log_err();
self.peer.respond_with_unhandled_message(message).log_err();
}
}
@@ -1589,24 +1724,8 @@ impl ProtoClient for Client {
self.request_dynamic(envelope, request_type).boxed()
}
fn send(&self, envelope: proto::Envelope, message_type: &'static str) -> Result<()> {
log::debug!("rpc send. client_id:{}, name:{}", self.id(), message_type);
let connection_id = self.connection_id()?;
self.peer.send_dynamic(connection_id, envelope)
}
fn send_response(&self, envelope: proto::Envelope, message_type: &'static str) -> Result<()> {
log::debug!(
"rpc respond. client_id:{}, name:{}",
self.id(),
message_type
);
let connection_id = self.connection_id()?;
self.peer.send_dynamic(connection_id, envelope)
}
fn message_handler_set(&self) -> &parking_lot::Mutex<ProtoMessageHandlerSet> {
&self.handler_set
fn send(&self, envelope: proto::Envelope) -> Result<()> {
self.send_dynamic(envelope)
}
}
@@ -1950,7 +2069,7 @@ mod tests {
let (done_tx1, mut done_rx1) = smol::channel::unbounded();
let (done_tx2, mut done_rx2) = smol::channel::unbounded();
AnyProtoClient::from(client.clone()).add_model_message_handler(
client.add_model_message_handler(
move |model: Model<TestModel>, _: TypedEnvelope<proto::JoinProject>, mut cx| {
match model.update(&mut cx, |model, _| model.id).unwrap() {
1 => done_tx1.try_send(()).unwrap(),

View File

@@ -111,7 +111,7 @@ struct AuthenticatedUserParams {
github_user_id: i32,
github_login: String,
github_email: Option<String>,
github_user_created_at: chrono::DateTime<chrono::Utc>,
github_user_created_at: Option<chrono::DateTime<chrono::Utc>>,
}
#[derive(Debug, Serialize)]

View File

@@ -65,7 +65,7 @@ impl Database {
github_login: &str,
github_user_id: i32,
github_email: Option<&str>,
github_user_created_at: DateTimeUtc,
github_user_created_at: Option<DateTimeUtc>,
initial_channel_id: Option<ChannelId>,
) -> Result<()> {
self.transaction(|tx| async move {
@@ -74,7 +74,7 @@ impl Database {
github_login,
github_user_id,
github_email,
github_user_created_at.naive_utc(),
github_user_created_at.map(|time| time.naive_utc()),
initial_channel_id,
&tx,
)

View File

@@ -101,7 +101,7 @@ impl Database {
github_login: &str,
github_user_id: i32,
github_email: Option<&str>,
github_user_created_at: DateTimeUtc,
github_user_created_at: Option<DateTimeUtc>,
initial_channel_id: Option<ChannelId>,
) -> Result<User> {
self.transaction(|tx| async move {
@@ -109,7 +109,7 @@ impl Database {
github_login,
github_user_id,
github_email,
github_user_created_at.naive_utc(),
github_user_created_at.map(|created_at| created_at.naive_utc()),
initial_channel_id,
&tx,
)
@@ -123,7 +123,7 @@ impl Database {
github_login: &str,
github_user_id: i32,
github_email: Option<&str>,
github_user_created_at: NaiveDateTime,
github_user_created_at: Option<NaiveDateTime>,
initial_channel_id: Option<ChannelId>,
tx: &DatabaseTransaction,
) -> Result<User> {
@@ -134,8 +134,10 @@ impl Database {
{
let mut user_by_github_user_id = user_by_github_user_id.into_active_model();
user_by_github_user_id.github_login = ActiveValue::set(github_login.into());
user_by_github_user_id.github_user_created_at =
ActiveValue::set(Some(github_user_created_at));
if github_user_created_at.is_some() {
user_by_github_user_id.github_user_created_at =
ActiveValue::set(github_user_created_at);
}
Ok(user_by_github_user_id.update(tx).await?)
} else if let Some(user_by_github_login) = user::Entity::find()
.filter(user::Column::GithubLogin.eq(github_login))
@@ -144,15 +146,17 @@ impl Database {
{
let mut user_by_github_login = user_by_github_login.into_active_model();
user_by_github_login.github_user_id = ActiveValue::set(github_user_id);
user_by_github_login.github_user_created_at =
ActiveValue::set(Some(github_user_created_at));
if github_user_created_at.is_some() {
user_by_github_login.github_user_created_at =
ActiveValue::set(github_user_created_at);
}
Ok(user_by_github_login.update(tx).await?)
} else {
let user = user::Entity::insert(user::ActiveModel {
email_address: ActiveValue::set(github_email.map(|email| email.into())),
github_login: ActiveValue::set(github_login.into()),
github_user_id: ActiveValue::set(github_user_id),
github_user_created_at: ActiveValue::set(Some(github_user_created_at)),
github_user_created_at: ActiveValue::set(github_user_created_at),
admin: ActiveValue::set(false),
invite_count: ActiveValue::set(0),
invite_code: ActiveValue::set(None),

View File

@@ -25,7 +25,7 @@ async fn test_contributors(db: &Arc<Database>) {
assert_eq!(db.get_contributors().await.unwrap(), Vec::<String>::new());
let user1_created_at = Utc::now();
db.add_contributor("user1", 1, None, user1_created_at, None)
db.add_contributor("user1", 1, None, Some(user1_created_at), None)
.await
.unwrap();
assert_eq!(
@@ -34,7 +34,7 @@ async fn test_contributors(db: &Arc<Database>) {
);
let user2_created_at = Utc::now();
db.add_contributor("user2", 2, None, user2_created_at, None)
db.add_contributor("user2", 2, None, Some(user2_created_at), None)
.await
.unwrap();
assert_eq!(

View File

@@ -101,7 +101,7 @@ async fn test_get_or_create_user_by_github_account(db: &Arc<Database>) {
.user_id;
let user = db
.get_or_create_user_by_github_account("the-new-login2", 102, None, Utc::now(), None)
.get_or_create_user_by_github_account("the-new-login2", 102, None, Some(Utc::now()), None)
.await
.unwrap();
assert_eq!(user.id, user_id2);
@@ -113,7 +113,7 @@ async fn test_get_or_create_user_by_github_account(db: &Arc<Database>) {
"login3",
103,
Some("user3@example.com"),
Utc::now(),
Some(Utc::now()),
None,
)
.await

View File

@@ -9,7 +9,6 @@ use crate::{
};
use anyhow::{anyhow, Context as _};
use authorization::authorize_access_to_language_model;
use axum::routing::get;
use axum::{
body::Body,
http::{self, HeaderName, HeaderValue, Request, StatusCode},
@@ -23,7 +22,6 @@ use collections::HashMap;
use db::{usage_measure::UsageMeasure, ActiveUserCount, LlmDatabase};
use futures::{Stream, StreamExt as _};
use http_client::IsahcHttpClient;
use rpc::ListModelsResponse;
use rpc::{
proto::Plan, LanguageModelProvider, PerformCompletionParams, EXPIRED_LLM_TOKEN_HEADER_NAME,
};
@@ -116,7 +114,6 @@ impl LlmState {
pub fn routes() -> Router<(), Body> {
Router::new()
.route("/models", get(list_models))
.route("/completion", post(perform_completion))
.layer(middleware::from_fn(validate_api_token))
}
@@ -176,37 +173,6 @@ async fn validate_api_token<B>(mut req: Request<B>, next: Next<B>) -> impl IntoR
}
}
async fn list_models(
Extension(state): Extension<Arc<LlmState>>,
Extension(claims): Extension<LlmTokenClaims>,
country_code_header: Option<TypedHeader<CloudflareIpCountryHeader>>,
) -> Result<Json<ListModelsResponse>> {
let country_code = country_code_header.map(|header| header.to_string());
let mut accessible_models = Vec::new();
for (provider, model) in state.db.all_models() {
let authorize_result = authorize_access_to_language_model(
&state.config,
&claims,
country_code.as_deref(),
provider,
&model.name,
);
if authorize_result.is_ok() {
accessible_models.push(rpc::LanguageModel {
provider,
name: model.name,
});
}
}
Ok(Json(ListModelsResponse {
models: accessible_models,
}))
}
async fn perform_completion(
Extension(state): Extension<Arc<LlmState>>,
Extension(claims): Extension<LlmTokenClaims>,
@@ -221,9 +187,7 @@ async fn perform_completion(
authorize_access_to_language_model(
&state.config,
&claims,
country_code_header
.map(|header| header.to_string())
.as_deref(),
country_code_header.map(|header| header.to_string()),
params.provider,
&model,
)?;

View File

@@ -7,7 +7,7 @@ use crate::{Config, Error, Result};
pub fn authorize_access_to_language_model(
config: &Config,
claims: &LlmTokenClaims,
country_code: Option<&str>,
country_code: Option<String>,
provider: LanguageModelProvider,
model: &str,
) -> Result<()> {
@@ -49,7 +49,7 @@ fn authorize_access_to_model(
fn authorize_access_for_country(
config: &Config,
country_code: Option<&str>,
country_code: Option<String>,
provider: LanguageModelProvider,
) -> Result<()> {
// In development we won't have the `CF-IPCountry` header, so we can't check
@@ -62,7 +62,7 @@ fn authorize_access_for_country(
}
// https://developers.cloudflare.com/fundamentals/reference/http-request-headers/#cf-ipcountry
let country_code = match country_code {
let country_code = match country_code.as_deref() {
// `XX` - Used for clients without country code data.
None | Some("XX") => Err(Error::http(
StatusCode::BAD_REQUEST,
@@ -128,7 +128,7 @@ mod tests {
authorize_access_to_language_model(
&config,
&claims,
Some(country_code),
Some(country_code.into()),
provider,
"the-model",
)
@@ -178,7 +178,7 @@ mod tests {
let error_response = authorize_access_to_language_model(
&config,
&claims,
Some(country_code),
Some(country_code.into()),
provider,
"the-model",
)
@@ -223,7 +223,7 @@ mod tests {
let error_response = authorize_access_to_language_model(
&config,
&claims,
Some(country_code),
Some(country_code.into()),
provider,
"the-model",
)
@@ -278,8 +278,13 @@ mod tests {
..Default::default()
};
let result =
authorize_access_to_language_model(&config, &claims, Some("US"), provider, model);
let result = authorize_access_to_language_model(
&config,
&claims,
Some("US".into()),
provider,
model,
);
if expected_access {
assert!(
@@ -319,8 +324,13 @@ mod tests {
];
for (provider, model) in test_cases {
let result =
authorize_access_to_language_model(&config, &claims, Some("US"), provider, model);
let result = authorize_access_to_language_model(
&config,
&claims,
Some("US".into()),
provider,
model,
);
assert!(
result.is_ok(),

View File

@@ -67,14 +67,6 @@ impl LlmDatabase {
Ok(())
}
/// Returns the list of all known models, with their [`LanguageModelProvider`].
pub fn all_models(&self) -> Vec<(LanguageModelProvider, model::Model)> {
self.models
.iter()
.map(|((model_provider, _model_name), model)| (*model_provider, model.clone()))
.collect::<Vec<_>>()
}
/// Returns the names of the known models for the given [`LanguageModelProvider`].
pub fn model_names_for_provider(&self, provider: LanguageModelProvider) -> Vec<String> {
self.models

View File

@@ -495,9 +495,6 @@ impl Server {
.add_request_handler(user_handler(
forward_read_only_project_request::<proto::InlayHints>,
))
.add_request_handler(user_handler(
forward_read_only_project_request::<proto::ResolveInlayHint>,
))
.add_request_handler(user_handler(
forward_read_only_project_request::<proto::OpenBufferByPath>,
))

View File

@@ -1,7 +1,6 @@
use crate::db::{self, ChannelRole, NewUserParams};
use anyhow::Context;
use chrono::{DateTime, Utc};
use db::Database;
use serde::{de::DeserializeOwned, Deserialize};
use std::{fmt::Write, fs, path::Path};
@@ -9,11 +8,10 @@ use std::{fmt::Write, fs, path::Path};
use crate::Config;
#[derive(Debug, Deserialize)]
struct GithubUser {
struct GitHubUser {
id: i32,
login: String,
email: Option<String>,
created_at: DateTime<Utc>,
}
#[derive(Deserialize)]
@@ -56,7 +54,7 @@ pub async fn seed(config: &Config, db: &Database, force: bool) -> anyhow::Result
}
for admin_login in seed_config.admins {
let user = fetch_github::<GithubUser>(
let user = fetch_github::<GitHubUser>(
&client,
&format!("https://api.github.com/users/{admin_login}"),
)
@@ -121,7 +119,7 @@ pub async fn seed(config: &Config, db: &Database, force: bool) -> anyhow::Result
if let Some(last_user_id) = last_user_id {
write!(&mut uri, "&since={}", last_user_id).unwrap();
}
let users = fetch_github::<Vec<GithubUser>>(&client, &uri).await;
let users = fetch_github::<Vec<GitHubUser>>(&client, &uri).await;
for github_user in users {
last_user_id = Some(github_user.id);
@@ -131,7 +129,7 @@ pub async fn seed(config: &Config, db: &Database, force: bool) -> anyhow::Result
&github_user.login,
github_user.id,
github_user.email.as_deref(),
github_user.created_at,
None,
None,
)
.await

View File

@@ -250,7 +250,6 @@ async fn test_channel_notes_participant_indices(
let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
// Clients A and B open the same file.
executor.start_waiting();
let editor_a = workspace_a
.update(cx_a, |workspace, cx| {
workspace.open_path((worktree_id_a, "file.txt"), None, true, cx)
@@ -259,7 +258,6 @@ async fn test_channel_notes_participant_indices(
.unwrap()
.downcast::<Editor>()
.unwrap();
executor.start_waiting();
let editor_b = workspace_b
.update(cx_b, |workspace, cx| {
workspace.open_path((worktree_id_a, "file.txt"), None, true, cx)

View File

@@ -168,7 +168,7 @@ async fn test_channel_requires_zed_cla(cx_a: &mut TestAppContext, cx_b: &mut Tes
server
.app_state
.db
.get_or_create_user_by_github_account("user_b", 100, None, Utc::now(), None)
.get_or_create_user_by_github_account("user_b", 100, None, Some(Utc::now()), None)
.await
.unwrap();
@@ -266,7 +266,7 @@ async fn test_channel_requires_zed_cla(cx_a: &mut TestAppContext, cx_b: &mut Tes
server
.app_state
.db
.add_contributor("user_b", 100, None, Utc::now(), None)
.add_contributor("user_b", 100, None, Some(Utc::now()), None)
.await
.unwrap();

View File

@@ -1021,8 +1021,8 @@ async fn test_language_server_statuses(cx_a: &mut TestAppContext, cx_b: &mut Tes
});
executor.run_until_parked();
project_a.read_with(cx_a, |project, cx| {
let status = project.language_server_statuses(cx).next().unwrap().1;
project_a.read_with(cx_a, |project, _| {
let status = project.language_server_statuses().next().unwrap().1;
assert_eq!(status.name, "the-language-server");
assert_eq!(status.pending_work.len(), 1);
assert_eq!(
@@ -1038,8 +1038,8 @@ async fn test_language_server_statuses(cx_a: &mut TestAppContext, cx_b: &mut Tes
executor.run_until_parked();
let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
project_b.read_with(cx_b, |project, cx| {
let status = project.language_server_statuses(cx).next().unwrap().1;
project_b.read_with(cx_b, |project, _| {
let status = project.language_server_statuses().next().unwrap().1;
assert_eq!(status.name, "the-language-server");
});
@@ -1055,8 +1055,8 @@ async fn test_language_server_statuses(cx_a: &mut TestAppContext, cx_b: &mut Tes
});
executor.run_until_parked();
project_a.read_with(cx_a, |project, cx| {
let status = project.language_server_statuses(cx).next().unwrap().1;
project_a.read_with(cx_a, |project, _| {
let status = project.language_server_statuses().next().unwrap().1;
assert_eq!(status.name, "the-language-server");
assert_eq!(status.pending_work.len(), 1);
assert_eq!(
@@ -1065,8 +1065,8 @@ async fn test_language_server_statuses(cx_a: &mut TestAppContext, cx_b: &mut Tes
);
});
project_b.read_with(cx_b, |project, cx| {
let status = project.language_server_statuses(cx).next().unwrap().1;
project_b.read_with(cx_b, |project, _| {
let status = project.language_server_statuses().next().unwrap().1;
assert_eq!(status.name, "the-language-server");
assert_eq!(status.pending_work.len(), 1);
assert_eq!(

View File

@@ -4780,8 +4780,8 @@ async fn test_references(
// User is informed that a request is pending.
executor.run_until_parked();
project_b.read_with(cx_b, |project, cx| {
let status = project.language_server_statuses(cx).next().unwrap().1;
project_b.read_with(cx_b, |project, _| {
let status = project.language_server_statuses().next().unwrap().1;
assert_eq!(status.name, "my-fake-lsp-adapter");
assert_eq!(
status.pending_work.values().next().unwrap().message,
@@ -4811,7 +4811,7 @@ async fn test_references(
executor.run_until_parked();
project_b.read_with(cx_b, |project, cx| {
// User is informed that a request is no longer pending.
let status = project.language_server_statuses(cx).next().unwrap().1;
let status = project.language_server_statuses().next().unwrap().1;
assert!(status.pending_work.is_empty());
assert_eq!(references.len(), 3);
@@ -4838,8 +4838,8 @@ async fn test_references(
// User is informed that a request is pending.
executor.run_until_parked();
project_b.read_with(cx_b, |project, cx| {
let status = project.language_server_statuses(cx).next().unwrap().1;
project_b.read_with(cx_b, |project, _| {
let status = project.language_server_statuses().next().unwrap().1;
assert_eq!(status.name, "my-fake-lsp-adapter");
assert_eq!(
status.pending_work.values().next().unwrap().message,
@@ -4855,8 +4855,8 @@ async fn test_references(
// User is informed that the request is no longer pending.
executor.run_until_parked();
project_b.read_with(cx_b, |project, cx| {
let status = project.language_server_statuses(cx).next().unwrap().1;
project_b.read_with(cx_b, |project, _| {
let status = project.language_server_statuses().next().unwrap().1;
assert!(status.pending_work.is_empty());
});
}

View File

@@ -2,7 +2,6 @@ use crate::tests::TestServer;
use call::ActiveCall;
use fs::{FakeFs, Fs as _};
use gpui::{Context as _, TestAppContext};
use language::language_settings::all_language_settings;
use remote::SshSession;
use remote_server::HeadlessProject;
use serde_json::json;
@@ -30,9 +29,6 @@ async fn test_sharing_an_ssh_remote_project(
"/code",
json!({
"project1": {
".zed": {
"settings.json": r#"{"languages":{"Rust":{"language_servers":["override-rust-analyzer"]}}}"#
},
"README.md": "# project 1",
"src": {
"lib.rs": "fn one() -> usize { 1 }"
@@ -72,8 +68,6 @@ async fn test_sharing_an_ssh_remote_project(
assert_eq!(
worktree.paths().map(Arc::as_ref).collect::<Vec<_>>(),
vec![
Path::new(".zed"),
Path::new(".zed/settings.json"),
Path::new("README.md"),
Path::new("src"),
Path::new("src/lib.rs"),
@@ -94,18 +88,6 @@ async fn test_sharing_an_ssh_remote_project(
buffer.edit([(ix..ix + 1, "100")], None, cx);
});
executor.run_until_parked();
cx_b.read(|cx| {
let file = buffer_b.read(cx).file();
assert_eq!(
all_language_settings(file, cx)
.language(Some("Rust"))
.language_servers,
["override-rust-analyzer".into()]
)
});
project_b
.update(cx_b, |project, cx| project.save_buffer(buffer_b, cx))
.await

View File

@@ -301,7 +301,7 @@ impl TestServer {
dev_server_projects::init(client.clone(), cx);
settings::KeymapFile::load_asset(os_keymap, cx).unwrap();
language_model::LanguageModelRegistry::test(cx);
assistant::context_store::init(&client.clone().into());
assistant::context_store::init(&client);
});
client

View File

@@ -86,7 +86,7 @@ impl UserBackfiller {
&user.github_login,
github_user.id,
user.email_address.as_deref(),
github_user.created_at,
Some(github_user.created_at),
initial_channel_id,
)
.await?;

View File

@@ -1,5 +1,5 @@
use gpui::prelude::*;
use story::{Story, StoryItem, StorySection};
use story::{StoryContainer, StoryItem, StorySection};
use ui::prelude::*;
use crate::notifications::collab_notification::CollabNotification;
@@ -10,39 +10,41 @@ impl Render for CollabNotificationStory {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
let window_container = |width, height| div().w(px(width)).h(px(height));
Story::container()
.child(Story::title_for::<CollabNotification>())
.child(
StorySection::new().child(StoryItem::new(
"Incoming Call Notification",
window_container(400., 72.).child(
CollabNotification::new(
"https://avatars.githubusercontent.com/u/1486634?v=4",
Button::new("accept", "Accept"),
Button::new("decline", "Decline"),
)
.child(
v_flex()
.overflow_hidden()
.child(Label::new("maxdeviant is sharing a project in Zed")),
),
StoryContainer::new(
"CollabNotification Story",
"crates/collab_ui/src/notifications/stories/collab_notification.rs",
)
.child(
StorySection::new().child(StoryItem::new(
"Incoming Call Notification",
window_container(400., 72.).child(
CollabNotification::new(
"https://avatars.githubusercontent.com/u/1486634?v=4",
Button::new("accept", "Accept"),
Button::new("decline", "Decline"),
)
.child(
v_flex()
.overflow_hidden()
.child(Label::new("maxdeviant is sharing a project in Zed")),
),
)),
)
.child(
StorySection::new().child(StoryItem::new(
"Project Shared Notification",
window_container(400., 72.).child(
CollabNotification::new(
"https://avatars.githubusercontent.com/u/1714999?v=4",
Button::new("open", "Open"),
Button::new("dismiss", "Dismiss"),
)
.child(Label::new("iamnbutler"))
.child(Label::new("is sharing a project in Zed:"))
.child(Label::new("zed")),
),
)),
)
),
)),
)
.child(
StorySection::new().child(StoryItem::new(
"Project Shared Notification",
window_container(400., 72.).child(
CollabNotification::new(
"https://avatars.githubusercontent.com/u/1714999?v=4",
Button::new("open", "Open"),
Button::new("dismiss", "Dismiss"),
)
.child(Label::new("iamnbutler"))
.child(Label::new("is sharing a project in Zed:"))
.child(Label::new("zed")),
),
)),
)
}
}

View File

@@ -39,7 +39,6 @@ pub struct ServerConfig {
pub id: String,
pub executable: String,
pub args: Vec<String>,
pub env: Option<HashMap<String, String>>,
}
impl Settings for ContextServerSettings {
@@ -71,13 +70,13 @@ impl ContextServer {
}
async fn start(&self, cx: &AsyncAppContext) -> anyhow::Result<()> {
log::info!("starting context server {}", self.config.id,);
log::info!("starting context server {}", self.config.id);
let client = Client::new(
client::ContextServerId(self.config.id.clone()),
client::ModelContextServerBinary {
executable: Path::new(&self.config.executable).to_path_buf(),
args: self.config.args.clone(),
env: self.config.env.clone(),
env: None,
},
cx.clone(),
)?;

View File

@@ -127,35 +127,6 @@ impl InitializedContextServerProtocol {
Ok(response)
}
pub async fn completion<P: Into<String>>(
&self,
reference: types::CompletionReference,
argument: P,
value: P,
) -> Result<types::Completion> {
let params = types::CompletionCompleteParams {
r#ref: reference,
argument: types::CompletionArgument {
name: argument.into(),
value: value.into(),
},
};
let result: types::CompletionCompleteResponse = self
.inner
.request(types::RequestType::CompletionComplete.as_str(), params)
.await?;
let completion = types::Completion {
values: result.completion.values,
total: types::CompletionTotal::from_options(
result.completion.has_more,
result.completion.total,
),
};
Ok(completion)
}
}
impl InitializedContextServerProtocol {

View File

@@ -14,7 +14,6 @@ pub enum RequestType {
LoggingSetLevel,
PromptsGet,
PromptsList,
CompletionComplete,
}
impl RequestType {
@@ -29,7 +28,6 @@ impl RequestType {
RequestType::LoggingSetLevel => "logging/setLevel",
RequestType::PromptsGet => "prompts/get",
RequestType::PromptsList => "prompts/list",
RequestType::CompletionComplete => "completion/complete",
}
}
}
@@ -80,50 +78,6 @@ pub struct PromptsGetParams {
pub arguments: Option<HashMap<String, String>>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CompletionCompleteParams {
pub r#ref: CompletionReference,
pub argument: CompletionArgument,
}
#[derive(Debug, Serialize)]
#[serde(untagged)]
pub enum CompletionReference {
Prompt(PromptReference),
Resource(ResourceReference),
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PromptReference {
pub r#type: PromptReferenceType,
pub name: String,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum PromptReferenceType {
#[serde(rename = "ref/prompt")]
Prompt,
#[serde(rename = "ref/resource")]
Resource,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ResourceReference {
pub r#type: String,
pub uri: String,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CompletionArgument {
pub name: String,
pub value: String,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct InitializeResponse {
@@ -158,20 +112,6 @@ pub struct PromptsListResponse {
pub prompts: Vec<PromptInfo>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CompletionCompleteResponse {
pub completion: CompletionResult,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CompletionResult {
pub values: Vec<String>,
pub total: Option<u32>,
pub has_more: Option<bool>,
}
#[derive(Debug, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct PromptInfo {
@@ -293,26 +233,3 @@ pub struct ProgressParams {
pub progress: f64,
pub total: Option<f64>,
}
// Helper Types that don't map directly to the protocol
pub enum CompletionTotal {
Exact(u32),
HasMore,
Unknown,
}
impl CompletionTotal {
pub fn from_options(has_more: Option<bool>, total: Option<u32>) -> Self {
match (has_more, total) {
(_, Some(count)) => CompletionTotal::Exact(count),
(Some(true), _) => CompletionTotal::HasMore,
_ => CompletionTotal::Unknown,
}
}
}
pub struct Completion {
pub values: Vec<String>,
pub total: CompletionTotal,
}

View File

@@ -60,14 +60,13 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
let language_server_id = LanguageServerId(0);
let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let cx = &mut VisualTestContext::from_window(*window, cx);
let workspace = window.root(cx).unwrap();
// Create some diagnostics
lsp_store.update(cx, |lsp_store, cx| {
lsp_store
project.update(cx, |project, cx| {
project
.update_diagnostic_entries(
language_server_id,
PathBuf::from("/test/main.rs"),
@@ -216,9 +215,9 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
});
// Diagnostics are added for another earlier path.
lsp_store.update(cx, |lsp_store, cx| {
lsp_store.disk_based_diagnostics_started(language_server_id, cx);
lsp_store
project.update(cx, |project, cx| {
project.disk_based_diagnostics_started(language_server_id, cx);
project
.update_diagnostic_entries(
language_server_id,
PathBuf::from("/test/consts.rs"),
@@ -237,7 +236,7 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
cx,
)
.unwrap();
lsp_store.disk_based_diagnostics_finished(language_server_id, cx);
project.disk_based_diagnostics_finished(language_server_id, cx);
});
view.next_notification(cx).await;
@@ -315,9 +314,9 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
});
// Diagnostics are added to the first path
lsp_store.update(cx, |lsp_store, cx| {
lsp_store.disk_based_diagnostics_started(language_server_id, cx);
lsp_store
project.update(cx, |project, cx| {
project.disk_based_diagnostics_started(language_server_id, cx);
project
.update_diagnostic_entries(
language_server_id,
PathBuf::from("/test/consts.rs"),
@@ -349,7 +348,7 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
cx,
)
.unwrap();
lsp_store.disk_based_diagnostics_finished(language_server_id, cx);
project.disk_based_diagnostics_finished(language_server_id, cx);
});
view.next_notification(cx).await;
@@ -450,7 +449,6 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
let server_id_1 = LanguageServerId(100);
let server_id_2 = LanguageServerId(101);
let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let cx = &mut VisualTestContext::from_window(*window, cx);
let workspace = window.root(cx).unwrap();
@@ -461,10 +459,10 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
let editor = view.update(cx, |view, _| view.editor.clone());
// Two language servers start updating diagnostics
lsp_store.update(cx, |lsp_store, cx| {
lsp_store.disk_based_diagnostics_started(server_id_1, cx);
lsp_store.disk_based_diagnostics_started(server_id_2, cx);
lsp_store
project.update(cx, |project, cx| {
project.disk_based_diagnostics_started(server_id_1, cx);
project.disk_based_diagnostics_started(server_id_2, cx);
project
.update_diagnostic_entries(
server_id_1,
PathBuf::from("/test/main.js"),
@@ -486,8 +484,8 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
});
// The first language server finishes
lsp_store.update(cx, |lsp_store, cx| {
lsp_store.disk_based_diagnostics_finished(server_id_1, cx);
project.update(cx, |project, cx| {
project.disk_based_diagnostics_finished(server_id_1, cx);
});
// Only the first language server's diagnostics are shown.
@@ -513,8 +511,8 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
);
// The second language server finishes
lsp_store.update(cx, |lsp_store, cx| {
lsp_store
project.update(cx, |project, cx| {
project
.update_diagnostic_entries(
server_id_2,
PathBuf::from("/test/main.js"),
@@ -533,7 +531,7 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
cx,
)
.unwrap();
lsp_store.disk_based_diagnostics_finished(server_id_2, cx);
project.disk_based_diagnostics_finished(server_id_2, cx);
});
// Both language server's diagnostics are shown.
@@ -568,10 +566,10 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
);
// Both language servers start updating diagnostics, and the first server finishes.
lsp_store.update(cx, |lsp_store, cx| {
lsp_store.disk_based_diagnostics_started(server_id_1, cx);
lsp_store.disk_based_diagnostics_started(server_id_2, cx);
lsp_store
project.update(cx, |project, cx| {
project.disk_based_diagnostics_started(server_id_1, cx);
project.disk_based_diagnostics_started(server_id_2, cx);
project
.update_diagnostic_entries(
server_id_1,
PathBuf::from("/test/main.js"),
@@ -590,7 +588,7 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
cx,
)
.unwrap();
lsp_store
project
.update_diagnostic_entries(
server_id_2,
PathBuf::from("/test/main.rs"),
@@ -599,7 +597,7 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
cx,
)
.unwrap();
lsp_store.disk_based_diagnostics_finished(server_id_1, cx);
project.disk_based_diagnostics_finished(server_id_1, cx);
});
// Only the first language server's diagnostics are updated.
@@ -635,8 +633,8 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
);
// The second language server finishes.
lsp_store.update(cx, |lsp_store, cx| {
lsp_store
project.update(cx, |project, cx| {
project
.update_diagnostic_entries(
server_id_2,
PathBuf::from("/test/main.js"),
@@ -655,7 +653,7 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
cx,
)
.unwrap();
lsp_store.disk_based_diagnostics_finished(server_id_2, cx);
project.disk_based_diagnostics_finished(server_id_2, cx);
});
// Both language servers' diagnostics are updated.
@@ -703,7 +701,6 @@ async fn test_random_diagnostics(cx: &mut TestAppContext, mut rng: StdRng) {
fs.insert_tree("/test", json!({})).await;
let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let cx = &mut VisualTestContext::from_window(*window, cx);
let workspace = window.root(cx).unwrap();
@@ -734,8 +731,8 @@ async fn test_random_diagnostics(cx: &mut TestAppContext, mut rng: StdRng) {
0..=20 if !updated_language_servers.is_empty() => {
let server_id = *updated_language_servers.iter().choose(&mut rng).unwrap();
log::info!("finishing diagnostic check for language server {server_id}");
lsp_store.update(cx, |lsp_store, cx| {
lsp_store.disk_based_diagnostics_finished(server_id, cx)
project.update(cx, |project, cx| {
project.disk_based_diagnostics_finished(server_id, cx)
});
if rng.gen_bool(0.5) {

View File

@@ -22,7 +22,7 @@ impl Render for ToolbarControls {
|| editor
.project
.read(cx)
.language_servers_running_disk_based_diagnostics(cx)
.language_servers_running_disk_based_diagnostics()
.next()
.is_some();
}

View File

@@ -105,7 +105,7 @@ pub struct DisplayMap {
inlay_highlights: InlayHighlights,
/// A container for explicitly foldable ranges, which supersede indentation based fold range suggestions.
crease_map: CreaseMap,
pub(crate) fold_placeholder: FoldPlaceholder,
fold_placeholder: FoldPlaceholder,
pub clip_at_line_ends: bool,
pub(crate) masked: bool,
}

View File

@@ -89,12 +89,13 @@ pub use inline_completion_provider::*;
pub use items::MAX_TAB_TITLE_LEN;
use itertools::Itertools;
use language::{
char_kind,
language_settings::{self, all_language_settings, InlayHintSettings},
markdown, point_from_lsp, AutoindentMode, BracketPair, Buffer, Capability, CharKind, CodeLabel,
CursorShape, Diagnostic, Documentation, IndentKind, IndentSize, Language, OffsetRangeExt,
Point, Selection, SelectionGoal, TransactionId,
};
use language::{point_to_lsp, BufferRow, CharClassifier, Runnable, RunnableRange};
use language::{point_to_lsp, BufferRow, Runnable, RunnableRange};
use linked_editing_ranges::refresh_linked_ranges;
use task::{ResolvedTask, TaskTemplate, TaskVariables};
@@ -555,9 +556,6 @@ pub struct Editor {
hovered_link_state: Option<HoveredLinkState>,
inline_completion_provider: Option<RegisteredInlineCompletionProvider>,
active_inline_completion: Option<(Inlay, Option<Range<Anchor>>)>,
// enable_inline_completions is a switch that Vim can use to disable
// inline completions based on its mode.
enable_inline_completions: bool,
show_inline_completions_override: Option<bool>,
inlay_hint_cache: InlayHintCache,
expanded_hunks: ExpandedHunks,
@@ -1915,7 +1913,6 @@ impl Editor {
next_editor_action_id: EditorActionId::default(),
editor_actions: Rc::default(),
show_inline_completions_override: None,
enable_inline_completions: true,
custom_context_menu: None,
show_git_blame_gutter: false,
show_git_blame_inline: false,
@@ -2280,10 +2277,6 @@ impl Editor {
self.input_enabled = input_enabled;
}
pub fn set_inline_completions_enabled(&mut self, enabled: bool) {
self.enable_inline_completions = enabled;
}
pub fn set_autoindent(&mut self, autoindent: bool) {
if autoindent {
self.autoindent_mode = Some(AutoindentMode::EachLine);
@@ -2442,8 +2435,7 @@ impl Editor {
if let Some(completion_menu) = completion_menu {
let cursor_position = new_cursor_position.to_offset(buffer);
let (word_range, kind) =
buffer.surrounding_word(completion_menu.initial_position, true);
let (word_range, kind) = buffer.surrounding_word(completion_menu.initial_position);
if kind == Some(CharKind::Word)
&& word_range.to_inclusive().contains(&cursor_position)
{
@@ -3289,8 +3281,10 @@ impl Editor {
let start_anchor = snapshot.anchor_before(selection.start);
let is_word_char = text.chars().next().map_or(true, |char| {
let classifier = snapshot.char_classifier_at(start_anchor.to_offset(&snapshot));
classifier.is_word(char)
let scope = snapshot.language_scope_at(start_anchor.to_offset(&snapshot));
let kind = char_kind(&scope, char);
kind == CharKind::Word
});
if is_word_char {
@@ -3921,7 +3915,7 @@ impl Editor {
fn completion_query(buffer: &MultiBufferSnapshot, position: impl ToOffset) -> Option<String> {
let offset = position.to_offset(buffer);
let (word_range, kind) = buffer.surrounding_word(offset, true);
let (word_range, kind) = buffer.surrounding_word(offset);
if offset > word_range.start && kind == Some(CharKind::Word) {
Some(
buffer
@@ -4983,7 +4977,6 @@ impl Editor {
let (buffer, cursor_buffer_position) =
self.buffer.read(cx).text_anchor_for_position(cursor, cx)?;
if !user_requested
&& self.enable_inline_completions
&& !self.should_show_inline_completions(&buffer, cursor_buffer_position, cx)
{
self.discard_inline_completion(false, cx);
@@ -5004,9 +4997,7 @@ impl Editor {
let cursor = self.selections.newest_anchor().head();
let (buffer, cursor_buffer_position) =
self.buffer.read(cx).text_anchor_for_position(cursor, cx)?;
if !self.enable_inline_completions
|| !self.should_show_inline_completions(&buffer, cursor_buffer_position, cx)
{
if !self.should_show_inline_completions(&buffer, cursor_buffer_position, cx) {
return None;
}
@@ -10397,10 +10388,6 @@ impl Editor {
}
}
pub fn default_fold_placeholder(&self, cx: &AppContext) -> FoldPlaceholder {
self.display_map.read(cx).fold_placeholder.clone()
}
pub fn set_gutter_hovered(&mut self, hovered: bool, cx: &mut ViewContext<Self>) {
if hovered != self.gutter_hovered {
self.gutter_hovered = hovered;
@@ -12300,11 +12287,10 @@ fn snippet_completions(
};
let scope = language.map(|language| language.default_scope());
let classifier = CharClassifier::new(scope).for_completion(true);
let mut last_word = line_at
.chars()
.rev()
.take_while(|c| classifier.is_word(*c))
.take_while(|c| char_kind(&scope, *c) == CharKind::Word)
.collect::<String>();
last_word = last_word.chars().rev().collect();
let as_offset = text::ToOffset::to_offset(&buffer_position, &snapshot);
@@ -12435,11 +12421,8 @@ impl CompletionProvider for Model<Project> {
}
let buffer = buffer.read(cx);
let classifier = buffer
.snapshot()
.char_classifier_at(position)
.for_completion(true);
if trigger_in_words && classifier.is_word(char) {
let scope = buffer.snapshot().language_scope_at(position);
if trigger_in_words && char_kind(&scope, char) == CharKind::Word {
return true;
}

View File

@@ -23,7 +23,6 @@ pub struct EditorSettings {
pub multi_cursor_modifier: MultiCursorModifier,
pub redact_private_values: bool,
pub expand_excerpt_lines: u32,
pub middle_click_paste: bool,
#[serde(default)]
pub double_click_in_multibuffer: DoubleClickInMultibuffer,
pub search_wrap: bool,
@@ -234,11 +233,6 @@ pub struct EditorSettingsContent {
/// Default: 3
pub expand_excerpt_lines: Option<u32>,
/// Whether to enable middle-click paste on Linux
///
/// Default: true
pub middle_click_paste: Option<bool>,
/// What to do when multibuffer is double clicked in some of its excerpts
/// (parts of singleton buffers).
///

View File

@@ -41,12 +41,8 @@ use gpui::{
ViewContext, WeakView, WindowContext,
};
use itertools::Itertools;
use language::{
language_settings::{
IndentGuideBackgroundColoring, IndentGuideColoring, IndentGuideSettings,
ShowWhitespaceSetting,
},
ChunkRendererContext,
use language::language_settings::{
IndentGuideBackgroundColoring, IndentGuideColoring, IndentGuideSettings, ShowWhitespaceSetting,
};
use lsp::DiagnosticSeverity;
use multi_buffer::{Anchor, MultiBufferPoint, MultiBufferRow};
@@ -658,24 +654,22 @@ impl EditorElement {
}
#[cfg(target_os = "linux")]
if EditorSettings::get_global(cx).middle_click_paste {
if let Some(text) = cx.read_from_primary().and_then(|item| item.text()) {
let point_for_position =
position_map.point_for_position(text_hitbox.bounds, event.position);
let position = point_for_position.previous_valid;
if let Some(text) = cx.read_from_primary().and_then(|item| item.text()) {
let point_for_position =
position_map.point_for_position(text_hitbox.bounds, event.position);
let position = point_for_position.previous_valid;
editor.select(
SelectPhase::Begin {
position,
add: false,
click_count: 1,
},
cx,
);
editor.insert(&text, cx);
}
cx.stop_propagation()
editor.select(
SelectPhase::Begin {
position,
add: false,
click_count: 1,
},
cx,
);
editor.insert(&text, cx);
}
cx.stop_propagation()
}
}
@@ -1590,7 +1584,6 @@ impl EditorElement {
fn layout_run_indicators(
&self,
line_height: Pixels,
range: Range<DisplayRow>,
scroll_pixel_position: gpui::Point<Pixels>,
gutter_dimensions: &GutterDimensions,
gutter_hitbox: &Hitbox,
@@ -1614,20 +1607,16 @@ impl EditorElement {
} else {
None
};
editor
.tasks
.iter()
.filter_map(|(_, tasks)| {
let multibuffer_point = tasks.offset.0.to_point(&snapshot.buffer_snapshot);
let multibuffer_row = MultiBufferRow(multibuffer_point.row);
let display_row = multibuffer_point.to_display_point(snapshot).row();
if range.start > display_row || range.end < display_row {
return None;
}
if snapshot.is_line_folded(multibuffer_row) {
return None;
}
let display_row = multibuffer_point.to_display_point(snapshot).row();
let button = editor.render_run_indicator(
&self.style,
Some(display_row) == active_task_indicator_row,
@@ -1883,7 +1872,6 @@ impl EditorElement {
line_number_layouts: &[Option<ShapedLine>],
snapshot: &EditorSnapshot,
style: &EditorStyle,
editor_width: Pixels,
cx: &mut WindowContext,
) -> Vec<LineWithInvisibles> {
if rows.start >= rows.end {
@@ -1934,7 +1922,6 @@ impl EditorElement {
rows.len(),
line_number_layouts,
snapshot.mode,
editor_width,
cx,
)
}
@@ -1979,7 +1966,6 @@ impl EditorElement {
line_height: Pixels,
em_width: Pixels,
text_hitbox: &Hitbox,
editor_width: Pixels,
scroll_width: &mut Pixels,
resized_blocks: &mut HashMap<CustomBlockId, u32>,
cx: &mut WindowContext,
@@ -1995,7 +1981,7 @@ impl EditorElement {
line_layouts[align_to.row().minus(rows.start) as usize]
.x_for_index(align_to.column() as usize)
} else {
layout_line(align_to.row(), snapshot, &self.style, editor_width, cx)
layout_line(align_to.row(), snapshot, &self.style, cx)
.x_for_index(align_to.column() as usize)
};
@@ -2432,7 +2418,6 @@ impl EditorElement {
snapshot: &EditorSnapshot,
hitbox: &Hitbox,
text_hitbox: &Hitbox,
editor_width: Pixels,
scroll_width: &mut Pixels,
gutter_dimensions: &GutterDimensions,
em_width: Pixels,
@@ -2472,7 +2457,6 @@ impl EditorElement {
line_height,
em_width,
text_hitbox,
editor_width,
scroll_width,
&mut resized_blocks,
cx,
@@ -2516,7 +2500,6 @@ impl EditorElement {
line_height,
em_width,
text_hitbox,
editor_width,
scroll_width,
&mut resized_blocks,
cx,
@@ -2561,7 +2544,6 @@ impl EditorElement {
line_height,
em_width,
text_hitbox,
editor_width,
scroll_width,
&mut resized_blocks,
cx,
@@ -4371,7 +4353,6 @@ impl fmt::Debug for LineFragment {
}
impl LineWithInvisibles {
#[allow(clippy::too_many_arguments)]
fn from_chunks<'a>(
chunks: impl Iterator<Item = HighlightedChunk<'a>>,
text_style: &TextStyle,
@@ -4379,7 +4360,6 @@ impl LineWithInvisibles {
max_line_count: usize,
line_number_layouts: &[Option<ShapedLine>],
editor_mode: EditorMode,
text_width: Pixels,
cx: &mut WindowContext,
) -> Vec<Self> {
let mut layouts = Vec::with_capacity(max_line_count);
@@ -4434,10 +4414,7 @@ impl LineWithInvisibles {
AvailableSpace::MinContent
};
let mut element = (renderer.render)(&mut ChunkRendererContext {
context: cx,
max_width: text_width,
});
let mut element = (renderer.render)(cx);
let line_height = text_style.line_height_in_pixels(cx.rem_size());
let size = element.layout_as_root(
size(available_width, AvailableSpace::Definite(line_height)),
@@ -4894,7 +4871,6 @@ impl Element for EditorElement {
&[],
&editor_snapshot,
&style,
px(f32::MAX),
cx,
)
.pop()
@@ -5007,9 +4983,6 @@ impl Element for EditorElement {
};
let overscroll = size(em_width + right_margin, px(0.));
let editor_width =
text_width - gutter_dimensions.margin - overscroll.width - em_width;
snapshot = self.editor.update(cx, |editor, cx| {
editor.last_bounds = Some(bounds);
editor.gutter_dimensions = gutter_dimensions;
@@ -5018,6 +4991,8 @@ impl Element for EditorElement {
if matches!(editor.mode, EditorMode::AutoHeight { .. }) {
snapshot
} else {
let editor_width =
text_width - gutter_dimensions.margin - overscroll.width - em_width;
let wrap_width = match editor.soft_wrap_mode(cx) {
SoftWrap::None => None,
SoftWrap::PreferLine => {
@@ -5187,7 +5162,6 @@ impl Element for EditorElement {
&line_numbers,
&snapshot,
&self.style,
editor_width,
cx,
);
for line_with_invisibles in &line_layouts {
@@ -5197,8 +5171,7 @@ impl Element for EditorElement {
}
let longest_line_width =
layout_line(snapshot.longest_row(), &snapshot, &style, editor_width, cx)
.width;
layout_line(snapshot.longest_row(), &snapshot, &style, cx).width;
let mut scroll_width =
longest_line_width.max(max_visible_line_width) + overscroll.width;
@@ -5208,7 +5181,6 @@ impl Element for EditorElement {
&snapshot,
&hitbox,
&text_hitbox,
editor_width,
&mut scroll_width,
&gutter_dimensions,
em_width,
@@ -5494,7 +5466,6 @@ impl Element for EditorElement {
let test_indicators = if gutter_settings.runnables {
self.layout_run_indicators(
line_height,
start_row..end_row,
scroll_pixel_position,
&gutter_dimensions,
&gutter_hitbox,
@@ -5505,6 +5476,7 @@ impl Element for EditorElement {
} else {
Vec::new()
};
let close_indicators = self.layout_hunk_diff_close_indicators(
line_height,
scroll_pixel_position,
@@ -5980,22 +5952,12 @@ fn layout_line(
row: DisplayRow,
snapshot: &EditorSnapshot,
style: &EditorStyle,
text_width: Pixels,
cx: &mut WindowContext,
) -> LineWithInvisibles {
let chunks = snapshot.highlighted_chunks(row..row + DisplayRow(1), true, style);
LineWithInvisibles::from_chunks(
chunks,
&style.text,
MAX_LINE_LEN,
1,
&[],
snapshot.mode,
text_width,
cx,
)
.pop()
.unwrap()
LineWithInvisibles::from_chunks(chunks, &style.text, MAX_LINE_LEN, 1, &[], snapshot.mode, cx)
.pop()
.unwrap()
}
#[derive(Debug)]

View File

@@ -613,7 +613,7 @@ pub fn show_link_definition(
TriggerPoint::Text(trigger_anchor) => {
// If no symbol range returned from language server, use the surrounding word.
let (offset_range, _) =
snapshot.surrounding_word(*trigger_anchor, false);
snapshot.surrounding_word(*trigger_anchor);
RangeInEditor::Text(
snapshot.anchor_before(offset_range.start)
..snapshot.anchor_after(offset_range.end),

View File

@@ -835,7 +835,7 @@ fn new_update_task(
let query_range_failed =
|range: &Range<language::Anchor>, e: anyhow::Error, cx: &mut AsyncWindowContext| {
log::error!("inlay hint update task for range failed: {e:#?}");
log::error!("inlay hint update task for range {range:?} failed: {e:#}");
editor
.update(cx, |editor, cx| {
if let Some(task_ranges) = editor

View File

@@ -844,13 +844,7 @@ impl Item for Editor {
.unwrap_or_default(),
)
.map(|path| path.to_string_lossy().to_string())
.unwrap_or_else(|| {
if multibuffer.is_singleton() {
multibuffer.title(cx).to_string()
} else {
"untitled".to_string()
}
})
.unwrap_or_else(|| "untitled".to_string())
});
let settings = ThemeSettings::get_global(cx);
@@ -1225,7 +1219,7 @@ impl SearchableItem for Editor {
}
SeedQuerySetting::Selection => String::new(),
SeedQuerySetting::Always => {
let (range, kind) = snapshot.surrounding_word(selection.start, true);
let (range, kind) = snapshot.surrounding_word(selection.start);
if kind == Some(CharKind::Word) {
let text: String = snapshot.text_for_range(range).collect();
if !text.trim().is_empty() {

View File

@@ -2,7 +2,9 @@
//! in editor given a given motion (e.g. it handles converting a "move left" command into coordinates in editor). It is exposed mostly for use by vim crate.
use super::{Bias, DisplayPoint, DisplaySnapshot, SelectionGoal, ToDisplayPoint};
use crate::{scroll::ScrollAnchor, CharKind, DisplayRow, EditorStyle, RowExt, ToOffset, ToPoint};
use crate::{
char_kind, scroll::ScrollAnchor, CharKind, DisplayRow, EditorStyle, RowExt, ToOffset, ToPoint,
};
use gpui::{px, Pixels, WindowTextSystem};
use language::Point;
use multi_buffer::{MultiBufferRow, MultiBufferSnapshot};
@@ -262,10 +264,10 @@ pub fn line_end(
/// uppercase letter, lowercase letter, '_' character or language-specific word character (like '-' in CSS).
pub fn previous_word_start(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
let raw_point = point.to_point(map);
let classifier = map.buffer_snapshot.char_classifier_at(raw_point);
let scope = map.buffer_snapshot.language_scope_at(raw_point);
find_preceding_boundary_display_point(map, point, FindRange::MultiLine, |left, right| {
(classifier.kind(left) != classifier.kind(right) && !classifier.is_whitespace(right))
(char_kind(&scope, left) != char_kind(&scope, right) && !right.is_whitespace())
|| left == '\n'
})
}
@@ -275,14 +277,13 @@ pub fn previous_word_start(map: &DisplaySnapshot, point: DisplayPoint) -> Displa
/// lowerspace characters and uppercase characters.
pub fn previous_subword_start(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
let raw_point = point.to_point(map);
let classifier = map.buffer_snapshot.char_classifier_at(raw_point);
let scope = map.buffer_snapshot.language_scope_at(raw_point);
find_preceding_boundary_display_point(map, point, FindRange::MultiLine, |left, right| {
let is_word_start =
classifier.kind(left) != classifier.kind(right) && !right.is_whitespace();
let is_subword_start = classifier.is_word('-') && left == '-' && right != '-'
|| left == '_' && right != '_'
|| left.is_lowercase() && right.is_uppercase();
char_kind(&scope, left) != char_kind(&scope, right) && !right.is_whitespace();
let is_subword_start =
left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase();
is_word_start || is_subword_start || left == '\n'
})
}
@@ -291,10 +292,10 @@ pub fn previous_subword_start(map: &DisplaySnapshot, point: DisplayPoint) -> Dis
/// uppercase letter, lowercase letter, '_' character or language-specific word character (like '-' in CSS).
pub fn next_word_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
let raw_point = point.to_point(map);
let classifier = map.buffer_snapshot.char_classifier_at(raw_point);
let scope = map.buffer_snapshot.language_scope_at(raw_point);
find_boundary(map, point, FindRange::MultiLine, |left, right| {
(classifier.kind(left) != classifier.kind(right) && !classifier.is_whitespace(left))
(char_kind(&scope, left) != char_kind(&scope, right) && !left.is_whitespace())
|| right == '\n'
})
}
@@ -304,14 +305,13 @@ pub fn next_word_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint
/// lowerspace characters and uppercase characters.
pub fn next_subword_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
let raw_point = point.to_point(map);
let classifier = map.buffer_snapshot.char_classifier_at(raw_point);
let scope = map.buffer_snapshot.language_scope_at(raw_point);
find_boundary(map, point, FindRange::MultiLine, |left, right| {
let is_word_end =
(classifier.kind(left) != classifier.kind(right)) && !classifier.is_whitespace(left);
let is_subword_end = classifier.is_word('-') && left != '-' && right == '-'
|| left != '_' && right == '_'
|| left.is_lowercase() && right.is_uppercase();
(char_kind(&scope, left) != char_kind(&scope, right)) && !left.is_whitespace();
let is_subword_end =
left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase();
is_word_end || is_subword_end || right == '\n'
})
}
@@ -509,14 +509,14 @@ pub fn chars_before(
pub(crate) fn is_inside_word(map: &DisplaySnapshot, point: DisplayPoint) -> bool {
let raw_point = point.to_point(map);
let classifier = map.buffer_snapshot.char_classifier_at(raw_point);
let scope = map.buffer_snapshot.language_scope_at(raw_point);
let ix = map.clip_point(point, Bias::Left).to_offset(map, Bias::Left);
let text = &map.buffer_snapshot;
let next_char_kind = text.chars_at(ix).next().map(|c| classifier.kind(c));
let next_char_kind = text.chars_at(ix).next().map(|c| char_kind(&scope, c));
let prev_char_kind = text
.reversed_chars_at(ix)
.next()
.map(|c| classifier.kind(c));
.map(|c| char_kind(&scope, c));
prev_char_kind.zip(next_char_kind) == Some((CharKind::Word, CharKind::Word))
}
@@ -527,7 +527,7 @@ pub(crate) fn surrounding_word(
let position = map
.clip_point(position, Bias::Left)
.to_offset(map, Bias::Left);
let (range, _) = map.buffer_snapshot.surrounding_word(position, false);
let (range, _) = map.buffer_snapshot.surrounding_word(position);
let start = range
.start
.to_point(&map.buffer_snapshot)

View File

@@ -226,7 +226,6 @@ impl EditorLspTestContext {
..Default::default()
},
block_comment: Some(("<!-- ".into(), " -->".into())),
word_characters: ['-'].into_iter().collect(),
..Default::default()
},
Some(tree_sitter_html::language()),

View File

@@ -772,7 +772,6 @@ impl ExtensionStore {
pub fn uninstall_extension(&mut self, extension_id: Arc<str>, cx: &mut ModelContext<Self>) {
let extension_dir = self.installed_dir.join(extension_id.as_ref());
let work_dir = self.wasm_host.work_dir.join(extension_id.as_ref());
let fs = self.fs.clone();
match self.outstanding_operations.entry(extension_id.clone()) {
@@ -794,15 +793,6 @@ impl ExtensionStore {
}
});
fs.remove_dir(
&work_dir,
RemoveOptions {
recursive: true,
ignore_if_not_exists: true,
},
)
.await?;
fs.remove_dir(
&extension_dir,
RemoveOptions {

View File

@@ -181,14 +181,6 @@ path = "examples/input.rs"
name = "shadow"
path = "examples/shadow.rs"
[[example]]
name = "svg"
path = "examples/svg/svg.rs"
[[example]]
name = "text_wrapper"
path = "examples/text_wrapper.rs"
[[example]]
name = "opacity"
path = "examples/opacity.rs"

View File

@@ -5,7 +5,7 @@ for Rust, designed to support a wide variety of applications.
## Getting Started
GPUI is still in active development as we work on the Zed code editor and isn't yet on crates.io. You'll also need to use the latest version of stable Rust and be on macOS or Linux. Add the following to your `Cargo.toml`:
GPUI is still in active development as we work on the Zed code editor and isn't yet on crates.io. You'll also need to use the latest version of stable rust and be on macOS. Add the following to your Cargo.toml:
```toml
gpui = { git = "https://github.com/zed-industries/zed" }
@@ -13,33 +13,9 @@ gpui = { git = "https://github.com/zed-industries/zed" }
Everything in GPUI starts with an `App`. You can create one with `App::new()`, and kick off your application by passing a callback to `App::run()`. Inside this callback, you can create a new window with `AppContext::open_window()`, and register your first root view. See [gpui.rs](https://www.gpui.rs/) for a complete example.
### Dependencies
GPUI has various system dependencies that it needs in order to work.
#### macOS
On macOS, GPUI uses Metal for rendering. In order to use Metal, you need to do the following:
- Install [Xcode](https://apps.apple.com/us/app/xcode/id497799835?mt=12) from the macOS App Store, or from the [Apple Developer](https://developer.apple.com/download/all/) website. Note this requires a developer account.
> Ensure you launch XCode after installing, and install the macOS components, which is the default option.
- Install [Xcode command line tools](https://developer.apple.com/xcode/resources/)
```sh
xcode-select --install
```
- Ensure that the Xcode command line tools are using your newly installed copy of Xcode:
```sh
sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer
```
## The Big Picture
GPUI offers three different [registers](<https://en.wikipedia.org/wiki/Register_(sociolinguistics)>) depending on your needs:
GPUI offers three different [registers](https://en.wikipedia.org/wiki/Register_(sociolinguistics)) depending on your needs:
- State management and communication with Models. Whenever you need to store application state that communicates between different parts of your application, you'll want to use GPUI's models. Models are owned by GPUI and are only accessible through an owned smart pointer similar to an `Rc`. See the `app::model_context` module for more information.

View File

@@ -1,173 +0,0 @@
use std::{fs, path::PathBuf, time::Duration};
use gpui::*;
struct Assets {
base: PathBuf,
}
impl AssetSource for Assets {
fn load(&self, path: &str) -> Result<Option<std::borrow::Cow<'static, [u8]>>> {
fs::read(self.base.join(path))
.map(|data| Some(std::borrow::Cow::Owned(data)))
.map_err(|e| e.into())
}
fn list(&self, path: &str) -> Result<Vec<SharedString>> {
fs::read_dir(self.base.join(path))
.map(|entries| {
entries
.filter_map(|entry| {
entry
.ok()
.and_then(|entry| entry.file_name().into_string().ok())
.map(SharedString::from)
})
.collect()
})
.map_err(|e| e.into())
}
}
struct HelloWorld {
_task: Option<Task<()>>,
opacity: f32,
}
impl HelloWorld {
fn new(_: &mut ViewContext<Self>) -> Self {
Self {
_task: None,
opacity: 0.5,
}
}
fn change_opacity(&mut self, _: &ClickEvent, cx: &mut ViewContext<Self>) {
self.opacity = 0.0;
cx.notify();
self._task = Some(cx.spawn(|view, mut cx| async move {
loop {
Timer::after(Duration::from_secs_f32(0.05)).await;
let mut stop = false;
let _ = cx.update(|cx| {
view.update(cx, |view, cx| {
if view.opacity >= 1.0 {
stop = true;
return;
}
view.opacity += 0.1;
cx.notify();
})
});
if stop {
break;
}
}
()
}));
}
}
impl Render for HelloWorld {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
div()
.flex()
.flex_row()
.size_full()
.bg(rgb(0xE0E0E0))
.text_xl()
.child(
div()
.flex()
.size_full()
.justify_center()
.items_center()
.border_1()
.text_color(gpui::blue())
.child(div().child("This is background text.")),
)
.child(
div()
.id("panel")
.on_click(cx.listener(Self::change_opacity))
.absolute()
.top_8()
.left_8()
.right_8()
.bottom_8()
.opacity(self.opacity)
.flex()
.justify_center()
.items_center()
.bg(gpui::white())
.border_3()
.border_color(gpui::red())
.text_color(gpui::yellow())
.child(
div()
.flex()
.flex_col()
.gap_2()
.justify_center()
.items_center()
.size(px(300.))
.bg(gpui::blue())
.border_3()
.border_color(gpui::black())
.shadow(smallvec::smallvec![BoxShadow {
color: hsla(0.0, 0.0, 0.0, 0.5),
blur_radius: px(1.0),
spread_radius: px(5.0),
offset: point(px(10.0), px(10.0)),
}])
.child(img("image/app-icon.png").size_8())
.child("Opacity Panel (Click to test)")
.child(
div()
.id("deep-level-text")
.flex()
.justify_center()
.items_center()
.p_4()
.bg(gpui::black())
.text_color(gpui::white())
.text_decoration_2()
.text_decoration_wavy()
.text_decoration_color(gpui::red())
.child(format!("opacity: {:.1}", self.opacity)),
)
.child(
svg()
.path("image/arrow_circle.svg")
.text_color(gpui::black())
.text_2xl()
.size_8(),
)
.child("🎊✈️🎉🎈🎁🎂")
.child(img("image/black-cat-typing.gif").size_12()),
),
)
}
}
fn main() {
App::new()
.with_assets(Assets {
base: PathBuf::from("crates/gpui/examples"),
})
.run(|cx: &mut AppContext| {
let bounds = Bounds::centered(None, size(px(500.0), px(500.0)), cx);
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
..Default::default()
},
|cx| cx.new_view(HelloWorld::new),
)
.unwrap();
});
}

View File

@@ -1,240 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<!-- Retrieved from https://commons.wikimedia.org/wiki/Category:SVG_files#/media/File:Dragon_clip_art.svg -->
<!-- License: CC BY-SA 3.0 -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="273.55759"
height="297.81952"
id="svg2"
version="1.1"
inkscape:version="0.48.2 r9819"
sodipodi:docname="New document 1">
<defs
id="defs4" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.78061715"
inkscape:cx="112.41895"
inkscape:cy="87.896202"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:window-width="1024"
inkscape:window-height="712"
inkscape:window-x="-4"
inkscape:window-y="-4"
inkscape:window-maximized="1" />
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(12.737105,20.750754)">
<path
style="fill:#ff0000;stroke:#000000;stroke-width:1.20000005;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:2"
d="M 73.44622,98.502944 95.574248,123.45583 103.10719,106.5067 73.44622,75.904107 c -3.686689,-4.753593 -11.648074,-6.667124 -21.18641,-7.532946 2.554639,2.715713 6.322156,2.39923 6.591328,10.82861 0.824915,3.302369 -2.23752,5.956846 -8.945373,8.003754 C 38.790859,84.969312 29.325417,85.209295 20.7156,86.732716 10.937191,88.437808 3.3459273,92.876831 -1.4124273,100.85699 L 14.595082,96.148898 c 7.184903,-0.279702 11.921181,1.889221 10.3578,10.357802 -1.994238,5.38177 -5.541365,9.21065 -11.299418,10.82861 -13.89013312,3.69644 -14.20560941,10.40948 -17.4199368,16.47832 2.98162616,-2.35421 5.619737,-5.05194 12.2410366,-3.76648 C 12.049969,137.75238 6.2740844,144.2887 0,150.76275 c -8.2557969,11.86874 -11.815365,18.89318 -11.299418,29.19017 -3.600096,18.58594 5.3801474,23.59338 8.4745634,34.36906 C -3.7567158,203.36083 -1.3904441,200.64501 0,195.48962 l 7.0621365,-10.3578 c -0.8666538,6.37974 -2.2779903,12.39636 0.4708091,21.18641 12.3493154,22.89597 28.0783714,46.43569 15.0658914,64.50084 l 15.065891,-22.59883 c 5.315347,9.96887 6.398134,20.64317 25.894501,28.24854 l 26.836118,-1.41242 c 0,0 -66.854892,-105.46124 -67.79651,-107.34448 -0.941618,-1.88323 53.201428,-70.621364 53.201428,-70.621364 z"
id="path3790"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccccccccccccccccccccscc" />
<path
style="fill:#b4daff;fill-opacity:1;stroke:#000000;stroke-width:1.20000005;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:2"
d="M 142.47916,274.20076 160.07509,273.64393 96.045057,235.03758 65.913274,202.08095 58.851138,160.64975 90.866157,124.86825 68.26732,97.561326 c -12.595581,1.14506 -19.978043,4.375364 -26.36531,8.003754 -10.473991,7.25982 -17.726182,17.74144 -24.482073,28.71936 -6.479109,23.36435 -7.280245,49.16212 3.766473,80.03754 11.380348,21.89016 28.33967,38.20135 48.493337,51.31819 l 18.835517,9.59325 z"
id="path3788"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccccccccc" />
<path
style="fill:#ffa100;fill-opacity:1;stroke:#000000;stroke-width:1.20000005;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:2"
d="m 156.77943,275.05636 17.41994,-9.41619 0.94162,-16.47831 -25.4237,-4.70809 -12.71184,21.1864 z"
id="path2998"
inkscape:connector-curvature="0" />
<path
style="fill:#ffa100;fill-opacity:1;stroke:#000000;stroke-width:1.20000005;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:2"
d="m 149.24649,255.75318 c 0,0 6.12051,-17.41993 4.23728,-17.41993 -1.88324,0 -22.59884,-3.76648 -22.59884,-3.76648 l -17.89075,17.41994 24.95289,13.65346 z"
id="path3000"
inkscape:connector-curvature="0" />
<path
style="fill:#ffa100;fill-opacity:1;stroke:#000000;stroke-width:1.20000005;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:2"
d="m 95.103438,237.39163 21.186412,16.47832 16.00751,-9.88699 0.47081,-18.36156 -23.54046,-5.64971 0.47081,10.82861 z"
id="path3002"
inkscape:connector-curvature="0" />
<path
style="fill:#ffa100;fill-opacity:1;stroke:#000000;stroke-width:1.20000005;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:2"
d="m 90.866157,216.67603 2.824854,-11.77023 17.419939,12.71185 -1.88324,15.06589 -16.007508,5.1789 -20.244791,-18.36156 z"
id="path3004"
inkscape:connector-curvature="0" />
<path
style="fill:#ffa100;fill-opacity:1;stroke:#000000;stroke-width:1.20000005;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:2"
d="m 62.61761,202.55176 18.832364,-8.47457 13.653464,8.47457 -2.824854,16.0075 -19.303173,0 z"
id="path3006"
inkscape:connector-curvature="0" />
<path
style="fill:#ffa100;fill-opacity:1;stroke:#000000;stroke-width:1.20000005;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:2"
d="m 57.909519,180.42373 18.832364,0.47081 9.416183,10.3578 -5.178901,9.88699 -21.186409,-0.47081 z"
id="path3008"
inkscape:connector-curvature="0" />
<path
style="fill:#ffa100;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.20000005;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:2"
d="m 56.497092,159.70813 18.361555,2.82485 6.591327,13.18266 -8.474563,9.88699 -16.949128,-5.1789 z"
id="path3780"
inkscape:connector-curvature="0" />
<path
style="fill:#ffa100;fill-opacity:1;stroke:#000000;stroke-width:1.20000005;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:2"
d="m 89.924538,126.28068 c -9.664322,8.18228 -14.377332,17.78125 -10.357799,33.42745 l -13.182656,8.00375 -9.886991,-8.47457 c 3.769665,-18.09309 15.869815,-31.15434 33.427446,-32.95663 z"
id="path3786"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccc" />
<path
style="fill:#80afb8;fill-opacity:1;stroke:#000000;stroke-width:1.20000005;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:2"
d="M 133.77127,121.15445 C 102.29445,108.16921 71.200271,103.60211 38.733769,68.843895 58.122127,89.497163 77.85045,109.22179 101.42657,118.35923 L 69.081877,100.38995 c 11.499182,15.61467 23.598373,27.80948 42.327623,31.94538 -11.24421,1.21671 -20.380767,-7.051 -29.549472,-15.17405 24.733762,29.06649 40.923142,28.22765 55.105772,20.36517 -8.16571,-3.14828 -15.52424,-7.10373 -21.56313,-12.37883 z"
id="path3798"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccccc" />
<path
style="fill:#ffaf29;fill-opacity:1;stroke:#000000;stroke-width:1.20000005;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:2"
d="M 138.56307,117.95991 128.58014,122.3524 87.051152,98.792686 C 67.454225,70.758132 70.300802,54.846161 60.696216,35.301249 68.447691,43.355436 74.77626,52.358226 85.453883,58.461647 79.819169,46.517664 80.927543,37.270916 80.262759,27.314905 c 3.166784,15.761986 9.719966,24.751173 15.972689,34.34128 -1.47816,-6.276965 -3.350278,-13.144867 -2.795221,-16.372006 5.304337,8.158007 10.295783,16.503749 20.764493,21.56313 3.59095,0.562069 10.55437,5.339728 18.36859,11.180882 7.24446,7.245148 12.72056,16.848114 16.77133,28.351519 z"
id="path3808"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccccccccccc" />
<path
style="fill:#00d7d6;fill-opacity:1;stroke:#000000;stroke-width:1.20000005;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:2"
d="m 113.8054,124.34899 15.57338,-3.59386 12.37883,-4.7918 6.78839,-10.78157 -0.79863,-6.389074 17.17064,13.976104 14.37542,7.18771 9.58361,13.9761 14.77474,-1.99659 c 4.39249,1.57017 8.78497,2.92164 13.17746,7.58703 5.76188,6.36887 11.20761,14.0023 17.96928,16.37201 4.23518,-1.76668 8.10347,-4.0837 14.37542,-2.79522 5.61406,0.5029 9.02747,3.75662 10.38225,9.58361 2.60388,11.36945 -3.06132,11.71345 -6.38908,15.17405 0.037,9.48753 -3.51318,15.38783 -8.78497,19.56654 -1.67328,-6.90259 -4.18558,-12.12712 -7.98635,-14.77473 -13.4304,-10.55772 -26.90659,-1.42587 -40.33104,-8.38566 -17.65083,-10.24469 -27.78384,-26.45774 -37.93513,-31.14674 -9.67041,-1.16902 -16.20698,-3.38266 -22.76109,-5.59045 z"
id="path3810"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccccccccccccccccccc" />
<path
style="fill:none;stroke:#000000;stroke-width:2;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:2"
d="m 154.53576,133.53328 c 7.83618,0.93458 14.84203,2.00755 16.37201,3.99317 12.61186,7.01602 21.03384,26.20016 31.54606,30.34811 -3.00497,-5.51683 -3.48439,-10.71796 1.99658,-15.17405 7.22535,-3.03546 10.55836,-0.45702 14.77474,2.3959 6.02262,6.9248 11.74338,16.2645 18.76791,15.17406"
id="path3814"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccccc" />
<path
style="fill:none;stroke:#000000;stroke-width:2;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:2"
d="m 209.24222,167.47525 c 0.33375,-1.59661 0.12171,-3.55709 1.59727,-4.39249 0.98845,-1.35075 2.83593,-0.98344 4.39249,-1.19796 4.8086,3.66257 4.22462,5.91709 5.98975,8.78498"
id="path3816"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccc" />
<path
style="fill:none;stroke:#000000;stroke-width:1.20000005;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:2"
d="m 219.22515,144.31485 11.18088,11.18088 13.57679,8.78498"
id="path3818"
inkscape:connector-curvature="0" />
<path
style="fill:#80b7c4;fill-opacity:1;stroke:#000000;stroke-width:1.20000005;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:2"
d="m 146.94873,99.59132 c -8.40512,-14.108233 -7.32446,-24.976745 -9.98293,-38.334452 2.28959,7.959558 5.31902,15.17927 10.78157,19.96586 -5.86678,-10.515353 -3.24744,-21.030706 -1.99659,-31.546059 1.01072,9.17552 3.11766,18.229239 13.57679,26.354936 5.88809,4.549002 13.0362,8.257998 15.97269,14.774736 6.35347,8.175649 4.50901,17.892639 8.78497,23.160399 -1.99075,-8.86593 -0.24555,-12.75061 0.79864,-17.569958 0.68276,7.697158 4.15451,11.675658 7.98634,15.174058 l 0.39932,-15.174058 c 8.25592,15.027878 19.79844,10.971398 23.95903,28.750838 l -6.78839,8.38566 -9.58361,-1.19795 -6.38908,4.39249 -8.38566,-2.79522 -9.58361,-13.17747 z"
id="path3820"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccccccccccccc" />
<path
style="fill:#333333;stroke:#000000;stroke-width:1.20000005;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:2"
d="m 163.10163,110.79268 c -0.23573,2.97072 13.46423,4.4681 19.60954,10.89419 2.70861,3.95943 5.16753,8.73304 8.71536,13.07303 -5.87363,0.0346 -11.0482,1.74698 -18.05324,-0.93378 -7.43617,-4.47099 -6.44315,-11.29641 -9.33788,-18.98702 z"
id="path3822"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccccc" />
<path
style="fill:#ffcd71;fill-opacity:1;stroke:#000000;stroke-width:1.20000005;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:2"
d="m 169.40234,116.31052 c 0.19359,4.0262 -0.47444,12.71073 6.79118,16.41203 3.5528,1.27665 7.10559,1.14632 10.65839,0.94322 -2.15752,-3.14407 -3.90555,-6.28813 -5.565,-9.4322 -2.11396,-2.77299 -6.25107,-5.40146 -11.88457,-7.92305 z"
id="path3824"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccc" />
<path
style="fill:#009d80;fill-opacity:1;stroke:#000000;stroke-width:1.20000005;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:2"
d="m 181.19259,123.29035 c -3.74814,1.60851 -4.45804,5.32041 -4.99907,9.14923 2.28898,-1.15726 4.81391,-2.06949 5.65932,-8.11169 z"
id="path3826"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccc" />
<path
style="fill:#de4000;fill-opacity:1;stroke:#000000;stroke-width:1.20000005;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:2"
d="m 137.41517,138.64674 15.74549,16.46119 -4.77136,20.03971 c -5.06394,19.66324 4.09464,25.78123 11.9284,33.16095 10.22958,3.59957 20.40736,7.66534 31.25241,5.72563 -6.95999,-6.56919 -15.51616,-8.88191 -20.75542,-20.03971 -6.49636,-7.22625 -3.74543,-16.18636 -0.23857,-25.28821 4.60728,-11.28719 1.0716,-14.4314 -2.38568,-17.65403 -7.16108,-6.52548 -8.39426,-6.27621 -11.45126,-8.11131 -6.42685,-0.0251 -14.02518,-3.48676 -19.32401,-4.29422 z"
id="path3828"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccccccccc" />
<path
style="fill:none;stroke:#000000;stroke-width:2;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:2"
d="m 153.87636,142.94096 c 3.90631,-1.87096 9.69299,16.23198 6.67991,19.80114 -5.63668,6.67696 -2.80544,19.95511 -0.23857,16.22262"
id="path3830"
inkscape:connector-curvature="0"
sodipodi:nodetypes="csc" />
<path
style="fill:#ececec;stroke:#000000;stroke-width:1.20000005;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:2"
d="m 147.43503,184.21322 c 5.98074,4.73827 16.11122,1.17708 25.76534,-1.43141 -7.03019,7.0566 -13.84727,16.88359 -21.47112,16.22263 l -4.77136,-7.87275 z"
id="path3832"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccc" />
<path
style="fill:#f2f2f2;stroke:#000000;stroke-width:1.20000005;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:2"
d="m 173.67751,213.31852 8.11131,1.43141 c 6.06788,-12.85113 5.3157,-19.63998 5.9642,-27.67389 -3.126,10.99543 -11.45068,18.09185 -22.18683,23.37966 l 7.87275,4.5328 z"
id="path3834"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccccc" />
<path
style="fill:#ff6c00;fill-opacity:1;stroke:#000000;stroke-width:1.20000005;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:2"
d="m 134.31379,139.12387 5.48706,0.47714 13.12124,8.58844 -1.90854,21.70969 -3.10139,26.71962 20.51685,15.02978 24.09537,2.14712 c -7.59703,15.51076 -21.44479,13.73677 -24.33394,26.24248 1.27084,-6.44195 0.86471,-13.55469 5.00993,-18.84688 -8.47582,5.06966 -15.09323,4.5641 -22.42539,6.20277 4.22385,-2.68547 8.10205,-6.06224 11.68983,-10.01985 -5.80515,-0.60962 -11.61031,-2.01583 -17.41546,-4.5328 -8.64382,-4.18289 -9.62981,-5.93913 -11.9284,-8.11131 7.01942,0.86252 9.473,-1.31884 12.88267,-2.86281 -26.93647,-7.55398 -26.92199,-14.26574 -35.0695,-21.23256 l 20.51685,7.39561 c 4.4984,0.68777 6.63439,-1.32438 6.6799,-5.72563 -16.33876,-9.8522 -21.88964,-21.17548 -24.33393,-32.92239 0.59372,8.94131 30.81833,14.94779 28.38959,14.79122 -3.07926,-0.19851 -18.61897,-12.87576 -17.1769,-16.22262 4.13518,3.60847 8.27036,6.74347 12.40554,5.9642 4.34471,1.92284 4.85595,-1.2656 5.48706,-4.29423 -6.58133,-6.79299 -11.82628,-7.57225 -17.17689,-8.82701 z"
id="path3836"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccccccccccccccsccccc" />
<path
style="fill:#f2f2f2;stroke:#000000;stroke-width:1.20000005;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:2"
d="m 203.49851,177.05618 c -1.12284,5.42482 -3.73922,9.99619 -9.30416,12.88267 6.07286,-0.34494 18.50532,-3.68719 24.09537,-12.88267 z"
id="path3838"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccc" />
<path
style="fill:#ececec;stroke:#000000;stroke-width:1.20000005;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:2"
d="m 230.21812,178.96473 c 3.65804,1.99083 7.31609,3.07314 10.97413,9.30415 -6.98529,5.73834 -13.78235,11.6649 -26.95818,11.2127 8.7089,-1.5798 12.95652,-10.09936 15.98405,-20.51685 z"
id="path3840"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccc" />
<path
style="fill:#999999;stroke:#000000;stroke-width:1.20000005;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:2"
d="m 186.79875,168.22917 c -1.59045,3.42787 -3.18091,5.13539 -4.77136,7.3956 4.18355,-0.76297 7.96823,-1.79187 10.25842,-3.81708 z"
id="path3842"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccc" />
<path
style="fill:#ca0000;fill-opacity:1;stroke:#130e13;stroke-width:1.20000005;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:2"
d="m 155.78491,120.51557 c 0,0 -0.47714,-10.73556 -2.14712,-12.64411 -1.66997,-1.90854 -21.94825,-15.506918 -21.94825,-15.506918 -9.7295,-4.220532 -14.38334,-6.731213 -20.51685,-15.745488 2.90484,10.265925 7.92673,20.108438 17.1769,29.105296 -5.32802,-3.09071 -10.65604,-6.298919 -15.98406,-10.019856 2.26988,6.838946 5.33355,13.677896 15.74549,20.516846 10.6125,6.05766 18.85054,4.20042 27.67389,4.29423 z"
id="path3846"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cscccccc" />
<path
style="fill:#000000;stroke:#000000;stroke-width:1.20000005;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:2"
d="M 139.45292,84.438938 125.20773,71.693241 C 119.15983,58.804456 109.97182,43.346495 95.217856,23.334566 86.780901,17.513373 80.270569,10.150883 68.226968,7.2150074 60.058839,6.6898637 52.035767,7.0350652 43.110447,1.9667792 34.99844,-5.3656363 30.6958,-11.065466 28.115509,-16.027146 34.01411,-10.45225 40.197961,-4.0216063 45.359687,-0.65733498 52.482283,2.4482615 59.604878,2.589719 66.727474,3.466273 c 14.767928,1.2266725 22.683537,9.305664 32.239116,15.744685 16.18998,15.07105 25.9637,31.746167 35.98785,48.358675 -1.05571,-9.882878 -3.6074,-20.064949 -8.62209,-30.739623 -6.0246,-9.94338 -14.00687,-17.755415 -20.7603,-26.241142 C 95.75565,5.1501111 90.473401,1.2722747 75.724437,-6.2804367 70.112554,-8.4271577 66.785373,-15.143282 62.603866,-20.150754 l 13.120571,10.121583 c 12.287926,4.1420139 32.155153,15.4397069 38.237093,21.367786 12.3045,10.369959 16.369,17.443923 20.99291,24.741648 l 8.99696,25.116521 0.37488,16.494432 c 0,0 -7.87234,-18.743673 -7.87234,-17.244179 0,1.499494 2.99898,23.991901 2.99898,23.991901 z"
id="path3848"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccccccccccccccccccsc" />
<path
style="fill:#1a1a1a;stroke:#000000;stroke-width:1.20000005;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:2"
d="m 99.012729,134.48743 11.893421,6.24404 c -25.631941,9.87193 -51.204911,20.45152 -77.604572,21.11082 -8.62273,0.0124 -17.24546,-2.90897 -25.86819,-6.83871 L -8.3253946,139.83947 9.5147367,151.13822 c 7.9289473,3.73642 15.8578943,4.42414 23.7868413,5.94671 16.056118,-2.54274 32.112237,-4.68742 48.168355,-13.3801 z"
id="path3850"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccccc" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 20 KiB

View File

@@ -1,82 +0,0 @@
use std::path::PathBuf;
use gpui::*;
use std::fs;
struct Assets {
base: PathBuf,
}
impl AssetSource for Assets {
fn load(&self, path: &str) -> Result<Option<std::borrow::Cow<'static, [u8]>>> {
fs::read(self.base.join(path))
.map(|data| Some(std::borrow::Cow::Owned(data)))
.map_err(|err| err.into())
}
fn list(&self, path: &str) -> Result<Vec<SharedString>> {
fs::read_dir(self.base.join(path))
.map(|entries| {
entries
.filter_map(|entry| {
entry
.ok()
.and_then(|entry| entry.file_name().into_string().ok())
.map(SharedString::from)
})
.collect()
})
.map_err(|err| err.into())
}
}
struct SvgExample;
impl Render for SvgExample {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
div()
.flex()
.flex_row()
.size_full()
.justify_center()
.items_center()
.gap_8()
.bg(rgb(0xffffff))
.child(
svg()
.path("svg/dragon.svg")
.size_8()
.text_color(rgb(0xff0000)),
)
.child(
svg()
.path("svg/dragon.svg")
.size_8()
.text_color(rgb(0x00ff00)),
)
.child(
svg()
.path("svg/dragon.svg")
.size_8()
.text_color(rgb(0x0000ff)),
)
}
}
fn main() {
App::new()
.with_assets(Assets {
base: PathBuf::from("crates/gpui/examples"),
})
.run(|cx: &mut AppContext| {
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_view(|_cx| SvgExample),
)
.unwrap();
});
}

View File

@@ -13,36 +13,6 @@ impl Render for HelloWorld {
.p_2()
.gap_2()
.bg(gpui::white())
.child(
div()
.flex()
.flex_row()
.gap_2()
.child(
div()
.flex()
.border_1()
.border_color(gpui::red())
.text_ellipsis()
.child("longer text in flex 1"),
)
.child(
div()
.flex()
.border_1()
.border_color(gpui::red())
.text_ellipsis()
.child("short flex"),
)
.child(
div()
.overflow_hidden()
.border_1()
.border_color(gpui::red())
.text_ellipsis()
.child("A short text in normal div"),
),
)
.child(
div()
.text_xl()

View File

@@ -461,16 +461,6 @@ impl Hsla {
pub fn fade_out(&mut self, factor: f32) {
self.a *= 1.0 - factor.clamp(0., 1.);
}
/// Returns a new HSLA color with the same hue, saturation, and lightness, but with a modified alpha value.
pub fn opacity(&self, factor: f32) -> Self {
Hsla {
h: self.h,
s: self.s,
l: self.l,
a: self.a * factor.clamp(0., 1.),
}
}
}
impl From<Rgba> for Hsla {

View File

@@ -1,114 +0,0 @@
use crate::{rgb, Hsla, Rgba, WindowAppearance};
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
/// The appearance of the base gpui colors, used to style gpui elements
///
/// Varies based on the system's current [WindowAppearance].
pub enum DefaultThemeAppearance {
#[default]
/// Use the set of colors for light appearances
Light,
/// Use the set of colors for dark appearances
Dark,
}
impl From<WindowAppearance> for DefaultThemeAppearance {
fn from(appearance: WindowAppearance) -> Self {
match appearance {
WindowAppearance::Light | WindowAppearance::VibrantLight => Self::Light,
WindowAppearance::Dark | WindowAppearance::VibrantDark => Self::Dark,
}
}
}
/// Get the default colors for the given appearance
pub fn colors(appearance: DefaultThemeAppearance) -> DefaultColors {
match appearance {
DefaultThemeAppearance::Light => DefaultColors::light(),
DefaultThemeAppearance::Dark => DefaultColors::dark(),
}
}
/// A collection of colors
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct DefaultColors {
text: Rgba,
selected_text: Rgba,
background: Rgba,
disabled: Rgba,
selected: Rgba,
border: Rgba,
separator: Rgba,
container: Rgba,
}
impl DefaultColors {
/// Get the default light colors
pub fn dark() -> Self {
Self {
text: rgb(0xFFFFFF),
selected_text: rgb(0xFFFFFF),
disabled: rgb(0x565656),
selected: rgb(0x2457CA),
background: rgb(0x222222),
border: rgb(0x000000),
separator: rgb(0xD9D9D9),
container: rgb(0x262626),
}
}
/// Get the default dark colors
pub fn light() -> Self {
Self {
text: rgb(0x252525),
selected_text: rgb(0xFFFFFF),
background: rgb(0xFFFFFF),
disabled: rgb(0xB0B0B0),
selected: rgb(0x2A63D9),
border: rgb(0xD9D9D9),
separator: rgb(0xE6E6E6),
container: rgb(0xF4F5F5),
}
}
}
/// A default gpui color
#[derive(Debug, Clone, Copy, PartialEq, Eq, strum::EnumIter)]
pub enum DefaultColor {
/// Text color
Text,
/// Selected text color
SelectedText,
/// Background color
Background,
/// Disabled color
Disabled,
/// Selected color
Selected,
/// Border color
Border,
/// Separator color
Separator,
/// Container color
Container,
}
impl DefaultColor {
/// Get the Rgb color for the given color type
pub fn color(&self, colors: &DefaultColors) -> Rgba {
match self {
DefaultColor::Text => colors.text,
DefaultColor::SelectedText => colors.selected_text,
DefaultColor::Background => colors.background,
DefaultColor::Disabled => colors.disabled,
DefaultColor::Selected => colors.selected,
DefaultColor::Border => colors.border,
DefaultColor::Separator => colors.separator,
DefaultColor::Container => colors.container,
}
}
/// Get the Hsla color for the given color type
pub fn hsla(&self, colors: &DefaultColors) -> Hsla {
self.color(&colors).into()
}
}

View File

@@ -1500,44 +1500,35 @@ impl Interactivity {
return ((), element_state);
}
cx.with_element_opacity(style.opacity, |cx| {
style.paint(bounds, cx, |cx: &mut WindowContext| {
cx.with_text_style(style.text_style().cloned(), |cx| {
cx.with_content_mask(
style.overflow_mask(bounds, cx.rem_size()),
|cx| {
if let Some(hitbox) = hitbox {
#[cfg(debug_assertions)]
self.paint_debug_info(global_id, hitbox, &style, cx);
style.paint(bounds, cx, |cx: &mut WindowContext| {
cx.with_text_style(style.text_style().cloned(), |cx| {
cx.with_content_mask(style.overflow_mask(bounds, cx.rem_size()), |cx| {
if let Some(hitbox) = hitbox {
#[cfg(debug_assertions)]
self.paint_debug_info(global_id, hitbox, &style, cx);
if !cx.has_active_drag() {
if let Some(mouse_cursor) = style.mouse_cursor {
cx.set_cursor_style(mouse_cursor, hitbox);
}
}
if let Some(group) = self.group.clone() {
GroupHitboxes::push(group, hitbox.id, cx);
}
self.paint_mouse_listeners(
hitbox,
element_state.as_mut(),
cx,
);
self.paint_scroll_listener(hitbox, &style, cx);
if !cx.has_active_drag() {
if let Some(mouse_cursor) = style.mouse_cursor {
cx.set_cursor_style(mouse_cursor, hitbox);
}
}
self.paint_keyboard_listeners(cx);
f(&style, cx);
if let Some(group) = self.group.clone() {
GroupHitboxes::push(group, hitbox.id, cx);
}
if hitbox.is_some() {
if let Some(group) = self.group.as_ref() {
GroupHitboxes::pop(group, cx);
}
}
},
);
self.paint_mouse_listeners(hitbox, element_state.as_mut(), cx);
self.paint_scroll_listener(hitbox, &style, cx);
}
self.paint_keyboard_listeners(cx);
f(&style, cx);
if hitbox.is_some() {
if let Some(group) = self.group.as_ref() {
GroupHitboxes::pop(group, cx);
}
}
});
});
});

View File

@@ -1,7 +1,6 @@
mod anchored;
mod animation;
mod canvas;
mod common;
mod deferred;
mod div;
mod img;
@@ -14,7 +13,6 @@ mod uniform_list;
pub use anchored::*;
pub use animation::*;
pub use canvas::*;
pub use common::*;
pub use deferred::*;
pub use div::*;
pub use img::*;

View File

@@ -548,9 +548,7 @@ fn fs_mono_sprite(input: MonoSpriteVarying) -> @location(0) vec4<f32> {
struct PolychromeSprite {
order: u32,
pad: u32,
grayscale: u32,
opacity: f32,
bounds: Bounds,
content_mask: Bounds,
corner_radii: Corners,
@@ -594,7 +592,7 @@ fn fs_poly_sprite(input: PolySpriteVarying) -> @location(0) vec4<f32> {
let grayscale = dot(color.rgb, GRAYSCALE_FACTORS);
color = vec4<f32>(vec3<f32>(grayscale), sample.a);
}
return blend_color(color, sprite.opacity * saturate(0.5 - distance));
return blend_color(color, saturate(0.5 - distance));
}
// --- surfaces --- //

View File

@@ -385,7 +385,7 @@ fragment float4 polychrome_sprite_fragment(
color.g = grayscale;
color.b = grayscale;
}
color.a *= sprite.opacity * saturate(0.5 - distance);
color.a *= saturate(0.5 - distance);
return color;
}

View File

@@ -640,19 +640,16 @@ impl From<MonochromeSprite> for Primitive {
}
}
#[derive(Clone, Debug, PartialEq)]
#[derive(Clone, Debug, Eq, PartialEq)]
#[repr(C)]
pub(crate) struct PolychromeSprite {
pub order: DrawOrder,
pub pad: u32, // align to 8 bytes
pub grayscale: bool,
pub opacity: f32,
pub bounds: Bounds<ScaledPixels>,
pub content_mask: ContentMask<ScaledPixels>,
pub corner_radii: Corners<ScaledPixels>,
pub tile: AtlasTile,
}
impl Eq for PolychromeSprite {}
impl Ord for PolychromeSprite {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {

View File

@@ -234,9 +234,6 @@ pub struct Style {
/// The mouse cursor style shown when the mouse pointer is over an element.
pub mouse_cursor: Option<CursorStyle>,
/// The opacity of this element
pub opacity: Option<f32>,
/// Whether to draw a red debugging outline around this element
#[cfg(debug_assertions)]
pub debug: bool,
@@ -697,7 +694,6 @@ impl Default for Style {
box_shadow: Default::default(),
text: TextStyleRefinement::default(),
mouse_cursor: None,
opacity: None,
#[cfg(debug_assertions)]
debug: false,

View File

@@ -547,12 +547,6 @@ pub trait Styled: Sized {
self
}
/// Set opacity on this element and its children.
fn opacity(mut self, opacity: f32) -> Self {
self.style().opacity = Some(opacity);
self
}
/// Draw a debug border around this element.
#[cfg(debug_assertions)]
fn debug(mut self) -> Self {

View File

@@ -106,29 +106,18 @@ impl LineWrapper {
ellipsis: Option<&str>,
) -> SharedString {
let mut width = px(0.);
let mut ellipsis_width = px(0.);
if let Some(ellipsis) = ellipsis {
for c in ellipsis.chars() {
ellipsis_width += self.width_for_char(c);
width += self.width_for_char(c);
}
}
let mut char_indices = line.char_indices();
let mut truncate_ix = 0;
for (ix, c) in char_indices {
if width + ellipsis_width <= truncate_width {
truncate_ix = ix;
}
let char_width = self.width_for_char(c);
width += char_width;
if width.floor() > truncate_width {
return SharedString::from(format!(
"{}{}",
&line[..truncate_ix],
ellipsis.unwrap_or("")
));
if width > truncate_width {
return SharedString::from(format!("{}{}", &line[..ix], ellipsis.unwrap_or("")));
}
}

View File

@@ -520,7 +520,6 @@ pub struct Window {
pub(crate) element_id_stack: SmallVec<[ElementId; 32]>,
pub(crate) text_style_stack: Vec<TextStyleRefinement>,
pub(crate) element_offset_stack: Vec<Point<Pixels>>,
pub(crate) element_opacity: Option<f32>,
pub(crate) content_mask_stack: Vec<ContentMask<Pixels>>,
pub(crate) requested_autoscroll: Option<Bounds<Pixels>>,
pub(crate) rendered_frame: Frame,
@@ -800,7 +799,6 @@ impl Window {
text_style_stack: Vec::new(),
element_offset_stack: Vec::new(),
content_mask_stack: Vec::new(),
element_opacity: None,
requested_autoscroll: None,
rendered_frame: Frame::new(DispatchTree::new(cx.keymap.clone(), cx.actions.clone())),
next_frame: Frame::new(DispatchTree::new(cx.keymap.clone(), cx.actions.clone())),
@@ -1910,28 +1908,6 @@ impl<'a> WindowContext<'a> {
result
}
pub(crate) fn with_element_opacity<R>(
&mut self,
opacity: Option<f32>,
f: impl FnOnce(&mut Self) -> R,
) -> R {
if opacity.is_none() {
return f(self);
}
debug_assert!(
matches!(
self.window.draw_phase,
DrawPhase::Prepaint | DrawPhase::Paint
),
"this method can only be called during prepaint, or paint"
);
self.window_mut().element_opacity = opacity;
let result = f(self);
self.window_mut().element_opacity = None;
result
}
/// Perform prepaint on child elements in a "retryable" manner, so that any side effects
/// of prepaints can be discarded before prepainting again. This is used to support autoscroll
/// where we need to prepaint children to detect the autoscroll bounds, then adjust the
@@ -2045,19 +2021,6 @@ impl<'a> WindowContext<'a> {
.unwrap_or_default()
}
/// Obtain the current element opacity. This method should only be called during the
/// prepaint phase of element drawing.
pub(crate) fn element_opacity(&self) -> f32 {
debug_assert!(
matches!(
self.window.draw_phase,
DrawPhase::Prepaint | DrawPhase::Paint
),
"this method can only be called during prepaint, or paint"
);
self.window().element_opacity.unwrap_or(1.0)
}
/// Obtain the current content mask. This method should only be called during element drawing.
pub fn content_mask(&self) -> ContentMask<Pixels> {
debug_assert!(
@@ -2295,7 +2258,6 @@ impl<'a> WindowContext<'a> {
let scale_factor = self.scale_factor();
let content_mask = self.content_mask();
let opacity = self.element_opacity();
for shadow in shadows {
let mut shadow_bounds = bounds;
shadow_bounds.origin += shadow.offset;
@@ -2306,7 +2268,7 @@ impl<'a> WindowContext<'a> {
bounds: shadow_bounds.scale(scale_factor),
content_mask: content_mask.scale(scale_factor),
corner_radii: corner_radii.scale(scale_factor),
color: shadow.color.opacity(opacity),
color: shadow.color,
});
}
}
@@ -2325,14 +2287,13 @@ impl<'a> WindowContext<'a> {
let scale_factor = self.scale_factor();
let content_mask = self.content_mask();
let opacity = self.element_opacity();
self.window.next_frame.scene.insert_primitive(Quad {
order: 0,
pad: 0,
bounds: quad.bounds.scale(scale_factor),
content_mask: content_mask.scale(scale_factor),
background: quad.background.opacity(opacity),
border_color: quad.border_color.opacity(opacity),
background: quad.background,
border_color: quad.border_color,
corner_radii: quad.corner_radii.scale(scale_factor),
border_widths: quad.border_widths.scale(scale_factor),
});
@@ -2350,9 +2311,8 @@ impl<'a> WindowContext<'a> {
let scale_factor = self.scale_factor();
let content_mask = self.content_mask();
let opacity = self.element_opacity();
path.content_mask = content_mask;
path.color = color.into().opacity(opacity);
path.color = color.into();
self.window
.next_frame
.scene
@@ -2385,14 +2345,13 @@ impl<'a> WindowContext<'a> {
size: size(width, height),
};
let content_mask = self.content_mask();
let element_opacity = self.element_opacity();
self.window.next_frame.scene.insert_primitive(Underline {
order: 0,
pad: 0,
bounds: bounds.scale(scale_factor),
content_mask: content_mask.scale(scale_factor),
color: style.color.unwrap_or_default().opacity(element_opacity),
color: style.color.unwrap_or_default(),
thickness: style.thickness.scale(scale_factor),
wavy: style.wavy,
});
@@ -2420,7 +2379,6 @@ impl<'a> WindowContext<'a> {
size: size(width, height),
};
let content_mask = self.content_mask();
let opacity = self.element_opacity();
self.window.next_frame.scene.insert_primitive(Underline {
order: 0,
@@ -2428,7 +2386,7 @@ impl<'a> WindowContext<'a> {
bounds: bounds.scale(scale_factor),
content_mask: content_mask.scale(scale_factor),
thickness: style.thickness.scale(scale_factor),
color: style.color.unwrap_or_default().opacity(opacity),
color: style.color.unwrap_or_default(),
wavy: false,
});
}
@@ -2455,7 +2413,6 @@ impl<'a> WindowContext<'a> {
"this method can only be called during paint"
);
let element_opacity = self.element_opacity();
let scale_factor = self.scale_factor();
let glyph_origin = origin.scale(scale_factor);
let subpixel_variant = Point {
@@ -2494,7 +2451,7 @@ impl<'a> WindowContext<'a> {
pad: 0,
bounds,
content_mask,
color: color.opacity(element_opacity),
color,
tile,
transformation: TransformationMatrix::unit(),
});
@@ -2551,20 +2508,17 @@ impl<'a> WindowContext<'a> {
size: tile.bounds.size.map(Into::into),
};
let content_mask = self.content_mask().scale(scale_factor);
let opacity = self.element_opacity();
self.window
.next_frame
.scene
.insert_primitive(PolychromeSprite {
order: 0,
pad: 0,
grayscale: false,
bounds,
corner_radii: Default::default(),
content_mask,
tile,
opacity,
});
}
Ok(())
@@ -2586,7 +2540,6 @@ impl<'a> WindowContext<'a> {
"this method can only be called during paint"
);
let element_opacity = self.element_opacity();
let scale_factor = self.scale_factor();
let bounds = bounds.scale(scale_factor);
// Render the SVG at twice the size to get a higher quality result.
@@ -2621,7 +2574,7 @@ impl<'a> WindowContext<'a> {
.map_origin(|origin| origin.floor())
.map_size(|size| size.ceil()),
content_mask,
color: color.opacity(element_opacity),
color,
tile,
transformation,
});
@@ -2669,20 +2622,17 @@ impl<'a> WindowContext<'a> {
.expect("Callback above only returns Some");
let content_mask = self.content_mask().scale(scale_factor);
let corner_radii = corner_radii.scale(scale_factor);
let opacity = self.element_opacity();
self.window
.next_frame
.scene
.insert_primitive(PolychromeSprite {
order: 0,
pad: 0,
grayscale,
bounds,
content_mask,
corner_radii,
tile,
opacity,
});
Ok(())
}

View File

@@ -21,7 +21,7 @@ use async_watch as watch;
pub use clock::ReplicaId;
use futures::channel::oneshot;
use gpui::{
AnyElement, AppContext, EventEmitter, HighlightStyle, ModelContext, Pixels, Task, TaskLabel,
AnyElement, AppContext, EventEmitter, HighlightStyle, ModelContext, Task, TaskLabel,
WindowContext,
};
use lsp::LanguageServerId;
@@ -40,7 +40,7 @@ use std::{
future::Future,
iter::{self, Iterator, Peekable},
mem,
ops::{Deref, DerefMut, Range},
ops::{Deref, Range},
path::{Path, PathBuf},
str,
sync::{Arc, LazyLock},
@@ -486,16 +486,11 @@ pub struct Chunk<'a> {
#[derive(Clone)]
pub struct ChunkRenderer {
/// creates a custom element to represent this chunk.
pub render: Arc<dyn Send + Sync + Fn(&mut ChunkRendererContext) -> AnyElement>,
pub render: Arc<dyn Send + Sync + Fn(&mut WindowContext) -> AnyElement>,
/// If true, the element is constrained to the shaped width of the text.
pub constrain_width: bool,
}
pub struct ChunkRendererContext<'a, 'b> {
pub context: &'a mut WindowContext<'b>,
pub max_width: Pixels,
}
impl fmt::Debug for ChunkRenderer {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_struct("ChunkRenderer")
@@ -504,20 +499,6 @@ impl fmt::Debug for ChunkRenderer {
}
}
impl<'a, 'b> Deref for ChunkRendererContext<'a, 'b> {
type Target = WindowContext<'b>;
fn deref(&self) -> &Self::Target {
self.context
}
}
impl<'a, 'b> DerefMut for ChunkRendererContext<'a, 'b> {
fn deref_mut(&mut self) -> &mut Self::Target {
self.context
}
}
/// A set of edits to a given version of a buffer, computed asynchronously.
#[derive(Debug)]
pub struct Diff {
@@ -1018,7 +999,7 @@ impl Buffer {
let offset = position.to_offset(self);
self.syntax_map
.lock()
.layers_for_range(offset..offset, &self.text, false)
.layers_for_range(offset..offset, &self.text)
.last()
.map(|info| info.language.clone())
.or_else(|| self.language.clone())
@@ -1591,13 +1572,12 @@ impl Buffer {
/// Checks if the buffer has unsaved changes.
pub fn is_dirty(&self) -> bool {
self.capability != Capability::ReadOnly
&& (self.has_conflict
|| self.has_unsaved_edits()
|| self
.file
.as_ref()
.map_or(false, |file| file.is_deleted() || !file.is_created()))
self.has_conflict
|| self.has_unsaved_edits()
|| self
.file
.as_ref()
.map_or(false, |file| file.is_deleted() || !file.is_created())
}
/// Checks if the buffer and its file have both changed since the buffer
@@ -2626,14 +2606,13 @@ impl BufferSnapshot {
/// Iterates over every [`SyntaxLayer`] in the buffer.
pub fn syntax_layers(&self) -> impl Iterator<Item = SyntaxLayer> + '_ {
self.syntax
.layers_for_range(0..self.len(), &self.text, true)
self.syntax.layers_for_range(0..self.len(), &self.text)
}
pub fn syntax_layer_at<D: ToOffset>(&self, position: D) -> Option<SyntaxLayer> {
let offset = position.to_offset(self);
self.syntax
.layers_for_range(offset..offset, &self.text, false)
.layers_for_range(offset..offset, &self.text)
.filter(|l| l.node().end_byte() > offset)
.last()
}
@@ -2659,10 +2638,6 @@ impl BufferSnapshot {
language_settings(self.language_at(position), self.file.as_ref(), cx)
}
pub fn char_classifier_at<T: ToOffset>(&self, point: T) -> CharClassifier {
CharClassifier::new(self.language_scope_at(point))
}
/// Returns the [LanguageScope] at the given location.
pub fn language_scope_at<D: ToOffset>(&self, position: D) -> Option<LanguageScope> {
let offset = position.to_offset(self);
@@ -2670,10 +2645,7 @@ impl BufferSnapshot {
let mut smallest_range: Option<Range<usize>> = None;
// Use the layer that has the smallest node intersecting the given point.
for layer in self
.syntax
.layers_for_range(offset..offset, &self.text, false)
{
for layer in self.syntax.layers_for_range(offset..offset, &self.text) {
let mut cursor = layer.node().walk();
let mut range = None;
@@ -2719,14 +2691,15 @@ impl BufferSnapshot {
let mut next_chars = self.chars_at(start).peekable();
let mut prev_chars = self.reversed_chars_at(start).peekable();
let classifier = self.char_classifier_at(start);
let scope = self.language_scope_at(start);
let kind = |c| char_kind(&scope, c);
let word_kind = cmp::max(
prev_chars.peek().copied().map(|c| classifier.kind(c)),
next_chars.peek().copied().map(|c| classifier.kind(c)),
prev_chars.peek().copied().map(kind),
next_chars.peek().copied().map(kind),
);
for ch in prev_chars {
if Some(classifier.kind(ch)) == word_kind && ch != '\n' {
if Some(kind(ch)) == word_kind && ch != '\n' {
start -= ch.len_utf8();
} else {
break;
@@ -2734,7 +2707,7 @@ impl BufferSnapshot {
}
for ch in next_chars {
if Some(classifier.kind(ch)) == word_kind && ch != '\n' {
if Some(kind(ch)) == word_kind && ch != '\n' {
end += ch.len_utf8();
} else {
break;
@@ -2748,10 +2721,7 @@ impl BufferSnapshot {
pub fn range_for_syntax_ancestor<T: ToOffset>(&self, range: Range<T>) -> Option<Range<usize>> {
let range = range.start.to_offset(self)..range.end.to_offset(self);
let mut result: Option<Range<usize>> = None;
'outer: for layer in self
.syntax
.layers_for_range(range.clone(), &self.text, true)
{
'outer: for layer in self.syntax.layers_for_range(range.clone(), &self.text) {
let mut cursor = layer.node().walk();
// Descend to the first leaf that touches the start of the range,
@@ -4218,72 +4188,25 @@ pub(crate) fn contiguous_ranges(
})
}
#[derive(Default, Debug)]
pub struct CharClassifier {
scope: Option<LanguageScope>,
for_completion: bool,
ignore_punctuation: bool,
}
impl CharClassifier {
pub fn new(scope: Option<LanguageScope>) -> Self {
Self {
scope,
for_completion: false,
ignore_punctuation: false,
}
/// Returns the [CharKind] for the given character. When a scope is provided,
/// the function checks if the character is considered a word character
/// based on the language scope's word character settings.
pub fn char_kind(scope: &Option<LanguageScope>, c: char) -> CharKind {
if c.is_whitespace() {
return CharKind::Whitespace;
} else if c.is_alphanumeric() || c == '_' {
return CharKind::Word;
}
pub fn for_completion(self, for_completion: bool) -> Self {
Self {
for_completion,
..self
}
}
pub fn ignore_punctuation(self, ignore_punctuation: bool) -> Self {
Self {
ignore_punctuation,
..self
}
}
pub fn is_whitespace(&self, c: char) -> bool {
self.kind(c) == CharKind::Whitespace
}
pub fn is_word(&self, c: char) -> bool {
self.kind(c) == CharKind::Word
}
pub fn is_punctuation(&self, c: char) -> bool {
self.kind(c) == CharKind::Punctuation
}
pub fn kind(&self, c: char) -> CharKind {
if c.is_whitespace() {
return CharKind::Whitespace;
} else if c.is_alphanumeric() || c == '_' {
return CharKind::Word;
}
if let Some(scope) = &self.scope {
if let Some(characters) = scope.word_characters() {
if characters.contains(&c) {
if c == '-' && !self.for_completion && !self.ignore_punctuation {
return CharKind::Punctuation;
}
return CharKind::Word;
}
if let Some(scope) = scope {
if let Some(characters) = scope.word_characters() {
if characters.contains(&c) {
return CharKind::Word;
}
}
if self.ignore_punctuation {
CharKind::Word
} else {
CharKind::Punctuation
}
}
CharKind::Punctuation
}
/// Find all of the ranges of whitespace that occur at the ends of lines

View File

@@ -2216,45 +2216,6 @@ fn test_language_scope_at_with_combined_injections(cx: &mut AppContext) {
});
}
#[gpui::test]
fn test_language_at_with_hidden_languages(cx: &mut AppContext) {
init_settings(cx, |_| {});
cx.new_model(|cx| {
let text = r#"
this is an *emphasized* word.
"#
.unindent();
let language_registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
language_registry.add(Arc::new(markdown_lang()));
language_registry.add(Arc::new(markdown_inline_lang()));
let mut buffer = Buffer::local(text, cx);
buffer.set_language_registry(language_registry.clone());
buffer.set_language(
language_registry
.language_for_name("Markdown")
.now_or_never()
.unwrap()
.ok(),
cx,
);
let snapshot = buffer.snapshot();
for point in [Point::new(0, 4), Point::new(0, 16)] {
let config = snapshot.language_scope_at(point).unwrap();
assert_eq!(config.language_name().as_ref(), "Markdown");
let language = snapshot.language_at(point).unwrap();
assert_eq!(language.name().as_ref(), "Markdown");
}
buffer
});
}
#[gpui::test]
fn test_serialization(cx: &mut gpui::AppContext) {
let mut now = Instant::now();
@@ -2907,45 +2868,6 @@ fn javascript_lang() -> Language {
.unwrap()
}
pub fn markdown_lang() -> Language {
Language::new(
LanguageConfig {
name: "Markdown".into(),
matcher: LanguageMatcher {
path_suffixes: vec!["md".into()],
..Default::default()
},
..Default::default()
},
Some(tree_sitter_md::language()),
)
.with_injection_query(
r#"
(fenced_code_block
(info_string
(language) @language)
(code_fence_content) @content)
((inline) @content
(#set! "language" "markdown-inline"))
"#,
)
.unwrap()
}
pub fn markdown_inline_lang() -> Language {
Language::new(
LanguageConfig {
name: "Markdown-Inline".into(),
hidden: true,
..LanguageConfig::default()
},
Some(tree_sitter_md::inline_language()),
)
.with_highlights_query("(emphasis) @emphasis")
.unwrap()
}
fn get_tree_sexp(buffer: &Model<Buffer>, cx: &mut gpui::TestAppContext) -> String {
buffer.update(cx, |buffer, _| {
let snapshot = buffer.snapshot();

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