Compare commits
8 Commits
telemetry-
...
tool-call-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c6724d598e | ||
|
|
61a516e95f | ||
|
|
eb1754a091 | ||
|
|
2386595de5 | ||
|
|
b36ed56443 | ||
|
|
1b72c5402d | ||
|
|
a143fdc630 | ||
|
|
1e9666649e |
24
.github/workflows/ci.yml
vendored
24
.github/workflows/ci.yml
vendored
@@ -244,8 +244,8 @@ jobs:
|
||||
#
|
||||
# 25 was chosen arbitrarily.
|
||||
fetch-depth: 25
|
||||
fetch-tags: true
|
||||
clean: false
|
||||
ref: ${{ github.ref }}
|
||||
|
||||
- name: Limit target directory size
|
||||
run: script/clear-target-dir-if-larger-than 100
|
||||
@@ -272,12 +272,18 @@ jobs:
|
||||
- name: Create macOS app bundle
|
||||
run: script/bundle-mac
|
||||
|
||||
- name: Rename binaries
|
||||
- name: Rename single-architecture binaries
|
||||
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
|
||||
run: |
|
||||
mv target/aarch64-apple-darwin/release/Zed.dmg target/aarch64-apple-darwin/release/Zed-aarch64.dmg
|
||||
mv target/x86_64-apple-darwin/release/Zed.dmg target/x86_64-apple-darwin/release/Zed-x86_64.dmg
|
||||
|
||||
- name: Upload app bundle (universal) to workflow run if main branch or specific label
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4
|
||||
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
|
||||
with:
|
||||
name: Zed_${{ github.event.pull_request.head.sha || github.sha }}.dmg
|
||||
path: target/release/Zed.dmg
|
||||
- name: Upload app bundle (aarch64) to workflow run if main branch or specific label
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4
|
||||
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
|
||||
@@ -303,6 +309,7 @@ jobs:
|
||||
target/zed-remote-server-macos-aarch64.gz
|
||||
target/aarch64-apple-darwin/release/Zed-aarch64.dmg
|
||||
target/x86_64-apple-darwin/release/Zed-x86_64.dmg
|
||||
target/release/Zed.dmg
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -397,16 +404,3 @@ jobs:
|
||||
target/release/zed-linux-aarch64.tar.gz
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
auto-release-preview:
|
||||
name: Auto release preview
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/v') && endsWith(github.ref, '-pre') && !endsWith(github.ref, '.0-pre') }}
|
||||
needs: [bundle-mac, bundle-linux, bundle-linux-aarch64]
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- bundle
|
||||
steps:
|
||||
- name: gh release
|
||||
run: gh release edit $GITHUB_REF_NAME --draft=false
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
2424
Cargo.lock
generated
2424
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
19
Cargo.toml
19
Cargo.toml
@@ -9,7 +9,6 @@ members = [
|
||||
"crates/assistant_tool",
|
||||
"crates/audio",
|
||||
"crates/auto_update",
|
||||
"crates/auto_update_ui",
|
||||
"crates/breadcrumbs",
|
||||
"crates/call",
|
||||
"crates/channel",
|
||||
@@ -50,13 +49,11 @@ members = [
|
||||
"crates/http_client",
|
||||
"crates/image_viewer",
|
||||
"crates/indexed_docs",
|
||||
"crates/inline_completion",
|
||||
"crates/inline_completion_button",
|
||||
"crates/install_cli",
|
||||
"crates/journal",
|
||||
"crates/language",
|
||||
"crates/language_model",
|
||||
"crates/language_models",
|
||||
"crates/language_selector",
|
||||
"crates/language_tools",
|
||||
"crates/languages",
|
||||
@@ -81,6 +78,7 @@ members = [
|
||||
"crates/project_panel",
|
||||
"crates/project_symbols",
|
||||
"crates/proto",
|
||||
"crates/quick_action_bar",
|
||||
"crates/recent_projects",
|
||||
"crates/refineable",
|
||||
"crates/refineable/derive_refineable",
|
||||
@@ -111,7 +109,7 @@ members = [
|
||||
"crates/tab_switcher",
|
||||
"crates/task",
|
||||
"crates/tasks_ui",
|
||||
"crates/telemetry",
|
||||
"crates/telemetry_events",
|
||||
"crates/terminal",
|
||||
"crates/terminal_view",
|
||||
"crates/text",
|
||||
@@ -128,7 +126,6 @@ members = [
|
||||
"crates/util",
|
||||
"crates/vcs_menu",
|
||||
"crates/vim",
|
||||
"crates/vim_mode_setting",
|
||||
"crates/welcome",
|
||||
"crates/workspace",
|
||||
"crates/worktree",
|
||||
@@ -188,7 +185,6 @@ assistant_slash_command = { path = "crates/assistant_slash_command" }
|
||||
assistant_tool = { path = "crates/assistant_tool" }
|
||||
audio = { path = "crates/audio" }
|
||||
auto_update = { path = "crates/auto_update" }
|
||||
auto_update_ui = { path = "crates/auto_update_ui" }
|
||||
breadcrumbs = { path = "crates/breadcrumbs" }
|
||||
call = { path = "crates/call" }
|
||||
channel = { path = "crates/channel" }
|
||||
@@ -225,13 +221,11 @@ html_to_markdown = { path = "crates/html_to_markdown" }
|
||||
http_client = { path = "crates/http_client" }
|
||||
image_viewer = { path = "crates/image_viewer" }
|
||||
indexed_docs = { path = "crates/indexed_docs" }
|
||||
inline_completion = { path = "crates/inline_completion" }
|
||||
inline_completion_button = { path = "crates/inline_completion_button" }
|
||||
install_cli = { path = "crates/install_cli" }
|
||||
journal = { path = "crates/journal" }
|
||||
language = { path = "crates/language" }
|
||||
language_model = { path = "crates/language_model" }
|
||||
language_models = { path = "crates/language_models" }
|
||||
language_selector = { path = "crates/language_selector" }
|
||||
language_tools = { path = "crates/language_tools" }
|
||||
languages = { path = "crates/languages" }
|
||||
@@ -258,6 +252,7 @@ project = { path = "crates/project" }
|
||||
project_panel = { path = "crates/project_panel" }
|
||||
project_symbols = { path = "crates/project_symbols" }
|
||||
proto = { path = "crates/proto" }
|
||||
quick_action_bar = { path = "crates/quick_action_bar" }
|
||||
recent_projects = { path = "crates/recent_projects" }
|
||||
refineable = { path = "crates/refineable" }
|
||||
release_channel = { path = "crates/release_channel" }
|
||||
@@ -287,7 +282,7 @@ supermaven_api = { path = "crates/supermaven_api" }
|
||||
tab_switcher = { path = "crates/tab_switcher" }
|
||||
task = { path = "crates/task" }
|
||||
tasks_ui = { path = "crates/tasks_ui" }
|
||||
telemetry = { path = "crates/telemetry" }
|
||||
telemetry_events = { path = "crates/telemetry_events" }
|
||||
terminal = { path = "crates/terminal" }
|
||||
terminal_view = { path = "crates/terminal_view" }
|
||||
text = { path = "crates/text" }
|
||||
@@ -303,7 +298,6 @@ ui_macros = { path = "crates/ui_macros" }
|
||||
util = { path = "crates/util" }
|
||||
vcs_menu = { path = "crates/vcs_menu" }
|
||||
vim = { path = "crates/vim" }
|
||||
vim_mode_setting = { path = "crates/vim_mode_setting" }
|
||||
welcome = { path = "crates/welcome" }
|
||||
workspace = { path = "crates/workspace" }
|
||||
worktree = { path = "crates/worktree" }
|
||||
@@ -338,7 +332,7 @@ blade-macros = { git = "https://github.com/kvark/blade", rev = "e142a3a5e678eb6a
|
||||
blade-util = { git = "https://github.com/kvark/blade", rev = "e142a3a5e678eb6a13e642ad8401b1f3aa38e969" }
|
||||
blake3 = "1.5.3"
|
||||
bytes = "1.0"
|
||||
cargo_metadata = "0.19"
|
||||
cargo_metadata = "0.18"
|
||||
cargo_toml = "0.20"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
clap = { version = "4.4", features = ["derive"] }
|
||||
@@ -354,7 +348,6 @@ derive_more = "0.99.17"
|
||||
dirs = "4.0"
|
||||
ec4rs = "1.1"
|
||||
emojis = "0.6.1"
|
||||
erased-serde = "0.4.5"
|
||||
env_logger = "0.11"
|
||||
exec = "0.3.1"
|
||||
fancy-regex = "0.14.0"
|
||||
@@ -604,7 +597,7 @@ snippets_ui = { codegen-units = 1 }
|
||||
sqlez_macros = { codegen-units = 1 }
|
||||
story = { codegen-units = 1 }
|
||||
supermaven_api = { codegen-units = 1 }
|
||||
telemetry = { codegen-units = 1 }
|
||||
telemetry_events = { codegen-units = 1 }
|
||||
theme_selector = { codegen-units = 1 }
|
||||
time_format = { codegen-units = 1 }
|
||||
ui_input = { codegen-units = 1 }
|
||||
|
||||
@@ -49,9 +49,8 @@
|
||||
"ctrl-d": "editor::Delete",
|
||||
"tab": "editor::Tab",
|
||||
"shift-tab": "editor::TabPrev",
|
||||
"ctrl-k": "editor::CutToEndOfLine",
|
||||
"ctrl-t": "editor::Transpose",
|
||||
"ctrl-k": "editor::KillRingCut",
|
||||
"ctrl-y": "editor::KillRingYank",
|
||||
"cmd-k q": "editor::Rewrap",
|
||||
"cmd-k cmd-q": "editor::Rewrap",
|
||||
"cmd-backspace": "editor::DeleteToBeginningOfLine",
|
||||
@@ -93,8 +92,6 @@
|
||||
"ctrl-e": "editor::MoveToEndOfLine",
|
||||
"cmd-up": "editor::MoveToBeginning",
|
||||
"cmd-down": "editor::MoveToEnd",
|
||||
"ctrl-home": "editor::MoveToBeginning",
|
||||
"ctrl-end": "editor::MoveToEnd",
|
||||
"shift-up": "editor::SelectUp",
|
||||
"ctrl-shift-p": "editor::SelectUp",
|
||||
"shift-down": "editor::SelectDown",
|
||||
|
||||
@@ -381,7 +381,8 @@
|
||||
"shift-b": "vim::CurlyBrackets",
|
||||
"<": "vim::AngleBrackets",
|
||||
">": "vim::AngleBrackets",
|
||||
"a": "vim::Argument"
|
||||
"a": "vim::AngleBrackets",
|
||||
"g": "vim::Argument"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -577,7 +578,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "EmptyPane || SharedScreen || MarkdownPreview || KeyContextView || Welcome",
|
||||
"context": "EmptyPane || SharedScreen || MarkdownPreview || KeyContextView",
|
||||
"use_layout_keys": true,
|
||||
"bindings": {
|
||||
":": "command_palette::Toggle",
|
||||
|
||||
@@ -1,206 +0,0 @@
|
||||
<task_description>
|
||||
|
||||
The user of a code editor wants to make a change to their codebase.
|
||||
You must describe the change using the following XML structure:
|
||||
|
||||
- <patch> - A group of related code changes.
|
||||
Child tags:
|
||||
- <title> (required) - A high-level description of the changes. This should be as short
|
||||
as possible, possibly using common abbreviations.
|
||||
- <edit> (1 or more) - An edit to make at a particular range within a file.
|
||||
Includes the following child tags:
|
||||
- <path> (required) - The path to the file that will be changed.
|
||||
- <description> (optional) - An arbitrarily-long comment that describes the purpose
|
||||
of this edit.
|
||||
- <old_text> (optional) - An excerpt from the file's current contents that uniquely
|
||||
identifies a range within the file where the edit should occur. If this tag is not
|
||||
specified, then the entire file will be used as the range.
|
||||
- <new_text> (required) - The new text to insert into the file.
|
||||
- <operation> (required) - The type of change that should occur at the given range
|
||||
of the file. Must be one of the following values:
|
||||
- `update`: Replaces the entire range with the new text.
|
||||
- `insert_before`: Inserts the new text before the range.
|
||||
- `insert_after`: Inserts new text after the range.
|
||||
- `create`: Creates a new file with the given path and the new text.
|
||||
- `delete`: Deletes the specified range from the file.
|
||||
|
||||
<guidelines>
|
||||
- Never provide multiple edits whose ranges intersect each other. Instead, merge them into one edit.
|
||||
- Prefer multiple edits to smaller, disjoint ranges, rather than one edit to a larger range.
|
||||
- There's no need to escape angle brackets within XML tags.
|
||||
- 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.
|
||||
|
||||
<patch>
|
||||
<title>Add origins and display impls to shapes</title>
|
||||
<edit>
|
||||
<path>src/shapes/rectangle.rs</path>
|
||||
<description>Add the origin field to Rectangle struct</description>
|
||||
<operation>insert_after</operation>
|
||||
<old_text>
|
||||
pub struct Rectangle {
|
||||
</old_text>
|
||||
<new_text>
|
||||
origin: (f64, f64),
|
||||
</new_text>
|
||||
</edit>
|
||||
|
||||
<edit>
|
||||
<path>src/shapes/rectangle.rs</path>
|
||||
<operation>update</operation>
|
||||
<old_text>
|
||||
fn new(width: f64, height: f64) -> Self {
|
||||
Rectangle { width, height }
|
||||
}
|
||||
</old_text>
|
||||
<new_text>
|
||||
fn new(origin: (f64, f64), width: f64, height: f64) -> Self {
|
||||
Rectangle { origin, width, height }
|
||||
}
|
||||
</new_text>
|
||||
</edit>
|
||||
|
||||
<edit>
|
||||
<path>src/shapes/circle.rs</path>
|
||||
<description>Add the origin field to Circle struct</description>
|
||||
<operation>insert_after</operation>
|
||||
<old_text>
|
||||
pub struct Circle {
|
||||
radius: f64,
|
||||
</old_text>
|
||||
<new_text>
|
||||
origin: (f64, f64),
|
||||
</new_text>
|
||||
</edit>
|
||||
|
||||
<edit>
|
||||
<path>src/shapes/circle.rs</path>
|
||||
<operation>update</operation>
|
||||
<old_text>
|
||||
fn new(radius: f64) -> Self {
|
||||
Circle { radius }
|
||||
}
|
||||
</old_text>
|
||||
<new_text>
|
||||
fn new(origin: (f64, f64), radius: f64) -> Self {
|
||||
Circle { origin, radius }
|
||||
}
|
||||
</new_text>
|
||||
</edit>
|
||||
</step>
|
||||
|
||||
<edit>
|
||||
<path>src/shapes/rectangle.rs</path>
|
||||
<operation>insert_before</operation>
|
||||
<old_text>
|
||||
struct Rectangle {
|
||||
</old_text>
|
||||
<new_text>
|
||||
use std::fmt;
|
||||
|
||||
</new_text>
|
||||
</edit>
|
||||
|
||||
<edit>
|
||||
<path>src/shapes/rectangle.rs</path>
|
||||
<description>
|
||||
Add a manual Display implementation for Rectangle.
|
||||
Currently, this is the same as a derived Display implementation.
|
||||
</description>
|
||||
<operation>insert_after</operation>
|
||||
<old_text>
|
||||
Rectangle { width, height }
|
||||
}
|
||||
}
|
||||
</old_text>
|
||||
<new_text>
|
||||
impl fmt::Display for Rectangle {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.format_struct(f, "Rectangle")
|
||||
.field("origin", &self.origin)
|
||||
.field("width", &self.width)
|
||||
.field("height", &self.height)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
</new_text>
|
||||
</edit>
|
||||
|
||||
<edit>
|
||||
<path>src/shapes/circle.rs</path>
|
||||
<operation>insert_before</operation>
|
||||
<old_text>
|
||||
struct Circle {
|
||||
</old_text>
|
||||
<new_text>
|
||||
use std::fmt;
|
||||
</new_text>
|
||||
</edit>
|
||||
|
||||
<edit>
|
||||
<path>src/shapes/circle.rs</path>
|
||||
<operation>insert_after</operation>
|
||||
<old_text>
|
||||
Circle { radius }
|
||||
}
|
||||
}
|
||||
</old_text>
|
||||
<new_text>
|
||||
impl fmt::Display for Rectangle {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.format_struct(f, "Rectangle")
|
||||
.field("origin", &self.origin)
|
||||
.field("width", &self.width)
|
||||
.field("height", &self.height)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
</new_text>
|
||||
</edit>
|
||||
</patch>
|
||||
|
||||
</message>
|
||||
</example>
|
||||
|
||||
</task_description>
|
||||
@@ -668,7 +668,7 @@
|
||||
},
|
||||
// Add files or globs of files that will be excluded by Zed entirely:
|
||||
// they will be skipped during FS scan(s), file tree and file search
|
||||
// will lack the corresponding file entries. Overrides `file_scan_inclusions`.
|
||||
// will lack the corresponding file entries.
|
||||
"file_scan_exclusions": [
|
||||
"**/.git",
|
||||
"**/.svn",
|
||||
@@ -679,14 +679,6 @@
|
||||
"**/.classpath",
|
||||
"**/.settings"
|
||||
],
|
||||
// Add files or globs of files that will be included by Zed, even when
|
||||
// ignored by git. This is useful for files that are not tracked by git,
|
||||
// but are still important to your project. Note that globs that are
|
||||
// overly broad can slow down Zed's file scanning. Overridden by `file_scan_exclusions`.
|
||||
"file_scan_inclusions": [
|
||||
".env*",
|
||||
"docker-compose.*.yml"
|
||||
],
|
||||
// Git gutter behavior configuration.
|
||||
"git": {
|
||||
// Control whether the git gutter is shown. May take 2 values:
|
||||
@@ -847,12 +839,8 @@
|
||||
}
|
||||
},
|
||||
"toolbar": {
|
||||
// Whether to display the terminal title in its toolbar's breadcrumbs.
|
||||
// Only shown if the terminal title is not empty.
|
||||
//
|
||||
// The shell running in the terminal needs to be configured to emit the title.
|
||||
// Example: `echo -e "\e]2;New Title\007";`
|
||||
"breadcrumbs": true
|
||||
// Whether to display the terminal title in its toolbar.
|
||||
"title": true
|
||||
}
|
||||
// Set the terminal's font size. If this option is not included,
|
||||
// the terminal will default to matching the buffer's font size.
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
mod supported_countries;
|
||||
|
||||
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};
|
||||
use http_client::http::{HeaderMap, HeaderValue};
|
||||
use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{pin::Pin, str::FromStr};
|
||||
use strum::{EnumIter, EnumString};
|
||||
use thiserror::Error;
|
||||
use util::ResultExt as _;
|
||||
|
||||
@@ -50,7 +50,6 @@ indexed_docs.workspace = true
|
||||
indoc.workspace = true
|
||||
language.workspace = true
|
||||
language_model.workspace = true
|
||||
language_models.workspace = true
|
||||
log.workspace = true
|
||||
lsp.workspace = true
|
||||
markdown.workspace = true
|
||||
@@ -78,7 +77,7 @@ similar.workspace = true
|
||||
smallvec.workspace = true
|
||||
smol.workspace = true
|
||||
strum.workspace = true
|
||||
telemetry.workspace = true
|
||||
telemetry_events.workspace = true
|
||||
terminal.workspace = true
|
||||
terminal_view.workspace = true
|
||||
text.workspace = true
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::slash_command::file_command::codeblock_fence_for_path;
|
||||
use crate::slash_command_working_set::SlashCommandWorkingSet;
|
||||
use crate::tools::code_edits_tool::{CodeEditsTool, CodeEditsToolInput};
|
||||
use crate::ToolWorkingSet;
|
||||
use crate::{
|
||||
assistant_settings::{AssistantDockPosition, AssistantSettings},
|
||||
@@ -50,11 +51,11 @@ use indexed_docs::IndexedDocsStore;
|
||||
use language::{
|
||||
language_settings::SoftWrap, BufferSnapshot, LanguageRegistry, LspAdapterDelegate, ToOffset,
|
||||
};
|
||||
use language_model::{LanguageModelImage, LanguageModelToolUse};
|
||||
use language_model::{
|
||||
LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry, Role,
|
||||
ZED_CLOUD_PROVIDER_ID,
|
||||
provider::cloud::PROVIDER_ID, LanguageModelProvider, LanguageModelProviderId,
|
||||
LanguageModelRegistry, Role,
|
||||
};
|
||||
use language_model::{LanguageModelImage, LanguageModelToolUse};
|
||||
use multi_buffer::MultiBufferRow;
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use project::lsp_store::LocalLspAdapterDelegate;
|
||||
@@ -664,7 +665,7 @@ impl AssistantPanel {
|
||||
// If we're signed out and don't have a provider configured, or we're signed-out AND Zed.dev is
|
||||
// the provider, we want to show a nudge to sign in.
|
||||
let show_zed_ai_notice = client_status.is_signed_out()
|
||||
&& active_provider.map_or(true, |provider| provider.id().0 == ZED_CLOUD_PROVIDER_ID);
|
||||
&& active_provider.map_or(true, |provider| provider.id().0 == PROVIDER_ID);
|
||||
|
||||
self.show_zed_ai_notice = show_zed_ai_notice;
|
||||
cx.notify();
|
||||
@@ -1898,47 +1899,111 @@ impl ContextEditor {
|
||||
let creases = new_tool_uses
|
||||
.iter()
|
||||
.map(|tool_use| {
|
||||
let placeholder = FoldPlaceholder {
|
||||
render: render_fold_icon_button(
|
||||
cx.view().downgrade(),
|
||||
IconName::PocketKnife,
|
||||
tool_use.name.clone().into(),
|
||||
),
|
||||
..Default::default()
|
||||
};
|
||||
let render_trailer =
|
||||
move |_row, _unfold, _cx: &mut WindowContext| Empty.into_any();
|
||||
if &tool_use.name == CodeEditsTool::TOOL_NAME {
|
||||
// If this is a Code Edit tool,
|
||||
match serde_json::from_value::<CodeEditsToolInput>(
|
||||
tool_use.input.clone(),
|
||||
) {
|
||||
Ok(CodeEditsToolInput { title, edits }) => {
|
||||
let placeholder = FoldPlaceholder {
|
||||
render: render_fold_icon_button(
|
||||
cx.view().downgrade(),
|
||||
IconName::Sparkle,
|
||||
title.into(),
|
||||
),
|
||||
..Default::default()
|
||||
};
|
||||
let render_trailer =
|
||||
move |_row, _unfold, _cx: &mut WindowContext| {
|
||||
Empty.into_any()
|
||||
};
|
||||
|
||||
let start = buffer
|
||||
.anchor_in_excerpt(excerpt_id, tool_use.source_range.start)
|
||||
.unwrap();
|
||||
let end = buffer
|
||||
.anchor_in_excerpt(excerpt_id, tool_use.source_range.end)
|
||||
.unwrap();
|
||||
let start = buffer
|
||||
.anchor_in_excerpt(
|
||||
excerpt_id,
|
||||
tool_use.source_range.start,
|
||||
)
|
||||
.unwrap();
|
||||
let end = buffer
|
||||
.anchor_in_excerpt(
|
||||
excerpt_id,
|
||||
tool_use.source_range.end,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let buffer_row = MultiBufferRow(start.to_point(&buffer).row);
|
||||
buffer_rows_to_fold.insert(buffer_row);
|
||||
let buffer_row =
|
||||
MultiBufferRow(start.to_point(&buffer).row);
|
||||
buffer_rows_to_fold.insert(buffer_row);
|
||||
|
||||
self.context.update(cx, |context, cx| {
|
||||
context.insert_content(
|
||||
Content::ToolUse {
|
||||
range: tool_use.source_range.clone(),
|
||||
tool_use: LanguageModelToolUse {
|
||||
id: tool_use.id.to_string(),
|
||||
name: tool_use.name.clone(),
|
||||
input: tool_use.input.clone(),
|
||||
self.context.update(cx, |context, cx| {
|
||||
context.insert_content(
|
||||
Content::ToolUse {
|
||||
range: tool_use.source_range.clone(),
|
||||
tool_use: LanguageModelToolUse {
|
||||
id: tool_use.id.to_string(),
|
||||
name: tool_use.name.clone(),
|
||||
input: tool_use.input.clone(),
|
||||
},
|
||||
},
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
Crease::inline(
|
||||
start..end,
|
||||
placeholder,
|
||||
fold_toggle("tool-use"),
|
||||
render_trailer,
|
||||
)
|
||||
}
|
||||
Err(json_err) => {
|
||||
// TODO gracefully handle malformed JSON (should distinguish from "errored out" vs "not done streaming yet")
|
||||
todo!();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let placeholder = FoldPlaceholder {
|
||||
render: render_fold_icon_button(
|
||||
cx.view().downgrade(),
|
||||
IconName::PocketKnife,
|
||||
tool_use.name.clone().into(),
|
||||
),
|
||||
..Default::default()
|
||||
};
|
||||
let render_trailer =
|
||||
move |_row, _unfold, _cx: &mut WindowContext| Empty.into_any();
|
||||
|
||||
let start = buffer
|
||||
.anchor_in_excerpt(excerpt_id, tool_use.source_range.start)
|
||||
.unwrap();
|
||||
let end = buffer
|
||||
.anchor_in_excerpt(excerpt_id, tool_use.source_range.end)
|
||||
.unwrap();
|
||||
|
||||
let buffer_row = MultiBufferRow(start.to_point(&buffer).row);
|
||||
buffer_rows_to_fold.insert(buffer_row);
|
||||
|
||||
self.context.update(cx, |context, cx| {
|
||||
context.insert_content(
|
||||
Content::ToolUse {
|
||||
range: tool_use.source_range.clone(),
|
||||
tool_use: LanguageModelToolUse {
|
||||
id: tool_use.id.to_string(),
|
||||
name: tool_use.name.clone(),
|
||||
input: tool_use.input.clone(),
|
||||
},
|
||||
},
|
||||
},
|
||||
cx,
|
||||
);
|
||||
});
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
Crease::inline(
|
||||
start..end,
|
||||
placeholder,
|
||||
fold_toggle("tool-use"),
|
||||
render_trailer,
|
||||
)
|
||||
Crease::inline(
|
||||
start..end,
|
||||
placeholder,
|
||||
fold_toggle("tool-use"),
|
||||
render_trailer,
|
||||
)
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
|
||||
@@ -5,12 +5,13 @@ use anthropic::Model as AnthropicModel;
|
||||
use feature_flags::FeatureFlagAppExt;
|
||||
use fs::Fs;
|
||||
use gpui::{AppContext, Pixels};
|
||||
use language_model::{CloudModel, LanguageModel};
|
||||
use language_models::{
|
||||
provider::open_ai, AllLanguageModelSettings, AnthropicSettingsContent,
|
||||
AnthropicSettingsContentV1, OllamaSettingsContent, OpenAiSettingsContent,
|
||||
OpenAiSettingsContentV1, VersionedAnthropicSettingsContent, VersionedOpenAiSettingsContent,
|
||||
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 schemars::{schema::Schema, JsonSchema};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -6,12 +6,14 @@ use crate::ToolWorkingSet;
|
||||
use crate::{
|
||||
prompts::PromptBuilder,
|
||||
slash_command::{file_command::FileCommandMetadata, SlashCommandLine},
|
||||
AssistantEdit, AssistantPatch, AssistantPatchStatus, MessageId, MessageStatus,
|
||||
tools::code_edits_tool::CodeEditsTool,
|
||||
AssistantPatch, MessageId, MessageStatus,
|
||||
};
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use assistant_slash_command::{
|
||||
SlashCommandContent, SlashCommandEvent, SlashCommandOutputSection, SlashCommandResult,
|
||||
};
|
||||
use assistant_tool::Tool;
|
||||
use client::{self, proto, telemetry::Telemetry};
|
||||
use clock::ReplicaId;
|
||||
use collections::{HashMap, HashSet};
|
||||
@@ -25,15 +27,13 @@ use gpui::{
|
||||
|
||||
use language::{AnchorRangeExt, Bias, Buffer, LanguageRegistry, OffsetRangeExt, Point, ToOffset};
|
||||
use language_model::{
|
||||
logging::report_assistant_event,
|
||||
provider::cloud::{MaxMonthlySpendReachedError, PaymentRequiredError},
|
||||
LanguageModel, LanguageModelCacheConfiguration, LanguageModelCompletionEvent,
|
||||
LanguageModelImage, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage,
|
||||
LanguageModelRequestTool, LanguageModelToolResult, LanguageModelToolUse, MessageContent, Role,
|
||||
StopReason,
|
||||
};
|
||||
use language_models::{
|
||||
provider::cloud::{MaxMonthlySpendReachedError, PaymentRequiredError},
|
||||
report_assistant_event,
|
||||
};
|
||||
use open_ai::Model as OpenAiModel;
|
||||
use paths::contexts_dir;
|
||||
use project::Project;
|
||||
@@ -45,11 +45,10 @@ use std::{
|
||||
iter, mem,
|
||||
ops::Range,
|
||||
path::{Path, PathBuf},
|
||||
str::FromStr as _,
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use telemetry::{AssistantEvent, AssistantKind, AssistantPhase};
|
||||
use telemetry_events::{AssistantEvent, AssistantKind, AssistantPhase};
|
||||
use text::{BufferSnapshot, ToPoint};
|
||||
use util::{post_inc, ResultExt, TryFutureExt};
|
||||
use uuid::Uuid;
|
||||
@@ -563,7 +562,6 @@ pub struct Context {
|
||||
telemetry: Option<Arc<Telemetry>>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
patches: Vec<AssistantPatch>,
|
||||
xml_tags: Vec<XmlTag>,
|
||||
project: Option<Model<Project>>,
|
||||
prompt_builder: Arc<PromptBuilder>,
|
||||
}
|
||||
@@ -672,7 +670,6 @@ impl Context {
|
||||
slash_commands,
|
||||
tools,
|
||||
patches: Vec::new(),
|
||||
xml_tags: Vec::new(),
|
||||
prompt_builder,
|
||||
};
|
||||
|
||||
@@ -964,7 +961,6 @@ impl Context {
|
||||
}
|
||||
|
||||
if !changed_messages.is_empty() {
|
||||
self.message_roles_updated(changed_messages, cx);
|
||||
cx.emit(ContextEvent::MessagesEdited);
|
||||
cx.notify();
|
||||
}
|
||||
@@ -1388,8 +1384,6 @@ impl Context {
|
||||
|
||||
let mut removed_parsed_slash_command_ranges = Vec::new();
|
||||
let mut updated_parsed_slash_commands = Vec::new();
|
||||
let mut removed_patches = Vec::new();
|
||||
let mut updated_patches = Vec::new();
|
||||
while let Some(mut row_range) = row_ranges.next() {
|
||||
while let Some(next_row_range) = row_ranges.peek() {
|
||||
if row_range.end >= next_row_range.start {
|
||||
@@ -1414,13 +1408,6 @@ impl Context {
|
||||
cx,
|
||||
);
|
||||
self.invalidate_pending_slash_commands(&buffer, cx);
|
||||
self.reparse_patches_in_range(
|
||||
start..end,
|
||||
&buffer,
|
||||
&mut updated_patches,
|
||||
&mut removed_patches,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
if !updated_parsed_slash_commands.is_empty()
|
||||
@@ -1431,13 +1418,6 @@ impl Context {
|
||||
updated: updated_parsed_slash_commands,
|
||||
});
|
||||
}
|
||||
|
||||
if !updated_patches.is_empty() || !removed_patches.is_empty() {
|
||||
cx.emit(ContextEvent::PatchesUpdated {
|
||||
removed: removed_patches,
|
||||
updated: updated_patches,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn reparse_slash_commands_in_range(
|
||||
@@ -1528,267 +1508,6 @@ impl Context {
|
||||
}
|
||||
}
|
||||
|
||||
fn reparse_patches_in_range(
|
||||
&mut self,
|
||||
range: Range<text::Anchor>,
|
||||
buffer: &BufferSnapshot,
|
||||
updated: &mut Vec<Range<text::Anchor>>,
|
||||
removed: &mut Vec<Range<text::Anchor>>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
// Rebuild the XML tags in the edited range.
|
||||
let intersecting_tags_range =
|
||||
self.indices_intersecting_buffer_range(&self.xml_tags, range.clone(), cx);
|
||||
let new_tags = self.parse_xml_tags_in_range(buffer, range.clone(), cx);
|
||||
self.xml_tags
|
||||
.splice(intersecting_tags_range.clone(), new_tags);
|
||||
|
||||
// Find which patches intersect the changed range.
|
||||
let intersecting_patches_range =
|
||||
self.indices_intersecting_buffer_range(&self.patches, range.clone(), cx);
|
||||
|
||||
// Reparse all tags after the last unchanged patch before the change.
|
||||
let mut tags_start_ix = 0;
|
||||
if let Some(preceding_unchanged_patch) =
|
||||
self.patches[..intersecting_patches_range.start].last()
|
||||
{
|
||||
tags_start_ix = match self.xml_tags.binary_search_by(|tag| {
|
||||
tag.range
|
||||
.start
|
||||
.cmp(&preceding_unchanged_patch.range.end, buffer)
|
||||
.then(Ordering::Less)
|
||||
}) {
|
||||
Ok(ix) | Err(ix) => ix,
|
||||
};
|
||||
}
|
||||
|
||||
// Rebuild the patches in the range.
|
||||
let new_patches = self.parse_patches(tags_start_ix, range.end, buffer, cx);
|
||||
updated.extend(new_patches.iter().map(|patch| patch.range.clone()));
|
||||
let removed_patches = self.patches.splice(intersecting_patches_range, new_patches);
|
||||
removed.extend(
|
||||
removed_patches
|
||||
.map(|patch| patch.range)
|
||||
.filter(|range| !updated.contains(&range)),
|
||||
);
|
||||
}
|
||||
|
||||
fn parse_xml_tags_in_range(
|
||||
&self,
|
||||
buffer: &BufferSnapshot,
|
||||
range: Range<text::Anchor>,
|
||||
cx: &AppContext,
|
||||
) -> Vec<XmlTag> {
|
||||
let mut messages = self.messages(cx).peekable();
|
||||
|
||||
let mut tags = Vec::new();
|
||||
let mut lines = buffer.text_for_range(range).lines();
|
||||
let mut offset = lines.offset();
|
||||
|
||||
while let Some(line) = lines.next() {
|
||||
while let Some(message) = messages.peek() {
|
||||
if offset < message.offset_range.end {
|
||||
break;
|
||||
} else {
|
||||
messages.next();
|
||||
}
|
||||
}
|
||||
|
||||
let is_assistant_message = messages
|
||||
.peek()
|
||||
.map_or(false, |message| message.role == Role::Assistant);
|
||||
if is_assistant_message {
|
||||
for (start_ix, _) in line.match_indices('<') {
|
||||
let mut name_start_ix = start_ix + 1;
|
||||
let closing_bracket_ix = line[start_ix..].find('>').map(|i| start_ix + i);
|
||||
if let Some(closing_bracket_ix) = closing_bracket_ix {
|
||||
let end_ix = closing_bracket_ix + 1;
|
||||
let mut is_open_tag = true;
|
||||
if line[name_start_ix..closing_bracket_ix].starts_with('/') {
|
||||
name_start_ix += 1;
|
||||
is_open_tag = false;
|
||||
}
|
||||
let tag_inner = &line[name_start_ix..closing_bracket_ix];
|
||||
let tag_name_len = tag_inner
|
||||
.find(|c: char| c.is_whitespace())
|
||||
.unwrap_or(tag_inner.len());
|
||||
if let Ok(kind) = XmlTagKind::from_str(&tag_inner[..tag_name_len]) {
|
||||
tags.push(XmlTag {
|
||||
range: buffer.anchor_after(offset + start_ix)
|
||||
..buffer.anchor_before(offset + end_ix),
|
||||
is_open_tag,
|
||||
kind,
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
offset = lines.offset();
|
||||
}
|
||||
tags
|
||||
}
|
||||
|
||||
fn parse_patches(
|
||||
&mut self,
|
||||
tags_start_ix: usize,
|
||||
buffer_end: text::Anchor,
|
||||
buffer: &BufferSnapshot,
|
||||
cx: &AppContext,
|
||||
) -> Vec<AssistantPatch> {
|
||||
let mut new_patches = Vec::new();
|
||||
let mut pending_patch = None;
|
||||
let mut patch_tag_depth = 0;
|
||||
let mut tags = self.xml_tags[tags_start_ix..].iter().peekable();
|
||||
'tags: while let Some(tag) = tags.next() {
|
||||
if tag.range.start.cmp(&buffer_end, buffer).is_gt() && patch_tag_depth == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
if tag.kind == XmlTagKind::Patch && tag.is_open_tag {
|
||||
patch_tag_depth += 1;
|
||||
let patch_start = tag.range.start;
|
||||
let mut edits = Vec::<Result<AssistantEdit>>::new();
|
||||
let mut patch = AssistantPatch {
|
||||
range: patch_start..patch_start,
|
||||
title: String::new().into(),
|
||||
edits: Default::default(),
|
||||
status: crate::AssistantPatchStatus::Pending,
|
||||
};
|
||||
|
||||
while let Some(tag) = tags.next() {
|
||||
if tag.kind == XmlTagKind::Patch && !tag.is_open_tag {
|
||||
patch_tag_depth -= 1;
|
||||
if patch_tag_depth == 0 {
|
||||
patch.range.end = tag.range.end;
|
||||
|
||||
// Include the line immediately after this <patch> tag if it's empty.
|
||||
let patch_end_offset = patch.range.end.to_offset(buffer);
|
||||
let mut patch_end_chars = buffer.chars_at(patch_end_offset);
|
||||
if patch_end_chars.next() == Some('\n')
|
||||
&& patch_end_chars.next().map_or(true, |ch| ch == '\n')
|
||||
{
|
||||
let messages = self.messages_for_offsets(
|
||||
[patch_end_offset, patch_end_offset + 1],
|
||||
cx,
|
||||
);
|
||||
if messages.len() == 1 {
|
||||
patch.range.end = buffer.anchor_before(patch_end_offset + 1);
|
||||
}
|
||||
}
|
||||
|
||||
edits.sort_unstable_by(|a, b| {
|
||||
if let (Ok(a), Ok(b)) = (a, b) {
|
||||
a.path.cmp(&b.path)
|
||||
} else {
|
||||
Ordering::Equal
|
||||
}
|
||||
});
|
||||
patch.edits = edits.into();
|
||||
patch.status = AssistantPatchStatus::Ready;
|
||||
new_patches.push(patch);
|
||||
continue 'tags;
|
||||
}
|
||||
}
|
||||
|
||||
if tag.kind == XmlTagKind::Title && tag.is_open_tag {
|
||||
let content_start = tag.range.end;
|
||||
while let Some(tag) = tags.next() {
|
||||
if tag.kind == XmlTagKind::Title && !tag.is_open_tag {
|
||||
let content_end = tag.range.start;
|
||||
patch.title =
|
||||
trimmed_text_in_range(buffer, content_start..content_end)
|
||||
.into();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if tag.kind == XmlTagKind::Edit && tag.is_open_tag {
|
||||
let mut path = None;
|
||||
let mut old_text = None;
|
||||
let mut new_text = None;
|
||||
let mut operation = None;
|
||||
let mut description = None;
|
||||
|
||||
while let Some(tag) = tags.next() {
|
||||
if tag.kind == XmlTagKind::Edit && !tag.is_open_tag {
|
||||
edits.push(AssistantEdit::new(
|
||||
path,
|
||||
operation,
|
||||
old_text,
|
||||
new_text,
|
||||
description,
|
||||
));
|
||||
break;
|
||||
}
|
||||
|
||||
if tag.is_open_tag
|
||||
&& [
|
||||
XmlTagKind::Path,
|
||||
XmlTagKind::OldText,
|
||||
XmlTagKind::NewText,
|
||||
XmlTagKind::Operation,
|
||||
XmlTagKind::Description,
|
||||
]
|
||||
.contains(&tag.kind)
|
||||
{
|
||||
let kind = tag.kind;
|
||||
let content_start = tag.range.end;
|
||||
if let Some(tag) = tags.peek() {
|
||||
if tag.kind == kind && !tag.is_open_tag {
|
||||
let tag = tags.next().unwrap();
|
||||
let content_end = tag.range.start;
|
||||
let content = trimmed_text_in_range(
|
||||
buffer,
|
||||
content_start..content_end,
|
||||
);
|
||||
match kind {
|
||||
XmlTagKind::Path => path = Some(content),
|
||||
XmlTagKind::Operation => operation = Some(content),
|
||||
XmlTagKind::OldText => {
|
||||
old_text = Some(content).filter(|s| !s.is_empty())
|
||||
}
|
||||
XmlTagKind::NewText => {
|
||||
new_text = Some(content).filter(|s| !s.is_empty())
|
||||
}
|
||||
XmlTagKind::Description => {
|
||||
description =
|
||||
Some(content).filter(|s| !s.is_empty())
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
patch.edits = edits.into();
|
||||
pending_patch = Some(patch);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(mut pending_patch) = pending_patch {
|
||||
let patch_start = pending_patch.range.start.to_offset(buffer);
|
||||
if let Some(message) = self.message_for_offset(patch_start, cx) {
|
||||
if message.anchor_range.end == text::Anchor::MAX {
|
||||
pending_patch.range.end = text::Anchor::MAX;
|
||||
} else {
|
||||
let message_end = buffer.anchor_after(message.offset_range.end - 1);
|
||||
pending_patch.range.end = message_end;
|
||||
}
|
||||
} else {
|
||||
pending_patch.range.end = text::Anchor::MAX;
|
||||
}
|
||||
|
||||
new_patches.push(pending_patch);
|
||||
}
|
||||
|
||||
new_patches
|
||||
}
|
||||
|
||||
pub fn pending_command_for_position(
|
||||
&mut self,
|
||||
position: language::Anchor,
|
||||
@@ -2473,9 +2192,22 @@ impl Context {
|
||||
}
|
||||
}
|
||||
|
||||
let tools = if let RequestType::SuggestEdits = request_type {
|
||||
vec![{
|
||||
let tool = CodeEditsTool;
|
||||
LanguageModelRequestTool {
|
||||
name: tool.name(),
|
||||
description: tool.description(),
|
||||
input_schema: tool.input_schema(),
|
||||
}
|
||||
}]
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
let mut completion_request = LanguageModelRequest {
|
||||
messages: Vec::new(),
|
||||
tools: Vec::new(),
|
||||
tools,
|
||||
stop: Vec::new(),
|
||||
temperature: None,
|
||||
};
|
||||
@@ -2548,25 +2280,6 @@ impl Context {
|
||||
completion_request.messages.push(request_message);
|
||||
}
|
||||
|
||||
if let RequestType::SuggestEdits = request_type {
|
||||
if let Ok(preamble) = self.prompt_builder.generate_suggest_edits_prompt() {
|
||||
let last_elem_index = completion_request.messages.len();
|
||||
|
||||
completion_request
|
||||
.messages
|
||||
.push(LanguageModelRequestMessage {
|
||||
role: Role::User,
|
||||
content: vec![MessageContent::Text(preamble)],
|
||||
cache: false,
|
||||
});
|
||||
|
||||
// The preamble message should be sent right before the last actual user message.
|
||||
completion_request
|
||||
.messages
|
||||
.swap(last_elem_index, last_elem_index.saturating_sub(1));
|
||||
}
|
||||
}
|
||||
|
||||
completion_request
|
||||
}
|
||||
|
||||
@@ -2590,28 +2303,6 @@ impl Context {
|
||||
self.update_metadata(*id, cx, |metadata| metadata.role = role);
|
||||
}
|
||||
}
|
||||
|
||||
self.message_roles_updated(ids, cx);
|
||||
}
|
||||
|
||||
fn message_roles_updated(&mut self, ids: HashSet<MessageId>, cx: &mut ModelContext<Self>) {
|
||||
let mut ranges = Vec::new();
|
||||
for message in self.messages(cx) {
|
||||
if ids.contains(&message.id) {
|
||||
ranges.push(message.anchor_range.clone());
|
||||
}
|
||||
}
|
||||
|
||||
let buffer = self.buffer.read(cx).text_snapshot();
|
||||
let mut updated = Vec::new();
|
||||
let mut removed = Vec::new();
|
||||
for range in ranges {
|
||||
self.reparse_patches_in_range(range, &buffer, &mut updated, &mut removed, cx);
|
||||
}
|
||||
|
||||
if !updated.is_empty() || !removed.is_empty() {
|
||||
cx.emit(ContextEvent::PatchesUpdated { removed, updated })
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_metadata(
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use super::{AssistantEdit, MessageCacheMetadata};
|
||||
use super::MessageCacheMetadata;
|
||||
use crate::slash_command_working_set::SlashCommandWorkingSet;
|
||||
use crate::ToolWorkingSet;
|
||||
use crate::{
|
||||
assistant_panel, prompt_library, slash_command::file_command, AssistantEditKind, CacheStatus,
|
||||
Context, ContextEvent, ContextId, ContextOperation, InvokedSlashCommandId, MessageId,
|
||||
MessageStatus, PromptBuilder,
|
||||
};
|
||||
use crate::{AssistantEdit, ToolWorkingSet};
|
||||
use anyhow::Result;
|
||||
use assistant_slash_command::{
|
||||
ArgumentCompletion, SlashCommand, SlashCommandContent, SlashCommandEvent, SlashCommandOutput,
|
||||
|
||||
@@ -770,7 +770,7 @@ impl ContextStore {
|
||||
contexts.push(SavedContextMetadata {
|
||||
title: title.to_string(),
|
||||
path,
|
||||
mtime: metadata.mtime.timestamp_for_user().into(),
|
||||
mtime: metadata.mtime.into(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,10 +30,9 @@ use gpui::{
|
||||
};
|
||||
use language::{Buffer, IndentKind, Point, Selection, TransactionId};
|
||||
use language_model::{
|
||||
LanguageModel, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage,
|
||||
LanguageModelTextStream, Role,
|
||||
logging::report_assistant_event, LanguageModel, LanguageModelRegistry, LanguageModelRequest,
|
||||
LanguageModelRequestMessage, LanguageModelTextStream, Role,
|
||||
};
|
||||
use language_models::report_assistant_event;
|
||||
use multi_buffer::MultiBufferRow;
|
||||
use parking_lot::Mutex;
|
||||
use project::{CodeAction, ProjectTransaction};
|
||||
@@ -50,7 +49,7 @@ use std::{
|
||||
task::{self, Poll},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use telemetry::{AssistantEvent, AssistantKind, AssistantPhase};
|
||||
use telemetry_events::{AssistantEvent, AssistantKind, AssistantPhase};
|
||||
use terminal_view::terminal_panel::TerminalPanel;
|
||||
use text::{OffsetRangeExt, ToPoint as _};
|
||||
use theme::ThemeSettings;
|
||||
|
||||
@@ -310,10 +310,6 @@ impl PromptBuilder {
|
||||
.render("terminal_assistant_prompt", &context)
|
||||
}
|
||||
|
||||
pub fn generate_suggest_edits_prompt(&self) -> Result<String, RenderError> {
|
||||
self.handlebars.lock().render("suggest_edits", &())
|
||||
}
|
||||
|
||||
pub fn generate_project_slash_command_prompt(
|
||||
&self,
|
||||
context_buffer: String,
|
||||
|
||||
@@ -17,16 +17,16 @@ use gpui::{
|
||||
};
|
||||
use language::Buffer;
|
||||
use language_model::{
|
||||
LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, Role,
|
||||
logging::report_assistant_event, LanguageModelRegistry, LanguageModelRequest,
|
||||
LanguageModelRequestMessage, Role,
|
||||
};
|
||||
use language_models::report_assistant_event;
|
||||
use settings::Settings;
|
||||
use std::{
|
||||
cmp,
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use telemetry::{AssistantEvent, AssistantKind, AssistantPhase};
|
||||
use telemetry_events::{AssistantEvent, AssistantKind, AssistantPhase};
|
||||
use terminal::Terminal;
|
||||
use terminal_view::TerminalView;
|
||||
use theme::ThemeSettings;
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
pub mod code_edits_tool;
|
||||
pub mod context_server_tool;
|
||||
pub mod now_tool;
|
||||
|
||||
86
crates/assistant/src/tools/code_edits_tool.rs
Normal file
86
crates/assistant/src/tools/code_edits_tool.rs
Normal file
@@ -0,0 +1,86 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use assistant_tool::Tool;
|
||||
use gpui::{Task, WeakView, WindowContext};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct CodeEditsToolInput {
|
||||
/// A high-level description of the code changes. This should be as short as possible, possibly using common abbreviations.
|
||||
pub title: String,
|
||||
/// An array of edits to be applied.
|
||||
pub edits: Vec<Edit>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct Edit {
|
||||
/// The path to the file that this edit will change.
|
||||
pub path: String,
|
||||
/// An arbitrarily-long comment that describes the purpose of this edit.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub description: Option<String>,
|
||||
/// An excerpt from the file's current contents that uniquely identifies a range within the file where the edit should occur.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub old_text: Option<String>,
|
||||
/// The new text to insert into the file.
|
||||
pub new_text: String,
|
||||
/// The type of change that should occur at the given range of the file.
|
||||
pub operation: Operation,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Operation {
|
||||
/// Replaces the entire range with the new text.
|
||||
Update,
|
||||
/// Inserts the new text before the range.
|
||||
InsertBefore,
|
||||
/// Inserts new text after the range.
|
||||
InsertAfter,
|
||||
/// Creates a new file with the given path and the new text.
|
||||
Create,
|
||||
/// Deletes the specified range from the file.
|
||||
Delete,
|
||||
}
|
||||
|
||||
pub struct CodeEditsTool;
|
||||
|
||||
impl CodeEditsTool {
|
||||
pub const TOOL_NAME: &str = "zed_code_edits";
|
||||
}
|
||||
|
||||
impl Tool for CodeEditsTool {
|
||||
fn name(&self) -> String {
|
||||
Self::TOOL_NAME.to_string()
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
// Anthropic's best practices for tool descriptions:
|
||||
// https://docs.anthropic.com/en/docs/build-with-claude/tool-use#best-practices-for-tool-definitions
|
||||
include_str!("edit_tool_description.txt").to_string()
|
||||
}
|
||||
|
||||
fn input_schema(&self) -> serde_json::Value {
|
||||
let schema = schemars::schema_for!(CodeEditsToolInput);
|
||||
|
||||
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: CodeEditsToolInput = match serde_json::from_value(input) {
|
||||
Ok(input) => input,
|
||||
Err(err) => return Task::ready(Err(anyhow!(err))),
|
||||
};
|
||||
|
||||
let text = format!("The tool returned {:?}.", input);
|
||||
|
||||
Task::ready(Ok(text))
|
||||
}
|
||||
}
|
||||
15
crates/assistant/src/tools/edit_tool_description.txt
Normal file
15
crates/assistant/src/tools/edit_tool_description.txt
Normal file
@@ -0,0 +1,15 @@
|
||||
Describes the specific code changes that should be made to the files in a code base, based on the request the user made.
|
||||
It should be used when the user requests making changes to the code base, but not when the user is asking an question about information
|
||||
(including when asking for information about the code base) rather than requesting a change.
|
||||
|
||||
The tool will return an array of patches, each of which represents some related modifications to the code base.
|
||||
Each patch contains a high-level summary of the changes (which will be displayed in the code editor),
|
||||
as well as an array of specific edits to be made to specific individual files. The code editor will apply each of those edits to the code base, or not, at the discretion of the user of the editor.
|
||||
|
||||
Within each patch, the tool will never return multiple edits whose ranges intersect each other. Instead, it will merge them into one edit.
|
||||
On the other hand, for ranges that do not intersect each other, the tool will prefer multiple edits to smaller ranges over one edit to a larger range.
|
||||
|
||||
Whenever edits reference symbols that would be out of scope, the tool will always include earlier edits which add any necessary imports to bring those symbols into scope.
|
||||
|
||||
The overall goal is that if the user of the code editor accepts all edits within all patches, the code will end up in a correct state, and
|
||||
will successfully build and run without any further modifications from the user. It will also have correctly effected the changes to the code base that the user originally requested.
|
||||
@@ -16,16 +16,21 @@ doctest = false
|
||||
anyhow.workspace = true
|
||||
client.workspace = true
|
||||
db.workspace = true
|
||||
editor.workspace = true
|
||||
gpui.workspace = true
|
||||
http_client.workspace = true
|
||||
log.workspace = true
|
||||
markdown_preview.workspace = true
|
||||
menu.workspace = true
|
||||
paths.workspace = true
|
||||
release_channel.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
serde_derive.workspace = true
|
||||
serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
smol.workspace = true
|
||||
tempfile.workspace = true
|
||||
util.workspace = true
|
||||
which.workspace = true
|
||||
workspace.workspace = true
|
||||
|
||||
@@ -1,19 +1,27 @@
|
||||
mod update_notification;
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use client::{Client, TelemetrySettings};
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
use db::RELEASE_CHANNEL;
|
||||
use editor::{Editor, MultiBuffer};
|
||||
use gpui::{
|
||||
actions, AppContext, AsyncAppContext, Context as _, Global, Model, ModelContext,
|
||||
SemanticVersion, Task, WindowContext,
|
||||
SemanticVersion, SharedString, Task, View, ViewContext, VisualContext, WindowContext,
|
||||
};
|
||||
use http_client::{AsyncBody, HttpClient, HttpClientWithUrl};
|
||||
|
||||
use markdown_preview::markdown_preview_view::{MarkdownPreviewMode, MarkdownPreviewView};
|
||||
use paths::remote_servers_dir;
|
||||
use release_channel::{AppCommitSha, ReleaseChannel};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsSources, SettingsStore};
|
||||
use serde::Deserialize;
|
||||
use serde_derive::Serialize;
|
||||
use smol::{fs, io::AsyncReadExt};
|
||||
|
||||
use settings::{Settings, SettingsSources, SettingsStore};
|
||||
use smol::{fs::File, process::Command};
|
||||
|
||||
use http_client::{AsyncBody, HttpClient, HttpClientWithUrl};
|
||||
use release_channel::{AppCommitSha, AppVersion, ReleaseChannel};
|
||||
use std::{
|
||||
env::{
|
||||
self,
|
||||
@@ -24,13 +32,24 @@ use std::{
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
use update_notification::UpdateNotification;
|
||||
use util::ResultExt;
|
||||
use which::which;
|
||||
use workspace::notifications::NotificationId;
|
||||
use workspace::Workspace;
|
||||
|
||||
const SHOULD_SHOW_UPDATE_NOTIFICATION_KEY: &str = "auto-updater-should-show-updated-notification";
|
||||
const POLL_INTERVAL: Duration = Duration::from_secs(60 * 60);
|
||||
|
||||
actions!(auto_update, [Check, DismissErrorMessage, ViewReleaseNotes,]);
|
||||
actions!(
|
||||
auto_update,
|
||||
[
|
||||
Check,
|
||||
DismissErrorMessage,
|
||||
ViewReleaseNotes,
|
||||
ViewReleaseNotesLocally
|
||||
]
|
||||
);
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct UpdateRequestBody {
|
||||
@@ -127,6 +146,12 @@ struct GlobalAutoUpdate(Option<Model<AutoUpdater>>);
|
||||
|
||||
impl Global for GlobalAutoUpdate {}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ReleaseNotesBody {
|
||||
title: String,
|
||||
release_notes: String,
|
||||
}
|
||||
|
||||
pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut AppContext) {
|
||||
AutoUpdateSetting::register(cx);
|
||||
|
||||
@@ -136,6 +161,10 @@ pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut AppContext) {
|
||||
workspace.register_action(|_, action, cx| {
|
||||
view_release_notes(action, cx);
|
||||
});
|
||||
|
||||
workspace.register_action(|workspace, _: &ViewReleaseNotesLocally, cx| {
|
||||
view_release_notes_locally(workspace, cx);
|
||||
});
|
||||
})
|
||||
.detach();
|
||||
|
||||
@@ -235,6 +264,121 @@ pub fn view_release_notes(_: &ViewReleaseNotes, cx: &mut AppContext) -> Option<(
|
||||
None
|
||||
}
|
||||
|
||||
fn view_release_notes_locally(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
|
||||
let release_channel = ReleaseChannel::global(cx);
|
||||
|
||||
let url = match release_channel {
|
||||
ReleaseChannel::Nightly => Some("https://github.com/zed-industries/zed/commits/nightly/"),
|
||||
ReleaseChannel::Dev => Some("https://github.com/zed-industries/zed/commits/main/"),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if let Some(url) = url {
|
||||
cx.open_url(url);
|
||||
return;
|
||||
}
|
||||
|
||||
let version = AppVersion::global(cx).to_string();
|
||||
|
||||
let client = client::Client::global(cx).http_client();
|
||||
let url = client.build_url(&format!(
|
||||
"/api/release_notes/v2/{}/{}",
|
||||
release_channel.dev_name(),
|
||||
version
|
||||
));
|
||||
|
||||
let markdown = workspace
|
||||
.app_state()
|
||||
.languages
|
||||
.language_for_name("Markdown");
|
||||
|
||||
workspace
|
||||
.with_local_workspace(cx, move |_, cx| {
|
||||
cx.spawn(|workspace, mut cx| async move {
|
||||
let markdown = markdown.await.log_err();
|
||||
let response = client.get(&url, Default::default(), true).await;
|
||||
let Some(mut response) = response.log_err() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut body = Vec::new();
|
||||
response.body_mut().read_to_end(&mut body).await.ok();
|
||||
|
||||
let body: serde_json::Result<ReleaseNotesBody> =
|
||||
serde_json::from_slice(body.as_slice());
|
||||
|
||||
if let Ok(body) = body {
|
||||
workspace
|
||||
.update(&mut cx, |workspace, cx| {
|
||||
let project = workspace.project().clone();
|
||||
let buffer = project.update(cx, |project, cx| {
|
||||
project.create_local_buffer("", markdown, cx)
|
||||
});
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit([(0..0, body.release_notes)], None, cx)
|
||||
});
|
||||
let language_registry = project.read(cx).languages().clone();
|
||||
|
||||
let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
|
||||
let tab_description = SharedString::from(body.title.to_string());
|
||||
let editor = cx.new_view(|cx| {
|
||||
Editor::for_multibuffer(buffer, Some(project), true, cx)
|
||||
});
|
||||
let workspace_handle = workspace.weak_handle();
|
||||
let view: View<MarkdownPreviewView> = MarkdownPreviewView::new(
|
||||
MarkdownPreviewMode::Default,
|
||||
editor,
|
||||
workspace_handle,
|
||||
language_registry,
|
||||
Some(tab_description),
|
||||
cx,
|
||||
);
|
||||
workspace.add_item_to_active_pane(
|
||||
Box::new(view.clone()),
|
||||
None,
|
||||
true,
|
||||
cx,
|
||||
);
|
||||
cx.notify();
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub fn notify_of_any_new_update(cx: &mut ViewContext<Workspace>) -> Option<()> {
|
||||
let updater = AutoUpdater::get(cx)?;
|
||||
let version = updater.read(cx).current_version;
|
||||
let should_show_notification = updater.read(cx).should_show_update_notification(cx);
|
||||
|
||||
cx.spawn(|workspace, mut cx| async move {
|
||||
let should_show_notification = should_show_notification.await?;
|
||||
if should_show_notification {
|
||||
workspace.update(&mut cx, |workspace, cx| {
|
||||
let workspace_handle = workspace.weak_handle();
|
||||
workspace.show_notification(
|
||||
NotificationId::unique::<UpdateNotification>(),
|
||||
cx,
|
||||
|cx| cx.new_view(|_| UpdateNotification::new(version, workspace_handle)),
|
||||
);
|
||||
updater.update(cx, |updater, cx| {
|
||||
updater
|
||||
.set_should_show_update_notification(false, cx)
|
||||
.detach_and_log_err(cx);
|
||||
});
|
||||
})?;
|
||||
}
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach();
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
impl AutoUpdater {
|
||||
pub fn get(cx: &mut AppContext) -> Option<Model<Self>> {
|
||||
cx.default_global::<GlobalAutoUpdate>().0.clone()
|
||||
@@ -279,10 +423,6 @@ impl AutoUpdater {
|
||||
}));
|
||||
}
|
||||
|
||||
pub fn current_version(&self) -> SemanticVersion {
|
||||
self.current_version
|
||||
}
|
||||
|
||||
pub fn status(&self) -> AutoUpdateStatus {
|
||||
self.status.clone()
|
||||
}
|
||||
@@ -506,7 +646,7 @@ impl AutoUpdater {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_should_show_update_notification(
|
||||
fn set_should_show_update_notification(
|
||||
&self,
|
||||
should_show: bool,
|
||||
cx: &AppContext,
|
||||
@@ -528,7 +668,7 @@ impl AutoUpdater {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn should_show_update_notification(&self, cx: &AppContext) -> Task<Result<bool>> {
|
||||
fn should_show_update_notification(&self, cx: &AppContext) -> Task<Result<bool>> {
|
||||
cx.background_executor().spawn(async move {
|
||||
Ok(KEY_VALUE_STORE
|
||||
.read_kvp(SHOULD_SHOW_UPDATE_NOTIFICATION_KEY)?
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
[package]
|
||||
name = "auto_update_ui"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/auto_update_ui.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
auto_update.workspace = true
|
||||
client.workspace = true
|
||||
editor.workspace = true
|
||||
gpui.workspace = true
|
||||
http_client.workspace = true
|
||||
markdown_preview.workspace = true
|
||||
menu.workspace = true
|
||||
release_channel.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
smol.workspace = true
|
||||
util.workspace = true
|
||||
workspace.workspace = true
|
||||
@@ -1,147 +0,0 @@
|
||||
mod update_notification;
|
||||
|
||||
use auto_update::AutoUpdater;
|
||||
use editor::{Editor, MultiBuffer};
|
||||
use gpui::{actions, prelude::*, AppContext, SharedString, View, ViewContext};
|
||||
use http_client::HttpClient;
|
||||
use markdown_preview::markdown_preview_view::{MarkdownPreviewMode, MarkdownPreviewView};
|
||||
use release_channel::{AppVersion, ReleaseChannel};
|
||||
use serde::Deserialize;
|
||||
use smol::io::AsyncReadExt;
|
||||
use util::ResultExt as _;
|
||||
use workspace::notifications::NotificationId;
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::update_notification::UpdateNotification;
|
||||
|
||||
actions!(auto_update, [ViewReleaseNotesLocally]);
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.observe_new_views(|workspace: &mut Workspace, _cx| {
|
||||
workspace.register_action(|workspace, _: &ViewReleaseNotesLocally, cx| {
|
||||
view_release_notes_locally(workspace, cx);
|
||||
});
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ReleaseNotesBody {
|
||||
title: String,
|
||||
release_notes: String,
|
||||
}
|
||||
|
||||
fn view_release_notes_locally(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
|
||||
let release_channel = ReleaseChannel::global(cx);
|
||||
|
||||
let url = match release_channel {
|
||||
ReleaseChannel::Nightly => Some("https://github.com/zed-industries/zed/commits/nightly/"),
|
||||
ReleaseChannel::Dev => Some("https://github.com/zed-industries/zed/commits/main/"),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if let Some(url) = url {
|
||||
cx.open_url(url);
|
||||
return;
|
||||
}
|
||||
|
||||
let version = AppVersion::global(cx).to_string();
|
||||
|
||||
let client = client::Client::global(cx).http_client();
|
||||
let url = client.build_url(&format!(
|
||||
"/api/release_notes/v2/{}/{}",
|
||||
release_channel.dev_name(),
|
||||
version
|
||||
));
|
||||
|
||||
let markdown = workspace
|
||||
.app_state()
|
||||
.languages
|
||||
.language_for_name("Markdown");
|
||||
|
||||
workspace
|
||||
.with_local_workspace(cx, move |_, cx| {
|
||||
cx.spawn(|workspace, mut cx| async move {
|
||||
let markdown = markdown.await.log_err();
|
||||
let response = client.get(&url, Default::default(), true).await;
|
||||
let Some(mut response) = response.log_err() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut body = Vec::new();
|
||||
response.body_mut().read_to_end(&mut body).await.ok();
|
||||
|
||||
let body: serde_json::Result<ReleaseNotesBody> =
|
||||
serde_json::from_slice(body.as_slice());
|
||||
|
||||
if let Ok(body) = body {
|
||||
workspace
|
||||
.update(&mut cx, |workspace, cx| {
|
||||
let project = workspace.project().clone();
|
||||
let buffer = project.update(cx, |project, cx| {
|
||||
project.create_local_buffer("", markdown, cx)
|
||||
});
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit([(0..0, body.release_notes)], None, cx)
|
||||
});
|
||||
let language_registry = project.read(cx).languages().clone();
|
||||
|
||||
let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
|
||||
let tab_description = SharedString::from(body.title.to_string());
|
||||
let editor = cx.new_view(|cx| {
|
||||
Editor::for_multibuffer(buffer, Some(project), true, cx)
|
||||
});
|
||||
let workspace_handle = workspace.weak_handle();
|
||||
let view: View<MarkdownPreviewView> = MarkdownPreviewView::new(
|
||||
MarkdownPreviewMode::Default,
|
||||
editor,
|
||||
workspace_handle,
|
||||
language_registry,
|
||||
Some(tab_description),
|
||||
cx,
|
||||
);
|
||||
workspace.add_item_to_active_pane(
|
||||
Box::new(view.clone()),
|
||||
None,
|
||||
true,
|
||||
cx,
|
||||
);
|
||||
cx.notify();
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub fn notify_of_any_new_update(cx: &mut ViewContext<Workspace>) -> Option<()> {
|
||||
let updater = AutoUpdater::get(cx)?;
|
||||
let version = updater.read(cx).current_version();
|
||||
let should_show_notification = updater.read(cx).should_show_update_notification(cx);
|
||||
|
||||
cx.spawn(|workspace, mut cx| async move {
|
||||
let should_show_notification = should_show_notification.await?;
|
||||
if should_show_notification {
|
||||
workspace.update(&mut cx, |workspace, cx| {
|
||||
let workspace_handle = workspace.weak_handle();
|
||||
workspace.show_notification(
|
||||
NotificationId::unique::<UpdateNotification>(),
|
||||
cx,
|
||||
|cx| cx.new_view(|_| UpdateNotification::new(version, workspace_handle)),
|
||||
);
|
||||
updater.update(cx, |updater, cx| {
|
||||
updater
|
||||
.set_should_show_update_notification(false, cx)
|
||||
.detach_and_log_err(cx);
|
||||
});
|
||||
})?;
|
||||
}
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach();
|
||||
|
||||
None
|
||||
}
|
||||
@@ -42,7 +42,7 @@ serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
sha2.workspace = true
|
||||
smol.workspace = true
|
||||
telemetry.workspace = true
|
||||
telemetry_events.workspace = true
|
||||
text.workspace = true
|
||||
thiserror.workspace = true
|
||||
time.workspace = true
|
||||
|
||||
@@ -49,8 +49,8 @@ use thiserror::Error;
|
||||
use url::Url;
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
|
||||
pub use ::telemetry::EventBody;
|
||||
pub use rpc::*;
|
||||
pub use telemetry_events::Event;
|
||||
pub use user::*;
|
||||
|
||||
static ZED_SERVER_URL: LazyLock<Option<String>> =
|
||||
|
||||
@@ -4,8 +4,7 @@ use crate::{ChannelId, TelemetrySettings};
|
||||
use anyhow::Result;
|
||||
use clock::SystemClock;
|
||||
use collections::{HashMap, HashSet};
|
||||
use futures::channel::mpsc;
|
||||
use futures::{Future, StreamExt};
|
||||
use futures::Future;
|
||||
use gpui::{AppContext, BackgroundExecutor, Task};
|
||||
use http_client::{self, AsyncBody, HttpClient, HttpClientWithUrl, Method, Request};
|
||||
use once_cell::sync::Lazy;
|
||||
@@ -17,8 +16,8 @@ use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::time::Instant;
|
||||
use std::{env, mem, path::PathBuf, sync::Arc, time::Duration};
|
||||
use telemetry::{
|
||||
ActionEvent, AppEvent, AssistantEvent, CallEvent, EditEvent, EditorEvent, EventBody,
|
||||
use telemetry_events::{
|
||||
ActionEvent, AppEvent, AssistantEvent, CallEvent, EditEvent, EditorEvent, Event,
|
||||
EventRequestBody, EventWrapper, ExtensionEvent, InlineCompletionEvent, ReplEvent, SettingEvent,
|
||||
};
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
@@ -225,8 +224,6 @@ impl Telemetry {
|
||||
cx.background_executor()
|
||||
.spawn({
|
||||
let state = state.clone();
|
||||
let os_version = os_version();
|
||||
state.lock().os_version = Some(os_version.clone());
|
||||
async move {
|
||||
if let Some(tempfile) = File::create(Self::log_file_path()).log_err() {
|
||||
state.lock().log_file = Some(tempfile);
|
||||
@@ -288,29 +285,12 @@ impl Telemetry {
|
||||
session_id: String,
|
||||
cx: &AppContext,
|
||||
) {
|
||||
let (tx, mut rx) = mpsc::unbounded();
|
||||
|
||||
let mut state = self.state.lock();
|
||||
state.system_id = system_id.map(|id| id.into());
|
||||
state.installation_id = installation_id.map(|id| id.into());
|
||||
state.session_id = Some(session_id);
|
||||
state.app_version = release_channel::AppVersion::global(cx).to_string();
|
||||
state.os_name = os_name();
|
||||
drop(state);
|
||||
|
||||
let this = Arc::downgrade(&self);
|
||||
|
||||
::telemetry::init(tx);
|
||||
cx.background_executor()
|
||||
.spawn(async move {
|
||||
while let Some(event) = rx.next().await {
|
||||
let Some(this) = this.upgrade() else {
|
||||
break;
|
||||
};
|
||||
this.report_event(EventBody::Event(event));
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub fn metrics_enabled(self: &Arc<Self>) -> bool {
|
||||
@@ -346,7 +326,7 @@ impl Telemetry {
|
||||
copilot_enabled_for_language: bool,
|
||||
is_via_ssh: bool,
|
||||
) {
|
||||
let event = EventBody::Editor(EditorEvent {
|
||||
let event = Event::Editor(EditorEvent {
|
||||
file_extension,
|
||||
vim_mode,
|
||||
operation: operation.into(),
|
||||
@@ -364,7 +344,7 @@ impl Telemetry {
|
||||
suggestion_accepted: bool,
|
||||
file_extension: Option<String>,
|
||||
) {
|
||||
let event = EventBody::InlineCompletion(InlineCompletionEvent {
|
||||
let event = Event::InlineCompletion(InlineCompletionEvent {
|
||||
provider,
|
||||
suggestion_accepted,
|
||||
file_extension,
|
||||
@@ -374,7 +354,7 @@ impl Telemetry {
|
||||
}
|
||||
|
||||
pub fn report_assistant_event(self: &Arc<Self>, event: AssistantEvent) {
|
||||
self.report_event(EventBody::Assistant(event));
|
||||
self.report_event(Event::Assistant(event));
|
||||
}
|
||||
|
||||
pub fn report_call_event(
|
||||
@@ -383,7 +363,7 @@ impl Telemetry {
|
||||
room_id: Option<u64>,
|
||||
channel_id: Option<ChannelId>,
|
||||
) {
|
||||
let event = EventBody::Call(CallEvent {
|
||||
let event = Event::Call(CallEvent {
|
||||
operation: operation.to_string(),
|
||||
room_id,
|
||||
channel_id: channel_id.map(|cid| cid.0),
|
||||
@@ -392,8 +372,8 @@ impl Telemetry {
|
||||
self.report_event(event)
|
||||
}
|
||||
|
||||
pub fn report_app_event(self: &Arc<Self>, operation: String) -> EventBody {
|
||||
let event = EventBody::App(AppEvent { operation });
|
||||
pub fn report_app_event(self: &Arc<Self>, operation: String) -> Event {
|
||||
let event = Event::App(AppEvent { operation });
|
||||
|
||||
self.report_event(event.clone());
|
||||
|
||||
@@ -401,7 +381,7 @@ impl Telemetry {
|
||||
}
|
||||
|
||||
pub fn report_setting_event(self: &Arc<Self>, setting: &'static str, value: String) {
|
||||
let event = EventBody::Setting(SettingEvent {
|
||||
let event = Event::Setting(SettingEvent {
|
||||
setting: setting.to_string(),
|
||||
value,
|
||||
});
|
||||
@@ -410,7 +390,7 @@ impl Telemetry {
|
||||
}
|
||||
|
||||
pub fn report_extension_event(self: &Arc<Self>, extension_id: Arc<str>, version: Arc<str>) {
|
||||
self.report_event(EventBody::Extension(ExtensionEvent {
|
||||
self.report_event(Event::Extension(ExtensionEvent {
|
||||
extension_id,
|
||||
version,
|
||||
}))
|
||||
@@ -422,7 +402,7 @@ impl Telemetry {
|
||||
drop(state);
|
||||
|
||||
if let Some((start, end, environment)) = period_data {
|
||||
let event = EventBody::Edit(EditEvent {
|
||||
let event = Event::Edit(EditEvent {
|
||||
duration: end
|
||||
.saturating_duration_since(start)
|
||||
.min(Duration::from_secs(60 * 60 * 24))
|
||||
@@ -436,7 +416,7 @@ impl Telemetry {
|
||||
}
|
||||
|
||||
pub fn report_action_event(self: &Arc<Self>, source: &'static str, action: String) {
|
||||
let event = EventBody::Action(ActionEvent {
|
||||
let event = Event::Action(ActionEvent {
|
||||
source: source.to_string(),
|
||||
action,
|
||||
});
|
||||
@@ -496,7 +476,7 @@ impl Telemetry {
|
||||
kernel_status: String,
|
||||
repl_session_id: String,
|
||||
) {
|
||||
let event = EventBody::Repl(ReplEvent {
|
||||
let event = Event::Repl(ReplEvent {
|
||||
kernel_language,
|
||||
kernel_status,
|
||||
repl_session_id,
|
||||
@@ -505,7 +485,7 @@ impl Telemetry {
|
||||
self.report_event(event)
|
||||
}
|
||||
|
||||
fn report_event(self: &Arc<Self>, event: EventBody) {
|
||||
fn report_event(self: &Arc<Self>, event: Event) {
|
||||
let mut state = self.state.lock();
|
||||
|
||||
if !state.settings.metrics {
|
||||
@@ -687,7 +667,7 @@ mod tests {
|
||||
let event = telemetry.report_app_event(operation.clone());
|
||||
assert_eq!(
|
||||
event,
|
||||
EventBody::App(AppEvent {
|
||||
Event::App(AppEvent {
|
||||
operation: operation.clone(),
|
||||
})
|
||||
);
|
||||
@@ -703,7 +683,7 @@ mod tests {
|
||||
let event = telemetry.report_app_event(operation.clone());
|
||||
assert_eq!(
|
||||
event,
|
||||
EventBody::App(AppEvent {
|
||||
Event::App(AppEvent {
|
||||
operation: operation.clone(),
|
||||
})
|
||||
);
|
||||
@@ -719,7 +699,7 @@ mod tests {
|
||||
let event = telemetry.report_app_event(operation.clone());
|
||||
assert_eq!(
|
||||
event,
|
||||
EventBody::App(AppEvent {
|
||||
Event::App(AppEvent {
|
||||
operation: operation.clone(),
|
||||
})
|
||||
);
|
||||
@@ -736,7 +716,7 @@ mod tests {
|
||||
let event = telemetry.report_app_event(operation.clone());
|
||||
assert_eq!(
|
||||
event,
|
||||
EventBody::App(AppEvent {
|
||||
Event::App(AppEvent {
|
||||
operation: operation.clone(),
|
||||
})
|
||||
);
|
||||
@@ -770,7 +750,7 @@ mod tests {
|
||||
let event = telemetry.report_app_event(operation.clone());
|
||||
assert_eq!(
|
||||
event,
|
||||
EventBody::App(AppEvent {
|
||||
Event::App(AppEvent {
|
||||
operation: operation.clone(),
|
||||
})
|
||||
);
|
||||
|
||||
@@ -64,7 +64,7 @@ sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "postgres", "json"
|
||||
strum.workspace = true
|
||||
subtle.workspace = true
|
||||
supermaven_api.workspace = true
|
||||
telemetry.workspace = true
|
||||
telemetry_events.workspace = true
|
||||
text.workspace = true
|
||||
thiserror.workspace = true
|
||||
time.workspace = true
|
||||
|
||||
@@ -18,8 +18,8 @@ use serde::{Deserialize, Serialize, Serializer};
|
||||
use serde_json::json;
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::sync::{Arc, OnceLock};
|
||||
use telemetry::{
|
||||
ActionEvent, AppEvent, AssistantEvent, CallEvent, CpuEvent, EditEvent, EditorEvent, EventBody,
|
||||
use telemetry_events::{
|
||||
ActionEvent, AppEvent, AssistantEvent, CallEvent, CpuEvent, EditEvent, EditorEvent, Event,
|
||||
EventRequestBody, EventWrapper, ExtensionEvent, InlineCompletionEvent, MemoryEvent, Panic,
|
||||
ReplEvent, SettingEvent,
|
||||
};
|
||||
@@ -240,7 +240,7 @@ pub async fn post_hang(
|
||||
.ok();
|
||||
}
|
||||
|
||||
let report: telemetry::HangReport = serde_json::from_slice(&body).map_err(|err| {
|
||||
let report: telemetry_events::HangReport = serde_json::from_slice(&body).map_err(|err| {
|
||||
log::error!("can't parse report json: {err}");
|
||||
Error::Internal(anyhow!(err))
|
||||
})?;
|
||||
@@ -283,7 +283,7 @@ pub async fn post_panic(
|
||||
))?;
|
||||
}
|
||||
|
||||
let report: telemetry::PanicRequest = serde_json::from_slice(&body)
|
||||
let report: telemetry_events::PanicRequest = serde_json::from_slice(&body)
|
||||
.map_err(|_| Error::http(StatusCode::BAD_REQUEST, "invalid json".into()))?;
|
||||
let panic = report.panic;
|
||||
|
||||
@@ -400,7 +400,7 @@ pub async fn post_events(
|
||||
|
||||
let checksum_matched = checksum == expected;
|
||||
|
||||
let request_body: telemetry::EventRequestBody =
|
||||
let request_body: telemetry_events::EventRequestBody =
|
||||
serde_json::from_slice(&body).map_err(|err| {
|
||||
log::error!("can't parse event json: {err}");
|
||||
Error::Internal(anyhow!(err))
|
||||
@@ -445,8 +445,7 @@ pub async fn post_events(
|
||||
|
||||
for wrapper in &request_body.events {
|
||||
match &wrapper.event {
|
||||
EventBody::Event(_) => continue,
|
||||
EventBody::Editor(event) => to_upload.editor_events.push(EditorEventRow::from_event(
|
||||
Event::Editor(event) => to_upload.editor_events.push(EditorEventRow::from_event(
|
||||
event.clone(),
|
||||
wrapper,
|
||||
&request_body,
|
||||
@@ -454,7 +453,7 @@ pub async fn post_events(
|
||||
country_code.clone(),
|
||||
checksum_matched,
|
||||
)),
|
||||
EventBody::InlineCompletion(event) => {
|
||||
Event::InlineCompletion(event) => {
|
||||
to_upload
|
||||
.inline_completion_events
|
||||
.push(InlineCompletionEventRow::from_event(
|
||||
@@ -466,14 +465,14 @@ pub async fn post_events(
|
||||
checksum_matched,
|
||||
))
|
||||
}
|
||||
EventBody::Call(event) => to_upload.call_events.push(CallEventRow::from_event(
|
||||
Event::Call(event) => to_upload.call_events.push(CallEventRow::from_event(
|
||||
event.clone(),
|
||||
wrapper,
|
||||
&request_body,
|
||||
first_event_at,
|
||||
checksum_matched,
|
||||
)),
|
||||
EventBody::Assistant(event) => {
|
||||
Event::Assistant(event) => {
|
||||
to_upload
|
||||
.assistant_events
|
||||
.push(AssistantEventRow::from_event(
|
||||
@@ -484,38 +483,36 @@ pub async fn post_events(
|
||||
checksum_matched,
|
||||
))
|
||||
}
|
||||
EventBody::Cpu(_) | EventBody::Memory(_) => continue,
|
||||
EventBody::App(event) => to_upload.app_events.push(AppEventRow::from_event(
|
||||
Event::Cpu(_) | Event::Memory(_) => continue,
|
||||
Event::App(event) => to_upload.app_events.push(AppEventRow::from_event(
|
||||
event.clone(),
|
||||
wrapper,
|
||||
&request_body,
|
||||
first_event_at,
|
||||
checksum_matched,
|
||||
)),
|
||||
EventBody::Setting(event) => {
|
||||
to_upload.setting_events.push(SettingEventRow::from_event(
|
||||
event.clone(),
|
||||
wrapper,
|
||||
&request_body,
|
||||
first_event_at,
|
||||
checksum_matched,
|
||||
))
|
||||
}
|
||||
EventBody::Edit(event) => to_upload.edit_events.push(EditEventRow::from_event(
|
||||
Event::Setting(event) => to_upload.setting_events.push(SettingEventRow::from_event(
|
||||
event.clone(),
|
||||
wrapper,
|
||||
&request_body,
|
||||
first_event_at,
|
||||
checksum_matched,
|
||||
)),
|
||||
EventBody::Action(event) => to_upload.action_events.push(ActionEventRow::from_event(
|
||||
Event::Edit(event) => to_upload.edit_events.push(EditEventRow::from_event(
|
||||
event.clone(),
|
||||
wrapper,
|
||||
&request_body,
|
||||
first_event_at,
|
||||
checksum_matched,
|
||||
)),
|
||||
EventBody::Extension(event) => {
|
||||
Event::Action(event) => to_upload.action_events.push(ActionEventRow::from_event(
|
||||
event.clone(),
|
||||
wrapper,
|
||||
&request_body,
|
||||
first_event_at,
|
||||
checksum_matched,
|
||||
)),
|
||||
Event::Extension(event) => {
|
||||
let metadata = app
|
||||
.db
|
||||
.get_extension_version(&event.extension_id, &event.version)
|
||||
@@ -531,7 +528,7 @@ pub async fn post_events(
|
||||
checksum_matched,
|
||||
))
|
||||
}
|
||||
EventBody::Repl(event) => to_upload.repl_events.push(ReplEventRow::from_event(
|
||||
Event::Repl(event) => to_upload.repl_events.push(ReplEventRow::from_event(
|
||||
event.clone(),
|
||||
wrapper,
|
||||
&request_body,
|
||||
@@ -1390,7 +1387,7 @@ fn for_snowflake(
|
||||
let timestamp =
|
||||
first_event_at + Duration::milliseconds(event.milliseconds_since_first_event);
|
||||
let (event_type, mut event_properties) = match &event.event {
|
||||
EventBody::Editor(e) => (
|
||||
Event::Editor(e) => (
|
||||
match e.operation.as_str() {
|
||||
"open" => "Editor Opened".to_string(),
|
||||
"save" => "Editor Saved".to_string(),
|
||||
@@ -1398,8 +1395,7 @@ fn for_snowflake(
|
||||
},
|
||||
serde_json::to_value(e).unwrap(),
|
||||
),
|
||||
EventBody::Event(e) => (e.name.clone(), serde_json::to_value(&e.properties).unwrap()),
|
||||
EventBody::InlineCompletion(e) => (
|
||||
Event::InlineCompletion(e) => (
|
||||
format!(
|
||||
"Inline Completion {}",
|
||||
if e.suggestion_accepted {
|
||||
@@ -1410,7 +1406,7 @@ fn for_snowflake(
|
||||
),
|
||||
serde_json::to_value(e).unwrap(),
|
||||
),
|
||||
EventBody::Call(e) => {
|
||||
Event::Call(e) => {
|
||||
let event_type = match e.operation.trim() {
|
||||
"unshare project" => "Project Unshared".to_string(),
|
||||
"open channel notes" => "Channel Notes Opened".to_string(),
|
||||
@@ -1431,21 +1427,21 @@ fn for_snowflake(
|
||||
|
||||
(event_type, serde_json::to_value(e).unwrap())
|
||||
}
|
||||
EventBody::Assistant(e) => (
|
||||
Event::Assistant(e) => (
|
||||
match e.phase {
|
||||
telemetry::AssistantPhase::Response => "Assistant Responded".to_string(),
|
||||
telemetry::AssistantPhase::Invoked => "Assistant Invoked".to_string(),
|
||||
telemetry::AssistantPhase::Accepted => {
|
||||
telemetry_events::AssistantPhase::Response => "Assistant Responded".to_string(),
|
||||
telemetry_events::AssistantPhase::Invoked => "Assistant Invoked".to_string(),
|
||||
telemetry_events::AssistantPhase::Accepted => {
|
||||
"Assistant Response Accepted".to_string()
|
||||
}
|
||||
telemetry::AssistantPhase::Rejected => {
|
||||
telemetry_events::AssistantPhase::Rejected => {
|
||||
"Assistant Response Rejected".to_string()
|
||||
}
|
||||
},
|
||||
serde_json::to_value(e).unwrap(),
|
||||
),
|
||||
EventBody::Cpu(_) | EventBody::Memory(_) => return None,
|
||||
EventBody::App(e) => {
|
||||
Event::Cpu(_) | Event::Memory(_) => return None,
|
||||
Event::App(e) => {
|
||||
let mut properties = json!({});
|
||||
let event_type = match e.operation.trim() {
|
||||
"extensions: install extension" => "Extension Installed".to_string(),
|
||||
@@ -1526,23 +1522,23 @@ fn for_snowflake(
|
||||
};
|
||||
(event_type, properties)
|
||||
}
|
||||
EventBody::Setting(e) => (
|
||||
Event::Setting(e) => (
|
||||
"Settings Changed".to_string(),
|
||||
serde_json::to_value(e).unwrap(),
|
||||
),
|
||||
EventBody::Extension(e) => (
|
||||
Event::Extension(e) => (
|
||||
"Extension Loaded".to_string(),
|
||||
serde_json::to_value(e).unwrap(),
|
||||
),
|
||||
EventBody::Edit(e) => (
|
||||
Event::Edit(e) => (
|
||||
"Editor Edited".to_string(),
|
||||
serde_json::to_value(e).unwrap(),
|
||||
),
|
||||
EventBody::Action(e) => (
|
||||
Event::Action(e) => (
|
||||
"Action Invoked".to_string(),
|
||||
serde_json::to_value(e).unwrap(),
|
||||
),
|
||||
EventBody::Repl(e) => (
|
||||
Event::Repl(e) => (
|
||||
"Kernel Status Changed".to_string(),
|
||||
serde_json::to_value(e).unwrap(),
|
||||
),
|
||||
@@ -1559,15 +1555,15 @@ fn for_snowflake(
|
||||
);
|
||||
map.insert("signed_in".to_string(), event.signed_in.into());
|
||||
if let Some(country_code) = country_code.as_ref() {
|
||||
map.insert("country".to_string(), country_code.clone().into());
|
||||
map.insert("country_code".to_string(), country_code.clone().into());
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: most amplitude user properties are read out of our event_properties
|
||||
// dictionary. See https://app.amplitude.com/data/zed/Zed/sources/detail/production/falcon%3A159998
|
||||
// for how that is configured.
|
||||
let user_properties = Some(serde_json::json!({
|
||||
"is_staff": body.is_staff,
|
||||
"Country": country_code.clone(),
|
||||
"OS": format!("{} {}", body.os_name, body.os_version.clone().unwrap_or_default()),
|
||||
"Version": body.app_version.clone(),
|
||||
}));
|
||||
|
||||
Some(SnowflakeRow {
|
||||
@@ -1582,7 +1578,7 @@ fn for_snowflake(
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct SnowflakeRow {
|
||||
pub time: chrono::DateTime<chrono::Utc>,
|
||||
pub user_id: Option<String>,
|
||||
@@ -1592,3 +1588,48 @@ struct SnowflakeRow {
|
||||
pub user_properties: Option<serde_json::Value>,
|
||||
pub insert_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct SnowflakeData {
|
||||
/// Identifier unique to each Zed installation (differs for stable, preview, dev)
|
||||
pub installation_id: Option<String>,
|
||||
/// Identifier unique to each logged in Zed user (randomly generated on first sign in)
|
||||
/// Identifier unique to each Zed session (differs for each time you open Zed)
|
||||
pub session_id: Option<String>,
|
||||
pub metrics_id: Option<String>,
|
||||
/// True for Zed staff, otherwise false
|
||||
pub is_staff: Option<bool>,
|
||||
/// Zed version number
|
||||
pub app_version: String,
|
||||
pub os_name: String,
|
||||
pub os_version: Option<String>,
|
||||
pub architecture: String,
|
||||
/// Zed release channel (stable, preview, dev)
|
||||
pub release_channel: Option<String>,
|
||||
pub signed_in: bool,
|
||||
|
||||
#[serde(flatten)]
|
||||
pub editor_event: Option<EditorEvent>,
|
||||
#[serde(flatten)]
|
||||
pub inline_completion_event: Option<InlineCompletionEvent>,
|
||||
#[serde(flatten)]
|
||||
pub call_event: Option<CallEvent>,
|
||||
#[serde(flatten)]
|
||||
pub assistant_event: Option<AssistantEvent>,
|
||||
#[serde(flatten)]
|
||||
pub cpu_event: Option<CpuEvent>,
|
||||
#[serde(flatten)]
|
||||
pub memory_event: Option<MemoryEvent>,
|
||||
#[serde(flatten)]
|
||||
pub app_event: Option<AppEvent>,
|
||||
#[serde(flatten)]
|
||||
pub setting_event: Option<SettingEvent>,
|
||||
#[serde(flatten)]
|
||||
pub extension_event: Option<ExtensionEvent>,
|
||||
#[serde(flatten)]
|
||||
pub edit_event: Option<EditEvent>,
|
||||
#[serde(flatten)]
|
||||
pub repl_event: Option<ReplEvent>,
|
||||
#[serde(flatten)]
|
||||
pub action_event: Option<ActionEvent>,
|
||||
}
|
||||
|
||||
@@ -835,7 +835,7 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||
.map_ok(|_| ())
|
||||
.boxed(),
|
||||
LspRequestKind::CodeAction => project
|
||||
.code_actions(&buffer, offset..offset, None, cx)
|
||||
.code_actions(&buffer, offset..offset, cx)
|
||||
.map(|_| Ok(()))
|
||||
.boxed(),
|
||||
LspRequestKind::Definition => project
|
||||
|
||||
@@ -58,11 +58,12 @@ settings.workspace = true
|
||||
smallvec.workspace = true
|
||||
story = { workspace = true, optional = true }
|
||||
theme.workspace = true
|
||||
time.workspace = true
|
||||
time_format.workspace = true
|
||||
time.workspace = true
|
||||
title_bar.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
vcs_menu.workspace = true
|
||||
workspace.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -33,6 +33,7 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
||||
notification_panel::init(cx);
|
||||
notifications::init(app_state, cx);
|
||||
title_bar::init(cx);
|
||||
vcs_menu::init(cx);
|
||||
}
|
||||
|
||||
fn notification_window_options(
|
||||
|
||||
@@ -11,7 +11,7 @@ use command_palette_hooks::{
|
||||
};
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use gpui::{
|
||||
Action, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Global,
|
||||
actions, Action, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Global,
|
||||
ParentElement, Render, Styled, Task, UpdateGlobal, View, ViewContext, VisualContext, WeakView,
|
||||
};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
@@ -21,7 +21,9 @@ use settings::Settings;
|
||||
use ui::{h_flex, prelude::*, v_flex, HighlightedLabel, KeyBinding, ListItem, ListItemSpacing};
|
||||
use util::ResultExt;
|
||||
use workspace::{ModalView, Workspace, WorkspaceSettings};
|
||||
use zed_actions::{command_palette::Toggle, OpenZedUrl};
|
||||
use zed_actions::OpenZedUrl;
|
||||
|
||||
actions!(command_palette, [Toggle]);
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
client::init_settings(cx);
|
||||
|
||||
@@ -9,7 +9,7 @@ use serde_json::{value::RawValue, Value};
|
||||
use smol::{
|
||||
channel,
|
||||
io::{AsyncBufReadExt, AsyncWriteExt, BufReader},
|
||||
process::Child,
|
||||
process::{self, Child},
|
||||
};
|
||||
use std::{
|
||||
fmt,
|
||||
@@ -152,7 +152,7 @@ impl Client {
|
||||
&binary.args
|
||||
);
|
||||
|
||||
let mut command = util::command::new_smol_command(&binary.executable);
|
||||
let mut command = process::Command::new(&binary.executable);
|
||||
command
|
||||
.args(&binary.args)
|
||||
.envs(binary.env.unwrap_or_default())
|
||||
|
||||
@@ -24,8 +24,6 @@ use gpui::{AsyncAppContext, EventEmitter, Model, ModelContext, Subscription, Tas
|
||||
use log;
|
||||
use parking_lot::RwLock;
|
||||
use project::Project;
|
||||
use schemars::gen::SchemaGenerator;
|
||||
use schemars::schema::{InstanceType, Schema, SchemaObject};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsSources, SettingsStore};
|
||||
@@ -38,32 +36,16 @@ use crate::{
|
||||
|
||||
#[derive(Deserialize, Serialize, Default, Clone, PartialEq, Eq, JsonSchema, Debug)]
|
||||
pub struct ContextServerSettings {
|
||||
/// Settings for context servers used in the Assistant.
|
||||
#[serde(default)]
|
||||
pub context_servers: HashMap<Arc<str>, ServerConfig>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema, Debug, Default)]
|
||||
pub struct ServerConfig {
|
||||
/// The command to run this context server.
|
||||
///
|
||||
/// This will override the command set by an extension.
|
||||
pub command: Option<ServerCommand>,
|
||||
/// The settings for this context server.
|
||||
///
|
||||
/// Consult the documentation for the context server to see what settings
|
||||
/// are supported.
|
||||
#[schemars(schema_with = "server_config_settings_json_schema")]
|
||||
pub settings: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
fn server_config_settings_json_schema(_generator: &mut SchemaGenerator) -> Schema {
|
||||
Schema::Object(SchemaObject {
|
||||
instance_type: Some(InstanceType::Object.into()),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema, Debug)]
|
||||
pub struct ServerCommand {
|
||||
pub path: String,
|
||||
|
||||
@@ -29,14 +29,14 @@ anyhow.workspace = true
|
||||
async-compression.workspace = true
|
||||
async-tar.workspace = true
|
||||
chrono.workspace = true
|
||||
client.workspace = true
|
||||
collections.workspace = true
|
||||
client.workspace = true
|
||||
command_palette_hooks.workspace = true
|
||||
editor.workspace = true
|
||||
fs.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
http_client.workspace = true
|
||||
inline_completion.workspace = true
|
||||
language.workspace = true
|
||||
lsp.workspace = true
|
||||
menu.workspace = true
|
||||
@@ -44,12 +44,12 @@ node_runtime.workspace = true
|
||||
parking_lot.workspace = true
|
||||
paths.workspace = true
|
||||
project.workspace = true
|
||||
schemars = { workspace = true, optional = true }
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
schemars = { workspace = true, optional = true }
|
||||
strum.workspace = true
|
||||
settings.workspace = true
|
||||
smol.workspace = true
|
||||
strum.workspace = true
|
||||
task.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
|
||||
@@ -38,8 +38,8 @@ use std::{
|
||||
};
|
||||
use util::{fs::remove_matching, maybe, ResultExt};
|
||||
|
||||
pub use crate::copilot_completion_provider::CopilotCompletionProvider;
|
||||
pub use crate::sign_in::{initiate_sign_in, CopilotCodeVerification};
|
||||
pub use copilot_completion_provider::CopilotCompletionProvider;
|
||||
pub use sign_in::CopilotCodeVerification;
|
||||
|
||||
actions!(
|
||||
copilot,
|
||||
@@ -1231,7 +1231,7 @@ mod tests {
|
||||
|
||||
fn disk_state(&self) -> language::DiskState {
|
||||
language::DiskState::Present {
|
||||
mtime: ::fs::MTime::from_seconds_and_nanos(100, 42),
|
||||
mtime: std::time::UNIX_EPOCH,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use crate::{Completion, Copilot};
|
||||
use anyhow::Result;
|
||||
use client::telemetry::Telemetry;
|
||||
use editor::{CompletionProposal, Direction, InlayProposal, InlineCompletionProvider};
|
||||
use gpui::{AppContext, EntityId, Model, ModelContext, Task};
|
||||
use inline_completion::{CompletionProposal, Direction, InlayProposal, InlineCompletionProvider};
|
||||
use language::{
|
||||
language_settings::{all_language_settings, AllLanguageSettings},
|
||||
Buffer, OffsetRangeExt, ToOffset,
|
||||
|
||||
@@ -5,79 +5,10 @@ use gpui::{
|
||||
Styled, Subscription, ViewContext,
|
||||
};
|
||||
use ui::{prelude::*, Button, Label, Vector, VectorName};
|
||||
use util::ResultExt as _;
|
||||
use workspace::notifications::NotificationId;
|
||||
use workspace::{ModalView, Toast, Workspace};
|
||||
use workspace::ModalView;
|
||||
|
||||
const COPILOT_SIGN_UP_URL: &str = "https://github.com/features/copilot";
|
||||
|
||||
struct CopilotStartingToast;
|
||||
|
||||
pub fn initiate_sign_in(cx: &mut WindowContext) {
|
||||
let Some(copilot) = Copilot::global(cx) else {
|
||||
return;
|
||||
};
|
||||
let status = copilot.read(cx).status();
|
||||
let Some(workspace) = cx.window_handle().downcast::<Workspace>() else {
|
||||
return;
|
||||
};
|
||||
match status {
|
||||
Status::Starting { task } => {
|
||||
let Some(workspace) = cx.window_handle().downcast::<Workspace>() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Ok(workspace) = workspace.update(cx, |workspace, cx| {
|
||||
workspace.show_toast(
|
||||
Toast::new(
|
||||
NotificationId::unique::<CopilotStartingToast>(),
|
||||
"Copilot is starting...",
|
||||
),
|
||||
cx,
|
||||
);
|
||||
workspace.weak_handle()
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
|
||||
cx.spawn(|mut cx| async move {
|
||||
task.await;
|
||||
if let Some(copilot) = cx.update(|cx| Copilot::global(cx)).ok().flatten() {
|
||||
workspace
|
||||
.update(&mut cx, |workspace, cx| match copilot.read(cx).status() {
|
||||
Status::Authorized => workspace.show_toast(
|
||||
Toast::new(
|
||||
NotificationId::unique::<CopilotStartingToast>(),
|
||||
"Copilot has started!",
|
||||
),
|
||||
cx,
|
||||
),
|
||||
_ => {
|
||||
workspace.dismiss_toast(
|
||||
&NotificationId::unique::<CopilotStartingToast>(),
|
||||
cx,
|
||||
);
|
||||
copilot
|
||||
.update(cx, |copilot, cx| copilot.sign_in(cx))
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
_ => {
|
||||
copilot.update(cx, |this, cx| this.sign_in(cx)).detach();
|
||||
workspace
|
||||
.update(cx, |this, cx| {
|
||||
this.toggle_modal(cx, |cx| CopilotCodeVerification::new(&copilot, cx));
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CopilotCodeVerification {
|
||||
status: Status,
|
||||
connect_clicked: bool,
|
||||
|
||||
@@ -776,7 +776,7 @@ impl Item for ProjectDiagnosticsEditor {
|
||||
}
|
||||
}
|
||||
|
||||
fn breadcrumb_location(&self, _: &AppContext) -> ToolbarItemLocation {
|
||||
fn breadcrumb_location(&self) -> ToolbarItemLocation {
|
||||
ToolbarItemLocation::PrimaryLeft
|
||||
}
|
||||
|
||||
|
||||
@@ -42,12 +42,10 @@ emojis.workspace = true
|
||||
file_icons.workspace = true
|
||||
futures.workspace = true
|
||||
fuzzy.workspace = true
|
||||
fs.workspace = true
|
||||
git.workspace = true
|
||||
gpui.workspace = true
|
||||
http_client.workspace = true
|
||||
indoc.workspace = true
|
||||
inline_completion.workspace = true
|
||||
itertools.workspace = true
|
||||
language.workspace = true
|
||||
linkify.workspace = true
|
||||
|
||||
@@ -271,8 +271,6 @@ gpui::actions!(
|
||||
Hover,
|
||||
Indent,
|
||||
JoinLines,
|
||||
KillRingCut,
|
||||
KillRingYank,
|
||||
LineDown,
|
||||
LineUp,
|
||||
MoveDown,
|
||||
|
||||
@@ -28,6 +28,7 @@ mod hover_popover;
|
||||
mod hunk_diff;
|
||||
mod indent_guides;
|
||||
mod inlay_hint_cache;
|
||||
mod inline_completion_provider;
|
||||
pub mod items;
|
||||
mod linked_editing_ranges;
|
||||
mod lsp_ext;
|
||||
@@ -74,7 +75,7 @@ use gpui::{
|
||||
div, impl_actions, point, prelude::*, px, relative, size, uniform_list, Action, AnyElement,
|
||||
AppContext, AsyncWindowContext, AvailableSpace, BackgroundExecutor, Bounds, ClipboardEntry,
|
||||
ClipboardItem, Context, DispatchPhase, ElementId, EventEmitter, FocusHandle, FocusOutEvent,
|
||||
FocusableView, FontId, FontWeight, Global, HighlightStyle, Hsla, InteractiveText, KeyContext,
|
||||
FocusableView, FontId, FontWeight, HighlightStyle, Hsla, InteractiveText, KeyContext,
|
||||
ListSizingBehavior, Model, ModelContext, MouseButton, PaintQuad, ParentElement, Pixels, Render,
|
||||
ScrollStrategy, SharedString, Size, StrikethroughStyle, Styled, StyledText, Subscription, Task,
|
||||
TextStyle, TextStyleRefinement, UTF16Selection, UnderlineStyle, UniformListScrollHandle, View,
|
||||
@@ -86,8 +87,7 @@ pub(crate) use hunk_diff::HoveredHunk;
|
||||
use hunk_diff::{diff_hunk_to_display, ExpandedHunks};
|
||||
use indent_guides::ActiveIndentGuidesState;
|
||||
use inlay_hint_cache::{InlayHintCache, InlaySplice, InvalidationStrategy};
|
||||
pub use inline_completion::Direction;
|
||||
use inline_completion::{InlayProposal, InlineCompletionProvider, InlineCompletionProviderHandle};
|
||||
pub use inline_completion_provider::*;
|
||||
pub use items::MAX_TAB_TITLE_LEN;
|
||||
use itertools::Itertools;
|
||||
use language::{
|
||||
@@ -273,6 +273,12 @@ enum DocumentHighlightRead {}
|
||||
enum DocumentHighlightWrite {}
|
||||
enum InputComposition {}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
pub enum Direction {
|
||||
Prev,
|
||||
Next,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
pub enum Navigated {
|
||||
Yes,
|
||||
@@ -7364,7 +7370,7 @@ impl Editor {
|
||||
.update(cx, |buffer, cx| buffer.edit(edits, None, cx));
|
||||
}
|
||||
|
||||
pub fn cut_common(&mut self, cx: &mut ViewContext<Self>) -> ClipboardItem {
|
||||
pub fn cut(&mut self, _: &Cut, cx: &mut ViewContext<Self>) {
|
||||
let mut text = String::new();
|
||||
let buffer = self.buffer.read(cx).snapshot(cx);
|
||||
let mut selections = self.selections.all::<Point>(cx);
|
||||
@@ -7408,38 +7414,11 @@ impl Editor {
|
||||
s.select(selections);
|
||||
});
|
||||
this.insert("", cx);
|
||||
cx.write_to_clipboard(ClipboardItem::new_string_with_json_metadata(
|
||||
text,
|
||||
clipboard_selections,
|
||||
));
|
||||
});
|
||||
ClipboardItem::new_string_with_json_metadata(text, clipboard_selections)
|
||||
}
|
||||
|
||||
pub fn cut(&mut self, _: &Cut, cx: &mut ViewContext<Self>) {
|
||||
let item = self.cut_common(cx);
|
||||
cx.write_to_clipboard(item);
|
||||
}
|
||||
|
||||
pub fn kill_ring_cut(&mut self, _: &KillRingCut, cx: &mut ViewContext<Self>) {
|
||||
self.change_selections(None, cx, |s| {
|
||||
s.move_with(|snapshot, sel| {
|
||||
if sel.is_empty() {
|
||||
sel.end = DisplayPoint::new(sel.end.row(), snapshot.line_len(sel.end.row()))
|
||||
}
|
||||
});
|
||||
});
|
||||
let item = self.cut_common(cx);
|
||||
cx.set_global(KillRing(item))
|
||||
}
|
||||
|
||||
pub fn kill_ring_yank(&mut self, _: &KillRingYank, cx: &mut ViewContext<Self>) {
|
||||
let (text, metadata) = if let Some(KillRing(item)) = cx.try_global() {
|
||||
if let Some(ClipboardEntry::String(kill_ring)) = item.entries().first() {
|
||||
(kill_ring.text().to_string(), kill_ring.metadata_json())
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
self.do_paste(&text, metadata, false, cx);
|
||||
}
|
||||
|
||||
pub fn copy(&mut self, _: &Copy, cx: &mut ViewContext<Self>) {
|
||||
@@ -11896,15 +11875,7 @@ impl Editor {
|
||||
style: &EditorStyle,
|
||||
cx: &mut WindowContext,
|
||||
) -> Option<AnyElement> {
|
||||
let selection = self.selections.newest::<Point>(cx);
|
||||
if !selection.is_empty() {
|
||||
return None;
|
||||
};
|
||||
|
||||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
||||
let buffer_row = MultiBufferRow(selection.head().row);
|
||||
|
||||
if snapshot.line_len(buffer_row) != 0 || self.has_active_inline_completion(cx) {
|
||||
if !self.newest_selection_head_on_empty_line(cx) || self.has_active_inline_completion(cx) {
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -13811,9 +13782,7 @@ impl CodeActionProvider for Model<Project> {
|
||||
range: Range<text::Anchor>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<Result<Vec<CodeAction>>> {
|
||||
self.update(cx, |project, cx| {
|
||||
project.code_actions(buffer, range, None, cx)
|
||||
})
|
||||
self.update(cx, |project, cx| project.code_actions(buffer, range, cx))
|
||||
}
|
||||
|
||||
fn apply_code_action(
|
||||
@@ -14457,16 +14426,15 @@ impl ViewInputHandler for Editor {
|
||||
fn text_for_range(
|
||||
&mut self,
|
||||
range_utf16: Range<usize>,
|
||||
adjusted_range: &mut Option<Range<usize>>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<String> {
|
||||
let snapshot = self.buffer.read(cx).read(cx);
|
||||
let start = snapshot.clip_offset_utf16(OffsetUtf16(range_utf16.start), Bias::Left);
|
||||
let end = snapshot.clip_offset_utf16(OffsetUtf16(range_utf16.end), Bias::Right);
|
||||
if (start.0..end.0) != range_utf16 {
|
||||
adjusted_range.replace(start.0..end.0);
|
||||
}
|
||||
Some(snapshot.text_for_range(start..end).collect())
|
||||
Some(
|
||||
self.buffer
|
||||
.read(cx)
|
||||
.read(cx)
|
||||
.text_for_range(OffsetUtf16(range_utf16.start)..OffsetUtf16(range_utf16.end))
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
fn selected_text_range(
|
||||
@@ -15174,7 +15142,4 @@ fn check_multiline_range(buffer: &Buffer, range: Range<usize>) -> Range<usize> {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct KillRing(ClipboardItem);
|
||||
impl Global for KillRing {}
|
||||
|
||||
const UPDATE_DEBOUNCE: Duration = Duration::from_millis(50);
|
||||
|
||||
@@ -217,8 +217,6 @@ impl EditorElement {
|
||||
register_action(view, cx, Editor::transpose);
|
||||
register_action(view, cx, Editor::rewrap);
|
||||
register_action(view, cx, Editor::cut);
|
||||
register_action(view, cx, Editor::kill_ring_cut);
|
||||
register_action(view, cx, Editor::kill_ring_yank);
|
||||
register_action(view, cx, Editor::copy);
|
||||
register_action(view, cx, Editor::paste);
|
||||
register_action(view, cx, Editor::undo);
|
||||
|
||||
@@ -1,18 +1,9 @@
|
||||
use crate::Direction;
|
||||
use gpui::{AppContext, Model, ModelContext};
|
||||
use language::Buffer;
|
||||
use std::ops::Range;
|
||||
use text::{Anchor, Rope};
|
||||
|
||||
// TODO: Find a better home for `Direction`.
|
||||
//
|
||||
// This should live in an ancestor crate of `editor` and `inline_completion`,
|
||||
// but at time of writing there isn't an obvious spot.
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
pub enum Direction {
|
||||
Prev,
|
||||
Next,
|
||||
}
|
||||
|
||||
pub enum InlayProposal {
|
||||
Hint(Anchor, project::InlayHint),
|
||||
Suggestion(Anchor, Rope),
|
||||
@@ -841,7 +841,7 @@ impl Item for Editor {
|
||||
self.pixel_position_of_newest_cursor
|
||||
}
|
||||
|
||||
fn breadcrumb_location(&self, _: &AppContext) -> ToolbarItemLocation {
|
||||
fn breadcrumb_location(&self) -> ToolbarItemLocation {
|
||||
if self.show_breadcrumbs {
|
||||
ToolbarItemLocation::PrimaryLeft
|
||||
} else {
|
||||
@@ -1618,14 +1618,15 @@ fn path_for_file<'a>(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::editor_tests::init_test;
|
||||
use fs::Fs;
|
||||
|
||||
use super::*;
|
||||
use fs::MTime;
|
||||
use gpui::{AppContext, VisualTestContext};
|
||||
use language::{LanguageMatcher, TestFile};
|
||||
use project::FakeFs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
time::SystemTime,
|
||||
};
|
||||
|
||||
#[gpui::test]
|
||||
fn test_path_for_file(cx: &mut AppContext) {
|
||||
@@ -1678,7 +1679,9 @@ mod tests {
|
||||
async fn test_deserialize(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let now = SystemTime::now();
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.set_next_mtime(now);
|
||||
fs.insert_file("/file.rs", Default::default()).await;
|
||||
|
||||
// Test case 1: Deserialize with path and contents
|
||||
@@ -1687,18 +1690,12 @@ mod tests {
|
||||
let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
|
||||
let workspace_id = workspace::WORKSPACE_DB.next_id().await.unwrap();
|
||||
let item_id = 1234 as ItemId;
|
||||
let mtime = fs
|
||||
.metadata(Path::new("/file.rs"))
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.mtime;
|
||||
|
||||
let serialized_editor = SerializedEditor {
|
||||
abs_path: Some(PathBuf::from("/file.rs")),
|
||||
contents: Some("fn main() {}".to_string()),
|
||||
language: Some("Rust".to_string()),
|
||||
mtime: Some(mtime),
|
||||
mtime: Some(now),
|
||||
};
|
||||
|
||||
DB.save_serialized_editor(item_id, workspace_id, serialized_editor.clone())
|
||||
@@ -1795,7 +1792,9 @@ mod tests {
|
||||
let workspace_id = workspace::WORKSPACE_DB.next_id().await.unwrap();
|
||||
|
||||
let item_id = 9345 as ItemId;
|
||||
let old_mtime = MTime::from_seconds_and_nanos(0, 50);
|
||||
let old_mtime = now
|
||||
.checked_sub(std::time::Duration::from_secs(60 * 60 * 24))
|
||||
.unwrap();
|
||||
let serialized_editor = SerializedEditor {
|
||||
abs_path: Some(PathBuf::from("/file.rs")),
|
||||
contents: Some("fn main() {}".to_string()),
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use anyhow::Result;
|
||||
use db::sqlez::bindable::{Bind, Column, StaticColumnCount};
|
||||
use db::sqlez::statement::Statement;
|
||||
use fs::MTime;
|
||||
use std::path::PathBuf;
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
|
||||
use db::sqlez_macros::sql;
|
||||
use db::{define_connection, query};
|
||||
@@ -14,7 +14,7 @@ pub(crate) struct SerializedEditor {
|
||||
pub(crate) abs_path: Option<PathBuf>,
|
||||
pub(crate) contents: Option<String>,
|
||||
pub(crate) language: Option<String>,
|
||||
pub(crate) mtime: Option<MTime>,
|
||||
pub(crate) mtime: Option<SystemTime>,
|
||||
}
|
||||
|
||||
impl StaticColumnCount for SerializedEditor {
|
||||
@@ -29,13 +29,16 @@ impl Bind for SerializedEditor {
|
||||
let start_index = statement.bind(&self.contents, start_index)?;
|
||||
let start_index = statement.bind(&self.language, start_index)?;
|
||||
|
||||
let start_index = match self
|
||||
.mtime
|
||||
.and_then(|mtime| mtime.to_seconds_and_nanos_for_persistence())
|
||||
{
|
||||
let mtime = self.mtime.and_then(|mtime| {
|
||||
mtime
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.ok()
|
||||
.map(|duration| (duration.as_secs() as i64, duration.subsec_nanos() as i32))
|
||||
});
|
||||
let start_index = match mtime {
|
||||
Some((seconds, nanos)) => {
|
||||
let start_index = statement.bind(&(seconds as i64), start_index)?;
|
||||
statement.bind(&(nanos as i32), start_index)?
|
||||
let start_index = statement.bind(&seconds, start_index)?;
|
||||
statement.bind(&nanos, start_index)?
|
||||
}
|
||||
None => {
|
||||
let start_index = statement.bind::<Option<i64>>(&None, start_index)?;
|
||||
@@ -61,7 +64,7 @@ impl Column for SerializedEditor {
|
||||
|
||||
let mtime = mtime_seconds
|
||||
.zip(mtime_nanos)
|
||||
.map(|(seconds, nanos)| MTime::from_seconds_and_nanos(seconds as u64, nanos as u32));
|
||||
.map(|(seconds, nanos)| UNIX_EPOCH + Duration::new(seconds as u64, nanos as u32));
|
||||
|
||||
let editor = Self {
|
||||
abs_path,
|
||||
@@ -277,11 +280,12 @@ mod tests {
|
||||
assert_eq!(have, serialized_editor);
|
||||
|
||||
// Storing and retrieving mtime
|
||||
let now = SystemTime::now();
|
||||
let serialized_editor = SerializedEditor {
|
||||
abs_path: None,
|
||||
contents: None,
|
||||
language: None,
|
||||
mtime: Some(MTime::from_seconds_and_nanos(100, 42)),
|
||||
mtime: Some(now),
|
||||
};
|
||||
|
||||
DB.save_serialized_editor(1234, workspace_id, serialized_editor.clone())
|
||||
|
||||
@@ -30,10 +30,9 @@ languages.workspace = true
|
||||
node_runtime.workspace = true
|
||||
open_ai.workspace = true
|
||||
project.workspace = true
|
||||
reqwest_client.workspace = true
|
||||
semantic_index.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
smol.workspace = true
|
||||
util.workspace = true
|
||||
reqwest_client.workspace = true
|
||||
|
||||
@@ -27,7 +27,7 @@ use std::time::Duration;
|
||||
use std::{
|
||||
fs,
|
||||
path::Path,
|
||||
process::{exit, Stdio},
|
||||
process::{exit, Command, Stdio},
|
||||
sync::{
|
||||
atomic::{AtomicUsize, Ordering::SeqCst},
|
||||
Arc,
|
||||
@@ -667,7 +667,7 @@ async fn fetch_eval_repo(
|
||||
return;
|
||||
}
|
||||
if !repo_dir.join(".git").exists() {
|
||||
let init_output = util::command::new_std_command("git")
|
||||
let init_output = Command::new("git")
|
||||
.current_dir(&repo_dir)
|
||||
.args(&["init"])
|
||||
.output()
|
||||
@@ -682,13 +682,13 @@ async fn fetch_eval_repo(
|
||||
}
|
||||
}
|
||||
let url = format!("https://github.com/{}.git", repo);
|
||||
util::command::new_std_command("git")
|
||||
Command::new("git")
|
||||
.current_dir(&repo_dir)
|
||||
.args(&["remote", "add", "-f", "origin", &url])
|
||||
.stdin(Stdio::null())
|
||||
.output()
|
||||
.unwrap();
|
||||
let fetch_output = util::command::new_std_command("git")
|
||||
let fetch_output = Command::new("git")
|
||||
.current_dir(&repo_dir)
|
||||
.args(&["fetch", "--depth", "1", "origin", &sha])
|
||||
.stdin(Stdio::null())
|
||||
@@ -703,7 +703,7 @@ async fn fetch_eval_repo(
|
||||
);
|
||||
return;
|
||||
}
|
||||
let checkout_output = util::command::new_std_command("git")
|
||||
let checkout_output = Command::new("git")
|
||||
.current_dir(&repo_dir)
|
||||
.args(&["checkout", &sha])
|
||||
.output()
|
||||
|
||||
@@ -28,7 +28,6 @@ semantic_version.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
toml.workspace = true
|
||||
util.workspace = true
|
||||
wasm-encoder.workspace = true
|
||||
wasmparser.workspace = true
|
||||
wit-component.workspace = true
|
||||
|
||||
@@ -11,7 +11,7 @@ use serde::Deserialize;
|
||||
use std::{
|
||||
env, fs, mem,
|
||||
path::{Path, PathBuf},
|
||||
process::Stdio,
|
||||
process::{Command, Stdio},
|
||||
sync::Arc,
|
||||
};
|
||||
use wasm_encoder::{ComponentSectionId, Encode as _, RawSection, Section as _};
|
||||
@@ -130,7 +130,7 @@ impl ExtensionBuilder {
|
||||
"compiling Rust crate for extension {}",
|
||||
extension_dir.display()
|
||||
);
|
||||
let output = util::command::new_std_command("cargo")
|
||||
let output = Command::new("cargo")
|
||||
.args(["build", "--target", RUST_TARGET])
|
||||
.args(options.release.then_some("--release"))
|
||||
.arg("--target-dir")
|
||||
@@ -237,7 +237,7 @@ impl ExtensionBuilder {
|
||||
let scanner_path = src_path.join("scanner.c");
|
||||
|
||||
log::info!("compiling {grammar_name} parser");
|
||||
let clang_output = util::command::new_std_command(&clang_path)
|
||||
let clang_output = Command::new(&clang_path)
|
||||
.args(["-fPIC", "-shared", "-Os"])
|
||||
.arg(format!("-Wl,--export=tree_sitter_{grammar_name}"))
|
||||
.arg("-o")
|
||||
@@ -264,7 +264,7 @@ impl ExtensionBuilder {
|
||||
let git_dir = directory.join(".git");
|
||||
|
||||
if directory.exists() {
|
||||
let remotes_output = util::command::new_std_command("git")
|
||||
let remotes_output = Command::new("git")
|
||||
.arg("--git-dir")
|
||||
.arg(&git_dir)
|
||||
.args(["remote", "-v"])
|
||||
@@ -287,7 +287,7 @@ impl ExtensionBuilder {
|
||||
fs::create_dir_all(directory).with_context(|| {
|
||||
format!("failed to create grammar directory {}", directory.display(),)
|
||||
})?;
|
||||
let init_output = util::command::new_std_command("git")
|
||||
let init_output = Command::new("git")
|
||||
.arg("init")
|
||||
.current_dir(directory)
|
||||
.output()?;
|
||||
@@ -298,7 +298,7 @@ impl ExtensionBuilder {
|
||||
);
|
||||
}
|
||||
|
||||
let remote_add_output = util::command::new_std_command("git")
|
||||
let remote_add_output = Command::new("git")
|
||||
.arg("--git-dir")
|
||||
.arg(&git_dir)
|
||||
.args(["remote", "add", "origin", url])
|
||||
@@ -312,14 +312,14 @@ impl ExtensionBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
let fetch_output = util::command::new_std_command("git")
|
||||
let fetch_output = Command::new("git")
|
||||
.arg("--git-dir")
|
||||
.arg(&git_dir)
|
||||
.args(["fetch", "--depth", "1", "origin", rev])
|
||||
.output()
|
||||
.context("failed to execute `git fetch`")?;
|
||||
|
||||
let checkout_output = util::command::new_std_command("git")
|
||||
let checkout_output = Command::new("git")
|
||||
.arg("--git-dir")
|
||||
.arg(&git_dir)
|
||||
.args(["checkout", rev])
|
||||
@@ -346,7 +346,7 @@ impl ExtensionBuilder {
|
||||
}
|
||||
|
||||
fn install_rust_wasm_target_if_needed(&self) -> Result<()> {
|
||||
let rustc_output = util::command::new_std_command("rustc")
|
||||
let rustc_output = Command::new("rustc")
|
||||
.arg("--print")
|
||||
.arg("sysroot")
|
||||
.output()
|
||||
@@ -363,7 +363,7 @@ impl ExtensionBuilder {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let output = util::command::new_std_command("rustup")
|
||||
let output = Command::new("rustup")
|
||||
.args(["target", "add", RUST_TARGET])
|
||||
.stderr(Stdio::piped())
|
||||
.stdout(Stdio::inherit())
|
||||
|
||||
@@ -34,7 +34,6 @@ lsp.workspace = true
|
||||
node_runtime.workspace = true
|
||||
paths.workspace = true
|
||||
project.workspace = true
|
||||
remote.workspace = true
|
||||
release_channel.workspace = true
|
||||
schemars.workspace = true
|
||||
semantic_version.workspace = true
|
||||
@@ -43,7 +42,6 @@ serde_json.workspace = true
|
||||
serde_json_lenient.workspace = true
|
||||
settings.workspace = true
|
||||
task.workspace = true
|
||||
tempfile.workspace = true
|
||||
toml.workspace = true
|
||||
url.workspace = true
|
||||
util.workspace = true
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
pub mod extension_lsp_adapter;
|
||||
pub mod extension_settings;
|
||||
pub mod headless_host;
|
||||
pub mod wasm_host;
|
||||
|
||||
#[cfg(test)]
|
||||
mod extension_store_test;
|
||||
|
||||
use crate::extension_lsp_adapter::ExtensionLspAdapter;
|
||||
use anyhow::{anyhow, bail, Context as _, Result};
|
||||
use async_compression::futures::bufread::GzipDecoder;
|
||||
use async_tar::Archive;
|
||||
use client::{proto, telemetry::Telemetry, Client, ExtensionMetadata, GetExtensionsResponse};
|
||||
use collections::{btree_map, BTreeMap, HashMap, HashSet};
|
||||
use client::{telemetry::Telemetry, Client, ExtensionMetadata, GetExtensionsResponse};
|
||||
use collections::{btree_map, BTreeMap, HashSet};
|
||||
use extension::extension_builder::{CompileExtensionOptions, ExtensionBuilder};
|
||||
use extension::Extension;
|
||||
pub use extension::ExtensionManifest;
|
||||
@@ -36,7 +36,6 @@ use lsp::LanguageServerName;
|
||||
use node_runtime::NodeRuntime;
|
||||
use project::ContextProviderWithTasks;
|
||||
use release_channel::ReleaseChannel;
|
||||
use remote::SshRemoteClient;
|
||||
use semantic_version::SemanticVersion;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::Settings;
|
||||
@@ -121,13 +120,7 @@ pub trait ExtensionRegistrationHooks: Send + Sync + 'static {
|
||||
) {
|
||||
}
|
||||
|
||||
fn register_lsp_adapter(
|
||||
&self,
|
||||
_extension: Arc<dyn Extension>,
|
||||
_language_server_id: LanguageServerName,
|
||||
_language: LanguageName,
|
||||
) {
|
||||
}
|
||||
fn register_lsp_adapter(&self, _language: LanguageName, _adapter: ExtensionLspAdapter) {}
|
||||
|
||||
fn remove_lsp_adapter(&self, _language: &LanguageName, _server_name: &LanguageServerName) {}
|
||||
|
||||
@@ -185,8 +178,6 @@ pub struct ExtensionStore {
|
||||
pub wasm_host: Arc<WasmHost>,
|
||||
pub wasm_extensions: Vec<(Arc<ExtensionManifest>, WasmExtension)>,
|
||||
pub tasks: Vec<Task<()>>,
|
||||
pub ssh_clients: HashMap<String, WeakModel<SshRemoteClient>>,
|
||||
pub ssh_registered_tx: UnboundedSender<()>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
@@ -298,7 +289,6 @@ impl ExtensionStore {
|
||||
let index_path = extensions_dir.join("index.json");
|
||||
|
||||
let (reload_tx, mut reload_rx) = unbounded();
|
||||
let (connection_registered_tx, mut connection_registered_rx) = unbounded();
|
||||
let mut this = Self {
|
||||
registration_hooks: extension_api.clone(),
|
||||
extension_index: Default::default(),
|
||||
@@ -322,9 +312,6 @@ impl ExtensionStore {
|
||||
telemetry,
|
||||
reload_tx,
|
||||
tasks: Vec::new(),
|
||||
|
||||
ssh_clients: HashMap::default(),
|
||||
ssh_registered_tx: connection_registered_tx,
|
||||
};
|
||||
|
||||
// The extensions store maintains an index file, which contains a complete
|
||||
@@ -350,10 +337,7 @@ impl ExtensionStore {
|
||||
if let (Ok(Some(index_metadata)), Ok(Some(extensions_metadata))) =
|
||||
(index_metadata, extensions_metadata)
|
||||
{
|
||||
if index_metadata
|
||||
.mtime
|
||||
.bad_is_greater_than(extensions_metadata.mtime)
|
||||
{
|
||||
if index_metadata.mtime > extensions_metadata.mtime {
|
||||
extension_index_needs_rebuild = false;
|
||||
}
|
||||
}
|
||||
@@ -402,14 +386,6 @@ impl ExtensionStore {
|
||||
.await;
|
||||
index_changed = false;
|
||||
}
|
||||
|
||||
Self::update_ssh_clients(&this, &mut cx).await?;
|
||||
}
|
||||
_ = connection_registered_rx.next() => {
|
||||
debounce_timer = cx
|
||||
.background_executor()
|
||||
.timer(RELOAD_DEBOUNCE_DURATION)
|
||||
.fuse();
|
||||
}
|
||||
extension_id = reload_rx.next() => {
|
||||
let Some(extension_id) = extension_id else { break; };
|
||||
@@ -1260,9 +1236,12 @@ impl ExtensionStore {
|
||||
for (language_server_id, language_server_config) in &manifest.language_servers {
|
||||
for language in language_server_config.languages() {
|
||||
this.registration_hooks.register_lsp_adapter(
|
||||
extension.clone(),
|
||||
language_server_id.clone(),
|
||||
language.clone(),
|
||||
ExtensionLspAdapter {
|
||||
extension: extension.clone(),
|
||||
language_server_id: language_server_id.clone(),
|
||||
language_name: language.clone(),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1452,144 +1431,6 @@ impl ExtensionStore {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn prepare_remote_extension(
|
||||
&mut self,
|
||||
extension_id: Arc<str>,
|
||||
tmp_dir: PathBuf,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let src_dir = self.extensions_dir().join(extension_id.as_ref());
|
||||
let Some(loaded_extension) = self.extension_index.extensions.get(&extension_id).cloned()
|
||||
else {
|
||||
return Task::ready(Err(anyhow!("extension no longer installed")));
|
||||
};
|
||||
let fs = self.fs.clone();
|
||||
cx.background_executor().spawn(async move {
|
||||
for well_known_path in ["extension.toml", "extension.json", "extension.wasm"] {
|
||||
if fs.is_file(&src_dir.join(well_known_path)).await {
|
||||
fs.copy_file(
|
||||
&src_dir.join(well_known_path),
|
||||
&tmp_dir.join(well_known_path),
|
||||
fs::CopyOptions::default(),
|
||||
)
|
||||
.await?
|
||||
}
|
||||
}
|
||||
|
||||
for language_path in loaded_extension.manifest.languages.iter() {
|
||||
if fs
|
||||
.is_file(&src_dir.join(language_path).join("config.toml"))
|
||||
.await
|
||||
{
|
||||
fs.create_dir(&tmp_dir.join(language_path)).await?;
|
||||
fs.copy_file(
|
||||
&src_dir.join(language_path).join("config.toml"),
|
||||
&tmp_dir.join(language_path).join("config.toml"),
|
||||
fs::CopyOptions::default(),
|
||||
)
|
||||
.await?
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
async fn sync_extensions_over_ssh(
|
||||
this: &WeakModel<Self>,
|
||||
client: WeakModel<SshRemoteClient>,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
let extensions = this.update(cx, |this, _cx| {
|
||||
this.extension_index
|
||||
.extensions
|
||||
.iter()
|
||||
.filter_map(|(id, entry)| {
|
||||
if entry.manifest.language_servers.is_empty() {
|
||||
return None;
|
||||
}
|
||||
Some(proto::Extension {
|
||||
id: id.to_string(),
|
||||
version: entry.manifest.version.to_string(),
|
||||
dev: entry.dev,
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
})?;
|
||||
|
||||
let response = client
|
||||
.update(cx, |client, _cx| {
|
||||
client
|
||||
.proto_client()
|
||||
.request(proto::SyncExtensions { extensions })
|
||||
})?
|
||||
.await?;
|
||||
|
||||
for missing_extension in response.missing_extensions.into_iter() {
|
||||
let tmp_dir = tempfile::tempdir()?;
|
||||
this.update(cx, |this, cx| {
|
||||
this.prepare_remote_extension(
|
||||
missing_extension.id.clone().into(),
|
||||
tmp_dir.path().to_owned(),
|
||||
cx,
|
||||
)
|
||||
})?
|
||||
.await?;
|
||||
let dest_dir = PathBuf::from(&response.tmp_dir).join(missing_extension.clone().id);
|
||||
log::info!("Uploading extension {}", missing_extension.clone().id);
|
||||
|
||||
client
|
||||
.update(cx, |client, cx| {
|
||||
client.upload_directory(tmp_dir.path().to_owned(), dest_dir.clone(), cx)
|
||||
})?
|
||||
.await?;
|
||||
|
||||
client
|
||||
.update(cx, |client, _cx| {
|
||||
client.proto_client().request(proto::InstallExtension {
|
||||
tmp_dir: dest_dir.to_string_lossy().to_string(),
|
||||
extension: Some(missing_extension),
|
||||
})
|
||||
})?
|
||||
.await?;
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
}
|
||||
|
||||
pub async fn update_ssh_clients(
|
||||
this: &WeakModel<Self>,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
let clients = this.update(cx, |this, _cx| {
|
||||
this.ssh_clients.retain(|_k, v| v.upgrade().is_some());
|
||||
this.ssh_clients.values().cloned().collect::<Vec<_>>()
|
||||
})?;
|
||||
|
||||
for client in clients {
|
||||
Self::sync_extensions_over_ssh(&this, client, cx)
|
||||
.await
|
||||
.log_err();
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
}
|
||||
|
||||
pub fn register_ssh_client(
|
||||
&mut self,
|
||||
client: Model<SshRemoteClient>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
let connection_options = client.read(cx).connection_options();
|
||||
if self.ssh_clients.contains_key(&connection_options.ssh_url()) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.ssh_clients
|
||||
.insert(connection_options.ssh_url(), client.downgrade());
|
||||
self.ssh_registered_tx.unbounded_send(()).ok();
|
||||
}
|
||||
}
|
||||
|
||||
fn load_plugin_queries(root_path: &Path) -> LanguageQueries {
|
||||
|
||||
@@ -45,23 +45,9 @@ impl WorktreeDelegate for WorktreeDelegateAdapter {
|
||||
}
|
||||
|
||||
pub struct ExtensionLspAdapter {
|
||||
extension: Arc<dyn Extension>,
|
||||
language_server_id: LanguageServerName,
|
||||
language_name: LanguageName,
|
||||
}
|
||||
|
||||
impl ExtensionLspAdapter {
|
||||
pub fn new(
|
||||
extension: Arc<dyn Extension>,
|
||||
language_server_id: LanguageServerName,
|
||||
language_name: LanguageName,
|
||||
) -> Self {
|
||||
Self {
|
||||
extension,
|
||||
language_server_id,
|
||||
language_name,
|
||||
}
|
||||
}
|
||||
pub(crate) extension: Arc<dyn Extension>,
|
||||
pub(crate) language_server_id: LanguageServerName,
|
||||
pub(crate) language_name: LanguageName,
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
|
||||
@@ -7,14 +7,11 @@ use crate::{
|
||||
use anyhow::Result;
|
||||
use async_compression::futures::bufread::GzipEncoder;
|
||||
use collections::BTreeMap;
|
||||
use extension::Extension;
|
||||
use fs::{FakeFs, Fs, RealFs};
|
||||
use futures::{io::BufReader, AsyncReadExt, StreamExt};
|
||||
use gpui::{BackgroundExecutor, Context, SemanticVersion, SharedString, Task, TestAppContext};
|
||||
use http_client::{FakeHttpClient, Response};
|
||||
use language::{
|
||||
LanguageMatcher, LanguageName, LanguageRegistry, LanguageServerBinaryStatus, LoadedLanguage,
|
||||
};
|
||||
use language::{LanguageMatcher, LanguageRegistry, LanguageServerBinaryStatus, LoadedLanguage};
|
||||
use lsp::LanguageServerName;
|
||||
use node_runtime::NodeRuntime;
|
||||
use parking_lot::Mutex;
|
||||
@@ -83,18 +80,11 @@ impl ExtensionRegistrationHooks for TestExtensionRegistrationHooks {
|
||||
|
||||
fn register_lsp_adapter(
|
||||
&self,
|
||||
extension: Arc<dyn Extension>,
|
||||
language_server_id: LanguageServerName,
|
||||
language: LanguageName,
|
||||
language_name: language::LanguageName,
|
||||
adapter: ExtensionLspAdapter,
|
||||
) {
|
||||
self.language_registry.register_lsp_adapter(
|
||||
language.clone(),
|
||||
Arc::new(ExtensionLspAdapter::new(
|
||||
extension,
|
||||
language_server_id,
|
||||
language,
|
||||
)),
|
||||
);
|
||||
self.language_registry
|
||||
.register_lsp_adapter(language_name, Arc::new(adapter));
|
||||
}
|
||||
|
||||
fn update_lsp_status(
|
||||
|
||||
@@ -1,388 +0,0 @@
|
||||
use std::{path::PathBuf, sync::Arc};
|
||||
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use client::{proto, TypedEnvelope};
|
||||
use collections::{HashMap, HashSet};
|
||||
use extension::{Extension, ExtensionManifest};
|
||||
use fs::{Fs, RemoveOptions, RenameOptions};
|
||||
use gpui::{AppContext, AsyncAppContext, Context, Model, ModelContext, Task, WeakModel};
|
||||
use http_client::HttpClient;
|
||||
use language::{LanguageConfig, LanguageName, LanguageQueries, LanguageRegistry, LoadedLanguage};
|
||||
use lsp::LanguageServerName;
|
||||
use node_runtime::NodeRuntime;
|
||||
|
||||
use crate::{
|
||||
extension_lsp_adapter::ExtensionLspAdapter,
|
||||
wasm_host::{WasmExtension, WasmHost},
|
||||
ExtensionRegistrationHooks,
|
||||
};
|
||||
|
||||
pub struct HeadlessExtensionStore {
|
||||
pub registration_hooks: Arc<dyn ExtensionRegistrationHooks>,
|
||||
pub fs: Arc<dyn Fs>,
|
||||
pub extension_dir: PathBuf,
|
||||
pub wasm_host: Arc<WasmHost>,
|
||||
pub loaded_extensions: HashMap<Arc<str>, Arc<str>>,
|
||||
pub loaded_languages: HashMap<Arc<str>, Vec<LanguageName>>,
|
||||
pub loaded_language_servers: HashMap<Arc<str>, Vec<(LanguageServerName, LanguageName)>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ExtensionVersion {
|
||||
pub id: String,
|
||||
pub version: String,
|
||||
pub dev: bool,
|
||||
}
|
||||
|
||||
impl HeadlessExtensionStore {
|
||||
pub fn new(
|
||||
fs: Arc<dyn Fs>,
|
||||
http_client: Arc<dyn HttpClient>,
|
||||
languages: Arc<LanguageRegistry>,
|
||||
extension_dir: PathBuf,
|
||||
node_runtime: NodeRuntime,
|
||||
cx: &mut AppContext,
|
||||
) -> Model<Self> {
|
||||
let registration_hooks = Arc::new(HeadlessRegistrationHooks::new(languages.clone()));
|
||||
cx.new_model(|cx| Self {
|
||||
registration_hooks: registration_hooks.clone(),
|
||||
fs: fs.clone(),
|
||||
wasm_host: WasmHost::new(
|
||||
fs.clone(),
|
||||
http_client.clone(),
|
||||
node_runtime,
|
||||
registration_hooks,
|
||||
extension_dir.join("work"),
|
||||
cx,
|
||||
),
|
||||
extension_dir,
|
||||
loaded_extensions: Default::default(),
|
||||
loaded_languages: Default::default(),
|
||||
loaded_language_servers: Default::default(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn sync_extensions(
|
||||
&mut self,
|
||||
extensions: Vec<ExtensionVersion>,
|
||||
cx: &ModelContext<Self>,
|
||||
) -> Task<Result<Vec<ExtensionVersion>>> {
|
||||
let on_client = HashSet::from_iter(extensions.iter().map(|e| e.id.as_str()));
|
||||
let to_remove: Vec<Arc<str>> = self
|
||||
.loaded_extensions
|
||||
.keys()
|
||||
.filter(|id| !on_client.contains(id.as_ref()))
|
||||
.cloned()
|
||||
.collect();
|
||||
let to_load: Vec<ExtensionVersion> = extensions
|
||||
.into_iter()
|
||||
.filter(|e| {
|
||||
if e.dev {
|
||||
return true;
|
||||
}
|
||||
!self
|
||||
.loaded_extensions
|
||||
.get(e.id.as_str())
|
||||
.is_some_and(|loaded| loaded.as_ref() == e.version.as_str())
|
||||
})
|
||||
.collect();
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let mut missing = Vec::new();
|
||||
|
||||
for extension_id in to_remove {
|
||||
log::info!("removing extension: {}", extension_id);
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.uninstall_extension(&extension_id, cx)
|
||||
})?
|
||||
.await?;
|
||||
}
|
||||
|
||||
for extension in to_load {
|
||||
if let Err(e) = Self::load_extension(this.clone(), extension.clone(), &mut cx).await
|
||||
{
|
||||
log::info!("failed to load extension: {}, {:?}", extension.id, e);
|
||||
missing.push(extension)
|
||||
} else if extension.dev {
|
||||
missing.push(extension)
|
||||
}
|
||||
}
|
||||
|
||||
Ok(missing)
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn load_extension(
|
||||
this: WeakModel<Self>,
|
||||
extension: ExtensionVersion,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
let (fs, wasm_host, extension_dir) = this.update(cx, |this, _cx| {
|
||||
this.loaded_extensions.insert(
|
||||
extension.id.clone().into(),
|
||||
extension.version.clone().into(),
|
||||
);
|
||||
(
|
||||
this.fs.clone(),
|
||||
this.wasm_host.clone(),
|
||||
this.extension_dir.join(&extension.id),
|
||||
)
|
||||
})?;
|
||||
|
||||
let manifest = Arc::new(ExtensionManifest::load(fs.clone(), &extension_dir).await?);
|
||||
|
||||
debug_assert!(!manifest.languages.is_empty() || !manifest.language_servers.is_empty());
|
||||
|
||||
if manifest.version.as_ref() != extension.version.as_str() {
|
||||
anyhow::bail!(
|
||||
"mismatched versions: ({}) != ({})",
|
||||
manifest.version,
|
||||
extension.version
|
||||
)
|
||||
}
|
||||
|
||||
for language_path in &manifest.languages {
|
||||
let language_path = extension_dir.join(language_path);
|
||||
let config = fs.load(&language_path.join("config.toml")).await?;
|
||||
let mut config = ::toml::from_str::<LanguageConfig>(&config)?;
|
||||
|
||||
this.update(cx, |this, _cx| {
|
||||
this.loaded_languages
|
||||
.entry(manifest.id.clone())
|
||||
.or_default()
|
||||
.push(config.name.clone());
|
||||
|
||||
config.grammar = None;
|
||||
|
||||
this.registration_hooks.register_language(
|
||||
config.name.clone(),
|
||||
None,
|
||||
config.matcher.clone(),
|
||||
Arc::new(move || {
|
||||
Ok(LoadedLanguage {
|
||||
config: config.clone(),
|
||||
queries: LanguageQueries::default(),
|
||||
context_provider: None,
|
||||
toolchain_provider: None,
|
||||
})
|
||||
}),
|
||||
);
|
||||
})?;
|
||||
}
|
||||
|
||||
if manifest.language_servers.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let wasm_extension: Arc<dyn Extension> =
|
||||
Arc::new(WasmExtension::load(extension_dir, &manifest, wasm_host.clone(), &cx).await?);
|
||||
|
||||
for (language_server_id, language_server_config) in &manifest.language_servers {
|
||||
for language in language_server_config.languages() {
|
||||
this.update(cx, |this, _cx| {
|
||||
this.loaded_language_servers
|
||||
.entry(manifest.id.clone())
|
||||
.or_default()
|
||||
.push((language_server_id.clone(), language.clone()));
|
||||
this.registration_hooks.register_lsp_adapter(
|
||||
wasm_extension.clone(),
|
||||
language_server_id.clone(),
|
||||
language.clone(),
|
||||
);
|
||||
})?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn uninstall_extension(
|
||||
&mut self,
|
||||
extension_id: &Arc<str>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
self.loaded_extensions.remove(extension_id);
|
||||
let languages_to_remove = self
|
||||
.loaded_languages
|
||||
.remove(extension_id)
|
||||
.unwrap_or_default();
|
||||
self.registration_hooks
|
||||
.remove_languages(&languages_to_remove, &[]);
|
||||
for (language_server_name, language) in self
|
||||
.loaded_language_servers
|
||||
.remove(extension_id)
|
||||
.unwrap_or_default()
|
||||
{
|
||||
self.registration_hooks
|
||||
.remove_lsp_adapter(&language, &language_server_name);
|
||||
}
|
||||
|
||||
let path = self.extension_dir.join(&extension_id.to_string());
|
||||
let fs = self.fs.clone();
|
||||
cx.spawn(|_, _| async move {
|
||||
fs.remove_dir(
|
||||
&path,
|
||||
RemoveOptions {
|
||||
recursive: true,
|
||||
ignore_if_not_exists: true,
|
||||
},
|
||||
)
|
||||
.await
|
||||
})
|
||||
}
|
||||
|
||||
pub fn install_extension(
|
||||
&mut self,
|
||||
extension: ExtensionVersion,
|
||||
tmp_path: PathBuf,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let path = self.extension_dir.join(&extension.id);
|
||||
let fs = self.fs.clone();
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
if fs.is_dir(&path).await {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.uninstall_extension(&extension.id.clone().into(), cx)
|
||||
})?
|
||||
.await?;
|
||||
}
|
||||
|
||||
fs.rename(&tmp_path, &path, RenameOptions::default())
|
||||
.await?;
|
||||
|
||||
Self::load_extension(this, extension, &mut cx).await
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn handle_sync_extensions(
|
||||
extension_store: Model<HeadlessExtensionStore>,
|
||||
envelope: TypedEnvelope<proto::SyncExtensions>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<proto::SyncExtensionsResponse> {
|
||||
let requested_extensions =
|
||||
envelope
|
||||
.payload
|
||||
.extensions
|
||||
.into_iter()
|
||||
.map(|p| ExtensionVersion {
|
||||
id: p.id,
|
||||
version: p.version,
|
||||
dev: p.dev,
|
||||
});
|
||||
let missing_extensions = extension_store
|
||||
.update(&mut cx, |extension_store, cx| {
|
||||
extension_store.sync_extensions(requested_extensions.collect(), cx)
|
||||
})?
|
||||
.await?;
|
||||
|
||||
Ok(proto::SyncExtensionsResponse {
|
||||
missing_extensions: missing_extensions
|
||||
.into_iter()
|
||||
.map(|e| proto::Extension {
|
||||
id: e.id,
|
||||
version: e.version,
|
||||
dev: e.dev,
|
||||
})
|
||||
.collect(),
|
||||
tmp_dir: paths::remote_extensions_uploads_dir()
|
||||
.to_string_lossy()
|
||||
.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn handle_install_extension(
|
||||
extensions: Model<HeadlessExtensionStore>,
|
||||
envelope: TypedEnvelope<proto::InstallExtension>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<proto::Ack> {
|
||||
let extension = envelope
|
||||
.payload
|
||||
.extension
|
||||
.with_context(|| anyhow!("Invalid InstallExtension request"))?;
|
||||
|
||||
extensions
|
||||
.update(&mut cx, |extensions, cx| {
|
||||
extensions.install_extension(
|
||||
ExtensionVersion {
|
||||
id: extension.id,
|
||||
version: extension.version,
|
||||
dev: extension.dev,
|
||||
},
|
||||
PathBuf::from(envelope.payload.tmp_dir),
|
||||
cx,
|
||||
)
|
||||
})?
|
||||
.await?;
|
||||
|
||||
Ok(proto::Ack {})
|
||||
}
|
||||
}
|
||||
|
||||
struct HeadlessRegistrationHooks {
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
}
|
||||
|
||||
impl HeadlessRegistrationHooks {
|
||||
fn new(language_registry: Arc<LanguageRegistry>) -> Self {
|
||||
Self { language_registry }
|
||||
}
|
||||
}
|
||||
|
||||
impl ExtensionRegistrationHooks for HeadlessRegistrationHooks {
|
||||
fn register_language(
|
||||
&self,
|
||||
language: LanguageName,
|
||||
_grammar: Option<Arc<str>>,
|
||||
matcher: language::LanguageMatcher,
|
||||
load: Arc<dyn Fn() -> Result<LoadedLanguage> + 'static + Send + Sync>,
|
||||
) {
|
||||
log::info!("registering language: {:?}", language);
|
||||
self.language_registry
|
||||
.register_language(language, None, matcher, load)
|
||||
}
|
||||
|
||||
fn register_lsp_adapter(
|
||||
&self,
|
||||
extension: Arc<dyn Extension>,
|
||||
language_server_id: LanguageServerName,
|
||||
language: LanguageName,
|
||||
) {
|
||||
log::info!("registering lsp adapter {:?}", language);
|
||||
self.language_registry.register_lsp_adapter(
|
||||
language.clone(),
|
||||
Arc::new(ExtensionLspAdapter::new(
|
||||
extension,
|
||||
language_server_id,
|
||||
language,
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
||||
fn register_wasm_grammars(&self, grammars: Vec<(Arc<str>, PathBuf)>) {
|
||||
self.language_registry.register_wasm_grammars(grammars)
|
||||
}
|
||||
|
||||
fn remove_lsp_adapter(&self, language: &LanguageName, server_name: &LanguageServerName) {
|
||||
self.language_registry
|
||||
.remove_lsp_adapter(language, server_name)
|
||||
}
|
||||
|
||||
fn remove_languages(
|
||||
&self,
|
||||
languages_to_remove: &[LanguageName],
|
||||
_grammars_to_remove: &[Arc<str>],
|
||||
) {
|
||||
self.language_registry
|
||||
.remove_languages(languages_to_remove, &[])
|
||||
}
|
||||
|
||||
fn update_lsp_status(
|
||||
&self,
|
||||
server_name: LanguageServerName,
|
||||
status: language::LanguageServerBinaryStatus,
|
||||
) {
|
||||
self.language_registry
|
||||
.update_lsp_status(server_name, status)
|
||||
}
|
||||
}
|
||||
@@ -37,14 +37,13 @@ serde.workspace = true
|
||||
settings.workspace = true
|
||||
smallvec.workspace = true
|
||||
snippet_provider.workspace = true
|
||||
telemetry.workspace = true
|
||||
theme.workspace = true
|
||||
theme_selector.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
vim_mode_setting.workspace = true
|
||||
vim.workspace = true
|
||||
wasmtime-wasi.workspace = true
|
||||
workspace.workspace = true
|
||||
zed_actions.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
editor = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use client::telemetry::Telemetry;
|
||||
use gpui::{AnyElement, Div, StyleRefinement};
|
||||
use smallvec::SmallVec;
|
||||
use ui::{prelude::*, ButtonLike};
|
||||
@@ -5,15 +8,17 @@ use ui::{prelude::*, ButtonLike};
|
||||
#[derive(IntoElement)]
|
||||
pub struct FeatureUpsell {
|
||||
base: Div,
|
||||
telemetry: Arc<Telemetry>,
|
||||
text: SharedString,
|
||||
docs_url: Option<SharedString>,
|
||||
children: SmallVec<[AnyElement; 2]>,
|
||||
}
|
||||
|
||||
impl FeatureUpsell {
|
||||
pub fn new(text: impl Into<SharedString>) -> Self {
|
||||
pub fn new(telemetry: Arc<Telemetry>, text: impl Into<SharedString>) -> Self {
|
||||
Self {
|
||||
base: h_flex(),
|
||||
telemetry,
|
||||
text: text.into(),
|
||||
docs_url: None,
|
||||
children: SmallVec::new(),
|
||||
@@ -62,13 +67,12 @@ impl RenderOnce for FeatureUpsell {
|
||||
.child(Icon::new(IconName::ArrowUpRight)),
|
||||
)
|
||||
.on_click({
|
||||
let telemetry = self.telemetry.clone();
|
||||
let docs_url = docs_url.clone();
|
||||
move |_event, cx| {
|
||||
telemetry::event!(
|
||||
"Documentation Viewed",
|
||||
source = "Feature Upsell",
|
||||
url = docs_url
|
||||
);
|
||||
telemetry.report_app_event(format!(
|
||||
"feature upsell: viewed docs ({docs_url})"
|
||||
));
|
||||
cx.open_url(&docs_url)
|
||||
}
|
||||
}),
|
||||
|
||||
@@ -11,8 +11,7 @@ use extension_host::{extension_lsp_adapter::ExtensionLspAdapter, wasm_host};
|
||||
use fs::Fs;
|
||||
use gpui::{AppContext, BackgroundExecutor, Model, Task};
|
||||
use indexed_docs::{ExtensionIndexedDocsProvider, IndexedDocsRegistry, ProviderId};
|
||||
use language::{LanguageName, LanguageRegistry, LanguageServerBinaryStatus, LoadedLanguage};
|
||||
use lsp::LanguageServerName;
|
||||
use language::{LanguageRegistry, LanguageServerBinaryStatus, LoadedLanguage};
|
||||
use snippet_provider::SnippetRegistry;
|
||||
use theme::{ThemeRegistry, ThemeSettings};
|
||||
use ui::SharedString;
|
||||
@@ -160,18 +159,11 @@ impl extension_host::ExtensionRegistrationHooks for ConcreteExtensionRegistratio
|
||||
|
||||
fn register_lsp_adapter(
|
||||
&self,
|
||||
extension: Arc<dyn Extension>,
|
||||
language_server_id: LanguageServerName,
|
||||
language: LanguageName,
|
||||
language_name: language::LanguageName,
|
||||
adapter: ExtensionLspAdapter,
|
||||
) {
|
||||
self.language_registry.register_lsp_adapter(
|
||||
language.clone(),
|
||||
Arc::new(ExtensionLspAdapter::new(
|
||||
extension,
|
||||
language_server_id,
|
||||
language,
|
||||
)),
|
||||
);
|
||||
self.language_registry
|
||||
.register_lsp_adapter(language_name, Arc::new(adapter));
|
||||
}
|
||||
|
||||
fn remove_lsp_adapter(
|
||||
|
||||
@@ -17,9 +17,9 @@ use editor::{Editor, EditorElement, EditorStyle};
|
||||
use extension_host::{ExtensionManifest, ExtensionOperation, ExtensionStore};
|
||||
use fuzzy::{match_strings, StringMatchCandidate};
|
||||
use gpui::{
|
||||
actions, uniform_list, Action, AppContext, EventEmitter, Flatten, FocusableView,
|
||||
InteractiveElement, KeyContext, ParentElement, Render, Styled, Task, TextStyle,
|
||||
UniformListScrollHandle, View, ViewContext, VisualContext, WeakView, WindowContext,
|
||||
actions, uniform_list, AppContext, EventEmitter, Flatten, FocusableView, InteractiveElement,
|
||||
KeyContext, ParentElement, Render, Styled, Task, TextStyle, UniformListScrollHandle, View,
|
||||
ViewContext, VisualContext, WeakView, WindowContext,
|
||||
};
|
||||
use num_format::{Locale, ToFormattedString};
|
||||
use project::DirectoryLister;
|
||||
@@ -27,7 +27,7 @@ use release_channel::ReleaseChannel;
|
||||
use settings::Settings;
|
||||
use theme::ThemeSettings;
|
||||
use ui::{prelude::*, CheckboxWithLabel, ContextMenu, PopoverMenu, ToggleButton, Tooltip};
|
||||
use vim_mode_setting::VimModeSetting;
|
||||
use vim::VimModeSetting;
|
||||
use workspace::{
|
||||
item::{Item, ItemEvent},
|
||||
Workspace, WorkspaceId,
|
||||
@@ -38,12 +38,12 @@ use crate::extension_version_selector::{
|
||||
ExtensionVersionSelector, ExtensionVersionSelectorDelegate,
|
||||
};
|
||||
|
||||
actions!(zed, [InstallDevExtension]);
|
||||
actions!(zed, [Extensions, InstallDevExtension]);
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.observe_new_views(move |workspace: &mut Workspace, cx| {
|
||||
workspace
|
||||
.register_action(move |workspace, _: &zed_actions::Extensions, cx| {
|
||||
.register_action(move |workspace, _: &Extensions, cx| {
|
||||
let existing = workspace
|
||||
.active_pane()
|
||||
.read(cx)
|
||||
@@ -254,13 +254,14 @@ impl ExtensionsPage {
|
||||
.collect::<Vec<_>>();
|
||||
if !themes.is_empty() {
|
||||
workspace
|
||||
.update(cx, |_workspace, cx| {
|
||||
cx.dispatch_action(
|
||||
zed_actions::theme_selector::Toggle {
|
||||
.update(cx, |workspace, cx| {
|
||||
theme_selector::toggle(
|
||||
workspace,
|
||||
&theme_selector::Toggle {
|
||||
themes_filter: Some(themes),
|
||||
}
|
||||
.boxed_clone(),
|
||||
);
|
||||
},
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
@@ -975,16 +976,19 @@ impl ExtensionsPage {
|
||||
let upsells_count = self.upsells.len();
|
||||
|
||||
v_flex().children(self.upsells.iter().enumerate().map(|(ix, feature)| {
|
||||
let telemetry = self.telemetry.clone();
|
||||
let upsell = match feature {
|
||||
Feature::Git => FeatureUpsell::new(
|
||||
telemetry,
|
||||
"Zed comes with basic Git support. More Git features are coming in the future.",
|
||||
)
|
||||
.docs_url("https://zed.dev/docs/git"),
|
||||
Feature::OpenIn => FeatureUpsell::new(
|
||||
telemetry,
|
||||
"Zed supports linking to a source line on GitHub and others.",
|
||||
)
|
||||
.docs_url("https://zed.dev/docs/git#git-integrations"),
|
||||
Feature::Vim => FeatureUpsell::new("Vim support is built-in to Zed!")
|
||||
Feature::Vim => FeatureUpsell::new(telemetry, "Vim support is built-in to Zed!")
|
||||
.docs_url("https://zed.dev/docs/vim")
|
||||
.child(CheckboxWithLabel::new(
|
||||
"enable-vim",
|
||||
@@ -1004,22 +1008,36 @@ impl ExtensionsPage {
|
||||
);
|
||||
}),
|
||||
)),
|
||||
Feature::LanguageBash => FeatureUpsell::new("Shell support is built-in to Zed!")
|
||||
.docs_url("https://zed.dev/docs/languages/bash"),
|
||||
Feature::LanguageC => FeatureUpsell::new("C support is built-in to Zed!")
|
||||
.docs_url("https://zed.dev/docs/languages/c"),
|
||||
Feature::LanguageCpp => FeatureUpsell::new("C++ support is built-in to Zed!")
|
||||
.docs_url("https://zed.dev/docs/languages/cpp"),
|
||||
Feature::LanguageGo => FeatureUpsell::new("Go support is built-in to Zed!")
|
||||
.docs_url("https://zed.dev/docs/languages/go"),
|
||||
Feature::LanguagePython => FeatureUpsell::new("Python support is built-in to Zed!")
|
||||
.docs_url("https://zed.dev/docs/languages/python"),
|
||||
Feature::LanguageReact => FeatureUpsell::new("React support is built-in to Zed!")
|
||||
.docs_url("https://zed.dev/docs/languages/typescript"),
|
||||
Feature::LanguageRust => FeatureUpsell::new("Rust support is built-in to Zed!")
|
||||
.docs_url("https://zed.dev/docs/languages/rust"),
|
||||
Feature::LanguageBash => {
|
||||
FeatureUpsell::new(telemetry, "Shell support is built-in to Zed!")
|
||||
.docs_url("https://zed.dev/docs/languages/bash")
|
||||
}
|
||||
Feature::LanguageC => {
|
||||
FeatureUpsell::new(telemetry, "C support is built-in to Zed!")
|
||||
.docs_url("https://zed.dev/docs/languages/c")
|
||||
}
|
||||
Feature::LanguageCpp => {
|
||||
FeatureUpsell::new(telemetry, "C++ support is built-in to Zed!")
|
||||
.docs_url("https://zed.dev/docs/languages/cpp")
|
||||
}
|
||||
Feature::LanguageGo => {
|
||||
FeatureUpsell::new(telemetry, "Go support is built-in to Zed!")
|
||||
.docs_url("https://zed.dev/docs/languages/go")
|
||||
}
|
||||
Feature::LanguagePython => {
|
||||
FeatureUpsell::new(telemetry, "Python support is built-in to Zed!")
|
||||
.docs_url("https://zed.dev/docs/languages/python")
|
||||
}
|
||||
Feature::LanguageReact => {
|
||||
FeatureUpsell::new(telemetry, "React support is built-in to Zed!")
|
||||
.docs_url("https://zed.dev/docs/languages/typescript")
|
||||
}
|
||||
Feature::LanguageRust => {
|
||||
FeatureUpsell::new(telemetry, "Rust support is built-in to Zed!")
|
||||
.docs_url("https://zed.dev/docs/languages/rust")
|
||||
}
|
||||
Feature::LanguageTypescript => {
|
||||
FeatureUpsell::new("Typescript support is built-in to Zed!")
|
||||
FeatureUpsell::new(telemetry, "Typescript support is built-in to Zed!")
|
||||
.docs_url("https://zed.dev/docs/languages/typescript")
|
||||
}
|
||||
};
|
||||
|
||||
@@ -22,8 +22,8 @@ db.workspace = true
|
||||
editor.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
http_client.workspace = true
|
||||
human_bytes = "0.4.1"
|
||||
http_client.workspace = true
|
||||
language.workspace = true
|
||||
log.workspace = true
|
||||
menu.workspace = true
|
||||
@@ -39,7 +39,6 @@ ui.workspace = true
|
||||
urlencoding = "2.1.2"
|
||||
util.workspace = true
|
||||
workspace.workspace = true
|
||||
zed_actions.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
editor = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -5,6 +5,8 @@ use workspace::Workspace;
|
||||
|
||||
pub mod feedback_modal;
|
||||
|
||||
actions!(feedback, [GiveFeedback, SubmitFeedback]);
|
||||
|
||||
mod system_specs;
|
||||
|
||||
actions!(
|
||||
|
||||
@@ -18,9 +18,8 @@ use serde_derive::Serialize;
|
||||
use ui::{prelude::*, Button, ButtonStyle, IconPosition, Tooltip};
|
||||
use util::ResultExt;
|
||||
use workspace::{DismissDecision, ModalView, Workspace};
|
||||
use zed_actions::feedback::GiveFeedback;
|
||||
|
||||
use crate::{system_specs::SystemSpecs, OpenZedRepo};
|
||||
use crate::{system_specs::SystemSpecs, GiveFeedback, OpenZedRepo};
|
||||
|
||||
// For UI testing purposes
|
||||
const SEND_SUCCESS_IN_DEV_MODE: bool = true;
|
||||
|
||||
@@ -24,7 +24,6 @@ libc.workspace = true
|
||||
parking_lot.workspace = true
|
||||
paths.workspace = true
|
||||
rope.workspace = true
|
||||
proto.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
smol.workspace = true
|
||||
|
||||
@@ -27,14 +27,13 @@ use futures::{future::BoxFuture, AsyncRead, Stream, StreamExt};
|
||||
use git::repository::{GitRepository, RealGitRepository};
|
||||
use gpui::{AppContext, Global, ReadGlobal};
|
||||
use rope::Rope;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use smol::io::AsyncWriteExt;
|
||||
use std::{
|
||||
io::{self, Write},
|
||||
path::{Component, Path, PathBuf},
|
||||
pin::Pin,
|
||||
sync::Arc,
|
||||
time::{Duration, SystemTime, UNIX_EPOCH},
|
||||
time::{Duration, SystemTime},
|
||||
};
|
||||
use tempfile::{NamedTempFile, TempDir};
|
||||
use text::LineEnding;
|
||||
@@ -180,62 +179,13 @@ pub struct RemoveOptions {
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub struct Metadata {
|
||||
pub inode: u64,
|
||||
pub mtime: MTime,
|
||||
pub mtime: SystemTime,
|
||||
pub is_symlink: bool,
|
||||
pub is_dir: bool,
|
||||
pub len: u64,
|
||||
pub is_fifo: bool,
|
||||
}
|
||||
|
||||
/// Filesystem modification time. The purpose of this newtype is to discourage use of operations
|
||||
/// that do not make sense for mtimes. In particular, it is not always valid to compare mtimes using
|
||||
/// `<` or `>`, as there are many things that can cause the mtime of a file to be earlier than it
|
||||
/// was. See ["mtime comparison considered harmful" - apenwarr](https://apenwarr.ca/log/20181113).
|
||||
///
|
||||
/// Do not derive Ord, PartialOrd, or arithmetic operation traits.
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct MTime(SystemTime);
|
||||
|
||||
impl MTime {
|
||||
/// Conversion intended for persistence and testing.
|
||||
pub fn from_seconds_and_nanos(secs: u64, nanos: u32) -> Self {
|
||||
MTime(UNIX_EPOCH + Duration::new(secs, nanos))
|
||||
}
|
||||
|
||||
/// Conversion intended for persistence.
|
||||
pub fn to_seconds_and_nanos_for_persistence(self) -> Option<(u64, u32)> {
|
||||
self.0
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.ok()
|
||||
.map(|duration| (duration.as_secs(), duration.subsec_nanos()))
|
||||
}
|
||||
|
||||
/// Returns the value wrapped by this `MTime`, for presentation to the user. The name including
|
||||
/// "_for_user" is to discourage misuse - this method should not be used when making decisions
|
||||
/// about file dirtiness.
|
||||
pub fn timestamp_for_user(self) -> SystemTime {
|
||||
self.0
|
||||
}
|
||||
|
||||
/// Temporary method to split out the behavior changes from introduction of this newtype.
|
||||
pub fn bad_is_greater_than(self, other: MTime) -> bool {
|
||||
self.0 > other.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<proto::Timestamp> for MTime {
|
||||
fn from(timestamp: proto::Timestamp) -> Self {
|
||||
MTime(timestamp.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MTime> for proto::Timestamp {
|
||||
fn from(mtime: MTime) -> Self {
|
||||
mtime.0.into()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct RealFs {
|
||||
git_hosting_provider_registry: Arc<GitHostingProviderRegistry>,
|
||||
@@ -608,7 +558,7 @@ impl Fs for RealFs {
|
||||
|
||||
Ok(Some(Metadata {
|
||||
inode,
|
||||
mtime: MTime(metadata.modified().unwrap()),
|
||||
mtime: metadata.modified().unwrap(),
|
||||
len: metadata.len(),
|
||||
is_symlink,
|
||||
is_dir: metadata.file_type().is_dir(),
|
||||
@@ -868,13 +818,13 @@ struct FakeFsState {
|
||||
enum FakeFsEntry {
|
||||
File {
|
||||
inode: u64,
|
||||
mtime: MTime,
|
||||
mtime: SystemTime,
|
||||
len: u64,
|
||||
content: Vec<u8>,
|
||||
},
|
||||
Dir {
|
||||
inode: u64,
|
||||
mtime: MTime,
|
||||
mtime: SystemTime,
|
||||
len: u64,
|
||||
entries: BTreeMap<String, Arc<Mutex<FakeFsEntry>>>,
|
||||
git_repo_state: Option<Arc<Mutex<git::repository::FakeGitRepositoryState>>>,
|
||||
@@ -886,18 +836,6 @@ enum FakeFsEntry {
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
impl FakeFsState {
|
||||
fn get_and_increment_mtime(&mut self) -> MTime {
|
||||
let mtime = self.next_mtime;
|
||||
self.next_mtime += FakeFs::SYSTEMTIME_INTERVAL;
|
||||
MTime(mtime)
|
||||
}
|
||||
|
||||
fn get_and_increment_inode(&mut self) -> u64 {
|
||||
let inode = self.next_inode;
|
||||
self.next_inode += 1;
|
||||
inode
|
||||
}
|
||||
|
||||
fn read_path(&self, target: &Path) -> Result<Arc<Mutex<FakeFsEntry>>> {
|
||||
Ok(self
|
||||
.try_read_path(target, true)
|
||||
@@ -1021,7 +959,7 @@ pub static FS_DOT_GIT: std::sync::LazyLock<&'static OsStr> =
|
||||
impl FakeFs {
|
||||
/// We need to use something large enough for Windows and Unix to consider this a new file.
|
||||
/// https://doc.rust-lang.org/nightly/std/time/struct.SystemTime.html#platform-specific-behavior
|
||||
const SYSTEMTIME_INTERVAL: Duration = Duration::from_nanos(100);
|
||||
const SYSTEMTIME_INTERVAL: u64 = 100;
|
||||
|
||||
pub fn new(executor: gpui::BackgroundExecutor) -> Arc<Self> {
|
||||
let (tx, mut rx) = smol::channel::bounded::<PathBuf>(10);
|
||||
@@ -1031,13 +969,13 @@ impl FakeFs {
|
||||
state: Mutex::new(FakeFsState {
|
||||
root: Arc::new(Mutex::new(FakeFsEntry::Dir {
|
||||
inode: 0,
|
||||
mtime: MTime(UNIX_EPOCH),
|
||||
mtime: SystemTime::UNIX_EPOCH,
|
||||
len: 0,
|
||||
entries: Default::default(),
|
||||
git_repo_state: None,
|
||||
})),
|
||||
git_event_tx: tx,
|
||||
next_mtime: UNIX_EPOCH + Self::SYSTEMTIME_INTERVAL,
|
||||
next_mtime: SystemTime::UNIX_EPOCH,
|
||||
next_inode: 1,
|
||||
event_txs: Default::default(),
|
||||
buffered_events: Vec::new(),
|
||||
@@ -1069,16 +1007,13 @@ impl FakeFs {
|
||||
state.next_mtime = next_mtime;
|
||||
}
|
||||
|
||||
pub fn get_and_increment_mtime(&self) -> MTime {
|
||||
let mut state = self.state.lock();
|
||||
state.get_and_increment_mtime()
|
||||
}
|
||||
|
||||
pub async fn touch_path(&self, path: impl AsRef<Path>) {
|
||||
let mut state = self.state.lock();
|
||||
let path = path.as_ref();
|
||||
let new_mtime = state.get_and_increment_mtime();
|
||||
let new_inode = state.get_and_increment_inode();
|
||||
let new_mtime = state.next_mtime;
|
||||
let new_inode = state.next_inode;
|
||||
state.next_inode += 1;
|
||||
state.next_mtime += Duration::from_nanos(Self::SYSTEMTIME_INTERVAL);
|
||||
state
|
||||
.write_path(path, move |entry| {
|
||||
match entry {
|
||||
@@ -1127,14 +1062,19 @@ impl FakeFs {
|
||||
|
||||
fn write_file_internal(&self, path: impl AsRef<Path>, content: Vec<u8>) -> Result<()> {
|
||||
let mut state = self.state.lock();
|
||||
let path = path.as_ref();
|
||||
let inode = state.next_inode;
|
||||
let mtime = state.next_mtime;
|
||||
state.next_inode += 1;
|
||||
state.next_mtime += Duration::from_nanos(Self::SYSTEMTIME_INTERVAL);
|
||||
let file = Arc::new(Mutex::new(FakeFsEntry::File {
|
||||
inode: state.get_and_increment_inode(),
|
||||
mtime: state.get_and_increment_mtime(),
|
||||
inode,
|
||||
mtime,
|
||||
len: content.len() as u64,
|
||||
content,
|
||||
}));
|
||||
let mut kind = None;
|
||||
state.write_path(path.as_ref(), {
|
||||
state.write_path(path, {
|
||||
let kind = &mut kind;
|
||||
move |entry| {
|
||||
match entry {
|
||||
@@ -1150,7 +1090,7 @@ impl FakeFs {
|
||||
Ok(())
|
||||
}
|
||||
})?;
|
||||
state.emit_event([(path.as_ref(), kind)]);
|
||||
state.emit_event([(path, kind)]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1443,6 +1383,16 @@ impl FakeFsEntry {
|
||||
}
|
||||
}
|
||||
|
||||
fn set_file_content(&mut self, path: &Path, new_content: Vec<u8>) -> Result<()> {
|
||||
if let Self::File { content, mtime, .. } = self {
|
||||
*mtime = SystemTime::now();
|
||||
*content = new_content;
|
||||
Ok(())
|
||||
} else {
|
||||
Err(anyhow!("not a file: {}", path.display()))
|
||||
}
|
||||
}
|
||||
|
||||
fn dir_entries(
|
||||
&mut self,
|
||||
path: &Path,
|
||||
@@ -1506,8 +1456,10 @@ impl Fs for FakeFs {
|
||||
}
|
||||
let mut state = self.state.lock();
|
||||
|
||||
let inode = state.get_and_increment_inode();
|
||||
let mtime = state.get_and_increment_mtime();
|
||||
let inode = state.next_inode;
|
||||
let mtime = state.next_mtime;
|
||||
state.next_mtime += Duration::from_nanos(Self::SYSTEMTIME_INTERVAL);
|
||||
state.next_inode += 1;
|
||||
state.write_path(&cur_path, |entry| {
|
||||
entry.or_insert_with(|| {
|
||||
created_dirs.push((cur_path.clone(), Some(PathEventKind::Created)));
|
||||
@@ -1530,8 +1482,10 @@ impl Fs for FakeFs {
|
||||
async fn create_file(&self, path: &Path, options: CreateOptions) -> Result<()> {
|
||||
self.simulate_random_delay().await;
|
||||
let mut state = self.state.lock();
|
||||
let inode = state.get_and_increment_inode();
|
||||
let mtime = state.get_and_increment_mtime();
|
||||
let inode = state.next_inode;
|
||||
let mtime = state.next_mtime;
|
||||
state.next_mtime += Duration::from_nanos(Self::SYSTEMTIME_INTERVAL);
|
||||
state.next_inode += 1;
|
||||
let file = Arc::new(Mutex::new(FakeFsEntry::File {
|
||||
inode,
|
||||
mtime,
|
||||
@@ -1671,12 +1625,13 @@ impl Fs for FakeFs {
|
||||
let source = normalize_path(source);
|
||||
let target = normalize_path(target);
|
||||
let mut state = self.state.lock();
|
||||
let mtime = state.get_and_increment_mtime();
|
||||
let inode = state.get_and_increment_inode();
|
||||
let mtime = state.next_mtime;
|
||||
let inode = util::post_inc(&mut state.next_inode);
|
||||
state.next_mtime += Duration::from_nanos(Self::SYSTEMTIME_INTERVAL);
|
||||
let source_entry = state.read_path(&source)?;
|
||||
let content = source_entry.lock().file_content(&source)?.clone();
|
||||
let mut kind = Some(PathEventKind::Created);
|
||||
state.write_path(&target, |e| match e {
|
||||
let entry = state.write_path(&target, |e| match e {
|
||||
btree_map::Entry::Occupied(e) => {
|
||||
if options.overwrite {
|
||||
kind = Some(PathEventKind::Changed);
|
||||
@@ -1692,11 +1647,14 @@ impl Fs for FakeFs {
|
||||
inode,
|
||||
mtime,
|
||||
len: content.len() as u64,
|
||||
content,
|
||||
content: Vec::new(),
|
||||
})))
|
||||
.clone(),
|
||||
)),
|
||||
})?;
|
||||
if let Some(entry) = entry {
|
||||
entry.lock().set_file_content(&target, content)?;
|
||||
}
|
||||
state.emit_event([(target, kind)]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -31,6 +31,10 @@ time.workspace = true
|
||||
url.workspace = true
|
||||
util.workspace = true
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
windows.workspace = true
|
||||
|
||||
|
||||
[dev-dependencies]
|
||||
unindent.workspace = true
|
||||
serde_json.workspace = true
|
||||
|
||||
@@ -4,7 +4,7 @@ use anyhow::{anyhow, Context, Result};
|
||||
use collections::{HashMap, HashSet};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::io::Write;
|
||||
use std::process::Stdio;
|
||||
use std::process::{Command, Stdio};
|
||||
use std::sync::Arc;
|
||||
use std::{ops::Range, path::Path};
|
||||
use text::Rope;
|
||||
@@ -80,7 +80,9 @@ fn run_git_blame(
|
||||
path: &Path,
|
||||
contents: &Rope,
|
||||
) -> Result<String> {
|
||||
let child = util::command::new_std_command(git_binary)
|
||||
let mut child = Command::new(git_binary);
|
||||
|
||||
child
|
||||
.current_dir(working_directory)
|
||||
.arg("blame")
|
||||
.arg("--incremental")
|
||||
@@ -89,7 +91,15 @@ fn run_git_blame(
|
||||
.arg(path.as_os_str())
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
use std::os::windows::process::CommandExt;
|
||||
child.creation_flags(windows::Win32::System::Threading::CREATE_NO_WINDOW.0);
|
||||
}
|
||||
|
||||
let child = child
|
||||
.spawn()
|
||||
.map_err(|e| anyhow!("Failed to start git blame process: {}", e))?;
|
||||
|
||||
|
||||
@@ -2,6 +2,10 @@ use crate::Oid;
|
||||
use anyhow::{anyhow, Result};
|
||||
use collections::HashMap;
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
|
||||
#[cfg(windows)]
|
||||
use std::os::windows::process::CommandExt;
|
||||
|
||||
pub fn get_messages(working_directory: &Path, shas: &[Oid]) -> Result<HashMap<Oid, String>> {
|
||||
if shas.is_empty() {
|
||||
@@ -10,12 +14,19 @@ pub fn get_messages(working_directory: &Path, shas: &[Oid]) -> Result<HashMap<Oi
|
||||
|
||||
const MARKER: &str = "<MARKER>";
|
||||
|
||||
let output = util::command::new_std_command("git")
|
||||
let mut command = Command::new("git");
|
||||
|
||||
command
|
||||
.current_dir(working_directory)
|
||||
.arg("show")
|
||||
.arg("-s")
|
||||
.arg(format!("--format=%B{}", MARKER))
|
||||
.args(shas.iter().map(ToString::to_string))
|
||||
.args(shas.iter().map(ToString::to_string));
|
||||
|
||||
#[cfg(windows)]
|
||||
command.creation_flags(windows::Win32::System::Threading::CREATE_NO_WINDOW.0);
|
||||
|
||||
let output = command
|
||||
.output()
|
||||
.map_err(|e| anyhow!("Failed to start git blame process: {}", e))?;
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ use crate::repository::{GitFileStatus, RepoPath};
|
||||
use anyhow::{anyhow, Result};
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
process::Stdio,
|
||||
process::{Command, Stdio},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
@@ -17,7 +17,9 @@ impl GitStatus {
|
||||
working_directory: &Path,
|
||||
path_prefixes: &[PathBuf],
|
||||
) -> Result<Self> {
|
||||
let child = util::command::new_std_command(git_binary)
|
||||
let mut child = Command::new(git_binary);
|
||||
|
||||
child
|
||||
.current_dir(working_directory)
|
||||
.args([
|
||||
"--no-optional-locks",
|
||||
@@ -35,7 +37,15 @@ impl GitStatus {
|
||||
}))
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
use std::os::windows::process::CommandExt;
|
||||
child.creation_flags(windows::Win32::System::Threading::CREATE_NO_WINDOW.0);
|
||||
}
|
||||
|
||||
let child = child
|
||||
.spawn()
|
||||
.map_err(|e| anyhow!("Failed to start git status process: {}", e))?;
|
||||
|
||||
|
||||
@@ -15,10 +15,7 @@ actions!(
|
||||
SelectAll,
|
||||
Home,
|
||||
End,
|
||||
ShowCharacterPalette,
|
||||
Paste,
|
||||
Cut,
|
||||
Copy,
|
||||
ShowCharacterPalette
|
||||
]
|
||||
);
|
||||
|
||||
@@ -110,28 +107,6 @@ impl TextInput {
|
||||
cx.show_character_palette();
|
||||
}
|
||||
|
||||
fn paste(&mut self, _: &Paste, cx: &mut ViewContext<Self>) {
|
||||
if let Some(text) = cx.read_from_clipboard().and_then(|item| item.text()) {
|
||||
self.replace_text_in_range(None, &text.replace("\n", " "), cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn copy(&mut self, _: &Copy, cx: &mut ViewContext<Self>) {
|
||||
if !self.selected_range.is_empty() {
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(
|
||||
(&self.content[self.selected_range.clone()]).to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
fn cut(&mut self, _: &Copy, cx: &mut ViewContext<Self>) {
|
||||
if !self.selected_range.is_empty() {
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(
|
||||
(&self.content[self.selected_range.clone()]).to_string(),
|
||||
));
|
||||
self.replace_text_in_range(None, "", cx)
|
||||
}
|
||||
}
|
||||
|
||||
fn move_to(&mut self, offset: usize, cx: &mut ViewContext<Self>) {
|
||||
self.selected_range = offset..offset;
|
||||
cx.notify()
|
||||
@@ -244,11 +219,9 @@ impl ViewInputHandler for TextInput {
|
||||
fn text_for_range(
|
||||
&mut self,
|
||||
range_utf16: Range<usize>,
|
||||
actual_range: &mut Option<Range<usize>>,
|
||||
_cx: &mut ViewContext<Self>,
|
||||
) -> Option<String> {
|
||||
let range = self.range_from_utf16(&range_utf16);
|
||||
actual_range.replace(self.range_to_utf16(&range));
|
||||
Some(self.content[range].to_string())
|
||||
}
|
||||
|
||||
@@ -524,9 +497,6 @@ impl Render for TextInput {
|
||||
.on_action(cx.listener(Self::home))
|
||||
.on_action(cx.listener(Self::end))
|
||||
.on_action(cx.listener(Self::show_character_palette))
|
||||
.on_action(cx.listener(Self::paste))
|
||||
.on_action(cx.listener(Self::cut))
|
||||
.on_action(cx.listener(Self::copy))
|
||||
.on_mouse_down(MouseButton::Left, cx.listener(Self::on_mouse_down))
|
||||
.on_mouse_up(MouseButton::Left, cx.listener(Self::on_mouse_up))
|
||||
.on_mouse_up_out(MouseButton::Left, cx.listener(Self::on_mouse_up))
|
||||
@@ -611,8 +581,8 @@ impl Render for InputExample {
|
||||
format!(
|
||||
"{:} {}",
|
||||
ks.unparse(),
|
||||
if let Some(key_char) = ks.key_char.as_ref() {
|
||||
format!("-> {:?}", key_char)
|
||||
if let Some(ime_key) = ks.ime_key.as_ref() {
|
||||
format!("-> {:?}", ime_key)
|
||||
} else {
|
||||
"".to_owned()
|
||||
}
|
||||
@@ -632,9 +602,6 @@ fn main() {
|
||||
KeyBinding::new("shift-left", SelectLeft, None),
|
||||
KeyBinding::new("shift-right", SelectRight, None),
|
||||
KeyBinding::new("cmd-a", SelectAll, None),
|
||||
KeyBinding::new("cmd-v", Paste, None),
|
||||
KeyBinding::new("cmd-c", Copy, None),
|
||||
KeyBinding::new("cmd-x", Cut, None),
|
||||
KeyBinding::new("home", Home, None),
|
||||
KeyBinding::new("end", End, None),
|
||||
KeyBinding::new("ctrl-cmd-space", ShowCharacterPalette, None),
|
||||
|
||||
@@ -263,7 +263,7 @@ impl TextLayout {
|
||||
.line_height
|
||||
.to_pixels(font_size.into(), cx.rem_size());
|
||||
|
||||
let mut runs = if let Some(runs) = runs {
|
||||
let runs = if let Some(runs) = runs {
|
||||
runs
|
||||
} else {
|
||||
vec![text_style.to_run(text.len())]
|
||||
@@ -306,7 +306,7 @@ impl TextLayout {
|
||||
|
||||
let mut line_wrapper = cx.text_system().line_wrapper(text_style.font(), font_size);
|
||||
let text = if let Some(truncate_width) = truncate_width {
|
||||
line_wrapper.truncate_line(text.clone(), truncate_width, ellipsis, &mut runs)
|
||||
line_wrapper.truncate_line(text.clone(), truncate_width, ellipsis)
|
||||
} else {
|
||||
text.clone()
|
||||
};
|
||||
|
||||
@@ -9,12 +9,8 @@ use std::ops::Range;
|
||||
/// See [`InputHandler`] for details on how to implement each method.
|
||||
pub trait ViewInputHandler: 'static + Sized {
|
||||
/// See [`InputHandler::text_for_range`] for details
|
||||
fn text_for_range(
|
||||
&mut self,
|
||||
range: Range<usize>,
|
||||
adjusted_range: &mut Option<Range<usize>>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<String>;
|
||||
fn text_for_range(&mut self, range: Range<usize>, cx: &mut ViewContext<Self>)
|
||||
-> Option<String>;
|
||||
|
||||
/// See [`InputHandler::selected_text_range`] for details
|
||||
fn selected_text_range(
|
||||
@@ -93,12 +89,10 @@ impl<V: ViewInputHandler> InputHandler for ElementInputHandler<V> {
|
||||
fn text_for_range(
|
||||
&mut self,
|
||||
range_utf16: Range<usize>,
|
||||
adjusted_range: &mut Option<Range<usize>>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Option<String> {
|
||||
self.view.update(cx, |view, cx| {
|
||||
view.text_for_range(range_utf16, adjusted_range, cx)
|
||||
})
|
||||
self.view
|
||||
.update(cx, |view, cx| view.text_for_range(range_utf16, cx))
|
||||
}
|
||||
|
||||
fn replace_text_in_range(
|
||||
|
||||
@@ -643,13 +643,9 @@ impl PlatformInputHandler {
|
||||
}
|
||||
|
||||
#[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
|
||||
fn text_for_range(
|
||||
&mut self,
|
||||
range_utf16: Range<usize>,
|
||||
adjusted: &mut Option<Range<usize>>,
|
||||
) -> Option<String> {
|
||||
fn text_for_range(&mut self, range_utf16: Range<usize>) -> Option<String> {
|
||||
self.cx
|
||||
.update(|cx| self.handler.text_for_range(range_utf16, adjusted, cx))
|
||||
.update(|cx| self.handler.text_for_range(range_utf16, cx))
|
||||
.ok()
|
||||
.flatten()
|
||||
}
|
||||
@@ -716,7 +712,6 @@ impl PlatformInputHandler {
|
||||
|
||||
/// A struct representing a selection in a text buffer, in UTF16 characters.
|
||||
/// This is different from a range because the head may be before the tail.
|
||||
#[derive(Debug)]
|
||||
pub struct UTF16Selection {
|
||||
/// The range of text in the document this selection corresponds to
|
||||
/// in UTF16 characters.
|
||||
@@ -754,7 +749,6 @@ pub trait InputHandler: 'static {
|
||||
fn text_for_range(
|
||||
&mut self,
|
||||
range_utf16: Range<usize>,
|
||||
adjusted_range: &mut Option<Range<usize>>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Option<String>;
|
||||
|
||||
|
||||
@@ -12,15 +12,14 @@ pub struct Keystroke {
|
||||
/// e.g. for option-s, key is "s"
|
||||
pub key: String,
|
||||
|
||||
/// key_char is the character that could have been typed when
|
||||
/// this binding was pressed.
|
||||
/// e.g. for s this is "s", for option-s "ß", and cmd-s None
|
||||
pub key_char: Option<String>,
|
||||
/// ime_key is the character inserted by the IME engine when that key was pressed.
|
||||
/// e.g. for option-s, ime_key is "ß"
|
||||
pub ime_key: Option<String>,
|
||||
}
|
||||
|
||||
impl Keystroke {
|
||||
/// When matching a key we cannot know whether the user intended to type
|
||||
/// the key_char or the key itself. On some non-US keyboards keys we use in our
|
||||
/// the ime_key or the key itself. On some non-US keyboards keys we use in our
|
||||
/// bindings are behind option (for example `$` is typed `alt-ç` on a Czech keyboard),
|
||||
/// and on some keyboards the IME handler converts a sequence of keys into a
|
||||
/// specific character (for example `"` is typed as `" space` on a brazilian keyboard).
|
||||
@@ -28,10 +27,10 @@ impl Keystroke {
|
||||
/// This method assumes that `self` was typed and `target' is in the keymap, and checks
|
||||
/// both possibilities for self against the target.
|
||||
pub(crate) fn should_match(&self, target: &Keystroke) -> bool {
|
||||
if let Some(key_char) = self
|
||||
.key_char
|
||||
if let Some(ime_key) = self
|
||||
.ime_key
|
||||
.as_ref()
|
||||
.filter(|key_char| key_char != &&self.key)
|
||||
.filter(|ime_key| ime_key != &&self.key)
|
||||
{
|
||||
let ime_modifiers = Modifiers {
|
||||
control: self.modifiers.control,
|
||||
@@ -39,7 +38,7 @@ impl Keystroke {
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
if &target.key == key_char && target.modifiers == ime_modifiers {
|
||||
if &target.key == ime_key && target.modifiers == ime_modifiers {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -48,9 +47,9 @@ impl Keystroke {
|
||||
}
|
||||
|
||||
/// key syntax is:
|
||||
/// [ctrl-][alt-][shift-][cmd-][fn-]key[->key_char]
|
||||
/// key_char syntax is only used for generating test events,
|
||||
/// when matching a key with an key_char set will be matched without it.
|
||||
/// [ctrl-][alt-][shift-][cmd-][fn-]key[->ime_key]
|
||||
/// ime_key syntax is only used for generating test events,
|
||||
/// when matching a key with an ime_key set will be matched without it.
|
||||
pub fn parse(source: &str) -> anyhow::Result<Self> {
|
||||
let mut control = false;
|
||||
let mut alt = false;
|
||||
@@ -58,7 +57,7 @@ impl Keystroke {
|
||||
let mut platform = false;
|
||||
let mut function = false;
|
||||
let mut key = None;
|
||||
let mut key_char = None;
|
||||
let mut ime_key = None;
|
||||
|
||||
let mut components = source.split('-').peekable();
|
||||
while let Some(component) = components.next() {
|
||||
@@ -75,7 +74,7 @@ impl Keystroke {
|
||||
break;
|
||||
} else if next.len() > 1 && next.starts_with('>') {
|
||||
key = Some(String::from(component));
|
||||
key_char = Some(String::from(&next[1..]));
|
||||
ime_key = Some(String::from(&next[1..]));
|
||||
components.next();
|
||||
} else {
|
||||
return Err(anyhow!("Invalid keystroke `{}`", source));
|
||||
@@ -119,7 +118,7 @@ impl Keystroke {
|
||||
function,
|
||||
},
|
||||
key,
|
||||
key_char: key_char,
|
||||
ime_key,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -155,7 +154,7 @@ impl Keystroke {
|
||||
/// Returns true if this keystroke left
|
||||
/// the ime system in an incomplete state.
|
||||
pub fn is_ime_in_progress(&self) -> bool {
|
||||
self.key_char.is_none()
|
||||
self.ime_key.is_none()
|
||||
&& (is_printable_key(&self.key) || self.key.is_empty())
|
||||
&& !(self.modifiers.platform
|
||||
|| self.modifiers.control
|
||||
@@ -163,17 +162,17 @@ impl Keystroke {
|
||||
|| self.modifiers.alt)
|
||||
}
|
||||
|
||||
/// Returns a new keystroke with the key_char filled.
|
||||
/// Returns a new keystroke with the ime_key filled.
|
||||
/// This is used for dispatch_keystroke where we want users to
|
||||
/// be able to simulate typing "space", etc.
|
||||
pub fn with_simulated_ime(mut self) -> Self {
|
||||
if self.key_char.is_none()
|
||||
if self.ime_key.is_none()
|
||||
&& !self.modifiers.platform
|
||||
&& !self.modifiers.control
|
||||
&& !self.modifiers.function
|
||||
&& !self.modifiers.alt
|
||||
{
|
||||
self.key_char = match self.key.as_str() {
|
||||
self.ime_key = match self.key.as_str() {
|
||||
"space" => Some(" ".into()),
|
||||
"tab" => Some("\t".into()),
|
||||
"enter" => Some("\n".into()),
|
||||
|
||||
@@ -742,14 +742,14 @@ impl Keystroke {
|
||||
}
|
||||
}
|
||||
|
||||
// Ignore control characters (and DEL) for the purposes of key_char
|
||||
let key_char =
|
||||
// Ignore control characters (and DEL) for the purposes of ime_key
|
||||
let ime_key =
|
||||
(key_utf32 >= 32 && key_utf32 != 127 && !key_utf8.is_empty()).then_some(key_utf8);
|
||||
|
||||
Keystroke {
|
||||
modifiers,
|
||||
key,
|
||||
key_char,
|
||||
ime_key,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1208,7 +1208,7 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr {
|
||||
compose.feed(keysym);
|
||||
match compose.status() {
|
||||
xkb::Status::Composing => {
|
||||
keystroke.key_char = None;
|
||||
keystroke.ime_key = None;
|
||||
state.pre_edit_text =
|
||||
compose.utf8().or(Keystroke::underlying_dead_key(keysym));
|
||||
let pre_edit =
|
||||
@@ -1220,7 +1220,7 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr {
|
||||
|
||||
xkb::Status::Composed => {
|
||||
state.pre_edit_text.take();
|
||||
keystroke.key_char = compose.utf8();
|
||||
keystroke.ime_key = compose.utf8();
|
||||
if let Some(keysym) = compose.keysym() {
|
||||
keystroke.key = xkb::keysym_get_name(keysym);
|
||||
}
|
||||
@@ -1340,7 +1340,7 @@ impl Dispatch<zwp_text_input_v3::ZwpTextInputV3, ()> for WaylandClientStatePtr {
|
||||
keystroke: Keystroke {
|
||||
modifiers: Modifiers::default(),
|
||||
key: commit_text.clone(),
|
||||
key_char: Some(commit_text),
|
||||
ime_key: Some(commit_text),
|
||||
},
|
||||
is_held: false,
|
||||
}));
|
||||
|
||||
@@ -687,11 +687,11 @@ impl WaylandWindowStatePtr {
|
||||
}
|
||||
}
|
||||
if let PlatformInput::KeyDown(event) = input {
|
||||
if let Some(key_char) = &event.keystroke.key_char {
|
||||
if let Some(ime_key) = &event.keystroke.ime_key {
|
||||
let mut state = self.state.borrow_mut();
|
||||
if let Some(mut input_handler) = state.input_handler.take() {
|
||||
drop(state);
|
||||
input_handler.replace_text_in_range(None, key_char);
|
||||
input_handler.replace_text_in_range(None, ime_key);
|
||||
self.state.borrow_mut().input_handler = Some(input_handler);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,7 +178,7 @@ pub struct X11ClientState {
|
||||
pub(crate) compose_state: Option<xkbc::compose::State>,
|
||||
pub(crate) pre_edit_text: Option<String>,
|
||||
pub(crate) composing: bool,
|
||||
pub(crate) pre_key_char_down: Option<Keystroke>,
|
||||
pub(crate) pre_ime_key_down: Option<Keystroke>,
|
||||
pub(crate) cursor_handle: cursor::Handle,
|
||||
pub(crate) cursor_styles: HashMap<xproto::Window, CursorStyle>,
|
||||
pub(crate) cursor_cache: HashMap<CursorStyle, xproto::Cursor>,
|
||||
@@ -446,7 +446,7 @@ impl X11Client {
|
||||
|
||||
compose_state,
|
||||
pre_edit_text: None,
|
||||
pre_key_char_down: None,
|
||||
pre_ime_key_down: None,
|
||||
composing: false,
|
||||
|
||||
cursor_handle,
|
||||
@@ -858,7 +858,7 @@ impl X11Client {
|
||||
|
||||
let modifiers = modifiers_from_state(event.state);
|
||||
state.modifiers = modifiers;
|
||||
state.pre_key_char_down.take();
|
||||
state.pre_ime_key_down.take();
|
||||
let keystroke = {
|
||||
let code = event.detail.into();
|
||||
let xkb_state = state.previous_xkb_state.clone();
|
||||
@@ -880,13 +880,13 @@ impl X11Client {
|
||||
match compose_state.status() {
|
||||
xkbc::Status::Composed => {
|
||||
state.pre_edit_text.take();
|
||||
keystroke.key_char = compose_state.utf8();
|
||||
keystroke.ime_key = compose_state.utf8();
|
||||
if let Some(keysym) = compose_state.keysym() {
|
||||
keystroke.key = xkbc::keysym_get_name(keysym);
|
||||
}
|
||||
}
|
||||
xkbc::Status::Composing => {
|
||||
keystroke.key_char = None;
|
||||
keystroke.ime_key = None;
|
||||
state.pre_edit_text = compose_state
|
||||
.utf8()
|
||||
.or(crate::Keystroke::underlying_dead_key(keysym));
|
||||
@@ -1156,7 +1156,7 @@ impl X11Client {
|
||||
match event {
|
||||
Event::KeyPress(event) | Event::KeyRelease(event) => {
|
||||
let mut state = self.0.borrow_mut();
|
||||
state.pre_key_char_down = Some(Keystroke::from_xkb(
|
||||
state.pre_ime_key_down = Some(Keystroke::from_xkb(
|
||||
&state.xkb,
|
||||
state.modifiers,
|
||||
event.detail.into(),
|
||||
@@ -1187,11 +1187,11 @@ impl X11Client {
|
||||
fn xim_handle_commit(&self, window: xproto::Window, text: String) -> Option<()> {
|
||||
let window = self.get_window(window).unwrap();
|
||||
let mut state = self.0.borrow_mut();
|
||||
let keystroke = state.pre_key_char_down.take();
|
||||
let keystroke = state.pre_ime_key_down.take();
|
||||
state.composing = false;
|
||||
drop(state);
|
||||
if let Some(mut keystroke) = keystroke {
|
||||
keystroke.key_char = Some(text.clone());
|
||||
keystroke.ime_key = Some(text.clone());
|
||||
window.handle_input(PlatformInput::KeyDown(crate::KeyDownEvent {
|
||||
keystroke,
|
||||
is_held: false,
|
||||
|
||||
@@ -846,9 +846,9 @@ impl X11WindowStatePtr {
|
||||
if let PlatformInput::KeyDown(event) = input {
|
||||
let mut state = self.state.borrow_mut();
|
||||
if let Some(mut input_handler) = state.input_handler.take() {
|
||||
if let Some(key_char) = &event.keystroke.key_char {
|
||||
if let Some(ime_key) = &event.keystroke.ime_key {
|
||||
drop(state);
|
||||
input_handler.replace_text_in_range(None, key_char);
|
||||
input_handler.replace_text_in_range(None, ime_key);
|
||||
state = self.state.borrow_mut();
|
||||
}
|
||||
state.input_handler = Some(input_handler);
|
||||
|
||||
@@ -245,7 +245,7 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke {
|
||||
.charactersIgnoringModifiers()
|
||||
.to_str()
|
||||
.to_string();
|
||||
let mut key_char = None;
|
||||
let mut ime_key = None;
|
||||
let first_char = characters.chars().next().map(|ch| ch as u16);
|
||||
let modifiers = native_event.modifierFlags();
|
||||
|
||||
@@ -261,19 +261,13 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke {
|
||||
#[allow(non_upper_case_globals)]
|
||||
let key = match first_char {
|
||||
Some(SPACE_KEY) => {
|
||||
key_char = Some(" ".to_string());
|
||||
ime_key = Some(" ".to_string());
|
||||
"space".to_string()
|
||||
}
|
||||
Some(TAB_KEY) => {
|
||||
key_char = Some("\t".to_string());
|
||||
"tab".to_string()
|
||||
}
|
||||
Some(ENTER_KEY) | Some(NUMPAD_ENTER_KEY) => {
|
||||
key_char = Some("\n".to_string());
|
||||
"enter".to_string()
|
||||
}
|
||||
Some(BACKSPACE_KEY) => "backspace".to_string(),
|
||||
Some(ENTER_KEY) | Some(NUMPAD_ENTER_KEY) => "enter".to_string(),
|
||||
Some(ESCAPE_KEY) => "escape".to_string(),
|
||||
Some(TAB_KEY) => "tab".to_string(),
|
||||
Some(SHIFT_TAB_KEY) => "tab".to_string(),
|
||||
Some(NSUpArrowFunctionKey) => "up".to_string(),
|
||||
Some(NSDownArrowFunctionKey) => "down".to_string(),
|
||||
@@ -341,18 +335,6 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke {
|
||||
chars_ignoring_modifiers = chars_with_cmd;
|
||||
}
|
||||
|
||||
if !control && !command && !function {
|
||||
let mut mods = NO_MOD;
|
||||
if shift {
|
||||
mods |= SHIFT_MOD;
|
||||
}
|
||||
if alt {
|
||||
mods |= OPTION_MOD;
|
||||
}
|
||||
|
||||
key_char = Some(chars_for_modified_key(native_event.keyCode(), mods));
|
||||
}
|
||||
|
||||
let mut key = if shift
|
||||
&& chars_ignoring_modifiers
|
||||
.chars()
|
||||
@@ -366,6 +348,20 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke {
|
||||
chars_ignoring_modifiers
|
||||
};
|
||||
|
||||
if always_use_cmd_layout || alt {
|
||||
let mut mods = NO_MOD;
|
||||
if shift {
|
||||
mods |= SHIFT_MOD;
|
||||
}
|
||||
if alt {
|
||||
mods |= OPTION_MOD;
|
||||
}
|
||||
let alt_key = chars_for_modified_key(native_event.keyCode(), mods);
|
||||
if alt_key != key {
|
||||
ime_key = Some(alt_key);
|
||||
}
|
||||
};
|
||||
|
||||
key
|
||||
}
|
||||
};
|
||||
@@ -379,7 +375,7 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke {
|
||||
function,
|
||||
},
|
||||
key,
|
||||
key_char,
|
||||
ime_key,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -844,9 +844,7 @@ impl Platform for MacPlatform {
|
||||
let app: id = msg_send![APP_CLASS, sharedApplication];
|
||||
let mut state = self.0.lock();
|
||||
let actions = &mut state.menu_actions;
|
||||
let menu = self.create_menu_bar(menus, NSWindow::delegate(app), actions, keymap);
|
||||
drop(state);
|
||||
app.setMainMenu_(menu);
|
||||
app.setMainMenu_(self.create_menu_bar(menus, NSWindow::delegate(app), actions, keymap));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,6 @@ use std::{
|
||||
cell::Cell,
|
||||
ffi::{c_void, CStr},
|
||||
mem,
|
||||
ops::Range,
|
||||
path::PathBuf,
|
||||
ptr::{self, NonNull},
|
||||
rc::Rc,
|
||||
@@ -1284,17 +1283,18 @@ extern "C" fn handle_key_event(this: &Object, native_event: id, key_equivalent:
|
||||
}
|
||||
|
||||
if event.is_held {
|
||||
if let Some(key_char) = event.keystroke.key_char.as_ref() {
|
||||
let handled = with_input_handler(&this, |input_handler| {
|
||||
if !input_handler.apple_press_and_hold_enabled() {
|
||||
input_handler.replace_text_in_range(None, &key_char);
|
||||
return YES;
|
||||
}
|
||||
NO
|
||||
});
|
||||
if handled == Some(YES) {
|
||||
let handled = with_input_handler(&this, |input_handler| {
|
||||
if !input_handler.apple_press_and_hold_enabled() {
|
||||
input_handler.replace_text_in_range(
|
||||
None,
|
||||
&event.keystroke.ime_key.unwrap_or(event.keystroke.key),
|
||||
);
|
||||
return YES;
|
||||
}
|
||||
NO
|
||||
});
|
||||
if handled == Some(YES) {
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1437,7 +1437,7 @@ extern "C" fn cancel_operation(this: &Object, _sel: Sel, _sender: id) {
|
||||
let keystroke = Keystroke {
|
||||
modifiers: Default::default(),
|
||||
key: ".".into(),
|
||||
key_char: None,
|
||||
ime_key: None,
|
||||
};
|
||||
let event = PlatformInput::KeyDown(KeyDownEvent {
|
||||
keystroke: keystroke.clone(),
|
||||
@@ -1755,21 +1755,15 @@ extern "C" fn attributed_substring_for_proposed_range(
|
||||
this: &Object,
|
||||
_: Sel,
|
||||
range: NSRange,
|
||||
actual_range: *mut c_void,
|
||||
_actual_range: *mut c_void,
|
||||
) -> id {
|
||||
with_input_handler(this, |input_handler| {
|
||||
let range = range.to_range()?;
|
||||
if range.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let mut adjusted: Option<Range<usize>> = None;
|
||||
|
||||
let selected_text = input_handler.text_for_range(range.clone(), &mut adjusted)?;
|
||||
if let Some(adjusted) = adjusted {
|
||||
if adjusted != range {
|
||||
unsafe { (actual_range as *mut NSRange).write(NSRange::from(adjusted)) };
|
||||
}
|
||||
}
|
||||
let selected_text = input_handler.text_for_range(range.clone())?;
|
||||
unsafe {
|
||||
let string: id = msg_send![class!(NSAttributedString), alloc];
|
||||
let string: id = msg_send![string, initWithString: ns_string(&selected_text)];
|
||||
|
||||
@@ -386,7 +386,7 @@ fn handle_char_msg(
|
||||
return Some(1);
|
||||
};
|
||||
drop(lock);
|
||||
let key_char = keystroke.key_char.clone();
|
||||
let ime_key = keystroke.ime_key.clone();
|
||||
let event = KeyDownEvent {
|
||||
keystroke,
|
||||
is_held: lparam.0 & (0x1 << 30) > 0,
|
||||
@@ -397,7 +397,7 @@ fn handle_char_msg(
|
||||
if dispatch_event_result.default_prevented || !dispatch_event_result.propagate {
|
||||
return Some(0);
|
||||
}
|
||||
let Some(ime_char) = key_char else {
|
||||
let Some(ime_char) = ime_key else {
|
||||
return Some(1);
|
||||
};
|
||||
with_input_handler(&state_ptr, |input_handler| {
|
||||
@@ -1172,7 +1172,7 @@ fn parse_syskeydown_msg_keystroke(wparam: WPARAM) -> Option<Keystroke> {
|
||||
Some(Keystroke {
|
||||
modifiers,
|
||||
key,
|
||||
key_char: None,
|
||||
ime_key: None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1220,7 +1220,7 @@ fn parse_keydown_msg_keystroke(wparam: WPARAM) -> Option<KeystrokeOrModifier> {
|
||||
return Some(KeystrokeOrModifier::Keystroke(Keystroke {
|
||||
modifiers,
|
||||
key: format!("f{}", offset + 1),
|
||||
key_char: None,
|
||||
ime_key: None,
|
||||
}));
|
||||
};
|
||||
return None;
|
||||
@@ -1231,7 +1231,7 @@ fn parse_keydown_msg_keystroke(wparam: WPARAM) -> Option<KeystrokeOrModifier> {
|
||||
Some(KeystrokeOrModifier::Keystroke(Keystroke {
|
||||
modifiers,
|
||||
key,
|
||||
key_char: None,
|
||||
ime_key: None,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -1253,7 +1253,7 @@ fn parse_char_msg_keystroke(wparam: WPARAM) -> Option<Keystroke> {
|
||||
Some(Keystroke {
|
||||
modifiers,
|
||||
key,
|
||||
key_char: Some(first_char.to_string()),
|
||||
ime_key: Some(first_char.to_string()),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1327,7 +1327,7 @@ fn basic_vkcode_to_string(code: u16, modifiers: Modifiers) -> Option<Keystroke>
|
||||
Some(Keystroke {
|
||||
modifiers,
|
||||
key,
|
||||
key_char: None,
|
||||
ime_key: None,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -292,7 +292,7 @@ impl Platform for WindowsPlatform {
|
||||
pid,
|
||||
app_path.display(),
|
||||
);
|
||||
let restart_process = util::command::new_std_command("powershell.exe")
|
||||
let restart_process = std::process::Command::new("powershell.exe")
|
||||
.arg("-command")
|
||||
.arg(script)
|
||||
.spawn();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::{px, FontId, FontRun, Pixels, PlatformTextSystem, SharedString, TextRun};
|
||||
use crate::{px, FontId, FontRun, Pixels, PlatformTextSystem, SharedString};
|
||||
use collections::HashMap;
|
||||
use std::{iter, sync::Arc};
|
||||
|
||||
@@ -104,7 +104,6 @@ impl LineWrapper {
|
||||
line: SharedString,
|
||||
truncate_width: Pixels,
|
||||
ellipsis: Option<&str>,
|
||||
runs: &mut Vec<TextRun>,
|
||||
) -> SharedString {
|
||||
let mut width = px(0.);
|
||||
let mut ellipsis_width = px(0.);
|
||||
@@ -125,15 +124,15 @@ impl LineWrapper {
|
||||
width += char_width;
|
||||
|
||||
if width.floor() > truncate_width {
|
||||
let ellipsis = ellipsis.unwrap_or("");
|
||||
let result = SharedString::from(format!("{}{}", &line[..truncate_ix], ellipsis));
|
||||
update_runs_after_truncation(&result, ellipsis, runs);
|
||||
|
||||
return result;
|
||||
return SharedString::from(format!(
|
||||
"{}{}",
|
||||
&line[..truncate_ix],
|
||||
ellipsis.unwrap_or("")
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
line
|
||||
line.clone()
|
||||
}
|
||||
|
||||
pub(crate) fn is_word_char(c: char) -> bool {
|
||||
@@ -196,23 +195,6 @@ impl LineWrapper {
|
||||
}
|
||||
}
|
||||
|
||||
fn update_runs_after_truncation(result: &str, ellipsis: &str, runs: &mut Vec<TextRun>) {
|
||||
let mut truncate_at = result.len() - ellipsis.len();
|
||||
let mut run_end = None;
|
||||
for (run_index, run) in runs.iter_mut().enumerate() {
|
||||
if run.len <= truncate_at {
|
||||
truncate_at -= run.len;
|
||||
} else {
|
||||
run.len = truncate_at + ellipsis.len();
|
||||
run_end = Some(run_index + 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if let Some(run_end) = run_end {
|
||||
runs.truncate(run_end);
|
||||
}
|
||||
}
|
||||
|
||||
/// A boundary between two lines of text.
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Boundary {
|
||||
@@ -231,9 +213,7 @@ impl Boundary {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{
|
||||
font, Font, FontFeatures, FontStyle, FontWeight, Hsla, TestAppContext, TestDispatcher,
|
||||
};
|
||||
use crate::{font, TestAppContext, TestDispatcher};
|
||||
#[cfg(target_os = "macos")]
|
||||
use crate::{TextRun, WindowTextSystem, WrapBoundary};
|
||||
use rand::prelude::*;
|
||||
@@ -252,26 +232,6 @@ mod tests {
|
||||
LineWrapper::new(id, px(16.), cx.text_system().platform_text_system.clone())
|
||||
}
|
||||
|
||||
fn generate_test_runs(input_run_len: &[usize]) -> Vec<TextRun> {
|
||||
input_run_len
|
||||
.iter()
|
||||
.map(|run_len| TextRun {
|
||||
len: *run_len,
|
||||
font: Font {
|
||||
family: "Dummy".into(),
|
||||
features: FontFeatures::default(),
|
||||
fallbacks: None,
|
||||
weight: FontWeight::default(),
|
||||
style: FontStyle::Normal,
|
||||
},
|
||||
color: Hsla::default(),
|
||||
background_color: None,
|
||||
underline: None,
|
||||
strikethrough: None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_wrap_line() {
|
||||
let mut wrapper = build_wrapper();
|
||||
@@ -333,135 +293,28 @@ mod tests {
|
||||
fn test_truncate_line() {
|
||||
let mut wrapper = build_wrapper();
|
||||
|
||||
fn perform_test(
|
||||
wrapper: &mut LineWrapper,
|
||||
text: &'static str,
|
||||
result: &'static str,
|
||||
ellipsis: Option<&str>,
|
||||
) {
|
||||
let dummy_run_lens = vec![text.len()];
|
||||
let mut dummy_runs = generate_test_runs(&dummy_run_lens);
|
||||
assert_eq!(
|
||||
wrapper.truncate_line(text.into(), px(220.), ellipsis, &mut dummy_runs),
|
||||
result
|
||||
);
|
||||
assert_eq!(dummy_runs.first().unwrap().len, result.len());
|
||||
}
|
||||
|
||||
perform_test(
|
||||
&mut wrapper,
|
||||
"aa bbb cccc ddddd eeee ffff gggg",
|
||||
"aa bbb cccc ddddd eeee",
|
||||
None,
|
||||
assert_eq!(
|
||||
wrapper.truncate_line("aa bbb cccc ddddd eeee ffff gggg".into(), px(220.), None),
|
||||
"aa bbb cccc ddddd eeee"
|
||||
);
|
||||
perform_test(
|
||||
&mut wrapper,
|
||||
"aa bbb cccc ddddd eeee ffff gggg",
|
||||
"aa bbb cccc ddddd eee…",
|
||||
Some("…"),
|
||||
assert_eq!(
|
||||
wrapper.truncate_line(
|
||||
"aa bbb cccc ddddd eeee ffff gggg".into(),
|
||||
px(220.),
|
||||
Some("…")
|
||||
),
|
||||
"aa bbb cccc ddddd eee…"
|
||||
);
|
||||
perform_test(
|
||||
&mut wrapper,
|
||||
"aa bbb cccc ddddd eeee ffff gggg",
|
||||
"aa bbb cccc dddd......",
|
||||
Some("......"),
|
||||
assert_eq!(
|
||||
wrapper.truncate_line(
|
||||
"aa bbb cccc ddddd eeee ffff gggg".into(),
|
||||
px(220.),
|
||||
Some("......")
|
||||
),
|
||||
"aa bbb cccc dddd......"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_truncate_multiple_runs() {
|
||||
let mut wrapper = build_wrapper();
|
||||
|
||||
fn perform_test(
|
||||
wrapper: &mut LineWrapper,
|
||||
text: &'static str,
|
||||
result: &str,
|
||||
run_lens: &[usize],
|
||||
result_run_len: &[usize],
|
||||
line_width: Pixels,
|
||||
) {
|
||||
let mut dummy_runs = generate_test_runs(run_lens);
|
||||
assert_eq!(
|
||||
wrapper.truncate_line(text.into(), line_width, Some("…"), &mut dummy_runs),
|
||||
result
|
||||
);
|
||||
for (run, result_len) in dummy_runs.iter().zip(result_run_len) {
|
||||
assert_eq!(run.len, *result_len);
|
||||
}
|
||||
}
|
||||
// Case 0: Normal
|
||||
// Text: abcdefghijkl
|
||||
// Runs: Run0 { len: 12, ... }
|
||||
//
|
||||
// Truncate res: abcd… (truncate_at = 4)
|
||||
// Run res: Run0 { string: abcd…, len: 7, ... }
|
||||
perform_test(&mut wrapper, "abcdefghijkl", "abcd…", &[12], &[7], px(50.));
|
||||
// Case 1: Drop some runs
|
||||
// Text: abcdefghijkl
|
||||
// Runs: Run0 { len: 4, ... }, Run1 { len: 4, ... }, Run2 { len: 4, ... }
|
||||
//
|
||||
// Truncate res: abcdef… (truncate_at = 6)
|
||||
// Runs res: Run0 { string: abcd, len: 4, ... }, Run1 { string: ef…, len:
|
||||
// 5, ... }
|
||||
perform_test(
|
||||
&mut wrapper,
|
||||
"abcdefghijkl",
|
||||
"abcdef…",
|
||||
&[4, 4, 4],
|
||||
&[4, 5],
|
||||
px(70.),
|
||||
);
|
||||
// Case 2: Truncate at start of some run
|
||||
// Text: abcdefghijkl
|
||||
// Runs: Run0 { len: 4, ... }, Run1 { len: 4, ... }, Run2 { len: 4, ... }
|
||||
//
|
||||
// Truncate res: abcdefgh… (truncate_at = 8)
|
||||
// Runs res: Run0 { string: abcd, len: 4, ... }, Run1 { string: efgh, len:
|
||||
// 4, ... }, Run2 { string: …, len: 3, ... }
|
||||
perform_test(
|
||||
&mut wrapper,
|
||||
"abcdefghijkl",
|
||||
"abcdefgh…",
|
||||
&[4, 4, 4],
|
||||
&[4, 4, 3],
|
||||
px(90.),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_update_run_after_truncation() {
|
||||
fn perform_test(result: &str, run_lens: &[usize], result_run_lens: &[usize]) {
|
||||
let mut dummy_runs = generate_test_runs(run_lens);
|
||||
update_runs_after_truncation(result, "…", &mut dummy_runs);
|
||||
for (run, result_len) in dummy_runs.iter().zip(result_run_lens) {
|
||||
assert_eq!(run.len, *result_len);
|
||||
}
|
||||
}
|
||||
// Case 0: Normal
|
||||
// Text: abcdefghijkl
|
||||
// Runs: Run0 { len: 12, ... }
|
||||
//
|
||||
// Truncate res: abcd… (truncate_at = 4)
|
||||
// Run res: Run0 { string: abcd…, len: 7, ... }
|
||||
perform_test("abcd…", &[12], &[7]);
|
||||
// Case 1: Drop some runs
|
||||
// Text: abcdefghijkl
|
||||
// Runs: Run0 { len: 4, ... }, Run1 { len: 4, ... }, Run2 { len: 4, ... }
|
||||
//
|
||||
// Truncate res: abcdef… (truncate_at = 6)
|
||||
// Runs res: Run0 { string: abcd, len: 4, ... }, Run1 { string: ef…, len:
|
||||
// 5, ... }
|
||||
perform_test("abcdef…", &[4, 4, 4], &[4, 5]);
|
||||
// Case 2: Truncate at start of some run
|
||||
// Text: abcdefghijkl
|
||||
// Runs: Run0 { len: 4, ... }, Run1 { len: 4, ... }, Run2 { len: 4, ... }
|
||||
//
|
||||
// Truncate res: abcdefgh… (truncate_at = 8)
|
||||
// Runs res: Run0 { string: abcd, len: 4, ... }, Run1 { string: efgh, len:
|
||||
// 4, ... }, Run2 { string: …, len: 3, ... }
|
||||
perform_test("abcdefgh…", &[4, 4, 4], &[4, 4, 3]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_word_char() {
|
||||
#[track_caller]
|
||||
|
||||
@@ -3038,7 +3038,7 @@ impl<'a> WindowContext<'a> {
|
||||
return true;
|
||||
}
|
||||
|
||||
if let Some(input) = keystroke.key_char {
|
||||
if let Some(input) = keystroke.with_simulated_ime().ime_key {
|
||||
if let Some(mut input_handler) = self.window.platform_window.take_input_handler() {
|
||||
input_handler.dispatch_input(&input, self);
|
||||
self.window.platform_window.set_input_handler(input_handler);
|
||||
@@ -3267,7 +3267,7 @@ impl<'a> WindowContext<'a> {
|
||||
if let Some(key) = key {
|
||||
keystroke = Some(Keystroke {
|
||||
key: key.to_string(),
|
||||
key_char: None,
|
||||
ime_key: None,
|
||||
modifiers: Modifiers::default(),
|
||||
});
|
||||
}
|
||||
@@ -3482,7 +3482,13 @@ impl<'a> WindowContext<'a> {
|
||||
if !self.propagate_event {
|
||||
continue 'replay;
|
||||
}
|
||||
if let Some(input) = replay.keystroke.key_char.as_ref().cloned() {
|
||||
if let Some(input) = replay
|
||||
.keystroke
|
||||
.with_simulated_ime()
|
||||
.ime_key
|
||||
.as_ref()
|
||||
.cloned()
|
||||
{
|
||||
if let Some(mut input_handler) = self.window.platform_window.take_input_handler() {
|
||||
input_handler.dispatch_input(&input, self);
|
||||
self.window.platform_window.set_input_handler(input_handler)
|
||||
|
||||
@@ -116,7 +116,7 @@ impl Item for ImageView {
|
||||
.map(Icon::from_path)
|
||||
}
|
||||
|
||||
fn breadcrumb_location(&self, _: &AppContext) -> ToolbarItemLocation {
|
||||
fn breadcrumb_location(&self) -> ToolbarItemLocation {
|
||||
ToolbarItemLocation::PrimaryLeft
|
||||
}
|
||||
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
[package]
|
||||
name = "inline_completion"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/inline_completion.rs"
|
||||
|
||||
[dependencies]
|
||||
gpui.workspace = true
|
||||
language.workspace = true
|
||||
project.workspace = true
|
||||
text.workspace = true
|
||||
@@ -23,6 +23,7 @@ paths.workspace = true
|
||||
settings.workspace = true
|
||||
supermaven.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
workspace.workspace = true
|
||||
zed_actions.workspace = true
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use anyhow::Result;
|
||||
use copilot::{Copilot, Status};
|
||||
use copilot::{Copilot, CopilotCodeVerification, Status};
|
||||
use editor::{scroll::Autoscroll, Editor};
|
||||
use fs::Fs;
|
||||
use gpui::{
|
||||
@@ -15,6 +15,7 @@ use language::{
|
||||
use settings::{update_settings_file, Settings, SettingsStore};
|
||||
use std::{path::Path, sync::Arc};
|
||||
use supermaven::{AccountStatus, Supermaven};
|
||||
use util::ResultExt;
|
||||
use workspace::{
|
||||
create_and_open_local_file,
|
||||
item::ItemHandle,
|
||||
@@ -28,6 +29,8 @@ use zed_actions::OpenBrowser;
|
||||
|
||||
const COPILOT_SETTINGS_URL: &str = "https://github.com/settings/copilot";
|
||||
|
||||
struct CopilotStartingToast;
|
||||
|
||||
struct CopilotErrorToast;
|
||||
|
||||
pub struct InlineCompletionButton {
|
||||
@@ -218,7 +221,7 @@ impl InlineCompletionButton {
|
||||
pub fn build_copilot_start_menu(&mut self, cx: &mut ViewContext<Self>) -> View<ContextMenu> {
|
||||
let fs = self.fs.clone();
|
||||
ContextMenu::build(cx, |menu, _| {
|
||||
menu.entry("Sign In", None, copilot::initiate_sign_in)
|
||||
menu.entry("Sign In", None, initiate_sign_in)
|
||||
.entry("Disable Copilot", None, {
|
||||
let fs = fs.clone();
|
||||
move |cx| hide_copilot(fs.clone(), cx)
|
||||
@@ -481,3 +484,68 @@ fn hide_copilot(fs: Arc<dyn Fs>, cx: &mut AppContext) {
|
||||
.inline_completion_provider = Some(InlineCompletionProvider::None);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn initiate_sign_in(cx: &mut WindowContext) {
|
||||
let Some(copilot) = Copilot::global(cx) else {
|
||||
return;
|
||||
};
|
||||
let status = copilot.read(cx).status();
|
||||
let Some(workspace) = cx.window_handle().downcast::<Workspace>() else {
|
||||
return;
|
||||
};
|
||||
match status {
|
||||
Status::Starting { task } => {
|
||||
let Some(workspace) = cx.window_handle().downcast::<Workspace>() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Ok(workspace) = workspace.update(cx, |workspace, cx| {
|
||||
workspace.show_toast(
|
||||
Toast::new(
|
||||
NotificationId::unique::<CopilotStartingToast>(),
|
||||
"Copilot is starting...",
|
||||
),
|
||||
cx,
|
||||
);
|
||||
workspace.weak_handle()
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
|
||||
cx.spawn(|mut cx| async move {
|
||||
task.await;
|
||||
if let Some(copilot) = cx.update(|cx| Copilot::global(cx)).ok().flatten() {
|
||||
workspace
|
||||
.update(&mut cx, |workspace, cx| match copilot.read(cx).status() {
|
||||
Status::Authorized => workspace.show_toast(
|
||||
Toast::new(
|
||||
NotificationId::unique::<CopilotStartingToast>(),
|
||||
"Copilot has started!",
|
||||
),
|
||||
cx,
|
||||
),
|
||||
_ => {
|
||||
workspace.dismiss_toast(
|
||||
&NotificationId::unique::<CopilotStartingToast>(),
|
||||
cx,
|
||||
);
|
||||
copilot
|
||||
.update(cx, |copilot, cx| copilot.sign_in(cx))
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
_ => {
|
||||
copilot.update(cx, |this, cx| this.sign_in(cx)).detach();
|
||||
workspace
|
||||
.update(cx, |this, cx| {
|
||||
this.toggle_modal(cx, |cx| CopilotCodeVerification::new(&copilot, cx));
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,6 @@ async-watch.workspace = true
|
||||
clock.workspace = true
|
||||
collections.workspace = true
|
||||
ec4rs.workspace = true
|
||||
fs.workspace = true
|
||||
futures.workspace = true
|
||||
fuzzy.workspace = true
|
||||
git.workspace = true
|
||||
|
||||
@@ -21,7 +21,6 @@ use async_watch as watch;
|
||||
use clock::Lamport;
|
||||
pub use clock::ReplicaId;
|
||||
use collections::HashMap;
|
||||
use fs::MTime;
|
||||
use futures::channel::oneshot;
|
||||
use gpui::{
|
||||
AnyElement, AppContext, Context as _, EventEmitter, HighlightStyle, Model, ModelContext,
|
||||
@@ -52,7 +51,7 @@ use std::{
|
||||
path::{Path, PathBuf},
|
||||
str,
|
||||
sync::{Arc, LazyLock},
|
||||
time::{Duration, Instant},
|
||||
time::{Duration, Instant, SystemTime},
|
||||
vec,
|
||||
};
|
||||
use sum_tree::TreeMap;
|
||||
@@ -109,7 +108,7 @@ pub struct Buffer {
|
||||
file: Option<Arc<dyn File>>,
|
||||
/// The mtime of the file when this buffer was last loaded from
|
||||
/// or saved to disk.
|
||||
saved_mtime: Option<MTime>,
|
||||
saved_mtime: Option<SystemTime>,
|
||||
/// The version vector when this buffer was last loaded from
|
||||
/// or saved to disk.
|
||||
saved_version: clock::Global,
|
||||
@@ -407,19 +406,22 @@ pub trait File: Send + Sync {
|
||||
/// modified. In the case where the file is not stored, it can be either `New` or `Deleted`. In the
|
||||
/// UI these two states are distinguished. For example, the buffer tab does not display a deletion
|
||||
/// indicator for new files.
|
||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum DiskState {
|
||||
/// File created in Zed that has not been saved.
|
||||
New,
|
||||
/// File present on the filesystem.
|
||||
Present { mtime: MTime },
|
||||
Present {
|
||||
/// Last known mtime (modification time).
|
||||
mtime: SystemTime,
|
||||
},
|
||||
/// Deleted file that was previously present.
|
||||
Deleted,
|
||||
}
|
||||
|
||||
impl DiskState {
|
||||
/// Returns the file's last known modification time on disk.
|
||||
pub fn mtime(self) -> Option<MTime> {
|
||||
pub fn mtime(self) -> Option<SystemTime> {
|
||||
match self {
|
||||
DiskState::New => None,
|
||||
DiskState::Present { mtime } => Some(mtime),
|
||||
@@ -974,7 +976,7 @@ impl Buffer {
|
||||
}
|
||||
|
||||
/// The mtime of the buffer's file when the buffer was last saved or reloaded from disk.
|
||||
pub fn saved_mtime(&self) -> Option<MTime> {
|
||||
pub fn saved_mtime(&self) -> Option<SystemTime> {
|
||||
self.saved_mtime
|
||||
}
|
||||
|
||||
@@ -1009,7 +1011,7 @@ impl Buffer {
|
||||
pub fn did_save(
|
||||
&mut self,
|
||||
version: clock::Global,
|
||||
mtime: Option<MTime>,
|
||||
mtime: Option<SystemTime>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
self.saved_version = version;
|
||||
@@ -1075,7 +1077,7 @@ impl Buffer {
|
||||
&mut self,
|
||||
version: clock::Global,
|
||||
line_ending: LineEnding,
|
||||
mtime: Option<MTime>,
|
||||
mtime: Option<SystemTime>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
self.saved_version = version;
|
||||
@@ -1775,9 +1777,7 @@ impl Buffer {
|
||||
match file.disk_state() {
|
||||
DiskState::New => false,
|
||||
DiskState::Present { mtime } => match self.saved_mtime {
|
||||
Some(saved_mtime) => {
|
||||
mtime.bad_is_greater_than(saved_mtime) && self.has_unsaved_edits()
|
||||
}
|
||||
Some(saved_mtime) => mtime > saved_mtime && self.has_unsaved_edits(),
|
||||
None => true,
|
||||
},
|
||||
DiskState::Deleted => true,
|
||||
|
||||
@@ -20,8 +20,6 @@ pub struct Toolchain {
|
||||
pub name: SharedString,
|
||||
pub path: SharedString,
|
||||
pub language_name: LanguageName,
|
||||
/// Full toolchain data (including language-specific details)
|
||||
pub as_json: serde_json::Value,
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
@@ -31,8 +29,6 @@ pub trait ToolchainLister: Send + Sync {
|
||||
worktree_root: PathBuf,
|
||||
project_env: Option<HashMap<String, String>>,
|
||||
) -> ToolchainList;
|
||||
// Returns a term which we should use in UI to refer to a toolchain.
|
||||
fn term(&self) -> SharedString;
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
|
||||
@@ -13,31 +13,57 @@ path = "src/language_model.rs"
|
||||
doctest = false
|
||||
|
||||
[features]
|
||||
test-support = []
|
||||
test-support = [
|
||||
"editor/test-support",
|
||||
"language/test-support",
|
||||
"project/test-support",
|
||||
"text/test-support",
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
anthropic = { workspace = true, features = ["schemars"] }
|
||||
anyhow.workspace = true
|
||||
base64.workspace = true
|
||||
client.workspace = true
|
||||
collections.workspace = true
|
||||
copilot = { workspace = true, features = ["schemars"] }
|
||||
editor.workspace = true
|
||||
feature_flags.workspace = true
|
||||
futures.workspace = true
|
||||
google_ai = { workspace = true, features = ["schemars"] }
|
||||
gpui.workspace = true
|
||||
http_client.workspace = true
|
||||
image.workspace = true
|
||||
inline_completion_button.workspace = true
|
||||
log.workspace = true
|
||||
menu.workspace = true
|
||||
ollama = { workspace = true, features = ["schemars"] }
|
||||
open_ai = { workspace = true, features = ["schemars"] }
|
||||
parking_lot.workspace = true
|
||||
proto.workspace = true
|
||||
project.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
smol.workspace = true
|
||||
strum.workspace = true
|
||||
telemetry.workspace = true
|
||||
telemetry_events.workspace = true
|
||||
theme.workspace = true
|
||||
thiserror.workspace = true
|
||||
tiktoken-rs.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
base64.workspace = true
|
||||
image.workspace = true
|
||||
|
||||
|
||||
[dev-dependencies]
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
ctor.workspace = true
|
||||
editor = { workspace = true, features = ["test-support"] }
|
||||
env_logger.workspace = true
|
||||
language = { workspace = true, features = ["test-support"] }
|
||||
log.workspace = true
|
||||
project = { workspace = true, features = ["test-support"] }
|
||||
proto = { workspace = true, features = ["test-support"] }
|
||||
rand.workspace = true
|
||||
text = { workspace = true, features = ["test-support"] }
|
||||
unindent.workspace = true
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
pub mod logging;
|
||||
mod model;
|
||||
pub mod provider;
|
||||
mod rate_limiter;
|
||||
mod registry;
|
||||
mod request;
|
||||
mod role;
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub mod fake_provider;
|
||||
pub mod settings;
|
||||
|
||||
use anyhow::Result;
|
||||
use client::{Client, UserStore};
|
||||
use futures::FutureExt;
|
||||
use futures::{future::BoxFuture, stream::BoxStream, StreamExt, TryStreamExt as _};
|
||||
use gpui::{AnyElement, AnyView, AppContext, AsyncAppContext, SharedString, Task, WindowContext};
|
||||
use gpui::{
|
||||
AnyElement, AnyView, AppContext, AsyncAppContext, Model, SharedString, Task, WindowContext,
|
||||
};
|
||||
pub use model::*;
|
||||
use project::Fs;
|
||||
use proto::Plan;
|
||||
pub use rate_limiter::*;
|
||||
pub(crate) use rate_limiter::*;
|
||||
pub use registry::*;
|
||||
pub use request::*;
|
||||
pub use role::*;
|
||||
@@ -23,10 +27,14 @@ use std::fmt;
|
||||
use std::{future::Future, sync::Arc};
|
||||
use ui::IconName;
|
||||
|
||||
pub const ZED_CLOUD_PROVIDER_ID: &str = "zed.dev";
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
registry::init(cx);
|
||||
pub fn init(
|
||||
user_store: Model<UserStore>,
|
||||
client: Arc<Client>,
|
||||
fs: Arc<dyn Fs>,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
settings::init(fs, cx);
|
||||
registry::init(user_store, client, cx);
|
||||
}
|
||||
|
||||
/// The availability of a [`LanguageModel`].
|
||||
@@ -176,7 +184,7 @@ pub trait LanguageModel: Send + Sync {
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
fn as_fake(&self) -> &fake_provider::FakeLanguageModel {
|
||||
fn as_fake(&self) -> &provider::fake::FakeLanguageModel {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ use gpui::BackgroundExecutor;
|
||||
use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest};
|
||||
use std::env;
|
||||
use std::sync::Arc;
|
||||
use telemetry::{AssistantEvent, AssistantKind, AssistantPhase};
|
||||
use telemetry_events::{AssistantEvent, AssistantKind, AssistantPhase};
|
||||
use util::ResultExt;
|
||||
|
||||
use crate::provider::anthropic::PROVIDER_ID as ANTHROPIC_PROVIDER_ID;
|
||||
@@ -1,6 +1,8 @@
|
||||
pub mod anthropic;
|
||||
pub mod cloud;
|
||||
pub mod copilot_chat;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub mod fake;
|
||||
pub mod google;
|
||||
pub mod ollama;
|
||||
pub mod open_ai;
|
||||
@@ -1,4 +1,9 @@
|
||||
use crate::AllLanguageModelSettings;
|
||||
use crate::{
|
||||
settings::AllLanguageModelSettings, LanguageModel, LanguageModelCacheConfiguration,
|
||||
LanguageModelId, LanguageModelName, LanguageModelProvider, LanguageModelProviderId,
|
||||
LanguageModelProviderName, LanguageModelProviderState, LanguageModelRequest, RateLimiter, Role,
|
||||
};
|
||||
use crate::{LanguageModelCompletionEvent, LanguageModelToolUse, StopReason};
|
||||
use anthropic::{AnthropicError, ContentDelta, Event, ResponseContent};
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use collections::{BTreeMap, HashMap};
|
||||
@@ -10,12 +15,6 @@ use gpui::{
|
||||
View, WhiteSpace,
|
||||
};
|
||||
use http_client::HttpClient;
|
||||
use language_model::{
|
||||
LanguageModel, LanguageModelCacheConfiguration, LanguageModelId, LanguageModelName,
|
||||
LanguageModelProvider, LanguageModelProviderId, LanguageModelProviderName,
|
||||
LanguageModelProviderState, LanguageModelRequest, RateLimiter, Role,
|
||||
};
|
||||
use language_model::{LanguageModelCompletionEvent, LanguageModelToolUse, StopReason};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsStore};
|
||||
@@ -257,7 +256,7 @@ pub fn count_anthropic_tokens(
|
||||
let mut string_messages = Vec::with_capacity(messages.len());
|
||||
|
||||
for message in messages {
|
||||
use language_model::MessageContent;
|
||||
use crate::MessageContent;
|
||||
|
||||
let mut string_contents = String::new();
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
use super::open_ai::count_open_ai_tokens;
|
||||
use crate::provider::anthropic::map_to_language_model_completion_events;
|
||||
use crate::{
|
||||
settings::AllLanguageModelSettings, CloudModel, LanguageModel, LanguageModelCacheConfiguration,
|
||||
LanguageModelId, LanguageModelName, LanguageModelProviderId, LanguageModelProviderName,
|
||||
LanguageModelProviderState, LanguageModelRequest, RateLimiter,
|
||||
};
|
||||
use anthropic::AnthropicError;
|
||||
use anyhow::{anyhow, Result};
|
||||
use client::{
|
||||
@@ -16,14 +22,6 @@ use gpui::{
|
||||
ModelContext, ReadGlobal, Subscription, Task,
|
||||
};
|
||||
use http_client::{AsyncBody, HttpClient, Method, Response, StatusCode};
|
||||
use language_model::{
|
||||
CloudModel, LanguageModel, LanguageModelCacheConfiguration, LanguageModelId, LanguageModelName,
|
||||
LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
|
||||
LanguageModelRequest, RateLimiter, ZED_CLOUD_PROVIDER_ID,
|
||||
};
|
||||
use language_model::{
|
||||
LanguageModelAvailability, LanguageModelCompletionEvent, LanguageModelProvider,
|
||||
};
|
||||
use proto::TypedEnvelope;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||
@@ -42,11 +40,11 @@ use strum::IntoEnumIterator;
|
||||
use thiserror::Error;
|
||||
use ui::{prelude::*, TintColor};
|
||||
|
||||
use crate::provider::anthropic::map_to_language_model_completion_events;
|
||||
use crate::AllLanguageModelSettings;
|
||||
use crate::{LanguageModelAvailability, LanguageModelCompletionEvent, LanguageModelProvider};
|
||||
|
||||
use super::anthropic::count_anthropic_tokens;
|
||||
|
||||
pub const PROVIDER_ID: &str = "zed.dev";
|
||||
pub const PROVIDER_NAME: &str = "Zed";
|
||||
|
||||
const ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: Option<&str> =
|
||||
@@ -257,7 +255,7 @@ impl LanguageModelProviderState for CloudLanguageModelProvider {
|
||||
|
||||
impl LanguageModelProvider for CloudLanguageModelProvider {
|
||||
fn id(&self) -> LanguageModelProviderId {
|
||||
LanguageModelProviderId(ZED_CLOUD_PROVIDER_ID.into())
|
||||
LanguageModelProviderId(PROVIDER_ID.into())
|
||||
}
|
||||
|
||||
fn name(&self) -> LanguageModelProviderName {
|
||||
@@ -537,7 +535,7 @@ impl LanguageModel for CloudLanguageModel {
|
||||
}
|
||||
|
||||
fn provider_id(&self) -> LanguageModelProviderId {
|
||||
LanguageModelProviderId(ZED_CLOUD_PROVIDER_ID.into())
|
||||
LanguageModelProviderId(PROVIDER_ID.into())
|
||||
}
|
||||
|
||||
fn provider_name(&self) -> LanguageModelProviderName {
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user