Compare commits

..

5 Commits

Author SHA1 Message Date
Mikayla
1cb301a827 Add a free list to the atlas textures for quick re-use of empty slots 2024-11-21 16:18:01 -08:00
Mikayla
859092d722 clippy 2024-11-21 15:36:51 -08:00
Mikayla
c34320c2ad fix compile errors in blade atlas 2024-11-21 11:09:26 -08:00
Piotr Osiewicz
efe795242f Code review refinements
Notably, we've reverted the texture index reuse, as it would impose cost upon callers who do not
remove images from the atlas.

Co-authored-by: Mikayla Maki <mikayla@zed.dev>
2024-11-21 19:14:37 +01:00
143mailliw
04398619f7 gpui: Add support for removing images from sprite atlas 2024-11-01 18:33:06 -07:00
172 changed files with 3039 additions and 4808 deletions

View File

@@ -15,13 +15,6 @@ body:
description: A clear and concise description of what you want to happen.
validations:
required: true
- type: textarea
id: environment
attributes:
label: Environment
description: Run the `copy system specs into clipboard` command palette action and paste the output in the field below.
validations:
required: true
- type: textarea
attributes:
label: |

View File

@@ -2,7 +2,7 @@ name: Bug Report
description: |
Use this template for **non-crash-related** bug reports.
Tip: open this issue template from within Zed with the `file bug report` command palette action.
labels: ["admin read", "triage", "bug"]
labels: ["admin read", "triage", "defect"]
body:
- type: checkboxes
attributes:
@@ -38,12 +38,9 @@ body:
Linux: `~/.local/share/zed/logs/Zed.log` or $XDG_DATA_HOME
If you only need the most recent lines, you can run the `zed: open log` command palette action to see the last 1000.
value: |
<details><summary>Zed.log</summary>
<details><summary>Zed.log</summary><pre>
<!-- Click below this line and paste or drag-and-drop your log-->
```
```
<!-- Click above this line and paste or drag-and-drop your log--></details>
<!-- Click above this line and paste or drag-and-drop your log--></pre></details>
validations:
required: false

View File

@@ -1,7 +1,7 @@
name: Crash Report
description: |
Use this template for crash reports.
labels: ["admin read", "triage", "bug", "panic / crash"]
labels: ["admin read", "triage", "defect", "panic / crash"]
body:
- type: checkboxes
attributes:
@@ -31,12 +31,9 @@ body:
Linux: `~/.local/share/zed/logs/Zed.log` or $XDG_DATA_HOME
If you only need the most recent lines, you can run the `zed: open log` command palette action to see the last 1000.
value: |
<details><summary>Zed.log</summary>
<details><summary>Zed.log</summary><pre>
<!-- Click below this line and paste or drag-and-drop your log-->
```
```
<!-- Click above this line and paste or drag-and-drop your log--></details>
<!-- Click above this line and paste or drag-and-drop your log--></pre></details>
validations:
required: false

View File

@@ -13,7 +13,7 @@ on:
branches:
- "**"
paths-ignore:
- "docs/**/*"
- "docs/**"
- ".github/workflows/community_*"
concurrency:
@@ -260,7 +260,7 @@ jobs:
run: |
mkdir -p target/
# Ignore any errors that occur while drafting release notes to not fail the build.
script/draft-release-notes "$RELEASE_VERSION" "$RELEASE_CHANNEL" > target/release-notes.md || true
script/draft-release-notes "$version" "$channel" > target/release-notes.md || true
- name: Generate license file
run: script/generate-licenses

View File

@@ -24,7 +24,7 @@ jobs:
# issues, preventing 365 days from working until then.
days-before-stale: 180
days-before-close: 7
any-of-issue-labels: "bug,panic / crash"
any-of-issue-labels: "defect,panic / crash"
operations-per-run: 1000
ascending: true
enable-statistics: true

View File

@@ -60,8 +60,6 @@ Max Brunsfeld <maxbrunsfeld@gmail.com>
Max Brunsfeld <maxbrunsfeld@gmail.com> <max@zed.dev>
Max Linke <maxlinke88@gmail.com>
Max Linke <maxlinke88@gmail.com> <kain88-de@users.noreply.github.com>
Michael Sloan <michael@zed.dev>
Michael Sloan <michael@zed.dev> <mgsloan@google.com>
Mikayla Maki <mikayla@zed.dev>
Mikayla Maki <mikayla@zed.dev> <mikayla.c.maki@gmail.com>
Mikayla Maki <mikayla@zed.dev> <mikayla.c.maki@icloud.com>

29
Cargo.lock generated
View File

@@ -2432,6 +2432,7 @@ dependencies = [
"smol",
"sysinfo",
"telemetry_events",
"tempfile",
"text",
"thiserror",
"time",
@@ -3490,6 +3491,7 @@ dependencies = [
"ctor",
"editor",
"env_logger 0.11.5",
"futures 0.3.30",
"gpui",
"language",
"log",
@@ -3716,7 +3718,6 @@ dependencies = [
"tree-sitter-rust",
"tree-sitter-typescript",
"ui",
"unicode-script",
"unicode-segmentation",
"unindent",
"url",
@@ -5796,7 +5797,6 @@ dependencies = [
"gpui",
"project",
"settings",
"theme",
"ui",
"workspace",
]
@@ -6178,11 +6178,12 @@ dependencies = [
[[package]]
name = "jupyter-serde"
version = "0.4.0"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd71aa17c4fa65e6d7536ab2728881a41f8feb2ee5841c2240516c3c3d65d8b3"
checksum = "77b96de099fc23d5c21e05de32cc087c8326983895b7f6c242562af01f7d4c81"
dependencies = [
"anyhow",
"chrono",
"serde",
"serde_json",
"thiserror",
@@ -6402,7 +6403,6 @@ dependencies = [
"pet",
"pet-conda",
"pet-core",
"pet-fs",
"pet-poetry",
"pet-reporter",
"project",
@@ -7177,9 +7177,9 @@ dependencies = [
[[package]]
name = "nbformat"
version = "0.5.0"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9ffb2ca556072f114bcaf2ca01dde7f1bc8a4946097dd804cb5a22d8af7d6df"
checksum = "84f8a9ab08b34237c2c1d0504b794c2ff01c08dfc46a060d160f004a7f479c31"
dependencies = [
"anyhow",
"chrono",
@@ -7794,7 +7794,6 @@ dependencies = [
"project",
"rope",
"serde_json",
"settings",
"smol",
"theme",
"tree-sitter-rust",
@@ -7819,7 +7818,6 @@ dependencies = [
"language",
"log",
"menu",
"outline",
"pretty_assertions",
"project",
"schemars",
@@ -9561,7 +9559,6 @@ dependencies = [
"itertools 0.13.0",
"log",
"parking_lot",
"paths",
"prost",
"release_channel",
"rpc",
@@ -9613,7 +9610,6 @@ dependencies = [
"settings",
"shellexpand 2.1.2",
"smol",
"sysinfo",
"telemetry_events",
"toml 0.8.19",
"util",
@@ -9994,9 +9990,9 @@ dependencies = [
[[package]]
name = "runtimelib"
version = "0.19.0"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe23ba9967355bbb1be2fb9a8e51bd239ffdf9c791fad5a9b765122ee2bde2e4"
checksum = "bc7fe3c17675445fe89de68d130be00b7115104924fbcf53a9b0a84b0283fc81"
dependencies = [
"anyhow",
"async-dispatcher",
@@ -13131,9 +13127,9 @@ checksum = "52ea75f83c0137a9b98608359a5f1af8144876eb67bcb1ce837368e906a9f524"
[[package]]
name = "unicode-script"
version = "0.5.7"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fb421b350c9aff471779e262955939f565ec18b86c15364e6bdf0d662ca7c1f"
checksum = "ad8d71f5726e5f285a935e9fe8edfd53f0491eb6e9a5774097fdabee7cd8c9cd"
[[package]]
name = "unicode-segmentation"
@@ -14777,7 +14773,6 @@ dependencies = [
"settings",
"smallvec",
"sqlez",
"strum 0.25.0",
"task",
"tempfile",
"theme",
@@ -15062,7 +15057,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.161.2"
version = "0.161.0"
dependencies = [
"activity_indicator",
"anyhow",

View File

@@ -372,7 +372,7 @@ linkify = "0.10.0"
log = { version = "0.4.16", features = ["kv_unstable_serde", "serde"] }
markup5ever_rcdom = "0.3.0"
nanoid = "0.4"
nbformat = "0.5.0"
nbformat = "0.3.2"
nix = "0.29"
num-format = "0.4.4"
once_cell = "1.19.0"
@@ -381,7 +381,6 @@ palette = { version = "0.7.5", default-features = false, features = ["std"] }
parking_lot = "0.12.1"
pathdiff = "0.2"
pet = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
pet-fs = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
pet-conda = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
pet-core = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
pet-poetry = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
@@ -406,7 +405,7 @@ reqwest = { git = "https://github.com/zed-industries/reqwest.git", rev = "fd110f
"stream",
] }
rsa = "0.9.6"
runtimelib = { version = "0.19.0", default-features = false, features = [
runtimelib = { version = "0.16.1", default-features = false, features = [
"async-dispatcher-runtime",
] }
rustc-demangle = "0.1.23"
@@ -476,7 +475,6 @@ tree-sitter-yaml = { git = "https://github.com/zed-industries/tree-sitter-yaml",
unicase = "2.6"
unindent = "0.1.7"
unicode-segmentation = "1.10"
unicode-script = "0.5.7"
url = "2.2"
uuid = { version = "1.1.2", features = ["v4", "v5", "serde"] }
wasmparser = "0.215"

View File

@@ -1,5 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7 21V12M7 12H3M7 12H11" stroke="black" stroke-width="2" stroke-linecap="round"/>
<path d="M21 19L16 19L16 14" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7.99987 5.07027L7.49915 4.20467L7.99987 5.07027ZM6.04652 5.25026C5.63245 5.61573 5.59305 6.24766 5.95851 6.66173C6.32398 7.0758 6.95592 7.1152 7.36999 6.74974L6.04652 5.25026ZM11.9999 5C15.8659 5 18.9999 8.13401 18.9999 12H20.9999C20.9999 7.02944 16.9705 3 11.9999 3V5ZM18.9999 12C18.9999 14.2101 17.9768 16.1806 16.3744 17.4651L17.6254 19.0256C19.6809 17.3779 20.9999 14.8426 20.9999 12H18.9999ZM8.5006 5.93588C9.5292 5.34086 10.7232 5 11.9999 5V3C10.3623 3 8.82395 3.4383 7.49915 4.20467L8.5006 5.93588ZM7.36999 6.74974C7.71803 6.44255 8.09667 6.16954 8.5006 5.93588L7.49915 4.20467C6.9797 4.50515 6.49329 4.85593 6.04652 5.25026L7.36999 6.74974Z" fill="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 979 B

View File

@@ -564,11 +564,9 @@
"ctrl-alt-c": "outline_panel::CopyPath",
"alt-ctrl-shift-c": "outline_panel::CopyRelativePath",
"alt-ctrl-r": "outline_panel::RevealInFileManager",
"space": "outline_panel::Open",
"space": ["outline_panel::Open", { "change_selection": false }],
"shift-down": "menu::SelectNext",
"shift-up": "menu::SelectPrev",
"alt-enter": "editor::OpenExcerpts",
"ctrl-k enter": "editor::OpenExcerptsSplit"
"shift-up": "menu::SelectPrev"
}
},
{

View File

@@ -577,11 +577,9 @@
"cmd-alt-c": "outline_panel::CopyPath",
"alt-cmd-shift-c": "outline_panel::CopyRelativePath",
"alt-cmd-r": "outline_panel::RevealInFileManager",
"space": "outline_panel::Open",
"space": ["outline_panel::Open", { "change_selection": false }],
"shift-down": "menu::SelectNext",
"shift-up": "menu::SelectPrev",
"alt-enter": "editor::OpenExcerpts",
"cmd-k enter": "editor::OpenExcerptsSplit"
"shift-up": "menu::SelectPrev"
}
},
{

View File

@@ -25,8 +25,8 @@
"alt-j": ["editor::SelectNext", { "replace_newest": false }],
"alt-shift-j": ["editor::SelectPrevious", { "replace_newest": false }],
"ctrl-/": ["editor::ToggleComments", { "advance_downwards": true }],
"ctrl-w": "editor::SelectLargerSyntaxNode",
"ctrl-shift-w": "editor::SelectSmallerSyntaxNode",
"alt-up": "editor::SelectLargerSyntaxNode",
"alt-down": "editor::SelectSmallerSyntaxNode",
"shift-alt-up": "editor::MoveLineUp",
"shift-alt-down": "editor::MoveLineDown",
"ctrl-alt-l": "editor::Format",

View File

@@ -24,8 +24,8 @@
"ctrl-g": ["editor::SelectNext", { "replace_newest": false }],
"ctrl-cmd-g": ["editor::SelectPrevious", { "replace_newest": false }],
"cmd-/": ["editor::ToggleComments", { "advance_downwards": true }],
"cmd-up": "editor::SelectLargerSyntaxNode",
"cmd-down": "editor::SelectSmallerSyntaxNode",
"alt-up": "editor::SelectLargerSyntaxNode",
"alt-down": "editor::SelectSmallerSyntaxNode",
"shift-alt-up": "editor::MoveLineUp",
"shift-alt-down": "editor::MoveLineDown",
"cmd-alt-l": "editor::Format",
@@ -58,12 +58,6 @@
"alt-enter": "editor::ToggleCodeActions"
}
},
{
"context": "BufferSearchBar > Editor",
"bindings": {
"shift-enter": "search::SelectPrevMatch"
}
},
{
"context": "Workspace",
"bindings": {

View File

@@ -127,9 +127,6 @@
"shift-h": "vim::WindowTop",
"shift-m": "vim::WindowMiddle",
"shift-l": "vim::WindowBottom",
"q": "vim::ToggleRecord",
"shift-q": "vim::ReplayLastRecording",
"@": ["vim::PushOperator", "ReplayRegister"],
// z commands
"z enter": ["workspace::SendKeystrokes", "z t ^"],
"z -": ["workspace::SendKeystrokes", "z b ^"],
@@ -140,14 +137,14 @@
"z .": ["workspace::SendKeystrokes", "z z ^"],
"z b": "editor::ScrollCursorBottom",
"z a": "editor::ToggleFold",
"z shift-a": "editor::ToggleFoldRecursive",
"z A": "editor::ToggleFoldRecursive",
"z c": "editor::Fold",
"z shift-c": "editor::FoldRecursive",
"z C": "editor::FoldRecursive",
"z o": "editor::UnfoldLines",
"z shift-o": "editor::UnfoldRecursive",
"z O": "editor::UnfoldRecursive",
"z f": "editor::FoldSelectedRanges",
"z shift-m": "editor::FoldAll",
"z shift-r": "editor::UnfoldAll",
"z M": "editor::FoldAll",
"z R": "editor::UnfoldAll",
"shift-z shift-q": ["pane::CloseActiveItem", { "saveIntent": "skip" }],
"shift-z shift-z": ["pane::CloseActiveItem", { "saveIntent": "saveAll" }],
// Count support
@@ -209,6 +206,9 @@
"\"": ["vim::PushOperator", "Register"],
"g q": ["vim::PushOperator", "Rewrap"],
"g w": ["vim::PushOperator", "Rewrap"],
"q": "vim::ToggleRecord",
"shift-q": "vim::ReplayLastRecording",
"@": ["vim::PushOperator", "ReplayRegister"],
"ctrl-pagedown": "pane::ActivateNextItem",
"ctrl-pageup": "pane::ActivatePrevItem",
"insert": "vim::InsertBefore",
@@ -234,7 +234,7 @@
"bindings": {
":": "vim::VisualCommand",
"u": "vim::ConvertToLowerCase",
"shift-u": "vim::ConvertToUpperCase",
"U": "vim::ConvertToUpperCase",
"o": "vim::OtherEnd",
"shift-o": "vim::OtherEnd",
"d": "vim::VisualDelete",
@@ -258,8 +258,8 @@
"g ctrl-x": ["vim::Decrement", { "step": true }],
"shift-i": "vim::InsertBefore",
"shift-a": "vim::InsertAfter",
"g shift-i": "vim::VisualInsertFirstNonWhiteSpace",
"g shift-a": "vim::VisualInsertEndOfLine",
"g I": "vim::VisualInsertFirstNonWhiteSpace",
"g A": "vim::VisualInsertEndOfLine",
"shift-j": "vim::JoinLines",
"r": ["vim::PushOperator", "Replace"],
"ctrl-c": ["vim::SwitchMode", "Normal"],
@@ -364,14 +364,12 @@
"b": "vim::Parentheses",
"[": "vim::SquareBrackets",
"]": "vim::SquareBrackets",
"r": "vim::SquareBrackets",
"{": "vim::CurlyBrackets",
"}": "vim::CurlyBrackets",
"shift-b": "vim::CurlyBrackets",
"<": "vim::AngleBrackets",
">": "vim::AngleBrackets",
"a": "vim::AngleBrackets",
"g": "vim::Argument"
"a": "vim::Argument"
}
},
{

View File

@@ -68,17 +68,9 @@
"ui_font_size": 16,
// How much to fade out unused code.
"unnecessary_code_fade": 0.3,
// Active pane styling settings.
"active_pane_modifiers": {
// The factor to grow the active pane by. Defaults to 1.0
// which gives the same size as all other panes.
"magnification": 1.0,
// Inset border size of the active pane, in pixels.
"border_size": 0.0,
// Opacity of the inactive panes. 0 means transparent, 1 means opaque.
// Values are clamped to the [0.0, 1.0] range.
"inactive_opacity": 1.0
},
// The factor to grow the active pane by. Defaults to 1.0
// which gives the same size as all other panes.
"active_pane_magnification": 1.0,
// The direction that you want to split panes horizontally. Defaults to "up"
"pane_split_direction_horizontal": "up",
// The direction that you want to split panes horizontally. Defaults to "left"
@@ -160,7 +152,7 @@
"show_signature_help_after_edits": true,
// Whether to show wrap guides (vertical rulers) in the editor.
// Setting this to true will show a guide at the 'preferred_line_length' value
// if 'soft_wrap' is set to 'preferred_line_length', and will show any
// if softwrap is set to 'preferred_line_length', and will show any
// additional guides as specified by the 'wrap_guides' setting.
"show_wrap_guides": true,
// Character counts at which to show wrap guides in the editor.
@@ -182,8 +174,6 @@
// bracket, brace, single or double quote characters.
// For example, when you select text and type (, Zed will surround the text with ().
"use_auto_surround": true,
// Whether indentation of pasted content should be adjusted based on the context.
"auto_indent_on_paste": true,
// Controls how the editor handles the autoclosed characters.
// When set to `false`(default), skipping over and auto-removing of the closing characters
// happen only for auto-inserted characters.
@@ -485,7 +475,7 @@
"default_width": 640,
// Default height when the assistant is docked to the bottom.
"default_height": 320,
// The default model to use when creating new chats.
// The default model to use when creating new contexts.
"default_model": {
// The provider to use.
"provider": "zed.dev",
@@ -830,6 +820,7 @@
"tasks": {
"variables": {}
},
"toolchain": { "name": "default", "path": "default" },
// An object whose keys are language names, and whose values
// are arrays of filenames or extensions of files that should
// use those languages.

View File

@@ -41,12 +41,10 @@ use prompts::PromptLoadingParams;
use semantic_index::{CloudEmbeddingProvider, SemanticDb};
use serde::{Deserialize, Serialize};
use settings::{update_settings_file, Settings, SettingsStore};
use slash_command::search_command::SearchSlashCommandFeatureFlag;
use slash_command::{
auto_command, cargo_workspace_command, context_server_command, default_command, delta_command,
diagnostics_command, docs_command, fetch_command, file_command, now_command, project_command,
prompt_command, search_command, selection_command, symbols_command, tab_command,
terminal_command,
prompt_command, search_command, symbols_command, tab_command, terminal_command,
};
use std::path::PathBuf;
use std::sync::Arc;
@@ -213,23 +211,21 @@ pub fn init(
});
}
if cx.has_flag::<SearchSlashCommandFeatureFlag>() {
cx.spawn(|mut cx| {
let client = client.clone();
async move {
let embedding_provider = CloudEmbeddingProvider::new(client.clone());
let semantic_index = SemanticDb::new(
paths::embeddings_dir().join("semantic-index-db.0.mdb"),
Arc::new(embedding_provider),
&mut cx,
)
.await?;
cx.spawn(|mut cx| {
let client = client.clone();
async move {
let embedding_provider = CloudEmbeddingProvider::new(client.clone());
let semantic_index = SemanticDb::new(
paths::embeddings_dir().join("semantic-index-db.0.mdb"),
Arc::new(embedding_provider),
&mut cx,
)
.await?;
cx.update(|cx| cx.set_global(semantic_index))
}
})
.detach();
}
cx.update(|cx| cx.set_global(semantic_index))
}
})
.detach();
context_store::init(&client.clone().into());
prompt_library::init(cx);
@@ -440,7 +436,6 @@ fn register_slash_commands(prompt_builder: Option<Arc<PromptBuilder>>, cx: &mut
slash_command_registry
.register_command(cargo_workspace_command::CargoWorkspaceSlashCommand, true);
slash_command_registry.register_command(prompt_command::PromptSlashCommand, true);
slash_command_registry.register_command(selection_command::SelectionCommand, true);
slash_command_registry.register_command(default_command::DefaultSlashCommand, false);
slash_command_registry.register_command(terminal_command::TerminalSlashCommand, true);
slash_command_registry.register_command(now_command::NowSlashCommand, false);

View File

@@ -64,7 +64,6 @@ use serde::{Deserialize, Serialize};
use settings::{update_settings_file, Settings};
use smol::stream::StreamExt;
use std::{
any::TypeId,
borrow::Cow,
cmp,
ops::{ControlFlow, Range},
@@ -443,24 +442,27 @@ impl AssistantPanel {
);
let _pane = cx.view().clone();
let right_children = h_flex()
.gap(Spacing::XSmall.rems(cx))
.gap(Spacing::Small.rems(cx))
.child(
IconButton::new("new-chat", IconName::Plus)
IconButton::new("new-context", IconName::Plus)
.on_click(
cx.listener(|_, _, cx| {
cx.dispatch_action(NewContext.boxed_clone())
}),
)
.tooltip(move |cx| {
Tooltip::for_action_in("New Chat", &NewContext, &focus_handle, cx)
Tooltip::for_action_in(
"New Context",
&NewContext,
&focus_handle,
cx,
)
}),
)
.child(
PopoverMenu::new("assistant-panel-popover-menu")
.trigger(
IconButton::new("menu", IconName::EllipsisVertical)
.icon_size(IconSize::Small)
.tooltip(|cx| Tooltip::text("Toggle Assistant Menu", cx)),
IconButton::new("menu", IconName::Menu).icon_size(IconSize::Small),
)
.menu(move |cx| {
let zoom_label = if _pane.read(cx).is_zoomed() {
@@ -471,7 +473,7 @@ impl AssistantPanel {
let focus_handle = _pane.focus_handle(cx);
Some(ContextMenu::build(cx, move |menu, _| {
menu.context(focus_handle.clone())
.action("New Chat", Box::new(NewContext))
.action("New Context", Box::new(NewContext))
.action("History", Box::new(DeployHistory))
.action("Prompt Library", Box::new(DeployPromptLibrary))
.action("Configure", Box::new(ShowConfiguration))
@@ -1080,21 +1082,7 @@ impl AssistantPanel {
self.show_updated_summary(&context_editor, cx);
cx.notify()
}
EditorEvent::Edited { .. } => {
self.workspace
.update(cx, |workspace, cx| {
let is_via_ssh = workspace
.project()
.update(cx, |project, _| project.is_via_ssh());
workspace
.client()
.telemetry()
.log_edit_event("assistant panel", is_via_ssh);
})
.log_err();
cx.emit(AssistantPanelEvent::ContextEdited)
}
EditorEvent::Edited { .. } => cx.emit(AssistantPanelEvent::ContextEdited),
_ => {}
}
}
@@ -1509,7 +1497,7 @@ pub struct ContextEditor {
dragged_file_worktrees: Vec<Model<Worktree>>,
}
const DEFAULT_TAB_TITLE: &str = "New Chat";
const DEFAULT_TAB_TITLE: &str = "New Context";
const MAX_TAB_TITLE_LEN: usize = 16;
impl ContextEditor {
@@ -2610,108 +2598,57 @@ impl ContextEditor {
let context = self.context.clone();
move |cx| {
let message_id = MessageId(message.timestamp);
let llm_loading = message.role == Role::Assistant
let show_spinner = message.role == Role::Assistant
&& message.status == MessageStatus::Pending;
let (label, spinner, note) = match message.role {
Role::User => (
Label::new("You").color(Color::Default).into_any_element(),
None,
None,
),
let label = match message.role {
Role::User => {
Label::new("You").color(Color::Default).into_any_element()
}
Role::Assistant => {
let base_label = Label::new("Assistant").color(Color::Info);
let mut spinner = None;
let mut note = None;
let animated_label = if llm_loading {
base_label
let label = Label::new("Assistant").color(Color::Info);
if show_spinner {
label
.with_animation(
"pulsating-label",
Animation::new(Duration::from_secs(2))
.repeat()
.with_easing(pulsating_between(0.3, 0.9)),
.with_easing(pulsating_between(0.4, 0.8)),
|label, delta| label.alpha(delta),
)
.into_any_element()
} else {
base_label.into_any_element()
};
if llm_loading {
spinner = Some(
Icon::new(IconName::ArrowCircle)
.size(IconSize::XSmall)
.color(Color::Muted)
.with_animation(
"arrow-circle",
Animation::new(Duration::from_secs(2)).repeat(),
|icon, delta| {
icon.transform(Transformation::rotate(
percentage(delta),
))
},
)
.into_any_element(),
);
note = Some(
div()
.font(
theme::ThemeSettings::get_global(cx)
.buffer_font
.clone(),
)
.child(
Label::new("Press 'esc' to cancel")
.color(Color::Muted)
.size(LabelSize::XSmall),
)
.into_any_element(),
);
label.into_any_element()
}
(animated_label, spinner, note)
}
Role::System => (
Label::new("System")
.color(Color::Warning)
.into_any_element(),
None,
None,
),
Role::System => Label::new("System")
.color(Color::Warning)
.into_any_element(),
};
let sender = h_flex()
.items_center()
.gap_2()
.child(
ButtonLike::new("role")
.style(ButtonStyle::Filled)
.child(
h_flex()
.items_center()
.gap_1p5()
.child(label)
.children(spinner),
)
.tooltip(|cx| {
Tooltip::with_meta(
"Toggle message role",
None,
"Available roles: You (User), Assistant, System",
let sender = ButtonLike::new("role")
.style(ButtonStyle::Filled)
.child(label)
.tooltip(|cx| {
Tooltip::with_meta(
"Toggle message role",
None,
"Available roles: You (User), Assistant, System",
cx,
)
})
.on_click({
let context = context.clone();
move |_, cx| {
context.update(cx, |context, cx| {
context.cycle_message_roles(
HashSet::from_iter(Some(message_id)),
cx,
)
})
.on_click({
let context = context.clone();
move |_, cx| {
context.update(cx, |context, cx| {
context.cycle_message_roles(
HashSet::from_iter(Some(message_id)),
cx,
)
})
}
}),
)
.children(note);
}
});
h_flex()
.id(("message_header", message_id.as_u64()))
@@ -3036,11 +2973,97 @@ impl ContextEditor {
let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
return;
};
let Some(creases) = selections_creases(workspace, cx) else {
let Some(editor) = workspace
.active_item(cx)
.and_then(|item| item.act_as::<Editor>(cx))
else {
return;
};
let mut creases = vec![];
editor.update(cx, |editor, cx| {
let selections = editor.selections.all_adjusted(cx);
let buffer = editor.buffer().read(cx).snapshot(cx);
for selection in selections {
let range = editor::ToOffset::to_offset(&selection.start, &buffer)
..editor::ToOffset::to_offset(&selection.end, &buffer);
let selected_text = buffer.text_for_range(range.clone()).collect::<String>();
if selected_text.is_empty() {
continue;
}
let start_language = buffer.language_at(range.start);
let end_language = buffer.language_at(range.end);
let language_name = if start_language == end_language {
start_language.map(|language| language.code_fence_block_name())
} else {
None
};
let language_name = language_name.as_deref().unwrap_or("");
let filename = buffer
.file_at(selection.start)
.map(|file| file.full_path(cx));
let text = if language_name == "markdown" {
selected_text
.lines()
.map(|line| format!("> {}", line))
.collect::<Vec<_>>()
.join("\n")
} else {
let start_symbols = buffer
.symbols_containing(selection.start, None)
.map(|(_, symbols)| symbols);
let end_symbols = buffer
.symbols_containing(selection.end, None)
.map(|(_, symbols)| symbols);
let outline_text = if let Some((start_symbols, end_symbols)) =
start_symbols.zip(end_symbols)
{
Some(
start_symbols
.into_iter()
.zip(end_symbols)
.take_while(|(a, b)| a == b)
.map(|(a, _)| a.text)
.collect::<Vec<_>>()
.join(" > "),
)
} else {
None
};
let line_comment_prefix = start_language
.and_then(|l| l.default_scope().line_comment_prefixes().first().cloned());
let fence = codeblock_fence_for_path(
filename.as_deref(),
Some(selection.start.row..=selection.end.row),
);
if let Some((line_comment_prefix, outline_text)) =
line_comment_prefix.zip(outline_text)
{
let breadcrumb =
format!("{line_comment_prefix}Excerpt from: {outline_text}\n");
format!("{fence}{breadcrumb}{selected_text}\n```")
} else {
format!("{fence}{selected_text}\n```")
}
};
let crease_title = if let Some(path) = filename {
let start_line = selection.start.row + 1;
let end_line = selection.end.row + 1;
if start_line == end_line {
format!("{}, Line {}", path.display(), start_line)
} else {
format!("{}, Lines {} to {}", path.display(), start_line, end_line)
}
} else {
"Quoted selection".to_string()
};
creases.push((text, crease_title));
}
});
if creases.is_empty() {
return;
}
@@ -3931,99 +3954,6 @@ fn find_surrounding_code_block(snapshot: &BufferSnapshot, offset: usize) -> Opti
None
}
pub fn selections_creases(
workspace: &mut workspace::Workspace,
cx: &mut ViewContext<Workspace>,
) -> Option<Vec<(String, String)>> {
let editor = workspace
.active_item(cx)
.and_then(|item| item.act_as::<Editor>(cx))?;
let mut creases = vec![];
editor.update(cx, |editor, cx| {
let selections = editor.selections.all_adjusted(cx);
let buffer = editor.buffer().read(cx).snapshot(cx);
for selection in selections {
let range = editor::ToOffset::to_offset(&selection.start, &buffer)
..editor::ToOffset::to_offset(&selection.end, &buffer);
let selected_text = buffer.text_for_range(range.clone()).collect::<String>();
if selected_text.is_empty() {
continue;
}
let start_language = buffer.language_at(range.start);
let end_language = buffer.language_at(range.end);
let language_name = if start_language == end_language {
start_language.map(|language| language.code_fence_block_name())
} else {
None
};
let language_name = language_name.as_deref().unwrap_or("");
let filename = buffer
.file_at(selection.start)
.map(|file| file.full_path(cx));
let text = if language_name == "markdown" {
selected_text
.lines()
.map(|line| format!("> {}", line))
.collect::<Vec<_>>()
.join("\n")
} else {
let start_symbols = buffer
.symbols_containing(selection.start, None)
.map(|(_, symbols)| symbols);
let end_symbols = buffer
.symbols_containing(selection.end, None)
.map(|(_, symbols)| symbols);
let outline_text =
if let Some((start_symbols, end_symbols)) = start_symbols.zip(end_symbols) {
Some(
start_symbols
.into_iter()
.zip(end_symbols)
.take_while(|(a, b)| a == b)
.map(|(a, _)| a.text)
.collect::<Vec<_>>()
.join(" > "),
)
} else {
None
};
let line_comment_prefix = start_language
.and_then(|l| l.default_scope().line_comment_prefixes().first().cloned());
let fence = codeblock_fence_for_path(
filename.as_deref(),
Some(selection.start.row..=selection.end.row),
);
if let Some((line_comment_prefix, outline_text)) =
line_comment_prefix.zip(outline_text)
{
let breadcrumb = format!("{line_comment_prefix}Excerpt from: {outline_text}\n");
format!("{fence}{breadcrumb}{selected_text}\n```")
} else {
format!("{fence}{selected_text}\n```")
}
};
let crease_title = if let Some(path) = filename {
let start_line = selection.start.row + 1;
let end_line = selection.end.row + 1;
if start_line == end_line {
format!("{}, Line {}", path.display(), start_line)
} else {
format!("{}, Lines {} to {}", path.display(), start_line, end_line)
}
} else {
"Quoted selection".to_string()
};
creases.push((text, crease_title));
}
});
Some(creases)
}
fn render_fold_icon_button(
editor: WeakView<Editor>,
icon: IconName,
@@ -4197,21 +4127,6 @@ impl Item for ContextEditor {
fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
self.editor.update(cx, Item::deactivated)
}
fn act_as_type<'a>(
&'a self,
type_id: TypeId,
self_handle: &'a View<Self>,
_: &'a AppContext,
) -> Option<AnyView> {
if type_id == TypeId::of::<Self>() {
Some(self_handle.to_any())
} else if type_id == TypeId::of::<Editor>() {
Some(self.editor.to_any())
} else {
None
}
}
}
impl SearchableItem for ContextEditor {
@@ -4401,11 +4316,26 @@ impl FollowableItem for ContextEditor {
pub struct ContextEditorToolbarItem {
fs: Arc<dyn Fs>,
workspace: WeakView<Workspace>,
active_context_editor: Option<WeakView<ContextEditor>>,
model_summary_editor: View<Editor>,
model_selector_menu_handle: PopoverMenuHandle<Picker<ModelPickerDelegate>>,
}
fn active_editor_focus_handle(
workspace: &WeakView<Workspace>,
cx: &WindowContext<'_>,
) -> Option<FocusHandle> {
workspace.upgrade().and_then(|workspace| {
Some(
workspace
.read(cx)
.active_item_as::<Editor>(cx)?
.focus_handle(cx),
)
})
}
fn render_inject_context_menu(
active_context_editor: WeakView<ContextEditor>,
cx: &mut WindowContext<'_>,
@@ -4432,6 +4362,7 @@ impl ContextEditorToolbarItem {
) -> Self {
Self {
fs: workspace.app_state().fs.clone(),
workspace: workspace.weak_handle(),
active_context_editor: None,
model_summary_editor,
model_selector_menu_handle,
@@ -4484,30 +4415,16 @@ impl ContextEditorToolbarItem {
impl Render for ContextEditorToolbarItem {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let left_side = h_flex()
.group("chat-title-group")
.pl_0p5()
.gap_1()
.items_center()
.flex_grow()
.child(
div()
.w_full()
.when(self.active_context_editor.is_some(), |left_side| {
left_side.child(self.model_summary_editor.clone())
}),
)
.child(
div().visible_on_hover("chat-title-group").child(
IconButton::new("regenerate-context", IconName::RefreshTitle)
.shape(ui::IconButtonShape::Square)
.tooltip(|cx| Tooltip::text("Regenerate Title", cx))
.on_click(cx.listener(move |_, _, cx| {
cx.emit(ContextEditorToolbarItemEvent::RegenerateSummary)
})),
),
);
.pl_1()
.gap_2()
.flex_1()
.min_w(rems(DEFAULT_TAB_TITLE.len() as f32))
.when(self.active_context_editor.is_some(), |left_side| {
left_side.child(self.model_summary_editor.clone())
});
let active_provider = LanguageModelRegistry::read_global(cx).active_provider();
let active_model = LanguageModelRegistry::read_global(cx).active_model();
let weak_self = cx.view().downgrade();
let right_side = h_flex()
.gap_2()
// TODO display this in a nicer way, once we have a design for it.
@@ -4520,6 +4437,7 @@ impl Render for ContextEditorToolbarItem {
// let scan_items_remaining = cx.update_global(|db: &mut SemanticDb, cx| {
// project.and_then(|project| db.remaining_summaries(&project, cx))
// });
// scan_items_remaining
// .map(|remaining_items| format!("Files to scan: {}", remaining_items))
// })
@@ -4541,13 +4459,9 @@ impl Render for ContextEditorToolbarItem {
(Some(provider), Some(model)) => h_flex()
.gap_1()
.child(
Icon::new(
model
.icon()
.unwrap_or_else(|| provider.icon()),
)
.color(Color::Muted)
.size(IconSize::XSmall),
Icon::new(model.icon().unwrap_or_else(|| provider.icon()))
.color(Color::Muted)
.size(IconSize::XSmall),
)
.child(
Label::new(model.name().0)
@@ -4573,7 +4487,71 @@ impl Render for ContextEditorToolbarItem {
)
.with_handle(self.model_selector_menu_handle.clone()),
)
.children(self.render_remaining_tokens(cx));
.children(self.render_remaining_tokens(cx))
.child(
PopoverMenu::new("context-editor-popover")
.trigger(
IconButton::new("context-editor-trigger", IconName::EllipsisVertical)
.icon_size(IconSize::Small)
.tooltip(|cx| Tooltip::text("Open Context Options", cx)),
)
.menu({
let weak_self = weak_self.clone();
move |cx| {
let weak_self = weak_self.clone();
Some(ContextMenu::build(cx, move |menu, cx| {
let context = weak_self
.update(cx, |this, cx| {
active_editor_focus_handle(&this.workspace, cx)
})
.ok()
.flatten();
menu.when_some(context, |menu, context| menu.context(context))
.entry("Regenerate Context Title", None, {
let weak_self = weak_self.clone();
move |cx| {
weak_self
.update(cx, |_, cx| {
cx.emit(ContextEditorToolbarItemEvent::RegenerateSummary)
})
.ok();
}
})
.custom_entry(
|_| {
h_flex()
.w_full()
.justify_between()
.gap_2()
.child(Label::new("Add Context"))
.child(Label::new("/ command").color(Color::Muted))
.into_any()
},
{
let weak_self = weak_self.clone();
move |cx| {
weak_self
.update(cx, |this, cx| {
if let Some(editor) =
&this.active_context_editor
{
editor
.update(cx, |this, cx| {
this.slash_menu_handle
.toggle(cx);
})
.ok();
}
})
.ok();
}
},
)
.action("Add Selection", QuoteSelection.boxed_clone())
}))
}
}),
);
h_flex()
.size_full()
@@ -4788,7 +4766,7 @@ impl ConfigurationView {
h_flex().justify_end().child(
Button::new(
SharedString::from(format!("new-context-{provider_id}")),
"Open New Chat",
"Open new context",
)
.icon_position(IconPosition::Start)
.icon(IconName::Plus)

View File

@@ -410,7 +410,7 @@ pub struct AssistantSettingsContentV2 {
///
/// Default: 320
default_height: Option<f32>,
/// The default model to use when creating new chats.
/// The default model to use when creating new contexts.
default_model: Option<LanguageModelSelection>,
/// Additional models with which to generate alternatives when performing inline assists.
inline_alternatives: Option<Vec<LanguageModelSelection>>,
@@ -498,11 +498,11 @@ pub struct LegacyAssistantSettingsContent {
///
/// Default: 320
pub default_height: Option<f32>,
/// The default OpenAI model to use when creating new chats.
/// The default OpenAI model to use when creating new contexts.
///
/// Default: gpt-4-1106-preview
pub default_open_ai_model: Option<OpenAiModel>,
/// OpenAI API base URL to use when creating new chats.
/// OpenAI API base URL to use when creating new contexts.
///
/// Default: https://api.openai.com/v1
pub openai_api_url: Option<String>,

View File

@@ -1052,9 +1052,7 @@ impl Context {
}
pub(crate) fn count_remaining_tokens(&mut self, cx: &mut ModelContext<Self>) {
// Assume it will be a Chat request, even though that takes fewer tokens (and risks going over the limit),
// because otherwise you see in the UI that your empty message has a bunch of tokens already used.
let request = self.to_completion_request(RequestType::Chat, cx);
let request = self.to_completion_request(RequestType::SuggestEdits, cx); // Conservatively assume SuggestEdits, since it takes more tokens.
let Some(model) = LanguageModelRegistry::read_global(cx).active_model() else {
return;
};
@@ -2204,7 +2202,7 @@ impl Context {
}
if let RequestType::SuggestEdits = request_type {
if let Ok(preamble) = self.prompt_builder.generate_suggest_edits_prompt() {
if let Ok(preamble) = self.prompt_builder.generate_workflow_prompt() {
let last_elem_index = completion_request.messages.len();
completion_request

View File

@@ -84,7 +84,7 @@ pub struct InlineAssistant {
confirmed_assists: HashMap<InlineAssistId, Model<CodegenAlternative>>,
prompt_history: VecDeque<String>,
prompt_builder: Arc<PromptBuilder>,
telemetry: Arc<Telemetry>,
telemetry: Option<Arc<Telemetry>>,
fs: Arc<dyn Fs>,
}
@@ -105,7 +105,7 @@ impl InlineAssistant {
confirmed_assists: HashMap::default(),
prompt_history: VecDeque::default(),
prompt_builder,
telemetry,
telemetry: Some(telemetry),
fs,
}
}
@@ -241,17 +241,19 @@ impl InlineAssistant {
codegen_ranges.push(start..end);
if let Some(model) = LanguageModelRegistry::read_global(cx).active_model() {
self.telemetry.report_assistant_event(AssistantEvent {
conversation_id: None,
kind: AssistantKind::Inline,
phase: AssistantPhase::Invoked,
message_id: None,
model: model.telemetry_id(),
model_provider: model.provider_id().to_string(),
response_latency: None,
error_message: None,
language_name: buffer.language().map(|language| language.name().to_proto()),
});
if let Some(telemetry) = self.telemetry.as_ref() {
telemetry.report_assistant_event(AssistantEvent {
conversation_id: None,
kind: AssistantKind::Inline,
phase: AssistantPhase::Invoked,
message_id: None,
model: model.telemetry_id(),
model_provider: model.provider_id().to_string(),
response_latency: None,
error_message: None,
language_name: buffer.language().map(|language| language.name().to_proto()),
});
}
}
}
@@ -814,7 +816,7 @@ impl InlineAssistant {
error_message: None,
language_name: language_name.map(|name| name.to_proto()),
},
Some(self.telemetry.clone()),
self.telemetry.clone(),
cx.http_client(),
model.api_key(cx),
cx.background_executor(),
@@ -1755,20 +1757,6 @@ impl PromptEditor {
) {
match event {
EditorEvent::Edited { .. } => {
if let Some(workspace) = cx.window_handle().downcast::<Workspace>() {
workspace
.update(cx, |workspace, cx| {
let is_via_ssh = workspace
.project()
.update(cx, |project, _| project.is_via_ssh());
workspace
.client()
.telemetry()
.log_edit_event("inline assist", is_via_ssh);
})
.log_err();
}
let prompt = self.editor.read(cx).text(cx);
if self
.prompt_history_ix
@@ -2302,7 +2290,7 @@ pub struct Codegen {
buffer: Model<MultiBuffer>,
range: Range<Anchor>,
initial_transaction_id: Option<TransactionId>,
telemetry: Arc<Telemetry>,
telemetry: Option<Arc<Telemetry>>,
builder: Arc<PromptBuilder>,
is_insertion: bool,
}
@@ -2312,7 +2300,7 @@ impl Codegen {
buffer: Model<MultiBuffer>,
range: Range<Anchor>,
initial_transaction_id: Option<TransactionId>,
telemetry: Arc<Telemetry>,
telemetry: Option<Arc<Telemetry>>,
builder: Arc<PromptBuilder>,
cx: &mut ModelContext<Self>,
) -> Self {
@@ -2321,7 +2309,7 @@ impl Codegen {
buffer.clone(),
range.clone(),
false,
Some(telemetry.clone()),
telemetry.clone(),
builder.clone(),
cx,
)
@@ -2412,7 +2400,7 @@ impl Codegen {
self.buffer.clone(),
self.range.clone(),
false,
Some(self.telemetry.clone()),
self.telemetry.clone(),
self.builder.clone(),
cx,
)

View File

@@ -310,7 +310,7 @@ impl PromptBuilder {
.render("terminal_assistant_prompt", &context)
}
pub fn generate_suggest_edits_prompt(&self) -> Result<String, RenderError> {
pub fn generate_workflow_prompt(&self) -> Result<String, RenderError> {
self.handlebars.lock().render("suggest_edits", &())
}

View File

@@ -31,7 +31,6 @@ pub mod now_command;
pub mod project_command;
pub mod prompt_command;
pub mod search_command;
pub mod selection_command;
pub mod streaming_example_command;
pub mod symbols_command;
pub mod tab_command;

View File

@@ -21,6 +21,8 @@ use ui::prelude::*;
use util::ResultExt;
use workspace::Workspace;
use crate::slash_command::diagnostics_command::collect_buffer_diagnostics;
pub(crate) struct FileSlashCommand;
impl FileSlashCommand {
@@ -541,6 +543,8 @@ pub fn append_buffer_to_output(
output.text.push('\n');
let section_ix = output.sections.len();
collect_buffer_diagnostics(output, buffer, false);
output.sections.insert(
section_ix,
build_entry_output_section(prev_len..output.text.len(), path, false, None),

View File

@@ -21,10 +21,6 @@ pub(crate) struct SearchSlashCommandFeatureFlag;
impl FeatureFlag for SearchSlashCommandFeatureFlag {
const NAME: &'static str = "search-slash-command";
fn enabled_for_staff() -> bool {
false
}
}
pub(crate) struct SearchSlashCommand;

View File

@@ -1,98 +0,0 @@
use crate::assistant_panel::selections_creases;
use anyhow::{anyhow, Result};
use assistant_slash_command::{
ArgumentCompletion, SlashCommand, SlashCommandContent, SlashCommandEvent,
SlashCommandOutputSection, SlashCommandResult,
};
use futures::StreamExt;
use gpui::{AppContext, Task, WeakView};
use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate};
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use ui::{IconName, SharedString, WindowContext};
use workspace::Workspace;
pub(crate) struct SelectionCommand;
impl SlashCommand for SelectionCommand {
fn name(&self) -> String {
"selection".into()
}
fn label(&self, _cx: &AppContext) -> CodeLabel {
CodeLabel::plain(self.name(), None)
}
fn description(&self) -> String {
"Insert editor selection".into()
}
fn icon(&self) -> IconName {
IconName::Quote
}
fn menu_text(&self) -> String {
self.description()
}
fn requires_argument(&self) -> bool {
false
}
fn accepts_arguments(&self) -> bool {
true
}
fn complete_argument(
self: Arc<Self>,
_arguments: &[String],
_cancel: Arc<AtomicBool>,
_workspace: Option<WeakView<Workspace>>,
_cx: &mut WindowContext,
) -> Task<Result<Vec<ArgumentCompletion>>> {
Task::ready(Err(anyhow!("this command does not require argument")))
}
fn run(
self: Arc<Self>,
_arguments: &[String],
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
_context_buffer: BufferSnapshot,
workspace: WeakView<Workspace>,
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
cx: &mut WindowContext,
) -> Task<SlashCommandResult> {
let mut events = vec![];
let Some(creases) = workspace
.update(cx, selections_creases)
.unwrap_or_else(|e| {
events.push(Err(e));
None
})
else {
return Task::ready(Err(anyhow!("no active selection")));
};
for (text, title) in creases {
events.push(Ok(SlashCommandEvent::StartSection {
icon: IconName::TextSnippet,
label: SharedString::from(title),
metadata: None,
}));
events.push(Ok(SlashCommandEvent::Content(SlashCommandContent::Text {
text,
run_commands_in_text: false,
})));
events.push(Ok(SlashCommandEvent::EndSection { metadata: None }));
events.push(Ok(SlashCommandEvent::Content(SlashCommandContent::Text {
text: "\n".to_string(),
run_commands_in_text: false,
})));
}
let result = futures::stream::iter(events).boxed();
Task::ready(Ok(result))
}
}

View File

@@ -4,9 +4,10 @@ use assistant_slash_command::SlashCommandRegistry;
use gpui::{AnyElement, DismissEvent, SharedString, Task, WeakView};
use picker::{Picker, PickerDelegate, PickerEditorPosition};
use ui::{prelude::*, ListItem, ListItemSpacing, PopoverMenu, PopoverTrigger};
use ui::{prelude::*, KeyBinding, ListItem, ListItemSpacing, PopoverMenu, PopoverTrigger};
use crate::assistant_panel::ContextEditor;
use crate::QuoteSelection;
#[derive(IntoElement)]
pub(super) struct SlashCommandSelector<T: PopoverTrigger> {
@@ -31,6 +32,7 @@ enum SlashCommandEntry {
renderer: fn(&mut WindowContext<'_>) -> AnyElement,
on_confirm: fn(&mut WindowContext<'_>),
},
QuoteButton,
}
impl AsRef<str> for SlashCommandEntry {
@@ -38,6 +40,7 @@ impl AsRef<str> for SlashCommandEntry {
match self {
SlashCommandEntry::Info(SlashCommandInfo { name, .. })
| SlashCommandEntry::Advert { name, .. } => name,
SlashCommandEntry::QuoteButton => "Quote Selection",
}
}
}
@@ -150,6 +153,9 @@ impl PickerDelegate for SlashCommandDelegate {
})
.ok();
}
SlashCommandEntry::QuoteButton => {
cx.dispatch_action(Box::new(QuoteSelection));
}
SlashCommandEntry::Advert { on_confirm, .. } => {
on_confirm(cx);
}
@@ -217,6 +223,40 @@ impl PickerDelegate for SlashCommandDelegate {
),
),
),
SlashCommandEntry::QuoteButton => {
let focus = cx.focus_handle();
let key_binding = KeyBinding::for_action_in(&QuoteSelection, &focus, cx);
Some(
ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Dense)
.selected(selected)
.child(
v_flex()
.child(
h_flex()
.gap_1p5()
.child(Icon::new(IconName::Quote).size(IconSize::XSmall))
.child(
div().font_buffer(cx).child(
Label::new("selection").size(LabelSize::Small),
),
),
)
.child(
h_flex()
.gap_1p5()
.child(
Label::new("Insert editor selection")
.color(Color::Muted)
.size(LabelSize::Small),
)
.children(key_binding.map(|kb| kb.render(cx))),
),
),
)
}
SlashCommandEntry::Advert { renderer, .. } => Some(
ListItem::new(ix)
.inset(true)
@@ -250,44 +290,47 @@ impl<T: PopoverTrigger> RenderOnce for SlashCommandSelector<T> {
icon: command.icon(),
}))
})
.chain([SlashCommandEntry::Advert {
name: "create-your-command".into(),
renderer: |cx| {
v_flex()
.w_full()
.child(
h_flex()
.w_full()
.font_buffer(cx)
.items_center()
.justify_between()
.child(
h_flex()
.items_center()
.gap_1p5()
.child(Icon::new(IconName::Plus).size(IconSize::XSmall))
.child(
div().font_buffer(cx).child(
Label::new("create-your-command")
.size(LabelSize::Small),
.chain([
SlashCommandEntry::Advert {
name: "create-your-command".into(),
renderer: |cx| {
v_flex()
.w_full()
.child(
h_flex()
.w_full()
.font_buffer(cx)
.items_center()
.justify_between()
.child(
h_flex()
.items_center()
.gap_1p5()
.child(Icon::new(IconName::Plus).size(IconSize::XSmall))
.child(
div().font_buffer(cx).child(
Label::new("create-your-command")
.size(LabelSize::Small),
),
),
),
)
.child(
Icon::new(IconName::ArrowUpRight)
.size(IconSize::XSmall)
.color(Color::Muted),
),
)
.child(
Label::new("Create your custom command")
.size(LabelSize::Small)
.color(Color::Muted),
)
.into_any_element()
)
.child(
Icon::new(IconName::ArrowUpRight)
.size(IconSize::XSmall)
.color(Color::Muted),
),
)
.child(
Label::new("Create your custom command")
.size(LabelSize::Small)
.color(Color::Muted),
)
.into_any_element()
},
on_confirm: |cx| cx.open_url("https://zed.dev/docs/extensions/slash-commands"),
},
on_confirm: |cx| cx.open_url("https://zed.dev/docs/extensions/slash-commands"),
}])
SlashCommandEntry::QuoteButton,
])
.collect::<Vec<_>>();
let delegate = SlashCommandDelegate {

View File

@@ -28,36 +28,13 @@ impl Matrix {
self.cols = cols;
}
fn swap_columns(&mut self, col1: usize, col2: usize) {
if col1 == col2 {
return;
}
if col1 >= self.cols {
panic!("column out of bounds");
}
if col2 >= self.cols {
panic!("column out of bounds");
}
unsafe {
let ptr = self.cells.as_mut_ptr();
std::ptr::swap_nonoverlapping(
ptr.add(col1 * self.rows),
ptr.add(col2 * self.rows),
self.rows,
);
}
}
fn get(&self, row: usize, col: usize) -> f64 {
if row >= self.rows {
panic!("row out of bounds")
}
if col >= self.cols {
panic!("column out of bounds")
panic!("col out of bounds")
}
self.cells[col * self.rows + row]
}
@@ -68,7 +45,7 @@ impl Matrix {
}
if col >= self.cols {
panic!("column out of bounds")
panic!("col out of bounds")
}
self.cells[col * self.rows + row] = value;
@@ -129,32 +106,26 @@ impl StreamingDiff {
pub fn push_new(&mut self, text: &str) -> Vec<CharOperation> {
self.new.extend(text.chars());
self.scores.swap_columns(0, self.scores.cols - 1);
self.scores
.resize(self.old.len() + 1, self.new.len() - self.new_text_ix + 1);
self.equal_runs.retain(|(_i, j), _| *j == self.new_text_ix);
self.scores.resize(self.old.len() + 1, self.new.len() + 1);
for j in self.new_text_ix + 1..=self.new.len() {
let relative_j = j - self.new_text_ix;
self.scores
.set(0, relative_j, j as f64 * Self::INSERTION_SCORE);
self.scores.set(0, j, j as f64 * Self::INSERTION_SCORE);
for i in 1..=self.old.len() {
let insertion_score = self.scores.get(i, relative_j - 1) + Self::INSERTION_SCORE;
let deletion_score = self.scores.get(i - 1, relative_j) + Self::DELETION_SCORE;
let insertion_score = self.scores.get(i, j - 1) + Self::INSERTION_SCORE;
let deletion_score = self.scores.get(i - 1, j) + Self::DELETION_SCORE;
let equality_score = if self.old[i - 1] == self.new[j - 1] {
let mut equal_run = self.equal_runs.get(&(i - 1, j - 1)).copied().unwrap_or(0);
equal_run += 1;
self.equal_runs.insert((i, j), equal_run);
let exponent = cmp::min(equal_run as i32 / 4, Self::MAX_EQUALITY_EXPONENT);
self.scores.get(i - 1, relative_j - 1) + Self::EQUALITY_BASE.powi(exponent)
self.scores.get(i - 1, j - 1) + Self::EQUALITY_BASE.powi(exponent)
} else {
f64::NEG_INFINITY
};
let score = insertion_score.max(deletion_score).max(equality_score);
self.scores.set(i, relative_j, score);
self.scores.set(i, j, score);
}
}
@@ -162,7 +133,7 @@ impl StreamingDiff {
let mut next_old_text_ix = self.old_text_ix;
let next_new_text_ix = self.new.len();
for i in self.old_text_ix..=self.old.len() {
let score = self.scores.get(i, next_new_text_ix - self.new_text_ix);
let score = self.scores.get(i, next_new_text_ix);
if score > max_score {
max_score = score;
next_old_text_ix = i;
@@ -203,9 +174,7 @@ impl StreamingDiff {
let (prev_i, prev_j) = [insertion_score, deletion_score, equality_score]
.iter()
.max_by_key(|cell| {
cell.map(|(i, j)| OrderedFloat(self.scores.get(i, j - self.new_text_ix)))
})
.max_by_key(|cell| cell.map(|(i, j)| OrderedFloat(self.scores.get(i, j))))
.unwrap()
.unwrap();

View File

@@ -432,9 +432,6 @@ impl AutoUpdater {
cx.notify();
}
// If you are packaging Zed and need to override the place it downloads SSH remotes from,
// you can override this function. You should also update get_remote_server_release_url to return
// Ok(None).
pub async fn download_remote_server_release(
os: &str,
arch: &str,
@@ -485,7 +482,7 @@ impl AutoUpdater {
release_channel: ReleaseChannel,
version: Option<SemanticVersion>,
cx: &mut AsyncAppContext,
) -> Result<Option<(String, String)>> {
) -> Result<(JsonRelease, String)> {
let this = cx.update(|cx| {
cx.default_global::<GlobalAutoUpdate>()
.0
@@ -507,7 +504,7 @@ impl AutoUpdater {
let update_request_body = build_remote_server_update_request_body(cx)?;
let body = serde_json::to_string(&update_request_body)?;
Ok(Some((release.url, body)))
Ok((release, body))
}
async fn get_release(

View File

@@ -44,6 +44,7 @@ sha2.workspace = true
smol.workspace = true
sysinfo.workspace = true
telemetry_events.workspace = true
tempfile.workspace = true
text.workspace = true
thiserror.workspace = true
time.workspace = true

View File

@@ -13,7 +13,6 @@ use parking_lot::Mutex;
use release_channel::ReleaseChannel;
use settings::{Settings, SettingsStore};
use sha2::{Digest, Sha256};
use std::fs::File;
use std::io::Write;
use std::{env, mem, path::PathBuf, sync::Arc, time::Duration};
use sysinfo::{CpuRefreshKind, Pid, ProcessRefreshKind, RefreshKind, System};
@@ -22,7 +21,10 @@ use telemetry_events::{
EventRequestBody, EventWrapper, ExtensionEvent, InlineCompletionEvent, MemoryEvent, ReplEvent,
SettingEvent,
};
use util::{ResultExt, TryFutureExt};
use tempfile::NamedTempFile;
#[cfg(not(debug_assertions))]
use util::ResultExt;
use util::TryFutureExt;
use worktree::{UpdatedEntriesSet, WorktreeId};
use self::event_coalescer::EventCoalescer;
@@ -44,7 +46,7 @@ struct TelemetryState {
architecture: &'static str,
events_queue: Vec<EventWrapper>,
flush_events_task: Option<Task<()>>,
log_file: Option<File>,
log_file: Option<NamedTempFile>,
is_staff: Option<bool>,
first_event_date_time: Option<DateTime<Utc>>,
event_coalescer: EventCoalescer,
@@ -221,13 +223,15 @@ impl Telemetry {
os_name: os_name(),
app_version: release_channel::AppVersion::global(cx).to_string(),
}));
Self::log_file_path();
#[cfg(not(debug_assertions))]
cx.background_executor()
.spawn({
let state = state.clone();
async move {
if let Some(tempfile) = File::create(Self::log_file_path()).log_err() {
if let Some(tempfile) =
NamedTempFile::new_in(paths::logs_dir().as_path()).log_err()
{
state.lock().log_file = Some(tempfile);
}
}
@@ -276,8 +280,8 @@ impl Telemetry {
Task::ready(())
}
pub fn log_file_path() -> PathBuf {
paths::logs_dir().join("telemetry.log")
pub fn log_file_path(&self) -> Option<PathBuf> {
Some(self.state.lock().log_file.as_ref()?.path().to_path_buf())
}
pub fn start(
@@ -641,6 +645,7 @@ impl Telemetry {
let mut json_bytes = Vec::new();
if let Some(file) = &mut this.state.lock().log_file {
let file = file.as_file_mut();
for event in &mut events {
json_bytes.clear();
serde_json::to_writer(&mut json_bytes, event)?;

View File

@@ -6497,7 +6497,7 @@ async fn test_context_collaboration_with_reconnect(
.await
.unwrap();
// Client A creates a new chats.
// Client A creates a new context.
let context_a = context_store_a.update(cx_a, |store, cx| store.create(cx));
executor.run_until_parked();

View File

@@ -35,30 +35,14 @@ pub enum Model {
Gpt4,
#[serde(alias = "gpt-3.5-turbo", rename = "gpt-3.5-turbo")]
Gpt3_5Turbo,
#[serde(alias = "o1-preview", rename = "o1-preview-2024-09-12")]
O1Preview,
#[serde(alias = "o1-mini", rename = "o1-mini-2024-09-12")]
O1Mini,
#[serde(alias = "claude-3-5-sonnet", rename = "claude-3.5-sonnet")]
Claude3_5Sonnet,
}
impl Model {
pub fn uses_streaming(&self) -> bool {
match self {
Self::Gpt4o | Self::Gpt4 | Self::Gpt3_5Turbo | Self::Claude3_5Sonnet => true,
Self::O1Mini | Self::O1Preview => false,
}
}
pub fn from_id(id: &str) -> Result<Self> {
match id {
"gpt-4o" => Ok(Self::Gpt4o),
"gpt-4" => Ok(Self::Gpt4),
"gpt-3.5-turbo" => Ok(Self::Gpt3_5Turbo),
"o1-preview" => Ok(Self::O1Preview),
"o1-mini" => Ok(Self::O1Mini),
"claude-3-5-sonnet" => Ok(Self::Claude3_5Sonnet),
_ => Err(anyhow!("Invalid model id: {}", id)),
}
}
@@ -68,9 +52,6 @@ impl Model {
Self::Gpt3_5Turbo => "gpt-3.5-turbo",
Self::Gpt4 => "gpt-4",
Self::Gpt4o => "gpt-4o",
Self::O1Mini => "o1-mini",
Self::O1Preview => "o1-preview",
Self::Claude3_5Sonnet => "claude-3-5-sonnet",
}
}
@@ -79,20 +60,14 @@ impl Model {
Self::Gpt3_5Turbo => "GPT-3.5",
Self::Gpt4 => "GPT-4",
Self::Gpt4o => "GPT-4o",
Self::O1Mini => "o1-mini",
Self::O1Preview => "o1-preview",
Self::Claude3_5Sonnet => "Claude 3.5 Sonnet",
}
}
pub fn max_token_count(&self) -> usize {
match self {
Self::Gpt4o => 64000,
Self::Gpt4 => 32768,
Self::Gpt3_5Turbo => 12288,
Self::O1Mini => 20000,
Self::O1Preview => 20000,
Self::Claude3_5Sonnet => 200_000,
Self::Gpt4o => 128000,
Self::Gpt4 => 8192,
Self::Gpt3_5Turbo => 16385,
}
}
}
@@ -112,7 +87,7 @@ impl Request {
Self {
intent: true,
n: 1,
stream: model.uses_streaming(),
stream: true,
temperature: 0.1,
model,
messages,
@@ -138,8 +113,7 @@ pub struct ResponseEvent {
pub struct ResponseChoice {
pub index: usize,
pub finish_reason: Option<String>,
pub delta: Option<ResponseDelta>,
pub message: Option<ResponseDelta>,
pub delta: ResponseDelta,
}
#[derive(Debug, Deserialize)]
@@ -359,23 +333,9 @@ async fn stream_completion(
if let Some(low_speed_timeout) = low_speed_timeout {
request_builder = request_builder.read_timeout(low_speed_timeout);
}
let is_streaming = request.stream;
let request = request_builder.body(AsyncBody::from(serde_json::to_string(&request)?))?;
let mut response = client.send(request).await?;
if !response.status().is_success() {
let mut body = Vec::new();
response.body_mut().read_to_end(&mut body).await?;
let body_str = std::str::from_utf8(&body)?;
return Err(anyhow!(
"Failed to connect to API: {} {}",
response.status(),
body_str
));
}
if is_streaming {
if response.status().is_success() {
let reader = BufReader::new(response.into_body());
Ok(reader
.lines()
@@ -407,9 +367,19 @@ async fn stream_completion(
} else {
let mut body = Vec::new();
response.body_mut().read_to_end(&mut body).await?;
let body_str = std::str::from_utf8(&body)?;
let response: ResponseEvent = serde_json::from_str(body_str)?;
Ok(futures::stream::once(async move { Ok(response) }).boxed())
let body_str = std::str::from_utf8(&body)?;
match serde_json::from_str::<ResponseEvent>(body_str) {
Ok(_) => Err(anyhow!(
"Unexpected success response while expecting an error: {}",
body_str,
)),
Err(_) => Err(anyhow!(
"Failed to connect to API: {} {}",
response.status(),
body_str,
)),
}
}
}

View File

@@ -18,6 +18,7 @@ collections.workspace = true
ctor.workspace = true
editor.workspace = true
env_logger.workspace = true
futures.workspace = true
gpui.workspace = true
language.workspace = true
log.workspace = true

View File

@@ -14,6 +14,10 @@ use editor::{
scroll::Autoscroll,
Editor, EditorEvent, ExcerptId, ExcerptRange, MultiBuffer, ToOffset,
};
use futures::{
channel::mpsc::{self, UnboundedSender},
StreamExt as _,
};
use gpui::{
actions, div, svg, AnyElement, AnyView, AppContext, Context, EventEmitter, FocusHandle,
FocusableView, HighlightStyle, InteractiveElement, IntoElement, Model, ParentElement, Render,
@@ -58,10 +62,11 @@ struct ProjectDiagnosticsEditor {
summary: DiagnosticSummary,
excerpts: Model<MultiBuffer>,
path_states: Vec<PathState>,
paths_to_update: BTreeSet<(ProjectPath, Option<LanguageServerId>)>,
paths_to_update: BTreeSet<(ProjectPath, LanguageServerId)>,
include_warnings: bool,
context: u32,
update_excerpts_task: Option<Task<Result<()>>>,
update_paths_tx: UnboundedSender<(ProjectPath, Option<LanguageServerId>)>,
_update_excerpts_task: Task<Result<()>>,
_subscription: Subscription,
}
@@ -124,14 +129,14 @@ impl ProjectDiagnosticsEditor {
}
project::Event::DiskBasedDiagnosticsFinished { language_server_id } => {
log::debug!("disk based diagnostics finished for server {language_server_id}");
this.update_stale_excerpts(cx);
this.enqueue_update_stale_excerpts(Some(*language_server_id));
}
project::Event::DiagnosticsUpdated {
language_server_id,
path,
} => {
this.paths_to_update
.insert((path.clone(), Some(*language_server_id)));
.insert((path.clone(), *language_server_id));
this.summary = project.read(cx).diagnostic_summary(false, cx);
cx.emit(EditorEvent::TitleChanged);
@@ -139,7 +144,7 @@ impl ProjectDiagnosticsEditor {
log::debug!("diagnostics updated for server {language_server_id}, path {path:?}. recording change");
} else {
log::debug!("diagnostics updated for server {language_server_id}, path {path:?}. updating excerpts");
this.update_stale_excerpts(cx);
this.enqueue_update_stale_excerpts(Some(*language_server_id));
}
}
_ => {}
@@ -166,12 +171,14 @@ impl ProjectDiagnosticsEditor {
cx.focus(&this.focus_handle);
}
}
EditorEvent::Blurred => this.update_stale_excerpts(cx),
EditorEvent::Blurred => this.enqueue_update_stale_excerpts(None),
_ => {}
}
})
.detach();
let (update_excerpts_tx, mut update_excerpts_rx) = mpsc::unbounded();
let project = project_handle.read(cx);
let mut this = Self {
project: project_handle.clone(),
@@ -184,45 +191,27 @@ impl ProjectDiagnosticsEditor {
path_states: Default::default(),
paths_to_update: Default::default(),
include_warnings: ProjectDiagnosticsSettings::get_global(cx).include_warnings,
update_excerpts_task: None,
update_paths_tx: update_excerpts_tx,
_update_excerpts_task: cx.spawn(move |this, mut cx| async move {
while let Some((path, language_server_id)) = update_excerpts_rx.next().await {
if let Some(buffer) = project_handle
.update(&mut cx, |project, cx| project.open_buffer(path.clone(), cx))?
.await
.log_err()
{
this.update(&mut cx, |this, cx| {
this.update_excerpts(path, language_server_id, buffer, cx);
})?;
}
}
anyhow::Ok(())
}),
_subscription: project_event_subscription,
};
this.update_all_excerpts(cx);
this.enqueue_update_all_excerpts(cx);
this
}
fn update_stale_excerpts(&mut self, cx: &mut ViewContext<Self>) {
if self.update_excerpts_task.is_some() {
return;
}
let project_handle = self.project.clone();
self.update_excerpts_task = Some(cx.spawn(|this, mut cx| async move {
loop {
let Some((path, language_server_id)) = this.update(&mut cx, |this, _| {
let Some((path, language_server_id)) = this.paths_to_update.pop_first() else {
this.update_excerpts_task.take();
return None;
};
Some((path, language_server_id))
})?
else {
break;
};
if let Some(buffer) = project_handle
.update(&mut cx, |project, cx| project.open_buffer(path.clone(), cx))?
.await
.log_err()
{
this.update(&mut cx, |this, cx| {
this.update_excerpts(path, language_server_id, buffer, cx);
})?;
}
}
Ok(())
}));
}
fn new(
project_handle: Model<Project>,
workspace: WeakView<Workspace>,
@@ -250,7 +239,7 @@ impl ProjectDiagnosticsEditor {
fn toggle_warnings(&mut self, _: &ToggleWarnings, cx: &mut ViewContext<Self>) {
self.include_warnings = !self.include_warnings;
self.update_all_excerpts(cx);
self.enqueue_update_all_excerpts(cx);
cx.notify();
}
@@ -262,28 +251,37 @@ impl ProjectDiagnosticsEditor {
fn focus_out(&mut self, cx: &mut ViewContext<Self>) {
if !self.focus_handle.is_focused(cx) && !self.editor.focus_handle(cx).is_focused(cx) {
self.update_stale_excerpts(cx);
self.enqueue_update_stale_excerpts(None);
}
}
/// Enqueue an update of all excerpts. Updates all paths that either
/// currently have diagnostics or are currently present in this view.
fn update_all_excerpts(&mut self, cx: &mut ViewContext<Self>) {
fn enqueue_update_all_excerpts(&mut self, cx: &mut ViewContext<Self>) {
self.project.update(cx, |project, cx| {
let mut paths = project
.diagnostic_summaries(false, cx)
.map(|(path, _, _)| (path, None))
.map(|(path, _, _)| path)
.collect::<BTreeSet<_>>();
paths.extend(
self.path_states
.iter()
.map(|state| (state.path.clone(), None)),
);
let paths_to_update = std::mem::take(&mut self.paths_to_update);
paths.extend(paths_to_update.into_iter().map(|(path, _)| (path, None)));
self.paths_to_update = paths;
paths.extend(self.path_states.iter().map(|state| state.path.clone()));
for path in paths {
self.update_paths_tx.unbounded_send((path, None)).unwrap();
}
});
self.update_stale_excerpts(cx);
}
/// Enqueue an update of the excerpts for any path whose diagnostics are known
/// to have changed. If a language server id is passed, then only the excerpts for
/// that language server's diagnostics will be updated. Otherwise, all stale excerpts
/// will be refreshed.
fn enqueue_update_stale_excerpts(&mut self, language_server_id: Option<LanguageServerId>) {
for (path, server_id) in &self.paths_to_update {
if language_server_id.map_or(true, |id| id == *server_id) {
self.update_paths_tx
.unbounded_send((path.clone(), Some(*server_id)))
.unwrap();
}
}
}
fn update_excerpts(
@@ -293,6 +291,11 @@ impl ProjectDiagnosticsEditor {
buffer: Model<Buffer>,
cx: &mut ViewContext<Self>,
) {
self.paths_to_update.retain(|(path, server_id)| {
*path != path_to_update
|| server_to_update.map_or(false, |to_update| *server_id != to_update)
});
let was_empty = self.path_states.is_empty();
let snapshot = buffer.read(cx).snapshot();
let path_ix = match self

View File

@@ -800,7 +800,7 @@ async fn test_random_diagnostics(cx: &mut TestAppContext, mut rng: StdRng) {
}
log::info!("updating mutated diagnostics view");
mutated_view.update(cx, |view, cx| view.update_stale_excerpts(cx));
mutated_view.update(cx, |view, _| view.enqueue_update_stale_excerpts(None));
cx.run_until_parked();
log::info!("constructing reference diagnostics view");

View File

@@ -14,12 +14,12 @@ impl Render for ToolbarControls {
let mut has_stale_excerpts = false;
let mut is_updating = false;
if let Some(editor) = self.diagnostics() {
let diagnostics = editor.read(cx);
include_warnings = diagnostics.include_warnings;
has_stale_excerpts = !diagnostics.paths_to_update.is_empty();
is_updating = diagnostics.update_excerpts_task.is_some()
|| diagnostics
if let Some(editor) = self.editor() {
let editor = editor.read(cx);
include_warnings = editor.include_warnings;
has_stale_excerpts = !editor.paths_to_update.is_empty();
is_updating = !editor.update_paths_tx.is_empty()
|| editor
.project
.read(cx)
.language_servers_running_disk_based_diagnostics(cx)
@@ -49,9 +49,9 @@ impl Render for ToolbarControls {
.disabled(is_updating)
.tooltip(move |cx| Tooltip::text("Update excerpts", cx))
.on_click(cx.listener(|this, _, cx| {
if let Some(diagnostics) = this.diagnostics() {
diagnostics.update(cx, |diagnostics, cx| {
diagnostics.update_all_excerpts(cx);
if let Some(editor) = this.editor() {
editor.update(cx, |editor, _| {
editor.enqueue_update_stale_excerpts(None);
});
}
})),
@@ -63,7 +63,7 @@ impl Render for ToolbarControls {
.shape(IconButtonShape::Square)
.tooltip(move |cx| Tooltip::text(tooltip, cx))
.on_click(cx.listener(|this, _, cx| {
if let Some(editor) = this.diagnostics() {
if let Some(editor) = this.editor() {
editor.update(cx, |editor, cx| {
editor.toggle_warnings(&Default::default(), cx);
});
@@ -105,7 +105,7 @@ impl ToolbarControls {
ToolbarControls { editor: None }
}
fn diagnostics(&self) -> Option<View<ProjectDiagnosticsEditor>> {
fn editor(&self) -> Option<View<ProjectDiagnosticsEditor>> {
self.editor.as_ref()?.upgrade()
}
}

View File

@@ -77,7 +77,6 @@ tree-sitter-html = { workspace = true, optional = true }
tree-sitter-rust = { workspace = true, optional = true }
tree-sitter-typescript = { workspace = true, optional = true }
unicode-segmentation.workspace = true
unicode-script.workspace = true
unindent = { workspace = true, optional = true }
ui.workspace = true
url.workspace = true

View File

@@ -1172,7 +1172,7 @@ impl Sub for DisplayPoint {
#[serde(transparent)]
pub struct DisplayRow(pub u32);
impl Add<DisplayRow> for DisplayRow {
impl Add for DisplayRow {
type Output = Self;
fn add(self, other: Self) -> Self::Output {
@@ -1180,15 +1180,7 @@ impl Add<DisplayRow> for DisplayRow {
}
}
impl Add<u32> for DisplayRow {
type Output = Self;
fn add(self, other: u32) -> Self::Output {
DisplayRow(self.0 + other)
}
}
impl Sub<DisplayRow> for DisplayRow {
impl Sub for DisplayRow {
type Output = Self;
fn sub(self, other: Self) -> Self::Output {
@@ -1196,14 +1188,6 @@ impl Sub<DisplayRow> for DisplayRow {
}
}
impl Sub<u32> for DisplayRow {
type Output = Self;
fn sub(self, other: u32) -> Self::Output {
DisplayRow(self.0 - other)
}
}
impl DisplayPoint {
pub fn new(row: DisplayRow, column: u32) -> Self {
Self(BlockPoint(Point::new(row.0, column)))

View File

@@ -49,7 +49,6 @@ pub mod test;
use ::git::diff::DiffHunkStatus;
pub(crate) use actions::*;
pub use actions::{OpenExcerpts, OpenExcerptsSplit};
use aho_corasick::AhoCorasick;
use anyhow::{anyhow, Context as _, Result};
use blink_manager::BlinkManager;
@@ -104,7 +103,6 @@ pub use proposed_changes_editor::{
ProposedChangeLocation, ProposedChangesEditor, ProposedChangesEditorToolbar,
};
use similar::{ChangeTag, TextDiff};
use std::iter::Peekable;
use task::{ResolvedTask, TaskTemplate, TaskVariables};
use hover_links::{find_file, HoverLink, HoveredLinkState, InlayHighlight};
@@ -1355,22 +1353,22 @@ impl CompletionsMenu {
// Strong matches are the ones with a high fuzzy-matcher score (the "obvious" matches)
// and the Weak matches are the rest.
//
// For the strong matches, we sort by our fuzzy-finder score first and for the weak
// matches, we prefer language-server sort_text first.
// For the strong matches, we sort by the language-servers score first and for the weak
// matches, we prefer our fuzzy finder first.
//
// The thinking behind that: we want to show strong matches first in order of relevance(fuzzy score).
// Rest of the matches(weak) can be sorted as language-server expects.
// The thinking behind that: it's useless to take the sort_text the language-server gives
// us into account when it's obviously a bad match.
#[derive(PartialEq, Eq, PartialOrd, Ord)]
enum MatchScore<'a> {
Strong {
score: Reverse<OrderedFloat<f64>>,
sort_text: Option<&'a str>,
score: Reverse<OrderedFloat<f64>>,
sort_key: (usize, &'a str),
},
Weak {
sort_text: Option<&'a str>,
score: Reverse<OrderedFloat<f64>>,
sort_text: Option<&'a str>,
sort_key: (usize, &'a str),
},
}
@@ -1382,14 +1380,14 @@ impl CompletionsMenu {
if mat.score >= 0.2 {
MatchScore::Strong {
score,
sort_text,
score,
sort_key,
}
} else {
MatchScore::Weak {
sort_text,
score,
sort_text,
sort_key,
}
}
@@ -7010,8 +7008,6 @@ impl Editor {
}
}
let tab_size = buffer.settings_at(selection.head(), cx).tab_size;
// Since not all lines in the selection may be at the same indent
// level, choose the indent size that is the most common between all
// of the lines.
@@ -7029,7 +7025,7 @@ impl Editor {
let indent_size = indent_size_occurrences
.into_iter()
.max_by_key(|(indent, count)| (*count, indent.len_with_expanded_tabs(tab_size)))
.max_by_key(|(indent, count)| (*count, indent.len))
.map(|(indent, _)| indent)
.unwrap_or_default();
let row = rows_by_indent_size[&indent_size][0];
@@ -7055,10 +7051,6 @@ impl Editor {
should_rewrap = true;
}
if !should_rewrap {
continue;
}
if selection.is_empty() {
'expand_upwards: while start_row > 0 {
let prev_row = start_row - 1;
@@ -7083,6 +7075,10 @@ impl Editor {
}
}
if !should_rewrap {
continue;
}
let start = Point::new(start_row, 0);
let end = Point::new(end_row, buffer.line_len(MultiBufferRow(end_row)));
let selection_text = buffer.text_for_range(start..end).collect::<String>();
@@ -7101,15 +7097,29 @@ impl Editor {
continue;
};
let unwrapped_text = lines_without_prefixes.join(" ");
let wrap_column = buffer
.settings_at(Point::new(start_row, 0), cx)
.preferred_line_length as usize;
let wrapped_text = wrap_with_prefix(
line_prefix,
lines_without_prefixes.join(" "),
wrap_column,
tab_size,
);
let mut wrapped_text = String::new();
let mut current_line = line_prefix.clone();
for word in unwrapped_text.split_whitespace() {
if current_line.len() + word.len() >= wrap_column {
wrapped_text.push_str(&current_line);
wrapped_text.push('\n');
current_line.truncate(line_prefix.len());
}
if current_line.len() > line_prefix.len() {
current_line.push(' ');
}
current_line.push_str(word);
}
if !current_line.is_empty() {
wrapped_text.push_str(&current_line);
}
let diff = TextDiff::from_lines(&selection_text, &wrapped_text);
let mut offset = start.to_offset(&buffer);
@@ -7270,14 +7280,9 @@ impl Editor {
if clipboard_selections.len() != old_selections.len() {
clipboard_selections.drain(..);
}
let cursor_offset = this.selections.last::<usize>(cx).head();
let mut auto_indent_on_paste = true;
this.buffer.update(cx, |buffer, cx| {
let snapshot = buffer.read(cx);
auto_indent_on_paste =
snapshot.settings_at(cursor_offset, cx).auto_indent_on_paste;
let mut start_offset = 0;
let mut edits = Vec::new();
let mut original_indent_columns = Vec::new();
@@ -7316,13 +7321,9 @@ impl Editor {
buffer.edit(
edits,
if auto_indent_on_paste {
Some(AutoindentMode::Block {
original_indent_columns,
})
} else {
None
},
Some(AutoindentMode::Block {
original_indent_columns,
}),
cx,
);
});
@@ -12540,11 +12541,11 @@ impl Editor {
});
}
pub fn open_excerpts_in_split(&mut self, _: &OpenExcerptsSplit, cx: &mut ViewContext<Self>) {
fn open_excerpts_in_split(&mut self, _: &OpenExcerptsSplit, cx: &mut ViewContext<Self>) {
self.open_excerpts_common(true, cx)
}
pub fn open_excerpts(&mut self, _: &OpenExcerpts, cx: &mut ViewContext<Self>) {
fn open_excerpts(&mut self, _: &OpenExcerpts, cx: &mut ViewContext<Self>) {
self.open_excerpts_common(false, cx)
}
@@ -13046,289 +13047,6 @@ impl Editor {
}
}
fn char_len_with_expanded_tabs(offset: usize, text: &str, tab_size: NonZeroU32) -> usize {
let tab_size = tab_size.get() as usize;
let mut width = offset;
for ch in text.chars() {
width += if ch == '\t' {
tab_size - (width % tab_size)
} else {
1
};
}
width - offset
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_string_size_with_expanded_tabs() {
let nz = |val| NonZeroU32::new(val).unwrap();
assert_eq!(char_len_with_expanded_tabs(0, "", nz(4)), 0);
assert_eq!(char_len_with_expanded_tabs(0, "hello", nz(4)), 5);
assert_eq!(char_len_with_expanded_tabs(0, "\thello", nz(4)), 9);
assert_eq!(char_len_with_expanded_tabs(0, "abc\tab", nz(4)), 6);
assert_eq!(char_len_with_expanded_tabs(0, "hello\t", nz(4)), 8);
assert_eq!(char_len_with_expanded_tabs(0, "\t\t", nz(8)), 16);
assert_eq!(char_len_with_expanded_tabs(0, "x\t", nz(8)), 8);
assert_eq!(char_len_with_expanded_tabs(7, "x\t", nz(8)), 9);
}
}
/// Tokenizes a string into runs of text that should stick together, or that is whitespace.
struct WordBreakingTokenizer<'a> {
input: &'a str,
}
impl<'a> WordBreakingTokenizer<'a> {
fn new(input: &'a str) -> Self {
Self { input }
}
}
fn is_char_ideographic(ch: char) -> bool {
use unicode_script::Script::*;
use unicode_script::UnicodeScript;
matches!(ch.script(), Han | Tangut | Yi)
}
fn is_grapheme_ideographic(text: &str) -> bool {
text.chars().any(is_char_ideographic)
}
fn is_grapheme_whitespace(text: &str) -> bool {
text.chars().any(|x| x.is_whitespace())
}
fn should_stay_with_preceding_ideograph(text: &str) -> bool {
text.chars().next().map_or(false, |ch| {
matches!(ch, '。' | '、' | '' | '' | '' | '' | '' | '…')
})
}
#[derive(PartialEq, Eq, Debug, Clone, Copy)]
struct WordBreakToken<'a> {
token: &'a str,
grapheme_len: usize,
is_whitespace: bool,
}
impl<'a> Iterator for WordBreakingTokenizer<'a> {
/// Yields a span, the count of graphemes in the token, and whether it was
/// whitespace. Note that it also breaks at word boundaries.
type Item = WordBreakToken<'a>;
fn next(&mut self) -> Option<Self::Item> {
use unicode_segmentation::UnicodeSegmentation;
if self.input.is_empty() {
return None;
}
let mut iter = self.input.graphemes(true).peekable();
let mut offset = 0;
let mut graphemes = 0;
if let Some(first_grapheme) = iter.next() {
let is_whitespace = is_grapheme_whitespace(first_grapheme);
offset += first_grapheme.len();
graphemes += 1;
if is_grapheme_ideographic(first_grapheme) && !is_whitespace {
if let Some(grapheme) = iter.peek().copied() {
if should_stay_with_preceding_ideograph(grapheme) {
offset += grapheme.len();
graphemes += 1;
}
}
} else {
let mut words = self.input[offset..].split_word_bound_indices().peekable();
let mut next_word_bound = words.peek().copied();
if next_word_bound.map_or(false, |(i, _)| i == 0) {
next_word_bound = words.next();
}
while let Some(grapheme) = iter.peek().copied() {
if next_word_bound.map_or(false, |(i, _)| i == offset) {
break;
};
if is_grapheme_whitespace(grapheme) != is_whitespace {
break;
};
offset += grapheme.len();
graphemes += 1;
iter.next();
}
}
let token = &self.input[..offset];
self.input = &self.input[offset..];
if is_whitespace {
Some(WordBreakToken {
token: " ",
grapheme_len: 1,
is_whitespace: true,
})
} else {
Some(WordBreakToken {
token,
grapheme_len: graphemes,
is_whitespace: false,
})
}
} else {
None
}
}
}
#[test]
fn test_word_breaking_tokenizer() {
let tests: &[(&str, &[(&str, usize, bool)])] = &[
("", &[]),
(" ", &[(" ", 1, true)]),
("Ʒ", &[("Ʒ", 1, false)]),
("Ǽ", &[("Ǽ", 1, false)]),
("", &[("", 1, false)]),
("⋑⋑", &[("⋑⋑", 2, false)]),
(
"原理,进而",
&[
("", 1, false),
("理,", 2, false),
("", 1, false),
("", 1, false),
],
),
(
"hello world",
&[("hello", 5, false), (" ", 1, true), ("world", 5, false)],
),
(
"hello, world",
&[("hello,", 6, false), (" ", 1, true), ("world", 5, false)],
),
(
" hello world",
&[
(" ", 1, true),
("hello", 5, false),
(" ", 1, true),
("world", 5, false),
],
),
(
"这是什么 \n 钢笔",
&[
("", 1, false),
("", 1, false),
("", 1, false),
("", 1, false),
(" ", 1, true),
("", 1, false),
("", 1, false),
],
),
("mutton", &[(" ", 1, true), ("mutton", 6, false)]),
];
for (input, result) in tests {
assert_eq!(
WordBreakingTokenizer::new(input).collect::<Vec<_>>(),
result
.iter()
.copied()
.map(|(token, grapheme_len, is_whitespace)| WordBreakToken {
token,
grapheme_len,
is_whitespace,
})
.collect::<Vec<_>>()
);
}
}
fn wrap_with_prefix(
line_prefix: String,
unwrapped_text: String,
wrap_column: usize,
tab_size: NonZeroU32,
) -> String {
let line_prefix_len = char_len_with_expanded_tabs(0, &line_prefix, tab_size);
let mut wrapped_text = String::new();
let mut current_line = line_prefix.clone();
let tokenizer = WordBreakingTokenizer::new(&unwrapped_text);
let mut current_line_len = line_prefix_len;
for WordBreakToken {
token,
grapheme_len,
is_whitespace,
} in tokenizer
{
if current_line_len + grapheme_len > wrap_column && current_line_len != line_prefix_len {
wrapped_text.push_str(current_line.trim_end());
wrapped_text.push('\n');
current_line.truncate(line_prefix.len());
current_line_len = line_prefix_len;
if !is_whitespace {
current_line.push_str(token);
current_line_len += grapheme_len;
}
} else if !is_whitespace {
current_line.push_str(token);
current_line_len += grapheme_len;
} else if current_line_len != line_prefix_len {
current_line.push(' ');
current_line_len += 1;
}
}
if !current_line.is_empty() {
wrapped_text.push_str(&current_line);
}
wrapped_text
}
#[test]
fn test_wrap_with_prefix() {
assert_eq!(
wrap_with_prefix(
"# ".to_string(),
"abcdefg".to_string(),
4,
NonZeroU32::new(4).unwrap()
),
"# abcdefg"
);
assert_eq!(
wrap_with_prefix(
"".to_string(),
"\thello world".to_string(),
8,
NonZeroU32::new(4).unwrap()
),
"hello\nworld"
);
assert_eq!(
wrap_with_prefix(
"// ".to_string(),
"xx \nyy zz aa bb cc".to_string(),
12,
NonZeroU32::new(4).unwrap()
),
"// xx yy zz\n// aa bb cc"
);
assert_eq!(
wrap_with_prefix(
String::new(),
"这是什么 \n 钢笔".to_string(),
3,
NonZeroU32::new(4).unwrap()
),
"这是什\n么 钢\n"
);
}
fn hunks_for_selections(
multi_buffer_snapshot: &MultiBufferSnapshot,
selections: &[Selection<Anchor>],
@@ -13828,7 +13546,7 @@ fn consume_contiguous_rows(
contiguous_row_selections: &mut Vec<Selection<Point>>,
selection: &Selection<Point>,
display_map: &DisplaySnapshot,
selections: &mut Peekable<std::slice::Iter<Selection<Point>>>,
selections: &mut std::iter::Peekable<std::slice::Iter<Selection<Point>>>,
) -> (MultiBufferRow, MultiBufferRow) {
contiguous_row_selections.push(selection.clone());
let start_row = MultiBufferRow(selection.start.row);

View File

@@ -8385,74 +8385,6 @@ async fn test_completion_page_up_down_keys(cx: &mut gpui::TestAppContext) {
});
}
#[gpui::test]
async fn test_completion_sort(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorLspTestContext::new_rust(
lsp::ServerCapabilities {
completion_provider: Some(lsp::CompletionOptions {
trigger_characters: Some(vec![".".to_string()]),
..Default::default()
}),
..Default::default()
},
cx,
)
.await;
cx.lsp
.handle_request::<lsp::request::Completion, _, _>(move |_, _| async move {
Ok(Some(lsp::CompletionResponse::Array(vec![
lsp::CompletionItem {
label: "Range".into(),
sort_text: Some("a".into()),
..Default::default()
},
lsp::CompletionItem {
label: "r".into(),
sort_text: Some("b".into()),
..Default::default()
},
lsp::CompletionItem {
label: "ret".into(),
sort_text: Some("c".into()),
..Default::default()
},
lsp::CompletionItem {
label: "return".into(),
sort_text: Some("d".into()),
..Default::default()
},
lsp::CompletionItem {
label: "slice".into(),
sort_text: Some("d".into()),
..Default::default()
},
])))
});
cx.set_state("");
cx.executor().run_until_parked();
cx.update_editor(|editor, cx| {
editor.show_completions(
&ShowCompletions {
trigger: Some("r".into()),
},
cx,
);
});
cx.executor().run_until_parked();
cx.update_editor(|editor, _| {
if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
assert_eq!(
menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
&["r", "ret", "Range", "return"]
);
} else {
panic!("expected completion menu to be open");
}
});
}
#[gpui::test]
async fn test_no_duplicated_completion_requests(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});

View File

@@ -7,7 +7,7 @@ use crate::{
};
use gpui::{
div, px, AnyElement, AsyncWindowContext, FontWeight, Hsla, InteractiveElement, IntoElement,
MouseButton, ParentElement, Pixels, ScrollHandle, Size, Stateful, StatefulInteractiveElement,
MouseButton, ParentElement, Pixels, ScrollHandle, Size, StatefulInteractiveElement,
StyleRefinement, Styled, Task, TextStyleRefinement, View, ViewContext,
};
use itertools::Itertools;
@@ -21,7 +21,7 @@ use std::rc::Rc;
use std::{borrow::Cow, cell::RefCell};
use std::{ops::Range, sync::Arc, time::Duration};
use theme::ThemeSettings;
use ui::{prelude::*, window_is_transparent, Scrollbar, ScrollbarState};
use ui::{prelude::*, window_is_transparent};
use util::TryFutureExt;
pub const HOVER_DELAY_MILLIS: u64 = 350;
pub const HOVER_REQUEST_DELAY_MILLIS: u64 = 200;
@@ -144,12 +144,10 @@ pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut Vie
let blocks = vec![inlay_hover.tooltip];
let parsed_content = parse_blocks(&blocks, &language_registry, None, &mut cx).await;
let scroll_handle = ScrollHandle::new();
let hover_popover = InfoPopover {
symbol_range: RangeInEditor::Inlay(inlay_hover.range.clone()),
parsed_content,
scrollbar_state: ScrollbarState::new(scroll_handle.clone()),
scroll_handle,
scroll_handle: ScrollHandle::new(),
keyboard_grace: Rc::new(RefCell::new(false)),
anchor: None,
};
@@ -437,14 +435,12 @@ fn show_hover(
let language = hover_result.language;
let parsed_content =
parse_blocks(&blocks, &language_registry, language, &mut cx).await;
let scroll_handle = ScrollHandle::new();
info_popover_tasks.push((
range.clone(),
InfoPopover {
symbol_range: RangeInEditor::Text(range),
parsed_content,
scrollbar_state: ScrollbarState::new(scroll_handle.clone()),
scroll_handle,
scroll_handle: ScrollHandle::new(),
keyboard_grace: Rc::new(RefCell::new(ignore_timeout)),
anchor: Some(anchor),
},
@@ -615,7 +611,7 @@ impl HoverState {
!self.info_popovers.is_empty() || self.diagnostic_popover.is_some()
}
pub(crate) fn render(
pub fn render(
&mut self,
snapshot: &EditorSnapshot,
visible_rows: Range<DisplayRow>,
@@ -684,25 +680,25 @@ impl HoverState {
}
#[derive(Debug, Clone)]
pub(crate) struct InfoPopover {
pub(crate) symbol_range: RangeInEditor,
pub(crate) parsed_content: Option<View<Markdown>>,
pub(crate) scroll_handle: ScrollHandle,
pub(crate) scrollbar_state: ScrollbarState,
pub(crate) keyboard_grace: Rc<RefCell<bool>>,
pub(crate) anchor: Option<Anchor>,
pub struct InfoPopover {
pub symbol_range: RangeInEditor,
pub parsed_content: Option<View<Markdown>>,
pub scroll_handle: ScrollHandle,
pub keyboard_grace: Rc<RefCell<bool>>,
pub anchor: Option<Anchor>,
}
impl InfoPopover {
pub(crate) fn render(
&mut self,
max_size: Size<Pixels>,
cx: &mut ViewContext<Editor>,
) -> AnyElement {
pub fn render(&mut self, max_size: Size<Pixels>, cx: &mut ViewContext<Editor>) -> AnyElement {
let keyboard_grace = Rc::clone(&self.keyboard_grace);
let mut d = div()
.id("info_popover")
.elevation_2(cx)
.overflow_y_scroll()
.track_scroll(&self.scroll_handle)
.max_w(max_size.width)
.max_h(max_size.height)
// Prevent a mouse down/move on the popover from being propagated to the editor,
// because that would dismiss the popover.
.on_mouse_move(|_, cx| cx.stop_propagation())
@@ -710,21 +706,11 @@ impl InfoPopover {
let mut keyboard_grace = keyboard_grace.borrow_mut();
*keyboard_grace = false;
cx.stop_propagation();
});
})
.p_2();
if let Some(markdown) = &self.parsed_content {
d = d
.child(
div()
.id("info-md-container")
.overflow_y_scroll()
.max_w(max_size.width)
.max_h(max_size.height)
.p_2()
.track_scroll(&self.scroll_handle)
.child(markdown.clone()),
)
.child(self.render_vertical_scrollbar(cx));
d = d.child(markdown.clone());
}
d.into_any_element()
}
@@ -738,38 +724,6 @@ impl InfoPopover {
cx.notify();
self.scroll_handle.set_offset(current);
}
fn render_vertical_scrollbar(&self, cx: &mut ViewContext<Editor>) -> Stateful<Div> {
div()
.occlude()
.id("info-popover-vertical-scroll")
.on_mouse_move(cx.listener(|_, _, cx| {
cx.notify();
cx.stop_propagation()
}))
.on_hover(|_, cx| {
cx.stop_propagation();
})
.on_any_mouse_down(|_, cx| {
cx.stop_propagation();
})
.on_mouse_up(
MouseButton::Left,
cx.listener(|_, _, cx| {
cx.stop_propagation();
}),
)
.on_scroll_wheel(cx.listener(|_, _, cx| {
cx.notify();
}))
.h_full()
.absolute()
.right_1()
.top_1()
.bottom_0()
.w(px(12.))
.cursor_default()
.children(Scrollbar::vertical(self.scrollbar_state.clone()))
}
}
#[derive(Debug, Clone)]

View File

@@ -992,15 +992,12 @@ impl SerializableItem for Editor {
};
// First create the empty buffer
let buffer = project
.update(&mut cx, |project, cx| project.create_buffer(cx))?
.await?;
let buffer = project.update(&mut cx, |project, cx| {
project.create_local_buffer("", language, cx)
})?;
// Then set the text so that the dirty bit is set correctly
buffer.update(&mut cx, |buffer, cx| {
if let Some(language) = language {
buffer.set_language(Some(language), cx);
}
buffer.set_text(contents, cx);
})?;

View File

@@ -1,4 +1,4 @@
use std::{ops::Range, time::Duration};
use std::ops::Range;
use collections::HashMap;
use itertools::Itertools;
@@ -36,53 +36,35 @@ impl LinkedEditingRanges {
self.0.is_empty()
}
}
const UPDATE_DEBOUNCE: Duration = Duration::from_millis(50);
// TODO do not refresh anything at all, if the settings/capabilities do not have it enabled.
pub(super) fn refresh_linked_ranges(
editor: &mut Editor,
cx: &mut ViewContext<Editor>,
) -> Option<()> {
if editor.pending_rename.is_some() {
pub(super) fn refresh_linked_ranges(this: &mut Editor, cx: &mut ViewContext<Editor>) -> Option<()> {
if this.pending_rename.is_some() {
return None;
}
let project = editor.project.as_ref()?.downgrade();
editor.linked_editing_range_task = Some(cx.spawn(|editor, mut cx| async move {
cx.background_executor().timer(UPDATE_DEBOUNCE).await;
let mut applicable_selections = Vec::new();
editor
.update(&mut cx, |editor, cx| {
let selections = editor.selections.all::<usize>(cx);
let snapshot = editor.buffer.read(cx).snapshot(cx);
let buffer = editor.buffer.read(cx);
for selection in selections {
let cursor_position = selection.head();
let start_position = snapshot.anchor_before(cursor_position);
let end_position = snapshot.anchor_after(selection.tail());
if start_position.buffer_id != end_position.buffer_id
|| end_position.buffer_id.is_none()
{
// Throw away selections spanning multiple buffers.
continue;
}
if let Some(buffer) = end_position.buffer_id.and_then(|id| buffer.buffer(id)) {
applicable_selections.push((
buffer,
start_position.text_anchor,
end_position.text_anchor,
));
}
}
})
.ok()?;
if applicable_selections.is_empty() {
return None;
let project = this.project.clone()?;
let selections = this.selections.all::<usize>(cx);
let buffer = this.buffer.read(cx);
let mut applicable_selections = vec![];
let snapshot = buffer.snapshot(cx);
for selection in selections {
let cursor_position = selection.head();
let start_position = snapshot.anchor_before(cursor_position);
let end_position = snapshot.anchor_after(selection.tail());
if start_position.buffer_id != end_position.buffer_id || end_position.buffer_id.is_none() {
// Throw away selections spanning multiple buffers.
continue;
}
if let Some(buffer) = end_position.buffer_id.and_then(|id| buffer.buffer(id)) {
applicable_selections.push((
buffer,
start_position.text_anchor,
end_position.text_anchor,
));
}
}
if applicable_selections.is_empty() {
return None;
}
this.linked_editing_range_task = Some(cx.spawn(|this, mut cx| async move {
let highlights = project
.update(&mut cx, |project, cx| {
let mut linked_edits_tasks = vec![];
@@ -128,38 +110,37 @@ pub(super) fn refresh_linked_ranges(
}
linked_edits_tasks
})
.ok()?;
.log_err()?;
let highlights = futures::future::join_all(highlights).await;
editor
.update(&mut cx, |this, cx| {
this.linked_edit_ranges.0.clear();
if this.pending_rename.is_some() {
return;
}
for (buffer_id, ranges) in highlights.into_iter().flatten() {
this.linked_edit_ranges
.0
.entry(buffer_id)
.or_default()
.extend(ranges);
}
for (buffer_id, values) in this.linked_edit_ranges.0.iter_mut() {
let Some(snapshot) = this
.buffer
.read(cx)
.buffer(*buffer_id)
.map(|buffer| buffer.read(cx).snapshot())
else {
continue;
};
values.sort_by(|lhs, rhs| lhs.0.cmp(&rhs.0, &snapshot));
}
this.update(&mut cx, |this, cx| {
this.linked_edit_ranges.0.clear();
if this.pending_rename.is_some() {
return;
}
for (buffer_id, ranges) in highlights.into_iter().flatten() {
this.linked_edit_ranges
.0
.entry(buffer_id)
.or_default()
.extend(ranges);
}
for (buffer_id, values) in this.linked_edit_ranges.0.iter_mut() {
let Some(snapshot) = this
.buffer
.read(cx)
.buffer(*buffer_id)
.map(|buffer| buffer.read(cx).snapshot())
else {
continue;
};
values.sort_by(|lhs, rhs| lhs.0.cmp(&rhs.0, &snapshot));
}
cx.notify();
})
.ok()?;
cx.notify();
})
.log_err();
Some(())
}));

View File

@@ -135,8 +135,6 @@ impl ExtensionBuilder {
.args(options.release.then_some("--release"))
.arg("--target-dir")
.arg(extension_dir.join("target"))
// WASI builds do not work with sccache and just stuck, so disable it.
.env("RUSTC_WRAPPER", "")
.current_dir(extension_dir)
.output()
.context("failed to run `cargo`")?;

View File

@@ -2,7 +2,6 @@ interface lsp {
/// An LSP completion.
record completion {
label: string,
label-details: option<completion-label-details>,
detail: option<string>,
kind: option<completion-kind>,
insert-text-format: option<insert-text-format>,
@@ -38,12 +37,6 @@ interface lsp {
other(s32),
}
/// Label details for an LSP completion.
record completion-label-details {
detail: option<string>,
description: option<string>,
}
/// Defines how to interpret the insert text in a completion item.
variant insert-text-format {
plain-text,

View File

@@ -387,7 +387,6 @@ impl From<lsp::CompletionItem> for wit::Completion {
fn from(value: lsp::CompletionItem) -> Self {
Self {
label: value.label,
label_details: value.label_details.map(Into::into),
detail: value.detail,
kind: value.kind.map(Into::into),
insert_text_format: value.insert_text_format.map(Into::into),
@@ -395,15 +394,6 @@ impl From<lsp::CompletionItem> for wit::Completion {
}
}
impl From<lsp::CompletionItemLabelDetails> for wit::CompletionLabelDetails {
fn from(value: lsp::CompletionItemLabelDetails) -> Self {
Self {
detail: value.detail,
description: value.description,
}
}
}
impl From<lsp::CompletionItemKind> for wit::CompletionKind {
fn from(value: lsp::CompletionItemKind) -> Self {
match value {

View File

@@ -20,9 +20,7 @@ use wasmtime::{
#[cfg(test)]
pub use latest::CodeLabelSpanLiteral;
pub use latest::{
zed::extension::lsp::{
Completion, CompletionKind, CompletionLabelDetails, InsertTextFormat, Symbol, SymbolKind,
},
zed::extension::lsp::{Completion, CompletionKind, InsertTextFormat, Symbol, SymbolKind},
zed::extension::slash_command::{SlashCommandArgumentCompletion, SlashCommandOutput},
CodeLabel, CodeLabelSpan, Command, Range, SlashCommand,
};
@@ -78,18 +76,13 @@ impl Extension {
version: SemanticVersion,
component: &Component,
) -> Result<Self> {
if version >= latest::MIN_VERSION {
// Note: The release channel can be used to stage a new version of the extension API.
// We always allow the latest in tests so that the extension tests pass on release branches.
let allow_latest_version = match release_channel {
ReleaseChannel::Dev | ReleaseChannel::Nightly => true,
ReleaseChannel::Stable | ReleaseChannel::Preview => cfg!(test),
};
if !allow_latest_version {
Err(anyhow!(
"unreleased versions of the extension API can only be used on development builds of Zed"
))?;
}
// Note: The release channel can be used to stage a new version of the extension API.
let allow_latest_version = match release_channel {
ReleaseChannel::Dev | ReleaseChannel::Nightly => true,
ReleaseChannel::Stable | ReleaseChannel::Preview => false,
};
if allow_latest_version && version >= latest::MIN_VERSION {
let extension =
latest::Extension::instantiate_async(store, component, latest::linker())
.await
@@ -269,11 +262,7 @@ impl Extension {
.await
}
Extension::V010(ext) => Ok(ext
.call_labels_for_completions(
store,
&language_server_id.0,
&completions.into_iter().map(Into::into).collect::<Vec<_>>(),
)
.call_labels_for_completions(store, &language_server_id.0, &completions)
.await?
.map(|labels| {
labels
@@ -282,11 +271,7 @@ impl Extension {
.collect()
})),
Extension::V006(ext) => Ok(ext
.call_labels_for_completions(
store,
&language_server_id.0,
&completions.into_iter().map(Into::into).collect::<Vec<_>>(),
)
.call_labels_for_completions(store, &language_server_id.0, &completions)
.await?
.map(|labels| {
labels
@@ -310,11 +295,7 @@ impl Extension {
.await
}
Extension::V010(ext) => Ok(ext
.call_labels_for_symbols(
store,
&language_server_id.0,
&symbols.into_iter().map(Into::into).collect::<Vec<_>>(),
)
.call_labels_for_symbols(store, &language_server_id.0, &symbols)
.await?
.map(|labels| {
labels
@@ -323,11 +304,7 @@ impl Extension {
.collect()
})),
Extension::V006(ext) => Ok(ext
.call_labels_for_symbols(
store,
&language_server_id.0,
&symbols.into_iter().map(Into::into).collect::<Vec<_>>(),
)
.call_labels_for_symbols(store, &language_server_id.0, &symbols)
.await?
.map(|labels| {
labels

View File

@@ -1,4 +1,4 @@
use super::{latest, since_v0_1_0};
use super::latest;
use crate::wasm_host::WasmState;
use anyhow::Result;
use async_trait::async_trait;
@@ -16,7 +16,7 @@ wasmtime::component::bindgen!({
with: {
"worktree": ExtensionWorktree,
"zed:extension/github": latest::zed::extension::github,
"zed:extension/lsp": since_v0_1_0::zed::extension::lsp,
"zed:extension/lsp": latest::zed::extension::lsp,
"zed:extension/nodejs": latest::zed::extension::nodejs,
"zed:extension/platform": latest::zed::extension::platform,
},

View File

@@ -35,6 +35,7 @@ wasmtime::component::bindgen!({
"key-value-store": ExtensionKeyValueStore,
"zed:extension/http-client/http-response-stream": ExtensionHttpResponseStream,
"zed:extension/github": latest::zed::extension::github,
"zed:extension/lsp": latest::zed::extension::lsp,
"zed:extension/nodejs": latest::zed::extension::nodejs,
"zed:extension/platform": latest::zed::extension::platform,
"zed:extension/slash-command": latest::zed::extension::slash_command,
@@ -134,103 +135,6 @@ impl From<CodeLabel> for latest::CodeLabel {
}
}
impl From<latest::Completion> for Completion {
fn from(value: latest::Completion) -> Self {
Self {
label: value.label,
detail: value.detail,
kind: value.kind.map(Into::into),
insert_text_format: value.insert_text_format.map(Into::into),
}
}
}
impl From<latest::lsp::CompletionKind> for lsp::CompletionKind {
fn from(value: latest::lsp::CompletionKind) -> Self {
match value {
latest::lsp::CompletionKind::Text => Self::Text,
latest::lsp::CompletionKind::Method => Self::Method,
latest::lsp::CompletionKind::Function => Self::Function,
latest::lsp::CompletionKind::Constructor => Self::Constructor,
latest::lsp::CompletionKind::Field => Self::Field,
latest::lsp::CompletionKind::Variable => Self::Variable,
latest::lsp::CompletionKind::Class => Self::Class,
latest::lsp::CompletionKind::Interface => Self::Interface,
latest::lsp::CompletionKind::Module => Self::Module,
latest::lsp::CompletionKind::Property => Self::Property,
latest::lsp::CompletionKind::Unit => Self::Unit,
latest::lsp::CompletionKind::Value => Self::Value,
latest::lsp::CompletionKind::Enum => Self::Enum,
latest::lsp::CompletionKind::Keyword => Self::Keyword,
latest::lsp::CompletionKind::Snippet => Self::Snippet,
latest::lsp::CompletionKind::Color => Self::Color,
latest::lsp::CompletionKind::File => Self::File,
latest::lsp::CompletionKind::Reference => Self::Reference,
latest::lsp::CompletionKind::Folder => Self::Folder,
latest::lsp::CompletionKind::EnumMember => Self::EnumMember,
latest::lsp::CompletionKind::Constant => Self::Constant,
latest::lsp::CompletionKind::Struct => Self::Struct,
latest::lsp::CompletionKind::Event => Self::Event,
latest::lsp::CompletionKind::Operator => Self::Operator,
latest::lsp::CompletionKind::TypeParameter => Self::TypeParameter,
latest::lsp::CompletionKind::Other(kind) => Self::Other(kind),
}
}
}
impl From<latest::lsp::InsertTextFormat> for lsp::InsertTextFormat {
fn from(value: latest::lsp::InsertTextFormat) -> Self {
match value {
latest::lsp::InsertTextFormat::PlainText => Self::PlainText,
latest::lsp::InsertTextFormat::Snippet => Self::Snippet,
latest::lsp::InsertTextFormat::Other(value) => Self::Other(value),
}
}
}
impl From<latest::lsp::Symbol> for lsp::Symbol {
fn from(value: latest::lsp::Symbol) -> Self {
Self {
name: value.name,
kind: value.kind.into(),
}
}
}
impl From<latest::lsp::SymbolKind> for lsp::SymbolKind {
fn from(value: latest::lsp::SymbolKind) -> Self {
match value {
latest::lsp::SymbolKind::File => Self::File,
latest::lsp::SymbolKind::Module => Self::Module,
latest::lsp::SymbolKind::Namespace => Self::Namespace,
latest::lsp::SymbolKind::Package => Self::Package,
latest::lsp::SymbolKind::Class => Self::Class,
latest::lsp::SymbolKind::Method => Self::Method,
latest::lsp::SymbolKind::Property => Self::Property,
latest::lsp::SymbolKind::Field => Self::Field,
latest::lsp::SymbolKind::Constructor => Self::Constructor,
latest::lsp::SymbolKind::Enum => Self::Enum,
latest::lsp::SymbolKind::Interface => Self::Interface,
latest::lsp::SymbolKind::Function => Self::Function,
latest::lsp::SymbolKind::Variable => Self::Variable,
latest::lsp::SymbolKind::Constant => Self::Constant,
latest::lsp::SymbolKind::String => Self::String,
latest::lsp::SymbolKind::Number => Self::Number,
latest::lsp::SymbolKind::Boolean => Self::Boolean,
latest::lsp::SymbolKind::Array => Self::Array,
latest::lsp::SymbolKind::Object => Self::Object,
latest::lsp::SymbolKind::Key => Self::Key,
latest::lsp::SymbolKind::Null => Self::Null,
latest::lsp::SymbolKind::EnumMember => Self::EnumMember,
latest::lsp::SymbolKind::Struct => Self::Struct,
latest::lsp::SymbolKind::Event => Self::Event,
latest::lsp::SymbolKind::Operator => Self::Operator,
latest::lsp::SymbolKind::TypeParameter => Self::TypeParameter,
latest::lsp::SymbolKind::Other(kind) => Self::Other(kind),
}
}
}
#[async_trait]
impl HostKeyValueStore for WasmState {
async fn insert(
@@ -432,9 +336,6 @@ async fn convert_response(
Ok(extension_response)
}
#[async_trait]
impl lsp::Host for WasmState {}
#[async_trait]
impl ExtensionImports for WasmState {
async fn get_settings(

View File

@@ -17,7 +17,6 @@ const SUGGESTIONS_BY_EXTENSION_ID: &[(&str, &[&str])] = &[
("astro", &["astro"]),
("beancount", &["beancount"]),
("clojure", &["bb", "clj", "cljc", "cljs", "edn"]),
("neocmake", &["CMakeLists.txt", "cmake"]),
("csharp", &["cs"]),
("dart", &["dart"]),
("dockerfile", &["Dockerfile"]),

View File

@@ -29,7 +29,7 @@ const fn request_feature_url() -> &'static str {
fn file_bug_report_url(specs: &SystemSpecs) -> String {
format!(
"https://github.com/zed-industries/zed/issues/new?assignees=&labels=admin+read%2Ctriage%2Cbug&projects=&template=1_bug_report.yml&environment={}",
"https://github.com/zed-industries/zed/issues/new?assignees=&labels=admin+read%2Ctriage%2Cdefect&projects=&template=1_bug_report.yml&environment={}",
urlencoding::encode(&specs.to_string())
)
}

View File

@@ -1,9 +1,3 @@
#[cfg(target_os = "macos")]
mod mac_watcher;
#[cfg(target_os = "linux")]
pub mod linux_watcher;
use anyhow::{anyhow, Result};
use git::GitHostingProviderRegistry;
@@ -536,21 +530,14 @@ impl Fs for RealFs {
Pin<Box<dyn Send + Stream<Item = Vec<PathEvent>>>>,
Arc<dyn Watcher>,
) {
use fsevent::StreamFlags;
use fsevent::{EventStream, StreamFlags};
let (events_tx, events_rx) = smol::channel::unbounded();
let handles = Arc::new(parking_lot::Mutex::new(collections::BTreeMap::default()));
let watcher = Arc::new(mac_watcher::MacWatcher::new(
events_tx,
Arc::downgrade(&handles),
latency,
));
watcher.add(path).expect("handles can't be dropped");
(
Box::pin(
events_rx
.map(|events| {
let (tx, rx) = smol::channel::unbounded();
let (stream, handle) = EventStream::new(&[path], latency);
std::thread::spawn(move || {
stream.run(move |events| {
smol::block_on(
tx.send(
events
.into_iter()
.map(|event| {
@@ -568,14 +555,19 @@ impl Fs for RealFs {
kind,
}
})
.collect()
})
.chain(futures::stream::once(async move {
drop(handles);
vec![]
})),
),
watcher,
.collect(),
),
)
.is_ok()
});
});
(
Box::pin(rx.chain(futures::stream::once(async move {
drop(handle);
vec![]
}))),
Arc::new(RealWatcher {}),
)
}
@@ -588,26 +580,81 @@ impl Fs for RealFs {
Pin<Box<dyn Send + Stream<Item = Vec<PathEvent>>>>,
Arc<dyn Watcher>,
) {
use notify::EventKind;
use parking_lot::Mutex;
let (tx, rx) = smol::channel::unbounded();
let pending_paths: Arc<Mutex<Vec<PathEvent>>> = Default::default();
let watcher = Arc::new(linux_watcher::LinuxWatcher::new(tx, pending_paths.clone()));
let root_path = path.to_path_buf();
watcher.add(&path).ok(); // Ignore "file doesn't exist error" and rely on parent watcher.
if let Some(parent) = path.parent() {
// watch the parent dir so we can tell when settings.json is created
watcher.add(parent).log_err();
}
// Check if root path is a symlink
let target_path = self.read_link(&path).await.ok();
watcher::global({
let target_path = target_path.clone();
|g| {
let tx = tx.clone();
let pending_paths = pending_paths.clone();
g.add(move |event: &notify::Event| {
let kind = match event.kind {
EventKind::Create(_) => Some(PathEventKind::Created),
EventKind::Modify(_) => Some(PathEventKind::Changed),
EventKind::Remove(_) => Some(PathEventKind::Removed),
_ => None,
};
let mut paths = event
.paths
.iter()
.filter_map(|path| {
if let Some(target) = target_path.clone() {
if path.starts_with(target) {
return Some(PathEvent {
path: path.clone(),
kind,
});
}
} else if path.starts_with(&root_path) {
return Some(PathEvent {
path: path.clone(),
kind,
});
}
None
})
.collect::<Vec<_>>();
if !paths.is_empty() {
paths.sort();
let mut pending_paths = pending_paths.lock();
if pending_paths.is_empty() {
tx.try_send(()).ok();
}
util::extend_sorted(&mut *pending_paths, paths, usize::MAX, |a, b| {
a.path.cmp(&b.path)
});
}
})
}
})
.log_err();
let watcher = Arc::new(RealWatcher {});
watcher.add(path).ok(); // Ignore "file doesn't exist error" and rely on parent watcher.
// Check if path is a symlink and follow the target parent
if let Some(target) = self.read_link(&path).await.ok() {
if let Some(target) = target_path {
watcher.add(&target).ok();
if let Some(parent) = target.parent() {
watcher.add(parent).log_err();
}
}
// watch the parent dir so we can tell when settings.json is created
if let Some(parent) = path.parent() {
watcher.add(parent).log_err();
}
(
Box::pin(rx.filter_map({
let watcher = watcher.clone();
@@ -737,6 +784,23 @@ impl Watcher for RealWatcher {
}
}
#[cfg(target_os = "linux")]
impl Watcher for RealWatcher {
fn add(&self, path: &Path) -> Result<()> {
use notify::Watcher;
Ok(watcher::global(|w| {
w.inotify
.lock()
.watch(path, notify::RecursiveMode::NonRecursive)
})??)
}
fn remove(&self, path: &Path) -> Result<()> {
use notify::Watcher;
Ok(watcher::global(|w| w.inotify.lock().unwatch(path))??)
}
}
#[cfg(any(test, feature = "test-support"))]
pub struct FakeFs {
// Use an unfair lock to ensure tests are deterministic.
@@ -2020,3 +2084,49 @@ mod tests {
);
}
}
#[cfg(target_os = "linux")]
pub mod watcher {
use std::sync::OnceLock;
use parking_lot::Mutex;
use util::ResultExt;
pub struct GlobalWatcher {
// two mutexes because calling inotify.add triggers an inotify.event, which needs watchers.
pub(super) inotify: Mutex<notify::INotifyWatcher>,
pub(super) watchers: Mutex<Vec<Box<dyn Fn(&notify::Event) + Send + Sync>>>,
}
impl GlobalWatcher {
pub(super) fn add(&self, cb: impl Fn(&notify::Event) + Send + Sync + 'static) {
self.watchers.lock().push(Box::new(cb))
}
}
static INOTIFY_INSTANCE: OnceLock<anyhow::Result<GlobalWatcher, notify::Error>> =
OnceLock::new();
fn handle_event(event: Result<notify::Event, notify::Error>) {
let Some(event) = event.log_err() else { return };
global::<()>(move |watcher| {
for f in watcher.watchers.lock().iter() {
f(&event)
}
})
.log_err();
}
pub fn global<T>(f: impl FnOnce(&GlobalWatcher) -> T) -> anyhow::Result<T> {
let result = INOTIFY_INSTANCE.get_or_init(|| {
notify::recommended_watcher(handle_event).map(|file_watcher| GlobalWatcher {
inotify: Mutex::new(file_watcher),
watchers: Default::default(),
})
});
match result {
Ok(g) => Ok(f(g)),
Err(e) => Err(anyhow::anyhow!("{}", e)),
}
}
}

View File

@@ -1,121 +0,0 @@
use notify::EventKind;
use parking_lot::Mutex;
use std::sync::{Arc, OnceLock};
use util::ResultExt;
use crate::{PathEvent, PathEventKind, Watcher};
pub struct LinuxWatcher {
tx: smol::channel::Sender<()>,
pending_path_events: Arc<Mutex<Vec<PathEvent>>>,
}
impl LinuxWatcher {
pub fn new(
tx: smol::channel::Sender<()>,
pending_path_events: Arc<Mutex<Vec<PathEvent>>>,
) -> Self {
Self {
tx,
pending_path_events,
}
}
}
impl Watcher for LinuxWatcher {
fn add(&self, path: &std::path::Path) -> gpui::Result<()> {
let root_path = path.to_path_buf();
let tx = self.tx.clone();
let pending_paths = self.pending_path_events.clone();
use notify::Watcher;
global({
|g| {
g.add(move |event: &notify::Event| {
let kind = match event.kind {
EventKind::Create(_) => Some(PathEventKind::Created),
EventKind::Modify(_) => Some(PathEventKind::Changed),
EventKind::Remove(_) => Some(PathEventKind::Removed),
_ => None,
};
let mut path_events = event
.paths
.iter()
.filter_map(|event_path| {
event_path.starts_with(&root_path).then(|| PathEvent {
path: event_path.clone(),
kind,
})
})
.collect::<Vec<_>>();
if !path_events.is_empty() {
path_events.sort();
let mut pending_paths = pending_paths.lock();
if pending_paths.is_empty() {
tx.try_send(()).ok();
}
util::extend_sorted(
&mut *pending_paths,
path_events,
usize::MAX,
|a, b| a.path.cmp(&b.path),
);
}
})
}
})?;
global(|g| {
g.inotify
.lock()
.watch(path, notify::RecursiveMode::NonRecursive)
})??;
Ok(())
}
fn remove(&self, path: &std::path::Path) -> gpui::Result<()> {
use notify::Watcher;
Ok(global(|w| w.inotify.lock().unwatch(path))??)
}
}
pub struct GlobalWatcher {
// two mutexes because calling inotify.add triggers an inotify.event, which needs watchers.
pub(super) inotify: Mutex<notify::INotifyWatcher>,
pub(super) watchers: Mutex<Vec<Box<dyn Fn(&notify::Event) + Send + Sync>>>,
}
impl GlobalWatcher {
pub(super) fn add(&self, cb: impl Fn(&notify::Event) + Send + Sync + 'static) {
self.watchers.lock().push(Box::new(cb))
}
}
static INOTIFY_INSTANCE: OnceLock<anyhow::Result<GlobalWatcher, notify::Error>> = OnceLock::new();
fn handle_event(event: Result<notify::Event, notify::Error>) {
let Some(event) = event.log_err() else { return };
global::<()>(move |watcher| {
for f in watcher.watchers.lock().iter() {
f(&event)
}
})
.log_err();
}
pub fn global<T>(f: impl FnOnce(&GlobalWatcher) -> T) -> anyhow::Result<T> {
let result = INOTIFY_INSTANCE.get_or_init(|| {
notify::recommended_watcher(handle_event).map(|file_watcher| GlobalWatcher {
inotify: Mutex::new(file_watcher),
watchers: Default::default(),
})
});
match result {
Ok(g) => Ok(f(g)),
Err(e) => Err(anyhow::anyhow!("{}", e)),
}
}

View File

@@ -1,70 +0,0 @@
use crate::Watcher;
use anyhow::{Context as _, Result};
use collections::{BTreeMap, Bound};
use fsevent::EventStream;
use parking_lot::Mutex;
use std::{
path::{Path, PathBuf},
sync::Weak,
time::Duration,
};
pub struct MacWatcher {
events_tx: smol::channel::Sender<Vec<fsevent::Event>>,
handles: Weak<Mutex<BTreeMap<PathBuf, fsevent::Handle>>>,
latency: Duration,
}
impl MacWatcher {
pub fn new(
events_tx: smol::channel::Sender<Vec<fsevent::Event>>,
handles: Weak<Mutex<BTreeMap<PathBuf, fsevent::Handle>>>,
latency: Duration,
) -> Self {
Self {
events_tx,
handles,
latency,
}
}
}
impl Watcher for MacWatcher {
fn add(&self, path: &Path) -> Result<()> {
let handles = self
.handles
.upgrade()
.context("unable to watch path, receiver dropped")?;
let mut handles = handles.lock();
// Return early if an ancestor of this path was already being watched.
if let Some((watched_path, _)) = handles
.range::<Path, _>((Bound::Unbounded, Bound::Included(path)))
.next_back()
{
if path.starts_with(watched_path) {
return Ok(());
}
}
let (stream, handle) = EventStream::new(&[path], self.latency);
let tx = self.events_tx.clone();
std::thread::spawn(move || {
stream.run(move |events| smol::block_on(tx.send(events)).is_ok());
});
handles.insert(path.into(), handle);
Ok(())
}
fn remove(&self, path: &Path) -> gpui::Result<()> {
let handles = self
.handles
.upgrade()
.context("unable to remove path, receiver dropped")?;
let mut handles = handles.lock();
handles.remove(path);
Ok(())
}
}

View File

@@ -45,8 +45,6 @@ pub trait GitRepository: Send + Sync {
fn branch_exits(&self, _: &str) -> Result<bool>;
fn blame(&self, path: &Path, content: Rope) -> Result<crate::blame::Blame>;
fn path(&self) -> PathBuf;
}
impl std::fmt::Debug for dyn GitRepository {
@@ -85,11 +83,6 @@ impl GitRepository for RealGitRepository {
}
}
fn path(&self) -> PathBuf {
let repo = self.repository.lock();
repo.path().into()
}
fn load_index_text(&self, relative_file_path: &Path) -> Option<String> {
fn logic(repo: &git2::Repository, relative_file_path: &Path) -> Result<Option<String>> {
const STAGE_NORMAL: i32 = 0;
@@ -283,11 +276,6 @@ impl GitRepository for FakeGitRepository {
None
}
fn path(&self) -> PathBuf {
let state = self.state.lock();
state.path.clone()
}
fn status(&self, path_prefixes: &[PathBuf]) -> Result<GitStatus> {
let state = self.state.lock();
let mut entries = state

View File

@@ -1,9 +1,9 @@
use editor::{Editor, ToPoint};
use gpui::{AppContext, Subscription, Task, View, WeakView};
use gpui::{AppContext, Subscription, View, WeakView};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources};
use std::{fmt::Write, time::Duration};
use std::fmt::Write;
use text::{Point, Selection};
use ui::{
div, Button, ButtonCommon, Clickable, FluentBuilder, IntoElement, LabelSize, ParentElement,
@@ -23,7 +23,6 @@ pub struct CursorPosition {
position: Option<Point>,
selected_count: SelectionStats,
workspace: WeakView<Workspace>,
update_position: Task<()>,
_observe_active_editor: Option<Subscription>,
}
@@ -33,61 +32,40 @@ impl CursorPosition {
position: None,
selected_count: Default::default(),
workspace: workspace.weak_handle(),
update_position: Task::ready(()),
_observe_active_editor: None,
}
}
fn update_position(
&mut self,
editor: View<Editor>,
debounce: Option<Duration>,
cx: &mut ViewContext<Self>,
) {
let editor = editor.downgrade();
self.update_position = cx.spawn(|cursor_position, mut cx| async move {
if let Some(debounce) = debounce {
cx.background_executor().timer(debounce).await;
}
fn update_position(&mut self, editor: View<Editor>, cx: &mut ViewContext<Self>) {
editor.update(cx, |editor, cx| {
let buffer = editor.buffer().read(cx).snapshot(cx);
editor
.update(&mut cx, |editor, cx| {
let buffer = editor.buffer().read(cx).snapshot(cx);
cursor_position.update(cx, |cursor_position, cx| {
cursor_position.selected_count = SelectionStats::default();
cursor_position.selected_count.selections = editor.selections.count();
let mut last_selection = None::<Selection<usize>>;
for selection in editor.selections.all::<usize>(cx) {
cursor_position.selected_count.characters += buffer
.text_for_range(selection.start..selection.end)
.map(|t| t.chars().count())
.sum::<usize>();
if last_selection
.as_ref()
.map_or(true, |last_selection| selection.id > last_selection.id)
{
last_selection = Some(selection);
}
}
for selection in editor.selections.all::<Point>(cx) {
if selection.end != selection.start {
cursor_position.selected_count.lines +=
(selection.end.row - selection.start.row) as usize;
if selection.end.column != 0 {
cursor_position.selected_count.lines += 1;
}
}
}
cursor_position.position =
last_selection.map(|s| s.head().to_point(&buffer));
cx.notify();
})
})
.ok()
.transpose()
.ok()
.flatten();
self.selected_count = Default::default();
self.selected_count.selections = editor.selections.count();
let mut last_selection: Option<Selection<usize>> = None;
for selection in editor.selections.all::<usize>(cx) {
self.selected_count.characters += buffer
.text_for_range(selection.start..selection.end)
.map(|t| t.chars().count())
.sum::<usize>();
if last_selection
.as_ref()
.map_or(true, |last_selection| selection.id > last_selection.id)
{
last_selection = Some(selection);
}
}
for selection in editor.selections.all::<Point>(cx) {
if selection.end != selection.start {
self.selected_count.lines += (selection.end.row - selection.start.row) as usize;
if selection.end.column != 0 {
self.selected_count.lines += 1;
}
}
}
self.position = last_selection.map(|s| s.head().to_point(&buffer));
});
cx.notify();
}
fn write_position(&self, text: &mut String, cx: &AppContext) {
@@ -176,8 +154,6 @@ impl Render for CursorPosition {
}
}
const UPDATE_DEBOUNCE: Duration = Duration::from_millis(50);
impl StatusItemView for CursorPosition {
fn set_active_pane_item(
&mut self,
@@ -185,11 +161,8 @@ impl StatusItemView for CursorPosition {
cx: &mut ViewContext<Self>,
) {
if let Some(editor) = active_pane_item.and_then(|item| item.act_as::<Editor>(cx)) {
self._observe_active_editor =
Some(cx.observe(&editor, |cursor_position, editor, cx| {
Self::update_position(cursor_position, editor, Some(UPDATE_DEBOUNCE), cx)
}));
self.update_position(editor, None, cx);
self._observe_active_editor = Some(cx.observe(&editor, Self::update_position));
self.update_position(editor, cx);
} else {
self.position = None;
self._observe_active_editor = None;

View File

@@ -229,7 +229,7 @@ mod tests {
use indoc::indoc;
use project::{FakeFs, Project};
use serde_json::json;
use std::{sync::Arc, time::Duration};
use std::sync::Arc;
use workspace::{AppState, Workspace};
#[gpui::test]
@@ -379,7 +379,6 @@ mod tests {
.downcast::<Editor>()
.unwrap();
cx.executor().advance_clock(Duration::from_millis(200));
workspace.update(cx, |workspace, cx| {
assert_eq!(
&SelectionStats {
@@ -398,7 +397,6 @@ mod tests {
);
});
editor.update(cx, |editor, cx| editor.select_all(&SelectAll, cx));
cx.executor().advance_clock(Duration::from_millis(200));
workspace.update(cx, |workspace, cx| {
assert_eq!(
&SelectionStats {

View File

@@ -136,7 +136,7 @@ pub struct GenerateContentResponse {
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GenerateContentCandidate {
pub index: Option<usize>,
pub index: usize,
pub content: Content,
pub finish_reason: Option<String>,
pub finish_message: Option<String>,

View File

@@ -1,7 +1,7 @@
use crate::SharedString;
use anyhow::{anyhow, Context, Result};
use collections::HashMap;
pub use no_action::{is_no_action, NoAction};
pub use no_action::NoAction;
use serde_json::json;
use std::any::{Any, TypeId};
@@ -321,12 +321,6 @@ macro_rules! __impl_action {
mod no_action {
use crate as gpui;
use std::any::Any as _;
actions!(zed, [NoAction]);
/// Returns whether or not this action represents a removed key binding.
pub fn is_no_action(action: &dyn gpui::Action) -> bool {
action.as_any().type_id() == (NoAction {}).type_id()
}
}

View File

@@ -4,10 +4,10 @@ mod context;
pub use binding::*;
pub use context::*;
use crate::{is_no_action, Action, Keystroke};
use crate::{Action, Keystroke, NoAction};
use collections::HashMap;
use smallvec::SmallVec;
use std::any::TypeId;
use std::any::{Any, TypeId};
/// An opaque identifier of which version of the keymap is currently active.
/// The keymap's version is changed whenever bindings are added or removed.
@@ -19,7 +19,6 @@ pub struct KeymapVersion(usize);
pub struct Keymap {
bindings: Vec<KeyBinding>,
binding_indices_by_action_id: HashMap<TypeId, SmallVec<[usize; 3]>>,
no_action_binding_indices: Vec<usize>,
version: KeymapVersion,
}
@@ -40,14 +39,10 @@ impl Keymap {
pub fn add_bindings<T: IntoIterator<Item = KeyBinding>>(&mut self, bindings: T) {
for binding in bindings {
let action_id = binding.action().as_any().type_id();
if is_no_action(&*binding.action) {
self.no_action_binding_indices.push(self.bindings.len());
} else {
self.binding_indices_by_action_id
.entry(action_id)
.or_default()
.push(self.bindings.len());
}
self.binding_indices_by_action_id
.entry(action_id)
.or_default()
.push(self.bindings.len());
self.bindings.push(binding);
}
@@ -58,7 +53,6 @@ impl Keymap {
pub fn clear(&mut self) {
self.bindings.clear();
self.binding_indices_by_action_id.clear();
self.no_action_binding_indices.clear();
self.version.0 += 1;
}
@@ -73,39 +67,12 @@ impl Keymap {
action: &'a dyn Action,
) -> impl 'a + DoubleEndedIterator<Item = &'a KeyBinding> {
let action_id = action.type_id();
let binding_indices = self
.binding_indices_by_action_id
self.binding_indices_by_action_id
.get(&action_id)
.map_or(&[] as _, SmallVec::as_slice)
.iter();
binding_indices.filter_map(|ix| {
let binding = &self.bindings[*ix];
if !binding.action().partial_eq(action) {
return None;
}
for null_ix in &self.no_action_binding_indices {
if null_ix > ix {
let null_binding = &self.bindings[*null_ix];
if null_binding.keystrokes == binding.keystrokes {
let null_binding_matches =
match (&null_binding.context_predicate, &binding.context_predicate) {
(None, _) => true,
(Some(_), None) => false,
(Some(null_predicate), Some(predicate)) => {
null_predicate.is_superset(predicate)
}
};
if null_binding_matches {
return None;
}
}
}
}
Some(binding)
})
.iter()
.map(|ix| &self.bindings[*ix])
.filter(move |binding| binding.action().partial_eq(action))
}
/// all bindings for input returns all bindings that might match the input
@@ -167,7 +134,7 @@ impl Keymap {
let bindings = bindings
.into_iter()
.map_while(|(binding, _)| {
if is_no_action(&*binding.action) {
if binding.action.as_any().type_id() == (NoAction {}).type_id() {
None
} else {
Some(binding)
@@ -195,7 +162,7 @@ impl Keymap {
mod tests {
use super::*;
use crate as gpui;
use gpui::{actions, NoAction};
use gpui::actions;
actions!(
keymap_test,
@@ -274,31 +241,4 @@ mod tests {
.0
.is_empty());
}
#[test]
fn test_bindings_for_action() {
let bindings = [
KeyBinding::new("ctrl-a", ActionAlpha {}, Some("pane")),
KeyBinding::new("ctrl-b", ActionBeta {}, Some("editor && mode == full")),
KeyBinding::new("ctrl-c", ActionGamma {}, Some("workspace")),
KeyBinding::new("ctrl-a", NoAction {}, Some("pane && active")),
KeyBinding::new("ctrl-b", NoAction {}, Some("editor")),
];
let mut keymap = Keymap::default();
keymap.add_bindings(bindings.clone());
assert_bindings(&keymap, &ActionAlpha {}, &["ctrl-a"]);
assert_bindings(&keymap, &ActionBeta {}, &[]);
assert_bindings(&keymap, &ActionGamma {}, &["ctrl-c"]);
#[track_caller]
fn assert_bindings(keymap: &Keymap, action: &dyn Action, expected: &[&str]) {
let actual = keymap
.bindings_for_action(action)
.map(|binding| binding.keystrokes[0].unparse())
.collect::<Vec<_>>();
assert_eq!(actual, expected, "{:?}", action);
}
}
}

View File

@@ -269,30 +269,6 @@ impl KeyBindingContextPredicate {
}
}
/// Returns whether or not this predicate matches all possible contexts matched by
/// the other predicate.
pub fn is_superset(&self, other: &Self) -> bool {
if self == other {
return true;
}
if let KeyBindingContextPredicate::Or(left, right) = self {
return left.is_superset(other) || right.is_superset(other);
}
match other {
KeyBindingContextPredicate::Child(_, child) => self.is_superset(child),
KeyBindingContextPredicate::And(left, right) => {
self.is_superset(left) || self.is_superset(right)
}
KeyBindingContextPredicate::Identifier(_) => false,
KeyBindingContextPredicate::Equal(_, _) => false,
KeyBindingContextPredicate::NotEqual(_, _) => false,
KeyBindingContextPredicate::Not(_) => false,
KeyBindingContextPredicate::Or(_, _) => false,
}
}
fn parse_expr(mut source: &str, min_precedence: u32) -> anyhow::Result<(Self, &str)> {
type Op = fn(
KeyBindingContextPredicate,
@@ -583,27 +559,4 @@ mod tests {
)
);
}
#[test]
fn test_is_superset() {
assert_is_superset("editor", "editor", true);
assert_is_superset("editor", "workspace", false);
assert_is_superset("editor", "editor && vim_mode", true);
assert_is_superset("editor", "mode == full && editor", true);
assert_is_superset("editor && mode == full", "editor", false);
assert_is_superset("editor", "something > editor", true);
assert_is_superset("editor", "editor > menu", false);
assert_is_superset("foo || bar || baz", "bar", true);
assert_is_superset("foo || bar || baz", "quux", false);
#[track_caller]
fn assert_is_superset(a: &str, b: &str, result: bool) {
let a = KeyBindingContextPredicate::parse(a).unwrap();
let b = KeyBindingContextPredicate::parse(b).unwrap();
assert_eq!(a.is_superset(&b), result, "({a:?}).is_superset({b:?})");
}
}
}

View File

@@ -43,6 +43,7 @@ use smallvec::SmallVec;
use std::borrow::Cow;
use std::hash::{Hash, Hasher};
use std::io::Cursor;
use std::ops;
use std::time::{Duration, Instant};
use std::{
fmt::{self, Debug},
@@ -548,6 +549,41 @@ pub(crate) trait PlatformAtlas: Send + Sync {
key: &AtlasKey,
build: &mut dyn FnMut() -> Result<Option<(Size<DevicePixels>, Cow<'a, [u8]>)>>,
) -> Result<Option<AtlasTile>>;
fn remove(&self, key: &AtlasKey);
}
struct AtlasTextureList<T> {
textures: Vec<Option<T>>,
free_list: Vec<usize>,
}
impl<T> Default for AtlasTextureList<T> {
fn default() -> Self {
Self {
textures: Vec::default(),
free_list: Vec::default(),
}
}
}
impl<T> ops::Index<usize> for AtlasTextureList<T> {
type Output = Option<T>;
fn index(&self, index: usize) -> &Self::Output {
&self.textures[index]
}
}
impl<T> AtlasTextureList<T> {
#[allow(unused)]
fn drain(&mut self) -> std::vec::Drain<Option<T>> {
self.free_list.clear();
self.textures.drain(..)
}
fn iter_mut(&mut self) -> impl DoubleEndedIterator<Item = &mut T> {
self.textures.iter_mut().flatten()
}
}
#[derive(Clone, Debug, PartialEq, Eq)]

View File

@@ -1,6 +1,6 @@
use crate::{
AtlasKey, AtlasTextureId, AtlasTextureKind, AtlasTile, Bounds, DevicePixels, PlatformAtlas,
Point, Size,
platform::AtlasTextureList, AtlasKey, AtlasTextureId, AtlasTextureKind, AtlasTile, Bounds,
DevicePixels, PlatformAtlas, Point, Size,
};
use anyhow::Result;
use blade_graphics as gpu;
@@ -67,7 +67,7 @@ impl BladeAtlas {
pub(crate) fn clear_textures(&self, texture_kind: AtlasTextureKind) {
let mut lock = self.0.lock();
let textures = &mut lock.storage[texture_kind];
for texture in textures {
for texture in textures.iter_mut() {
texture.clear();
}
}
@@ -130,19 +130,48 @@ impl PlatformAtlas for BladeAtlas {
Ok(Some(tile))
}
}
fn remove(&self, key: &AtlasKey) {
let mut lock = self.0.lock();
let Some(id) = lock.tiles_by_key.remove(key).map(|tile| tile.texture_id) else {
return;
};
let Some(texture_slot) = lock.storage[id.kind].textures.get_mut(id.index as usize) else {
return;
};
if let Some(mut texture) = texture_slot.take() {
texture.decrement_ref_count();
if texture.is_unreferenced() {
lock.storage[id.kind]
.free_list
.push(texture.id.index as usize);
texture.destroy(&lock.gpu);
} else {
*texture_slot = Some(texture);
}
}
}
}
impl BladeAtlasState {
fn allocate(&mut self, size: Size<DevicePixels>, texture_kind: AtlasTextureKind) -> AtlasTile {
let textures = &mut self.storage[texture_kind];
textures
.iter_mut()
.rev()
.find_map(|texture| texture.allocate(size))
.unwrap_or_else(|| {
let texture = self.push_texture(size, texture_kind);
texture.allocate(size).unwrap()
})
{
let textures = &mut self.storage[texture_kind];
if let Some(tile) = textures
.iter_mut()
.rev()
.find_map(|texture| texture.allocate(size))
{
return tile;
}
}
let texture = self.push_texture(size, texture_kind);
texture.allocate(size).unwrap()
}
fn push_texture(
@@ -198,21 +227,30 @@ impl BladeAtlasState {
},
);
let textures = &mut self.storage[kind];
let texture_list = &mut self.storage[kind];
let index = texture_list.free_list.pop();
let atlas_texture = BladeAtlasTexture {
id: AtlasTextureId {
index: textures.len() as u32,
index: index.unwrap_or(texture_list.textures.len()) as u32,
kind,
},
allocator: etagere::BucketedAtlasAllocator::new(size.into()),
format,
raw,
raw_view,
live_atlas_keys: 0,
};
self.initializations.push(atlas_texture.id);
textures.push(atlas_texture);
textures.last_mut().unwrap()
if let Some(ix) = index {
texture_list.textures[ix] = Some(atlas_texture);
texture_list.textures.get_mut(ix).unwrap().as_mut().unwrap()
} else {
texture_list.textures.push(Some(atlas_texture));
texture_list.textures.last_mut().unwrap().as_mut().unwrap()
}
}
fn upload_texture(&mut self, id: AtlasTextureId, bounds: Bounds<DevicePixels>, bytes: &[u8]) {
@@ -258,13 +296,13 @@ impl BladeAtlasState {
#[derive(Default)]
struct BladeAtlasStorage {
monochrome_textures: Vec<BladeAtlasTexture>,
polychrome_textures: Vec<BladeAtlasTexture>,
path_textures: Vec<BladeAtlasTexture>,
monochrome_textures: AtlasTextureList<BladeAtlasTexture>,
polychrome_textures: AtlasTextureList<BladeAtlasTexture>,
path_textures: AtlasTextureList<BladeAtlasTexture>,
}
impl ops::Index<AtlasTextureKind> for BladeAtlasStorage {
type Output = Vec<BladeAtlasTexture>;
type Output = AtlasTextureList<BladeAtlasTexture>;
fn index(&self, kind: AtlasTextureKind) -> &Self::Output {
match kind {
crate::AtlasTextureKind::Monochrome => &self.monochrome_textures,
@@ -292,19 +330,19 @@ impl ops::Index<AtlasTextureId> for BladeAtlasStorage {
crate::AtlasTextureKind::Polychrome => &self.polychrome_textures,
crate::AtlasTextureKind::Path => &self.path_textures,
};
&textures[id.index as usize]
textures[id.index as usize].as_ref().unwrap()
}
}
impl BladeAtlasStorage {
fn destroy(&mut self, gpu: &gpu::Context) {
for mut texture in self.monochrome_textures.drain(..) {
for mut texture in self.monochrome_textures.drain().flatten() {
texture.destroy(gpu);
}
for mut texture in self.polychrome_textures.drain(..) {
for mut texture in self.polychrome_textures.drain().flatten() {
texture.destroy(gpu);
}
for mut texture in self.path_textures.drain(..) {
for mut texture in self.path_textures.drain().flatten() {
texture.destroy(gpu);
}
}
@@ -316,6 +354,7 @@ struct BladeAtlasTexture {
raw: gpu::Texture,
raw_view: gpu::TextureView,
format: gpu::TextureFormat,
live_atlas_keys: u32,
}
impl BladeAtlasTexture {
@@ -334,6 +373,7 @@ impl BladeAtlasTexture {
size,
},
};
self.live_atlas_keys += 1;
Some(tile)
}
@@ -345,6 +385,14 @@ impl BladeAtlasTexture {
fn bytes_per_pixel(&self) -> u8 {
self.format.block_info().size
}
fn decrement_ref_count(&mut self) {
self.live_atlas_keys -= 1;
}
fn is_unreferenced(&mut self) -> bool {
self.live_atlas_keys == 0
}
}
impl From<Size<DevicePixels>> for etagere::Size {

View File

@@ -36,9 +36,11 @@ struct CosmicTextSystemState {
impl CosmicTextSystem {
pub(crate) fn new() -> Self {
// todo(linux) make font loading non-blocking
let mut font_system = FontSystem::new();
// todo(linux) make font loading non-blocking
font_system.db_mut().load_system_fonts();
Self(RwLock::new(CosmicTextSystemState {
font_system,
swash_cache: SwashCache::new(),

View File

@@ -1,6 +1,6 @@
use crate::{
AtlasKey, AtlasTextureId, AtlasTextureKind, AtlasTile, Bounds, DevicePixels, PlatformAtlas,
Point, Size,
platform::AtlasTextureList, AtlasKey, AtlasTextureId, AtlasTextureKind, AtlasTile, Bounds,
DevicePixels, PlatformAtlas, Point, Size,
};
use anyhow::{anyhow, Result};
use collections::FxHashMap;
@@ -42,7 +42,7 @@ impl MetalAtlas {
AtlasTextureKind::Polychrome => &mut lock.polychrome_textures,
AtlasTextureKind::Path => &mut lock.path_textures,
};
for texture in textures {
for texture in textures.iter_mut() {
texture.clear();
}
}
@@ -50,9 +50,9 @@ impl MetalAtlas {
struct MetalAtlasState {
device: AssertSend<Device>,
monochrome_textures: Vec<MetalAtlasTexture>,
polychrome_textures: Vec<MetalAtlasTexture>,
path_textures: Vec<MetalAtlasTexture>,
monochrome_textures: AtlasTextureList<MetalAtlasTexture>,
polychrome_textures: AtlasTextureList<MetalAtlasTexture>,
path_textures: AtlasTextureList<MetalAtlasTexture>,
tiles_by_key: FxHashMap<AtlasKey, AtlasTile>,
}
@@ -78,6 +78,38 @@ impl PlatformAtlas for MetalAtlas {
Ok(Some(tile))
}
}
fn remove(&self, key: &AtlasKey) {
let mut lock = self.0.lock();
let Some(id) = lock.tiles_by_key.get(key).map(|v| v.texture_id) else {
return;
};
let textures = match id.kind {
AtlasTextureKind::Monochrome => &mut lock.monochrome_textures,
AtlasTextureKind::Polychrome => &mut lock.polychrome_textures,
AtlasTextureKind::Path => &mut lock.polychrome_textures,
};
let Some(texture_slot) = textures
.textures
.iter_mut()
.find(|texture| texture.as_ref().is_some_and(|v| v.id == id))
else {
return;
};
if let Some(mut texture) = texture_slot.take() {
texture.decrement_ref_count();
if texture.is_unreferenced() {
textures.free_list.push(id.index as usize);
lock.tiles_by_key.remove(key);
} else {
*texture_slot = Some(texture);
}
}
}
}
impl MetalAtlasState {
@@ -86,20 +118,24 @@ impl MetalAtlasState {
size: Size<DevicePixels>,
texture_kind: AtlasTextureKind,
) -> Option<AtlasTile> {
let textures = match texture_kind {
AtlasTextureKind::Monochrome => &mut self.monochrome_textures,
AtlasTextureKind::Polychrome => &mut self.polychrome_textures,
AtlasTextureKind::Path => &mut self.path_textures,
};
{
let textures = match texture_kind {
AtlasTextureKind::Monochrome => &mut self.monochrome_textures,
AtlasTextureKind::Polychrome => &mut self.polychrome_textures,
AtlasTextureKind::Path => &mut self.path_textures,
};
textures
.iter_mut()
.rev()
.find_map(|texture| texture.allocate(size))
.or_else(|| {
let texture = self.push_texture(size, texture_kind);
texture.allocate(size)
})
if let Some(tile) = textures
.iter_mut()
.rev()
.find_map(|texture| texture.allocate(size))
{
return Some(tile);
}
}
let texture = self.push_texture(size, texture_kind);
texture.allocate(size)
}
fn push_texture(
@@ -140,21 +176,31 @@ impl MetalAtlasState {
texture_descriptor.set_usage(usage);
let metal_texture = self.device.new_texture(&texture_descriptor);
let textures = match kind {
let texture_list = match kind {
AtlasTextureKind::Monochrome => &mut self.monochrome_textures,
AtlasTextureKind::Polychrome => &mut self.polychrome_textures,
AtlasTextureKind::Path => &mut self.path_textures,
};
let index = texture_list.free_list.pop();
let atlas_texture = MetalAtlasTexture {
id: AtlasTextureId {
index: textures.len() as u32,
index: index.unwrap_or(texture_list.textures.len()) as u32,
kind,
},
allocator: etagere::BucketedAtlasAllocator::new(size.into()),
metal_texture: AssertSend(metal_texture),
live_atlas_keys: 0,
};
textures.push(atlas_texture);
textures.last_mut().unwrap()
if let Some(ix) = index {
texture_list.textures[ix] = Some(atlas_texture);
texture_list.textures.get_mut(ix).unwrap().as_mut().unwrap()
} else {
texture_list.textures.push(Some(atlas_texture));
texture_list.textures.last_mut().unwrap().as_mut().unwrap()
}
}
fn texture(&self, id: AtlasTextureId) -> &MetalAtlasTexture {
@@ -163,7 +209,7 @@ impl MetalAtlasState {
crate::AtlasTextureKind::Polychrome => &self.polychrome_textures,
crate::AtlasTextureKind::Path => &self.path_textures,
};
&textures[id.index as usize]
textures[id.index as usize].as_ref().unwrap()
}
}
@@ -171,6 +217,7 @@ struct MetalAtlasTexture {
id: AtlasTextureId,
allocator: BucketedAtlasAllocator,
metal_texture: AssertSend<metal::Texture>,
live_atlas_keys: u32,
}
impl MetalAtlasTexture {
@@ -189,6 +236,7 @@ impl MetalAtlasTexture {
},
padding: 0,
};
self.live_atlas_keys += 1;
Some(tile)
}
@@ -215,6 +263,14 @@ impl MetalAtlasTexture {
_ => unimplemented!(),
}
}
fn decrement_ref_count(&mut self) {
self.live_atlas_keys -= 1;
}
fn is_unreferenced(&mut self) -> bool {
self.live_atlas_keys == 0
}
}
impl From<Size<DevicePixels>> for etagere::Size {

View File

@@ -294,10 +294,8 @@ impl MacPlatform {
Some(crate::OsAction::Copy) => selector("copy:"),
Some(crate::OsAction::Paste) => selector("paste:"),
Some(crate::OsAction::SelectAll) => selector("selectAll:"),
// "undo:" and "redo:" are always disabled in our case, as
// we don't have a NSTextView/NSTextField to enable them on.
Some(crate::OsAction::Undo) => selector("handleGPUIMenuItem:"),
Some(crate::OsAction::Redo) => selector("handleGPUIMenuItem:"),
Some(crate::OsAction::Undo) => selector("undo:"),
Some(crate::OsAction::Redo) => selector("redo:"),
None => selector("handleGPUIMenuItem:"),
};
@@ -393,7 +391,7 @@ impl MacPlatform {
}
}
fn os_version() -> Result<SemanticVersion> {
fn os_version(&self) -> Result<SemanticVersion> {
unsafe {
let process_info = NSProcessInfo::processInfo(nil);
let version = process_info.operatingSystemVersion();
@@ -587,7 +585,7 @@ impl Platform for MacPlatform {
// API only available post Monterey
// https://developer.apple.com/documentation/appkit/nsworkspace/3753004-setdefaultapplicationaturl
let (done_tx, done_rx) = oneshot::channel();
if Self::os_version().ok() < Some(SemanticVersion::new(12, 0, 0)) {
if self.os_version().ok() < Some(SemanticVersion::new(12, 0, 0)) {
return Task::ready(Err(anyhow!(
"macOS 12.0 or later is required to register URL schemes"
)));
@@ -697,36 +695,7 @@ impl Platform for MacPlatform {
if response == NSModalResponse::NSModalResponseOk {
let url = panel.URL();
if url.isFileURL() == YES {
result = ns_url_to_path(panel.URL()).ok().map(|mut result| {
let Some(filename) = result.file_name() else {
return result;
};
let chunks = filename
.as_bytes()
.split(|&b| b == b'.')
.collect::<Vec<_>>();
// https://github.com/zed-industries/zed/issues/16969
// Workaround a bug in macOS Sequoia that adds an extra file-extension
// sometimes. e.g. `a.sql` becomes `a.sql.s` or `a.txtx` becomes `a.txtx.txt`
//
// This is conditional on OS version because I'd like to get rid of it, so that
// you can manually create a file called `a.sql.s`. That said it seems better
// to break that use-case than breaking `a.sql`.
if chunks.len() == 3 && chunks[1].starts_with(chunks[2]) {
if Self::os_version()
.is_ok_and(|v| v >= SemanticVersion::new(15, 0, 0))
{
let new_filename = OsStr::from_bytes(
&filename.as_bytes()
[..chunks[0].len() + 1 + chunks[1].len()],
)
.to_owned();
result.set_file_name(&new_filename);
}
}
return result;
})
result = ns_url_to_path(panel.URL()).ok()
}
}

View File

@@ -339,4 +339,9 @@ impl PlatformAtlas for TestAtlas {
Ok(Some(state.tiles[key].clone()))
}
fn remove(&self, key: &AtlasKey) {
let mut state = self.0.lock();
state.tiles.remove(key);
}
}

View File

@@ -2689,6 +2689,20 @@ impl<'a> WindowContext<'a> {
});
}
/// Removes an image from the sprite atlas.
pub fn drop_image(&mut self, data: Arc<RenderImage>) -> Result<()> {
for frame_index in 0..data.frame_count() {
let params = RenderImageParams {
image_id: data.id,
frame_index,
};
self.window.sprite_atlas.remove(&params.clone().into());
}
Ok(())
}
#[must_use]
/// Add a node to the layout tree for the current frame. Takes the `Style` of the element for which
/// layout is being requested, along with the layout ids of any children. This method is called during

View File

@@ -15,10 +15,9 @@ doctest = false
[dependencies]
anyhow.workspace = true
db.workspace = true
file_icons.workspace = true
gpui.workspace = true
project.workspace = true
settings.workspace = true
theme.workspace = true
file_icons.workspace = true
ui.workspace = true
settings.workspace = true
workspace.workspace = true
project.workspace = true

View File

@@ -6,7 +6,6 @@ use gpui::{
WindowContext,
};
use persistence::IMAGE_VIEWER;
use theme::Theme;
use ui::prelude::*;
use file_icons::FileIcons;
@@ -14,8 +13,8 @@ use project::{Project, ProjectEntryId, ProjectPath};
use settings::Settings;
use std::{ffi::OsStr, path::PathBuf};
use workspace::{
item::{BreadcrumbText, Item, ProjectItem, SerializableItem, TabContentParams},
ItemId, ItemSettings, Pane, ToolbarItemLocation, Workspace, WorkspaceId,
item::{Item, ProjectItem, SerializableItem, TabContentParams},
ItemId, ItemSettings, Pane, Workspace, WorkspaceId,
};
const IMAGE_VIEWER_KIND: &str = "ImageView";
@@ -24,7 +23,6 @@ pub struct ImageItem {
id: ProjectEntryId,
path: PathBuf,
project_path: ProjectPath,
project: Model<Project>,
}
impl project::Item for ImageItem {
@@ -58,7 +56,6 @@ impl project::Item for ImageItem {
.id;
cx.new_model(|_| ImageItem {
project,
path: abs_path,
project_path: path,
id,
@@ -121,19 +118,6 @@ impl Item for ImageView {
.map(Icon::from_path)
}
fn breadcrumb_location(&self) -> ToolbarItemLocation {
ToolbarItemLocation::PrimaryLeft
}
fn breadcrumbs(&self, _theme: &Theme, cx: &AppContext) -> Option<Vec<BreadcrumbText>> {
let text = breadcrumbs_text_for_image(self.image.read(cx), cx);
Some(vec![BreadcrumbText {
text,
highlights: None,
font: None,
}])
}
fn clone_on_split(
&self,
_workspace_id: Option<WorkspaceId>,
@@ -149,25 +133,6 @@ impl Item for ImageView {
}
}
fn breadcrumbs_text_for_image(image: &ImageItem, cx: &AppContext) -> String {
let path = &image.project_path.path;
let project = image.project.read(cx);
if project.visible_worktrees(cx).count() <= 1 {
return path.to_string_lossy().to_string();
}
project
.worktree_for_entry(image.id, cx)
.map(|worktree| {
PathBuf::from(worktree.read(cx).root_name())
.join(path)
.to_string_lossy()
.to_string()
})
.unwrap_or_else(|| path.to_string_lossy().to_string())
}
impl SerializableItem for ImageView {
fn serialized_item_kind() -> &'static str {
IMAGE_VIEWER_KIND
@@ -210,7 +175,6 @@ impl SerializableItem for ImageView {
id,
path: image_path,
project_path,
project,
});
Ok(cx.new_view(|cx| ImageView {

View File

@@ -46,7 +46,6 @@ use std::{
future::Future,
iter::{self, Iterator, Peekable},
mem,
num::NonZeroU32,
ops::{Deref, DerefMut, Range},
path::{Path, PathBuf},
str,
@@ -4326,13 +4325,6 @@ impl IndentSize {
}
self
}
pub fn len_with_expanded_tabs(&self, tab_size: NonZeroU32) -> usize {
match self.kind {
IndentKind::Space => self.len as usize,
IndentKind::Tab => self.len as usize * tab_size.get() as usize,
}
}
}
#[cfg(any(test, feature = "test-support"))]

View File

@@ -2120,8 +2120,8 @@ fn test_language_scope_at_with_javascript(cx: &mut AppContext) {
},
],
disabled_scopes_by_bracket_ix: vec![
Vec::new(), //
vec!["string".into(), "comment".into()], // single quotes disabled
Vec::new(), //
vec!["string".into()],
],
},
overrides: [(
@@ -2142,7 +2142,6 @@ fn test_language_scope_at_with_javascript(cx: &mut AppContext) {
r#"
(jsx_element) @element
(string) @string
(comment) @comment.inclusive
[
(jsx_opening_element)
(jsx_closing_element)
@@ -2156,7 +2155,7 @@ fn test_language_scope_at_with_javascript(cx: &mut AppContext) {
a["b"] = <C d="e">
<F></F>
{ g() }
</C>; // a comment
</C>;
"#
.unindent();
@@ -2171,14 +2170,6 @@ fn test_language_scope_at_with_javascript(cx: &mut AppContext) {
&[true, true]
);
let comment_config = snapshot
.language_scope_at(text.find("comment").unwrap() + "comment".len())
.unwrap();
assert_eq!(
comment_config.brackets().map(|e| e.1).collect::<Vec<_>>(),
&[true, false]
);
let string_config = snapshot
.language_scope_at(text.find("b\"").unwrap())
.unwrap();

View File

@@ -591,9 +591,6 @@ pub struct LanguageConfig {
/// the indentation level for a new line.
#[serde(default = "auto_indent_using_last_non_empty_line_default")]
pub auto_indent_using_last_non_empty_line: bool,
// Whether indentation of pasted content should be adjusted based on the context.
#[serde(default)]
pub auto_indent_on_paste: Option<bool>,
/// A regex that is used to determine whether the indentation level should be
/// increased in the following line.
#[serde(default, deserialize_with = "deserialize_regex")]
@@ -721,7 +718,6 @@ impl Default for LanguageConfig {
matcher: LanguageMatcher::default(),
brackets: Default::default(),
auto_indent_using_last_non_empty_line: auto_indent_using_last_non_empty_line_default(),
auto_indent_on_paste: None,
increase_indent_pattern: Default::default(),
decrease_indent_pattern: Default::default(),
autoclose_before: Default::default(),
@@ -945,14 +941,7 @@ struct RunnableConfig {
struct OverrideConfig {
query: Query,
values: HashMap<u32, OverrideEntry>,
}
#[derive(Debug)]
struct OverrideEntry {
name: String,
range_is_inclusive: bool,
value: LanguageConfigOverride,
values: HashMap<u32, (String, LanguageConfigOverride)>,
}
#[derive(Default, Clone)]
@@ -1272,66 +1261,58 @@ impl Language {
};
let mut override_configs_by_id = HashMap::default();
for (ix, mut name) in query.capture_names().iter().copied().enumerate() {
let mut range_is_inclusive = false;
if name.starts_with('_') {
continue;
}
if let Some(prefix) = name.strip_suffix(".inclusive") {
name = prefix;
range_is_inclusive = true;
}
let value = self.config.overrides.get(name).cloned().unwrap_or_default();
for server_name in &value.opt_into_language_servers {
if !self
.config
.scope_opt_in_language_servers
.contains(server_name)
{
util::debug_panic!("Server {server_name:?} has been opted-in by scope {name:?} but has not been marked as an opt-in server");
for (ix, name) in query.capture_names().iter().enumerate() {
if !name.starts_with('_') {
let value = self.config.overrides.remove(*name).unwrap_or_default();
for server_name in &value.opt_into_language_servers {
if !self
.config
.scope_opt_in_language_servers
.contains(server_name)
{
util::debug_panic!("Server {server_name:?} has been opted-in by scope {name:?} but has not been marked as an opt-in server");
}
}
}
override_configs_by_id.insert(
ix as u32,
OverrideEntry {
name: name.to_string(),
range_is_inclusive,
value,
},
);
override_configs_by_id.insert(ix as u32, (name.to_string(), value));
}
}
let referenced_override_names = self.config.overrides.keys().chain(
self.config
.brackets
.disabled_scopes_by_bracket_ix
.iter()
.flatten(),
);
if !self.config.overrides.is_empty() {
let keys = self.config.overrides.keys().collect::<Vec<_>>();
Err(anyhow!(
"language {:?} has overrides in config not in query: {keys:?}",
self.config.name
))?;
}
for referenced_name in referenced_override_names {
for disabled_scope_name in self
.config
.brackets
.disabled_scopes_by_bracket_ix
.iter()
.flatten()
{
if !override_configs_by_id
.values()
.any(|entry| entry.name == *referenced_name)
.any(|(scope_name, _)| scope_name == disabled_scope_name)
{
Err(anyhow!(
"language {:?} has overrides in config not in query: {referenced_name:?}",
"language {:?} has overrides in config not in query: {disabled_scope_name:?}",
self.config.name
))?;
}
}
for entry in override_configs_by_id.values_mut() {
entry.value.disabled_bracket_ixs = self
for (name, override_config) in override_configs_by_id.values_mut() {
override_config.disabled_bracket_ixs = self
.config
.brackets
.disabled_scopes_by_bracket_ix
.iter()
.enumerate()
.filter_map(|(ix, disabled_scope_names)| {
if disabled_scope_names.contains(&entry.name) {
if disabled_scope_names.contains(name) {
Some(ix as u16)
} else {
None
@@ -1549,14 +1530,14 @@ impl LanguageScope {
let id = self.override_id?;
let grammar = self.language.grammar.as_ref()?;
let override_config = grammar.override_config.as_ref()?;
override_config.values.get(&id).map(|e| e.name.as_str())
override_config.values.get(&id).map(|e| e.0.as_str())
}
fn config_override(&self) -> Option<&LanguageConfigOverride> {
let id = self.override_id?;
let grammar = self.language.grammar.as_ref()?;
let override_config = grammar.override_config.as_ref()?;
override_config.values.get(&id).map(|e| &e.value)
override_config.values.get(&id).map(|e| &e.1)
}
}

View File

@@ -965,7 +965,6 @@ impl LanguageRegistryState {
tab_size: language.config.tab_size,
hard_tabs: language.config.hard_tabs,
soft_wrap: language.config.soft_wrap,
auto_indent_on_paste: language.config.auto_indent_on_paste,
..Default::default()
}
.clone(),

View File

@@ -5,7 +5,7 @@ use anyhow::Result;
use collections::{HashMap, HashSet};
use core::slice;
use ec4rs::{
property::{FinalNewline, IndentSize, IndentStyle, TabWidth, TrimTrailingWs},
property::{FinalNewline, IndentSize, IndentStyle, MaxLineLen, TabWidth, TrimTrailingWs},
Properties as EditorconfigProperties,
};
use globset::{Glob, GlobMatcher, GlobSet, GlobSetBuilder};
@@ -125,8 +125,6 @@ pub struct LanguageSettings {
/// Whether to use additional LSP queries to format (and amend) the code after
/// every "trigger" symbol input, defined by LSP server capabilities.
pub use_on_type_format: bool,
/// Whether indentation of pasted content should be adjusted based on the context.
pub auto_indent_on_paste: bool,
// Controls how the editor handles the autoclosed characters.
pub always_treat_brackets_as_autoclosed: bool,
/// Which code actions to run on save
@@ -362,10 +360,6 @@ pub struct LanguageSettingsContent {
///
/// Default: true
pub linked_edits: Option<bool>,
/// Whether indentation of pasted content should be adjusted based on the context.
///
/// Default: true
pub auto_indent_on_paste: Option<bool>,
/// Task configuration for this language.
///
/// Default: {}
@@ -876,6 +870,10 @@ impl AllLanguageSettings {
}
fn merge_with_editorconfig(settings: &mut LanguageSettings, cfg: &EditorconfigProperties) {
let max_line_length = cfg.get::<MaxLineLen>().ok().and_then(|v| match v {
MaxLineLen::Value(u) => Some(u as u32),
MaxLineLen::Off => None,
});
let tab_size = cfg.get::<IndentSize>().ok().and_then(|v| match v {
IndentSize::Value(u) => NonZeroU32::new(u as u32),
IndentSize::UseTabWidth => cfg.get::<TabWidth>().ok().and_then(|w| match w {
@@ -898,6 +896,13 @@ fn merge_with_editorconfig(settings: &mut LanguageSettings, cfg: &EditorconfigPr
TrimTrailingWs::Value(b) => b,
})
.ok();
let preferred_line_length = max_line_length;
let soft_wrap = if max_line_length.is_some() {
Some(SoftWrap::PreferredLineLength)
} else {
None
};
fn merge<T>(target: &mut T, value: Option<T>) {
if let Some(value) = value {
*target = value;
@@ -913,6 +918,8 @@ fn merge_with_editorconfig(settings: &mut LanguageSettings, cfg: &EditorconfigPr
&mut settings.ensure_final_newline_on_save,
ensure_final_newline_on_save,
);
merge(&mut settings.preferred_line_length, preferred_line_length);
merge(&mut settings.soft_wrap, soft_wrap);
}
/// The kind of an inlay hint.
@@ -1125,7 +1132,6 @@ fn merge_settings(settings: &mut LanguageSettings, src: &LanguageSettingsContent
merge(&mut settings.use_autoclose, src.use_autoclose);
merge(&mut settings.use_auto_surround, src.use_auto_surround);
merge(&mut settings.use_on_type_format, src.use_on_type_format);
merge(&mut settings.auto_indent_on_paste, src.auto_indent_on_paste);
merge(
&mut settings.always_treat_brackets_as_autoclosed,
src.always_treat_brackets_as_autoclosed,

View File

@@ -1,7 +1,9 @@
use crate::{BufferSnapshot, Point, ToPoint};
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{BackgroundExecutor, HighlightStyle};
use gpui::{relative, AppContext, BackgroundExecutor, HighlightStyle, StyledText, TextStyle};
use settings::Settings;
use std::ops::Range;
use theme::{color_alpha, ActiveTheme, ThemeSettings};
/// An outline of all the symbols contained in a buffer.
#[derive(Debug)]
@@ -191,6 +193,42 @@ impl<T> Outline<T> {
}
}
pub fn render_item<T>(
outline_item: &OutlineItem<T>,
match_ranges: impl IntoIterator<Item = Range<usize>>,
cx: &AppContext,
) -> StyledText {
let highlight_style = HighlightStyle {
background_color: Some(color_alpha(cx.theme().colors().text_accent, 0.3)),
..Default::default()
};
let custom_highlights = match_ranges
.into_iter()
.map(|range| (range, highlight_style));
let settings = ThemeSettings::get_global(cx);
// TODO: We probably shouldn't need to build a whole new text style here
// but I'm not sure how to get the current one and modify it.
// Before this change TextStyle::default() was used here, which was giving us the wrong font and text color.
let text_style = TextStyle {
color: cx.theme().colors().text,
font_family: settings.buffer_font.family.clone(),
font_features: settings.buffer_font.features.clone(),
font_fallbacks: settings.buffer_font.fallbacks.clone(),
font_size: settings.buffer_font_size(cx).into(),
font_weight: settings.buffer_font.weight,
line_height: relative(1.),
..Default::default()
};
let highlights = gpui::combine_highlights(
custom_highlights,
outline_item.highlight_ranges.iter().cloned(),
);
StyledText::new(outline_item.text.clone()).with_highlights(&text_style, highlights)
}
#[cfg(test)]
mod tests {
use super::*;

View File

@@ -1520,24 +1520,18 @@ impl<'a> SyntaxLayer<'a> {
let config = self.language.grammar.as_ref()?.override_config.as_ref()?;
let mut query_cursor = QueryCursorHandle::new();
query_cursor.set_byte_range(offset.saturating_sub(1)..offset.saturating_add(1));
query_cursor.set_byte_range(offset..offset);
let mut smallest_match: Option<(u32, Range<usize>)> = None;
for mat in query_cursor.matches(&config.query, self.node(), text) {
for capture in mat.captures {
let Some(override_entry) = config.values.get(&capture.index) else {
if !config.values.contains_key(&capture.index) {
continue;
};
}
let range = capture.node.byte_range();
if override_entry.range_is_inclusive {
if offset < range.start || offset > range.end {
continue;
}
} else {
if offset <= range.start || offset >= range.end {
continue;
}
if offset <= range.start || offset >= range.end {
continue;
}
if let Some((_, smallest_range)) = &smallest_match {

View File

@@ -7,7 +7,6 @@
use std::{path::PathBuf, sync::Arc};
use async_trait::async_trait;
use collections::HashMap;
use gpui::{AsyncAppContext, SharedString};
use settings::WorktreeId;
@@ -24,11 +23,7 @@ pub struct Toolchain {
#[async_trait(?Send)]
pub trait ToolchainLister: Send + Sync {
async fn list(
&self,
worktree_root: PathBuf,
project_env: Option<HashMap<String, String>>,
) -> ToolchainList;
async fn list(&self, _: PathBuf) -> ToolchainList;
}
#[async_trait(?Send)]

View File

@@ -30,7 +30,6 @@ use crate::{
};
use crate::{LanguageModelCompletionEvent, LanguageModelProviderState};
use super::anthropic::count_anthropic_tokens;
use super::open_ai::count_open_ai_tokens;
const PROVIDER_ID: &str = "copilot_chat";
@@ -180,19 +179,13 @@ impl LanguageModel for CopilotChatLanguageModel {
request: LanguageModelRequest,
cx: &AppContext,
) -> BoxFuture<'static, Result<usize>> {
match self.model {
CopilotChatModel::Claude3_5Sonnet => count_anthropic_tokens(request, cx),
_ => {
let model = match self.model {
CopilotChatModel::Gpt4o => open_ai::Model::FourOmni,
CopilotChatModel::Gpt4 => open_ai::Model::Four,
CopilotChatModel::Gpt3_5Turbo => open_ai::Model::ThreePointFiveTurbo,
CopilotChatModel::O1Preview | CopilotChatModel::O1Mini => open_ai::Model::Four,
CopilotChatModel::Claude3_5Sonnet => unreachable!(),
};
count_open_ai_tokens(request, model, cx)
}
}
let model = match self.model {
CopilotChatModel::Gpt4o => open_ai::Model::FourOmni,
CopilotChatModel::Gpt4 => open_ai::Model::Four,
CopilotChatModel::Gpt3_5Turbo => open_ai::Model::ThreePointFiveTurbo,
};
count_open_ai_tokens(request, model, cx)
}
fn stream_completion(
@@ -216,8 +209,7 @@ impl LanguageModel for CopilotChatLanguageModel {
}
}
let copilot_request = self.to_copilot_chat_request(request);
let is_streaming = copilot_request.stream;
let request = self.to_copilot_chat_request(request);
let Ok(low_speed_timeout) = cx.update(|cx| {
AllLanguageModelSettings::get_global(cx)
.copilot_chat
@@ -228,31 +220,16 @@ impl LanguageModel for CopilotChatLanguageModel {
let request_limiter = self.request_limiter.clone();
let future = cx.spawn(|cx| async move {
let response = CopilotChat::stream_completion(copilot_request, low_speed_timeout, cx);
let response = CopilotChat::stream_completion(request, low_speed_timeout, cx);
request_limiter.stream(async move {
let response = response.await?;
let stream = response
.filter_map(move |response| async move {
.filter_map(|response| async move {
match response {
Ok(result) => {
let choice = result.choices.first();
match choice {
Some(choice) if !is_streaming => {
match &choice.message {
Some(msg) => Some(Ok(msg.content.clone().unwrap_or_default())),
None => Some(Err(anyhow::anyhow!(
"The Copilot Chat API returned a response with no message content"
))),
}
},
Some(choice) => {
match &choice.delta {
Some(delta) => Some(Ok(delta.content.clone().unwrap_or_default())),
None => Some(Err(anyhow::anyhow!(
"The Copilot Chat API returned a response with no delta content"
))),
}
},
Some(choice) => Some(Ok(choice.delta.content.clone().unwrap_or_default())),
None => Some(Err(anyhow::anyhow!(
"The Copilot Chat API returned a response with no choices, but hadn't finished the message yet. Please try again."
))),

View File

@@ -47,7 +47,6 @@ lsp.workspace = true
node_runtime.workspace = true
paths.workspace = true
pet.workspace = true
pet-fs.workspace = true
pet-core.workspace = true
pet-conda.workspace = true
pet-poetry.workspace = true

View File

@@ -7,7 +7,5 @@ first_line_pattern = '^#!.*\b(?:ash|bash|dash|sh|zsh)\b'
brackets = [
{ start = "[", end = "]", close = true, newline = false },
{ start = "(", end = ")", close = true, newline = false },
{ start = "{", end = "}", close = true, newline = false },
{ start = "\"", end = "\"", close = true, newline = false, not_in = ["comment", "string"] },
{ start = "'", end = "'", close = true, newline = false, not_in = ["string", "comment"] },
]

View File

@@ -1,2 +1,2 @@
(comment) @comment.inclusive
(comment) @comment
(string_literal) @string

View File

@@ -1,2 +1,2 @@
(comment) @comment.inclusive
(comment) @comment
(string_literal) @string

View File

@@ -1,2 +1,2 @@
(comment) @comment.inclusive
(comment) @comment
(string_value) @string

View File

@@ -408,8 +408,6 @@ fn adjust_runs(
pub(crate) struct GoContextProvider;
const GO_PACKAGE_TASK_VARIABLE: VariableName = VariableName::Custom(Cow::Borrowed("GO_PACKAGE"));
const GO_MODULE_ROOT_TASK_VARIABLE: VariableName =
VariableName::Custom(Cow::Borrowed("GO_MODULE_ROOT"));
const GO_SUBTEST_NAME_TASK_VARIABLE: VariableName =
VariableName::Custom(Cow::Borrowed("GO_SUBTEST_NAME"));
@@ -449,33 +447,15 @@ impl ContextProvider for GoContextProvider {
(GO_PACKAGE_TASK_VARIABLE.clone(), package_name.to_string())
});
let go_module_root_variable = local_abs_path
.as_deref()
.and_then(|local_abs_path| local_abs_path.parent())
.map(|buffer_dir| {
// Walk dirtree up until getting the first go.mod file
let module_dir = buffer_dir
.ancestors()
.find(|dir| dir.join("go.mod").is_file())
.map(|dir| dir.to_string_lossy().to_string())
.unwrap_or_else(|| ".".to_string());
(GO_MODULE_ROOT_TASK_VARIABLE.clone(), module_dir)
});
let _subtest_name = variables.get(&VariableName::Custom(Cow::Borrowed("_subtest_name")));
let go_subtest_variable = extract_subtest_name(_subtest_name.unwrap_or(""))
.map(|subtest_name| (GO_SUBTEST_NAME_TASK_VARIABLE.clone(), subtest_name));
Ok(TaskVariables::from_iter(
[
go_package_variable,
go_subtest_variable,
go_module_root_variable,
]
.into_iter()
.flatten(),
[go_package_variable, go_subtest_variable]
.into_iter()
.flatten(),
))
}
@@ -489,7 +469,6 @@ impl ContextProvider for GoContextProvider {
} else {
Some("$ZED_DIRNAME".to_string())
};
let module_cwd = Some(GO_MODULE_ROOT_TASK_VARIABLE.template_value());
Some(TaskTemplates(vec![
TaskTemplate {
@@ -519,7 +498,7 @@ impl ContextProvider for GoContextProvider {
label: "go test ./...".into(),
command: "go".into(),
args: vec!["test".into(), "./...".into()],
cwd: module_cwd.clone(),
cwd: package_cwd.clone(),
..TaskTemplate::default()
},
TaskTemplate {
@@ -570,21 +549,6 @@ impl ContextProvider for GoContextProvider {
tags: vec!["go-main".to_owned()],
..TaskTemplate::default()
},
TaskTemplate {
label: format!("go generate {}", GO_PACKAGE_TASK_VARIABLE.template_value()),
command: "go".into(),
args: vec!["generate".into()],
cwd: package_cwd.clone(),
tags: vec!["go-generate".to_owned()],
..TaskTemplate::default()
},
TaskTemplate {
label: "go generate ./...".into(),
command: "go".into(),
args: vec!["generate".into(), "./...".into()],
cwd: module_cwd.clone(),
..TaskTemplate::default()
},
]))
}
}

View File

@@ -1,4 +1,4 @@
(comment) @comment.inclusive
(comment) @comment
[
(interpreted_string_literal)
(raw_string_literal)

View File

@@ -7,13 +7,6 @@
(#set! tag go-test)
)
; `go:generate` comments
(
((comment) @_comment @run
(#match? @_comment "^//go:generate"))
(#set! tag go-generate)
)
; `t.Run`
(
(

View File

@@ -1,4 +1,4 @@
(comment) @comment.inclusive
(comment) @comment
[
(string)

View File

@@ -12,7 +12,5 @@ brackets = [
{ start = "`", end = "`", close = false, newline = false },
]
auto_indent_on_paste = false
auto_indent_using_last_non_empty_line = false
tab_size = 2
prettier_parser_name = "markdown"

View File

@@ -11,13 +11,11 @@ use language::ToolchainLister;
use language::{ContextProvider, LanguageServerName, LspAdapter, LspAdapterDelegate};
use lsp::LanguageServerBinary;
use node_runtime::NodeRuntime;
use pet_core::os_environment::Environment;
use pet_core::python_environment::PythonEnvironmentKind;
use pet_core::Configuration;
use project::lsp_store::language_server_settings;
use serde_json::Value;
use std::sync::Mutex;
use std::{
any::Any,
borrow::Cow,
@@ -382,13 +380,8 @@ fn env_priority(kind: Option<PythonEnvironmentKind>) -> usize {
#[async_trait(?Send)]
impl ToolchainLister for PythonToolchainProvider {
async fn list(
&self,
worktree_root: PathBuf,
project_env: Option<HashMap<String, String>>,
) -> ToolchainList {
let env = project_env.unwrap_or_default();
let environment = EnvironmentApi::from_env(&env);
async fn list(&self, worktree_root: PathBuf) -> ToolchainList {
let environment = pet_core::os_environment::EnvironmentApi::new();
let locators = pet::locators::create_locators(
Arc::new(pet_conda::Conda::from(&environment)),
Arc::new(pet_poetry::Poetry::from(&environment)),
@@ -434,78 +427,6 @@ impl ToolchainLister for PythonToolchainProvider {
}
}
pub struct EnvironmentApi<'a> {
global_search_locations: Arc<Mutex<Vec<PathBuf>>>,
project_env: &'a HashMap<String, String>,
pet_env: pet_core::os_environment::EnvironmentApi,
}
impl<'a> EnvironmentApi<'a> {
pub fn from_env(project_env: &'a HashMap<String, String>) -> Self {
let paths = project_env
.get("PATH")
.map(|p| std::env::split_paths(p).collect())
.unwrap_or_default();
EnvironmentApi {
global_search_locations: Arc::new(Mutex::new(paths)),
project_env,
pet_env: pet_core::os_environment::EnvironmentApi::new(),
}
}
fn user_home(&self) -> Option<PathBuf> {
self.project_env
.get("HOME")
.or_else(|| self.project_env.get("USERPROFILE"))
.map(|home| pet_fs::path::norm_case(PathBuf::from(home)))
.or_else(|| self.pet_env.get_user_home())
}
}
impl<'a> pet_core::os_environment::Environment for EnvironmentApi<'a> {
fn get_user_home(&self) -> Option<PathBuf> {
self.user_home()
}
fn get_root(&self) -> Option<PathBuf> {
None
}
fn get_env_var(&self, key: String) -> Option<String> {
self.project_env
.get(&key)
.cloned()
.or_else(|| self.pet_env.get_env_var(key))
}
fn get_know_global_search_locations(&self) -> Vec<PathBuf> {
if self.global_search_locations.lock().unwrap().is_empty() {
let mut paths =
std::env::split_paths(&self.get_env_var("PATH".to_string()).unwrap_or_default())
.collect::<Vec<PathBuf>>();
log::trace!("Env PATH: {:?}", paths);
for p in self.pet_env.get_know_global_search_locations() {
if !paths.contains(&p) {
paths.push(p);
}
}
let mut paths = paths
.into_iter()
.filter(|p| p.exists())
.collect::<Vec<PathBuf>>();
self.global_search_locations
.lock()
.unwrap()
.append(&mut paths);
}
self.global_search_locations.lock().unwrap().clone()
}
}
#[cfg(test)]
mod tests {
use gpui::{BorrowAppContext, Context, ModelContext, TestAppContext};

View File

@@ -1,2 +1,2 @@
(comment) @comment.inclusive
(comment) @comment
(string) @string

View File

@@ -5,4 +5,4 @@
[
(line_comment)
(block_comment)
] @comment.inclusive
] @comment

View File

@@ -1,4 +1,4 @@
(comment) @comment.inclusive
(comment) @comment
[
(string)

View File

@@ -1,2 +1,2 @@
(comment) @comment.inclusive
(comment) @comment
(string) @string

View File

@@ -24,16 +24,11 @@ pub struct VtslsLspAdapter {
}
impl VtslsLspAdapter {
const PACKAGE_NAME: &'static str = "@vtsls/language-server";
const SERVER_PATH: &'static str = "node_modules/@vtsls/language-server/bin/vtsls.js";
const TYPESCRIPT_PACKAGE_NAME: &'static str = "typescript";
const TYPESCRIPT_TSDK_PATH: &'static str = "node_modules/typescript/lib";
pub fn new(node: NodeRuntime) -> Self {
VtslsLspAdapter { node }
}
async fn tsdk_path(adapter: &Arc<dyn LspAdapterDelegate>) -> &'static str {
let is_yarn = adapter
.read_text_file(PathBuf::from(".yarn/sdks/typescript/lib/typescript.js"))
@@ -43,7 +38,7 @@ impl VtslsLspAdapter {
if is_yarn {
".yarn/sdks/typescript/lib"
} else {
Self::TYPESCRIPT_TSDK_PATH
"node_modules/typescript/lib"
}
}
}
@@ -54,7 +49,6 @@ struct TypeScriptVersions {
}
const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("vtsls");
#[async_trait(?Send)]
impl LspAdapter for VtslsLspAdapter {
fn name(&self) -> LanguageServerName {
@@ -96,41 +90,32 @@ impl LspAdapter for VtslsLspAdapter {
) -> Result<LanguageServerBinary> {
let latest_version = latest_version.downcast::<TypeScriptVersions>().unwrap();
let server_path = container_dir.join(Self::SERVER_PATH);
let package_name = "typescript";
let mut packages_to_install = Vec::new();
if self
let should_install_language_server = self
.node
.should_install_npm_package(
Self::PACKAGE_NAME,
package_name,
&server_path,
&container_dir,
&latest_version.server_version,
)
.await
{
packages_to_install.push((Self::PACKAGE_NAME, latest_version.server_version.as_str()));
}
if self
.node
.should_install_npm_package(
Self::TYPESCRIPT_PACKAGE_NAME,
&container_dir.join(Self::TYPESCRIPT_TSDK_PATH),
&container_dir,
&latest_version.typescript_version,
)
.await
{
packages_to_install.push((
Self::TYPESCRIPT_PACKAGE_NAME,
latest_version.typescript_version.as_str(),
));
}
)
.await;
self.node
.npm_install_packages(&container_dir, &packages_to_install)
.await?;
if should_install_language_server {
self.node
.npm_install_packages(
&container_dir,
&[
(package_name, latest_version.typescript_version.as_str()),
(
"@vtsls/language-server",
latest_version.server_version.as_str(),
),
],
)
.await?;
}
Ok(LanguageServerBinary {
path: self.node.binary_path().await?,

View File

@@ -10,7 +10,6 @@ brackets = [
{ start = "'", end = "'", close = true, newline = false, not_in = ["string"] },
]
auto_indent_on_paste = false
auto_indent_using_last_non_empty_line = false
increase_indent_pattern = ":\\s*[|>]?\\s*$"
prettier_parser_name = "yaml"

View File

@@ -297,7 +297,7 @@ impl MarkdownPreviewView {
let subscription = cx.subscribe(&editor, |this, editor, event: &EditorEvent, cx| {
match event {
EditorEvent::Edited { .. } | EditorEvent::DirtyChanged => {
EditorEvent::Edited { .. } => {
this.parse_markdown_from_active_editor(true, cx);
}
EditorEvent::SelectionsChanged { .. } => {

View File

@@ -5,8 +5,8 @@ use crate::markdown_elements::{
ParsedMarkdownTableRow, ParsedMarkdownText,
};
use gpui::{
div, px, rems, AbsoluteLength, AnyElement, ClipboardItem, DefiniteLength, Div, Element,
ElementId, HighlightStyle, Hsla, InteractiveText, IntoElement, Keystroke, Length, Modifiers,
div, px, rems, AbsoluteLength, AnyElement, DefiniteLength, Div, Element, ElementId,
HighlightStyle, Hsla, InteractiveText, IntoElement, Keystroke, Length, Modifiers,
ParentElement, SharedString, Styled, StyledText, TextStyle, WeakView, WindowContext,
};
use settings::Settings;
@@ -16,9 +16,8 @@ use std::{
};
use theme::{ActiveTheme, SyntaxTheme, ThemeSettings};
use ui::{
h_flex, relative, v_flex, Checkbox, Clickable, FluentBuilder, IconButton, IconName, IconSize,
InteractiveElement, LinkPreview, Selection, StatefulInteractiveElement, StyledExt, Tooltip,
VisibleOnHover,
h_flex, relative, v_flex, Checkbox, FluentBuilder, InteractiveElement, LinkPreview, Selection,
StatefulInteractiveElement, Tooltip,
};
use workspace::Workspace;
@@ -370,16 +369,6 @@ fn render_markdown_code_block(
StyledText::new(parsed.contents.clone())
};
let copy_block_button = IconButton::new("copy-code", IconName::Copy)
.icon_size(IconSize::Small)
.on_click({
let contents = parsed.contents.clone();
move |_, cx| {
cx.write_to_clipboard(ClipboardItem::new_string(contents.to_string()));
}
})
.visible_on_hover("markdown-block");
cx.with_common_p(div())
.font_family(cx.buffer_font_family.clone())
.px_3()
@@ -387,14 +376,6 @@ fn render_markdown_code_block(
.bg(cx.code_block_background_color)
.rounded_md()
.child(body)
.child(
div()
.h_flex()
.absolute()
.right_1()
.top_1()
.child(copy_block_button),
)
.into_any()
}

View File

@@ -161,10 +161,6 @@ impl NodeRuntime {
directory: &Path,
packages: &[(&str, &str)],
) -> Result<()> {
if packages.is_empty() {
return Ok(());
}
let packages: Vec<_> = packages
.iter()
.map(|(name, version)| format!("{name}@{version}"))

View File

@@ -19,7 +19,6 @@ gpui.workspace = true
language.workspace = true
ordered-float.workspace = true
picker.workspace = true
settings.workspace = true
smol.workspace = true
theme.workspace = true
ui.workspace = true

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