Compare commits
1 Commits
tasks-subs
...
ime-pre-ed
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
29611ac1cb |
66
Cargo.lock
generated
66
Cargo.lock
generated
@@ -347,13 +347,13 @@ dependencies = [
|
||||
"ctor",
|
||||
"editor",
|
||||
"env_logger",
|
||||
"feature_flags",
|
||||
"file_icons",
|
||||
"fs",
|
||||
"futures 0.3.28",
|
||||
"fuzzy",
|
||||
"gpui",
|
||||
"heed",
|
||||
"html_to_markdown",
|
||||
"gray_matter",
|
||||
"http 0.1.0",
|
||||
"indoc",
|
||||
"language",
|
||||
@@ -368,6 +368,7 @@ dependencies = [
|
||||
"rand 0.8.5",
|
||||
"regex",
|
||||
"rope",
|
||||
"rustdoc_to_markdown",
|
||||
"schemars",
|
||||
"search",
|
||||
"semantic_index",
|
||||
@@ -2394,7 +2395,6 @@ dependencies = [
|
||||
"util",
|
||||
"uuid",
|
||||
"workspace",
|
||||
"worktree",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3393,7 +3393,7 @@ version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412"
|
||||
dependencies = [
|
||||
"libloading 0.8.0",
|
||||
"libloading 0.7.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4788,6 +4788,18 @@ dependencies = [
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gray_matter"
|
||||
version = "0.2.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7188a951c53316d94711b3d944c28cf79968685d295cbe782494e8811fc75554"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"toml 0.5.11",
|
||||
"yaml-rust",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "grid"
|
||||
version = "0.13.0"
|
||||
@@ -5067,18 +5079,6 @@ dependencies = [
|
||||
"syn 2.0.59",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "html_to_markdown"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"html5ever",
|
||||
"indoc",
|
||||
"markup5ever_rcdom",
|
||||
"pretty_assertions",
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "0.1.0"
|
||||
@@ -5954,6 +5954,12 @@ dependencies = [
|
||||
"safemem",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linked-hash-map"
|
||||
version = "0.5.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
|
||||
|
||||
[[package]]
|
||||
name = "linkify"
|
||||
version = "0.10.0"
|
||||
@@ -7828,7 +7834,6 @@ dependencies = [
|
||||
"unicase",
|
||||
"util",
|
||||
"workspace",
|
||||
"worktree",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8630,6 +8635,18 @@ dependencies = [
|
||||
"semver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustdoc_to_markdown"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"html5ever",
|
||||
"indoc",
|
||||
"markup5ever_rcdom",
|
||||
"pretty_assertions",
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "0.37.23"
|
||||
@@ -13062,6 +13079,15 @@ dependencies = [
|
||||
"toml 0.8.10",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yaml-rust"
|
||||
version = "0.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85"
|
||||
dependencies = [
|
||||
"linked-hash-map",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yansi"
|
||||
version = "0.5.1"
|
||||
@@ -13287,7 +13313,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed_elixir"
|
||||
version = "0.0.5"
|
||||
version = "0.0.4"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.0.6",
|
||||
]
|
||||
@@ -13386,7 +13412,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed_php"
|
||||
version = "0.0.6"
|
||||
version = "0.0.5"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.0.4",
|
||||
]
|
||||
@@ -13442,7 +13468,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed_vue"
|
||||
version = "0.0.3"
|
||||
version = "0.0.2"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.0.6",
|
||||
]
|
||||
|
||||
11
Cargo.toml
11
Cargo.toml
@@ -41,7 +41,6 @@ members = [
|
||||
"crates/gpui",
|
||||
"crates/gpui_macros",
|
||||
"crates/headless",
|
||||
"crates/html_to_markdown",
|
||||
"crates/http",
|
||||
"crates/image_viewer",
|
||||
"crates/inline_completion_button",
|
||||
@@ -77,6 +76,7 @@ members = [
|
||||
"crates/rich_text",
|
||||
"crates/rope",
|
||||
"crates/rpc",
|
||||
"crates/rustdoc_to_markdown",
|
||||
"crates/task",
|
||||
"crates/tasks_ui",
|
||||
"crates/search",
|
||||
@@ -150,7 +150,6 @@ assets = { path = "crates/assets" }
|
||||
assistant = { path = "crates/assistant" }
|
||||
assistant_slash_command = { path = "crates/assistant_slash_command" }
|
||||
assistant_tooling = { path = "crates/assistant_tooling" }
|
||||
async-watch = "0.3.1"
|
||||
audio = { path = "crates/audio" }
|
||||
auto_update = { path = "crates/auto_update" }
|
||||
base64 = "0.13"
|
||||
@@ -167,7 +166,6 @@ color = { path = "crates/color" }
|
||||
command_palette = { path = "crates/command_palette" }
|
||||
command_palette_hooks = { path = "crates/command_palette_hooks" }
|
||||
copilot = { path = "crates/copilot" }
|
||||
dashmap = "5.5.3"
|
||||
db = { path = "crates/db" }
|
||||
diagnostics = { path = "crates/diagnostics" }
|
||||
editor = { path = "crates/editor" }
|
||||
@@ -187,7 +185,6 @@ google_ai = { path = "crates/google_ai" }
|
||||
gpui = { path = "crates/gpui" }
|
||||
gpui_macros = { path = "crates/gpui_macros" }
|
||||
headless = { path = "crates/headless" }
|
||||
html_to_markdown = { path = "crates/html_to_markdown" }
|
||||
http = { path = "crates/http" }
|
||||
install_cli = { path = "crates/install_cli" }
|
||||
image_viewer = { path = "crates/image_viewer" }
|
||||
@@ -224,6 +221,7 @@ dev_server_projects = { path = "crates/dev_server_projects" }
|
||||
rich_text = { path = "crates/rich_text" }
|
||||
rope = { path = "crates/rope" }
|
||||
rpc = { path = "crates/rpc" }
|
||||
rustdoc_to_markdown = { path = "crates/rustdoc_to_markdown" }
|
||||
task = { path = "crates/task" }
|
||||
tasks_ui = { path = "crates/tasks_ui" }
|
||||
search = { path = "crates/search" }
|
||||
@@ -496,5 +494,10 @@ non_canonical_partial_ord_impl = "allow"
|
||||
reversed_empty_ranges = "allow"
|
||||
type_complexity = "allow"
|
||||
|
||||
[workspace.lints.rust]
|
||||
unexpected_cfgs = { level = "warn", check-cfg = [
|
||||
'cfg(gles)', # used in gpui
|
||||
] }
|
||||
|
||||
[workspace.metadata.cargo-machete]
|
||||
ignored = ["bindgen", "cbindgen", "prost_build", "serde"]
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M6.97942 1.25171L6.9585 1.30199L5.58662 4.60039C5.54342 4.70426 5.44573 4.77523 5.3336 4.78422L1.7727 5.0697L1.71841 5.07405L1.38687 5.10063L1.08608 5.12475C0.820085 5.14607 0.712228 5.47802 0.914889 5.65162L1.14406 5.84793L1.39666 6.06431L1.43802 6.09974L4.15105 8.42374C4.23648 8.49692 4.2738 8.61176 4.24769 8.72118L3.41882 12.196L3.40618 12.249L3.32901 12.5725L3.25899 12.866C3.19708 13.1256 3.47945 13.3308 3.70718 13.1917L3.9647 13.0344L4.24854 12.861L4.29502 12.8326L7.34365 10.9705C7.43965 10.9119 7.5604 10.9119 7.6564 10.9705L10.705 12.8326L10.7515 12.861L11.0354 13.0344L11.2929 13.1917C11.5206 13.3308 11.803 13.1256 11.7411 12.866L11.671 12.5725L11.5939 12.249L11.5812 12.196L10.7524 8.72118C10.7263 8.61176 10.7636 8.49692 10.849 8.42374L13.562 6.09974L13.6034 6.06431L13.856 5.84793L14.0852 5.65162C14.2878 5.47802 14.18 5.14607 13.914 5.12475L13.6132 5.10063L13.2816 5.07405L13.2274 5.0697L9.66645 4.78422C9.55432 4.77523 9.45663 4.70426 9.41343 4.60039L8.04155 1.30199L8.02064 1.25171L7.89291 0.944609L7.77702 0.665992C7.67454 0.419604 7.32551 0.419604 7.22303 0.665992L7.10715 0.944609L6.97942 1.25171ZM7.50003 2.60397L6.50994 4.98442C6.32273 5.43453 5.89944 5.74207 5.41351 5.78103L2.84361 5.98705L4.8016 7.66428C5.17183 7.98142 5.33351 8.47903 5.2204 8.95321L4.62221 11.461L6.8224 10.1171C7.23842 9.86302 7.76164 9.86302 8.17766 10.1171L10.3778 11.461L9.77965 8.95321C9.66654 8.47903 9.82822 7.98142 10.1984 7.66428L12.1564 5.98705L9.58654 5.78103C9.10061 5.74207 8.67732 5.43453 8.49011 4.98442L7.50003 2.60397Z" fill="currentColor" fill-rule="evenodd" clip-rule="evenodd"></path></svg>
|
||||
|
Before Width: | Height: | Size: 1.7 KiB |
@@ -1 +0,0 @@
|
||||
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M7.22303 0.665992C7.32551 0.419604 7.67454 0.419604 7.77702 0.665992L9.41343 4.60039C9.45663 4.70426 9.55432 4.77523 9.66645 4.78422L13.914 5.12475C14.18 5.14607 14.2878 5.47802 14.0852 5.65162L10.849 8.42374C10.7636 8.49692 10.7263 8.61176 10.7524 8.72118L11.7411 12.866C11.803 13.1256 11.5206 13.3308 11.2929 13.1917L7.6564 10.9705C7.5604 10.9119 7.43965 10.9119 7.34365 10.9705L3.70718 13.1917C3.47945 13.3308 3.19708 13.1256 3.25899 12.866L4.24769 8.72118C4.2738 8.61176 4.23648 8.49692 4.15105 8.42374L0.914889 5.65162C0.712228 5.47802 0.820086 5.14607 1.08608 5.12475L5.3336 4.78422C5.44573 4.77523 5.54342 4.70426 5.58662 4.60039L7.22303 0.665992Z" fill="currentColor"></path></svg>
|
||||
|
Before Width: | Height: | Size: 794 B |
@@ -1,5 +0,0 @@
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7 1.75L5.88467 5.14092C5.82759 5.31446 5.73055 5.47218 5.60136 5.60136C5.47218 5.73055 5.31446 5.82759 5.14092 5.88467L1.75 7L5.14092 8.11533C5.31446 8.17241 5.47218 8.26945 5.60136 8.39864C5.73055 8.52782 5.82759 8.68554 5.88467 8.85908L7 12.25L8.11533 8.85908C8.17241 8.68554 8.26945 8.52782 8.39864 8.39864C8.52782 8.26945 8.68554 8.17241 8.85908 8.11533L12.25 7L8.85908 5.88467C8.68554 5.82759 8.52782 5.73055 8.39864 5.60136C8.26945 5.47218 8.17241 5.31446 8.11533 5.14092L7 1.75Z" fill="black" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M2.91667 1.75V4.08333M1.75 2.91667H4.08333" stroke="black" stroke-opacity="0.75" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M11.0833 9.91667V12.25M9.91667 11.0833H12.25" stroke="black" stroke-opacity="0.75" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1017 B |
@@ -28,6 +28,7 @@
|
||||
"ctrl-0": "zed::ResetBufferFontSize",
|
||||
"ctrl-,": "zed::OpenSettings",
|
||||
"ctrl-q": "zed::Quit",
|
||||
"alt-f9": "zed::Hide",
|
||||
"f11": "zed::ToggleFullScreen"
|
||||
}
|
||||
},
|
||||
@@ -216,13 +217,6 @@
|
||||
"alt-enter": "editor::Newline"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "PromptLibrary",
|
||||
"bindings": {
|
||||
"ctrl-n": "prompt_library::NewPrompt",
|
||||
"ctrl-shift-s": "prompt_library::ToggleDefaultPrompt"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "BufferSearchBar",
|
||||
"bindings": {
|
||||
@@ -642,7 +636,12 @@
|
||||
"pagedown": ["terminal::SendKeystroke", "pagedown"],
|
||||
"escape": ["terminal::SendKeystroke", "escape"],
|
||||
"enter": ["terminal::SendKeystroke", "enter"],
|
||||
"ctrl-c": ["terminal::SendKeystroke", "ctrl-c"]
|
||||
"ctrl-c": ["terminal::SendKeystroke", "ctrl-c"],
|
||||
|
||||
// Some nice conveniences
|
||||
"ctrl-backspace": ["terminal::SendText", "\u0015"],
|
||||
"ctrl-right": ["terminal::SendText", "\u0005"],
|
||||
"ctrl-left": ["terminal::SendText", "\u0001"]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -233,14 +233,6 @@
|
||||
"alt-enter": "editor::Newline"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "PromptLibrary",
|
||||
"bindings": {
|
||||
"cmd-n": "prompt_library::NewPrompt",
|
||||
"cmd-shift-s": "prompt_library::ToggleDefaultPrompt",
|
||||
"cmd-w": "workspace::CloseWindow"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "BufferSearchBar",
|
||||
"bindings": {
|
||||
@@ -647,7 +639,7 @@
|
||||
{
|
||||
"context": "Picker",
|
||||
"bindings": {
|
||||
"f2": "picker::UseSelectedQuery",
|
||||
"alt-e": "picker::UseSelectedQuery",
|
||||
"alt-enter": ["picker::ConfirmInput", { "secondary": false }],
|
||||
"cmd-alt-enter": ["picker::ConfirmInput", { "secondary": true }]
|
||||
}
|
||||
|
||||
@@ -379,8 +379,8 @@
|
||||
"r": ["vim::PushOperator", "Replace"],
|
||||
"s": "vim::Substitute",
|
||||
"shift-s": "vim::SubstituteLine",
|
||||
">": ["vim::PushOperator", "Indent"],
|
||||
"<": ["vim::PushOperator", "Outdent"],
|
||||
"> >": "vim::Indent",
|
||||
"< <": "vim::Outdent",
|
||||
"ctrl-pagedown": "pane::ActivateNextItem",
|
||||
"ctrl-pageup": "pane::ActivatePrevItem",
|
||||
// tree-sitter related commands
|
||||
@@ -459,18 +459,6 @@
|
||||
"s": "vim::CurrentLine"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_operator == >",
|
||||
"bindings": {
|
||||
">": "vim::CurrentLine"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_operator == <",
|
||||
"bindings": {
|
||||
"<": "vim::CurrentLine"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && VimObject",
|
||||
"bindings": {
|
||||
@@ -580,7 +568,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_mode == normal && !VimWaiting",
|
||||
"context": "Editor && vim_mode == normal",
|
||||
"bindings": {
|
||||
"g c c": "editor::ToggleComments"
|
||||
}
|
||||
|
||||
@@ -453,8 +453,7 @@
|
||||
// Send anonymized usage data like what languages you're using Zed with.
|
||||
"metrics": true
|
||||
},
|
||||
// Automatically update Zed. This setting may be ignored on Linux if
|
||||
// installed through a package manager.
|
||||
// Automatically update Zed
|
||||
"auto_update": true,
|
||||
// Diagnostics configuration.
|
||||
"diagnostics": {
|
||||
@@ -673,6 +672,9 @@
|
||||
"Elixir": {
|
||||
"language_servers": ["elixir-ls", "!next-ls", "!lexical", "..."]
|
||||
},
|
||||
"Gleam": {
|
||||
"tab_size": 2
|
||||
},
|
||||
"Go": {
|
||||
"code_actions_on_format": {
|
||||
"source.organizeImports": true
|
||||
@@ -698,7 +700,6 @@
|
||||
}
|
||||
},
|
||||
"JavaScript": {
|
||||
"language_servers": ["typescript-language-server", "!vtsls", ".."],
|
||||
"prettier": {
|
||||
"allowed": true
|
||||
}
|
||||
@@ -708,6 +709,9 @@
|
||||
"allowed": true
|
||||
}
|
||||
},
|
||||
"Make": {
|
||||
"hard_tabs": true
|
||||
},
|
||||
"Markdown": {
|
||||
"format_on_save": "off",
|
||||
"prettier": {
|
||||
@@ -720,6 +724,9 @@
|
||||
"plugins": ["@prettier/plugin-php"]
|
||||
}
|
||||
},
|
||||
"Prisma": {
|
||||
"tab_size": 2
|
||||
},
|
||||
"Ruby": {
|
||||
"language_servers": ["solargraph", "!ruby-lsp", "..."]
|
||||
},
|
||||
@@ -741,7 +748,6 @@
|
||||
}
|
||||
},
|
||||
"TSX": {
|
||||
"language_servers": ["typescript-language-server", "!vtsls", ".."],
|
||||
"prettier": {
|
||||
"allowed": true
|
||||
}
|
||||
@@ -752,7 +758,6 @@
|
||||
}
|
||||
},
|
||||
"TypeScript": {
|
||||
"language_servers": ["typescript-language-server", "!vtsls", ".."],
|
||||
"prettier": {
|
||||
"allowed": true
|
||||
}
|
||||
|
||||
@@ -22,13 +22,12 @@ client.workspace = true
|
||||
collections.workspace = true
|
||||
command_palette_hooks.workspace = true
|
||||
editor.workspace = true
|
||||
feature_flags.workspace = true
|
||||
file_icons.workspace = true
|
||||
fs.workspace = true
|
||||
futures.workspace = true
|
||||
fuzzy.workspace = true
|
||||
gpui.workspace = true
|
||||
heed.workspace = true
|
||||
html_to_markdown.workspace = true
|
||||
http.workspace = true
|
||||
indoc.workspace = true
|
||||
language.workspace = true
|
||||
@@ -41,6 +40,7 @@ parking_lot.workspace = true
|
||||
project.workspace = true
|
||||
regex.workspace = true
|
||||
rope.workspace = true
|
||||
rustdoc_to_markdown.workspace = true
|
||||
schemars.workspace = true
|
||||
search.workspace = true
|
||||
semantic_index.workspace = true
|
||||
@@ -59,6 +59,7 @@ util.workspace = true
|
||||
uuid.workspace = true
|
||||
workspace.workspace = true
|
||||
picker.workspace = true
|
||||
gray_matter = "0.2.7"
|
||||
|
||||
[dev-dependencies]
|
||||
ctor.workspace = true
|
||||
|
||||
@@ -3,7 +3,6 @@ pub mod assistant_settings;
|
||||
mod codegen;
|
||||
mod completion_provider;
|
||||
mod model_selector;
|
||||
mod prompt_library;
|
||||
mod prompts;
|
||||
mod saved_conversation;
|
||||
mod search;
|
||||
@@ -12,8 +11,7 @@ mod streaming_diff;
|
||||
|
||||
pub use assistant_panel::AssistantPanel;
|
||||
|
||||
use assistant_settings::{AnthropicModel, AssistantSettings, CloudModel, OpenAiModel};
|
||||
use assistant_slash_command::SlashCommandRegistry;
|
||||
use assistant_settings::{AnthropicModel, AssistantSettings, OpenAiModel, ZedDotDevModel};
|
||||
use client::{proto, Client};
|
||||
use command_palette_hooks::CommandPaletteFilter;
|
||||
pub(crate) use completion_provider::*;
|
||||
@@ -23,10 +21,6 @@ pub(crate) use saved_conversation::*;
|
||||
use semantic_index::{CloudEmbeddingProvider, SemanticIndex};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsStore};
|
||||
use slash_command::{
|
||||
active_command, default_command, fetch_command, file_command, project_command, prompt_command,
|
||||
rustdoc_command, search_command, tabs_command,
|
||||
};
|
||||
use std::{
|
||||
fmt::{self, Display},
|
||||
sync::Arc,
|
||||
@@ -86,14 +80,14 @@ impl Display for Role {
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub enum LanguageModel {
|
||||
Cloud(CloudModel),
|
||||
ZedDotDev(ZedDotDevModel),
|
||||
OpenAi(OpenAiModel),
|
||||
Anthropic(AnthropicModel),
|
||||
}
|
||||
|
||||
impl Default for LanguageModel {
|
||||
fn default() -> Self {
|
||||
LanguageModel::Cloud(CloudModel::default())
|
||||
LanguageModel::ZedDotDev(ZedDotDevModel::default())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,7 +96,7 @@ impl LanguageModel {
|
||||
match self {
|
||||
LanguageModel::OpenAi(model) => format!("openai/{}", model.id()),
|
||||
LanguageModel::Anthropic(model) => format!("anthropic/{}", model.id()),
|
||||
LanguageModel::Cloud(model) => format!("zed.dev/{}", model.id()),
|
||||
LanguageModel::ZedDotDev(model) => format!("zed.dev/{}", model.id()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,7 +104,7 @@ impl LanguageModel {
|
||||
match self {
|
||||
LanguageModel::OpenAi(model) => model.display_name().into(),
|
||||
LanguageModel::Anthropic(model) => model.display_name().into(),
|
||||
LanguageModel::Cloud(model) => model.display_name().into(),
|
||||
LanguageModel::ZedDotDev(model) => model.display_name().into(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,7 +112,7 @@ impl LanguageModel {
|
||||
match self {
|
||||
LanguageModel::OpenAi(model) => model.max_token_count(),
|
||||
LanguageModel::Anthropic(model) => model.max_token_count(),
|
||||
LanguageModel::Cloud(model) => model.max_token_count(),
|
||||
LanguageModel::ZedDotDev(model) => model.max_token_count(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,7 +120,7 @@ impl LanguageModel {
|
||||
match self {
|
||||
LanguageModel::OpenAi(model) => model.id(),
|
||||
LanguageModel::Anthropic(model) => model.id(),
|
||||
LanguageModel::Cloud(model) => model.id(),
|
||||
LanguageModel::ZedDotDev(model) => model.id(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -171,20 +165,6 @@ impl LanguageModelRequest {
|
||||
tools: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Before we send the request to the server, we can perform fixups on it appropriate to the model.
|
||||
pub fn preprocess(&mut self) {
|
||||
match &self.model {
|
||||
LanguageModel::OpenAi(_) => {}
|
||||
LanguageModel::Anthropic(_) => {}
|
||||
LanguageModel::Cloud(model) => match model {
|
||||
CloudModel::Claude3Opus | CloudModel::Claude3Sonnet | CloudModel::Claude3Haiku => {
|
||||
preprocess_anthropic_request(self);
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
|
||||
@@ -271,11 +251,8 @@ pub fn init(client: Arc<Client>, cx: &mut AppContext) {
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
prompt_library::init(cx);
|
||||
completion_provider::init(client, cx);
|
||||
assistant_slash_command::init(cx);
|
||||
register_slash_commands(cx);
|
||||
assistant_panel::init(cx);
|
||||
|
||||
CommandPaletteFilter::update_global(cx, |filter, _cx| {
|
||||
@@ -289,25 +266,13 @@ pub fn init(client: Arc<Client>, cx: &mut AppContext) {
|
||||
cx.observe_global::<SettingsStore>(|cx| {
|
||||
Assistant::update_global(cx, |assistant, cx| {
|
||||
let settings = AssistantSettings::get_global(cx);
|
||||
|
||||
assistant.set_enabled(settings.enabled, cx);
|
||||
});
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn register_slash_commands(cx: &mut AppContext) {
|
||||
let slash_command_registry = SlashCommandRegistry::global(cx);
|
||||
slash_command_registry.register_command(file_command::FileSlashCommand, true);
|
||||
slash_command_registry.register_command(active_command::ActiveSlashCommand, true);
|
||||
slash_command_registry.register_command(tabs_command::TabsSlashCommand, true);
|
||||
slash_command_registry.register_command(project_command::ProjectSlashCommand, true);
|
||||
slash_command_registry.register_command(search_command::SearchSlashCommand, true);
|
||||
slash_command_registry.register_command(prompt_command::PromptSlashCommand, true);
|
||||
slash_command_registry.register_command(default_command::DefaultSlashCommand, true);
|
||||
slash_command_registry.register_command(rustdoc_command::RustdocSlashCommand, false);
|
||||
slash_command_registry.register_command(fetch_command::FetchSlashCommand, false);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[ctor::ctor]
|
||||
fn init_logger() {
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
use crate::prompts::{generate_content_prompt, PromptLibrary, PromptManager};
|
||||
use crate::slash_command::{rustdoc_command, search_command, tabs_command};
|
||||
use crate::{
|
||||
assistant_settings::{AssistantDockPosition, AssistantSettings},
|
||||
codegen::{self, Codegen, CodegenKind},
|
||||
prompt_library::open_prompt_library,
|
||||
prompts::generate_content_prompt,
|
||||
search::*,
|
||||
slash_command::{
|
||||
default_command::DefaultSlashCommand, SlashCommandCompletionProvider, SlashCommandLine,
|
||||
SlashCommandRegistry,
|
||||
active_command, file_command, project_command, prompt_command,
|
||||
SlashCommandCompletionProvider, SlashCommandLine, SlashCommandRegistry,
|
||||
},
|
||||
ApplyEdit, Assist, CompletionProvider, ConfirmCommand, CycleMessageRole, InlineAssist,
|
||||
LanguageModelRequest, LanguageModelRequestMessage, MessageId, MessageMetadata, MessageStatus,
|
||||
ModelSelector, QuoteSelection, ResetKey, Role, SavedConversation, SavedConversationMetadata,
|
||||
SavedMessage, Split, ToggleFocus, ToggleHistory, ToggleModelSelector,
|
||||
QuoteSelection, ResetKey, Role, SavedConversation, SavedConversationMetadata, SavedMessage,
|
||||
Split, ToggleFocus, ToggleHistory,
|
||||
};
|
||||
use crate::{ModelSelector, ToggleModelSelector};
|
||||
use anyhow::{anyhow, Result};
|
||||
use assistant_slash_command::{SlashCommand, SlashCommandOutput, SlashCommandOutputSection};
|
||||
use assistant_slash_command::{SlashCommandOutput, SlashCommandOutputSection};
|
||||
use client::telemetry::Telemetry;
|
||||
use collections::{hash_map, BTreeSet, HashMap, HashSet, VecDeque};
|
||||
use editor::{actions::ShowCompletions, GutterDimensions};
|
||||
use editor::actions::ShowCompletions;
|
||||
use editor::{
|
||||
actions::{FoldAt, MoveDown, MoveToEndOfLine, MoveUp, Newline, UnfoldAt},
|
||||
display_map::{
|
||||
@@ -28,21 +29,24 @@ use editor::{
|
||||
ToOffset as _, ToPoint,
|
||||
};
|
||||
use editor::{display_map::FlapId, FoldPlaceholder};
|
||||
use feature_flags::{FeatureFlag, FeatureFlagAppExt, FeatureFlagViewExt};
|
||||
use file_icons::FileIcons;
|
||||
use fs::Fs;
|
||||
use futures::future::Shared;
|
||||
use futures::{FutureExt, StreamExt};
|
||||
use gpui::{
|
||||
div, point, relative, rems, uniform_list, Action, AnyElement, AnyView, AppContext,
|
||||
AsyncAppContext, AsyncWindowContext, ClipboardItem, Context, Empty, EventEmitter, FocusHandle,
|
||||
FocusableView, FontStyle, FontWeight, HighlightStyle, InteractiveElement, IntoElement, Model,
|
||||
ModelContext, ParentElement, Pixels, Render, SharedString, StatefulInteractiveElement, Styled,
|
||||
Subscription, Task, TextStyle, UniformListScrollHandle, View, ViewContext, VisualContext,
|
||||
WeakModel, WeakView, WhiteSpace, WindowContext,
|
||||
canvas, div, point, relative, rems, uniform_list, Action, AnyElement, AnyView, AppContext,
|
||||
AsyncAppContext, AsyncWindowContext, AvailableSpace, ClipboardItem, Context, Empty,
|
||||
EventEmitter, FocusHandle, FocusableView, FontStyle, FontWeight, HighlightStyle,
|
||||
InteractiveElement, IntoElement, Model, ModelContext, ParentElement, Pixels, Render,
|
||||
SharedString, StatefulInteractiveElement, Styled, Subscription, Task, TextStyle,
|
||||
UniformListScrollHandle, View, ViewContext, VisualContext, WeakModel, WeakView, WhiteSpace,
|
||||
WindowContext,
|
||||
};
|
||||
use language::LspAdapterDelegate;
|
||||
use language::{
|
||||
language_settings::SoftWrap, AnchorRangeExt, AutoindentMode, Buffer, LanguageRegistry,
|
||||
LspAdapterDelegate, OffsetRangeExt as _, Point, ToOffset as _,
|
||||
OffsetRangeExt as _, Point, ToOffset as _,
|
||||
};
|
||||
use multi_buffer::MultiBufferRow;
|
||||
use parking_lot::Mutex;
|
||||
@@ -107,6 +111,7 @@ pub struct AssistantPanel {
|
||||
toolbar: View<Toolbar>,
|
||||
languages: Arc<LanguageRegistry>,
|
||||
slash_commands: Arc<SlashCommandRegistry>,
|
||||
prompt_library: Arc<PromptLibrary>,
|
||||
fs: Arc<dyn Fs>,
|
||||
telemetry: Arc<Telemetry>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
@@ -124,6 +129,12 @@ struct ActiveConversationEditor {
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
struct PromptLibraryFeatureFlag;
|
||||
|
||||
impl FeatureFlag for PromptLibraryFeatureFlag {
|
||||
const NAME: &'static str = "prompt-library";
|
||||
}
|
||||
|
||||
impl AssistantPanel {
|
||||
const INLINE_PROMPT_HISTORY_MAX_LEN: usize = 20;
|
||||
|
||||
@@ -138,13 +149,23 @@ impl AssistantPanel {
|
||||
.log_err()
|
||||
.unwrap_or_default();
|
||||
|
||||
let prompt_library = Arc::new(
|
||||
PromptLibrary::load_index(fs.clone())
|
||||
.await
|
||||
.log_err()
|
||||
.unwrap_or_default(),
|
||||
);
|
||||
|
||||
// TODO: deserialize state.
|
||||
let workspace_handle = workspace.clone();
|
||||
workspace.update(&mut cx, |workspace, cx| {
|
||||
cx.new_view::<Self>(|cx| {
|
||||
cx.observe_flag::<PromptLibraryFeatureFlag, _>(|_, _, cx| cx.notify())
|
||||
.detach();
|
||||
|
||||
const CONVERSATION_WATCH_DURATION: Duration = Duration::from_millis(100);
|
||||
let _watch_saved_conversations = cx.spawn(move |this, mut cx| async move {
|
||||
let (mut events, _) = fs
|
||||
let mut events = fs
|
||||
.watch(&CONVERSATIONS_DIR, CONVERSATION_WATCH_DURATION)
|
||||
.await;
|
||||
while events.next().await.is_some() {
|
||||
@@ -189,6 +210,23 @@ impl AssistantPanel {
|
||||
})
|
||||
.detach();
|
||||
|
||||
let slash_command_registry = SlashCommandRegistry::global(cx);
|
||||
|
||||
slash_command_registry.register_command(file_command::FileSlashCommand, true);
|
||||
slash_command_registry.register_command(
|
||||
prompt_command::PromptSlashCommand::new(prompt_library.clone()),
|
||||
true,
|
||||
);
|
||||
slash_command_registry
|
||||
.register_command(active_command::ActiveSlashCommand, true);
|
||||
slash_command_registry.register_command(tabs_command::TabsSlashCommand, true);
|
||||
slash_command_registry
|
||||
.register_command(project_command::ProjectSlashCommand, true);
|
||||
slash_command_registry
|
||||
.register_command(search_command::SearchSlashCommand, true);
|
||||
slash_command_registry
|
||||
.register_command(rustdoc_command::RustdocSlashCommand, false);
|
||||
|
||||
Self {
|
||||
workspace: workspace_handle,
|
||||
active_conversation_editor: None,
|
||||
@@ -199,7 +237,8 @@ impl AssistantPanel {
|
||||
focus_handle,
|
||||
toolbar,
|
||||
languages: workspace.app_state().languages.clone(),
|
||||
slash_commands: SlashCommandRegistry::global(cx),
|
||||
slash_commands: slash_command_registry,
|
||||
prompt_library,
|
||||
fs: workspace.app_state().fs.clone(),
|
||||
telemetry: workspace.client().telemetry().clone(),
|
||||
width: None,
|
||||
@@ -389,7 +428,7 @@ impl AssistantPanel {
|
||||
)
|
||||
});
|
||||
|
||||
let measurements = Arc::new(Mutex::new(GutterDimensions::default()));
|
||||
let measurements = Arc::new(Mutex::new(BlockMeasurements::default()));
|
||||
let inline_assistant = cx.new_view(|cx| {
|
||||
InlineAssistant::new(
|
||||
inline_assist_id,
|
||||
@@ -412,7 +451,10 @@ impl AssistantPanel {
|
||||
render: Box::new({
|
||||
let inline_assistant = inline_assistant.clone();
|
||||
move |cx: &mut BlockContext| {
|
||||
*measurements.lock() = *cx.gutter_dimensions;
|
||||
*measurements.lock() = BlockMeasurements {
|
||||
gutter_width: cx.gutter_dimensions.width,
|
||||
gutter_margin: cx.gutter_dimensions.margin,
|
||||
};
|
||||
inline_assistant.clone().into_any_element()
|
||||
}
|
||||
}),
|
||||
@@ -500,7 +542,6 @@ impl AssistantPanel {
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
self.pending_inline_assist_ids_by_editor
|
||||
.entry(editor.downgrade())
|
||||
.or_default()
|
||||
@@ -728,7 +769,7 @@ impl AssistantPanel {
|
||||
codegen.update(&mut cx, |codegen, cx| codegen.start(request, cx))?;
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn update_highlights_for_editor(&self, editor: &View<Editor>, cx: &mut ViewContext<Self>) {
|
||||
@@ -1113,6 +1154,21 @@ impl AssistantPanel {
|
||||
})
|
||||
}
|
||||
|
||||
fn show_prompt_manager(&mut self, cx: &mut ViewContext<Self>) {
|
||||
if let Some(workspace) = self.workspace.upgrade() {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.toggle_modal(cx, |cx| {
|
||||
PromptManager::new(
|
||||
self.prompt_library.clone(),
|
||||
self.languages.clone(),
|
||||
self.fs.clone(),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn is_authenticated(&mut self, cx: &mut ViewContext<Self>) -> bool {
|
||||
CompletionProvider::global(cx).is_authenticated()
|
||||
}
|
||||
@@ -1155,17 +1211,15 @@ impl AssistantPanel {
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(self.render_inject_context_menu(cx))
|
||||
.child(
|
||||
IconButton::new("show-prompt-library", IconName::Library)
|
||||
.icon_size(IconSize::Small)
|
||||
.on_click({
|
||||
let language_registry = self.languages.clone();
|
||||
cx.listener(move |_this, _event, cx| {
|
||||
open_prompt_library(language_registry.clone(), cx)
|
||||
.detach_and_log_err(cx);
|
||||
})
|
||||
})
|
||||
.tooltip(|cx| Tooltip::text("Prompt Library…", cx)),
|
||||
.children(
|
||||
cx.has_flag::<PromptLibraryFeatureFlag>().then_some(
|
||||
IconButton::new("show_prompt_manager", IconName::Library)
|
||||
.icon_size(IconSize::Small)
|
||||
.on_click(cx.listener(|this, _event, cx| {
|
||||
this.show_prompt_manager(cx)
|
||||
}))
|
||||
.tooltip(|cx| Tooltip::text("Prompt Library…", cx)),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -1207,18 +1261,30 @@ impl AssistantPanel {
|
||||
let view = cx.view().clone();
|
||||
let scroll_handle = self.saved_conversations_scroll_handle.clone();
|
||||
let conversation_count = self.saved_conversations.len();
|
||||
uniform_list(
|
||||
view,
|
||||
"saved_conversations",
|
||||
conversation_count,
|
||||
|this, range, cx| {
|
||||
range
|
||||
.map(|ix| this.render_saved_conversation(ix, cx))
|
||||
.collect()
|
||||
canvas(
|
||||
move |bounds, cx| {
|
||||
let mut saved_conversations = uniform_list(
|
||||
view,
|
||||
"saved_conversations",
|
||||
conversation_count,
|
||||
|this, range, cx| {
|
||||
range
|
||||
.map(|ix| this.render_saved_conversation(ix, cx))
|
||||
.collect()
|
||||
},
|
||||
)
|
||||
.track_scroll(scroll_handle)
|
||||
.into_any_element();
|
||||
saved_conversations.prepaint_as_root(
|
||||
bounds.origin,
|
||||
bounds.size.map(AvailableSpace::Definite),
|
||||
cx,
|
||||
);
|
||||
saved_conversations
|
||||
},
|
||||
|_bounds, mut saved_conversations, cx| saved_conversations.paint(cx),
|
||||
)
|
||||
.size_full()
|
||||
.track_scroll(scroll_handle)
|
||||
.into_any_element()
|
||||
} else if let Some(editor) = self.active_conversation_editor() {
|
||||
let editor = editor.clone();
|
||||
@@ -1348,7 +1414,7 @@ impl Panel for AssistantPanel {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(IconName::ZedAssistant)
|
||||
Some(IconName::Ai)
|
||||
}
|
||||
|
||||
fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
|
||||
@@ -1379,9 +1445,7 @@ enum ConversationEvent {
|
||||
updated: Vec<PendingSlashCommand>,
|
||||
},
|
||||
SlashCommandFinished {
|
||||
output_range: Range<language::Anchor>,
|
||||
sections: Vec<SlashCommandOutputSection<language::Anchor>>,
|
||||
run_commands_in_output: bool,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1648,7 +1712,18 @@ impl Conversation {
|
||||
buffer.line_len(row_range.end - 1),
|
||||
));
|
||||
|
||||
let old_range = self.pending_command_indices_for_range(start..end, cx);
|
||||
let start_ix = match self
|
||||
.pending_slash_commands
|
||||
.binary_search_by(|probe| probe.source_range.start.cmp(&start, buffer))
|
||||
{
|
||||
Ok(ix) | Err(ix) => ix,
|
||||
};
|
||||
let end_ix = match self.pending_slash_commands[start_ix..]
|
||||
.binary_search_by(|probe| probe.source_range.end.cmp(&end, buffer))
|
||||
{
|
||||
Ok(ix) => start_ix + ix + 1,
|
||||
Err(ix) => start_ix + ix,
|
||||
};
|
||||
|
||||
let mut new_commands = Vec::new();
|
||||
let mut lines = buffer.text_for_range(start..end).lines();
|
||||
@@ -1683,7 +1758,9 @@ impl Conversation {
|
||||
offset = lines.offset();
|
||||
}
|
||||
|
||||
let removed_commands = self.pending_slash_commands.splice(old_range, new_commands);
|
||||
let removed_commands = self
|
||||
.pending_slash_commands
|
||||
.splice(start_ix..end_ix, new_commands);
|
||||
removed.extend(removed_commands.map(|command| command.source_range));
|
||||
}
|
||||
|
||||
@@ -1757,60 +1834,25 @@ impl Conversation {
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Option<&mut PendingSlashCommand> {
|
||||
let buffer = self.buffer.read(cx);
|
||||
match self
|
||||
let ix = self
|
||||
.pending_slash_commands
|
||||
.binary_search_by(|probe| probe.source_range.end.cmp(&position, buffer))
|
||||
{
|
||||
Ok(ix) => Some(&mut self.pending_slash_commands[ix]),
|
||||
Err(ix) => {
|
||||
let cmd = self.pending_slash_commands.get_mut(ix)?;
|
||||
if position.cmp(&cmd.source_range.start, buffer).is_ge()
|
||||
&& position.cmp(&cmd.source_range.end, buffer).is_le()
|
||||
{
|
||||
Some(cmd)
|
||||
.binary_search_by(|probe| {
|
||||
if probe.source_range.start.cmp(&position, buffer).is_gt() {
|
||||
Ordering::Less
|
||||
} else if probe.source_range.end.cmp(&position, buffer).is_lt() {
|
||||
Ordering::Greater
|
||||
} else {
|
||||
None
|
||||
Ordering::Equal
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn pending_commands_for_range(
|
||||
&self,
|
||||
range: Range<language::Anchor>,
|
||||
cx: &AppContext,
|
||||
) -> &[PendingSlashCommand] {
|
||||
let range = self.pending_command_indices_for_range(range, cx);
|
||||
&self.pending_slash_commands[range]
|
||||
}
|
||||
|
||||
fn pending_command_indices_for_range(
|
||||
&self,
|
||||
range: Range<language::Anchor>,
|
||||
cx: &AppContext,
|
||||
) -> Range<usize> {
|
||||
let buffer = self.buffer.read(cx);
|
||||
let start_ix = match self
|
||||
.pending_slash_commands
|
||||
.binary_search_by(|probe| probe.source_range.end.cmp(&range.start, &buffer))
|
||||
{
|
||||
Ok(ix) | Err(ix) => ix,
|
||||
};
|
||||
let end_ix = match self
|
||||
.pending_slash_commands
|
||||
.binary_search_by(|probe| probe.source_range.start.cmp(&range.end, &buffer))
|
||||
{
|
||||
Ok(ix) => ix + 1,
|
||||
Err(ix) => ix,
|
||||
};
|
||||
start_ix..end_ix
|
||||
})
|
||||
.ok()?;
|
||||
self.pending_slash_commands.get_mut(ix)
|
||||
}
|
||||
|
||||
fn insert_command_output(
|
||||
&mut self,
|
||||
command_range: Range<language::Anchor>,
|
||||
output: Task<Result<SlashCommandOutput>>,
|
||||
insert_trailing_newline: bool,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
self.reparse_slash_commands(cx);
|
||||
@@ -1821,14 +1863,13 @@ impl Conversation {
|
||||
let output = output.await;
|
||||
this.update(&mut cx, |this, cx| match output {
|
||||
Ok(mut output) => {
|
||||
if insert_trailing_newline {
|
||||
if !output.text.ends_with('\n') {
|
||||
output.text.push('\n');
|
||||
}
|
||||
|
||||
let event = this.buffer.update(cx, |buffer, cx| {
|
||||
let sections = this.buffer.update(cx, |buffer, cx| {
|
||||
let start = command_range.start.to_offset(buffer);
|
||||
let old_end = command_range.end.to_offset(buffer);
|
||||
let new_end = start + output.text.len();
|
||||
buffer.edit([(start..old_end, output.text)], None, cx);
|
||||
|
||||
let mut sections = output
|
||||
@@ -1841,14 +1882,9 @@ impl Conversation {
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
sections.sort_by(|a, b| a.range.cmp(&b.range, buffer));
|
||||
ConversationEvent::SlashCommandFinished {
|
||||
output_range: buffer.anchor_after(start)
|
||||
..buffer.anchor_before(new_end),
|
||||
sections,
|
||||
run_commands_in_output: output.run_commands_in_text,
|
||||
}
|
||||
sections
|
||||
});
|
||||
cx.emit(event);
|
||||
cx.emit(ConversationEvent::SlashCommandFinished { sections });
|
||||
}
|
||||
Err(error) => {
|
||||
if let Some(pending_command) =
|
||||
@@ -2564,10 +2600,7 @@ impl ConversationEditor {
|
||||
)
|
||||
});
|
||||
|
||||
let mut this =
|
||||
Self::for_conversation(conversation, fs, workspace, lsp_adapter_delegate, cx);
|
||||
this.insert_default_prompt(cx);
|
||||
this
|
||||
Self::for_conversation(conversation, fs, workspace, lsp_adapter_delegate, cx)
|
||||
}
|
||||
|
||||
fn for_conversation(
|
||||
@@ -2580,9 +2613,9 @@ impl ConversationEditor {
|
||||
let slash_command_registry = conversation.read(cx).slash_command_registry.clone();
|
||||
|
||||
let completion_provider = SlashCommandCompletionProvider::new(
|
||||
cx.view().downgrade(),
|
||||
slash_command_registry.clone(),
|
||||
Some(cx.view().downgrade()),
|
||||
Some(workspace.downgrade()),
|
||||
workspace.downgrade(),
|
||||
);
|
||||
|
||||
let editor = cx.new_view(|cx| {
|
||||
@@ -2619,32 +2652,6 @@ impl ConversationEditor {
|
||||
this
|
||||
}
|
||||
|
||||
fn insert_default_prompt(&mut self, cx: &mut ViewContext<Self>) {
|
||||
let command_name = DefaultSlashCommand.name();
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.insert(&format!("/{command_name}"), cx)
|
||||
});
|
||||
self.split(&Split, cx);
|
||||
let command = self.conversation.update(cx, |conversation, cx| {
|
||||
conversation
|
||||
.messages_metadata
|
||||
.get_mut(&MessageId::default())
|
||||
.unwrap()
|
||||
.role = Role::System;
|
||||
conversation.reparse_slash_commands(cx);
|
||||
conversation.pending_slash_commands[0].clone()
|
||||
});
|
||||
|
||||
self.run_command(
|
||||
command.source_range,
|
||||
&command.name,
|
||||
command.argument.as_deref(),
|
||||
false,
|
||||
self.workspace.clone(),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
fn assist(&mut self, _: &Assist, cx: &mut ViewContext<Self>) {
|
||||
let cursors = self.cursors(cx);
|
||||
|
||||
@@ -2767,7 +2774,6 @@ impl ConversationEditor {
|
||||
command.source_range,
|
||||
&command.name,
|
||||
command.argument.as_deref(),
|
||||
true,
|
||||
workspace.clone(),
|
||||
cx,
|
||||
);
|
||||
@@ -2781,7 +2787,6 @@ impl ConversationEditor {
|
||||
command_range: Range<language::Anchor>,
|
||||
name: &str,
|
||||
argument: Option<&str>,
|
||||
insert_trailing_newline: bool,
|
||||
workspace: WeakView<Workspace>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
@@ -2790,12 +2795,7 @@ impl ConversationEditor {
|
||||
let argument = argument.map(ToString::to_string);
|
||||
let output = command.run(argument.as_deref(), workspace, lsp_adapter_delegate, cx);
|
||||
self.conversation.update(cx, |conversation, cx| {
|
||||
conversation.insert_command_output(
|
||||
command_range,
|
||||
output,
|
||||
insert_trailing_newline,
|
||||
cx,
|
||||
)
|
||||
conversation.insert_command_output(command_range, output, cx)
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -2895,7 +2895,6 @@ impl ConversationEditor {
|
||||
command.source_range.clone(),
|
||||
&command.name,
|
||||
command.argument.as_deref(),
|
||||
false,
|
||||
workspace.clone(),
|
||||
cx,
|
||||
);
|
||||
@@ -2949,93 +2948,62 @@ impl ConversationEditor {
|
||||
);
|
||||
})
|
||||
}
|
||||
ConversationEvent::SlashCommandFinished {
|
||||
output_range,
|
||||
sections,
|
||||
run_commands_in_output,
|
||||
} => {
|
||||
self.insert_slash_command_output_sections(sections.iter().cloned(), cx);
|
||||
|
||||
if *run_commands_in_output {
|
||||
let commands = self.conversation.update(cx, |conversation, cx| {
|
||||
conversation.reparse_slash_commands(cx);
|
||||
conversation
|
||||
.pending_commands_for_range(output_range.clone(), cx)
|
||||
.to_vec()
|
||||
});
|
||||
|
||||
for command in commands {
|
||||
self.run_command(
|
||||
command.source_range,
|
||||
&command.name,
|
||||
command.argument.as_deref(),
|
||||
false,
|
||||
self.workspace.clone(),
|
||||
cx,
|
||||
);
|
||||
ConversationEvent::SlashCommandFinished { sections } => {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
let buffer = editor.buffer().read(cx).snapshot(cx);
|
||||
let excerpt_id = *buffer.as_singleton().unwrap().0;
|
||||
let mut buffer_rows_to_fold = BTreeSet::new();
|
||||
let mut flaps = Vec::new();
|
||||
for section in sections {
|
||||
let start = buffer
|
||||
.anchor_in_excerpt(excerpt_id, section.range.start)
|
||||
.unwrap();
|
||||
let end = buffer
|
||||
.anchor_in_excerpt(excerpt_id, section.range.end)
|
||||
.unwrap();
|
||||
let buffer_row = MultiBufferRow(start.to_point(&buffer).row);
|
||||
buffer_rows_to_fold.insert(buffer_row);
|
||||
flaps.push(Flap::new(
|
||||
start..end,
|
||||
FoldPlaceholder {
|
||||
render: Arc::new({
|
||||
let editor = cx.view().downgrade();
|
||||
let render_placeholder = section.render_placeholder.clone();
|
||||
move |fold_id, fold_range, cx| {
|
||||
let editor = editor.clone();
|
||||
let unfold = Arc::new(move |cx: &mut WindowContext| {
|
||||
editor
|
||||
.update(cx, |editor, cx| {
|
||||
let buffer_start = fold_range.start.to_point(
|
||||
&editor.buffer().read(cx).read(cx),
|
||||
);
|
||||
let buffer_row =
|
||||
MultiBufferRow(buffer_start.row);
|
||||
editor.unfold_at(&UnfoldAt { buffer_row }, cx);
|
||||
})
|
||||
.ok();
|
||||
});
|
||||
render_placeholder(fold_id.into(), unfold, cx)
|
||||
}
|
||||
}),
|
||||
constrain_width: false,
|
||||
merge_adjacent: false,
|
||||
},
|
||||
render_slash_command_output_toggle,
|
||||
|_, _, _| Empty.into_any_element(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
editor.insert_flaps(flaps, cx);
|
||||
|
||||
for buffer_row in buffer_rows_to_fold.into_iter().rev() {
|
||||
editor.fold_at(&FoldAt { buffer_row }, cx);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn insert_slash_command_output_sections(
|
||||
&mut self,
|
||||
sections: impl IntoIterator<Item = SlashCommandOutputSection<language::Anchor>>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
let buffer = editor.buffer().read(cx).snapshot(cx);
|
||||
let excerpt_id = *buffer.as_singleton().unwrap().0;
|
||||
let mut buffer_rows_to_fold = BTreeSet::new();
|
||||
let mut flaps = Vec::new();
|
||||
for section in sections {
|
||||
let start = buffer
|
||||
.anchor_in_excerpt(excerpt_id, section.range.start)
|
||||
.unwrap();
|
||||
let end = buffer
|
||||
.anchor_in_excerpt(excerpt_id, section.range.end)
|
||||
.unwrap();
|
||||
let buffer_row = MultiBufferRow(start.to_point(&buffer).row);
|
||||
buffer_rows_to_fold.insert(buffer_row);
|
||||
flaps.push(Flap::new(
|
||||
start..end,
|
||||
FoldPlaceholder {
|
||||
render: Arc::new({
|
||||
let editor = cx.view().downgrade();
|
||||
let render_placeholder = section.render_placeholder.clone();
|
||||
move |fold_id, fold_range, cx| {
|
||||
let editor = editor.clone();
|
||||
let unfold = Arc::new(move |cx: &mut WindowContext| {
|
||||
editor
|
||||
.update(cx, |editor, cx| {
|
||||
let buffer_start = fold_range
|
||||
.start
|
||||
.to_point(&editor.buffer().read(cx).read(cx));
|
||||
let buffer_row = MultiBufferRow(buffer_start.row);
|
||||
editor.unfold_at(&UnfoldAt { buffer_row }, cx);
|
||||
})
|
||||
.ok();
|
||||
});
|
||||
render_placeholder(fold_id.into(), unfold, cx)
|
||||
}
|
||||
}),
|
||||
constrain_width: false,
|
||||
merge_adjacent: false,
|
||||
},
|
||||
render_slash_command_output_toggle,
|
||||
|_, _, _| Empty.into_any_element(),
|
||||
));
|
||||
}
|
||||
|
||||
editor.insert_flaps(flaps, cx);
|
||||
|
||||
for buffer_row in buffer_rows_to_fold.into_iter().rev() {
|
||||
editor.fold_at(&FoldAt { buffer_row }, cx);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn handle_editor_event(
|
||||
&mut self,
|
||||
_: View<Editor>,
|
||||
@@ -3131,7 +3099,7 @@ impl ConversationEditor {
|
||||
|
||||
h_flex()
|
||||
.id(("message_header", message_id.0))
|
||||
.pl(cx.gutter_dimensions.full_width())
|
||||
.pl(cx.gutter_dimensions.width + cx.gutter_dimensions.margin)
|
||||
.h_11()
|
||||
.w_full()
|
||||
.relative()
|
||||
@@ -3531,7 +3499,7 @@ struct InlineAssistant {
|
||||
prompt_editor: View<Editor>,
|
||||
confirmed: bool,
|
||||
include_conversation: bool,
|
||||
gutter_dimensions: Arc<Mutex<GutterDimensions>>,
|
||||
measurements: Arc<Mutex<BlockMeasurements>>,
|
||||
prompt_history: VecDeque<String>,
|
||||
prompt_history_ix: Option<usize>,
|
||||
pending_prompt: String,
|
||||
@@ -3543,8 +3511,7 @@ impl EventEmitter<InlineAssistantEvent> for InlineAssistant {}
|
||||
|
||||
impl Render for InlineAssistant {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let gutter_dimensions = *self.gutter_dimensions.lock();
|
||||
let icon_size = IconSize::default();
|
||||
let measurements = *self.measurements.lock();
|
||||
h_flex()
|
||||
.w_full()
|
||||
.py_2()
|
||||
@@ -3557,20 +3524,14 @@ impl Render for InlineAssistant {
|
||||
.on_action(cx.listener(Self::move_down))
|
||||
.child(
|
||||
h_flex()
|
||||
.w(gutter_dimensions.full_width() + (gutter_dimensions.margin / 2.0))
|
||||
.pr(gutter_dimensions.fold_area_width())
|
||||
.justify_end()
|
||||
.w(measurements.gutter_width + measurements.gutter_margin)
|
||||
.children(if let Some(error) = self.codegen.read(cx).error() {
|
||||
let error_message = SharedString::from(error.to_string());
|
||||
Some(
|
||||
div()
|
||||
.id("error")
|
||||
.tooltip(move |cx| Tooltip::text(error_message.clone(), cx))
|
||||
.child(
|
||||
Icon::new(IconName::XCircle)
|
||||
.size(icon_size)
|
||||
.color(Color::Error),
|
||||
),
|
||||
.child(Icon::new(IconName::XCircle).color(Color::Error)),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
@@ -3590,7 +3551,7 @@ impl InlineAssistant {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn new(
|
||||
id: usize,
|
||||
gutter_dimensions: Arc<Mutex<GutterDimensions>>,
|
||||
measurements: Arc<Mutex<BlockMeasurements>>,
|
||||
include_conversation: bool,
|
||||
prompt_history: VecDeque<String>,
|
||||
codegen: Model<Codegen>,
|
||||
@@ -3617,7 +3578,7 @@ impl InlineAssistant {
|
||||
prompt_editor,
|
||||
confirmed: false,
|
||||
include_conversation,
|
||||
gutter_dimensions,
|
||||
measurements,
|
||||
prompt_history,
|
||||
prompt_history_ix: None,
|
||||
pending_prompt: String::new(),
|
||||
@@ -3742,6 +3703,13 @@ impl InlineAssistant {
|
||||
}
|
||||
}
|
||||
|
||||
// This wouldn't need to exist if we could pass parameters when rendering child views.
|
||||
#[derive(Copy, Clone, Default)]
|
||||
struct BlockMeasurements {
|
||||
gutter_width: Pixels,
|
||||
gutter_margin: Pixels,
|
||||
}
|
||||
|
||||
struct PendingInlineAssist {
|
||||
editor: WeakView<Editor>,
|
||||
inline_assistant: Option<(BlockId, View<InlineAssistant>)>,
|
||||
@@ -3859,10 +3827,7 @@ fn make_lsp_adapter_delegate(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{
|
||||
slash_command::{active_command, file_command},
|
||||
FakeCompletionProvider, MessageId,
|
||||
};
|
||||
use crate::{FakeCompletionProvider, MessageId};
|
||||
use fs::FakeFs;
|
||||
use gpui::{AppContext, TestAppContext};
|
||||
use rope::Rope;
|
||||
@@ -4212,9 +4177,14 @@ mod tests {
|
||||
)
|
||||
.await;
|
||||
|
||||
let prompt_library = Arc::new(PromptLibrary::default());
|
||||
let slash_command_registry = SlashCommandRegistry::new();
|
||||
|
||||
slash_command_registry.register_command(file_command::FileSlashCommand, false);
|
||||
slash_command_registry.register_command(active_command::ActiveSlashCommand, false);
|
||||
slash_command_registry.register_command(
|
||||
prompt_command::PromptSlashCommand::new(prompt_library.clone()),
|
||||
false,
|
||||
);
|
||||
|
||||
let registry = Arc::new(LanguageRegistry::test(cx.executor()));
|
||||
let conversation = cx
|
||||
|
||||
@@ -14,10 +14,10 @@ use serde::{
|
||||
use settings::{Settings, SettingsSources};
|
||||
use strum::{EnumIter, IntoEnumIterator};
|
||||
|
||||
use crate::{preprocess_anthropic_request, LanguageModel, LanguageModelRequest};
|
||||
use crate::LanguageModel;
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, EnumIter)]
|
||||
pub enum CloudModel {
|
||||
pub enum ZedDotDevModel {
|
||||
Gpt3Point5Turbo,
|
||||
Gpt4,
|
||||
Gpt4Turbo,
|
||||
@@ -29,7 +29,7 @@ pub enum CloudModel {
|
||||
Custom(String),
|
||||
}
|
||||
|
||||
impl Serialize for CloudModel {
|
||||
impl Serialize for ZedDotDevModel {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
@@ -38,7 +38,7 @@ impl Serialize for CloudModel {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for CloudModel {
|
||||
impl<'de> Deserialize<'de> for ZedDotDevModel {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
@@ -46,7 +46,7 @@ impl<'de> Deserialize<'de> for CloudModel {
|
||||
struct ZedDotDevModelVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for ZedDotDevModelVisitor {
|
||||
type Value = CloudModel;
|
||||
type Value = ZedDotDevModel;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str("a string for a ZedDotDevModel variant or a custom model")
|
||||
@@ -56,9 +56,9 @@ impl<'de> Deserialize<'de> for CloudModel {
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
let model = CloudModel::iter()
|
||||
let model = ZedDotDevModel::iter()
|
||||
.find(|model| model.id() == value)
|
||||
.unwrap_or_else(|| CloudModel::Custom(value.to_string()));
|
||||
.unwrap_or_else(|| ZedDotDevModel::Custom(value.to_string()));
|
||||
Ok(model)
|
||||
}
|
||||
}
|
||||
@@ -67,13 +67,13 @@ impl<'de> Deserialize<'de> for CloudModel {
|
||||
}
|
||||
}
|
||||
|
||||
impl JsonSchema for CloudModel {
|
||||
impl JsonSchema for ZedDotDevModel {
|
||||
fn schema_name() -> String {
|
||||
"ZedDotDevModel".to_owned()
|
||||
}
|
||||
|
||||
fn json_schema(_generator: &mut schemars::gen::SchemaGenerator) -> Schema {
|
||||
let variants = CloudModel::iter()
|
||||
let variants = ZedDotDevModel::iter()
|
||||
.filter_map(|model| {
|
||||
let id = model.id();
|
||||
if id.is_empty() {
|
||||
@@ -88,7 +88,7 @@ impl JsonSchema for CloudModel {
|
||||
enum_values: Some(variants.iter().map(|s| s.clone().into()).collect()),
|
||||
metadata: Some(Box::new(Metadata {
|
||||
title: Some("ZedDotDevModel".to_owned()),
|
||||
default: Some(CloudModel::default().id().into()),
|
||||
default: Some(ZedDotDevModel::default().id().into()),
|
||||
examples: variants.into_iter().map(Into::into).collect(),
|
||||
..Default::default()
|
||||
})),
|
||||
@@ -97,7 +97,7 @@ impl JsonSchema for CloudModel {
|
||||
}
|
||||
}
|
||||
|
||||
impl CloudModel {
|
||||
impl ZedDotDevModel {
|
||||
pub fn id(&self) -> &str {
|
||||
match self {
|
||||
Self::Gpt3Point5Turbo => "gpt-3.5-turbo",
|
||||
@@ -133,15 +133,6 @@ impl CloudModel {
|
||||
Self::Custom(_) => 4096, // TODO: Make this configurable
|
||||
}
|
||||
}
|
||||
|
||||
pub fn preprocess_request(&self, request: &mut LanguageModelRequest) {
|
||||
match self {
|
||||
Self::Claude3Opus | Self::Claude3Sonnet | Self::Claude3Haiku => {
|
||||
preprocess_anthropic_request(request)
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
@@ -156,7 +147,7 @@ pub enum AssistantDockPosition {
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum AssistantProvider {
|
||||
ZedDotDev {
|
||||
model: CloudModel,
|
||||
model: ZedDotDevModel,
|
||||
},
|
||||
OpenAi {
|
||||
model: OpenAiModel,
|
||||
@@ -184,7 +175,9 @@ impl Default for AssistantProvider {
|
||||
#[serde(tag = "name", rename_all = "snake_case")]
|
||||
pub enum AssistantProviderContent {
|
||||
#[serde(rename = "zed.dev")]
|
||||
ZedDotDev { default_model: Option<CloudModel> },
|
||||
ZedDotDev {
|
||||
default_model: Option<ZedDotDevModel>,
|
||||
},
|
||||
#[serde(rename = "openai")]
|
||||
OpenAi {
|
||||
default_model: Option<OpenAiModel>,
|
||||
@@ -288,7 +281,7 @@ impl AssistantSettingsContent {
|
||||
Some(AssistantProviderContent::ZedDotDev {
|
||||
default_model: model,
|
||||
}) => {
|
||||
if let LanguageModel::Cloud(new_model) = new_model {
|
||||
if let LanguageModel::ZedDotDev(new_model) = new_model {
|
||||
*model = Some(new_model);
|
||||
}
|
||||
}
|
||||
@@ -309,7 +302,7 @@ impl AssistantSettingsContent {
|
||||
}
|
||||
}
|
||||
provider => match new_model {
|
||||
LanguageModel::Cloud(model) => {
|
||||
LanguageModel::ZedDotDev(model) => {
|
||||
*provider = Some(AssistantProviderContent::ZedDotDev {
|
||||
default_model: Some(model),
|
||||
})
|
||||
@@ -620,7 +613,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
AssistantSettings::get_global(cx).provider,
|
||||
AssistantProvider::ZedDotDev {
|
||||
model: CloudModel::Custom("custom".into())
|
||||
model: ZedDotDevModel::Custom("custom".into())
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ use language::{Rope, TransactionId};
|
||||
use multi_buffer::MultiBufferRow;
|
||||
use std::{cmp, future, ops::Range, sync::Arc, time::Instant};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Event {
|
||||
Finished,
|
||||
Undone,
|
||||
@@ -121,98 +120,91 @@ impl Codegen {
|
||||
let mut edit_start = range.start.to_offset(&snapshot);
|
||||
|
||||
let (mut hunks_tx, mut hunks_rx) = mpsc::channel(1);
|
||||
let diff: Task<anyhow::Result<()>> =
|
||||
cx.background_executor().spawn(async move {
|
||||
let mut response_latency = None;
|
||||
let request_start = Instant::now();
|
||||
let diff = async {
|
||||
let chunks = strip_invalid_spans_from_codeblock(response.await?);
|
||||
futures::pin_mut!(chunks);
|
||||
let mut diff = StreamingDiff::new(selected_text.to_string());
|
||||
let diff = cx.background_executor().spawn(async move {
|
||||
let mut response_latency = None;
|
||||
let request_start = Instant::now();
|
||||
let diff = async {
|
||||
let chunks = strip_invalid_spans_from_codeblock(response.await?);
|
||||
futures::pin_mut!(chunks);
|
||||
let mut diff = StreamingDiff::new(selected_text.to_string());
|
||||
|
||||
let mut new_text = String::new();
|
||||
let mut base_indent = None;
|
||||
let mut line_indent = None;
|
||||
let mut first_line = true;
|
||||
let mut new_text = String::new();
|
||||
let mut base_indent = None;
|
||||
let mut line_indent = None;
|
||||
let mut first_line = true;
|
||||
|
||||
while let Some(chunk) = chunks.next().await {
|
||||
if response_latency.is_none() {
|
||||
response_latency = Some(request_start.elapsed());
|
||||
}
|
||||
let chunk = chunk?;
|
||||
while let Some(chunk) = chunks.next().await {
|
||||
if response_latency.is_none() {
|
||||
response_latency = Some(request_start.elapsed());
|
||||
}
|
||||
let chunk = chunk?;
|
||||
|
||||
let mut lines = chunk.split('\n').peekable();
|
||||
while let Some(line) = lines.next() {
|
||||
new_text.push_str(line);
|
||||
if line_indent.is_none() {
|
||||
if let Some(non_whitespace_ch_ix) =
|
||||
new_text.find(|ch: char| !ch.is_whitespace())
|
||||
{
|
||||
line_indent = Some(non_whitespace_ch_ix);
|
||||
base_indent = base_indent.or(line_indent);
|
||||
let mut lines = chunk.split('\n').peekable();
|
||||
while let Some(line) = lines.next() {
|
||||
new_text.push_str(line);
|
||||
if line_indent.is_none() {
|
||||
if let Some(non_whitespace_ch_ix) =
|
||||
new_text.find(|ch: char| !ch.is_whitespace())
|
||||
{
|
||||
line_indent = Some(non_whitespace_ch_ix);
|
||||
base_indent = base_indent.or(line_indent);
|
||||
|
||||
let line_indent = line_indent.unwrap();
|
||||
let base_indent = base_indent.unwrap();
|
||||
let indent_delta =
|
||||
line_indent as i32 - base_indent as i32;
|
||||
let mut corrected_indent_len = cmp::max(
|
||||
0,
|
||||
suggested_line_indent.len as i32 + indent_delta,
|
||||
)
|
||||
as usize;
|
||||
if first_line {
|
||||
corrected_indent_len = corrected_indent_len
|
||||
.saturating_sub(
|
||||
selection_start.column as usize,
|
||||
);
|
||||
}
|
||||
|
||||
let indent_char = suggested_line_indent.char();
|
||||
let mut indent_buffer = [0; 4];
|
||||
let indent_str =
|
||||
indent_char.encode_utf8(&mut indent_buffer);
|
||||
new_text.replace_range(
|
||||
..line_indent,
|
||||
&indent_str.repeat(corrected_indent_len),
|
||||
);
|
||||
let line_indent = line_indent.unwrap();
|
||||
let base_indent = base_indent.unwrap();
|
||||
let indent_delta =
|
||||
line_indent as i32 - base_indent as i32;
|
||||
let mut corrected_indent_len = cmp::max(
|
||||
0,
|
||||
suggested_line_indent.len as i32 + indent_delta,
|
||||
)
|
||||
as usize;
|
||||
if first_line {
|
||||
corrected_indent_len = corrected_indent_len
|
||||
.saturating_sub(
|
||||
selection_start.column as usize,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if line_indent.is_some() {
|
||||
hunks_tx.send(diff.push_new(&new_text)).await?;
|
||||
new_text.clear();
|
||||
let indent_char = suggested_line_indent.char();
|
||||
let mut indent_buffer = [0; 4];
|
||||
let indent_str =
|
||||
indent_char.encode_utf8(&mut indent_buffer);
|
||||
new_text.replace_range(
|
||||
..line_indent,
|
||||
&indent_str.repeat(corrected_indent_len),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if lines.peek().is_some() {
|
||||
hunks_tx.send(diff.push_new("\n")).await?;
|
||||
line_indent = None;
|
||||
first_line = false;
|
||||
}
|
||||
if line_indent.is_some() {
|
||||
hunks_tx.send(diff.push_new(&new_text)).await?;
|
||||
new_text.clear();
|
||||
}
|
||||
|
||||
if lines.peek().is_some() {
|
||||
hunks_tx.send(diff.push_new("\n")).await?;
|
||||
line_indent = None;
|
||||
first_line = false;
|
||||
}
|
||||
}
|
||||
hunks_tx.send(diff.push_new(&new_text)).await?;
|
||||
hunks_tx.send(diff.finish()).await?;
|
||||
|
||||
anyhow::Ok(())
|
||||
};
|
||||
|
||||
let result = diff.await;
|
||||
|
||||
let error_message =
|
||||
result.as_ref().err().map(|error| error.to_string());
|
||||
if let Some(telemetry) = telemetry {
|
||||
telemetry.report_assistant_event(
|
||||
None,
|
||||
telemetry_events::AssistantKind::Inline,
|
||||
model_telemetry_id,
|
||||
response_latency,
|
||||
error_message,
|
||||
);
|
||||
}
|
||||
hunks_tx.send(diff.push_new(&new_text)).await?;
|
||||
hunks_tx.send(diff.finish()).await?;
|
||||
|
||||
result?;
|
||||
Ok(())
|
||||
});
|
||||
anyhow::Ok(())
|
||||
};
|
||||
|
||||
let error_message = diff.await.err().map(|error| error.to_string());
|
||||
if let Some(telemetry) = telemetry {
|
||||
telemetry.report_assistant_event(
|
||||
None,
|
||||
telemetry_events::AssistantKind::Inline,
|
||||
model_telemetry_id,
|
||||
response_latency,
|
||||
error_message,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
while let Some(hunks) = hunks_rx.next().await {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
@@ -274,7 +266,7 @@ impl Codegen {
|
||||
})?;
|
||||
}
|
||||
|
||||
diff.await?;
|
||||
diff.await;
|
||||
|
||||
anyhow::Ok(())
|
||||
};
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
mod anthropic;
|
||||
mod cloud;
|
||||
#[cfg(test)]
|
||||
mod fake;
|
||||
mod open_ai;
|
||||
mod zed;
|
||||
|
||||
pub use anthropic::*;
|
||||
pub use cloud::*;
|
||||
#[cfg(test)]
|
||||
pub use fake::*;
|
||||
pub use open_ai::*;
|
||||
pub use zed::*;
|
||||
|
||||
use crate::{
|
||||
assistant_settings::{AssistantProvider, AssistantSettings},
|
||||
@@ -25,8 +25,8 @@ use std::time::Duration;
|
||||
pub fn init(client: Arc<Client>, cx: &mut AppContext) {
|
||||
let mut settings_version = 0;
|
||||
let provider = match &AssistantSettings::get_global(cx).provider {
|
||||
AssistantProvider::ZedDotDev { model } => CompletionProvider::Cloud(
|
||||
CloudCompletionProvider::new(model.clone(), client.clone(), settings_version, cx),
|
||||
AssistantProvider::ZedDotDev { model } => CompletionProvider::ZedDotDev(
|
||||
ZedDotDevCompletionProvider::new(model.clone(), client.clone(), settings_version, cx),
|
||||
),
|
||||
AssistantProvider::OpenAi {
|
||||
model,
|
||||
@@ -87,11 +87,14 @@ pub fn init(client: Arc<Client>, cx: &mut AppContext) {
|
||||
settings_version,
|
||||
);
|
||||
}
|
||||
(CompletionProvider::Cloud(provider), AssistantProvider::ZedDotDev { model }) => {
|
||||
(
|
||||
CompletionProvider::ZedDotDev(provider),
|
||||
AssistantProvider::ZedDotDev { model },
|
||||
) => {
|
||||
provider.update(model.clone(), settings_version);
|
||||
}
|
||||
(_, AssistantProvider::ZedDotDev { model }) => {
|
||||
*provider = CompletionProvider::Cloud(CloudCompletionProvider::new(
|
||||
*provider = CompletionProvider::ZedDotDev(ZedDotDevCompletionProvider::new(
|
||||
model.clone(),
|
||||
client.clone(),
|
||||
settings_version,
|
||||
@@ -139,7 +142,7 @@ pub fn init(client: Arc<Client>, cx: &mut AppContext) {
|
||||
pub enum CompletionProvider {
|
||||
OpenAi(OpenAiCompletionProvider),
|
||||
Anthropic(AnthropicCompletionProvider),
|
||||
Cloud(CloudCompletionProvider),
|
||||
ZedDotDev(ZedDotDevCompletionProvider),
|
||||
#[cfg(test)]
|
||||
Fake(FakeCompletionProvider),
|
||||
}
|
||||
@@ -161,9 +164,9 @@ impl CompletionProvider {
|
||||
.available_models()
|
||||
.map(LanguageModel::Anthropic)
|
||||
.collect(),
|
||||
CompletionProvider::Cloud(provider) => provider
|
||||
CompletionProvider::ZedDotDev(provider) => provider
|
||||
.available_models()
|
||||
.map(LanguageModel::Cloud)
|
||||
.map(LanguageModel::ZedDotDev)
|
||||
.collect(),
|
||||
#[cfg(test)]
|
||||
CompletionProvider::Fake(_) => unimplemented!(),
|
||||
@@ -174,7 +177,7 @@ impl CompletionProvider {
|
||||
match self {
|
||||
CompletionProvider::OpenAi(provider) => provider.settings_version(),
|
||||
CompletionProvider::Anthropic(provider) => provider.settings_version(),
|
||||
CompletionProvider::Cloud(provider) => provider.settings_version(),
|
||||
CompletionProvider::ZedDotDev(provider) => provider.settings_version(),
|
||||
#[cfg(test)]
|
||||
CompletionProvider::Fake(_) => unimplemented!(),
|
||||
}
|
||||
@@ -184,7 +187,7 @@ impl CompletionProvider {
|
||||
match self {
|
||||
CompletionProvider::OpenAi(provider) => provider.is_authenticated(),
|
||||
CompletionProvider::Anthropic(provider) => provider.is_authenticated(),
|
||||
CompletionProvider::Cloud(provider) => provider.is_authenticated(),
|
||||
CompletionProvider::ZedDotDev(provider) => provider.is_authenticated(),
|
||||
#[cfg(test)]
|
||||
CompletionProvider::Fake(_) => true,
|
||||
}
|
||||
@@ -194,7 +197,7 @@ impl CompletionProvider {
|
||||
match self {
|
||||
CompletionProvider::OpenAi(provider) => provider.authenticate(cx),
|
||||
CompletionProvider::Anthropic(provider) => provider.authenticate(cx),
|
||||
CompletionProvider::Cloud(provider) => provider.authenticate(cx),
|
||||
CompletionProvider::ZedDotDev(provider) => provider.authenticate(cx),
|
||||
#[cfg(test)]
|
||||
CompletionProvider::Fake(_) => Task::ready(Ok(())),
|
||||
}
|
||||
@@ -204,7 +207,7 @@ impl CompletionProvider {
|
||||
match self {
|
||||
CompletionProvider::OpenAi(provider) => provider.authentication_prompt(cx),
|
||||
CompletionProvider::Anthropic(provider) => provider.authentication_prompt(cx),
|
||||
CompletionProvider::Cloud(provider) => provider.authentication_prompt(cx),
|
||||
CompletionProvider::ZedDotDev(provider) => provider.authentication_prompt(cx),
|
||||
#[cfg(test)]
|
||||
CompletionProvider::Fake(_) => unimplemented!(),
|
||||
}
|
||||
@@ -214,7 +217,7 @@ impl CompletionProvider {
|
||||
match self {
|
||||
CompletionProvider::OpenAi(provider) => provider.reset_credentials(cx),
|
||||
CompletionProvider::Anthropic(provider) => provider.reset_credentials(cx),
|
||||
CompletionProvider::Cloud(_) => Task::ready(Ok(())),
|
||||
CompletionProvider::ZedDotDev(_) => Task::ready(Ok(())),
|
||||
#[cfg(test)]
|
||||
CompletionProvider::Fake(_) => Task::ready(Ok(())),
|
||||
}
|
||||
@@ -224,7 +227,7 @@ impl CompletionProvider {
|
||||
match self {
|
||||
CompletionProvider::OpenAi(provider) => LanguageModel::OpenAi(provider.model()),
|
||||
CompletionProvider::Anthropic(provider) => LanguageModel::Anthropic(provider.model()),
|
||||
CompletionProvider::Cloud(provider) => LanguageModel::Cloud(provider.model()),
|
||||
CompletionProvider::ZedDotDev(provider) => LanguageModel::ZedDotDev(provider.model()),
|
||||
#[cfg(test)]
|
||||
CompletionProvider::Fake(_) => LanguageModel::default(),
|
||||
}
|
||||
@@ -238,7 +241,7 @@ impl CompletionProvider {
|
||||
match self {
|
||||
CompletionProvider::OpenAi(provider) => provider.count_tokens(request, cx),
|
||||
CompletionProvider::Anthropic(provider) => provider.count_tokens(request, cx),
|
||||
CompletionProvider::Cloud(provider) => provider.count_tokens(request, cx),
|
||||
CompletionProvider::ZedDotDev(provider) => provider.count_tokens(request, cx),
|
||||
#[cfg(test)]
|
||||
CompletionProvider::Fake(_) => futures::FutureExt::boxed(futures::future::ready(Ok(0))),
|
||||
}
|
||||
@@ -251,7 +254,7 @@ impl CompletionProvider {
|
||||
match self {
|
||||
CompletionProvider::OpenAi(provider) => provider.complete(request),
|
||||
CompletionProvider::Anthropic(provider) => provider.complete(request),
|
||||
CompletionProvider::Cloud(provider) => provider.complete(request),
|
||||
CompletionProvider::ZedDotDev(provider) => provider.complete(request),
|
||||
#[cfg(test)]
|
||||
CompletionProvider::Fake(provider) => provider.complete(),
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use crate::count_open_ai_tokens;
|
||||
use crate::{
|
||||
assistant_settings::AnthropicModel, CompletionProvider, LanguageModel, LanguageModelRequest,
|
||||
Role,
|
||||
};
|
||||
use crate::{count_open_ai_tokens, LanguageModelRequestMessage};
|
||||
use anthropic::{stream_completion, Request, RequestMessage};
|
||||
use anthropic::{stream_completion, Request, RequestMessage, Role as AnthropicRole};
|
||||
use anyhow::{anyhow, Result};
|
||||
use editor::{Editor, EditorElement, EditorStyle};
|
||||
use futures::{future::BoxFuture, stream::BoxStream, FutureExt, StreamExt};
|
||||
@@ -167,37 +167,53 @@ impl AnthropicCompletionProvider {
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn to_anthropic_request(&self, mut request: LanguageModelRequest) -> Request {
|
||||
preprocess_anthropic_request(&mut request);
|
||||
|
||||
fn to_anthropic_request(&self, request: LanguageModelRequest) -> Request {
|
||||
let model = match request.model {
|
||||
LanguageModel::Anthropic(model) => model,
|
||||
_ => self.model(),
|
||||
};
|
||||
|
||||
let mut system_message = String::new();
|
||||
if request
|
||||
.messages
|
||||
.first()
|
||||
.map_or(false, |message| message.role == Role::System)
|
||||
{
|
||||
system_message = request.messages.remove(0).content;
|
||||
|
||||
let mut messages: Vec<RequestMessage> = Vec::new();
|
||||
for message in request.messages {
|
||||
if message.content.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
match message.role {
|
||||
Role::User | Role::Assistant => {
|
||||
let role = match message.role {
|
||||
Role::User => AnthropicRole::User,
|
||||
Role::Assistant => AnthropicRole::Assistant,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
if let Some(last_message) = messages.last_mut() {
|
||||
if last_message.role == role {
|
||||
last_message.content.push_str("\n\n");
|
||||
last_message.content.push_str(&message.content);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
messages.push(RequestMessage {
|
||||
role,
|
||||
content: message.content,
|
||||
});
|
||||
}
|
||||
Role::System => {
|
||||
if !system_message.is_empty() {
|
||||
system_message.push_str("\n\n");
|
||||
}
|
||||
system_message.push_str(&message.content);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Request {
|
||||
model,
|
||||
messages: request
|
||||
.messages
|
||||
.iter()
|
||||
.map(|msg| RequestMessage {
|
||||
role: match msg.role {
|
||||
Role::User => anthropic::Role::User,
|
||||
Role::Assistant => anthropic::Role::Assistant,
|
||||
Role::System => unreachable!("filtered out by preprocess_request"),
|
||||
},
|
||||
content: msg.content.clone(),
|
||||
})
|
||||
.collect(),
|
||||
messages,
|
||||
stream: true,
|
||||
system: system_message,
|
||||
max_tokens: 4092,
|
||||
@@ -205,49 +221,6 @@ impl AnthropicCompletionProvider {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn preprocess_anthropic_request(request: &mut LanguageModelRequest) {
|
||||
let mut new_messages: Vec<LanguageModelRequestMessage> = Vec::new();
|
||||
let mut system_message = String::new();
|
||||
|
||||
for message in request.messages.drain(..) {
|
||||
if message.content.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
match message.role {
|
||||
Role::User | Role::Assistant => {
|
||||
if let Some(last_message) = new_messages.last_mut() {
|
||||
if last_message.role == message.role {
|
||||
last_message.content.push_str("\n\n");
|
||||
last_message.content.push_str(&message.content);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
new_messages.push(message);
|
||||
}
|
||||
Role::System => {
|
||||
if !system_message.is_empty() {
|
||||
system_message.push_str("\n\n");
|
||||
}
|
||||
system_message.push_str(&message.content);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !system_message.is_empty() {
|
||||
request.messages.insert(
|
||||
0,
|
||||
LanguageModelRequestMessage {
|
||||
role: Role::System,
|
||||
content: system_message,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
request.messages = new_messages;
|
||||
}
|
||||
|
||||
struct AuthenticationPrompt {
|
||||
api_key: View<Editor>,
|
||||
api_url: String,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::assistant_settings::CloudModel;
|
||||
use crate::assistant_settings::ZedDotDevModel;
|
||||
use crate::{
|
||||
assistant_settings::OpenAiModel, CompletionProvider, LanguageModel, LanguageModelRequest, Role,
|
||||
};
|
||||
@@ -210,9 +210,9 @@ pub fn count_open_ai_tokens(
|
||||
|
||||
match request.model {
|
||||
LanguageModel::Anthropic(_)
|
||||
| LanguageModel::Cloud(CloudModel::Claude3Opus)
|
||||
| LanguageModel::Cloud(CloudModel::Claude3Sonnet)
|
||||
| LanguageModel::Cloud(CloudModel::Claude3Haiku) => {
|
||||
| LanguageModel::ZedDotDev(ZedDotDevModel::Claude3Opus)
|
||||
| LanguageModel::ZedDotDev(ZedDotDevModel::Claude3Sonnet)
|
||||
| LanguageModel::ZedDotDev(ZedDotDevModel::Claude3Haiku) => {
|
||||
// Tiktoken doesn't yet support these models, so we manually use the
|
||||
// same tokenizer as GPT-4.
|
||||
tiktoken_rs::num_tokens_from_messages("gpt-4", &messages)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::{
|
||||
assistant_settings::CloudModel, count_open_ai_tokens, CompletionProvider, LanguageModel,
|
||||
assistant_settings::ZedDotDevModel, count_open_ai_tokens, CompletionProvider, LanguageModel,
|
||||
LanguageModelRequest,
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
@@ -10,17 +10,17 @@ use std::{future, sync::Arc};
|
||||
use strum::IntoEnumIterator;
|
||||
use ui::prelude::*;
|
||||
|
||||
pub struct CloudCompletionProvider {
|
||||
pub struct ZedDotDevCompletionProvider {
|
||||
client: Arc<Client>,
|
||||
model: CloudModel,
|
||||
model: ZedDotDevModel,
|
||||
settings_version: usize,
|
||||
status: client::Status,
|
||||
_maintain_client_status: Task<()>,
|
||||
}
|
||||
|
||||
impl CloudCompletionProvider {
|
||||
impl ZedDotDevCompletionProvider {
|
||||
pub fn new(
|
||||
model: CloudModel,
|
||||
model: ZedDotDevModel,
|
||||
client: Arc<Client>,
|
||||
settings_version: usize,
|
||||
cx: &mut AppContext,
|
||||
@@ -30,7 +30,7 @@ impl CloudCompletionProvider {
|
||||
let maintain_client_status = cx.spawn(|mut cx| async move {
|
||||
while let Some(status) = status_rx.next().await {
|
||||
let _ = cx.update_global::<CompletionProvider, _>(|provider, _cx| {
|
||||
if let CompletionProvider::Cloud(provider) = provider {
|
||||
if let CompletionProvider::ZedDotDev(provider) = provider {
|
||||
provider.status = status;
|
||||
} else {
|
||||
unreachable!()
|
||||
@@ -47,20 +47,20 @@ impl CloudCompletionProvider {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(&mut self, model: CloudModel, settings_version: usize) {
|
||||
pub fn update(&mut self, model: ZedDotDevModel, settings_version: usize) {
|
||||
self.model = model;
|
||||
self.settings_version = settings_version;
|
||||
}
|
||||
|
||||
pub fn available_models(&self) -> impl Iterator<Item = CloudModel> {
|
||||
let mut custom_model = if let CloudModel::Custom(custom_model) = self.model.clone() {
|
||||
pub fn available_models(&self) -> impl Iterator<Item = ZedDotDevModel> {
|
||||
let mut custom_model = if let ZedDotDevModel::Custom(custom_model) = self.model.clone() {
|
||||
Some(custom_model)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
CloudModel::iter().filter_map(move |model| {
|
||||
if let CloudModel::Custom(_) = model {
|
||||
Some(CloudModel::Custom(custom_model.take()?))
|
||||
ZedDotDevModel::iter().filter_map(move |model| {
|
||||
if let ZedDotDevModel::Custom(_) = model {
|
||||
Some(ZedDotDevModel::Custom(custom_model.take()?))
|
||||
} else {
|
||||
Some(model)
|
||||
}
|
||||
@@ -71,7 +71,7 @@ impl CloudCompletionProvider {
|
||||
self.settings_version
|
||||
}
|
||||
|
||||
pub fn model(&self) -> CloudModel {
|
||||
pub fn model(&self) -> ZedDotDevModel {
|
||||
self.model.clone()
|
||||
}
|
||||
|
||||
@@ -94,19 +94,21 @@ impl CloudCompletionProvider {
|
||||
cx: &AppContext,
|
||||
) -> BoxFuture<'static, Result<usize>> {
|
||||
match request.model {
|
||||
LanguageModel::Cloud(CloudModel::Gpt4)
|
||||
| LanguageModel::Cloud(CloudModel::Gpt4Turbo)
|
||||
| LanguageModel::Cloud(CloudModel::Gpt4Omni)
|
||||
| LanguageModel::Cloud(CloudModel::Gpt3Point5Turbo) => {
|
||||
LanguageModel::ZedDotDev(ZedDotDevModel::Gpt4)
|
||||
| LanguageModel::ZedDotDev(ZedDotDevModel::Gpt4Turbo)
|
||||
| LanguageModel::ZedDotDev(ZedDotDevModel::Gpt4Omni)
|
||||
| LanguageModel::ZedDotDev(ZedDotDevModel::Gpt3Point5Turbo) => {
|
||||
count_open_ai_tokens(request, cx.background_executor())
|
||||
}
|
||||
LanguageModel::Cloud(
|
||||
CloudModel::Claude3Opus | CloudModel::Claude3Sonnet | CloudModel::Claude3Haiku,
|
||||
LanguageModel::ZedDotDev(
|
||||
ZedDotDevModel::Claude3Opus
|
||||
| ZedDotDevModel::Claude3Sonnet
|
||||
| ZedDotDevModel::Claude3Haiku,
|
||||
) => {
|
||||
// Can't find a tokenizer for Claude 3, so for now just use the same as OpenAI's as an approximation.
|
||||
count_open_ai_tokens(request, cx.background_executor())
|
||||
}
|
||||
LanguageModel::Cloud(CloudModel::Custom(model)) => {
|
||||
LanguageModel::ZedDotDev(ZedDotDevModel::Custom(model)) => {
|
||||
let request = self.client.request(proto::CountTokensWithLanguageModel {
|
||||
model,
|
||||
messages: request
|
||||
@@ -127,10 +129,8 @@ impl CloudCompletionProvider {
|
||||
|
||||
pub fn complete(
|
||||
&self,
|
||||
mut request: LanguageModelRequest,
|
||||
request: LanguageModelRequest,
|
||||
) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>> {
|
||||
request.preprocess();
|
||||
|
||||
let request = proto::CompleteWithLanguageModel {
|
||||
model: request.model.id().to_string(),
|
||||
messages: request
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,95 +1,7 @@
|
||||
use language::BufferSnapshot;
|
||||
use std::{fmt::Write, ops::Range};
|
||||
mod prompt;
|
||||
mod prompt_library;
|
||||
mod prompt_manager;
|
||||
|
||||
pub fn generate_content_prompt(
|
||||
user_prompt: String,
|
||||
language_name: Option<&str>,
|
||||
buffer: BufferSnapshot,
|
||||
range: Range<usize>,
|
||||
project_name: Option<String>,
|
||||
) -> anyhow::Result<String> {
|
||||
let mut prompt = String::new();
|
||||
|
||||
let content_type = match language_name {
|
||||
None | Some("Markdown" | "Plain Text") => {
|
||||
writeln!(prompt, "You are an expert engineer.")?;
|
||||
"Text"
|
||||
}
|
||||
Some(language_name) => {
|
||||
writeln!(prompt, "You are an expert {language_name} engineer.")?;
|
||||
writeln!(
|
||||
prompt,
|
||||
"Your answer MUST always and only be valid {}.",
|
||||
language_name
|
||||
)?;
|
||||
"Code"
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(project_name) = project_name {
|
||||
writeln!(
|
||||
prompt,
|
||||
"You are currently working inside the '{project_name}' project in code editor Zed."
|
||||
)?;
|
||||
}
|
||||
|
||||
// Include file content.
|
||||
for chunk in buffer.text_for_range(0..range.start) {
|
||||
prompt.push_str(chunk);
|
||||
}
|
||||
|
||||
if range.is_empty() {
|
||||
prompt.push_str("<|START|>");
|
||||
} else {
|
||||
prompt.push_str("<|START|");
|
||||
}
|
||||
|
||||
for chunk in buffer.text_for_range(range.clone()) {
|
||||
prompt.push_str(chunk);
|
||||
}
|
||||
|
||||
if !range.is_empty() {
|
||||
prompt.push_str("|END|>");
|
||||
}
|
||||
|
||||
for chunk in buffer.text_for_range(range.end..buffer.len()) {
|
||||
prompt.push_str(chunk);
|
||||
}
|
||||
|
||||
prompt.push('\n');
|
||||
|
||||
if range.is_empty() {
|
||||
writeln!(
|
||||
prompt,
|
||||
"Assume the cursor is located where the `<|START|>` span is."
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(
|
||||
prompt,
|
||||
"{content_type} can't be replaced, so assume your answer will be inserted at the cursor.",
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(
|
||||
prompt,
|
||||
"Generate {content_type} based on the users prompt: {user_prompt}",
|
||||
)
|
||||
.unwrap();
|
||||
} else {
|
||||
writeln!(prompt, "Modify the user's selected {content_type} based upon the users prompt: '{user_prompt}'").unwrap();
|
||||
writeln!(prompt, "You must reply with only the adjusted {content_type} (within the '<|START|' and '|END|>' spans) not the entire file.").unwrap();
|
||||
writeln!(
|
||||
prompt,
|
||||
"Double check that you only return code and not the '<|START|' and '|END|'> spans"
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
writeln!(prompt, "Never make remarks about the output.").unwrap();
|
||||
writeln!(
|
||||
prompt,
|
||||
"Do not return anything else, except the generated {content_type}."
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
Ok(prompt)
|
||||
}
|
||||
pub use prompt::*;
|
||||
pub use prompt_library::*;
|
||||
pub use prompt_manager::*;
|
||||
|
||||
360
crates/assistant/src/prompts/prompt.rs
Normal file
360
crates/assistant/src/prompts/prompt.rs
Normal file
@@ -0,0 +1,360 @@
|
||||
use fs::Fs;
|
||||
use language::BufferSnapshot;
|
||||
use std::{fmt::Write, ops::Range, path::PathBuf, sync::Arc};
|
||||
use ui::SharedString;
|
||||
use util::paths::PROMPTS_DIR;
|
||||
|
||||
use gray_matter::{engine::YAML, Matter};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::prompt_library::PromptId;
|
||||
|
||||
pub const PROMPT_DEFAULT_TITLE: &str = "Untitled Prompt";
|
||||
|
||||
fn standardize_value(value: String) -> String {
|
||||
value.replace(['\n', '\r', '"', '\''], "")
|
||||
}
|
||||
|
||||
fn slugify(input: String) -> String {
|
||||
let mut slug = String::new();
|
||||
for c in input.chars() {
|
||||
if c.is_alphanumeric() {
|
||||
slug.push(c.to_ascii_lowercase());
|
||||
} else if c.is_whitespace() {
|
||||
slug.push('-');
|
||||
} else {
|
||||
slug.push('_');
|
||||
}
|
||||
}
|
||||
slug
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
pub struct StaticPromptFrontmatter {
|
||||
title: String,
|
||||
version: String,
|
||||
author: String,
|
||||
#[serde(default)]
|
||||
languages: Vec<String>,
|
||||
#[serde(default)]
|
||||
dependencies: Vec<String>,
|
||||
}
|
||||
|
||||
impl Default for StaticPromptFrontmatter {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
title: PROMPT_DEFAULT_TITLE.to_string(),
|
||||
version: "1.0".to_string(),
|
||||
author: "You <you@email.com>".to_string(),
|
||||
languages: vec![],
|
||||
dependencies: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StaticPromptFrontmatter {
|
||||
/// Returns the frontmatter as a markdown frontmatter string
|
||||
pub fn frontmatter_string(&self) -> String {
|
||||
let mut frontmatter = format!(
|
||||
"---\ntitle: \"{}\"\nversion: \"{}\"\nauthor: \"{}\"\n",
|
||||
standardize_value(self.title.clone()),
|
||||
standardize_value(self.version.clone()),
|
||||
standardize_value(self.author.clone()),
|
||||
);
|
||||
|
||||
if !self.languages.is_empty() {
|
||||
let languages = self
|
||||
.languages
|
||||
.iter()
|
||||
.map(|l| standardize_value(l.clone()))
|
||||
.collect::<Vec<String>>()
|
||||
.join(", ");
|
||||
writeln!(frontmatter, "languages: [{}]", languages).unwrap();
|
||||
}
|
||||
|
||||
if !self.dependencies.is_empty() {
|
||||
let dependencies = self
|
||||
.dependencies
|
||||
.iter()
|
||||
.map(|d| standardize_value(d.clone()))
|
||||
.collect::<Vec<String>>()
|
||||
.join(", ");
|
||||
writeln!(frontmatter, "dependencies: [{}]", dependencies).unwrap();
|
||||
}
|
||||
|
||||
frontmatter.push_str("---\n");
|
||||
|
||||
frontmatter
|
||||
}
|
||||
}
|
||||
|
||||
/// A static prompt that can be loaded into the prompt library
|
||||
/// from Markdown with a frontmatter header
|
||||
///
|
||||
/// Examples:
|
||||
///
|
||||
/// ### Globally available prompt
|
||||
///
|
||||
/// ```markdown
|
||||
/// ---
|
||||
/// title: Foo
|
||||
/// version: 1.0
|
||||
/// author: Jane Kim <jane@kim.com
|
||||
/// languages: ["*"]
|
||||
/// dependencies: []
|
||||
/// ---
|
||||
///
|
||||
/// Foo and bar are terms used in programming to describe generic concepts.
|
||||
/// ```
|
||||
///
|
||||
/// ### Language-specific prompt
|
||||
///
|
||||
/// ```markdown
|
||||
/// ---
|
||||
/// title: UI with GPUI
|
||||
/// version: 1.0
|
||||
/// author: Nate Butler <iamnbutler@gmail.com>
|
||||
/// languages: ["rust"]
|
||||
/// dependencies: ["gpui"]
|
||||
/// ---
|
||||
///
|
||||
/// When building a UI with GPUI, ensure you...
|
||||
/// ```
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
pub struct StaticPrompt {
|
||||
#[serde(skip_deserializing)]
|
||||
id: PromptId,
|
||||
#[serde(skip)]
|
||||
metadata: StaticPromptFrontmatter,
|
||||
content: String,
|
||||
file_name: Option<SharedString>,
|
||||
}
|
||||
|
||||
impl Default for StaticPrompt {
|
||||
fn default() -> Self {
|
||||
let metadata = StaticPromptFrontmatter::default();
|
||||
|
||||
let content = metadata.clone().frontmatter_string();
|
||||
|
||||
Self {
|
||||
id: PromptId::new(),
|
||||
metadata,
|
||||
content,
|
||||
file_name: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StaticPrompt {
|
||||
pub fn new(content: String, file_name: Option<String>) -> Self {
|
||||
let matter = Matter::<YAML>::new();
|
||||
let result = matter.parse(&content);
|
||||
let file_name = if let Some(file_name) = file_name {
|
||||
let shared_filename: SharedString = file_name.into();
|
||||
Some(shared_filename)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let metadata = result
|
||||
.data
|
||||
.map_or_else(
|
||||
|| Err(anyhow::anyhow!("Failed to parse frontmatter")),
|
||||
|data| {
|
||||
let front_matter: StaticPromptFrontmatter = data.deserialize()?;
|
||||
Ok(front_matter)
|
||||
},
|
||||
)
|
||||
.unwrap_or_else(|e| {
|
||||
if let Some(file_name) = &file_name {
|
||||
log::error!("Failed to parse frontmatter for {}: {}", file_name, e);
|
||||
} else {
|
||||
log::error!("Failed to parse frontmatter: {}", e);
|
||||
}
|
||||
StaticPromptFrontmatter::default()
|
||||
});
|
||||
|
||||
let id = if let Some(file_name) = &file_name {
|
||||
PromptId::from_str(file_name).unwrap_or_default()
|
||||
} else {
|
||||
PromptId::new()
|
||||
};
|
||||
|
||||
StaticPrompt {
|
||||
id,
|
||||
content,
|
||||
file_name,
|
||||
metadata,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(&mut self, id: PromptId, content: String) {
|
||||
let mut updated_prompt =
|
||||
StaticPrompt::new(content, self.file_name.clone().map(|s| s.to_string()));
|
||||
updated_prompt.id = id;
|
||||
*self = updated_prompt;
|
||||
}
|
||||
}
|
||||
|
||||
impl StaticPrompt {
|
||||
/// Returns the prompt's id
|
||||
pub fn id(&self) -> &PromptId {
|
||||
&self.id
|
||||
}
|
||||
|
||||
pub fn file_name(&self) -> Option<&SharedString> {
|
||||
self.file_name.as_ref()
|
||||
}
|
||||
|
||||
/// Sets the file name of the prompt
|
||||
pub fn new_file_name(&self) -> String {
|
||||
let in_name = format!(
|
||||
"{}_{}_{}",
|
||||
standardize_value(self.metadata.title.clone()),
|
||||
standardize_value(self.metadata.version.clone()),
|
||||
standardize_value(self.id.0.to_string())
|
||||
);
|
||||
let out_name = slugify(in_name);
|
||||
out_name
|
||||
}
|
||||
|
||||
/// Returns the prompt's content
|
||||
pub fn content(&self) -> &String {
|
||||
&self.content
|
||||
}
|
||||
|
||||
/// Returns the prompt's metadata
|
||||
pub fn _metadata(&self) -> &StaticPromptFrontmatter {
|
||||
&self.metadata
|
||||
}
|
||||
|
||||
/// Returns the prompt's title
|
||||
pub fn title(&self) -> SharedString {
|
||||
self.metadata.title.clone().into()
|
||||
}
|
||||
|
||||
pub fn body(&self) -> String {
|
||||
let matter = Matter::<YAML>::new();
|
||||
let result = matter.parse(self.content.as_str());
|
||||
result.content.clone()
|
||||
}
|
||||
|
||||
pub fn path(&self) -> Option<PathBuf> {
|
||||
if let Some(file_name) = self.file_name() {
|
||||
let path_str = format!("{}", file_name);
|
||||
Some(PROMPTS_DIR.join(path_str))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn save(&self, fs: Arc<dyn Fs>) -> anyhow::Result<()> {
|
||||
let file_name = self.file_name();
|
||||
let new_file_name = self.new_file_name();
|
||||
|
||||
let out_name = if let Some(file_name) = file_name {
|
||||
file_name.to_owned().to_string()
|
||||
} else {
|
||||
format!("{}.md", new_file_name)
|
||||
};
|
||||
let path = PROMPTS_DIR.join(&out_name);
|
||||
let json = self.content.clone();
|
||||
|
||||
fs.atomic_write(path, json).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_content_prompt(
|
||||
user_prompt: String,
|
||||
language_name: Option<&str>,
|
||||
buffer: BufferSnapshot,
|
||||
range: Range<usize>,
|
||||
project_name: Option<String>,
|
||||
) -> anyhow::Result<String> {
|
||||
let mut prompt = String::new();
|
||||
|
||||
let content_type = match language_name {
|
||||
None | Some("Markdown" | "Plain Text") => {
|
||||
writeln!(prompt, "You are an expert engineer.")?;
|
||||
"Text"
|
||||
}
|
||||
Some(language_name) => {
|
||||
writeln!(prompt, "You are an expert {language_name} engineer.")?;
|
||||
writeln!(
|
||||
prompt,
|
||||
"Your answer MUST always and only be valid {}.",
|
||||
language_name
|
||||
)?;
|
||||
"Code"
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(project_name) = project_name {
|
||||
writeln!(
|
||||
prompt,
|
||||
"You are currently working inside the '{project_name}' project in code editor Zed."
|
||||
)?;
|
||||
}
|
||||
|
||||
// Include file content.
|
||||
for chunk in buffer.text_for_range(0..range.start) {
|
||||
prompt.push_str(chunk);
|
||||
}
|
||||
|
||||
if range.is_empty() {
|
||||
prompt.push_str("<|START|>");
|
||||
} else {
|
||||
prompt.push_str("<|START|");
|
||||
}
|
||||
|
||||
for chunk in buffer.text_for_range(range.clone()) {
|
||||
prompt.push_str(chunk);
|
||||
}
|
||||
|
||||
if !range.is_empty() {
|
||||
prompt.push_str("|END|>");
|
||||
}
|
||||
|
||||
for chunk in buffer.text_for_range(range.end..buffer.len()) {
|
||||
prompt.push_str(chunk);
|
||||
}
|
||||
|
||||
prompt.push('\n');
|
||||
|
||||
if range.is_empty() {
|
||||
writeln!(
|
||||
prompt,
|
||||
"Assume the cursor is located where the `<|START|>` span is."
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(
|
||||
prompt,
|
||||
"{content_type} can't be replaced, so assume your answer will be inserted at the cursor.",
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(
|
||||
prompt,
|
||||
"Generate {content_type} based on the users prompt: {user_prompt}",
|
||||
)
|
||||
.unwrap();
|
||||
} else {
|
||||
writeln!(prompt, "Modify the user's selected {content_type} based upon the users prompt: '{user_prompt}'").unwrap();
|
||||
writeln!(prompt, "You must reply with only the adjusted {content_type} (within the '<|START|' and '|END|>' spans) not the entire file.").unwrap();
|
||||
writeln!(
|
||||
prompt,
|
||||
"Double check that you only return code and not the '<|START|' and '|END|'> spans"
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
writeln!(prompt, "Never make remarks about the output.").unwrap();
|
||||
writeln!(
|
||||
prompt,
|
||||
"Do not return anything else, except the generated {content_type}."
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
Ok(prompt)
|
||||
}
|
||||
245
crates/assistant/src/prompts/prompt_library.rs
Normal file
245
crates/assistant/src/prompts/prompt_library.rs
Normal file
@@ -0,0 +1,245 @@
|
||||
use anyhow::Context;
|
||||
use collections::HashMap;
|
||||
use fs::Fs;
|
||||
|
||||
use gray_matter::{engine::YAML, Matter};
|
||||
use parking_lot::RwLock;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use smol::stream::StreamExt;
|
||||
use std::sync::Arc;
|
||||
use util::paths::PROMPTS_DIR;
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::prompt::StaticPrompt;
|
||||
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Serialize, Deserialize)]
|
||||
pub struct PromptId(pub Uuid);
|
||||
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||
pub enum SortOrder {
|
||||
Alphabetical,
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
impl PromptId {
|
||||
pub fn new() -> Self {
|
||||
Self(Uuid::new_v4())
|
||||
}
|
||||
|
||||
pub fn from_str(id: &str) -> anyhow::Result<Self> {
|
||||
Ok(Self(Uuid::parse_str(id)?))
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PromptId {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Serialize, Deserialize)]
|
||||
pub struct PromptLibraryState {
|
||||
/// A set of prompts that all assistant contexts will start with
|
||||
default_prompt: Vec<PromptId>,
|
||||
/// All [Prompt]s loaded into the library
|
||||
prompts: HashMap<PromptId, StaticPrompt>,
|
||||
/// Prompts that have been changed but haven't been
|
||||
/// saved back to the file system
|
||||
dirty_prompts: Vec<PromptId>,
|
||||
version: usize,
|
||||
}
|
||||
|
||||
pub struct PromptLibrary {
|
||||
state: RwLock<PromptLibraryState>,
|
||||
}
|
||||
|
||||
impl Default for PromptLibrary {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl PromptLibrary {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
state: RwLock::new(PromptLibraryState::default()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_prompt(&self) -> StaticPrompt {
|
||||
StaticPrompt::default()
|
||||
}
|
||||
|
||||
pub fn add_prompt(&self, prompt: StaticPrompt) {
|
||||
let mut state = self.state.write();
|
||||
let id = *prompt.id();
|
||||
state.prompts.insert(id, prompt);
|
||||
state.version += 1;
|
||||
}
|
||||
|
||||
pub fn prompts(&self) -> HashMap<PromptId, StaticPrompt> {
|
||||
let state = self.state.read();
|
||||
state.prompts.clone()
|
||||
}
|
||||
|
||||
pub fn sorted_prompts(&self, sort_order: SortOrder) -> Vec<(PromptId, StaticPrompt)> {
|
||||
let state = self.state.read();
|
||||
|
||||
let mut prompts = state
|
||||
.prompts
|
||||
.iter()
|
||||
.map(|(id, prompt)| (*id, prompt.clone()))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
match sort_order {
|
||||
SortOrder::Alphabetical => prompts.sort_by(|(_, a), (_, b)| a.title().cmp(&b.title())),
|
||||
};
|
||||
|
||||
prompts
|
||||
}
|
||||
|
||||
pub fn prompt_by_id(&self, id: PromptId) -> Option<StaticPrompt> {
|
||||
let state = self.state.read();
|
||||
state.prompts.get(&id).cloned()
|
||||
}
|
||||
|
||||
pub fn first_prompt_id(&self) -> Option<PromptId> {
|
||||
let state = self.state.read();
|
||||
state.prompts.keys().next().cloned()
|
||||
}
|
||||
|
||||
pub fn is_dirty(&self, id: &PromptId) -> bool {
|
||||
let state = self.state.read();
|
||||
state.dirty_prompts.contains(&id)
|
||||
}
|
||||
|
||||
pub fn set_dirty(&self, id: PromptId, dirty: bool) {
|
||||
let mut state = self.state.write();
|
||||
if dirty {
|
||||
if !state.dirty_prompts.contains(&id) {
|
||||
state.dirty_prompts.push(id);
|
||||
}
|
||||
state.version += 1;
|
||||
} else {
|
||||
state.dirty_prompts.retain(|&i| i != id);
|
||||
state.version += 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// Load the state of the prompt library from the file system
|
||||
/// or create a new one if it doesn't exist
|
||||
pub async fn load_index(fs: Arc<dyn Fs>) -> anyhow::Result<Self> {
|
||||
let path = PROMPTS_DIR.join("index.json");
|
||||
|
||||
let state = if fs.is_file(&path).await {
|
||||
let json = fs.load(&path).await?;
|
||||
serde_json::from_str(&json)?
|
||||
} else {
|
||||
PromptLibraryState::default()
|
||||
};
|
||||
|
||||
let mut prompt_library = Self {
|
||||
state: RwLock::new(state),
|
||||
};
|
||||
|
||||
prompt_library.load_prompts(fs).await?;
|
||||
|
||||
Ok(prompt_library)
|
||||
}
|
||||
|
||||
/// Load all prompts from the file system
|
||||
/// adding them to the library if they don't already exist
|
||||
pub async fn load_prompts(&mut self, fs: Arc<dyn Fs>) -> anyhow::Result<()> {
|
||||
self.state.get_mut().prompts.clear();
|
||||
|
||||
let mut prompt_paths = fs.read_dir(&PROMPTS_DIR).await?;
|
||||
|
||||
while let Some(prompt_path) = prompt_paths.next().await {
|
||||
let prompt_path = prompt_path.with_context(|| "Failed to read prompt path")?;
|
||||
let file_name_lossy = if prompt_path.file_name().is_some() {
|
||||
Some(
|
||||
prompt_path
|
||||
.file_name()
|
||||
.unwrap()
|
||||
.to_string_lossy()
|
||||
.to_string(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if !fs.is_file(&prompt_path).await
|
||||
|| prompt_path.extension().and_then(|ext| ext.to_str()) != Some("md")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
let json = fs
|
||||
.load(&prompt_path)
|
||||
.await
|
||||
.with_context(|| format!("Failed to load prompt {:?}", prompt_path))?;
|
||||
|
||||
// Check that the prompt is valid
|
||||
let matter = Matter::<YAML>::new();
|
||||
let result = matter.parse(&json);
|
||||
if result.data.is_none() {
|
||||
log::warn!("Invalid prompt: {:?}", prompt_path);
|
||||
continue;
|
||||
}
|
||||
|
||||
let static_prompt = StaticPrompt::new(json, file_name_lossy.clone());
|
||||
|
||||
let state = self.state.get_mut();
|
||||
|
||||
let id = Uuid::new_v4();
|
||||
state.prompts.insert(PromptId(id), static_prompt);
|
||||
state.version += 1;
|
||||
}
|
||||
|
||||
// Write any changes back to the file system
|
||||
self.save_index(fs.clone()).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Save the current state of the prompt library to the
|
||||
/// file system as a JSON file
|
||||
pub async fn save_index(&self, fs: Arc<dyn Fs>) -> anyhow::Result<()> {
|
||||
fs.create_dir(&PROMPTS_DIR).await?;
|
||||
|
||||
let path = PROMPTS_DIR.join("index.json");
|
||||
|
||||
let json = {
|
||||
let state = self.state.read();
|
||||
serde_json::to_string(&*state)?
|
||||
};
|
||||
|
||||
fs.atomic_write(path, json).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn save_prompt(
|
||||
&self,
|
||||
prompt_id: PromptId,
|
||||
updated_content: Option<String>,
|
||||
fs: Arc<dyn Fs>,
|
||||
) -> anyhow::Result<()> {
|
||||
if let Some(updated_content) = updated_content {
|
||||
let mut state = self.state.write();
|
||||
if let Some(prompt) = state.prompts.get_mut(&prompt_id) {
|
||||
prompt.update(prompt_id, updated_content);
|
||||
state.version += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(prompt) = self.prompt_by_id(prompt_id) {
|
||||
prompt.save(fs).await?;
|
||||
self.set_dirty(prompt_id, false);
|
||||
} else {
|
||||
log::warn!("Failed to save prompt: {:?}", prompt_id);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
512
crates/assistant/src/prompts/prompt_manager.rs
Normal file
512
crates/assistant/src/prompts/prompt_manager.rs
Normal file
@@ -0,0 +1,512 @@
|
||||
use collections::HashMap;
|
||||
use editor::{Editor, EditorEvent};
|
||||
use fs::Fs;
|
||||
use gpui::{prelude::FluentBuilder, *};
|
||||
use language::{language_settings, Buffer, LanguageRegistry};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use std::sync::Arc;
|
||||
use ui::{prelude::*, IconButtonShape, Indicator, ListItem, ListItemSpacing, Tooltip};
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
use workspace::ModalView;
|
||||
|
||||
use crate::prompts::{PromptId, PromptLibrary, SortOrder, StaticPrompt, PROMPT_DEFAULT_TITLE};
|
||||
|
||||
actions!(prompt_manager, [NewPrompt, SavePrompt]);
|
||||
|
||||
pub struct PromptManager {
|
||||
focus_handle: FocusHandle,
|
||||
prompt_library: Arc<PromptLibrary>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
#[allow(dead_code)]
|
||||
fs: Arc<dyn Fs>,
|
||||
picker: View<Picker<PromptManagerDelegate>>,
|
||||
prompt_editors: HashMap<PromptId, View<Editor>>,
|
||||
active_prompt_id: Option<PromptId>,
|
||||
last_new_prompt_id: Option<PromptId>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
impl PromptManager {
|
||||
pub fn new(
|
||||
prompt_library: Arc<PromptLibrary>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
fs: Arc<dyn Fs>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let prompt_manager = cx.view().downgrade();
|
||||
let picker = cx.new_view(|cx| {
|
||||
Picker::uniform_list(
|
||||
PromptManagerDelegate {
|
||||
prompt_manager,
|
||||
matching_prompts: vec![],
|
||||
matching_prompt_ids: vec![],
|
||||
prompt_library: prompt_library.clone(),
|
||||
selected_index: 0,
|
||||
_subscriptions: vec![],
|
||||
},
|
||||
cx,
|
||||
)
|
||||
.max_height(rems(35.75))
|
||||
.modal(false)
|
||||
});
|
||||
|
||||
let focus_handle = picker.focus_handle(cx);
|
||||
|
||||
let subscriptions = vec![
|
||||
// cx.on_focus_in(&focus_handle, Self::focus_in),
|
||||
// cx.on_focus_out(&focus_handle, Self::focus_out),
|
||||
];
|
||||
|
||||
let mut manager = Self {
|
||||
focus_handle,
|
||||
prompt_library,
|
||||
language_registry,
|
||||
fs,
|
||||
picker,
|
||||
prompt_editors: HashMap::default(),
|
||||
active_prompt_id: None,
|
||||
last_new_prompt_id: None,
|
||||
_subscriptions: subscriptions,
|
||||
};
|
||||
|
||||
manager.active_prompt_id = manager.prompt_library.first_prompt_id();
|
||||
|
||||
manager
|
||||
}
|
||||
|
||||
fn dispatch_context(&self, cx: &ViewContext<Self>) -> KeyContext {
|
||||
let mut dispatch_context = KeyContext::new_with_defaults();
|
||||
dispatch_context.add("PromptManager");
|
||||
|
||||
let identifier = match self.active_editor() {
|
||||
Some(active_editor) if active_editor.focus_handle(cx).is_focused(cx) => "editing",
|
||||
_ => "not_editing",
|
||||
};
|
||||
|
||||
dispatch_context.add(identifier);
|
||||
dispatch_context
|
||||
}
|
||||
|
||||
pub fn new_prompt(&mut self, _: &NewPrompt, cx: &mut ViewContext<Self>) {
|
||||
// TODO: Why doesn't this prevent making a new prompt if you
|
||||
// move the picker selection/maybe unfocus the editor?
|
||||
|
||||
// Prevent making a new prompt if the last new prompt is still empty
|
||||
//
|
||||
// Instead, we'll focus the last new prompt
|
||||
if let Some(last_new_prompt_id) = self.last_new_prompt_id() {
|
||||
if let Some(last_new_prompt) = self.prompt_library.prompt_by_id(last_new_prompt_id) {
|
||||
let normalized_body = last_new_prompt
|
||||
.body()
|
||||
.trim()
|
||||
.replace(['\r', '\n'], "")
|
||||
.to_string();
|
||||
|
||||
if last_new_prompt.title() == PROMPT_DEFAULT_TITLE && normalized_body.is_empty() {
|
||||
self.set_editor_for_prompt(last_new_prompt_id, cx);
|
||||
self.focus_active_editor(cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let prompt = self.prompt_library.new_prompt();
|
||||
self.set_last_new_prompt_id(Some(prompt.id().to_owned()));
|
||||
|
||||
self.prompt_library.add_prompt(prompt.clone());
|
||||
|
||||
let id = *prompt.id();
|
||||
self.picker.update(cx, |picker, _cx| {
|
||||
let prompts = self
|
||||
.prompt_library
|
||||
.sorted_prompts(SortOrder::Alphabetical)
|
||||
.clone()
|
||||
.into_iter();
|
||||
|
||||
picker.delegate.prompt_library = self.prompt_library.clone();
|
||||
picker.delegate.matching_prompts = prompts.clone().map(|(_, p)| Arc::new(p)).collect();
|
||||
picker.delegate.matching_prompt_ids = prompts.map(|(id, _)| id).collect();
|
||||
picker.delegate.selected_index = picker
|
||||
.delegate
|
||||
.matching_prompts
|
||||
.iter()
|
||||
.position(|p| p.id() == &id)
|
||||
.unwrap_or(0);
|
||||
});
|
||||
|
||||
self.active_prompt_id = Some(id);
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn save_prompt(
|
||||
&mut self,
|
||||
fs: Arc<dyn Fs>,
|
||||
prompt_id: PromptId,
|
||||
new_content: String,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Result<()> {
|
||||
let library = self.prompt_library.clone();
|
||||
if library.prompt_by_id(prompt_id).is_some() {
|
||||
cx.spawn(|_, _| async move {
|
||||
library
|
||||
.save_prompt(prompt_id, Some(new_content), fs)
|
||||
.log_err()
|
||||
.await;
|
||||
})
|
||||
.detach();
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_active_prompt(&mut self, prompt_id: Option<PromptId>, cx: &mut ViewContext<Self>) {
|
||||
self.active_prompt_id = prompt_id;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn last_new_prompt_id(&self) -> Option<PromptId> {
|
||||
self.last_new_prompt_id
|
||||
}
|
||||
|
||||
pub fn set_last_new_prompt_id(&mut self, id: Option<PromptId>) {
|
||||
self.last_new_prompt_id = id;
|
||||
}
|
||||
|
||||
pub fn focus_active_editor(&self, cx: &mut ViewContext<Self>) {
|
||||
if let Some(active_prompt_id) = self.active_prompt_id {
|
||||
if let Some(editor) = self.prompt_editors.get(&active_prompt_id) {
|
||||
let focus_handle = editor.focus_handle(cx);
|
||||
|
||||
cx.focus(&focus_handle)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn active_editor(&self) -> Option<&View<Editor>> {
|
||||
self.active_prompt_id
|
||||
.and_then(|active_prompt_id| self.prompt_editors.get(&active_prompt_id))
|
||||
}
|
||||
|
||||
fn set_editor_for_prompt(
|
||||
&mut self,
|
||||
prompt_id: PromptId,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> impl IntoElement {
|
||||
let prompt_library = self.prompt_library.clone();
|
||||
|
||||
let editor_for_prompt = self.prompt_editors.entry(prompt_id).or_insert_with(|| {
|
||||
cx.new_view(|cx| {
|
||||
let text = if let Some(prompt) = prompt_library.prompt_by_id(prompt_id) {
|
||||
prompt.content().to_owned()
|
||||
} else {
|
||||
"".to_string()
|
||||
};
|
||||
|
||||
let buffer = cx.new_model(|cx| {
|
||||
let mut buffer = Buffer::local(text, cx);
|
||||
let markdown = self.language_registry.language_for_name("Markdown");
|
||||
cx.spawn(|buffer, mut cx| async move {
|
||||
if let Some(markdown) = markdown.await.log_err() {
|
||||
_ = buffer.update(&mut cx, |buffer, cx| {
|
||||
buffer.set_language(Some(markdown), cx);
|
||||
});
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
buffer.set_language_registry(self.language_registry.clone());
|
||||
buffer
|
||||
});
|
||||
let mut editor = Editor::for_buffer(buffer, None, cx);
|
||||
editor.set_soft_wrap_mode(language_settings::SoftWrap::EditorWidth, cx);
|
||||
editor.set_show_gutter(false, cx);
|
||||
editor
|
||||
})
|
||||
});
|
||||
|
||||
editor_for_prompt.clone()
|
||||
}
|
||||
|
||||
fn dismiss(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
|
||||
fn render_prompt_list(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let picker = self.picker.clone();
|
||||
|
||||
v_flex()
|
||||
.id("prompt-list")
|
||||
.bg(cx.theme().colors().surface_background)
|
||||
.h_full()
|
||||
.w_1_3()
|
||||
.overflow_hidden()
|
||||
.child(
|
||||
h_flex()
|
||||
.bg(cx.theme().colors().background)
|
||||
.p(Spacing::Small.rems(cx))
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.h(rems(1.75))
|
||||
.w_full()
|
||||
.flex_none()
|
||||
.justify_between()
|
||||
.child(Label::new("Prompt Library").size(LabelSize::Small))
|
||||
.child(
|
||||
IconButton::new("new-prompt", IconName::Plus)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(move |cx| Tooltip::text("New Prompt", cx))
|
||||
.on_click(|_, cx| {
|
||||
cx.dispatch_action(NewPrompt.boxed_clone());
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.h(rems(38.25))
|
||||
.flex_grow()
|
||||
.justify_start()
|
||||
.child(picker),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for PromptManager {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let active_prompt_id = self.active_prompt_id;
|
||||
let active_prompt = if let Some(active_prompt_id) = active_prompt_id {
|
||||
self.prompt_library.clone().prompt_by_id(active_prompt_id)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let active_editor = self.active_editor().map(|editor| editor.clone());
|
||||
let updated_content = if let Some(editor) = active_editor {
|
||||
Some(editor.read(cx).text(cx))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let can_save = active_prompt_id.is_some() && updated_content.is_some();
|
||||
let fs = self.fs.clone();
|
||||
|
||||
h_flex()
|
||||
.id("prompt-manager")
|
||||
.key_context(self.dispatch_context(cx))
|
||||
.track_focus(&self.focus_handle)
|
||||
.on_action(cx.listener(Self::dismiss))
|
||||
.on_action(cx.listener(Self::new_prompt))
|
||||
.elevation_3(cx)
|
||||
.size_full()
|
||||
.flex_none()
|
||||
.w(rems(64.))
|
||||
.h(rems(40.))
|
||||
.overflow_hidden()
|
||||
.child(self.render_prompt_list(cx))
|
||||
.child(
|
||||
div().w_2_3().h_full().child(
|
||||
v_flex()
|
||||
.id("prompt-editor")
|
||||
.border_l_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.size_full()
|
||||
.flex_none()
|
||||
.min_w_64()
|
||||
.h_full()
|
||||
.overflow_hidden()
|
||||
.child(
|
||||
h_flex()
|
||||
.bg(cx.theme().colors().background)
|
||||
.p(Spacing::Small.rems(cx))
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.h_7()
|
||||
.w_full()
|
||||
.justify_between()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap(Spacing::XXLarge.rems(cx))
|
||||
.child(if can_save {
|
||||
IconButton::new("save", IconName::Save)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(move |cx| Tooltip::text("Save Prompt", cx))
|
||||
.on_click(cx.listener(move |this, _event, cx| {
|
||||
if let Some(prompt_id) = active_prompt_id {
|
||||
this.save_prompt(
|
||||
fs.clone(),
|
||||
prompt_id,
|
||||
updated_content.clone().unwrap_or(
|
||||
"TODO: make unreachable"
|
||||
.to_string(),
|
||||
),
|
||||
cx,
|
||||
)
|
||||
.log_err();
|
||||
}
|
||||
}))
|
||||
} else {
|
||||
IconButton::new("save", IconName::Save)
|
||||
.shape(IconButtonShape::Square)
|
||||
.disabled(true)
|
||||
})
|
||||
.when_some(active_prompt, |this, active_prompt| {
|
||||
let path = active_prompt.path();
|
||||
|
||||
this.child(
|
||||
IconButton::new("reveal", IconName::Reveal)
|
||||
.shape(IconButtonShape::Square)
|
||||
.disabled(path.is_none())
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::text("Reveal in Finder", cx)
|
||||
})
|
||||
.on_click(cx.listener(move |_, _event, cx| {
|
||||
if let Some(path) = path.clone() {
|
||||
cx.reveal_path(&path);
|
||||
}
|
||||
})),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("dismiss", IconName::Close)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(move |cx| Tooltip::text("Close", cx))
|
||||
.on_click(|_, cx| {
|
||||
cx.dispatch_action(menu::Cancel.boxed_clone());
|
||||
}),
|
||||
),
|
||||
)
|
||||
.when_some(active_prompt_id, |this, active_prompt_id| {
|
||||
this.child(
|
||||
h_flex()
|
||||
.flex_1()
|
||||
.w_full()
|
||||
.py(Spacing::Large.rems(cx))
|
||||
.px(Spacing::XLarge.rems(cx))
|
||||
.child(self.set_editor_for_prompt(active_prompt_id, cx)),
|
||||
)
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<DismissEvent> for PromptManager {}
|
||||
impl EventEmitter<EditorEvent> for PromptManager {}
|
||||
|
||||
impl ModalView for PromptManager {}
|
||||
|
||||
impl FocusableView for PromptManager {
|
||||
fn focus_handle(&self, _cx: &AppContext) -> gpui::FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PromptManagerDelegate {
|
||||
prompt_manager: WeakView<PromptManager>,
|
||||
matching_prompts: Vec<Arc<StaticPrompt>>,
|
||||
matching_prompt_ids: Vec<PromptId>,
|
||||
prompt_library: Arc<PromptLibrary>,
|
||||
selected_index: usize,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
impl PickerDelegate for PromptManagerDelegate {
|
||||
type ListItem = ListItem;
|
||||
|
||||
fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
|
||||
"Find a prompt…".into()
|
||||
}
|
||||
|
||||
fn match_count(&self) -> usize {
|
||||
self.matching_prompt_ids.len()
|
||||
}
|
||||
|
||||
fn selected_index(&self) -> usize {
|
||||
self.selected_index
|
||||
}
|
||||
|
||||
fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<Picker<Self>>) {
|
||||
self.selected_index = ix;
|
||||
}
|
||||
|
||||
fn selected_index_changed(
|
||||
&self,
|
||||
ix: usize,
|
||||
_cx: &mut ViewContext<Picker<Self>>,
|
||||
) -> Option<Box<dyn Fn(&mut WindowContext) + 'static>> {
|
||||
let prompt_id = self.matching_prompt_ids.get(ix).copied()?;
|
||||
let prompt_manager = self.prompt_manager.upgrade()?;
|
||||
|
||||
Some(Box::new(move |cx| {
|
||||
prompt_manager.update(cx, |manager, cx| {
|
||||
manager.set_active_prompt(Some(prompt_id), cx);
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
|
||||
let prompt_library = self.prompt_library.clone();
|
||||
cx.spawn(|picker, mut cx| async move {
|
||||
async {
|
||||
let prompts = prompt_library.sorted_prompts(SortOrder::Alphabetical);
|
||||
let matching_prompts = prompts
|
||||
.into_iter()
|
||||
.filter(|(_, prompt)| {
|
||||
prompt
|
||||
.content()
|
||||
.to_lowercase()
|
||||
.contains(&query.to_lowercase())
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
picker.update(&mut cx, |picker, cx| {
|
||||
picker.delegate.matching_prompt_ids =
|
||||
matching_prompts.iter().map(|(id, _)| *id).collect();
|
||||
picker.delegate.matching_prompts = matching_prompts
|
||||
.into_iter()
|
||||
.map(|(_, prompt)| Arc::new(prompt))
|
||||
.collect();
|
||||
cx.notify();
|
||||
})?;
|
||||
anyhow::Ok(())
|
||||
}
|
||||
.log_err()
|
||||
.await;
|
||||
})
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
|
||||
let prompt_manager = self.prompt_manager.upgrade().unwrap();
|
||||
prompt_manager.update(cx, move |manager, cx| manager.focus_active_editor(cx));
|
||||
}
|
||||
|
||||
fn should_dismiss(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
|
||||
self.prompt_manager
|
||||
.update(cx, |_, cx| {
|
||||
cx.emit(DismissEvent);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
fn render_match(
|
||||
&self,
|
||||
ix: usize,
|
||||
selected: bool,
|
||||
_cx: &mut ViewContext<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
let prompt = self.matching_prompts.get(ix)?;
|
||||
|
||||
let is_diry = self.prompt_library.is_dirty(prompt.id());
|
||||
|
||||
Some(
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.selected(selected)
|
||||
.child(Label::new(prompt.title()))
|
||||
.end_slot(div().when(is_diry, |this| this.child(Indicator::dot()))),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -17,8 +17,6 @@ use std::{
|
||||
use workspace::Workspace;
|
||||
|
||||
pub mod active_command;
|
||||
pub mod default_command;
|
||||
pub mod fetch_command;
|
||||
pub mod file_command;
|
||||
pub mod project_command;
|
||||
pub mod prompt_command;
|
||||
@@ -27,10 +25,10 @@ pub mod search_command;
|
||||
pub mod tabs_command;
|
||||
|
||||
pub(crate) struct SlashCommandCompletionProvider {
|
||||
editor: WeakView<ConversationEditor>,
|
||||
commands: Arc<SlashCommandRegistry>,
|
||||
cancel_flag: Mutex<Arc<AtomicBool>>,
|
||||
editor: Option<WeakView<ConversationEditor>>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
workspace: WeakView<Workspace>,
|
||||
}
|
||||
|
||||
pub(crate) struct SlashCommandLine {
|
||||
@@ -42,9 +40,9 @@ pub(crate) struct SlashCommandLine {
|
||||
|
||||
impl SlashCommandCompletionProvider {
|
||||
pub fn new(
|
||||
editor: WeakView<ConversationEditor>,
|
||||
commands: Arc<SlashCommandRegistry>,
|
||||
editor: Option<WeakView<ConversationEditor>>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
workspace: WeakView<Workspace>,
|
||||
) -> Self {
|
||||
Self {
|
||||
cancel_flag: Mutex::new(Arc::new(AtomicBool::new(false))),
|
||||
@@ -98,30 +96,6 @@ impl SlashCommandCompletionProvider {
|
||||
new_text.push(' ');
|
||||
}
|
||||
|
||||
let confirm = editor.clone().zip(workspace.clone()).and_then(
|
||||
|(editor, workspace)| {
|
||||
(!requires_argument).then(|| {
|
||||
let command_name = mat.string.clone();
|
||||
let command_range = command_range.clone();
|
||||
let editor = editor.clone();
|
||||
let workspace = workspace.clone();
|
||||
Arc::new(move |cx: &mut WindowContext| {
|
||||
editor
|
||||
.update(cx, |editor, cx| {
|
||||
editor.run_command(
|
||||
command_range.clone(),
|
||||
&command_name,
|
||||
None,
|
||||
true,
|
||||
workspace.clone(),
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
}) as Arc<_>
|
||||
})
|
||||
},
|
||||
);
|
||||
Some(project::Completion {
|
||||
old_range: name_range.clone(),
|
||||
documentation: Some(Documentation::SingleLine(command.description())),
|
||||
@@ -130,7 +104,25 @@ impl SlashCommandCompletionProvider {
|
||||
server_id: LanguageServerId(0),
|
||||
lsp_completion: Default::default(),
|
||||
show_new_completions_on_confirm: requires_argument,
|
||||
confirm,
|
||||
confirm: (!requires_argument).then(|| {
|
||||
let command_name = mat.string.clone();
|
||||
let command_range = command_range.clone();
|
||||
let editor = editor.clone();
|
||||
let workspace = workspace.clone();
|
||||
Arc::new(move |cx: &mut WindowContext| {
|
||||
editor
|
||||
.update(cx, |editor, cx| {
|
||||
editor.run_command(
|
||||
command_range.clone(),
|
||||
&command_name,
|
||||
None,
|
||||
workspace.clone(),
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
}) as Arc<_>
|
||||
}),
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
@@ -165,42 +157,33 @@ impl SlashCommandCompletionProvider {
|
||||
Ok(completions
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|command_argument| {
|
||||
let confirm =
|
||||
editor
|
||||
.clone()
|
||||
.zip(workspace.clone())
|
||||
.map(|(editor, workspace)| {
|
||||
Arc::new({
|
||||
let command_range = command_range.clone();
|
||||
let command_name = command_name.clone();
|
||||
let command_argument = command_argument.clone();
|
||||
move |cx: &mut WindowContext| {
|
||||
editor
|
||||
.update(cx, |editor, cx| {
|
||||
editor.run_command(
|
||||
command_range.clone(),
|
||||
&command_name,
|
||||
Some(&command_argument),
|
||||
true,
|
||||
workspace.clone(),
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}) as Arc<_>
|
||||
});
|
||||
project::Completion {
|
||||
old_range: argument_range.clone(),
|
||||
label: CodeLabel::plain(command_argument.clone(), None),
|
||||
new_text: command_argument.clone(),
|
||||
documentation: None,
|
||||
server_id: LanguageServerId(0),
|
||||
lsp_completion: Default::default(),
|
||||
show_new_completions_on_confirm: false,
|
||||
confirm,
|
||||
}
|
||||
.map(|arg| project::Completion {
|
||||
old_range: argument_range.clone(),
|
||||
label: CodeLabel::plain(arg.clone(), None),
|
||||
new_text: arg.clone(),
|
||||
documentation: None,
|
||||
server_id: LanguageServerId(0),
|
||||
lsp_completion: Default::default(),
|
||||
show_new_completions_on_confirm: false,
|
||||
confirm: Some(Arc::new({
|
||||
let command_name = command_name.clone();
|
||||
let command_range = command_range.clone();
|
||||
let editor = editor.clone();
|
||||
let workspace = workspace.clone();
|
||||
move |cx| {
|
||||
editor
|
||||
.update(cx, |editor, cx| {
|
||||
editor.run_command(
|
||||
command_range.clone(),
|
||||
&command_name,
|
||||
Some(&arg),
|
||||
workspace.clone(),
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})),
|
||||
})
|
||||
.collect())
|
||||
})
|
||||
|
||||
@@ -27,7 +27,7 @@ impl SlashCommand for ActiveSlashCommand {
|
||||
&self,
|
||||
_query: String,
|
||||
_cancel: std::sync::Arc<std::sync::atomic::AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
_workspace: WeakView<Workspace>,
|
||||
_cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<String>>> {
|
||||
Task::ready(Err(anyhow!("this command does not require argument")))
|
||||
@@ -96,7 +96,6 @@ impl SlashCommand for ActiveSlashCommand {
|
||||
.into_any_element()
|
||||
}),
|
||||
}],
|
||||
run_commands_in_text: false,
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
use super::{prompt_command::PromptPlaceholder, SlashCommand, SlashCommandOutput};
|
||||
use crate::prompt_library::PromptStore;
|
||||
use anyhow::{anyhow, Result};
|
||||
use assistant_slash_command::SlashCommandOutputSection;
|
||||
use gpui::{AppContext, Task, WeakView};
|
||||
use language::LspAdapterDelegate;
|
||||
use std::{
|
||||
fmt::Write,
|
||||
sync::{atomic::AtomicBool, Arc},
|
||||
};
|
||||
use ui::prelude::*;
|
||||
use workspace::Workspace;
|
||||
|
||||
pub(crate) struct DefaultSlashCommand;
|
||||
|
||||
impl SlashCommand for DefaultSlashCommand {
|
||||
fn name(&self) -> String {
|
||||
"default".into()
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"insert default prompt".into()
|
||||
}
|
||||
|
||||
fn menu_text(&self) -> String {
|
||||
"Insert Default Prompt".into()
|
||||
}
|
||||
|
||||
fn requires_argument(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn complete_argument(
|
||||
&self,
|
||||
_query: String,
|
||||
_cancellation_flag: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
_cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<String>>> {
|
||||
Task::ready(Err(anyhow!("this command does not require argument")))
|
||||
}
|
||||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
_argument: Option<&str>,
|
||||
_workspace: WeakView<Workspace>,
|
||||
_delegate: Arc<dyn LspAdapterDelegate>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<Result<SlashCommandOutput>> {
|
||||
let store = PromptStore::global(cx);
|
||||
cx.background_executor().spawn(async move {
|
||||
let store = store.await?;
|
||||
let prompts = store.default_prompt_metadata();
|
||||
|
||||
let mut text = String::new();
|
||||
writeln!(text, "Default Prompt:").unwrap();
|
||||
for prompt in prompts {
|
||||
if let Some(title) = prompt.title {
|
||||
writeln!(text, "/prompt {}", title).unwrap();
|
||||
}
|
||||
}
|
||||
text.pop();
|
||||
|
||||
Ok(SlashCommandOutput {
|
||||
sections: vec![SlashCommandOutputSection {
|
||||
range: 0..text.len(),
|
||||
render_placeholder: Arc::new(move |id, unfold, _cx| {
|
||||
PromptPlaceholder {
|
||||
title: "Default".into(),
|
||||
id,
|
||||
unfold,
|
||||
}
|
||||
.into_any_element()
|
||||
}),
|
||||
}],
|
||||
text,
|
||||
run_commands_in_text: true,
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,134 +0,0 @@
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use assistant_slash_command::{SlashCommand, SlashCommandOutput, SlashCommandOutputSection};
|
||||
use futures::AsyncReadExt;
|
||||
use gpui::{AppContext, Task, WeakView};
|
||||
use html_to_markdown::convert_html_to_markdown;
|
||||
use http::{AsyncBody, HttpClient, HttpClientWithUrl};
|
||||
use language::LspAdapterDelegate;
|
||||
use ui::{prelude::*, ButtonLike, ElevationIndex};
|
||||
use workspace::Workspace;
|
||||
|
||||
pub(crate) struct FetchSlashCommand;
|
||||
|
||||
impl FetchSlashCommand {
|
||||
async fn build_message(http_client: Arc<HttpClientWithUrl>, url: &str) -> Result<String> {
|
||||
let mut url = url.to_owned();
|
||||
if !url.starts_with("https://") {
|
||||
url = format!("https://{url}");
|
||||
}
|
||||
|
||||
let mut response = http_client.get(&url, AsyncBody::default(), true).await?;
|
||||
|
||||
let mut body = Vec::new();
|
||||
response
|
||||
.body_mut()
|
||||
.read_to_end(&mut body)
|
||||
.await
|
||||
.context("error reading response body")?;
|
||||
|
||||
if response.status().is_client_error() {
|
||||
let text = String::from_utf8_lossy(body.as_slice());
|
||||
bail!(
|
||||
"status error {}, response: {text:?}",
|
||||
response.status().as_u16()
|
||||
);
|
||||
}
|
||||
|
||||
convert_html_to_markdown(&body[..])
|
||||
}
|
||||
}
|
||||
|
||||
impl SlashCommand for FetchSlashCommand {
|
||||
fn name(&self) -> String {
|
||||
"fetch".into()
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"insert URL contents".into()
|
||||
}
|
||||
|
||||
fn menu_text(&self) -> String {
|
||||
"Insert fetched URL contents".into()
|
||||
}
|
||||
|
||||
fn requires_argument(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn complete_argument(
|
||||
&self,
|
||||
_query: String,
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
_cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<String>>> {
|
||||
Task::ready(Ok(Vec::new()))
|
||||
}
|
||||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
argument: Option<&str>,
|
||||
workspace: WeakView<Workspace>,
|
||||
_delegate: Arc<dyn LspAdapterDelegate>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<Result<SlashCommandOutput>> {
|
||||
let Some(argument) = argument else {
|
||||
return Task::ready(Err(anyhow!("missing URL")));
|
||||
};
|
||||
let Some(workspace) = workspace.upgrade() else {
|
||||
return Task::ready(Err(anyhow!("workspace was dropped")));
|
||||
};
|
||||
|
||||
let http_client = workspace.read(cx).client().http_client();
|
||||
let url = argument.to_string();
|
||||
|
||||
let text = cx.background_executor().spawn({
|
||||
let url = url.clone();
|
||||
async move { Self::build_message(http_client, &url).await }
|
||||
});
|
||||
|
||||
let url = SharedString::from(url);
|
||||
cx.foreground_executor().spawn(async move {
|
||||
let text = text.await?;
|
||||
let range = 0..text.len();
|
||||
Ok(SlashCommandOutput {
|
||||
text,
|
||||
sections: vec![SlashCommandOutputSection {
|
||||
range,
|
||||
render_placeholder: Arc::new(move |id, unfold, _cx| {
|
||||
FetchPlaceholder {
|
||||
id,
|
||||
unfold,
|
||||
url: url.clone(),
|
||||
}
|
||||
.into_any_element()
|
||||
}),
|
||||
}],
|
||||
run_commands_in_text: false,
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
struct FetchPlaceholder {
|
||||
pub id: ElementId,
|
||||
pub unfold: Arc<dyn Fn(&mut WindowContext)>,
|
||||
pub url: SharedString,
|
||||
}
|
||||
|
||||
impl RenderOnce for FetchPlaceholder {
|
||||
fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
|
||||
let unfold = self.unfold;
|
||||
|
||||
ButtonLike::new(self.id)
|
||||
.style(ButtonStyle::Filled)
|
||||
.layer(ElevationIndex::ElevatedSurface)
|
||||
.child(Icon::new(IconName::AtSign))
|
||||
.child(Label::new(format!("fetch {url}", url = self.url)))
|
||||
.on_click(move |_, cx| unfold(cx))
|
||||
}
|
||||
}
|
||||
@@ -101,10 +101,10 @@ impl SlashCommand for FileSlashCommand {
|
||||
&self,
|
||||
query: String,
|
||||
cancellation_flag: Arc<AtomicBool>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
workspace: WeakView<Workspace>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<String>>> {
|
||||
let Some(workspace) = workspace.and_then(|workspace| workspace.upgrade()) else {
|
||||
let Some(workspace) = workspace.upgrade() else {
|
||||
return Task::ready(Err(anyhow!("workspace was dropped")));
|
||||
};
|
||||
|
||||
@@ -187,7 +187,6 @@ impl SlashCommand for FileSlashCommand {
|
||||
.into_any_element()
|
||||
}),
|
||||
}],
|
||||
run_commands_in_text: false,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -105,7 +105,7 @@ impl SlashCommand for ProjectSlashCommand {
|
||||
&self,
|
||||
_query: String,
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
_workspace: WeakView<Workspace>,
|
||||
_cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<String>>> {
|
||||
Task::ready(Err(anyhow!("this command does not require argument")))
|
||||
@@ -148,7 +148,6 @@ impl SlashCommand for ProjectSlashCommand {
|
||||
.into_any_element()
|
||||
}),
|
||||
}],
|
||||
run_commands_in_text: false,
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
@@ -1,14 +1,23 @@
|
||||
use super::{SlashCommand, SlashCommandOutput};
|
||||
use crate::prompt_library::PromptStore;
|
||||
use crate::prompts::PromptLibrary;
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use assistant_slash_command::SlashCommandOutputSection;
|
||||
use fuzzy::StringMatchCandidate;
|
||||
use gpui::{AppContext, Task, WeakView};
|
||||
use language::LspAdapterDelegate;
|
||||
use std::sync::{atomic::AtomicBool, Arc};
|
||||
use ui::{prelude::*, ButtonLike, ElevationIndex};
|
||||
use workspace::Workspace;
|
||||
|
||||
pub(crate) struct PromptSlashCommand;
|
||||
pub(crate) struct PromptSlashCommand {
|
||||
library: Arc<PromptLibrary>,
|
||||
}
|
||||
|
||||
impl PromptSlashCommand {
|
||||
pub fn new(library: Arc<PromptLibrary>) -> Self {
|
||||
Self { library }
|
||||
}
|
||||
}
|
||||
|
||||
impl SlashCommand for PromptSlashCommand {
|
||||
fn name(&self) -> String {
|
||||
@@ -30,16 +39,31 @@ impl SlashCommand for PromptSlashCommand {
|
||||
fn complete_argument(
|
||||
&self,
|
||||
query: String,
|
||||
_cancellation_flag: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
cancellation_flag: Arc<AtomicBool>,
|
||||
_workspace: WeakView<Workspace>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<String>>> {
|
||||
let store = PromptStore::global(cx);
|
||||
let library = self.library.clone();
|
||||
let executor = cx.background_executor().clone();
|
||||
cx.background_executor().spawn(async move {
|
||||
let prompts = store.await?.search(query).await;
|
||||
Ok(prompts
|
||||
let candidates = library
|
||||
.prompts()
|
||||
.into_iter()
|
||||
.filter_map(|prompt| Some(prompt.title?.to_string()))
|
||||
.enumerate()
|
||||
.map(|(ix, prompt)| StringMatchCandidate::new(ix, prompt.1.title().to_string()))
|
||||
.collect::<Vec<_>>();
|
||||
let matches = fuzzy::match_strings(
|
||||
&candidates,
|
||||
&query,
|
||||
false,
|
||||
100,
|
||||
&cancellation_flag,
|
||||
executor,
|
||||
)
|
||||
.await;
|
||||
Ok(matches
|
||||
.into_iter()
|
||||
.map(|mat| candidates[mat.candidate_id].string.clone())
|
||||
.collect())
|
||||
})
|
||||
}
|
||||
@@ -55,17 +79,19 @@ impl SlashCommand for PromptSlashCommand {
|
||||
return Task::ready(Err(anyhow!("missing prompt name")));
|
||||
};
|
||||
|
||||
let store = PromptStore::global(cx);
|
||||
let library = self.library.clone();
|
||||
let title = SharedString::from(title.to_string());
|
||||
let prompt = cx.background_executor().spawn({
|
||||
let title = title.clone();
|
||||
async move {
|
||||
let store = store.await?;
|
||||
let prompt_id = store
|
||||
.id_for_title(&title)
|
||||
.with_context(|| format!("no prompt found with title {:?}", title))?;
|
||||
let body = store.load(prompt_id).await?;
|
||||
anyhow::Ok(body)
|
||||
let prompt = library
|
||||
.prompts()
|
||||
.into_iter()
|
||||
.map(|prompt| (prompt.1.title(), prompt))
|
||||
.find(|(t, _)| t == &title)
|
||||
.with_context(|| format!("no prompt found with title {:?}", title))?
|
||||
.1;
|
||||
anyhow::Ok(prompt.1.body())
|
||||
}
|
||||
});
|
||||
cx.foreground_executor().spawn(async move {
|
||||
@@ -76,35 +102,16 @@ impl SlashCommand for PromptSlashCommand {
|
||||
sections: vec![SlashCommandOutputSection {
|
||||
range,
|
||||
render_placeholder: Arc::new(move |id, unfold, _cx| {
|
||||
PromptPlaceholder {
|
||||
id,
|
||||
unfold,
|
||||
title: title.clone(),
|
||||
}
|
||||
.into_any_element()
|
||||
ButtonLike::new(id)
|
||||
.style(ButtonStyle::Filled)
|
||||
.layer(ElevationIndex::ElevatedSurface)
|
||||
.child(Icon::new(IconName::Library))
|
||||
.child(Label::new(title.clone()))
|
||||
.on_click(move |_, cx| unfold(cx))
|
||||
.into_any_element()
|
||||
}),
|
||||
}],
|
||||
run_commands_in_text: true,
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct PromptPlaceholder {
|
||||
pub title: SharedString,
|
||||
pub id: ElementId,
|
||||
pub unfold: Arc<dyn Fn(&mut WindowContext)>,
|
||||
}
|
||||
|
||||
impl RenderOnce for PromptPlaceholder {
|
||||
fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
|
||||
let unfold = self.unfold;
|
||||
ButtonLike::new(self.id)
|
||||
.style(ButtonStyle::Filled)
|
||||
.layer(ElevationIndex::ElevatedSurface)
|
||||
.child(Icon::new(IconName::Library))
|
||||
.child(Label::new(self.title))
|
||||
.on_click(move |_, cx| unfold(cx))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,54 +1,24 @@
|
||||
use std::path::Path;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use assistant_slash_command::{SlashCommand, SlashCommandOutput, SlashCommandOutputSection};
|
||||
use fs::Fs;
|
||||
use futures::AsyncReadExt;
|
||||
use gpui::{AppContext, Model, Task, WeakView};
|
||||
use html_to_markdown::convert_rustdoc_to_markdown;
|
||||
use gpui::{AppContext, Task, WeakView};
|
||||
use http::{AsyncBody, HttpClient, HttpClientWithUrl};
|
||||
use language::LspAdapterDelegate;
|
||||
use project::{Project, ProjectPath};
|
||||
use rustdoc_to_markdown::convert_rustdoc_to_markdown;
|
||||
use ui::{prelude::*, ButtonLike, ElevationIndex};
|
||||
use workspace::Workspace;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum RustdocSource {
|
||||
/// The docs were sourced from local `cargo doc` output.
|
||||
Local,
|
||||
/// The docs were sourced from `docs.rs`.
|
||||
DocsDotRs,
|
||||
}
|
||||
|
||||
pub(crate) struct RustdocSlashCommand;
|
||||
|
||||
impl RustdocSlashCommand {
|
||||
async fn build_message(
|
||||
fs: Arc<dyn Fs>,
|
||||
http_client: Arc<HttpClientWithUrl>,
|
||||
crate_name: String,
|
||||
module_path: Vec<String>,
|
||||
path_to_cargo_toml: Option<&Path>,
|
||||
) -> Result<(RustdocSource, String)> {
|
||||
let cargo_workspace_root = path_to_cargo_toml.and_then(|path| path.parent());
|
||||
if let Some(cargo_workspace_root) = cargo_workspace_root {
|
||||
let mut local_cargo_doc_path = cargo_workspace_root.join("target/doc");
|
||||
local_cargo_doc_path.push(&crate_name);
|
||||
if !module_path.is_empty() {
|
||||
local_cargo_doc_path.push(module_path.join("/"));
|
||||
}
|
||||
local_cargo_doc_path.push("index.html");
|
||||
|
||||
if let Ok(contents) = fs.load(&local_cargo_doc_path).await {
|
||||
return Ok((
|
||||
RustdocSource::Local,
|
||||
convert_rustdoc_to_markdown(contents.as_bytes())?,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
) -> Result<String> {
|
||||
let version = "latest";
|
||||
let path = format!(
|
||||
"{crate_name}/{version}/{crate_name}/{module_path}",
|
||||
@@ -78,23 +48,7 @@ impl RustdocSlashCommand {
|
||||
);
|
||||
}
|
||||
|
||||
Ok((
|
||||
RustdocSource::DocsDotRs,
|
||||
convert_rustdoc_to_markdown(&body[..])?,
|
||||
))
|
||||
}
|
||||
|
||||
fn path_to_cargo_toml(project: Model<Project>, cx: &mut AppContext) -> Option<Arc<Path>> {
|
||||
let worktree = project.read(cx).worktrees().next()?;
|
||||
let worktree = worktree.read(cx);
|
||||
let entry = worktree.entry_for_path("Cargo.toml")?;
|
||||
let path = ProjectPath {
|
||||
worktree_id: worktree.id(),
|
||||
path: entry.path.clone(),
|
||||
};
|
||||
Some(Arc::from(
|
||||
project.read(cx).absolute_path(&path, cx)?.as_path(),
|
||||
))
|
||||
convert_rustdoc_to_markdown(&body[..])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,7 +73,7 @@ impl SlashCommand for RustdocSlashCommand {
|
||||
&self,
|
||||
_query: String,
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
_workspace: WeakView<Workspace>,
|
||||
_cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<String>>> {
|
||||
Task::ready(Ok(Vec::new()))
|
||||
@@ -139,8 +93,6 @@ impl SlashCommand for RustdocSlashCommand {
|
||||
return Task::ready(Err(anyhow!("workspace was dropped")));
|
||||
};
|
||||
|
||||
let project = workspace.read(cx).project().clone();
|
||||
let fs = project.read(cx).fs().clone();
|
||||
let http_client = workspace.read(cx).client().http_client();
|
||||
let mut path_components = argument.split("::");
|
||||
let crate_name = match path_components
|
||||
@@ -151,21 +103,11 @@ impl SlashCommand for RustdocSlashCommand {
|
||||
Err(err) => return Task::ready(Err(err)),
|
||||
};
|
||||
let module_path = path_components.map(ToString::to_string).collect::<Vec<_>>();
|
||||
let path_to_cargo_toml = Self::path_to_cargo_toml(project, cx);
|
||||
|
||||
let text = cx.background_executor().spawn({
|
||||
let crate_name = crate_name.clone();
|
||||
let module_path = module_path.clone();
|
||||
async move {
|
||||
Self::build_message(
|
||||
fs,
|
||||
http_client,
|
||||
crate_name,
|
||||
module_path,
|
||||
path_to_cargo_toml.as_deref(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
async move { Self::build_message(http_client, crate_name, module_path).await }
|
||||
});
|
||||
|
||||
let crate_name = SharedString::from(crate_name);
|
||||
@@ -175,7 +117,7 @@ impl SlashCommand for RustdocSlashCommand {
|
||||
Some(SharedString::from(module_path.join("::")))
|
||||
};
|
||||
cx.foreground_executor().spawn(async move {
|
||||
let (source, text) = text.await?;
|
||||
let text = text.await?;
|
||||
let range = 0..text.len();
|
||||
Ok(SlashCommandOutput {
|
||||
text,
|
||||
@@ -185,14 +127,12 @@ impl SlashCommand for RustdocSlashCommand {
|
||||
RustdocPlaceholder {
|
||||
id,
|
||||
unfold,
|
||||
source,
|
||||
crate_name: crate_name.clone(),
|
||||
module_path: module_path.clone(),
|
||||
}
|
||||
.into_any_element()
|
||||
}),
|
||||
}],
|
||||
run_commands_in_text: false,
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -202,7 +142,6 @@ impl SlashCommand for RustdocSlashCommand {
|
||||
struct RustdocPlaceholder {
|
||||
pub id: ElementId,
|
||||
pub unfold: Arc<dyn Fn(&mut WindowContext)>,
|
||||
pub source: RustdocSource,
|
||||
pub crate_name: SharedString,
|
||||
pub module_path: Option<SharedString>,
|
||||
}
|
||||
@@ -220,13 +159,7 @@ impl RenderOnce for RustdocPlaceholder {
|
||||
.style(ButtonStyle::Filled)
|
||||
.layer(ElevationIndex::ElevatedSurface)
|
||||
.child(Icon::new(IconName::FileRust))
|
||||
.child(Label::new(format!(
|
||||
"rustdoc ({source}): {crate_path}",
|
||||
source = match self.source {
|
||||
RustdocSource::Local => "local",
|
||||
RustdocSource::DocsDotRs => "docs.rs",
|
||||
}
|
||||
)))
|
||||
.child(Label::new(format!("rustdoc: {crate_path}")))
|
||||
.on_click(move |_, cx| unfold(cx))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ impl SlashCommand for SearchSlashCommand {
|
||||
&self,
|
||||
_query: String,
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
_workspace: WeakView<Workspace>,
|
||||
_cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<String>>> {
|
||||
Task::ready(Ok(Vec::new()))
|
||||
@@ -181,11 +181,7 @@ impl SlashCommand for SearchSlashCommand {
|
||||
}),
|
||||
});
|
||||
|
||||
SlashCommandOutput {
|
||||
text,
|
||||
sections,
|
||||
run_commands_in_text: false,
|
||||
}
|
||||
SlashCommandOutput { text, sections }
|
||||
})
|
||||
.await;
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ impl SlashCommand for TabsSlashCommand {
|
||||
&self,
|
||||
_query: String,
|
||||
_cancel: Arc<std::sync::atomic::AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
_workspace: WeakView<Workspace>,
|
||||
_cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<String>>> {
|
||||
Task::ready(Err(anyhow!("this command does not require argument")))
|
||||
@@ -109,11 +109,7 @@ impl SlashCommand for TabsSlashCommand {
|
||||
});
|
||||
}
|
||||
|
||||
Ok(SlashCommandOutput {
|
||||
text,
|
||||
sections,
|
||||
run_commands_in_text: false,
|
||||
})
|
||||
Ok(SlashCommandOutput { text, sections })
|
||||
}),
|
||||
Err(error) => Task::ready(Err(error)),
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ pub trait SlashCommand: 'static + Send + Sync {
|
||||
&self,
|
||||
query: String,
|
||||
cancel: Arc<AtomicBool>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
workspace: WeakView<Workspace>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<String>>>;
|
||||
fn requires_argument(&self) -> bool;
|
||||
@@ -52,7 +52,6 @@ pub type RenderFoldPlaceholder = Arc<
|
||||
pub struct SlashCommandOutput {
|
||||
pub text: String,
|
||||
pub sections: Vec<SlashCommandOutputSection<usize>>,
|
||||
pub run_commands_in_text: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
|
||||
@@ -23,10 +23,7 @@ use smol::{fs::File, process::Command};
|
||||
use http::{HttpClient, HttpClientWithUrl};
|
||||
use release_channel::{AppCommitSha, AppVersion, ReleaseChannel};
|
||||
use std::{
|
||||
env::{
|
||||
self,
|
||||
consts::{ARCH, OS},
|
||||
},
|
||||
env::consts::{ARCH, OS},
|
||||
ffi::OsString,
|
||||
path::PathBuf,
|
||||
sync::Arc,
|
||||
@@ -141,24 +138,20 @@ pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut AppContext) {
|
||||
let auto_updater = cx.new_model(|cx| {
|
||||
let updater = AutoUpdater::new(version, http_client);
|
||||
|
||||
if option_env!("ZED_UPDATE_EXPLANATION").is_none()
|
||||
&& env::var("ZED_UPDATE_EXPLANATION").is_err()
|
||||
{
|
||||
let mut update_subscription = AutoUpdateSetting::get_global(cx)
|
||||
.0
|
||||
.then(|| updater.start_polling(cx));
|
||||
let mut update_subscription = AutoUpdateSetting::get_global(cx)
|
||||
.0
|
||||
.then(|| updater.start_polling(cx));
|
||||
|
||||
cx.observe_global::<SettingsStore>(move |updater, cx| {
|
||||
if AutoUpdateSetting::get_global(cx).0 {
|
||||
if update_subscription.is_none() {
|
||||
update_subscription = Some(updater.start_polling(cx))
|
||||
}
|
||||
} else {
|
||||
update_subscription.take();
|
||||
cx.observe_global::<SettingsStore>(move |updater, cx| {
|
||||
if AutoUpdateSetting::get_global(cx).0 {
|
||||
if update_subscription.is_none() {
|
||||
update_subscription = Some(updater.start_polling(cx))
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
} else {
|
||||
update_subscription.take();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
updater
|
||||
});
|
||||
@@ -166,26 +159,6 @@ pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut AppContext) {
|
||||
}
|
||||
|
||||
pub fn check(_: &Check, cx: &mut WindowContext) {
|
||||
if let Some(message) = option_env!("ZED_UPDATE_EXPLANATION") {
|
||||
drop(cx.prompt(
|
||||
gpui::PromptLevel::Info,
|
||||
"Zed was installed via a package manager.",
|
||||
Some(message),
|
||||
&["Ok"],
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(message) = env::var("ZED_UPDATE_EXPLANATION").ok() {
|
||||
drop(cx.prompt(
|
||||
gpui::PromptLevel::Info,
|
||||
"Zed was installed via a package manager.",
|
||||
Some(&message),
|
||||
&["Ok"],
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(updater) = AutoUpdater::get(cx) {
|
||||
updater.update(cx, |updater, cx| updater.poll(cx));
|
||||
} else {
|
||||
@@ -369,6 +342,16 @@ impl AutoUpdater {
|
||||
}
|
||||
|
||||
async fn update(this: Model<Self>, mut cx: AsyncAppContext) -> Result<()> {
|
||||
// Skip auto-update for flatpaks
|
||||
#[cfg(target_os = "linux")]
|
||||
if matches!(std::env::var("ZED_IS_FLATPAK_INSTALL"), Ok(_)) {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.status = AutoUpdateStatus::Idle;
|
||||
cx.notify();
|
||||
})?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let (client, current_version) = this.read_with(&cx, |this, _| {
|
||||
(this.http_client.clone(), this.current_version)
|
||||
})?;
|
||||
@@ -526,7 +509,7 @@ async fn install_release_linux(
|
||||
cx: &AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
let channel = cx.update(|cx| ReleaseChannel::global(cx).dev_name())?;
|
||||
let home_dir = PathBuf::from(env::var("HOME").context("no HOME env var set")?);
|
||||
let home_dir = PathBuf::from(std::env::var("HOME").context("no HOME env var set")?);
|
||||
|
||||
let extracted = temp_dir.path().join("zed");
|
||||
fs::create_dir_all(&extracted)
|
||||
|
||||
@@ -191,15 +191,14 @@ mod linux {
|
||||
let cli = env::current_exe()?;
|
||||
let dir = cli
|
||||
.parent()
|
||||
.and_then(Path::parent)
|
||||
.ok_or_else(|| anyhow!("no parent path for cli"))?;
|
||||
|
||||
match dir.join("libexec").join("zed-editor").canonicalize() {
|
||||
match dir.join("zed").canonicalize() {
|
||||
Ok(path) => Ok(path),
|
||||
// In development cli and zed are in the ./target/ directory together
|
||||
Err(e) => match cli.parent().unwrap().join("zed").canonicalize() {
|
||||
Ok(path) if path != cli => Ok(path),
|
||||
_ => Err(e),
|
||||
// development builds have Zed capitalized
|
||||
Err(e) => match dir.join("Zed").canonicalize() {
|
||||
Ok(path) => Ok(path),
|
||||
Err(_) => Err(e),
|
||||
},
|
||||
}
|
||||
}?;
|
||||
@@ -255,8 +254,10 @@ mod linux {
|
||||
eprintln!("failed to setsid: {}", std::io::Error::last_os_error());
|
||||
process::exit(1);
|
||||
}
|
||||
if let Err(_) = fork::close_fd() {
|
||||
eprintln!("failed to close_fd: {}", std::io::Error::last_os_error());
|
||||
if std::env::var("ZED_KEEP_FD").is_err() {
|
||||
if let Err(_) = fork::close_fd() {
|
||||
eprintln!("failed to close_fd: {}", std::io::Error::last_os_error());
|
||||
}
|
||||
}
|
||||
let error =
|
||||
exec::execvp(path.clone(), &[path.as_os_str(), &OsString::from(ipc_url)]);
|
||||
@@ -314,7 +315,7 @@ mod flatpak {
|
||||
if let Some(flatpak_dir) = get_flatpak_dir() {
|
||||
let mut args = vec!["/usr/bin/flatpak-spawn".into(), "--host".into()];
|
||||
args.append(&mut get_xdg_env_args());
|
||||
args.push("--env=ZED_UPDATE_EXPLANATION=Please use flatpak to update zed".into());
|
||||
args.push("--env=ZED_IS_FLATPAK_INSTALL=1".into());
|
||||
args.push(
|
||||
format!(
|
||||
"--env={EXTRA_LIB_ENV_NAME}={}",
|
||||
@@ -332,7 +333,7 @@ mod flatpak {
|
||||
|
||||
if !is_app_location_set {
|
||||
args.push("--zed".into());
|
||||
args.push(flatpak_dir.join("libexec").join("zed-editor").into());
|
||||
args.push(flatpak_dir.join("bin").join("zed-app").into());
|
||||
}
|
||||
|
||||
let error = exec::execvp("/usr/bin/flatpak-spawn", args);
|
||||
@@ -346,8 +347,8 @@ mod flatpak {
|
||||
&& env::var("FLATPAK_ID").map_or(false, |id| id.starts_with("dev.zed.Zed"))
|
||||
{
|
||||
if args.zed.is_none() {
|
||||
args.zed = Some("/app/libexec/zed-editor".into());
|
||||
env::set_var("ZED_UPDATE_EXPLANATION", "Please use flatpak to update zed");
|
||||
args.zed = Some("/app/bin/zed-app".into());
|
||||
env::set_var("ZED_IS_FLATPAK_INSTALL", "1");
|
||||
}
|
||||
}
|
||||
args
|
||||
|
||||
@@ -107,5 +107,4 @@ theme.workspace = true
|
||||
unindent.workspace = true
|
||||
util.workspace = true
|
||||
workspace = { workspace = true, features = ["test-support"] }
|
||||
worktree = { workspace = true, features = ["test-support"] }
|
||||
headless.workspace = true
|
||||
|
||||
@@ -239,74 +239,61 @@ async fn fetch_extensions_from_blob_store(
|
||||
) -> anyhow::Result<()> {
|
||||
log::info!("fetching extensions from blob store");
|
||||
|
||||
let mut next_marker = None;
|
||||
let mut published_versions = HashMap::<String, Vec<String>>::default();
|
||||
let list = blob_store_client
|
||||
.list_objects()
|
||||
.bucket(blob_store_bucket)
|
||||
.prefix("extensions/")
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
loop {
|
||||
let list = blob_store_client
|
||||
.list_objects()
|
||||
.bucket(blob_store_bucket)
|
||||
.prefix("extensions/")
|
||||
.set_marker(next_marker.clone())
|
||||
.send()
|
||||
.await?;
|
||||
let objects = list.contents.unwrap_or_default();
|
||||
log::info!("fetched {} object(s) from blob store", objects.len());
|
||||
let objects = list.contents.unwrap_or_default();
|
||||
|
||||
for object in &objects {
|
||||
let Some(key) = object.key.as_ref() else {
|
||||
continue;
|
||||
};
|
||||
let mut parts = key.split('/');
|
||||
let Some(_) = parts.next().filter(|part| *part == "extensions") else {
|
||||
continue;
|
||||
};
|
||||
let Some(extension_id) = parts.next() else {
|
||||
continue;
|
||||
};
|
||||
let Some(version) = parts.next() else {
|
||||
continue;
|
||||
};
|
||||
if parts.next() == Some("manifest.json") {
|
||||
published_versions
|
||||
.entry(extension_id.to_owned())
|
||||
.or_default()
|
||||
.push(version.to_owned());
|
||||
}
|
||||
}
|
||||
|
||||
if let (Some(true), Some(last_object)) = (list.is_truncated, objects.last()) {
|
||||
next_marker.clone_from(&last_object.key);
|
||||
} else {
|
||||
break;
|
||||
let mut published_versions = HashMap::<&str, Vec<&str>>::default();
|
||||
for object in &objects {
|
||||
let Some(key) = object.key.as_ref() else {
|
||||
continue;
|
||||
};
|
||||
let mut parts = key.split('/');
|
||||
let Some(_) = parts.next().filter(|part| *part == "extensions") else {
|
||||
continue;
|
||||
};
|
||||
let Some(extension_id) = parts.next() else {
|
||||
continue;
|
||||
};
|
||||
let Some(version) = parts.next() else {
|
||||
continue;
|
||||
};
|
||||
if parts.next() == Some("manifest.json") {
|
||||
published_versions
|
||||
.entry(extension_id)
|
||||
.or_default()
|
||||
.push(version);
|
||||
}
|
||||
}
|
||||
|
||||
log::info!("found {} published extensions", published_versions.len());
|
||||
|
||||
let known_versions = app_state.db.get_known_extension_versions().await?;
|
||||
|
||||
let mut new_versions = HashMap::<&str, Vec<NewExtensionVersion>>::default();
|
||||
let empty = Vec::new();
|
||||
for (extension_id, published_versions) in &published_versions {
|
||||
for (extension_id, published_versions) in published_versions {
|
||||
let known_versions = known_versions.get(extension_id).unwrap_or(&empty);
|
||||
|
||||
for published_version in published_versions {
|
||||
if known_versions
|
||||
.binary_search_by_key(&published_version, |known_version| known_version)
|
||||
.binary_search_by_key(&published_version, String::as_str)
|
||||
.is_err()
|
||||
{
|
||||
if let Some(extension) = fetch_extension_manifest(
|
||||
blob_store_client,
|
||||
blob_store_bucket,
|
||||
&extension_id,
|
||||
&published_version,
|
||||
extension_id,
|
||||
published_version,
|
||||
)
|
||||
.await
|
||||
.log_err()
|
||||
{
|
||||
new_versions
|
||||
.entry(&extension_id)
|
||||
.entry(extension_id)
|
||||
.or_default()
|
||||
.push(extension);
|
||||
}
|
||||
|
||||
@@ -545,9 +545,6 @@ impl Server {
|
||||
.add_request_handler(user_handler(
|
||||
forward_mutating_project_request::<proto::MultiLspQuery>,
|
||||
))
|
||||
.add_request_handler(user_handler(
|
||||
forward_mutating_project_request::<proto::RestartLanguageServers>,
|
||||
))
|
||||
.add_message_handler(create_buffer_for_peer)
|
||||
.add_request_handler(update_buffer)
|
||||
.add_message_handler(broadcast_project_message_from_host::<proto::RefreshInlayHints>)
|
||||
|
||||
@@ -3022,6 +3022,7 @@ async fn test_fs_operations(
|
||||
let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
|
||||
|
||||
let worktree_a = project_a.read_with(cx_a, |project, _| project.worktrees().next().unwrap());
|
||||
|
||||
let worktree_b = project_b.read_with(cx_b, |project, _| project.worktrees().next().unwrap());
|
||||
|
||||
let entry = project_b
|
||||
@@ -3030,7 +3031,6 @@ async fn test_fs_operations(
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.to_included()
|
||||
.unwrap();
|
||||
|
||||
worktree_a.read_with(cx_a, |worktree, _| {
|
||||
@@ -3059,7 +3059,6 @@ async fn test_fs_operations(
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.to_included()
|
||||
.unwrap();
|
||||
|
||||
worktree_a.read_with(cx_a, |worktree, _| {
|
||||
@@ -3088,7 +3087,6 @@ async fn test_fs_operations(
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.to_included()
|
||||
.unwrap();
|
||||
|
||||
worktree_a.read_with(cx_a, |worktree, _| {
|
||||
@@ -3117,25 +3115,20 @@ async fn test_fs_operations(
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.to_included()
|
||||
.unwrap();
|
||||
|
||||
project_b
|
||||
.update(cx_b, |project, cx| {
|
||||
project.create_entry((worktree_id, "DIR/SUBDIR"), true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.to_included()
|
||||
.unwrap();
|
||||
|
||||
project_b
|
||||
.update(cx_b, |project, cx| {
|
||||
project.create_entry((worktree_id, "DIR/SUBDIR/f.txt"), false, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.to_included()
|
||||
.unwrap();
|
||||
|
||||
worktree_a.read_with(cx_a, |worktree, _| {
|
||||
|
||||
@@ -5,7 +5,7 @@ use client::{proto::PeerId, Client, User, UserStore};
|
||||
use gpui::{
|
||||
actions, canvas, div, point, px, Action, AnyElement, AppContext, Element, Hsla,
|
||||
InteractiveElement, IntoElement, Model, ParentElement, Path, Render,
|
||||
StatefulInteractiveElement, Styled, Subscription, ViewContext, VisualContext, WeakView,
|
||||
StatefulInteractiveElement, Styled, Subscription, View, ViewContext, VisualContext, WeakView,
|
||||
};
|
||||
use project::{Project, RepositoryEntry};
|
||||
use recent_projects::RecentProjects;
|
||||
@@ -17,7 +17,7 @@ use ui::{
|
||||
ButtonStyle, ContextMenu, Icon, IconButton, IconName, Indicator, TintColor, TitleBar, Tooltip,
|
||||
};
|
||||
use util::ResultExt;
|
||||
use vcs_menu::{BranchList, OpenRecent as ToggleVcsMenu};
|
||||
use vcs_menu::{build_branch_list, BranchList, OpenRecent as ToggleVcsMenu};
|
||||
use workspace::{notifications::NotifyResultExt, Workspace};
|
||||
|
||||
const MAX_PROJECT_NAME_LENGTH: usize = 40;
|
||||
@@ -487,7 +487,7 @@ impl CollabTitlebarItem {
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn render_project_branch(&self, cx: &mut ViewContext<Self>) -> Option<impl IntoElement> {
|
||||
pub fn render_project_branch(&self, cx: &mut ViewContext<Self>) -> Option<impl Element> {
|
||||
let entry = {
|
||||
let mut names_and_branches =
|
||||
self.project.read(cx).visible_worktrees(cx).map(|worktree| {
|
||||
@@ -503,23 +503,22 @@ impl CollabTitlebarItem {
|
||||
.and_then(RepositoryEntry::branch)
|
||||
.map(|branch| util::truncate_and_trailoff(&branch, MAX_BRANCH_NAME_LENGTH))?;
|
||||
Some(
|
||||
Button::new("project_branch_trigger", branch_name)
|
||||
.color(Color::Muted)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.label_size(LabelSize::Small)
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::with_meta(
|
||||
"Recent Branches",
|
||||
Some(&ToggleVcsMenu),
|
||||
"Local branches only",
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.on_click(move |_, cx| {
|
||||
let _ = workspace.update(cx, |this, cx| {
|
||||
BranchList::open(this, &Default::default(), cx)
|
||||
});
|
||||
}),
|
||||
popover_menu("project_branch_trigger")
|
||||
.trigger(
|
||||
Button::new("project_branch_trigger", branch_name)
|
||||
.color(Color::Muted)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.label_size(LabelSize::Small)
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::with_meta(
|
||||
"Recent Branches",
|
||||
Some(&ToggleVcsMenu),
|
||||
"Local branches only",
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
)
|
||||
.menu(move |cx| Self::render_vcs_popover(workspace.clone(), cx)),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -651,6 +650,16 @@ impl CollabTitlebarItem {
|
||||
.log_err();
|
||||
}
|
||||
|
||||
pub fn render_vcs_popover(
|
||||
workspace: View<Workspace>,
|
||||
cx: &mut WindowContext<'_>,
|
||||
) -> Option<View<BranchList>> {
|
||||
let view = build_branch_list(workspace, cx).log_err()?;
|
||||
let focus_handle = view.focus_handle(cx);
|
||||
cx.focus(&focus_handle);
|
||||
Some(view)
|
||||
}
|
||||
|
||||
fn render_connection_status(
|
||||
&self,
|
||||
status: &client::Status,
|
||||
|
||||
@@ -29,9 +29,13 @@ impl DebouncedDelay {
|
||||
let (sender, mut receiver) = oneshot::channel::<()>();
|
||||
self.cancel_channel = Some(sender);
|
||||
|
||||
drop(self.task.take());
|
||||
let previous_task = self.task.take();
|
||||
self.task = Some(cx.spawn(move |model, mut cx| async move {
|
||||
let mut timer = cx.background_executor().timer(delay).fuse();
|
||||
if let Some(previous_task) = previous_task {
|
||||
previous_task.await;
|
||||
}
|
||||
|
||||
futures::select_biased! {
|
||||
_ = receiver => return,
|
||||
_ = timer => {}
|
||||
|
||||
@@ -15,17 +15,18 @@
|
||||
pub mod actions;
|
||||
mod blame_entry_tooltip;
|
||||
mod blink_manager;
|
||||
mod debounced_delay;
|
||||
pub mod display_map;
|
||||
mod editor_settings;
|
||||
mod element;
|
||||
mod hunk_diff;
|
||||
mod inlay_hint_cache;
|
||||
|
||||
mod debounced_delay;
|
||||
mod git;
|
||||
mod highlight_matching_bracket;
|
||||
mod hover_links;
|
||||
mod hover_popover;
|
||||
mod hunk_diff;
|
||||
mod indent_guides;
|
||||
mod inlay_hint_cache;
|
||||
mod inline_completion_provider;
|
||||
pub mod items;
|
||||
mod mouse_context_menu;
|
||||
@@ -67,10 +68,10 @@ use gpui::{
|
||||
div, impl_actions, point, prelude::*, px, relative, size, uniform_list, Action, AnyElement,
|
||||
AppContext, AsyncWindowContext, AvailableSpace, BackgroundExecutor, Bounds, ClipboardItem,
|
||||
Context, DispatchPhase, ElementId, EventEmitter, FocusHandle, FocusableView, FontId, FontStyle,
|
||||
FontWeight, HighlightStyle, Hsla, InteractiveText, KeyContext, ListSizingBehavior, Model,
|
||||
MouseButton, PaintQuad, ParentElement, Pixels, Render, SharedString, Size, StrikethroughStyle,
|
||||
Styled, StyledText, Subscription, Task, TextStyle, UnderlineStyle, UniformListScrollHandle,
|
||||
View, ViewContext, ViewInputHandler, VisualContext, WeakView, WhiteSpace, WindowContext,
|
||||
FontWeight, HighlightStyle, Hsla, InteractiveText, KeyContext, Model, MouseButton, PaintQuad,
|
||||
ParentElement, Pixels, Render, SharedString, Size, StrikethroughStyle, Styled, StyledText,
|
||||
Subscription, Task, TextStyle, UnderlineStyle, UniformListScrollHandle, View, ViewContext,
|
||||
ViewInputHandler, VisualContext, WeakView, WhiteSpace, WindowContext,
|
||||
};
|
||||
use highlight_matching_bracket::refresh_matching_bracket_highlights;
|
||||
use hover_popover::{hide_hover, HoverState};
|
||||
@@ -553,20 +554,6 @@ pub struct GutterDimensions {
|
||||
pub git_blame_entries_width: Option<Pixels>,
|
||||
}
|
||||
|
||||
impl GutterDimensions {
|
||||
/// The full width of the space taken up by the gutter.
|
||||
pub fn full_width(&self) -> Pixels {
|
||||
self.margin + self.width
|
||||
}
|
||||
|
||||
/// The width of the space reserved for the fold indicators,
|
||||
/// use alongside 'justify_end' and `gutter_width` to
|
||||
/// right align content with the line numbers
|
||||
pub fn fold_area_width(&self) -> Pixels {
|
||||
self.margin + self.right_padding
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for GutterDimensions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
@@ -1126,8 +1113,7 @@ impl CompletionsMenu {
|
||||
.occlude()
|
||||
.max_h(max_height)
|
||||
.track_scroll(self.scroll_handle.clone())
|
||||
.with_width_from_item(widest_completion_ix)
|
||||
.with_sizing_behavior(ListSizingBehavior::Infer);
|
||||
.with_width_from_item(widest_completion_ix);
|
||||
|
||||
Popover::new()
|
||||
.child(list)
|
||||
@@ -1474,7 +1460,6 @@ impl CodeActionsMenu {
|
||||
})
|
||||
.map(|(ix, _)| ix),
|
||||
)
|
||||
.with_sizing_behavior(ListSizingBehavior::Infer)
|
||||
.into_any_element();
|
||||
|
||||
let cursor_position = if let Some(row) = self.deployed_from_indicator {
|
||||
@@ -1712,12 +1697,6 @@ impl Editor {
|
||||
cx.on_focus(&focus_handle, Self::handle_focus).detach();
|
||||
cx.on_blur(&focus_handle, Self::handle_blur).detach();
|
||||
|
||||
let show_indent_guides = if mode == EditorMode::SingleLine {
|
||||
Some(false)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mut this = Self {
|
||||
focus_handle,
|
||||
buffer: buffer.clone(),
|
||||
@@ -1747,7 +1726,7 @@ impl Editor {
|
||||
show_git_diff_gutter: None,
|
||||
show_code_actions: None,
|
||||
show_wrap_guides: None,
|
||||
show_indent_guides,
|
||||
show_indent_guides: None,
|
||||
placeholder_text: None,
|
||||
highlight_order: 0,
|
||||
highlighted_rows: HashMap::default(),
|
||||
@@ -3805,9 +3784,6 @@ impl Editor {
|
||||
let id = post_inc(&mut self.next_completion_id);
|
||||
let task = cx.spawn(|this, mut cx| {
|
||||
async move {
|
||||
this.update(&mut cx, |this, _| {
|
||||
this.completion_tasks.retain(|(task_id, _)| *task_id >= id);
|
||||
})?;
|
||||
let completions = completions.await.log_err();
|
||||
let menu = if let Some(completions) = completions {
|
||||
let mut menu = CompletionsMenu {
|
||||
@@ -3846,6 +3822,7 @@ impl Editor {
|
||||
let delay_ms = EditorSettings::get_global(cx)
|
||||
.completion_documentation_secondary_query_debounce;
|
||||
let delay = Duration::from_millis(delay_ms);
|
||||
|
||||
editor
|
||||
.completion_documentation_pre_resolve_debounce
|
||||
.fire_new(delay, cx, |editor, cx| {
|
||||
@@ -3866,6 +3843,8 @@ impl Editor {
|
||||
};
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.completion_tasks.retain(|(task_id, _)| *task_id >= id);
|
||||
|
||||
let mut context_menu = this.context_menu.write();
|
||||
match context_menu.as_ref() {
|
||||
None => {}
|
||||
@@ -9791,19 +9770,19 @@ impl Editor {
|
||||
}
|
||||
|
||||
pub fn toggle_indent_guides(&mut self, _: &ToggleIndentGuides, cx: &mut ViewContext<Self>) {
|
||||
let currently_enabled = self.should_show_indent_guides().unwrap_or_else(|| {
|
||||
let currently_enabled = self.should_show_indent_guides(cx);
|
||||
self.show_indent_guides = Some(!currently_enabled);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn should_show_indent_guides(&self, cx: &mut ViewContext<Self>) -> bool {
|
||||
self.show_indent_guides.unwrap_or_else(|| {
|
||||
self.buffer
|
||||
.read(cx)
|
||||
.settings_at(0, cx)
|
||||
.indent_guides
|
||||
.enabled
|
||||
});
|
||||
self.show_indent_guides = Some(!currently_enabled);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn should_show_indent_guides(&self) -> Option<bool> {
|
||||
self.show_indent_guides
|
||||
})
|
||||
}
|
||||
|
||||
pub fn toggle_line_numbers(&mut self, _: &ToggleLineNumbers, cx: &mut ViewContext<Self>) {
|
||||
@@ -11595,6 +11574,7 @@ impl ViewInputHandler for Editor {
|
||||
}
|
||||
|
||||
fn unmark_text(&mut self, cx: &mut ViewContext<Self>) {
|
||||
dbg!("unmark text");
|
||||
self.clear_highlights::<InputComposition>(cx);
|
||||
self.ime_transaction.take();
|
||||
}
|
||||
@@ -11605,6 +11585,7 @@ impl ViewInputHandler for Editor {
|
||||
text: &str,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
dbg!((&range_utf16, text));
|
||||
if !self.input_enabled {
|
||||
cx.emit(EditorEvent::InputIgnored { text: text.into() });
|
||||
return;
|
||||
@@ -11667,6 +11648,7 @@ impl ViewInputHandler for Editor {
|
||||
new_selected_range_utf16: Option<Range<usize>>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
dbg!((&range_utf16, text, &new_selected_range_utf16));
|
||||
if !self.input_enabled {
|
||||
cx.emit(EditorEvent::InputIgnored { text: text.into() });
|
||||
return;
|
||||
|
||||
@@ -20,7 +20,6 @@ use language::{
|
||||
FakeLspAdapter, IndentGuide, LanguageConfig, LanguageConfigOverride, LanguageMatcher, Override,
|
||||
Point,
|
||||
};
|
||||
use language_settings::IndentGuideSettings;
|
||||
use multi_buffer::MultiBufferIndentGuide;
|
||||
use parking_lot::Mutex;
|
||||
use project::project_settings::{LspSettings, ProjectSettings};
|
||||
@@ -11506,7 +11505,6 @@ fn assert_indent_guides(
|
||||
let snapshot = editor.snapshot(cx).display_snapshot;
|
||||
let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
|
||||
MultiBufferRow(range.start)..MultiBufferRow(range.end),
|
||||
true,
|
||||
&snapshot,
|
||||
cx,
|
||||
);
|
||||
@@ -11545,21 +11543,6 @@ fn assert_indent_guides(
|
||||
assert_eq!(indent_guides, expected, "Indent guides do not match");
|
||||
}
|
||||
|
||||
fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
|
||||
IndentGuide {
|
||||
buffer_id,
|
||||
start_row,
|
||||
end_row,
|
||||
depth,
|
||||
tab_size: 4,
|
||||
settings: IndentGuideSettings {
|
||||
enabled: true,
|
||||
line_width: 1,
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_indent_guide_single_line(cx: &mut gpui::TestAppContext) {
|
||||
let (buffer_id, mut cx) = setup_indent_guides_editor(
|
||||
@@ -11572,7 +11555,12 @@ async fn test_indent_guide_single_line(cx: &mut gpui::TestAppContext) {
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
|
||||
assert_indent_guides(
|
||||
0..3,
|
||||
vec![IndentGuide::new(buffer_id, 1, 1, 0, 4)],
|
||||
None,
|
||||
&mut cx,
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
@@ -11588,7 +11576,12 @@ async fn test_indent_guide_simple_block(cx: &mut gpui::TestAppContext) {
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
|
||||
assert_indent_guides(
|
||||
0..4,
|
||||
vec![IndentGuide::new(buffer_id, 1, 2, 0, 4)],
|
||||
None,
|
||||
&mut cx,
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
@@ -11611,9 +11604,9 @@ async fn test_indent_guide_nested(cx: &mut gpui::TestAppContext) {
|
||||
assert_indent_guides(
|
||||
0..8,
|
||||
vec![
|
||||
indent_guide(buffer_id, 1, 6, 0),
|
||||
indent_guide(buffer_id, 3, 3, 1),
|
||||
indent_guide(buffer_id, 5, 5, 1),
|
||||
IndentGuide::new(buffer_id, 1, 6, 0, 4),
|
||||
IndentGuide::new(buffer_id, 3, 3, 1, 4),
|
||||
IndentGuide::new(buffer_id, 5, 5, 1, 4),
|
||||
],
|
||||
None,
|
||||
&mut cx,
|
||||
@@ -11637,8 +11630,8 @@ async fn test_indent_guide_tab(cx: &mut gpui::TestAppContext) {
|
||||
assert_indent_guides(
|
||||
0..5,
|
||||
vec![
|
||||
indent_guide(buffer_id, 1, 3, 0),
|
||||
indent_guide(buffer_id, 2, 2, 1),
|
||||
IndentGuide::new(buffer_id, 1, 3, 0, 4),
|
||||
IndentGuide::new(buffer_id, 2, 2, 1, 4),
|
||||
],
|
||||
None,
|
||||
&mut cx,
|
||||
@@ -11659,7 +11652,12 @@ async fn test_indent_guide_continues_on_empty_line(cx: &mut gpui::TestAppContext
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
|
||||
assert_indent_guides(
|
||||
0..5,
|
||||
vec![IndentGuide::new(buffer_id, 1, 3, 0, 4)],
|
||||
None,
|
||||
&mut cx,
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
@@ -11685,9 +11683,9 @@ async fn test_indent_guide_complex(cx: &mut gpui::TestAppContext) {
|
||||
assert_indent_guides(
|
||||
0..11,
|
||||
vec![
|
||||
indent_guide(buffer_id, 1, 9, 0),
|
||||
indent_guide(buffer_id, 6, 6, 1),
|
||||
indent_guide(buffer_id, 8, 8, 1),
|
||||
IndentGuide::new(buffer_id, 1, 9, 0, 4),
|
||||
IndentGuide::new(buffer_id, 6, 6, 1, 4),
|
||||
IndentGuide::new(buffer_id, 8, 8, 1, 4),
|
||||
],
|
||||
None,
|
||||
&mut cx,
|
||||
@@ -11717,9 +11715,9 @@ async fn test_indent_guide_starts_off_screen(cx: &mut gpui::TestAppContext) {
|
||||
assert_indent_guides(
|
||||
1..11,
|
||||
vec![
|
||||
indent_guide(buffer_id, 1, 9, 0),
|
||||
indent_guide(buffer_id, 6, 6, 1),
|
||||
indent_guide(buffer_id, 8, 8, 1),
|
||||
IndentGuide::new(buffer_id, 1, 9, 0, 4),
|
||||
IndentGuide::new(buffer_id, 6, 6, 1, 4),
|
||||
IndentGuide::new(buffer_id, 8, 8, 1, 4),
|
||||
],
|
||||
None,
|
||||
&mut cx,
|
||||
@@ -11749,9 +11747,9 @@ async fn test_indent_guide_ends_off_screen(cx: &mut gpui::TestAppContext) {
|
||||
assert_indent_guides(
|
||||
1..10,
|
||||
vec![
|
||||
indent_guide(buffer_id, 1, 9, 0),
|
||||
indent_guide(buffer_id, 6, 6, 1),
|
||||
indent_guide(buffer_id, 8, 8, 1),
|
||||
IndentGuide::new(buffer_id, 1, 9, 0, 4),
|
||||
IndentGuide::new(buffer_id, 6, 6, 1, 4),
|
||||
IndentGuide::new(buffer_id, 8, 8, 1, 4),
|
||||
],
|
||||
None,
|
||||
&mut cx,
|
||||
@@ -11777,9 +11775,9 @@ async fn test_indent_guide_without_brackets(cx: &mut gpui::TestAppContext) {
|
||||
assert_indent_guides(
|
||||
1..10,
|
||||
vec![
|
||||
indent_guide(buffer_id, 1, 4, 0),
|
||||
indent_guide(buffer_id, 2, 3, 1),
|
||||
indent_guide(buffer_id, 3, 3, 2),
|
||||
IndentGuide::new(buffer_id, 1, 4, 0, 4),
|
||||
IndentGuide::new(buffer_id, 2, 3, 1, 4),
|
||||
IndentGuide::new(buffer_id, 3, 3, 2, 4),
|
||||
],
|
||||
None,
|
||||
&mut cx,
|
||||
@@ -11804,8 +11802,8 @@ async fn test_indent_guide_ends_before_empty_line(cx: &mut gpui::TestAppContext)
|
||||
assert_indent_guides(
|
||||
0..6,
|
||||
vec![
|
||||
indent_guide(buffer_id, 1, 2, 0),
|
||||
indent_guide(buffer_id, 2, 2, 1),
|
||||
IndentGuide::new(buffer_id, 1, 2, 0, 4),
|
||||
IndentGuide::new(buffer_id, 2, 2, 1, 4),
|
||||
],
|
||||
None,
|
||||
&mut cx,
|
||||
@@ -11827,7 +11825,12 @@ async fn test_indent_guide_continuing_off_screen(cx: &mut gpui::TestAppContext)
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
|
||||
assert_indent_guides(
|
||||
0..1,
|
||||
vec![IndentGuide::new(buffer_id, 1, 1, 0, 4)],
|
||||
None,
|
||||
&mut cx,
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
@@ -11849,8 +11852,8 @@ async fn test_indent_guide_tabs(cx: &mut gpui::TestAppContext) {
|
||||
assert_indent_guides(
|
||||
0..6,
|
||||
vec![
|
||||
indent_guide(buffer_id, 1, 6, 0),
|
||||
indent_guide(buffer_id, 3, 4, 1),
|
||||
IndentGuide::new(buffer_id, 1, 6, 0, 4),
|
||||
IndentGuide::new(buffer_id, 3, 4, 1, 4),
|
||||
],
|
||||
None,
|
||||
&mut cx,
|
||||
@@ -11877,7 +11880,7 @@ async fn test_active_indent_guide_single_line(cx: &mut gpui::TestAppContext) {
|
||||
|
||||
assert_indent_guides(
|
||||
0..3,
|
||||
vec![indent_guide(buffer_id, 1, 1, 0)],
|
||||
vec![IndentGuide::new(buffer_id, 1, 1, 0, 4)],
|
||||
Some(vec![0]),
|
||||
&mut cx,
|
||||
);
|
||||
@@ -11906,8 +11909,8 @@ async fn test_active_indent_guide_respect_indented_range(cx: &mut gpui::TestAppC
|
||||
assert_indent_guides(
|
||||
0..4,
|
||||
vec![
|
||||
indent_guide(buffer_id, 1, 3, 0),
|
||||
indent_guide(buffer_id, 2, 2, 1),
|
||||
IndentGuide::new(buffer_id, 1, 3, 0, 4),
|
||||
IndentGuide::new(buffer_id, 2, 2, 1, 4),
|
||||
],
|
||||
Some(vec![1]),
|
||||
&mut cx,
|
||||
@@ -11922,8 +11925,8 @@ async fn test_active_indent_guide_respect_indented_range(cx: &mut gpui::TestAppC
|
||||
assert_indent_guides(
|
||||
0..4,
|
||||
vec![
|
||||
indent_guide(buffer_id, 1, 3, 0),
|
||||
indent_guide(buffer_id, 2, 2, 1),
|
||||
IndentGuide::new(buffer_id, 1, 3, 0, 4),
|
||||
IndentGuide::new(buffer_id, 2, 2, 1, 4),
|
||||
],
|
||||
Some(vec![1]),
|
||||
&mut cx,
|
||||
@@ -11938,8 +11941,8 @@ async fn test_active_indent_guide_respect_indented_range(cx: &mut gpui::TestAppC
|
||||
assert_indent_guides(
|
||||
0..4,
|
||||
vec![
|
||||
indent_guide(buffer_id, 1, 3, 0),
|
||||
indent_guide(buffer_id, 2, 2, 1),
|
||||
IndentGuide::new(buffer_id, 1, 3, 0, 4),
|
||||
IndentGuide::new(buffer_id, 2, 2, 1, 4),
|
||||
],
|
||||
Some(vec![0]),
|
||||
&mut cx,
|
||||
@@ -11968,7 +11971,7 @@ async fn test_active_indent_guide_empty_line(cx: &mut gpui::TestAppContext) {
|
||||
|
||||
assert_indent_guides(
|
||||
0..5,
|
||||
vec![indent_guide(buffer_id, 1, 3, 0)],
|
||||
vec![IndentGuide::new(buffer_id, 1, 3, 0, 4)],
|
||||
Some(vec![0]),
|
||||
&mut cx,
|
||||
);
|
||||
@@ -11994,7 +11997,7 @@ async fn test_active_indent_guide_non_matching_indent(cx: &mut gpui::TestAppCont
|
||||
|
||||
assert_indent_guides(
|
||||
0..3,
|
||||
vec![indent_guide(buffer_id, 1, 2, 0)],
|
||||
vec![IndentGuide::new(buffer_id, 1, 2, 0, 4)],
|
||||
Some(vec![0]),
|
||||
&mut cx,
|
||||
);
|
||||
|
||||
@@ -38,7 +38,7 @@ use gpui::{
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use language::language_settings::{
|
||||
IndentGuideBackgroundColoring, IndentGuideColoring, IndentGuideSettings, ShowWhitespaceSetting,
|
||||
IndentGuideBackgroundColoring, IndentGuideColoring, ShowWhitespaceSetting,
|
||||
};
|
||||
use lsp::DiagnosticSeverity;
|
||||
use multi_buffer::{Anchor, MultiBufferPoint, MultiBufferRow};
|
||||
@@ -1125,7 +1125,9 @@ impl EditorElement {
|
||||
ix as f32 * line_height - (scroll_pixel_position.y % line_height),
|
||||
);
|
||||
let centering_offset = point(
|
||||
(gutter_dimensions.fold_area_width() - fold_indicator_size.width) / 2.,
|
||||
(gutter_dimensions.right_padding + gutter_dimensions.margin
|
||||
- fold_indicator_size.width)
|
||||
/ 2.,
|
||||
(line_height - fold_indicator_size.height) / 2.,
|
||||
);
|
||||
let origin = gutter_hitbox.origin + position + centering_offset;
|
||||
@@ -1220,41 +1222,34 @@ impl EditorElement {
|
||||
.collect::<HashMap<_, _>>()
|
||||
});
|
||||
|
||||
let git_gutter_setting = ProjectSettings::get_global(cx)
|
||||
.git
|
||||
.git_gutter
|
||||
.unwrap_or_default();
|
||||
buffer_snapshot
|
||||
.git_diff_hunks_in_range(buffer_start_row..buffer_end_row)
|
||||
.map(|hunk| diff_hunk_to_display(&hunk, snapshot))
|
||||
.dedup()
|
||||
.map(|hunk| match git_gutter_setting {
|
||||
GitGutterSetting::TrackedFiles => {
|
||||
let hitbox = if let DisplayDiffHunk::Unfolded {
|
||||
display_row_range, ..
|
||||
} = &hunk
|
||||
{
|
||||
let was_expanded = expanded_hunk_display_rows
|
||||
.get(&display_row_range.start)
|
||||
.map(|expanded_end_row| expanded_end_row == &display_row_range.end)
|
||||
.unwrap_or(false);
|
||||
if was_expanded {
|
||||
None
|
||||
} else {
|
||||
let hunk_bounds = Self::diff_hunk_bounds(
|
||||
&snapshot,
|
||||
line_height,
|
||||
gutter_hitbox.bounds,
|
||||
&hunk,
|
||||
);
|
||||
Some(cx.insert_hitbox(hunk_bounds, true))
|
||||
}
|
||||
} else {
|
||||
.map(|hunk| {
|
||||
let hitbox = if let DisplayDiffHunk::Unfolded {
|
||||
display_row_range, ..
|
||||
} = &hunk
|
||||
{
|
||||
let was_expanded = expanded_hunk_display_rows
|
||||
.get(&display_row_range.start)
|
||||
.map(|expanded_end_row| expanded_end_row == &display_row_range.end)
|
||||
.unwrap_or(false);
|
||||
if was_expanded {
|
||||
None
|
||||
};
|
||||
(hunk, hitbox)
|
||||
}
|
||||
GitGutterSetting::Hide => (hunk, None),
|
||||
} else {
|
||||
let hunk_bounds = Self::diff_hunk_bounds(
|
||||
&snapshot,
|
||||
line_height,
|
||||
gutter_hitbox.bounds,
|
||||
&hunk,
|
||||
);
|
||||
Some(cx.insert_hitbox(hunk_bounds, true))
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
(hunk, hitbox)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -1443,7 +1438,6 @@ impl EditorElement {
|
||||
single_indent_width,
|
||||
depth: indent_guide.depth,
|
||||
active: active_indent_guide_indices.contains(&i),
|
||||
settings: indent_guide.settings,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
@@ -2736,6 +2730,14 @@ impl EditorElement {
|
||||
return;
|
||||
};
|
||||
|
||||
let settings = self
|
||||
.editor
|
||||
.read(cx)
|
||||
.buffer()
|
||||
.read(cx)
|
||||
.settings_at(0, cx)
|
||||
.indent_guides;
|
||||
|
||||
let faded_color = |color: Hsla, alpha: f32| {
|
||||
let mut faded = color;
|
||||
faded.a = alpha;
|
||||
@@ -2744,7 +2746,6 @@ impl EditorElement {
|
||||
|
||||
for indent_guide in indent_guides {
|
||||
let indent_accent_colors = cx.theme().accents().color_for_index(indent_guide.depth);
|
||||
let settings = indent_guide.settings;
|
||||
|
||||
// TODO fixed for now, expose them through themes later
|
||||
const INDENT_AWARE_ALPHA: f32 = 0.2;
|
||||
@@ -2752,7 +2753,7 @@ impl EditorElement {
|
||||
const INDENT_AWARE_BACKGROUND_ALPHA: f32 = 0.1;
|
||||
const INDENT_AWARE_BACKGROUND_ACTIVE_ALPHA: f32 = 0.2;
|
||||
|
||||
let line_color = match (settings.coloring, indent_guide.active) {
|
||||
let line_color = match (&settings.coloring, indent_guide.active) {
|
||||
(IndentGuideColoring::Disabled, _) => None,
|
||||
(IndentGuideColoring::Fixed, false) => {
|
||||
Some(cx.theme().colors().editor_indent_guide)
|
||||
@@ -2768,7 +2769,7 @@ impl EditorElement {
|
||||
}
|
||||
};
|
||||
|
||||
let background_color = match (settings.background_coloring, indent_guide.active) {
|
||||
let background_color = match (&settings.background_coloring, indent_guide.active) {
|
||||
(IndentGuideBackgroundColoring::Disabled, _) => None,
|
||||
(IndentGuideBackgroundColoring::IndentAware, false) => Some(faded_color(
|
||||
indent_accent_colors,
|
||||
@@ -4634,7 +4635,7 @@ impl Element for EditorElement {
|
||||
&mut scroll_width,
|
||||
&gutter_dimensions,
|
||||
em_width,
|
||||
gutter_dimensions.full_width(),
|
||||
gutter_dimensions.width + gutter_dimensions.margin,
|
||||
line_height,
|
||||
&line_layouts,
|
||||
cx,
|
||||
@@ -5285,7 +5286,6 @@ pub struct IndentGuideLayout {
|
||||
single_indent_width: Pixels,
|
||||
depth: u32,
|
||||
active: bool,
|
||||
settings: IndentGuideSettings,
|
||||
}
|
||||
|
||||
pub struct CursorLayout {
|
||||
|
||||
@@ -320,7 +320,7 @@ impl Editor {
|
||||
div()
|
||||
.bg(deleted_hunk_color)
|
||||
.size_full()
|
||||
.pl(gutter_dimensions.full_width())
|
||||
.pl(gutter_dimensions.width + gutter_dimensions.margin)
|
||||
.child(editor_with_deleted_text.clone())
|
||||
.into_any_element()
|
||||
}),
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::{ops::Range, time::Duration};
|
||||
|
||||
use collections::HashSet;
|
||||
use gpui::{AppContext, Task};
|
||||
use language::{language_settings::language_settings, BufferRow};
|
||||
use language::BufferRow;
|
||||
use multi_buffer::{MultiBufferIndentGuide, MultiBufferRow};
|
||||
use text::{BufferId, LineIndent, Point};
|
||||
use ui::ViewContext;
|
||||
@@ -37,26 +37,13 @@ impl Editor {
|
||||
snapshot: &DisplaySnapshot,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) -> Option<Vec<MultiBufferIndentGuide>> {
|
||||
let show_indent_guides = self.should_show_indent_guides().unwrap_or_else(|| {
|
||||
if let Some(buffer) = self.buffer().read(cx).as_singleton() {
|
||||
language_settings(buffer.read(cx).language(), buffer.read(cx).file(), cx)
|
||||
.indent_guides
|
||||
.enabled
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
let enabled = self.should_show_indent_guides(cx);
|
||||
|
||||
if !show_indent_guides {
|
||||
return None;
|
||||
if enabled {
|
||||
Some(indent_guides_in_range(visible_buffer_range, snapshot, cx))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
||||
Some(indent_guides_in_range(
|
||||
visible_buffer_range,
|
||||
self.should_show_indent_guides() == Some(true),
|
||||
snapshot,
|
||||
cx,
|
||||
))
|
||||
}
|
||||
|
||||
pub fn find_active_indent_guide_indices(
|
||||
@@ -90,13 +77,8 @@ impl Editor {
|
||||
|
||||
if state.should_refresh() {
|
||||
state.cursor_row = cursor_row;
|
||||
state.dirty = false;
|
||||
|
||||
if indent_guides.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let snapshot = snapshot.clone();
|
||||
state.dirty = false;
|
||||
|
||||
let task = cx
|
||||
.background_executor()
|
||||
@@ -149,7 +131,6 @@ impl Editor {
|
||||
|
||||
pub fn indent_guides_in_range(
|
||||
visible_buffer_range: Range<MultiBufferRow>,
|
||||
ignore_disabled_for_language: bool,
|
||||
snapshot: &DisplaySnapshot,
|
||||
cx: &AppContext,
|
||||
) -> Vec<MultiBufferIndentGuide> {
|
||||
@@ -162,7 +143,7 @@ pub fn indent_guides_in_range(
|
||||
|
||||
snapshot
|
||||
.buffer_snapshot
|
||||
.indent_guides_in_range(start_anchor..end_anchor, ignore_disabled_for_language, cx)
|
||||
.indent_guides_in_range(start_anchor..end_anchor, cx)
|
||||
.into_iter()
|
||||
.filter(|indent_guide| {
|
||||
// Filter out indent guides that are inside a fold
|
||||
|
||||
@@ -39,7 +39,7 @@ impl SlashCommand for ExtensionSlashCommand {
|
||||
&self,
|
||||
_query: String,
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
_workspace: WeakView<Workspace>,
|
||||
_cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<String>>> {
|
||||
Task::ready(Ok(Vec::new()))
|
||||
@@ -100,7 +100,6 @@ impl SlashCommand for ExtensionSlashCommand {
|
||||
}
|
||||
}),
|
||||
}],
|
||||
run_commands_in_text: false,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -351,7 +351,7 @@ impl ExtensionStore {
|
||||
let reload_tx = this.reload_tx.clone();
|
||||
let installed_dir = this.installed_dir.clone();
|
||||
async move {
|
||||
let (mut paths, _) = fs.watch(&installed_dir, FS_WATCH_LATENCY).await;
|
||||
let mut paths = fs.watch(&installed_dir, FS_WATCH_LATENCY).await;
|
||||
while let Some(paths) = paths.next().await {
|
||||
for path in paths {
|
||||
let Ok(event_path) = path.strip_prefix(&installed_dir) else {
|
||||
|
||||
@@ -12,7 +12,7 @@ use editor::{Editor, EditorElement, EditorStyle};
|
||||
use extension::{ExtensionManifest, ExtensionOperation, ExtensionStore};
|
||||
use fuzzy::{match_strings, StringMatchCandidate};
|
||||
use gpui::{
|
||||
actions, uniform_list, AnyElement, AppContext, EventEmitter, FocusableView, FontStyle,
|
||||
actions, canvas, uniform_list, AnyElement, AppContext, EventEmitter, FocusableView, FontStyle,
|
||||
InteractiveElement, KeyContext, ParentElement, Render, Styled, Task, TextStyle,
|
||||
UniformListScrollHandle, View, ViewContext, VisualContext, WeakView, WhiteSpace, WindowContext,
|
||||
};
|
||||
@@ -938,10 +938,24 @@ impl Render for ExtensionsPage {
|
||||
let view = cx.view().clone();
|
||||
let scroll_handle = self.list.clone();
|
||||
this.child(
|
||||
uniform_list(view, "entries", count, Self::render_extensions)
|
||||
.flex_grow()
|
||||
.pb_4()
|
||||
.track_scroll(scroll_handle),
|
||||
canvas(
|
||||
move |bounds, cx| {
|
||||
let mut list = uniform_list::<_, ExtensionCard, _>(
|
||||
view,
|
||||
"entries",
|
||||
count,
|
||||
Self::render_extensions,
|
||||
)
|
||||
.size_full()
|
||||
.pb_4()
|
||||
.track_scroll(scroll_handle)
|
||||
.into_any_element();
|
||||
list.prepaint_as_root(bounds.origin, bounds.size.into(), cx);
|
||||
list
|
||||
},
|
||||
|_bounds, mut list, cx| list.paint(cx),
|
||||
)
|
||||
.size_full(),
|
||||
)
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -36,11 +36,6 @@ use smol::io::AsyncReadExt;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
use std::ffi::OsStr;
|
||||
|
||||
pub trait Watcher: Send + Sync {
|
||||
fn add(&self, path: &Path) -> Result<()>;
|
||||
fn remove(&self, path: &Path) -> Result<()>;
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait Fs: Send + Sync {
|
||||
async fn create_dir(&self, path: &Path) -> Result<()>;
|
||||
@@ -84,10 +79,7 @@ pub trait Fs: Send + Sync {
|
||||
&self,
|
||||
path: &Path,
|
||||
latency: Duration,
|
||||
) -> (
|
||||
Pin<Box<dyn Send + Stream<Item = Vec<PathBuf>>>>,
|
||||
Arc<dyn Watcher>,
|
||||
);
|
||||
) -> Pin<Box<dyn Send + Stream<Item = Vec<PathBuf>>>>;
|
||||
|
||||
fn open_repo(&self, abs_dot_git: &Path) -> Option<Arc<dyn GitRepository>>;
|
||||
fn is_fake(&self) -> bool;
|
||||
@@ -134,13 +126,6 @@ pub struct RealFs {
|
||||
git_binary_path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
pub struct RealWatcher {
|
||||
#[cfg(target_os = "linux")]
|
||||
root_path: PathBuf,
|
||||
#[cfg(target_os = "linux")]
|
||||
fs_watcher: parking_lot::Mutex<notify::INotifyWatcher>,
|
||||
}
|
||||
|
||||
impl RealFs {
|
||||
pub fn new(
|
||||
git_hosting_provider_registry: Arc<GitHostingProviderRegistry>,
|
||||
@@ -424,10 +409,7 @@ impl Fs for RealFs {
|
||||
&self,
|
||||
path: &Path,
|
||||
latency: Duration,
|
||||
) -> (
|
||||
Pin<Box<dyn Send + Stream<Item = Vec<PathBuf>>>>,
|
||||
Arc<dyn Watcher>,
|
||||
) {
|
||||
) -> Pin<Box<dyn Send + Stream<Item = Vec<PathBuf>>>> {
|
||||
use fsevent::EventStream;
|
||||
|
||||
let (tx, rx) = smol::channel::unbounded();
|
||||
@@ -439,76 +421,22 @@ impl Fs for RealFs {
|
||||
});
|
||||
});
|
||||
|
||||
(
|
||||
Box::pin(rx.chain(futures::stream::once(async move {
|
||||
drop(handle);
|
||||
vec![]
|
||||
}))),
|
||||
Arc::new(RealWatcher {}),
|
||||
)
|
||||
Box::pin(rx.chain(futures::stream::once(async move {
|
||||
drop(handle);
|
||||
vec![]
|
||||
})))
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
async fn watch(
|
||||
&self,
|
||||
path: &Path,
|
||||
_latency: Duration,
|
||||
) -> (
|
||||
Pin<Box<dyn Send + Stream<Item = Vec<PathBuf>>>>,
|
||||
Arc<dyn Watcher>,
|
||||
) {
|
||||
let (tx, rx) = smol::channel::unbounded();
|
||||
|
||||
let file_watcher = notify::recommended_watcher({
|
||||
let tx = tx.clone();
|
||||
move |event: Result<notify::Event, _>| {
|
||||
if let Some(event) = event.log_err() {
|
||||
tx.try_send(event.paths).ok();
|
||||
}
|
||||
}
|
||||
})
|
||||
.expect("Could not start file watcher");
|
||||
|
||||
let watcher = Arc::new(RealWatcher {
|
||||
root_path: path.to_path_buf(),
|
||||
fs_watcher: parking_lot::Mutex::new(file_watcher),
|
||||
});
|
||||
|
||||
watcher.add(path).ok(); // Ignore "file doesn't exist error" and rely on parent watcher.
|
||||
|
||||
// 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();
|
||||
move |mut paths| {
|
||||
paths.retain(|path| path.starts_with(&watcher.root_path));
|
||||
async move {
|
||||
if paths.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(paths)
|
||||
}
|
||||
}
|
||||
}
|
||||
})),
|
||||
watcher,
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
async fn watch(
|
||||
&self,
|
||||
path: &Path,
|
||||
_latency: Duration,
|
||||
) -> (
|
||||
Pin<Box<dyn Send + Stream<Item = Vec<PathBuf>>>>,
|
||||
Arc<dyn Watcher>,
|
||||
) {
|
||||
use notify::Watcher;
|
||||
) -> Pin<Box<dyn Send + Stream<Item = Vec<PathBuf>>>> {
|
||||
use notify::{event::EventKind, event::ModifyKind, Watcher};
|
||||
// todo(linux): This spawns two threads, while the macOS impl
|
||||
// only spawns one. Can we use a OnceLock or some such to make
|
||||
// this better
|
||||
|
||||
let (tx, rx) = smol::channel::unbounded();
|
||||
|
||||
@@ -524,15 +452,56 @@ impl Fs for RealFs {
|
||||
|
||||
file_watcher
|
||||
.watch(path, notify::RecursiveMode::Recursive)
|
||||
.log_err();
|
||||
.ok(); // It's ok if this fails, the parent watcher will add it.
|
||||
|
||||
(
|
||||
Box::pin(rx.chain(futures::stream::once(async move {
|
||||
drop(file_watcher);
|
||||
vec![]
|
||||
}))),
|
||||
Arc::new(RealWatcher {}),
|
||||
)
|
||||
let mut parent_watcher = notify::recommended_watcher({
|
||||
let watched_path = path.to_path_buf();
|
||||
let tx = tx.clone();
|
||||
move |event: Result<notify::Event, _>| {
|
||||
if let Some(event) = event.ok() {
|
||||
if event.paths.into_iter().any(|path| *path == watched_path) {
|
||||
match event.kind {
|
||||
EventKind::Modify(ev) => {
|
||||
if matches!(ev, ModifyKind::Name(_)) {
|
||||
file_watcher
|
||||
.watch(
|
||||
watched_path.as_path(),
|
||||
notify::RecursiveMode::Recursive,
|
||||
)
|
||||
.log_err();
|
||||
let _ = tx.try_send(vec![watched_path.clone()]).ok();
|
||||
}
|
||||
}
|
||||
EventKind::Create(_) => {
|
||||
file_watcher
|
||||
.watch(watched_path.as_path(), notify::RecursiveMode::Recursive)
|
||||
.log_err();
|
||||
let _ = tx.try_send(vec![watched_path.clone()]).ok();
|
||||
}
|
||||
EventKind::Remove(_) => {
|
||||
file_watcher.unwatch(&watched_path).log_err();
|
||||
let _ = tx.try_send(vec![watched_path.clone()]).ok();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.expect("Could not start file watcher");
|
||||
|
||||
parent_watcher
|
||||
.watch(
|
||||
path.parent()
|
||||
.expect("Watching root is probably not what you want"),
|
||||
notify::RecursiveMode::NonRecursive,
|
||||
)
|
||||
.expect("Could not start watcher on parent directory");
|
||||
|
||||
Box::pin(rx.chain(futures::stream::once(async move {
|
||||
drop(parent_watcher);
|
||||
vec![]
|
||||
})))
|
||||
}
|
||||
|
||||
fn open_repo(&self, dotgit_path: &Path) -> Option<Arc<dyn GitRepository>> {
|
||||
@@ -591,36 +560,6 @@ impl Fs for RealFs {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
impl Watcher for RealWatcher {
|
||||
fn add(&self, _: &Path) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn remove(&self, _: &Path) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
impl Watcher for RealWatcher {
|
||||
fn add(&self, path: &Path) -> Result<()> {
|
||||
use notify::Watcher;
|
||||
|
||||
self.fs_watcher
|
||||
.lock()
|
||||
.watch(path, notify::RecursiveMode::NonRecursive)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn remove(&self, path: &Path) -> Result<()> {
|
||||
use notify::Watcher;
|
||||
|
||||
self.fs_watcher.lock().unwatch(path)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub struct FakeFs {
|
||||
// Use an unfair lock to ensure tests are deterministic.
|
||||
@@ -1134,20 +1073,6 @@ impl FakeFsEntry {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
struct FakeWatcher {}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
impl Watcher for FakeWatcher {
|
||||
fn add(&self, _: &Path) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn remove(&self, _: &Path) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
#[async_trait::async_trait]
|
||||
impl Fs for FakeFs {
|
||||
@@ -1543,26 +1468,20 @@ impl Fs for FakeFs {
|
||||
&self,
|
||||
path: &Path,
|
||||
_: Duration,
|
||||
) -> (
|
||||
Pin<Box<dyn Send + Stream<Item = Vec<PathBuf>>>>,
|
||||
Arc<dyn Watcher>,
|
||||
) {
|
||||
) -> Pin<Box<dyn Send + Stream<Item = Vec<PathBuf>>>> {
|
||||
self.simulate_random_delay().await;
|
||||
let (tx, rx) = smol::channel::unbounded();
|
||||
self.state.lock().event_txs.push(tx);
|
||||
let path = path.to_path_buf();
|
||||
let executor = self.executor.clone();
|
||||
(
|
||||
Box::pin(futures::StreamExt::filter(rx, move |events| {
|
||||
let result = events.iter().any(|evt_path| evt_path.starts_with(&path));
|
||||
let executor = executor.clone();
|
||||
async move {
|
||||
executor.simulate_random_delay().await;
|
||||
result
|
||||
}
|
||||
})),
|
||||
Arc::new(FakeWatcher {}),
|
||||
)
|
||||
Box::pin(futures::StreamExt::filter(rx, move |events| {
|
||||
let result = events.iter().any(|evt_path| evt_path.starts_with(&path));
|
||||
let executor = executor.clone();
|
||||
async move {
|
||||
executor.simulate_random_delay().await;
|
||||
result
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
fn open_repo(&self, abs_dot_git: &Path) -> Option<Arc<dyn GitRepository>> {
|
||||
|
||||
@@ -99,6 +99,7 @@ objc = "0.2"
|
||||
|
||||
[target.'cfg(any(target_os = "linux", target_os = "windows"))'.dependencies]
|
||||
flume = "0.11"
|
||||
#TODO: use these on all platforms
|
||||
blade-graphics.workspace = true
|
||||
blade-macros.workspace = true
|
||||
blade-util.workspace = true
|
||||
|
||||
@@ -29,11 +29,10 @@ use crate::{
|
||||
current_platform, init_app_menus, Action, ActionRegistry, Any, AnyView, AnyWindowHandle,
|
||||
AppMetadata, AssetCache, AssetSource, BackgroundExecutor, ClipboardItem, Context,
|
||||
DispatchPhase, DisplayId, Entity, EventEmitter, ForegroundExecutor, Global, KeyBinding, Keymap,
|
||||
Keystroke, LayoutId, Menu, MenuItem, OwnedMenu, PathPromptOptions, Pixels, Platform,
|
||||
PlatformDisplay, Point, PromptBuilder, PromptHandle, PromptLevel, Render,
|
||||
RenderablePromptHandle, Reservation, SharedString, SubscriberSet, Subscription, SvgRenderer,
|
||||
Task, TextSystem, View, ViewContext, Window, WindowAppearance, WindowContext, WindowHandle,
|
||||
WindowId,
|
||||
Keystroke, LayoutId, Menu, MenuItem, PathPromptOptions, Pixels, Platform, PlatformDisplay,
|
||||
Point, PromptBuilder, PromptHandle, PromptLevel, Render, RenderablePromptHandle, Reservation,
|
||||
SharedString, SubscriberSet, Subscription, SvgRenderer, Task, TextSystem, View, ViewContext,
|
||||
Window, WindowAppearance, WindowContext, WindowHandle, WindowId,
|
||||
};
|
||||
|
||||
mod async_context;
|
||||
@@ -1168,11 +1167,6 @@ impl AppContext {
|
||||
self.platform.set_menus(menus, &self.keymap.borrow());
|
||||
}
|
||||
|
||||
/// Gets the menu bar for this application.
|
||||
pub fn get_menus(&self) -> Option<Vec<OwnedMenu>> {
|
||||
self.platform.get_menus()
|
||||
}
|
||||
|
||||
/// Sets the right click menu for the app icon in the dock
|
||||
pub fn set_dock_menu(&mut self, menus: Vec<MenuItem>) {
|
||||
self.platform.set_dock_menu(menus, &self.keymap.borrow());
|
||||
|
||||
@@ -80,7 +80,7 @@ pub struct ListScrollEvent {
|
||||
}
|
||||
|
||||
/// The sizing behavior to apply during layout.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
pub enum ListSizingBehavior {
|
||||
/// The list should calculate its size based on the size of its items.
|
||||
Infer,
|
||||
|
||||
@@ -6,9 +6,8 @@
|
||||
|
||||
use crate::{
|
||||
point, px, size, AnyElement, AvailableSpace, Bounds, ContentMask, Element, ElementId,
|
||||
GlobalElementId, Hitbox, InteractiveElement, Interactivity, IntoElement, LayoutId,
|
||||
ListSizingBehavior, Pixels, Render, ScrollHandle, Size, StyleRefinement, Styled, View,
|
||||
ViewContext, WindowContext,
|
||||
GlobalElementId, Hitbox, InteractiveElement, Interactivity, IntoElement, LayoutId, Pixels,
|
||||
Render, ScrollHandle, Size, StyleRefinement, Styled, View, ViewContext, WindowContext,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
use std::{cell::RefCell, cmp, ops::Range, rc::Rc};
|
||||
@@ -56,7 +55,6 @@ where
|
||||
..Default::default()
|
||||
},
|
||||
scroll_handle: None,
|
||||
sizing_behavior: ListSizingBehavior::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,7 +66,6 @@ pub struct UniformList {
|
||||
Box<dyn for<'a> Fn(Range<usize>, &'a mut WindowContext) -> SmallVec<[AnyElement; 64]>>,
|
||||
interactivity: Interactivity,
|
||||
scroll_handle: Option<UniformListScrollHandle>,
|
||||
sizing_behavior: ListSizingBehavior,
|
||||
}
|
||||
|
||||
/// Frame state used by the [UniformList].
|
||||
@@ -123,35 +120,24 @@ impl Element for UniformList {
|
||||
let item_size = self.measure_item(None, cx);
|
||||
let layout_id = self
|
||||
.interactivity
|
||||
.request_layout(global_id, cx, |style, cx| match self.sizing_behavior {
|
||||
ListSizingBehavior::Infer => {
|
||||
cx.with_text_style(style.text_style().cloned(), |cx| {
|
||||
cx.request_measured_layout(
|
||||
style,
|
||||
move |known_dimensions, available_space, _cx| {
|
||||
let desired_height = item_size.height * max_items;
|
||||
let width = known_dimensions.width.unwrap_or(match available_space
|
||||
.width
|
||||
{
|
||||
AvailableSpace::Definite(x) => x,
|
||||
AvailableSpace::MinContent | AvailableSpace::MaxContent => {
|
||||
item_size.width
|
||||
}
|
||||
});
|
||||
let height = match available_space.height {
|
||||
AvailableSpace::Definite(height) => desired_height.min(height),
|
||||
AvailableSpace::MinContent | AvailableSpace::MaxContent => {
|
||||
desired_height
|
||||
}
|
||||
};
|
||||
size(width, height)
|
||||
},
|
||||
)
|
||||
})
|
||||
}
|
||||
ListSizingBehavior::Auto => cx.with_text_style(style.text_style().cloned(), |cx| {
|
||||
cx.request_layout(style, None)
|
||||
}),
|
||||
.request_layout(global_id, cx, |style, cx| {
|
||||
cx.request_measured_layout(style, move |known_dimensions, available_space, _cx| {
|
||||
let desired_height = item_size.height * max_items;
|
||||
let width = known_dimensions
|
||||
.width
|
||||
.unwrap_or(match available_space.width {
|
||||
AvailableSpace::Definite(x) => x,
|
||||
AvailableSpace::MinContent | AvailableSpace::MaxContent => {
|
||||
item_size.width
|
||||
}
|
||||
});
|
||||
|
||||
let height = match available_space.height {
|
||||
AvailableSpace::Definite(height) => desired_height.min(height),
|
||||
AvailableSpace::MinContent | AvailableSpace::MaxContent => desired_height,
|
||||
};
|
||||
size(width, height)
|
||||
})
|
||||
});
|
||||
|
||||
(
|
||||
@@ -294,12 +280,6 @@ impl UniformList {
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the sizing behavior, similar to the `List` element.
|
||||
pub fn with_sizing_behavior(mut self, behavior: ListSizingBehavior) -> Self {
|
||||
self.sizing_behavior = behavior;
|
||||
self
|
||||
}
|
||||
|
||||
fn measure_item(&self, list_width: Option<Pixels>, cx: &mut WindowContext) -> Size<Pixels> {
|
||||
if self.item_count == 0 {
|
||||
return Size::default();
|
||||
|
||||
@@ -712,7 +712,7 @@ impl Size<Length> {
|
||||
/// assert_eq!(bounds.origin, origin);
|
||||
/// assert_eq!(bounds.size, size);
|
||||
/// ```
|
||||
#[derive(Refineable, Clone, Default, Debug, Eq, PartialEq, Hash)]
|
||||
#[derive(Refineable, Clone, Default, Debug, Eq, PartialEq)]
|
||||
#[refineable(Debug)]
|
||||
#[repr(C)]
|
||||
pub struct Bounds<T: Clone + Default + Debug> {
|
||||
|
||||
@@ -304,14 +304,6 @@ impl KeyBindingContextPredicate {
|
||||
source,
|
||||
))
|
||||
}
|
||||
_ if is_vim_operator_char(next) => {
|
||||
let (operator, rest) = source.split_at(1);
|
||||
source = skip_whitespace(rest);
|
||||
Ok((
|
||||
KeyBindingContextPredicate::Identifier(operator.to_string().into()),
|
||||
source,
|
||||
))
|
||||
}
|
||||
_ => Err(anyhow!("unexpected character {next:?}")),
|
||||
}
|
||||
}
|
||||
@@ -355,10 +347,6 @@ fn is_identifier_char(c: char) -> bool {
|
||||
c.is_alphanumeric() || c == '_' || c == '-'
|
||||
}
|
||||
|
||||
fn is_vim_operator_char(c: char) -> bool {
|
||||
c == '>' || c == '<'
|
||||
}
|
||||
|
||||
fn skip_whitespace(source: &str) -> &str {
|
||||
let len = source
|
||||
.find(|c: char| !c.is_whitespace())
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// todo(linux): remove
|
||||
#![cfg_attr(target_os = "linux", allow(dead_code))]
|
||||
// todo(windows): remove
|
||||
#![cfg_attr(windows, allow(dead_code))]
|
||||
|
||||
@@ -133,10 +135,6 @@ pub(crate) trait Platform: 'static {
|
||||
fn on_reopen(&self, callback: Box<dyn FnMut()>);
|
||||
|
||||
fn set_menus(&self, menus: Vec<Menu>, keymap: &Keymap);
|
||||
fn get_menus(&self) -> Option<Vec<OwnedMenu>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn set_dock_menu(&self, menu: Vec<MenuItem>, keymap: &Keymap);
|
||||
fn add_recent_document(&self, _path: &Path) {}
|
||||
fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>);
|
||||
@@ -205,7 +203,7 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
|
||||
fn content_size(&self) -> Size<Pixels>;
|
||||
fn scale_factor(&self) -> f32;
|
||||
fn appearance(&self) -> WindowAppearance;
|
||||
fn display(&self) -> Option<Rc<dyn PlatformDisplay>>;
|
||||
fn display(&self) -> Rc<dyn PlatformDisplay>;
|
||||
fn mouse_position(&self) -> Point<Pixels>;
|
||||
fn modifiers(&self) -> Modifiers;
|
||||
fn set_input_handler(&mut self, input_handler: PlatformInputHandler);
|
||||
@@ -415,7 +413,6 @@ impl PlatformInputHandler {
|
||||
.flatten()
|
||||
}
|
||||
|
||||
#[cfg_attr(target_os = "linux", allow(dead_code))]
|
||||
fn text_for_range(&mut self, range_utf16: Range<usize>) -> Option<String> {
|
||||
self.cx
|
||||
.update(|cx| self.handler.text_for_range(range_utf16, cx))
|
||||
@@ -576,17 +573,13 @@ pub(crate) struct WindowParams {
|
||||
pub titlebar: Option<TitlebarOptions>,
|
||||
|
||||
/// The kind of window to create
|
||||
#[cfg_attr(target_os = "linux", allow(dead_code))]
|
||||
pub kind: WindowKind,
|
||||
|
||||
/// Whether the window should be movable by the user
|
||||
#[cfg_attr(target_os = "linux", allow(dead_code))]
|
||||
pub is_movable: bool,
|
||||
|
||||
#[cfg_attr(target_os = "linux", allow(dead_code))]
|
||||
pub focus: bool,
|
||||
|
||||
#[cfg_attr(target_os = "linux", allow(dead_code))]
|
||||
pub show: bool,
|
||||
|
||||
pub display_id: Option<DisplayId>,
|
||||
@@ -804,6 +797,10 @@ pub enum CursorStyle {
|
||||
/// corresponds to the CSS curosr value `row-resize`
|
||||
ResizeRow,
|
||||
|
||||
/// A cursor indicating that something will disappear if moved here
|
||||
/// Does not correspond to a CSS cursor value
|
||||
DisappearingItem,
|
||||
|
||||
/// A text input cursor for vertical layout
|
||||
/// corresponds to the CSS cursor value `vertical-text`
|
||||
IBeamCursorForVerticalLayout,
|
||||
@@ -868,7 +865,6 @@ impl ClipboardItem {
|
||||
.and_then(|m| serde_json::from_str(m).ok())
|
||||
}
|
||||
|
||||
#[cfg_attr(target_os = "linux", allow(dead_code))]
|
||||
pub(crate) fn text_hash(text: &str) -> u64 {
|
||||
let mut hasher = SeaHasher::new();
|
||||
text.hash(&mut hasher);
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use crate::{Action, AppContext, Platform};
|
||||
use util::ResultExt;
|
||||
|
||||
@@ -12,16 +10,6 @@ pub struct Menu<'a> {
|
||||
pub items: Vec<MenuItem<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> Menu<'a> {
|
||||
/// Create an OwnedMenu from this Menu
|
||||
pub fn owned(self) -> OwnedMenu {
|
||||
OwnedMenu {
|
||||
name: self.name.to_string().into(),
|
||||
items: self.items.into_iter().map(|item| item.owned()).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The different kinds of items that can be in a menu
|
||||
pub enum MenuItem<'a> {
|
||||
/// A separator between items
|
||||
@@ -72,73 +60,6 @@ impl<'a> MenuItem<'a> {
|
||||
os_action: Some(os_action),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create an OwnedMenuItem from this MenuItem
|
||||
pub fn owned(self) -> OwnedMenuItem {
|
||||
match self {
|
||||
MenuItem::Separator => OwnedMenuItem::Separator,
|
||||
MenuItem::Submenu(submenu) => OwnedMenuItem::Submenu(submenu.owned()),
|
||||
MenuItem::Action {
|
||||
name,
|
||||
action,
|
||||
os_action,
|
||||
} => OwnedMenuItem::Action {
|
||||
name: name.into(),
|
||||
action,
|
||||
os_action,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A menu of the application, either a main menu or a submenu
|
||||
#[derive(Clone)]
|
||||
pub struct OwnedMenu {
|
||||
/// The name of the menu
|
||||
pub name: Cow<'static, str>,
|
||||
|
||||
/// The items in the menu
|
||||
pub items: Vec<OwnedMenuItem>,
|
||||
}
|
||||
|
||||
/// The different kinds of items that can be in a menu
|
||||
pub enum OwnedMenuItem {
|
||||
/// A separator between items
|
||||
Separator,
|
||||
|
||||
/// A submenu
|
||||
Submenu(OwnedMenu),
|
||||
|
||||
/// An action that can be performed
|
||||
Action {
|
||||
/// The name of this menu item
|
||||
name: String,
|
||||
|
||||
/// the action to perform when this menu item is selected
|
||||
action: Box<dyn Action>,
|
||||
|
||||
/// The OS Action that corresponds to this action, if any
|
||||
/// See [`OsAction`] for more information
|
||||
os_action: Option<OsAction>,
|
||||
},
|
||||
}
|
||||
|
||||
impl Clone for OwnedMenuItem {
|
||||
fn clone(&self) -> Self {
|
||||
match self {
|
||||
OwnedMenuItem::Separator => OwnedMenuItem::Separator,
|
||||
OwnedMenuItem::Submenu(submenu) => OwnedMenuItem::Submenu(submenu.clone()),
|
||||
OwnedMenuItem::Action {
|
||||
name,
|
||||
action,
|
||||
os_action,
|
||||
} => OwnedMenuItem::Action {
|
||||
name: name.clone(),
|
||||
action: action.boxed_clone(),
|
||||
os_action: *os_action,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: As part of the global selections refactor, these should
|
||||
|
||||
@@ -20,9 +20,7 @@ use std::{mem, sync::Arc};
|
||||
|
||||
const MAX_FRAME_TIME_MS: u32 = 1000;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub type Context = ();
|
||||
#[cfg(target_os = "macos")]
|
||||
pub type Renderer = BladeRenderer;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
|
||||
@@ -294,7 +294,7 @@ impl CosmicTextSystemState {
|
||||
.0,
|
||||
)
|
||||
.clone()
|
||||
.with_context(|| format!("no image for {params:?} in font {font:?}"))?;
|
||||
.unwrap();
|
||||
Ok(Bounds {
|
||||
origin: point(image.placement.left.into(), (-image.placement.top).into()),
|
||||
size: size(image.placement.width.into(), image.placement.height.into()),
|
||||
@@ -314,7 +314,7 @@ impl CosmicTextSystemState {
|
||||
let bitmap_size = glyph_bounds.size;
|
||||
let font = &self.loaded_fonts_store[params.font_id.0];
|
||||
let font_system = &mut self.font_system;
|
||||
let mut image = self
|
||||
let image = self
|
||||
.swash_cache
|
||||
.get_image(
|
||||
font_system,
|
||||
@@ -328,14 +328,7 @@ impl CosmicTextSystemState {
|
||||
.0,
|
||||
)
|
||||
.clone()
|
||||
.with_context(|| format!("no image for {params:?} in font {font:?}"))?;
|
||||
|
||||
if params.is_emoji {
|
||||
// Convert from RGBA to BGRA.
|
||||
for pixel in image.data.chunks_exact_mut(4) {
|
||||
pixel.swap(0, 2);
|
||||
}
|
||||
}
|
||||
.unwrap();
|
||||
|
||||
Ok((bitmap_size, image.data))
|
||||
}
|
||||
@@ -401,20 +394,13 @@ impl CosmicTextSystemState {
|
||||
for glyph in &layout.glyphs {
|
||||
let font_id = glyph.font_id;
|
||||
let font_id = self.font_id_for_cosmic_id(font_id);
|
||||
let is_emoji = self.is_emoji(font_id);
|
||||
let mut glyphs = SmallVec::new();
|
||||
|
||||
// HACK: Prevent crash caused by variation selectors.
|
||||
if glyph.glyph_id == 3 && is_emoji {
|
||||
continue;
|
||||
}
|
||||
|
||||
// todo(linux) this is definitely wrong, each glyph in glyphs from cosmic-text is a cluster with one glyph, ShapedRun takes a run of glyphs with the same font and direction
|
||||
glyphs.push(ShapedGlyph {
|
||||
id: GlyphId(glyph.glyph_id as u32),
|
||||
position: point((glyph.x).into(), glyph.y.into()),
|
||||
index: glyph.start,
|
||||
is_emoji,
|
||||
is_emoji: self.is_emoji(font_id),
|
||||
});
|
||||
|
||||
runs.push(crate::ShapedRun { font_id, glyphs });
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// todo(linux): remove
|
||||
#![allow(unused)]
|
||||
|
||||
mod dispatcher;
|
||||
mod headless;
|
||||
mod platform;
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
#![allow(non_upper_case_globals)]
|
||||
#![allow(non_camel_case_types)]
|
||||
#![allow(non_snake_case)]
|
||||
// todo(linux): remove
|
||||
#![allow(unused_variables)]
|
||||
|
||||
use crate::{PlatformDispatcher, TaskLabel};
|
||||
use async_task::Runnable;
|
||||
use calloop::{
|
||||
@@ -57,7 +63,7 @@ impl LinuxDispatcher {
|
||||
timer_handle
|
||||
.insert_source(
|
||||
calloop::timer::Timer::from_duration(timer.duration),
|
||||
move |_, _, _| {
|
||||
move |e, _, _| {
|
||||
if let Some(runnable) = runnable.take() {
|
||||
runnable.run();
|
||||
}
|
||||
|
||||
@@ -1,16 +1,27 @@
|
||||
use std::cell::RefCell;
|
||||
use std::ops::Deref;
|
||||
use std::rc::Rc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use calloop::{EventLoop, LoopHandle};
|
||||
use collections::HashMap;
|
||||
|
||||
use util::ResultExt;
|
||||
|
||||
use crate::platform::linux::LinuxClient;
|
||||
use crate::platform::{LinuxCommon, PlatformWindow};
|
||||
use crate::{AnyWindowHandle, CursorStyle, DisplayId, PlatformDisplay, WindowParams};
|
||||
use crate::{
|
||||
px, AnyWindowHandle, Bounds, CursorStyle, DisplayId, Modifiers, ModifiersChangedEvent, Pixels,
|
||||
PlatformDisplay, PlatformInput, Point, ScrollDelta, Size, TouchPhase, WindowParams,
|
||||
};
|
||||
|
||||
use calloop::{
|
||||
generic::{FdWrapper, Generic},
|
||||
RegistrationToken,
|
||||
};
|
||||
|
||||
pub struct HeadlessClientState {
|
||||
pub(crate) _loop_handle: LoopHandle<'static, HeadlessClient>,
|
||||
pub(crate) loop_handle: LoopHandle<'static, HeadlessClient>,
|
||||
pub(crate) event_loop: Option<calloop::EventLoop<'static, HeadlessClient>>,
|
||||
pub(crate) common: LinuxCommon,
|
||||
}
|
||||
@@ -26,17 +37,15 @@ impl HeadlessClient {
|
||||
|
||||
let handle = event_loop.handle();
|
||||
|
||||
handle
|
||||
.insert_source(main_receiver, |event, _, _: &mut HeadlessClient| {
|
||||
if let calloop::channel::Event::Msg(runnable) = event {
|
||||
runnable.run();
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
handle.insert_source(main_receiver, |event, _, _: &mut HeadlessClient| {
|
||||
if let calloop::channel::Event::Msg(runnable) = event {
|
||||
runnable.run();
|
||||
}
|
||||
});
|
||||
|
||||
HeadlessClient(Rc::new(RefCell::new(HeadlessClientState {
|
||||
event_loop: Some(event_loop),
|
||||
_loop_handle: handle,
|
||||
loop_handle: handle,
|
||||
common,
|
||||
})))
|
||||
}
|
||||
@@ -55,7 +64,7 @@ impl LinuxClient for HeadlessClient {
|
||||
None
|
||||
}
|
||||
|
||||
fn display(&self, _id: DisplayId) -> Option<Rc<dyn PlatformDisplay>> {
|
||||
fn display(&self, id: DisplayId) -> Option<Rc<dyn PlatformDisplay>> {
|
||||
None
|
||||
}
|
||||
|
||||
@@ -63,14 +72,10 @@ impl LinuxClient for HeadlessClient {
|
||||
return Err(anyhow::anyhow!("neither DISPLAY, nor WAYLAND_DISPLAY found. You can still run zed for remote development with --dev-server-token."));
|
||||
}
|
||||
|
||||
fn active_window(&self) -> Option<AnyWindowHandle> {
|
||||
None
|
||||
}
|
||||
|
||||
fn open_window(
|
||||
&self,
|
||||
_handle: AnyWindowHandle,
|
||||
_params: WindowParams,
|
||||
params: WindowParams,
|
||||
) -> Box<dyn PlatformWindow> {
|
||||
unimplemented!()
|
||||
}
|
||||
@@ -79,9 +84,9 @@ impl LinuxClient for HeadlessClient {
|
||||
|
||||
fn open_uri(&self, _uri: &str) {}
|
||||
|
||||
fn write_to_primary(&self, _item: crate::ClipboardItem) {}
|
||||
fn write_to_primary(&self, item: crate::ClipboardItem) {}
|
||||
|
||||
fn write_to_clipboard(&self, _item: crate::ClipboardItem) {}
|
||||
fn write_to_clipboard(&self, item: crate::ClipboardItem) {}
|
||||
|
||||
fn read_from_primary(&self) -> Option<crate::ClipboardItem> {
|
||||
None
|
||||
|
||||
@@ -38,9 +38,9 @@ use crate::platform::linux::xdg_desktop_portal::{should_auto_hide_scrollbars, wi
|
||||
use crate::{
|
||||
px, Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CosmicTextSystem, CursorStyle,
|
||||
DisplayId, ForegroundExecutor, Keymap, Keystroke, LinuxDispatcher, Menu, MenuItem, Modifiers,
|
||||
OwnedMenu, PathPromptOptions, Pixels, Platform, PlatformDisplay, PlatformInputHandler,
|
||||
PlatformTextSystem, PlatformWindow, Point, PromptLevel, Result, SemanticVersion, Size, Task,
|
||||
WindowAppearance, WindowOptions, WindowParams,
|
||||
PathPromptOptions, Pixels, Platform, PlatformDisplay, PlatformInputHandler, PlatformTextSystem,
|
||||
PlatformWindow, Point, PromptLevel, Result, SemanticVersion, Size, Task, WindowAppearance,
|
||||
WindowOptions, WindowParams,
|
||||
};
|
||||
|
||||
use super::x11::X11Client;
|
||||
@@ -72,7 +72,6 @@ pub trait LinuxClient {
|
||||
fn write_to_clipboard(&self, item: ClipboardItem);
|
||||
fn read_from_primary(&self) -> Option<ClipboardItem>;
|
||||
fn read_from_clipboard(&self) -> Option<ClipboardItem>;
|
||||
fn active_window(&self) -> Option<AnyWindowHandle>;
|
||||
fn run(&self);
|
||||
}
|
||||
|
||||
@@ -94,7 +93,6 @@ pub(crate) struct LinuxCommon {
|
||||
pub(crate) auto_hide_scrollbars: bool,
|
||||
pub(crate) callbacks: PlatformHandlers,
|
||||
pub(crate) signal: LoopSignal,
|
||||
pub(crate) menus: Vec<OwnedMenu>,
|
||||
}
|
||||
|
||||
impl LinuxCommon {
|
||||
@@ -120,7 +118,6 @@ impl LinuxCommon {
|
||||
auto_hide_scrollbars,
|
||||
callbacks,
|
||||
signal,
|
||||
menus: Vec::new(),
|
||||
};
|
||||
|
||||
(common, main_receiver)
|
||||
@@ -213,21 +210,18 @@ impl<P: LinuxClient + 'static> Platform for P {
|
||||
}
|
||||
}
|
||||
|
||||
fn activate(&self, ignoring_other_apps: bool) {
|
||||
log::info!("activate is not implemented on Linux, ignoring the call")
|
||||
}
|
||||
// todo(linux)
|
||||
fn activate(&self, ignoring_other_apps: bool) {}
|
||||
|
||||
fn hide(&self) {
|
||||
log::info!("hide is not implemented on Linux, ignoring the call")
|
||||
}
|
||||
// todo(linux)
|
||||
fn hide(&self) {}
|
||||
|
||||
fn hide_other_apps(&self) {
|
||||
log::info!("hide_other_apps is not implemented on Linux, ignoring the call")
|
||||
log::warn!("hide_other_apps is not implemented on Linux, ignoring the call")
|
||||
}
|
||||
|
||||
fn unhide_other_apps(&self) {
|
||||
log::info!("unhide_other_apps is not implemented on Linux, ignoring the call")
|
||||
}
|
||||
// todo(linux)
|
||||
fn unhide_other_apps(&self) {}
|
||||
|
||||
fn primary_display(&self) -> Option<Rc<dyn PlatformDisplay>> {
|
||||
self.primary_display()
|
||||
@@ -237,8 +231,9 @@ impl<P: LinuxClient + 'static> Platform for P {
|
||||
self.displays()
|
||||
}
|
||||
|
||||
// todo(linux)
|
||||
fn active_window(&self) -> Option<AnyWindowHandle> {
|
||||
self.active_window()
|
||||
None
|
||||
}
|
||||
|
||||
fn open_window(
|
||||
@@ -392,22 +387,15 @@ impl<P: LinuxClient + 'static> Platform for P {
|
||||
Ok(exe_path)
|
||||
}
|
||||
|
||||
fn set_menus(&self, menus: Vec<Menu>, _keymap: &Keymap) {
|
||||
self.with_common(|common| {
|
||||
common.menus = menus.into_iter().map(|menu| menu.owned()).collect();
|
||||
})
|
||||
}
|
||||
|
||||
fn get_menus(&self) -> Option<Vec<OwnedMenu>> {
|
||||
self.with_common(|common| Some(common.menus.clone()))
|
||||
}
|
||||
|
||||
// todo(linux)
|
||||
fn set_menus(&self, menus: Vec<Menu>, keymap: &Keymap) {}
|
||||
fn set_dock_menu(&self, menu: Vec<MenuItem>, keymap: &Keymap) {}
|
||||
|
||||
fn local_timezone(&self) -> UtcOffset {
|
||||
UtcOffset::UTC
|
||||
}
|
||||
|
||||
//todo(linux)
|
||||
fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf> {
|
||||
Err(anyhow::Error::msg(
|
||||
"Platform<LinuxPlatform>::path_for_auxiliary_executable is not implemented yet",
|
||||
@@ -561,6 +549,7 @@ impl CursorStyle {
|
||||
CursorStyle::ResizeUpDown => Shape::NsResize,
|
||||
CursorStyle::ResizeColumn => Shape::ColResize,
|
||||
CursorStyle::ResizeRow => Shape::RowResize,
|
||||
CursorStyle::DisappearingItem => Shape::Grabbing, // todo(linux) - couldn't find equivalent icon in linux
|
||||
CursorStyle::IBeamCursorForVerticalLayout => Shape::VerticalText,
|
||||
CursorStyle::OperationNotAllowed => Shape::NotAllowed,
|
||||
CursorStyle::DragLink => Shape::Alias,
|
||||
@@ -588,6 +577,7 @@ impl CursorStyle {
|
||||
CursorStyle::ResizeUpDown => "ns-resize",
|
||||
CursorStyle::ResizeColumn => "col-resize",
|
||||
CursorStyle::ResizeRow => "row-resize",
|
||||
CursorStyle::DisappearingItem => "grabbing", // todo(linux) - couldn't find equivalent icon in linux
|
||||
CursorStyle::IBeamCursorForVerticalLayout => "vertical-text",
|
||||
CursorStyle::OperationNotAllowed => "not-allowed",
|
||||
CursorStyle::DragLink => "alias",
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
use core::hash;
|
||||
use std::cell::{RefCell, RefMut};
|
||||
use std::ffi::OsString;
|
||||
use std::hash::Hash;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::os::fd::{AsRawFd, BorrowedFd};
|
||||
use std::path::PathBuf;
|
||||
use std::rc::{Rc, Weak};
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use async_task::Runnable;
|
||||
use calloop::timer::{TimeoutAction, Timer};
|
||||
use calloop::{EventLoop, LoopHandle};
|
||||
use calloop_wayland_source::WaylandSource;
|
||||
@@ -13,7 +16,7 @@ use collections::HashMap;
|
||||
use copypasta::wayland_clipboard::{create_clipboards_from_external, Clipboard, Primary};
|
||||
use copypasta::ClipboardProvider;
|
||||
use filedescriptor::Pipe;
|
||||
|
||||
use parking_lot::Mutex;
|
||||
use smallvec::SmallVec;
|
||||
use util::ResultExt;
|
||||
use wayland_backend::client::ObjectId;
|
||||
@@ -23,8 +26,9 @@ use wayland_client::globals::{registry_queue_init, GlobalList, GlobalListContent
|
||||
use wayland_client::protocol::wl_callback::{self, WlCallback};
|
||||
use wayland_client::protocol::wl_data_device_manager::DndAction;
|
||||
use wayland_client::protocol::wl_pointer::AxisSource;
|
||||
use wayland_client::protocol::wl_seat::WlSeat;
|
||||
use wayland_client::protocol::{
|
||||
wl_data_device, wl_data_device_manager, wl_data_offer, wl_output, wl_region,
|
||||
wl_data_device, wl_data_device_manager, wl_data_offer, wl_data_source, wl_output, wl_region,
|
||||
};
|
||||
use wayland_client::{
|
||||
delegate_noop,
|
||||
@@ -34,6 +38,7 @@ use wayland_client::{
|
||||
},
|
||||
Connection, Dispatch, Proxy, QueueHandle,
|
||||
};
|
||||
use wayland_protocols::wp::cursor_shape::v1::client::wp_cursor_shape_device_v1::Shape;
|
||||
use wayland_protocols::wp::cursor_shape::v1::client::{
|
||||
wp_cursor_shape_device_v1, wp_cursor_shape_manager_v1,
|
||||
};
|
||||
@@ -57,8 +62,7 @@ use xkbcommon::xkb::ffi::XKB_KEYMAP_FORMAT_TEXT_V1;
|
||||
use xkbcommon::xkb::{self, Keycode, KEYMAP_COMPILE_NO_FLAGS};
|
||||
|
||||
use super::super::{open_uri_internal, read_fd, DOUBLE_CLICK_INTERVAL};
|
||||
use super::display::WaylandDisplay;
|
||||
use super::window::{ImeInput, WaylandWindowStatePtr};
|
||||
use super::window::{ImeInput, WaylandWindowState, WaylandWindowStatePtr};
|
||||
use crate::platform::linux::is_within_click_distance;
|
||||
use crate::platform::linux::wayland::cursor::Cursor;
|
||||
use crate::platform::linux::wayland::serial::{SerialKind, SerialTracker};
|
||||
@@ -67,7 +71,7 @@ use crate::platform::linux::xdg_desktop_portal::{Event as XDPEvent, XDPEventSour
|
||||
use crate::platform::linux::LinuxClient;
|
||||
use crate::platform::PlatformWindow;
|
||||
use crate::{
|
||||
point, px, size, Bounds, DevicePixels, FileDropEvent, ForegroundExecutor, MouseExitEvent, Size,
|
||||
point, px, Bounds, FileDropEvent, ForegroundExecutor, MouseExitEvent, WindowAppearance,
|
||||
SCROLL_LINES,
|
||||
};
|
||||
use crate::{
|
||||
@@ -139,42 +143,11 @@ impl Globals {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct InProgressOutput {
|
||||
name: Option<String>,
|
||||
scale: Option<i32>,
|
||||
position: Option<Point<DevicePixels>>,
|
||||
size: Option<Size<DevicePixels>>,
|
||||
}
|
||||
|
||||
impl InProgressOutput {
|
||||
fn complete(&self) -> Option<Output> {
|
||||
if let Some((position, size)) = self.position.zip(self.size) {
|
||||
let scale = self.scale.unwrap_or(1);
|
||||
Some(Output {
|
||||
name: self.name.clone(),
|
||||
scale,
|
||||
bounds: Bounds::new(position, size),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct Output {
|
||||
pub name: Option<String>,
|
||||
pub scale: i32,
|
||||
pub bounds: Bounds<DevicePixels>,
|
||||
}
|
||||
|
||||
pub(crate) struct WaylandClientState {
|
||||
serial_tracker: SerialTracker,
|
||||
globals: Globals,
|
||||
wl_seat: wl_seat::WlSeat, // TODO: Multi seat support
|
||||
wl_seat: wl_seat::WlSeat, // todo(linux): multi-seat support
|
||||
wl_pointer: Option<wl_pointer::WlPointer>,
|
||||
wl_keyboard: Option<wl_keyboard::WlKeyboard>,
|
||||
cursor_shape_device: Option<wp_cursor_shape_device_v1::WpCursorShapeDeviceV1>,
|
||||
data_device: Option<wl_data_device::WlDataDevice>,
|
||||
text_input: Option<zwp_text_input_v3::ZwpTextInputV3>,
|
||||
@@ -183,16 +156,15 @@ pub(crate) struct WaylandClientState {
|
||||
// Surface to Window mapping
|
||||
windows: HashMap<ObjectId, WaylandWindowStatePtr>,
|
||||
// Output to scale mapping
|
||||
outputs: HashMap<ObjectId, Output>,
|
||||
in_progress_outputs: HashMap<ObjectId, InProgressOutput>,
|
||||
output_scales: HashMap<ObjectId, i32>,
|
||||
keymap_state: Option<xkb::State>,
|
||||
compose_state: Option<xkb::compose::State>,
|
||||
drag: DragState,
|
||||
click: ClickState,
|
||||
repeat: KeyRepeat,
|
||||
pub modifiers: Modifiers,
|
||||
modifiers: Modifiers,
|
||||
axis_source: AxisSource,
|
||||
pub mouse_location: Option<Point<Pixels>>,
|
||||
mouse_location: Option<Point<Pixels>>,
|
||||
continuous_scroll_delta: Option<Point<Pixels>>,
|
||||
discrete_scroll_delta: Option<Point<f32>>,
|
||||
vertical_modifier: f32,
|
||||
@@ -238,7 +210,7 @@ pub(crate) struct KeyRepeat {
|
||||
pub struct WaylandClientStatePtr(Weak<RefCell<WaylandClientState>>);
|
||||
|
||||
impl WaylandClientStatePtr {
|
||||
pub fn get_client(&self) -> Rc<RefCell<WaylandClientState>> {
|
||||
fn get_client(&self) -> Rc<RefCell<WaylandClientState>> {
|
||||
self.0
|
||||
.upgrade()
|
||||
.expect("The pointer should always be valid when dispatching in wayland")
|
||||
@@ -330,6 +302,7 @@ impl Drop for WaylandClient {
|
||||
}
|
||||
|
||||
const WL_DATA_DEVICE_MANAGER_VERSION: u32 = 3;
|
||||
const WL_OUTPUT_VERSION: u32 = 2;
|
||||
|
||||
fn wl_seat_version(version: u32) -> u32 {
|
||||
// We rely on the wl_pointer.frame event
|
||||
@@ -346,20 +319,6 @@ fn wl_seat_version(version: u32) -> u32 {
|
||||
version.clamp(WL_SEAT_MIN_VERSION, WL_SEAT_MAX_VERSION)
|
||||
}
|
||||
|
||||
fn wl_output_version(version: u32) -> u32 {
|
||||
const WL_OUTPUT_MIN_VERSION: u32 = 2;
|
||||
const WL_OUTPUT_MAX_VERSION: u32 = 4;
|
||||
|
||||
if version < WL_OUTPUT_MIN_VERSION {
|
||||
panic!(
|
||||
"wl_output below required version: {} < {}",
|
||||
version, WL_OUTPUT_MIN_VERSION
|
||||
);
|
||||
}
|
||||
|
||||
version.clamp(WL_OUTPUT_MIN_VERSION, WL_OUTPUT_MAX_VERSION)
|
||||
}
|
||||
|
||||
impl WaylandClient {
|
||||
pub(crate) fn new() -> Self {
|
||||
let conn = Connection::connect_to_env().unwrap();
|
||||
@@ -369,7 +328,7 @@ impl WaylandClient {
|
||||
let qh = event_queue.handle();
|
||||
|
||||
let mut seat: Option<wl_seat::WlSeat> = None;
|
||||
let mut in_progress_outputs = HashMap::default();
|
||||
let mut outputs = HashMap::default();
|
||||
globals.contents().with_list(|list| {
|
||||
for global in list {
|
||||
match &global.interface[..] {
|
||||
@@ -384,11 +343,11 @@ impl WaylandClient {
|
||||
"wl_output" => {
|
||||
let output = globals.registry().bind::<wl_output::WlOutput, _, _>(
|
||||
global.name,
|
||||
wl_output_version(global.version),
|
||||
WL_OUTPUT_VERSION,
|
||||
&qh,
|
||||
(),
|
||||
);
|
||||
in_progress_outputs.insert(output.id(), InProgressOutput::default());
|
||||
outputs.insert(output.id(), 1);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -402,13 +361,11 @@ impl WaylandClient {
|
||||
let (common, main_receiver) = LinuxCommon::new(event_loop.get_signal());
|
||||
|
||||
let handle = event_loop.handle();
|
||||
handle
|
||||
.insert_source(main_receiver, |event, _, _: &mut WaylandClientStatePtr| {
|
||||
if let calloop::channel::Event::Msg(runnable) = event {
|
||||
runnable.run();
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
handle.insert_source(main_receiver, |event, _, _: &mut WaylandClientStatePtr| {
|
||||
if let calloop::channel::Event::Msg(runnable) = event {
|
||||
runnable.run();
|
||||
}
|
||||
});
|
||||
|
||||
let seat = seat.unwrap();
|
||||
let globals = Globals::new(
|
||||
@@ -427,37 +384,33 @@ impl WaylandClient {
|
||||
|
||||
let cursor = Cursor::new(&conn, &globals, 24);
|
||||
|
||||
handle
|
||||
.insert_source(XDPEventSource::new(&common.background_executor), {
|
||||
move |event, _, client| match event {
|
||||
XDPEvent::WindowAppearance(appearance) => {
|
||||
if let Some(client) = client.0.upgrade() {
|
||||
let mut client = client.borrow_mut();
|
||||
handle.insert_source(XDPEventSource::new(&common.background_executor), {
|
||||
move |event, _, client| match event {
|
||||
XDPEvent::WindowAppearance(appearance) => {
|
||||
if let Some(client) = client.0.upgrade() {
|
||||
let mut client = client.borrow_mut();
|
||||
|
||||
client.common.appearance = appearance;
|
||||
client.common.appearance = appearance;
|
||||
|
||||
for (_, window) in &mut client.windows {
|
||||
window.set_appearance(appearance);
|
||||
}
|
||||
for (_, window) in &mut client.windows {
|
||||
window.set_appearance(appearance);
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
});
|
||||
|
||||
let mut state = Rc::new(RefCell::new(WaylandClientState {
|
||||
serial_tracker: SerialTracker::new(),
|
||||
globals,
|
||||
wl_seat: seat,
|
||||
wl_pointer: None,
|
||||
wl_keyboard: None,
|
||||
cursor_shape_device: None,
|
||||
data_device,
|
||||
text_input: None,
|
||||
pre_edit_text: None,
|
||||
composing: false,
|
||||
outputs: HashMap::default(),
|
||||
in_progress_outputs,
|
||||
output_scales: outputs,
|
||||
windows: HashMap::default(),
|
||||
common,
|
||||
keymap_state: None,
|
||||
@@ -506,9 +459,7 @@ impl WaylandClient {
|
||||
pending_open_uri: None,
|
||||
}));
|
||||
|
||||
WaylandSource::new(conn, event_queue)
|
||||
.insert(handle)
|
||||
.unwrap();
|
||||
WaylandSource::new(conn, event_queue).insert(handle);
|
||||
|
||||
Self(state)
|
||||
}
|
||||
@@ -516,34 +467,11 @@ impl WaylandClient {
|
||||
|
||||
impl LinuxClient for WaylandClient {
|
||||
fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>> {
|
||||
self.0
|
||||
.borrow()
|
||||
.outputs
|
||||
.iter()
|
||||
.map(|(id, output)| {
|
||||
Rc::new(WaylandDisplay {
|
||||
id: id.clone(),
|
||||
name: output.name.clone(),
|
||||
bounds: output.bounds,
|
||||
}) as Rc<dyn PlatformDisplay>
|
||||
})
|
||||
.collect()
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
fn display(&self, id: DisplayId) -> Option<Rc<dyn PlatformDisplay>> {
|
||||
self.0
|
||||
.borrow()
|
||||
.outputs
|
||||
.iter()
|
||||
.find_map(|(object_id, output)| {
|
||||
(object_id.protocol_id() == id.0).then(|| {
|
||||
Rc::new(WaylandDisplay {
|
||||
id: object_id.clone(),
|
||||
name: output.name.clone(),
|
||||
bounds: output.bounds,
|
||||
}) as Rc<dyn PlatformDisplay>
|
||||
})
|
||||
})
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn primary_display(&self) -> Option<Rc<dyn PlatformDisplay>> {
|
||||
@@ -558,7 +486,6 @@ impl LinuxClient for WaylandClient {
|
||||
let mut state = self.0.borrow_mut();
|
||||
|
||||
let (window, surface_id) = WaylandWindow::new(
|
||||
handle,
|
||||
state.globals.clone(),
|
||||
WaylandClientStatePtr(Rc::downgrade(&self.0)),
|
||||
params,
|
||||
@@ -639,8 +566,7 @@ impl LinuxClient for WaylandClient {
|
||||
.primary
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.set_contents(item.text)
|
||||
.ok();
|
||||
.set_contents(item.text);
|
||||
}
|
||||
|
||||
fn write_to_clipboard(&self, item: crate::ClipboardItem) {
|
||||
@@ -649,8 +575,7 @@ impl LinuxClient for WaylandClient {
|
||||
.clipboard
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.set_contents(item.text)
|
||||
.ok();
|
||||
.set_contents(item.text);
|
||||
}
|
||||
|
||||
fn read_from_primary(&self) -> Option<crate::ClipboardItem> {
|
||||
@@ -680,14 +605,6 @@ impl LinuxClient for WaylandClient {
|
||||
metadata: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn active_window(&self) -> Option<AnyWindowHandle> {
|
||||
self.0
|
||||
.borrow_mut()
|
||||
.keyboard_focused_window
|
||||
.as_ref()
|
||||
.map(|window| window.handle())
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<wl_registry::WlRegistry, GlobalListContents> for WaylandClientStatePtr {
|
||||
@@ -709,37 +626,18 @@ impl Dispatch<wl_registry::WlRegistry, GlobalListContents> for WaylandClientStat
|
||||
version,
|
||||
} => match &interface[..] {
|
||||
"wl_seat" => {
|
||||
if let Some(wl_pointer) = state.wl_pointer.take() {
|
||||
wl_pointer.release();
|
||||
}
|
||||
if let Some(wl_keyboard) = state.wl_keyboard.take() {
|
||||
wl_keyboard.release();
|
||||
}
|
||||
state.wl_seat.release();
|
||||
state.wl_seat = registry.bind::<wl_seat::WlSeat, _, _>(
|
||||
name,
|
||||
wl_seat_version(version),
|
||||
qh,
|
||||
(),
|
||||
);
|
||||
state.wl_pointer = None;
|
||||
registry.bind::<wl_seat::WlSeat, _, _>(name, wl_seat_version(version), qh, ());
|
||||
}
|
||||
"wl_output" => {
|
||||
let output = registry.bind::<wl_output::WlOutput, _, _>(
|
||||
name,
|
||||
wl_output_version(version),
|
||||
qh,
|
||||
(),
|
||||
);
|
||||
let output =
|
||||
registry.bind::<wl_output::WlOutput, _, _>(name, WL_OUTPUT_VERSION, qh, ());
|
||||
|
||||
state
|
||||
.in_progress_outputs
|
||||
.insert(output.id(), InProgressOutput::default());
|
||||
state.output_scales.insert(output.id(), 1);
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
wl_registry::Event::GlobalRemove { name: _ } => {
|
||||
// TODO: handle global removal
|
||||
}
|
||||
wl_registry::Event::GlobalRemove { name: _ } => {}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -769,7 +667,7 @@ impl Dispatch<WlCallback, ObjectId> for WaylandClientStatePtr {
|
||||
event: wl_callback::Event,
|
||||
surface_id: &ObjectId,
|
||||
_: &Connection,
|
||||
_: &QueueHandle<Self>,
|
||||
qh: &QueueHandle<Self>,
|
||||
) {
|
||||
let client = state.get_client();
|
||||
let mut state = client.borrow_mut();
|
||||
@@ -779,7 +677,7 @@ impl Dispatch<WlCallback, ObjectId> for WaylandClientStatePtr {
|
||||
drop(state);
|
||||
|
||||
match event {
|
||||
wl_callback::Event::Done { .. } => {
|
||||
wl_callback::Event::Done { callback_data } => {
|
||||
window.frame(true);
|
||||
}
|
||||
_ => {}
|
||||
@@ -809,10 +707,10 @@ impl Dispatch<wl_surface::WlSurface, ()> for WaylandClientStatePtr {
|
||||
let Some(window) = get_window(&mut state, &surface.id()) else {
|
||||
return;
|
||||
};
|
||||
let outputs = state.outputs.clone();
|
||||
let scales = state.output_scales.clone();
|
||||
drop(state);
|
||||
|
||||
window.handle_surface_event(event, outputs);
|
||||
window.handle_surface_event(event, scales);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -828,28 +726,13 @@ impl Dispatch<wl_output::WlOutput, ()> for WaylandClientStatePtr {
|
||||
let mut client = this.get_client();
|
||||
let mut state = client.borrow_mut();
|
||||
|
||||
let Some(mut in_progress_output) = state.in_progress_outputs.get_mut(&output.id()) else {
|
||||
let Some(mut output_scale) = state.output_scales.get_mut(&output.id()) else {
|
||||
return;
|
||||
};
|
||||
|
||||
match event {
|
||||
wl_output::Event::Name { name } => {
|
||||
in_progress_output.name = Some(name);
|
||||
}
|
||||
wl_output::Event::Scale { factor } => {
|
||||
in_progress_output.scale = Some(factor);
|
||||
}
|
||||
wl_output::Event::Geometry { x, y, .. } => {
|
||||
in_progress_output.position = Some(point(DevicePixels(x), DevicePixels(y)))
|
||||
}
|
||||
wl_output::Event::Mode { width, height, .. } => {
|
||||
in_progress_output.size = Some(size(DevicePixels(width), DevicePixels(height)))
|
||||
}
|
||||
wl_output::Event::Done => {
|
||||
if let Some(complete) = in_progress_output.complete() {
|
||||
state.outputs.insert(output.id(), complete);
|
||||
}
|
||||
state.in_progress_outputs.remove(&output.id());
|
||||
*output_scale = factor;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -859,7 +742,7 @@ impl Dispatch<wl_output::WlOutput, ()> for WaylandClientStatePtr {
|
||||
impl Dispatch<xdg_surface::XdgSurface, ObjectId> for WaylandClientStatePtr {
|
||||
fn event(
|
||||
state: &mut Self,
|
||||
_: &xdg_surface::XdgSurface,
|
||||
xdg_surface: &xdg_surface::XdgSurface,
|
||||
event: xdg_surface::Event,
|
||||
surface_id: &ObjectId,
|
||||
_: &Connection,
|
||||
@@ -878,7 +761,7 @@ impl Dispatch<xdg_surface::XdgSurface, ObjectId> for WaylandClientStatePtr {
|
||||
impl Dispatch<xdg_toplevel::XdgToplevel, ObjectId> for WaylandClientStatePtr {
|
||||
fn event(
|
||||
this: &mut Self,
|
||||
_: &xdg_toplevel::XdgToplevel,
|
||||
xdg_toplevel: &xdg_toplevel::XdgToplevel,
|
||||
event: <xdg_toplevel::XdgToplevel as Proxy>::Event,
|
||||
surface_id: &ObjectId,
|
||||
_: &Connection,
|
||||
@@ -941,8 +824,8 @@ impl Dispatch<wl_seat::WlSeat, ()> for WaylandClientStatePtr {
|
||||
state: &mut Self,
|
||||
seat: &wl_seat::WlSeat,
|
||||
event: wl_seat::Event,
|
||||
_: &(),
|
||||
_: &Connection,
|
||||
data: &(),
|
||||
conn: &Connection,
|
||||
qh: &QueueHandle<Self>,
|
||||
) {
|
||||
if let wl_seat::Event::Capabilities {
|
||||
@@ -952,19 +835,12 @@ impl Dispatch<wl_seat::WlSeat, ()> for WaylandClientStatePtr {
|
||||
let client = state.get_client();
|
||||
let mut state = client.borrow_mut();
|
||||
if capabilities.contains(wl_seat::Capability::Keyboard) {
|
||||
let keyboard = seat.get_keyboard(qh, ());
|
||||
|
||||
seat.get_keyboard(qh, ());
|
||||
state.text_input = state
|
||||
.globals
|
||||
.text_input_manager
|
||||
.as_ref()
|
||||
.map(|text_input_manager| text_input_manager.get_text_input(&seat, qh, ()));
|
||||
|
||||
if let Some(wl_keyboard) = &state.wl_keyboard {
|
||||
wl_keyboard.release();
|
||||
}
|
||||
|
||||
state.wl_keyboard = Some(keyboard);
|
||||
}
|
||||
if capabilities.contains(wl_seat::Capability::Pointer) {
|
||||
let pointer = seat.get_pointer(qh, ());
|
||||
@@ -973,11 +849,6 @@ impl Dispatch<wl_seat::WlSeat, ()> for WaylandClientStatePtr {
|
||||
.cursor_shape_manager
|
||||
.as_ref()
|
||||
.map(|cursor_shape_manager| cursor_shape_manager.get_pointer(&pointer, qh, ()));
|
||||
|
||||
if let Some(wl_pointer) = &state.wl_pointer {
|
||||
wl_pointer.release();
|
||||
}
|
||||
|
||||
state.wl_pointer = Some(pointer);
|
||||
}
|
||||
}
|
||||
@@ -987,11 +858,11 @@ impl Dispatch<wl_seat::WlSeat, ()> for WaylandClientStatePtr {
|
||||
impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr {
|
||||
fn event(
|
||||
this: &mut Self,
|
||||
_: &wl_keyboard::WlKeyboard,
|
||||
keyboard: &wl_keyboard::WlKeyboard,
|
||||
event: wl_keyboard::Event,
|
||||
_: &(),
|
||||
_: &Connection,
|
||||
_: &QueueHandle<Self>,
|
||||
data: &(),
|
||||
conn: &Connection,
|
||||
qh: &QueueHandle<Self>,
|
||||
) {
|
||||
let mut client = this.get_client();
|
||||
let mut state = client.borrow_mut();
|
||||
@@ -1147,8 +1018,8 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr {
|
||||
state.compose_state = Some(compose);
|
||||
}
|
||||
let input = PlatformInput::KeyDown(KeyDownEvent {
|
||||
keystroke: keystroke.clone(),
|
||||
is_held: false,
|
||||
keystroke: keystroke,
|
||||
is_held: false, // todo(linux)
|
||||
});
|
||||
|
||||
state.repeat.current_id += 1;
|
||||
@@ -1159,11 +1030,8 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr {
|
||||
state
|
||||
.loop_handle
|
||||
.insert_source(Timer::from_duration(state.repeat.delay), {
|
||||
let input = PlatformInput::KeyDown(KeyDownEvent {
|
||||
keystroke,
|
||||
is_held: true,
|
||||
});
|
||||
move |_event, _metadata, this| {
|
||||
let input = input.clone();
|
||||
move |event, _metadata, this| {
|
||||
let mut client = this.get_client();
|
||||
let mut state = client.borrow_mut();
|
||||
let is_repeating = id == state.repeat.current_id
|
||||
@@ -1212,18 +1080,18 @@ impl Dispatch<zwp_text_input_v3::ZwpTextInputV3, ()> for WaylandClientStatePtr {
|
||||
this: &mut Self,
|
||||
text_input: &zwp_text_input_v3::ZwpTextInputV3,
|
||||
event: <zwp_text_input_v3::ZwpTextInputV3 as Proxy>::Event,
|
||||
_: &(),
|
||||
_: &Connection,
|
||||
_: &QueueHandle<Self>,
|
||||
data: &(),
|
||||
conn: &Connection,
|
||||
qhandle: &QueueHandle<Self>,
|
||||
) {
|
||||
let client = this.get_client();
|
||||
let mut state = client.borrow_mut();
|
||||
match event {
|
||||
zwp_text_input_v3::Event::Enter { .. } => {
|
||||
zwp_text_input_v3::Event::Enter { surface } => {
|
||||
drop(state);
|
||||
this.enable_ime();
|
||||
}
|
||||
zwp_text_input_v3::Event::Leave { .. } => {
|
||||
zwp_text_input_v3::Event::Leave { surface } => {
|
||||
drop(state);
|
||||
this.disable_ime();
|
||||
}
|
||||
@@ -1251,7 +1119,11 @@ impl Dispatch<zwp_text_input_v3::ZwpTextInputV3, ()> for WaylandClientStatePtr {
|
||||
}
|
||||
}
|
||||
}
|
||||
zwp_text_input_v3::Event::PreeditString { text, .. } => {
|
||||
zwp_text_input_v3::Event::PreeditString {
|
||||
text,
|
||||
cursor_begin,
|
||||
cursor_end,
|
||||
} => {
|
||||
state.composing = true;
|
||||
state.pre_edit_text = text;
|
||||
}
|
||||
@@ -1311,9 +1183,9 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr {
|
||||
this: &mut Self,
|
||||
wl_pointer: &wl_pointer::WlPointer,
|
||||
event: wl_pointer::Event,
|
||||
_: &(),
|
||||
_: &Connection,
|
||||
_: &QueueHandle<Self>,
|
||||
data: &(),
|
||||
conn: &Connection,
|
||||
qh: &QueueHandle<Self>,
|
||||
) {
|
||||
let mut client = this.get_client();
|
||||
let mut state = client.borrow_mut();
|
||||
@@ -1348,7 +1220,7 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr {
|
||||
window.set_focused(true);
|
||||
}
|
||||
}
|
||||
wl_pointer::Event::Leave { .. } => {
|
||||
wl_pointer::Event::Leave { surface, .. } => {
|
||||
if let Some(focused_window) = state.mouse_focused_window.clone() {
|
||||
let input = PlatformInput::MouseExited(MouseExitEvent {
|
||||
position: state.mouse_location.unwrap(),
|
||||
@@ -1365,6 +1237,7 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr {
|
||||
}
|
||||
}
|
||||
wl_pointer::Event::Motion {
|
||||
time,
|
||||
surface_x,
|
||||
surface_y,
|
||||
..
|
||||
@@ -1407,6 +1280,7 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr {
|
||||
wl_pointer::ButtonState::Pressed => {
|
||||
if let Some(window) = state.keyboard_focused_window.clone() {
|
||||
if state.composing && state.text_input.is_some() {
|
||||
let text_input = state.text_input.as_ref().unwrap();
|
||||
drop(state);
|
||||
// text_input_v3 don't have something like a reset function
|
||||
this.disable_ime();
|
||||
@@ -1477,6 +1351,7 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr {
|
||||
state.axis_source = axis_source;
|
||||
}
|
||||
wl_pointer::Event::Axis {
|
||||
time,
|
||||
axis: WEnum::Value(axis),
|
||||
value,
|
||||
..
|
||||
@@ -1489,10 +1364,13 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr {
|
||||
wl_pointer::Axis::HorizontalScroll => state.horizontal_modifier,
|
||||
_ => 1.0,
|
||||
};
|
||||
let supports_relative_direction =
|
||||
wl_pointer.version() >= wl_pointer::EVT_AXIS_RELATIVE_DIRECTION_SINCE;
|
||||
state.scroll_event_received = true;
|
||||
let scroll_delta = state
|
||||
.continuous_scroll_delta
|
||||
.get_or_insert(point(px(0.0), px(0.0)));
|
||||
// TODO: Make nice feeling kinetic scrolling that integrates with the platform's scroll settings
|
||||
let modifier = 3.0;
|
||||
match axis {
|
||||
wl_pointer::Axis::VerticalScroll => {
|
||||
|
||||
@@ -1,41 +1,31 @@
|
||||
use std::{
|
||||
fmt::Debug,
|
||||
hash::{Hash, Hasher},
|
||||
};
|
||||
use std::fmt::Debug;
|
||||
|
||||
use uuid::Uuid;
|
||||
use wayland_backend::client::ObjectId;
|
||||
|
||||
use crate::{Bounds, DevicePixels, DisplayId, PlatformDisplay};
|
||||
use crate::{Bounds, DevicePixels, DisplayId, PlatformDisplay, Size};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct WaylandDisplay {
|
||||
/// The ID of the wl_output object
|
||||
pub id: ObjectId,
|
||||
pub name: Option<String>,
|
||||
pub bounds: Bounds<DevicePixels>,
|
||||
}
|
||||
|
||||
impl Hash for WaylandDisplay {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.id.hash(state);
|
||||
}
|
||||
}
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct WaylandDisplay {}
|
||||
|
||||
impl PlatformDisplay for WaylandDisplay {
|
||||
// todo(linux)
|
||||
fn id(&self) -> DisplayId {
|
||||
DisplayId(self.id.protocol_id())
|
||||
DisplayId(123) // return some fake data so it doesn't panic
|
||||
}
|
||||
|
||||
// todo(linux)
|
||||
fn uuid(&self) -> anyhow::Result<Uuid> {
|
||||
if let Some(name) = &self.name {
|
||||
Ok(Uuid::new_v5(&Uuid::NAMESPACE_DNS, name.as_bytes()))
|
||||
} else {
|
||||
Err(anyhow::anyhow!("Wayland display does not have a name"))
|
||||
}
|
||||
Ok(Uuid::from_bytes([0; 16])) // return some fake data so it doesn't panic
|
||||
}
|
||||
|
||||
// todo(linux)
|
||||
fn bounds(&self) -> Bounds<DevicePixels> {
|
||||
self.bounds
|
||||
Bounds {
|
||||
origin: Default::default(),
|
||||
size: Size {
|
||||
width: DevicePixels(1000),
|
||||
height: DevicePixels(500),
|
||||
},
|
||||
} // return some fake data so it doesn't panic
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use std::time::Instant;
|
||||
|
||||
use collections::HashMap;
|
||||
|
||||
#[derive(Debug, Hash, PartialEq, Eq)]
|
||||
@@ -12,11 +14,15 @@ pub(crate) enum SerialKind {
|
||||
#[derive(Debug)]
|
||||
struct SerialData {
|
||||
serial: u32,
|
||||
time: Instant,
|
||||
}
|
||||
|
||||
impl SerialData {
|
||||
fn new(value: u32) -> Self {
|
||||
Self { serial: value }
|
||||
Self {
|
||||
serial: value,
|
||||
time: Instant::now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,4 +52,41 @@ impl SerialTracker {
|
||||
.map(|serial_data| serial_data.serial)
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
/// Returns the newest serial of any of the provided [`SerialKind`]
|
||||
pub fn get_newest_of(&self, kinds: &[SerialKind]) -> u32 {
|
||||
kinds
|
||||
.iter()
|
||||
.filter_map(|kind| self.serials.get(&kind))
|
||||
.max_by_key(|serial_data| serial_data.time)
|
||||
.map(|serial_data| serial_data.serial)
|
||||
.unwrap_or(0)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_serial_tracker() {
|
||||
let mut tracker = SerialTracker::new();
|
||||
|
||||
tracker.update(SerialKind::KeyPress, 100);
|
||||
tracker.update(SerialKind::MousePress, 50);
|
||||
tracker.update(SerialKind::MouseEnter, 300);
|
||||
|
||||
assert_eq!(
|
||||
tracker.get_newest_of(&[SerialKind::KeyPress, SerialKind::MousePress]),
|
||||
50
|
||||
);
|
||||
assert_eq!(tracker.get(SerialKind::DataDevice), 0);
|
||||
|
||||
tracker.update(SerialKind::KeyPress, 2000);
|
||||
assert_eq!(tracker.get(SerialKind::KeyPress), 2000);
|
||||
assert_eq!(
|
||||
tracker.get_newest_of(&[SerialKind::KeyPress, SerialKind::MousePress]),
|
||||
2000
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +1,27 @@
|
||||
use std::any::Any;
|
||||
use std::cell::{Ref, RefCell, RefMut};
|
||||
use std::ffi::c_void;
|
||||
use std::num::NonZeroU32;
|
||||
use std::ops::{Deref, Range};
|
||||
use std::ptr::NonNull;
|
||||
use std::rc::Rc;
|
||||
use std::rc::{Rc, Weak};
|
||||
use std::sync::Arc;
|
||||
|
||||
use blade_graphics as gpu;
|
||||
use collections::HashMap;
|
||||
use collections::{HashMap, HashSet};
|
||||
use futures::channel::oneshot::Receiver;
|
||||
|
||||
use parking_lot::Mutex;
|
||||
use raw_window_handle as rwh;
|
||||
use wayland_backend::client::ObjectId;
|
||||
use wayland_client::protocol::wl_region::WlRegion;
|
||||
use wayland_client::WEnum;
|
||||
use wayland_client::{protocol::wl_surface, Proxy};
|
||||
use wayland_protocols::wp::fractional_scale::v1::client::wp_fractional_scale_v1;
|
||||
use wayland_protocols::wp::viewporter::client::wp_viewport;
|
||||
use wayland_protocols::xdg::decoration::zv1::client::zxdg_toplevel_decoration_v1;
|
||||
use wayland_protocols::xdg::shell::client::xdg_surface;
|
||||
use wayland_protocols::xdg::shell::client::xdg_toplevel::{self};
|
||||
use wayland_protocols_plasma::blur::client::org_kde_kwin_blur;
|
||||
use wayland_protocols::xdg::shell::client::xdg_toplevel::{self, WmCapabilities};
|
||||
use wayland_protocols_plasma::blur::client::{org_kde_kwin_blur, org_kde_kwin_blur_manager};
|
||||
|
||||
use crate::platform::blade::{BladeRenderer, BladeSurfaceConfig};
|
||||
use crate::platform::linux::wayland::display::WaylandDisplay;
|
||||
@@ -26,9 +29,9 @@ use crate::platform::linux::wayland::serial::SerialKind;
|
||||
use crate::platform::{PlatformAtlas, PlatformInputHandler, PlatformWindow};
|
||||
use crate::scene::Scene;
|
||||
use crate::{
|
||||
px, size, AnyWindowHandle, Bounds, DevicePixels, Globals, Modifiers, Output, Pixels,
|
||||
PlatformDisplay, PlatformInput, Point, PromptLevel, Size, WaylandClientStatePtr,
|
||||
WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowParams,
|
||||
px, size, Bounds, DevicePixels, Globals, Modifiers, Pixels, PlatformDisplay, PlatformInput,
|
||||
Point, PromptLevel, Size, WaylandClientStatePtr, WindowAppearance, WindowBackgroundAppearance,
|
||||
WindowBounds, WindowParams,
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -72,8 +75,7 @@ pub struct WaylandWindowState {
|
||||
blur: Option<org_kde_kwin_blur::OrgKdeKwinBlur>,
|
||||
toplevel: xdg_toplevel::XdgToplevel,
|
||||
viewport: Option<wp_viewport::WpViewport>,
|
||||
outputs: HashMap<ObjectId, Output>,
|
||||
display: Option<(ObjectId, Output)>,
|
||||
outputs: HashSet<ObjectId>,
|
||||
globals: Globals,
|
||||
renderer: BladeRenderer,
|
||||
bounds: Bounds<u32>,
|
||||
@@ -81,11 +83,10 @@ pub struct WaylandWindowState {
|
||||
input_handler: Option<PlatformInputHandler>,
|
||||
decoration_state: WaylandDecorationState,
|
||||
fullscreen: bool,
|
||||
restore_bounds: Bounds<DevicePixels>,
|
||||
maximized: bool,
|
||||
windowed_bounds: Bounds<DevicePixels>,
|
||||
client: WaylandClientStatePtr,
|
||||
handle: AnyWindowHandle,
|
||||
active: bool,
|
||||
callbacks: Callbacks,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -97,7 +98,6 @@ pub struct WaylandWindowStatePtr {
|
||||
impl WaylandWindowState {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn new(
|
||||
handle: AnyWindowHandle,
|
||||
surface: wl_surface::WlSurface,
|
||||
xdg_surface: xdg_surface::XdgSurface,
|
||||
toplevel: xdg_toplevel::XdgToplevel,
|
||||
@@ -150,20 +150,18 @@ impl WaylandWindowState {
|
||||
toplevel,
|
||||
viewport,
|
||||
globals,
|
||||
outputs: HashMap::default(),
|
||||
display: None,
|
||||
outputs: HashSet::default(),
|
||||
renderer: BladeRenderer::new(gpu, config),
|
||||
bounds,
|
||||
scale: 1.0,
|
||||
input_handler: None,
|
||||
decoration_state: WaylandDecorationState::Client,
|
||||
fullscreen: false,
|
||||
restore_bounds: Bounds::default(),
|
||||
maximized: false,
|
||||
windowed_bounds: options.bounds,
|
||||
callbacks: Callbacks::default(),
|
||||
client,
|
||||
appearance,
|
||||
handle,
|
||||
active: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -219,7 +217,6 @@ impl WaylandWindow {
|
||||
}
|
||||
|
||||
pub fn new(
|
||||
handle: AnyWindowHandle,
|
||||
globals: Globals,
|
||||
client: WaylandClientStatePtr,
|
||||
params: WindowParams,
|
||||
@@ -230,7 +227,6 @@ impl WaylandWindow {
|
||||
.wm_base
|
||||
.get_xdg_surface(&surface, &globals.qh, surface.id());
|
||||
let toplevel = xdg_surface.get_toplevel(&globals.qh, surface.id());
|
||||
toplevel.set_min_size(200, 200);
|
||||
|
||||
if let Some(fractional_scale_manager) = globals.fractional_scale_manager.as_ref() {
|
||||
fractional_scale_manager.get_fractional_scale(&surface, &globals.qh, surface.id());
|
||||
@@ -257,7 +253,6 @@ impl WaylandWindow {
|
||||
|
||||
let this = Self(WaylandWindowStatePtr {
|
||||
state: Rc::new(RefCell::new(WaylandWindowState::new(
|
||||
handle,
|
||||
surface.clone(),
|
||||
xdg_surface,
|
||||
toplevel,
|
||||
@@ -279,10 +274,6 @@ impl WaylandWindow {
|
||||
}
|
||||
|
||||
impl WaylandWindowStatePtr {
|
||||
pub fn handle(&self) -> AnyWindowHandle {
|
||||
self.state.borrow().handle
|
||||
}
|
||||
|
||||
pub fn surface(&self) -> wl_surface::WlSurface {
|
||||
self.state.borrow().surface.clone()
|
||||
}
|
||||
@@ -349,32 +340,23 @@ impl WaylandWindowStatePtr {
|
||||
pub fn handle_toplevel_event(&self, event: xdg_toplevel::Event) -> bool {
|
||||
match event {
|
||||
xdg_toplevel::Event::Configure {
|
||||
mut width,
|
||||
mut height,
|
||||
width,
|
||||
height,
|
||||
states,
|
||||
} => {
|
||||
let fullscreen = states.contains(&(xdg_toplevel::State::Fullscreen as u8));
|
||||
let maximized = states.contains(&(xdg_toplevel::State::Maximized as u8));
|
||||
|
||||
let mut state = self.state.borrow_mut();
|
||||
let got_unmaximized = state.maximized && !maximized;
|
||||
state.fullscreen = fullscreen;
|
||||
state.maximized = maximized;
|
||||
|
||||
if got_unmaximized {
|
||||
width = state.windowed_bounds.size.width.0;
|
||||
height = state.windowed_bounds.size.height.0;
|
||||
} else if width != 0 && height != 0 && !fullscreen && !maximized {
|
||||
state.windowed_bounds = Bounds {
|
||||
origin: Point::default(),
|
||||
size: size(width.into(), height.into()),
|
||||
};
|
||||
}
|
||||
|
||||
let width = NonZeroU32::new(width as u32);
|
||||
let height = NonZeroU32::new(height as u32);
|
||||
let fullscreen = states.contains(&(xdg_toplevel::State::Fullscreen as u8));
|
||||
let maximized = states.contains(&(xdg_toplevel::State::Maximized as u8));
|
||||
let mut state = self.state.borrow_mut();
|
||||
state.maximized = maximized;
|
||||
state.fullscreen = fullscreen;
|
||||
if fullscreen || maximized {
|
||||
state.restore_bounds = state.bounds.map(|p| DevicePixels(p as i32));
|
||||
}
|
||||
drop(state);
|
||||
self.resize(width, height);
|
||||
self.set_fullscreen(fullscreen);
|
||||
|
||||
false
|
||||
}
|
||||
@@ -399,48 +381,58 @@ impl WaylandWindowStatePtr {
|
||||
pub fn handle_surface_event(
|
||||
&self,
|
||||
event: wl_surface::Event,
|
||||
outputs: HashMap<ObjectId, Output>,
|
||||
output_scales: HashMap<ObjectId, i32>,
|
||||
) {
|
||||
let mut state = self.state.borrow_mut();
|
||||
|
||||
// We use `WpFractionalScale` instead to set the scale if it's available
|
||||
if state.globals.fractional_scale_manager.is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
match event {
|
||||
wl_surface::Event::Enter { output } => {
|
||||
let id = output.id();
|
||||
|
||||
let Some(output) = outputs.get(&id) else {
|
||||
return;
|
||||
};
|
||||
|
||||
state.outputs.insert(id, output.clone());
|
||||
|
||||
let scale = primary_output_scale(&mut state);
|
||||
|
||||
// We use `PreferredBufferScale` instead to set the scale if it's available
|
||||
if state.surface.version() < wl_surface::EVT_PREFERRED_BUFFER_SCALE_SINCE {
|
||||
state.surface.set_buffer_scale(scale);
|
||||
drop(state);
|
||||
self.rescale(scale as f32);
|
||||
if state.surface.version() >= wl_surface::EVT_PREFERRED_BUFFER_SCALE_SINCE {
|
||||
return;
|
||||
}
|
||||
|
||||
state.outputs.insert(output.id());
|
||||
|
||||
let mut scale = 1;
|
||||
for output in state.outputs.iter() {
|
||||
if let Some(s) = output_scales.get(output) {
|
||||
scale = scale.max(*s)
|
||||
}
|
||||
}
|
||||
|
||||
state.surface.set_buffer_scale(scale);
|
||||
drop(state);
|
||||
self.rescale(scale as f32);
|
||||
}
|
||||
wl_surface::Event::Leave { output } => {
|
||||
// We use `PreferredBufferScale` instead to set the scale if it's available
|
||||
if state.surface.version() >= wl_surface::EVT_PREFERRED_BUFFER_SCALE_SINCE {
|
||||
return;
|
||||
}
|
||||
|
||||
state.outputs.remove(&output.id());
|
||||
|
||||
let scale = primary_output_scale(&mut state);
|
||||
|
||||
// We use `PreferredBufferScale` instead to set the scale if it's available
|
||||
if state.surface.version() < wl_surface::EVT_PREFERRED_BUFFER_SCALE_SINCE {
|
||||
state.surface.set_buffer_scale(scale);
|
||||
drop(state);
|
||||
self.rescale(scale as f32);
|
||||
let mut scale = 1;
|
||||
for output in state.outputs.iter() {
|
||||
if let Some(s) = output_scales.get(output) {
|
||||
scale = scale.max(*s)
|
||||
}
|
||||
}
|
||||
|
||||
state.surface.set_buffer_scale(scale);
|
||||
drop(state);
|
||||
self.rescale(scale as f32);
|
||||
}
|
||||
wl_surface::Event::PreferredBufferScale { factor } => {
|
||||
// We use `WpFractionalScale` instead to set the scale if it's available
|
||||
if state.globals.fractional_scale_manager.is_none() {
|
||||
state.surface.set_buffer_scale(factor);
|
||||
drop(state);
|
||||
self.rescale(factor as f32);
|
||||
}
|
||||
state.surface.set_buffer_scale(factor);
|
||||
drop(state);
|
||||
self.rescale(factor as f32);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -542,6 +534,11 @@ impl WaylandWindowStatePtr {
|
||||
self.set_size_and_scale(None, None, Some(scale));
|
||||
}
|
||||
|
||||
pub fn set_fullscreen(&self, fullscreen: bool) {
|
||||
let mut state = self.state.borrow_mut();
|
||||
state.fullscreen = fullscreen;
|
||||
}
|
||||
|
||||
/// Notifies the window of the state of the decorations.
|
||||
///
|
||||
/// # Note
|
||||
@@ -580,7 +577,6 @@ impl WaylandWindowStatePtr {
|
||||
}
|
||||
|
||||
pub fn set_focused(&self, focus: bool) {
|
||||
self.state.borrow_mut().active = focus;
|
||||
if let Some(ref mut fun) = self.callbacks.borrow_mut().active_status_change {
|
||||
fun(focus);
|
||||
}
|
||||
@@ -596,23 +592,6 @@ impl WaylandWindowStatePtr {
|
||||
}
|
||||
}
|
||||
|
||||
fn primary_output_scale(state: &mut RefMut<WaylandWindowState>) -> i32 {
|
||||
let mut scale = 1;
|
||||
let mut current_output = state.display.take();
|
||||
for (id, output) in state.outputs.iter() {
|
||||
if let Some((_, output_data)) = ¤t_output {
|
||||
if output.scale > output_data.scale {
|
||||
current_output = Some((id.clone(), output.clone()));
|
||||
}
|
||||
} else {
|
||||
current_output = Some((id.clone(), output.clone()));
|
||||
}
|
||||
scale = scale.max(output.scale);
|
||||
}
|
||||
state.display = current_output;
|
||||
scale
|
||||
}
|
||||
|
||||
impl rwh::HasWindowHandle for WaylandWindow {
|
||||
fn window_handle(&self) -> Result<rwh::WindowHandle<'_>, rwh::HandleError> {
|
||||
unimplemented!()
|
||||
@@ -633,12 +612,14 @@ impl PlatformWindow for WaylandWindow {
|
||||
self.borrow().maximized
|
||||
}
|
||||
|
||||
// todo(linux)
|
||||
// check if it is right
|
||||
fn window_bounds(&self) -> WindowBounds {
|
||||
let state = self.borrow();
|
||||
if state.fullscreen {
|
||||
WindowBounds::Fullscreen(state.windowed_bounds)
|
||||
WindowBounds::Fullscreen(state.restore_bounds)
|
||||
} else if state.maximized {
|
||||
WindowBounds::Maximized(state.windowed_bounds)
|
||||
WindowBounds::Maximized(state.restore_bounds)
|
||||
} else {
|
||||
WindowBounds::Windowed(state.bounds.map(|p| DevicePixels(p as i32)))
|
||||
}
|
||||
@@ -660,27 +641,19 @@ impl PlatformWindow for WaylandWindow {
|
||||
self.borrow().appearance
|
||||
}
|
||||
|
||||
fn display(&self) -> Option<Rc<dyn PlatformDisplay>> {
|
||||
self.borrow().display.as_ref().map(|(id, display)| {
|
||||
Rc::new(WaylandDisplay {
|
||||
id: id.clone(),
|
||||
name: display.name.clone(),
|
||||
bounds: display.bounds,
|
||||
}) as Rc<dyn PlatformDisplay>
|
||||
})
|
||||
// todo(linux)
|
||||
fn display(&self) -> Rc<dyn PlatformDisplay> {
|
||||
Rc::new(WaylandDisplay {})
|
||||
}
|
||||
|
||||
// todo(linux)
|
||||
fn mouse_position(&self) -> Point<Pixels> {
|
||||
self.borrow()
|
||||
.client
|
||||
.get_client()
|
||||
.borrow()
|
||||
.mouse_location
|
||||
.unwrap_or_default()
|
||||
Point::default()
|
||||
}
|
||||
|
||||
// todo(linux)
|
||||
fn modifiers(&self) -> Modifiers {
|
||||
self.borrow().client.get_client().borrow().modifiers
|
||||
crate::Modifiers::default()
|
||||
}
|
||||
|
||||
fn set_input_handler(&mut self, input_handler: PlatformInputHandler) {
|
||||
@@ -693,20 +666,21 @@ impl PlatformWindow for WaylandWindow {
|
||||
|
||||
fn prompt(
|
||||
&self,
|
||||
_level: PromptLevel,
|
||||
_msg: &str,
|
||||
_detail: Option<&str>,
|
||||
_answers: &[&str],
|
||||
level: PromptLevel,
|
||||
msg: &str,
|
||||
detail: Option<&str>,
|
||||
answers: &[&str],
|
||||
) -> Option<Receiver<usize>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn activate(&self) {
|
||||
log::info!("Wayland does not support this API");
|
||||
// todo(linux)
|
||||
}
|
||||
|
||||
// todo(linux)
|
||||
fn is_active(&self) -> bool {
|
||||
self.borrow().active
|
||||
false
|
||||
}
|
||||
|
||||
fn set_title(&mut self, title: &str) {
|
||||
@@ -738,8 +712,8 @@ impl PlatformWindow for WaylandWindow {
|
||||
}
|
||||
|
||||
if let Some(ref blur_manager) = state.globals.blur_manager {
|
||||
if background_appearance == WindowBackgroundAppearance::Blurred {
|
||||
if state.blur.is_none() {
|
||||
if (background_appearance == WindowBackgroundAppearance::Blurred) {
|
||||
if (state.blur.is_none()) {
|
||||
let blur = blur_manager.create(&state.surface, &state.globals.qh, ());
|
||||
blur.set_region(Some(®ion));
|
||||
state.blur = Some(blur);
|
||||
@@ -757,12 +731,12 @@ impl PlatformWindow for WaylandWindow {
|
||||
region.destroy();
|
||||
}
|
||||
|
||||
fn set_edited(&mut self, _edited: bool) {
|
||||
log::info!("ignoring macOS specific set_edited");
|
||||
fn set_edited(&mut self, edited: bool) {
|
||||
// todo(linux)
|
||||
}
|
||||
|
||||
fn show_character_palette(&self) {
|
||||
log::info!("ignoring macOS specific show_character_palette");
|
||||
// todo(linux)
|
||||
}
|
||||
|
||||
fn minimize(&self) {
|
||||
@@ -780,6 +754,7 @@ impl PlatformWindow for WaylandWindow {
|
||||
|
||||
fn toggle_fullscreen(&self) {
|
||||
let mut state = self.borrow_mut();
|
||||
state.restore_bounds = state.bounds.map(|p| DevicePixels(p as i32));
|
||||
if !state.fullscreen {
|
||||
state.toplevel.set_fullscreen(None);
|
||||
} else {
|
||||
|
||||
@@ -2,6 +2,7 @@ use std::cell::RefCell;
|
||||
use std::ffi::OsString;
|
||||
use std::ops::Deref;
|
||||
use std::rc::{Rc, Weak};
|
||||
use std::sync::OnceLock;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use calloop::generic::{FdWrapper, Generic};
|
||||
@@ -10,29 +11,30 @@ use calloop::{channel, EventLoop, LoopHandle, RegistrationToken};
|
||||
use collections::HashMap;
|
||||
use copypasta::x11_clipboard::{Clipboard, Primary, X11ClipboardContext};
|
||||
use copypasta::ClipboardProvider;
|
||||
use parking_lot::Mutex;
|
||||
|
||||
use util::ResultExt;
|
||||
use x11rb::connection::{Connection, RequestConnection};
|
||||
use x11rb::cursor;
|
||||
use x11rb::errors::ConnectionError;
|
||||
use x11rb::protocol::randr::ConnectionExt as _;
|
||||
use x11rb::protocol::xinput::ConnectionExt;
|
||||
use x11rb::protocol::xinput::{ConnectionExt, ScrollClass};
|
||||
use x11rb::protocol::xkb::ConnectionExt as _;
|
||||
use x11rb::protocol::xproto::{ChangeWindowAttributesAux, ConnectionExt as _};
|
||||
use x11rb::protocol::{randr, render, xinput, xkb, xproto, Event};
|
||||
use x11rb::resource_manager::Database;
|
||||
use x11rb::xcb_ffi::XCBConnection;
|
||||
use xim::{x11rb::X11rbClient, Client};
|
||||
use xim::{AttributeName, InputStyle};
|
||||
use xim::{AHashMap, AttributeName, ClientHandler, InputStyle};
|
||||
use xkbc::x11::ffi::{XKB_X11_MIN_MAJOR_XKB_VERSION, XKB_X11_MIN_MINOR_XKB_VERSION};
|
||||
use xkbcommon::xkb as xkbc;
|
||||
|
||||
use crate::platform::linux::LinuxClient;
|
||||
use crate::platform::{LinuxCommon, PlatformWindow};
|
||||
use crate::platform::{LinuxCommon, PlatformWindow, WaylandClientState};
|
||||
use crate::{
|
||||
modifiers_from_xinput_info, point, px, AnyWindowHandle, Bounds, CursorStyle, DisplayId,
|
||||
Keystroke, Modifiers, ModifiersChangedEvent, Pixels, PlatformDisplay, PlatformInput, Point,
|
||||
ScrollDelta, Size, TouchPhase, WindowParams, X11Window,
|
||||
ForegroundExecutor, Keystroke, Modifiers, ModifiersChangedEvent, Pixels, PlatformDisplay,
|
||||
PlatformInput, Point, ScrollDelta, Size, TouchPhase, WindowAppearance, WindowParams, X11Window,
|
||||
};
|
||||
|
||||
use super::{
|
||||
@@ -52,12 +54,6 @@ pub(crate) struct WindowRef {
|
||||
refresh_event_token: RegistrationToken,
|
||||
}
|
||||
|
||||
impl WindowRef {
|
||||
pub fn handle(&self) -> AnyWindowHandle {
|
||||
self.window.state.borrow().handle
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for WindowRef {
|
||||
type Target = X11WindowStatePtr;
|
||||
|
||||
@@ -108,14 +104,13 @@ pub struct X11ClientState {
|
||||
|
||||
pub(crate) xcb_connection: Rc<XCBConnection>,
|
||||
pub(crate) x_root_index: usize,
|
||||
pub(crate) _resource_database: Database,
|
||||
pub(crate) resource_database: Database,
|
||||
pub(crate) atoms: XcbAtoms,
|
||||
pub(crate) windows: HashMap<xproto::Window, WindowRef>,
|
||||
pub(crate) focused_window: Option<xproto::Window>,
|
||||
pub(crate) xkb: xkbc::State,
|
||||
pub(crate) ximc: Option<X11rbClient<Rc<XCBConnection>>>,
|
||||
pub(crate) xim_handler: Option<XimHandler>,
|
||||
pub modifiers: Modifiers,
|
||||
|
||||
pub(crate) compose_state: xkbc::compose::State,
|
||||
pub(crate) pre_edit_text: Option<String>,
|
||||
@@ -164,13 +159,11 @@ impl X11Client {
|
||||
|
||||
let handle = event_loop.handle();
|
||||
|
||||
handle
|
||||
.insert_source(main_receiver, |event, _, _: &mut X11Client| {
|
||||
if let calloop::channel::Event::Msg(runnable) = event {
|
||||
runnable.run();
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
handle.insert_source(main_receiver, |event, _, _: &mut X11Client| {
|
||||
if let calloop::channel::Event::Msg(runnable) = event {
|
||||
runnable.run();
|
||||
}
|
||||
});
|
||||
|
||||
let (xcb_connection, x_root_index) = XCBConnection::connect(None).unwrap();
|
||||
xcb_connection
|
||||
@@ -255,7 +248,23 @@ impl X11Client {
|
||||
xkbc::compose::State::new(&table, xkbc::compose::STATE_NO_FLAGS)
|
||||
};
|
||||
|
||||
let resource_database = x11rb::resource_manager::new_from_default(&xcb_connection).unwrap();
|
||||
let screen = xcb_connection.setup().roots.get(x_root_index).unwrap();
|
||||
|
||||
// Values from `Database::GET_RESOURCE_DATABASE`
|
||||
let resource_manager = xcb_connection
|
||||
.get_property(
|
||||
false,
|
||||
screen.root,
|
||||
xproto::AtomEnum::RESOURCE_MANAGER,
|
||||
xproto::AtomEnum::STRING,
|
||||
0,
|
||||
100_000_000,
|
||||
)
|
||||
.unwrap();
|
||||
let resource_manager = resource_manager.reply().unwrap();
|
||||
|
||||
// todo(linux): read hostname
|
||||
let resource_database = Database::new_from_default(&resource_manager, "HOSTNAME".into());
|
||||
|
||||
let scale_factor = resource_database
|
||||
.get_value("Xft.dpi", "Xft.dpi")
|
||||
@@ -336,7 +345,7 @@ impl X11Client {
|
||||
.insert_source(xim_rx, {
|
||||
move |chan_event, _, client| match chan_event {
|
||||
channel::Event::Msg(xim_event) => {
|
||||
match xim_event {
|
||||
match (xim_event) {
|
||||
XimCallbackEvent::XimXEvent(event) => {
|
||||
client.handle_event(event);
|
||||
}
|
||||
@@ -354,21 +363,18 @@ impl X11Client {
|
||||
}
|
||||
})
|
||||
.expect("Failed to initialize XIM event source");
|
||||
handle
|
||||
.insert_source(XDPEventSource::new(&common.background_executor), {
|
||||
move |event, _, client| match event {
|
||||
XDPEvent::WindowAppearance(appearance) => {
|
||||
client.with_common(|common| common.appearance = appearance);
|
||||
for (_, window) in &mut client.0.borrow_mut().windows {
|
||||
window.window.set_appearance(appearance);
|
||||
}
|
||||
handle.insert_source(XDPEventSource::new(&common.background_executor), {
|
||||
move |event, _, client| match event {
|
||||
XDPEvent::WindowAppearance(appearance) => {
|
||||
client.with_common(|common| common.appearance = appearance);
|
||||
for (_, window) in &mut client.0.borrow_mut().windows {
|
||||
window.window.set_appearance(appearance);
|
||||
}
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
});
|
||||
|
||||
X11Client(Rc::new(RefCell::new(X11ClientState {
|
||||
modifiers: Modifiers::default(),
|
||||
event_loop: Some(event_loop),
|
||||
loop_handle: handle,
|
||||
common,
|
||||
@@ -379,7 +385,7 @@ impl X11Client {
|
||||
|
||||
xcb_connection,
|
||||
x_root_index,
|
||||
_resource_database: resource_database,
|
||||
resource_database,
|
||||
atoms,
|
||||
windows: HashMap::default(),
|
||||
focused_window: None,
|
||||
@@ -440,8 +446,7 @@ impl X11Client {
|
||||
});
|
||||
}
|
||||
}
|
||||
ximc.create_ic(xim_handler.im_id, ic_attributes.build())
|
||||
.ok();
|
||||
ximc.create_ic(xim_handler.im_id, ic_attributes.build());
|
||||
state = self.0.borrow_mut();
|
||||
state.xim_handler = Some(xim_handler);
|
||||
state.ximc = Some(ximc);
|
||||
@@ -452,7 +457,7 @@ impl X11Client {
|
||||
state.composing = false;
|
||||
if let Some(mut ximc) = state.ximc.take() {
|
||||
let xim_handler = state.xim_handler.as_ref().unwrap();
|
||||
ximc.destroy_ic(xim_handler.im_id, xim_handler.ic_id).ok();
|
||||
ximc.destroy_ic(xim_handler.im_id, xim_handler.ic_id);
|
||||
state.ximc = Some(ximc);
|
||||
}
|
||||
}
|
||||
@@ -530,7 +535,6 @@ impl X11Client {
|
||||
);
|
||||
let modifiers = Modifiers::from_xkb(&state.xkb);
|
||||
let focused_window_id = state.focused_window?;
|
||||
state.modifiers = modifiers;
|
||||
drop(state);
|
||||
|
||||
let focused_window = self.get_window(focused_window_id)?;
|
||||
@@ -543,8 +547,6 @@ impl X11Client {
|
||||
let mut state = self.0.borrow_mut();
|
||||
|
||||
let modifiers = modifiers_from_state(event.state);
|
||||
state.modifiers = modifiers;
|
||||
|
||||
let keystroke = {
|
||||
let code = event.detail.into();
|
||||
let mut keystroke = crate::Keystroke::from_xkb(&state.xkb, modifiers, code);
|
||||
@@ -598,8 +600,6 @@ impl X11Client {
|
||||
let mut state = self.0.borrow_mut();
|
||||
|
||||
let modifiers = modifiers_from_state(event.state);
|
||||
state.modifiers = modifiers;
|
||||
|
||||
let keystroke = {
|
||||
let code = event.detail.into();
|
||||
let keystroke = crate::Keystroke::from_xkb(&state.xkb, modifiers, code);
|
||||
@@ -618,8 +618,6 @@ impl X11Client {
|
||||
let mut state = self.0.borrow_mut();
|
||||
|
||||
let modifiers = modifiers_from_xinput_info(event.mods);
|
||||
state.modifiers = modifiers;
|
||||
|
||||
let position = point(
|
||||
px(event.event_x as f32 / u16::MAX as f32 / state.scale_factor),
|
||||
px(event.event_y as f32 / u16::MAX as f32 / state.scale_factor),
|
||||
@@ -666,10 +664,8 @@ impl X11Client {
|
||||
}
|
||||
Event::XinputButtonRelease(event) => {
|
||||
let window = self.get_window(event.event)?;
|
||||
let mut state = self.0.borrow_mut();
|
||||
let state = self.0.borrow();
|
||||
let modifiers = modifiers_from_xinput_info(event.mods);
|
||||
state.modifiers = modifiers;
|
||||
|
||||
let position = point(
|
||||
px(event.event_x as f32 / u16::MAX as f32 / state.scale_factor),
|
||||
px(event.event_y as f32 / u16::MAX as f32 / state.scale_factor),
|
||||
@@ -687,15 +683,14 @@ impl X11Client {
|
||||
}
|
||||
Event::XinputMotion(event) => {
|
||||
let window = self.get_window(event.event)?;
|
||||
let mut state = self.0.borrow_mut();
|
||||
let state = self.0.borrow();
|
||||
let pressed_button = pressed_button_from_mask(event.button_mask[0]);
|
||||
let position = point(
|
||||
px(event.event_x as f32 / u16::MAX as f32 / state.scale_factor),
|
||||
px(event.event_y as f32 / u16::MAX as f32 / state.scale_factor),
|
||||
);
|
||||
let modifiers = modifiers_from_xinput_info(event.mods);
|
||||
state.modifiers = modifiers;
|
||||
drop(state);
|
||||
let modifiers = modifiers_from_xinput_info(event.mods);
|
||||
|
||||
let axisvalues = event
|
||||
.axisvalues
|
||||
@@ -766,19 +761,18 @@ impl X11Client {
|
||||
valuator_idx += 1;
|
||||
}
|
||||
}
|
||||
Event::XinputLeave(event) if event.mode == xinput::NotifyMode::NORMAL => {
|
||||
Event::XinputLeave(event) => {
|
||||
self.0.borrow_mut().scroll_x = None; // Set last scroll to `None` so that a large delta isn't created if scrolling is done outside the window (the valuator is global)
|
||||
self.0.borrow_mut().scroll_y = None;
|
||||
|
||||
let window = self.get_window(event.event)?;
|
||||
let mut state = self.0.borrow_mut();
|
||||
let state = self.0.borrow();
|
||||
let pressed_button = pressed_button_from_mask(event.buttons[0]);
|
||||
let position = point(
|
||||
px(event.event_x as f32 / u16::MAX as f32 / state.scale_factor),
|
||||
px(event.event_y as f32 / u16::MAX as f32 / state.scale_factor),
|
||||
);
|
||||
let modifiers = modifiers_from_xinput_info(event.mods);
|
||||
state.modifiers = modifiers;
|
||||
drop(state);
|
||||
|
||||
window.handle_input(PlatformInput::MouseExited(crate::MouseExitEvent {
|
||||
@@ -861,8 +855,7 @@ impl X11Client {
|
||||
);
|
||||
})
|
||||
.build();
|
||||
ximc.set_ic_values(xim_handler.im_id, xim_handler.ic_id, ic_attributes)
|
||||
.ok();
|
||||
ximc.set_ic_values(xim_handler.im_id, xim_handler.ic_id, ic_attributes);
|
||||
}
|
||||
let mut state = self.0.borrow_mut();
|
||||
state.ximc = Some(ximc);
|
||||
@@ -911,14 +904,13 @@ impl LinuxClient for X11Client {
|
||||
|
||||
fn open_window(
|
||||
&self,
|
||||
handle: AnyWindowHandle,
|
||||
_handle: AnyWindowHandle,
|
||||
params: WindowParams,
|
||||
) -> Box<dyn PlatformWindow> {
|
||||
let mut state = self.0.borrow_mut();
|
||||
let x_window = state.xcb_connection.generate_id().unwrap();
|
||||
|
||||
let window = X11Window::new(
|
||||
handle,
|
||||
X11ClientStatePtr(Rc::downgrade(&self.0)),
|
||||
state.common.foreground_executor.clone(),
|
||||
params,
|
||||
@@ -1042,11 +1034,11 @@ impl LinuxClient for X11Client {
|
||||
}
|
||||
|
||||
fn write_to_primary(&self, item: crate::ClipboardItem) {
|
||||
self.0.borrow_mut().primary.set_contents(item.text).ok();
|
||||
self.0.borrow_mut().primary.set_contents(item.text);
|
||||
}
|
||||
|
||||
fn write_to_clipboard(&self, item: crate::ClipboardItem) {
|
||||
self.0.borrow_mut().clipboard.set_contents(item.text).ok();
|
||||
self.0.borrow_mut().clipboard.set_contents(item.text);
|
||||
}
|
||||
|
||||
fn read_from_primary(&self) -> Option<crate::ClipboardItem> {
|
||||
@@ -1083,16 +1075,6 @@ impl LinuxClient for X11Client {
|
||||
|
||||
event_loop.run(None, &mut self.clone(), |_| {}).log_err();
|
||||
}
|
||||
|
||||
fn active_window(&self) -> Option<AnyWindowHandle> {
|
||||
let state = self.0.borrow();
|
||||
state.focused_window.and_then(|focused_window| {
|
||||
state
|
||||
.windows
|
||||
.get(&focused_window)
|
||||
.map(|window| window.handle())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Adatpted from:
|
||||
|
||||
@@ -1,29 +1,41 @@
|
||||
// todo(linux): remove
|
||||
#![allow(unused)]
|
||||
|
||||
use crate::{
|
||||
platform::blade::{BladeRenderer, BladeSurfaceConfig},
|
||||
size, AnyWindowHandle, Bounds, DevicePixels, ForegroundExecutor, Modifiers, Pixels,
|
||||
PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow, Point,
|
||||
PromptLevel, Scene, Size, WindowAppearance, WindowBackgroundAppearance, WindowBounds,
|
||||
WindowParams, X11ClientStatePtr,
|
||||
size, Bounds, DevicePixels, ForegroundExecutor, Modifiers, Pixels, Platform, PlatformAtlas,
|
||||
PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow, Point, PromptLevel,
|
||||
Scene, Size, WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowOptions,
|
||||
WindowParams, X11Client, X11ClientState, X11ClientStatePtr,
|
||||
};
|
||||
|
||||
use blade_graphics as gpu;
|
||||
use parking_lot::Mutex;
|
||||
use raw_window_handle as rwh;
|
||||
use util::ResultExt;
|
||||
use x11rb::{
|
||||
connection::Connection,
|
||||
connection::{Connection as _, RequestConnection as _},
|
||||
protocol::{
|
||||
render,
|
||||
xinput::{self, ConnectionExt as _},
|
||||
xproto::{
|
||||
self, ClientMessageEvent, ConnectionExt as _, EventMask, TranslateCoordinatesReply,
|
||||
self, Atom, ClientMessageEvent, ConnectionExt as _, CreateWindowAux, EventMask,
|
||||
TranslateCoordinatesReply,
|
||||
},
|
||||
},
|
||||
resource_manager::Database,
|
||||
wrapper::ConnectionExt as _,
|
||||
xcb_ffi::XCBConnection,
|
||||
};
|
||||
|
||||
use std::ops::Deref;
|
||||
use std::rc::Weak;
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
cell::{Ref, RefCell, RefMut},
|
||||
collections::HashMap,
|
||||
ffi::c_void,
|
||||
iter::Zip,
|
||||
mem,
|
||||
num::NonZeroU32,
|
||||
ops::Div,
|
||||
ptr::NonNull,
|
||||
@@ -153,29 +165,29 @@ pub struct Callbacks {
|
||||
appearance_changed: Option<Box<dyn FnMut()>>,
|
||||
}
|
||||
|
||||
pub struct X11WindowState {
|
||||
pub(crate) struct X11WindowState {
|
||||
client: X11ClientStatePtr,
|
||||
executor: ForegroundExecutor,
|
||||
atoms: XcbAtoms,
|
||||
x_root_window: xproto::Window,
|
||||
_raw: RawWindow,
|
||||
raw: RawWindow,
|
||||
bounds: Bounds<i32>,
|
||||
scale_factor: f32,
|
||||
renderer: BladeRenderer,
|
||||
display: Rc<dyn PlatformDisplay>,
|
||||
input_handler: Option<PlatformInputHandler>,
|
||||
appearance: WindowAppearance,
|
||||
pub handle: AnyWindowHandle,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct X11WindowStatePtr {
|
||||
pub state: Rc<RefCell<X11WindowState>>,
|
||||
pub(crate) state: Rc<RefCell<X11WindowState>>,
|
||||
pub(crate) callbacks: Rc<RefCell<Callbacks>>,
|
||||
xcb_connection: Rc<XCBConnection>,
|
||||
x_window: xproto::Window,
|
||||
}
|
||||
|
||||
// todo(linux): Remove other RawWindowHandle implementation
|
||||
impl rwh::HasWindowHandle for RawWindow {
|
||||
fn window_handle(&self) -> Result<rwh::WindowHandle, rwh::HandleError> {
|
||||
let non_zero = NonZeroU32::new(self.window_id).unwrap();
|
||||
@@ -206,7 +218,6 @@ impl rwh::HasDisplayHandle for X11Window {
|
||||
impl X11WindowState {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new(
|
||||
handle: AnyWindowHandle,
|
||||
client: X11ClientStatePtr,
|
||||
executor: ForegroundExecutor,
|
||||
params: WindowParams,
|
||||
@@ -261,6 +272,8 @@ impl X11WindowState {
|
||||
.event_mask(
|
||||
xproto::EventMask::EXPOSURE
|
||||
| xproto::EventMask::STRUCTURE_NOTIFY
|
||||
| xproto::EventMask::ENTER_WINDOW
|
||||
| xproto::EventMask::LEAVE_WINDOW
|
||||
| xproto::EventMask::FOCUS_CHANGE
|
||||
| xproto::EventMask::KEY_PRESS
|
||||
| xproto::EventMask::KEY_RELEASE,
|
||||
@@ -359,7 +372,7 @@ impl X11WindowState {
|
||||
client,
|
||||
executor,
|
||||
display: Rc::new(X11Display::new(xcb_connection, x_screen_index).unwrap()),
|
||||
_raw: raw,
|
||||
raw,
|
||||
x_root_window: visual_set.root,
|
||||
bounds: params.bounds.map(|v| v.0),
|
||||
scale_factor,
|
||||
@@ -367,7 +380,6 @@ impl X11WindowState {
|
||||
atoms: *atoms,
|
||||
input_handler: None,
|
||||
appearance,
|
||||
handle,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -408,15 +420,14 @@ impl Drop for X11Window {
|
||||
}
|
||||
|
||||
enum WmHintPropertyState {
|
||||
// Remove = 0,
|
||||
// Add = 1,
|
||||
Remove = 0,
|
||||
Add = 1,
|
||||
Toggle = 2,
|
||||
}
|
||||
|
||||
impl X11Window {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new(
|
||||
handle: AnyWindowHandle,
|
||||
client: X11ClientStatePtr,
|
||||
executor: ForegroundExecutor,
|
||||
params: WindowParams,
|
||||
@@ -429,7 +440,6 @@ impl X11Window {
|
||||
) -> Self {
|
||||
Self(X11WindowStatePtr {
|
||||
state: Rc::new(RefCell::new(X11WindowState::new(
|
||||
handle,
|
||||
client,
|
||||
executor,
|
||||
params,
|
||||
@@ -607,21 +617,13 @@ impl X11WindowStatePtr {
|
||||
|
||||
pub fn configure(&self, bounds: Bounds<i32>) {
|
||||
let mut resize_args = None;
|
||||
let is_resize;
|
||||
let do_move;
|
||||
{
|
||||
let mut state = self.state.borrow_mut();
|
||||
|
||||
is_resize = bounds.size.width != state.bounds.size.width
|
||||
|| bounds.size.height != state.bounds.size.height;
|
||||
|
||||
// If it's a resize event (only width/height changed), we ignore `bounds.origin`
|
||||
// because it contains wrong values.
|
||||
if is_resize {
|
||||
state.bounds.size = bounds.size;
|
||||
} else {
|
||||
state.bounds = bounds;
|
||||
}
|
||||
|
||||
let old_bounds = mem::replace(&mut state.bounds, bounds);
|
||||
do_move = old_bounds.origin != bounds.origin;
|
||||
// todo(linux): use normal GPUI types here, refactor out the double
|
||||
// viewport check and extra casts ( )
|
||||
let gpu_size = query_render_extent(&self.xcb_connection, self.x_window);
|
||||
if state.renderer.viewport_size() != gpu_size {
|
||||
state
|
||||
@@ -637,7 +639,7 @@ impl X11WindowStatePtr {
|
||||
fun(content_size, scale_factor)
|
||||
}
|
||||
}
|
||||
if !is_resize {
|
||||
if do_move {
|
||||
if let Some(ref mut fun) = callbacks.moved {
|
||||
fun()
|
||||
}
|
||||
@@ -696,8 +698,8 @@ impl PlatformWindow for X11Window {
|
||||
self.0.state.borrow().appearance
|
||||
}
|
||||
|
||||
fn display(&self) -> Option<Rc<dyn PlatformDisplay>> {
|
||||
Some(self.0.state.borrow().display.clone())
|
||||
fn display(&self) -> Rc<dyn PlatformDisplay> {
|
||||
self.0.state.borrow().display.clone()
|
||||
}
|
||||
|
||||
fn mouse_position(&self) -> Point<Pixels> {
|
||||
@@ -711,15 +713,9 @@ impl PlatformWindow for X11Window {
|
||||
Point::new((reply.root_x as u32).into(), (reply.root_y as u32).into())
|
||||
}
|
||||
|
||||
// todo(linux)
|
||||
fn modifiers(&self) -> Modifiers {
|
||||
self.0
|
||||
.state
|
||||
.borrow()
|
||||
.client
|
||||
.0
|
||||
.upgrade()
|
||||
.map(|ref_cell| ref_cell.borrow().modifiers)
|
||||
.unwrap_or_default()
|
||||
Modifiers::default()
|
||||
}
|
||||
|
||||
fn set_input_handler(&mut self, input_handler: PlatformInputHandler) {
|
||||
@@ -796,9 +792,8 @@ impl PlatformWindow for X11Window {
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn set_edited(&mut self, _edited: bool) {
|
||||
log::info!("ignoring macOS specific set_edited");
|
||||
}
|
||||
// todo(linux)
|
||||
fn set_edited(&mut self, edited: bool) {}
|
||||
|
||||
fn set_background_appearance(&mut self, background_appearance: WindowBackgroundAppearance) {
|
||||
let mut inner = self.0.state.borrow_mut();
|
||||
@@ -806,8 +801,14 @@ impl PlatformWindow for X11Window {
|
||||
inner.renderer.update_transparency(transparent);
|
||||
}
|
||||
|
||||
// todo(linux), this corresponds to `orderFrontCharacterPalette` on macOS,
|
||||
// but it looks like the equivalent for Linux is GTK specific:
|
||||
//
|
||||
// https://docs.gtk.org/gtk3/signal.Entry.insert-emoji.html
|
||||
//
|
||||
// This API might need to change, or we might need to build an emoji picker into GPUI
|
||||
fn show_character_palette(&self) {
|
||||
log::info!("ignoring macOS specific show_character_palette");
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn minimize(&self) {
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
use std::cell::RefCell;
|
||||
use std::default::Default;
|
||||
use std::rc::Rc;
|
||||
|
||||
use calloop::channel;
|
||||
|
||||
use x11rb::protocol::{xproto, Event};
|
||||
use xim::{AHashMap, AttributeName, Client, ClientError, ClientHandler, InputStyle};
|
||||
use xim::{AHashMap, AttributeName, Client, ClientError, ClientHandler, InputStyle, Point};
|
||||
|
||||
use crate::{Keystroke, PlatformInput, X11ClientState};
|
||||
|
||||
pub enum XimCallbackEvent {
|
||||
XimXEvent(x11rb::protocol::Event),
|
||||
@@ -80,12 +84,10 @@ impl<C: Client<XEvent = xproto::KeyPressEvent>> ClientHandler<C> for XimHandler
|
||||
_input_context_id: u16,
|
||||
text: &str,
|
||||
) -> Result<(), ClientError> {
|
||||
self.xim_tx
|
||||
.send(XimCallbackEvent::XimCommitEvent(
|
||||
self.window,
|
||||
String::from(text),
|
||||
))
|
||||
.ok();
|
||||
self.xim_tx.send(XimCallbackEvent::XimCommitEvent(
|
||||
self.window,
|
||||
String::from(text),
|
||||
));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -97,16 +99,14 @@ impl<C: Client<XEvent = xproto::KeyPressEvent>> ClientHandler<C> for XimHandler
|
||||
_flag: xim::ForwardEventFlag,
|
||||
xev: C::XEvent,
|
||||
) -> Result<(), ClientError> {
|
||||
match xev.response_type {
|
||||
match (xev.response_type) {
|
||||
x11rb::protocol::xproto::KEY_PRESS_EVENT => {
|
||||
self.xim_tx
|
||||
.send(XimCallbackEvent::XimXEvent(Event::KeyPress(xev)))
|
||||
.ok();
|
||||
.send(XimCallbackEvent::XimXEvent(Event::KeyPress(xev)));
|
||||
}
|
||||
x11rb::protocol::xproto::KEY_RELEASE_EVENT => {
|
||||
self.xim_tx
|
||||
.send(XimCallbackEvent::XimXEvent(Event::KeyRelease(xev)))
|
||||
.ok();
|
||||
.send(XimCallbackEvent::XimXEvent(Event::KeyRelease(xev)));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -145,12 +145,10 @@ impl<C: Client<XEvent = xproto::KeyPressEvent>> ClientHandler<C> for XimHandler
|
||||
// XIMPrimary, XIMHighlight, XIMSecondary, XIMTertiary are not specified,
|
||||
// but interchangeable as above
|
||||
// Currently there's no way to support these.
|
||||
self.xim_tx
|
||||
.send(XimCallbackEvent::XimPreeditEvent(
|
||||
self.window,
|
||||
String::from(preedit_string),
|
||||
))
|
||||
.ok();
|
||||
let mark_range = self.xim_tx.send(XimCallbackEvent::XimPreeditEvent(
|
||||
self.window,
|
||||
String::from(preedit_string),
|
||||
));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ use std::future::Future;
|
||||
use ashpd::desktop::settings::{ColorScheme, Settings};
|
||||
use calloop::channel::{Channel, Sender};
|
||||
use calloop::{EventSource, Poll, PostAction, Readiness, Token, TokenFactory};
|
||||
use parking_lot::Mutex;
|
||||
use smol::stream::StreamExt;
|
||||
use util::ResultExt;
|
||||
|
||||
@@ -114,7 +115,6 @@ impl WindowAppearance {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(target_os = "linux", allow(dead_code))]
|
||||
fn set_native(&mut self, cs: ColorScheme) {
|
||||
*self = Self::from_native(cs);
|
||||
}
|
||||
|
||||
@@ -29,10 +29,7 @@ use cocoa::{
|
||||
};
|
||||
|
||||
use objc::runtime::{BOOL, NO, YES};
|
||||
use std::{
|
||||
ffi::{c_char, CStr},
|
||||
ops::Range,
|
||||
};
|
||||
use std::ops::Range;
|
||||
|
||||
pub(crate) use dispatcher::*;
|
||||
pub(crate) use display::*;
|
||||
@@ -55,21 +52,6 @@ impl BoolExt for bool {
|
||||
}
|
||||
}
|
||||
|
||||
trait NSStringExt {
|
||||
unsafe fn to_str(&self) -> &str;
|
||||
}
|
||||
|
||||
impl NSStringExt for id {
|
||||
unsafe fn to_str(&self) -> &str {
|
||||
let cstr = self.UTF8String();
|
||||
if cstr.is_null() {
|
||||
""
|
||||
} else {
|
||||
CStr::from_ptr(cstr as *mut c_char).to_str().unwrap()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
struct NSRange {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
use crate::{
|
||||
platform::mac::NSStringExt, point, px, KeyDownEvent, KeyUpEvent, Keystroke, Modifiers,
|
||||
ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseExitEvent, MouseMoveEvent,
|
||||
MouseUpEvent, NavigationDirection, Pixels, PlatformInput, ScrollDelta, ScrollWheelEvent,
|
||||
TouchPhase,
|
||||
point, px, KeyDownEvent, KeyUpEvent, Keystroke, Modifiers, ModifiersChangedEvent, MouseButton,
|
||||
MouseDownEvent, MouseExitEvent, MouseMoveEvent, MouseUpEvent, NavigationDirection, Pixels,
|
||||
PlatformInput, ScrollDelta, ScrollWheelEvent, TouchPhase,
|
||||
};
|
||||
use cocoa::{
|
||||
appkit::{NSEvent, NSEventModifierFlags, NSEventPhase, NSEventType},
|
||||
base::{id, YES},
|
||||
foundation::NSString as _,
|
||||
};
|
||||
use core_graphics::{
|
||||
event::{CGEvent, CGEventFlags, CGKeyCode},
|
||||
@@ -15,7 +15,7 @@ use core_graphics::{
|
||||
use ctor::ctor;
|
||||
use metal::foreign_types::ForeignType as _;
|
||||
use objc::{class, msg_send, sel, sel_impl};
|
||||
use std::{borrow::Cow, mem, ptr};
|
||||
use std::{borrow::Cow, ffi::CStr, mem, os::raw::c_char, ptr};
|
||||
|
||||
const BACKSPACE_KEY: u16 = 0x7f;
|
||||
const SPACE_KEY: u16 = b' ' as u16;
|
||||
@@ -243,10 +243,11 @@ impl PlatformInput {
|
||||
unsafe fn parse_keystroke(native_event: id) -> Keystroke {
|
||||
use cocoa::appkit::*;
|
||||
|
||||
let mut chars_ignoring_modifiers = native_event
|
||||
.charactersIgnoringModifiers()
|
||||
.to_str()
|
||||
.to_string();
|
||||
let mut chars_ignoring_modifiers =
|
||||
CStr::from_ptr(native_event.charactersIgnoringModifiers().UTF8String() as *mut c_char)
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.to_string();
|
||||
let first_char = chars_ignoring_modifiers.chars().next().map(|ch| ch as u16);
|
||||
let modifiers = native_event.modifierFlags();
|
||||
|
||||
@@ -350,6 +351,9 @@ fn chars_for_modified_key(code: CGKeyCode, cmd: bool, shift: bool) -> String {
|
||||
|
||||
unsafe {
|
||||
let event: id = msg_send![class!(NSEvent), eventWithCGEvent: &*event];
|
||||
event.characters().to_str().to_string()
|
||||
CStr::from_ptr(event.characters().UTF8String())
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -826,6 +826,9 @@ impl Platform for MacPlatform {
|
||||
CursorStyle::ResizeDown => msg_send![class!(NSCursor), resizeDownCursor],
|
||||
CursorStyle::ResizeUpDown => msg_send![class!(NSCursor), resizeUpDownCursor],
|
||||
CursorStyle::ResizeRow => msg_send![class!(NSCursor), resizeUpDownCursor],
|
||||
CursorStyle::DisappearingItem => {
|
||||
msg_send![class!(NSCursor), disappearingItemCursor]
|
||||
}
|
||||
CursorStyle::IBeamCursorForVerticalLayout => {
|
||||
msg_send![class!(NSCursor), IBeamCursorForVerticalLayout]
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::{ns_string, renderer, MacDisplay, NSRange, NSStringExt};
|
||||
use super::{ns_string, renderer, MacDisplay, NSRange};
|
||||
use crate::{
|
||||
platform::PlatformInputHandler, point, px, size, AnyWindowHandle, Bounds, DevicePixels,
|
||||
DisplayLink, ExternalPaths, FileDropEvent, ForegroundExecutor, KeyDownEvent, Keystroke,
|
||||
@@ -39,6 +39,7 @@ use std::{
|
||||
ffi::{c_void, CStr},
|
||||
mem,
|
||||
ops::Range,
|
||||
os::raw::c_char,
|
||||
path::PathBuf,
|
||||
ptr::{self, NonNull},
|
||||
rc::Rc,
|
||||
@@ -310,7 +311,7 @@ unsafe fn build_window_class(name: &'static str, superclass: &Class) -> *const C
|
||||
decl.register()
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug)]
|
||||
#[allow(clippy::enum_variant_names)]
|
||||
enum ImeInput {
|
||||
InsertText(String, Option<Range<usize>>),
|
||||
@@ -340,7 +341,7 @@ struct MacWindowState {
|
||||
traffic_light_position: Option<Point<Pixels>>,
|
||||
previous_modifiers_changed_event: Option<PlatformInput>,
|
||||
// State tracking what the IME did after the last request
|
||||
last_ime_action: Option<ImeInput>,
|
||||
input_during_keydown: Option<SmallVec<[ImeInput; 1]>>,
|
||||
previous_keydown_inserted_text: Option<String>,
|
||||
external_files_dragged: bool,
|
||||
// Whether the next left-mouse click is also the focusing click.
|
||||
@@ -636,7 +637,7 @@ impl MacWindow {
|
||||
.as_ref()
|
||||
.and_then(|titlebar| titlebar.traffic_light_position),
|
||||
previous_modifiers_changed_event: None,
|
||||
last_ime_action: None,
|
||||
input_during_keydown: None,
|
||||
previous_keydown_inserted_text: None,
|
||||
external_files_dragged: false,
|
||||
first_mouse: false,
|
||||
@@ -657,7 +658,7 @@ impl MacWindow {
|
||||
.as_ref()
|
||||
.and_then(|t| t.title.as_ref().map(AsRef::as_ref))
|
||||
{
|
||||
window.set_title(title);
|
||||
native_window.setTitle_(NSString::alloc(nil).init_str(title));
|
||||
}
|
||||
|
||||
native_window.setMovable_(is_movable as BOOL);
|
||||
@@ -799,7 +800,7 @@ impl PlatformWindow for MacWindow {
|
||||
}
|
||||
}
|
||||
|
||||
fn display(&self) -> Option<Rc<dyn PlatformDisplay>> {
|
||||
fn display(&self) -> Rc<dyn PlatformDisplay> {
|
||||
unsafe {
|
||||
let screen = self.0.lock().native_window.screen();
|
||||
let device_description: id = msg_send![screen, deviceDescription];
|
||||
@@ -810,7 +811,7 @@ impl PlatformWindow for MacWindow {
|
||||
|
||||
let screen_number: u32 = msg_send![screen_number, unsignedIntValue];
|
||||
|
||||
Some(Rc::new(MacDisplay(screen_number)))
|
||||
Rc::new(MacDisplay(screen_number))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1191,22 +1192,14 @@ extern "C" fn handle_key_down(this: &Object, _: Sel, native_event: id) {
|
||||
}
|
||||
|
||||
// Things to test if you're modifying this method:
|
||||
// U.S. layout:
|
||||
// - The IME consumes characters like 'j' and 'k', which makes paging through `less` in
|
||||
// the terminal behave incorrectly by default. This behavior should be patched by our
|
||||
// IME integration
|
||||
// Brazilian layout:
|
||||
// - `" space` should create an unmarked quote
|
||||
// - `" space` should type a quote
|
||||
// - `" backspace` should delete the marked quote
|
||||
// - `" up` should insert a quote, unmark it, and move up one line
|
||||
// - `" cmd-down` should insert a quote, unmark it, and move to the end of the file
|
||||
// - `" up` should type the quote, unmark it, and move up one line
|
||||
// - `" cmd-down` should not leave a marked quote behind (it maybe should dispatch the key though?)
|
||||
// - `cmd-ctrl-space` and clicking on an emoji should type it
|
||||
// Czech (QWERTY) layout:
|
||||
// - in vim mode `option-4` should go to end of line (same as $)
|
||||
// Japanese (Romaji) layout:
|
||||
// - Triggering the IME composer (e.g. via typing 'a i' and then the left key), and then selecting
|
||||
// results of different length (e.g. kana -> kanji -> emoji -> back to kanji via the up and down keys)
|
||||
// should maintain the composing state in the editor
|
||||
extern "C" fn handle_key_event(this: &Object, native_event: id, key_equivalent: bool) -> BOOL {
|
||||
let window_state = unsafe { get_window_state(this) };
|
||||
let mut lock = window_state.as_ref().lock();
|
||||
@@ -1227,52 +1220,78 @@ extern "C" fn handle_key_event(this: &Object, native_event: id, key_equivalent:
|
||||
|
||||
let keydown = event.keystroke.clone();
|
||||
let fn_modifier = keydown.modifiers.function;
|
||||
// Ignore events from held-down keys after some of the initially-pressed keys
|
||||
// were released.
|
||||
if event.is_held {
|
||||
if lock.last_fresh_keydown.as_ref() != Some(&keydown) {
|
||||
return YES;
|
||||
}
|
||||
} else {
|
||||
lock.last_fresh_keydown = Some(keydown.clone());
|
||||
}
|
||||
|
||||
drop(lock);
|
||||
|
||||
// Send the event to the input context for IME handling, unless the `fn` modifier is
|
||||
// being pressed. This will call back into other functions like `insert_text`, etc.
|
||||
// Note that the IME expects it's actions to be applied immediately, and buffering them
|
||||
// can break pre-edit
|
||||
// being pressed.
|
||||
// this will call back into `insert_text`, etc.
|
||||
if !fn_modifier {
|
||||
unsafe {
|
||||
let input_context: id = msg_send![this, inputContext];
|
||||
let _: BOOL = msg_send![input_context, handleEvent: native_event];
|
||||
}
|
||||
}
|
||||
let mut lock = window_state.lock();
|
||||
|
||||
println!("******************************************");
|
||||
///// THE PROBLEM AREA ///////
|
||||
// Ignore events from held-down keys after some of the initially-pressed keys
|
||||
// were released.
|
||||
if dbg!(event.is_held) {
|
||||
if dbg!(lock.last_fresh_keydown.as_ref()) != Some(&keydown) {
|
||||
return YES;
|
||||
}
|
||||
} else {
|
||||
//????
|
||||
lock.last_fresh_keydown = Some(dbg!(keydown.clone()));
|
||||
}
|
||||
lock.input_during_keydown = Some(SmallVec::new());
|
||||
///// THE PROBLEM AREA ///////
|
||||
|
||||
// Send the event to the input context for IME handling, unless the `fn` modifier is
|
||||
// being pressed.
|
||||
// this will call back into `insert_text`, etc.
|
||||
|
||||
let mut handled = false;
|
||||
let mut lock = window_state.lock();
|
||||
let previous_keydown_inserted_text = lock.previous_keydown_inserted_text.take();
|
||||
let mut last_ime = lock.last_ime_action.take();
|
||||
|
||||
let mut input_during_keydown = lock.input_during_keydown.take().unwrap();
|
||||
dbg!(&input_during_keydown);
|
||||
let mut callback = lock.event_callback.take();
|
||||
drop(lock);
|
||||
|
||||
let last_ime = input_during_keydown.pop();
|
||||
// on a brazilian keyboard typing `"` and then hitting `up` will cause two IME
|
||||
// events, one to unmark the quote, and one to send the up arrow.
|
||||
for ime in input_during_keydown {
|
||||
send_to_input_handler(this, ime);
|
||||
}
|
||||
|
||||
let is_composing =
|
||||
with_input_handler(this, |input_handler| input_handler.marked_text_range())
|
||||
.flatten()
|
||||
.is_some();
|
||||
|
||||
dbg!(is_composing, &last_ime);
|
||||
|
||||
if let Some(ime) = last_ime {
|
||||
// Problem area
|
||||
if let ImeInput::InsertText(text, _) = &ime {
|
||||
if !is_composing {
|
||||
window_state.lock().previous_keydown_inserted_text = Some(text.clone());
|
||||
if let Some(callback) = callback.as_mut() {
|
||||
event.keystroke.ime_key = Some(text.clone());
|
||||
let _ = callback(PlatformInput::KeyDown(event));
|
||||
handled = !callback(PlatformInput::KeyDown(event)).propagate;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handled = true;
|
||||
if !handled {
|
||||
handled = true;
|
||||
send_to_input_handler(this, ime);
|
||||
}
|
||||
// Problem area
|
||||
} else if !is_composing {
|
||||
let is_held = event.is_held;
|
||||
|
||||
@@ -1653,21 +1672,27 @@ extern "C" fn valid_attributes_for_marked_text(_: &Object, _: Sel) -> id {
|
||||
}
|
||||
|
||||
extern "C" fn has_marked_text(this: &Object, _: Sel) -> BOOL {
|
||||
with_input_handler(this, |input_handler| input_handler.marked_text_range())
|
||||
.flatten()
|
||||
.is_some() as BOOL
|
||||
dbg!("has marked range");
|
||||
with_input_handler(
|
||||
this,
|
||||
|input_handler| dbg!(input_handler.marked_text_range()),
|
||||
)
|
||||
.flatten()
|
||||
.is_some() as BOOL
|
||||
}
|
||||
|
||||
extern "C" fn marked_range(this: &Object, _: Sel) -> NSRange {
|
||||
dbg!("querying marked range");
|
||||
with_input_handler(this, |input_handler| input_handler.marked_text_range())
|
||||
.flatten()
|
||||
.map_or(NSRange::invalid(), |range| range.into())
|
||||
.map_or(NSRange::invalid(), |range| dbg!(range).into())
|
||||
}
|
||||
|
||||
extern "C" fn selected_range(this: &Object, _: Sel) -> NSRange {
|
||||
dbg!("asking for sel range");
|
||||
with_input_handler(this, |input_handler| input_handler.selected_text_range())
|
||||
.flatten()
|
||||
.map_or(NSRange::invalid(), |range| range.into())
|
||||
.map_or(NSRange::invalid(), |range| dbg!(range).into())
|
||||
}
|
||||
|
||||
extern "C" fn first_rect_for_character_range(
|
||||
@@ -1709,8 +1734,9 @@ extern "C" fn insert_text(this: &Object, _: Sel, text: id, replacement_range: NS
|
||||
} else {
|
||||
text
|
||||
};
|
||||
|
||||
let text = text.to_str();
|
||||
let text = CStr::from_ptr(text.UTF8String() as *mut c_char)
|
||||
.to_str()
|
||||
.unwrap();
|
||||
let replacement_range = replacement_range.to_range();
|
||||
send_to_input_handler(
|
||||
this,
|
||||
@@ -1736,7 +1762,9 @@ extern "C" fn set_marked_text(
|
||||
};
|
||||
let selected_range = selected_range.to_range();
|
||||
let replacement_range = replacement_range.to_range();
|
||||
let text = text.to_str();
|
||||
let text = CStr::from_ptr(text.UTF8String() as *mut c_char)
|
||||
.to_str()
|
||||
.unwrap();
|
||||
|
||||
send_to_input_handler(
|
||||
this,
|
||||
@@ -1925,8 +1953,10 @@ fn send_to_input_handler(window: &Object, ime: ImeInput) {
|
||||
unsafe {
|
||||
let window_state = get_window_state(window);
|
||||
let mut lock = window_state.lock();
|
||||
|
||||
lock.last_ime_action = Some(ime.clone());
|
||||
if let Some(ime_input) = lock.input_during_keydown.as_mut() {
|
||||
ime_input.push(ime);
|
||||
return;
|
||||
}
|
||||
if let Some(mut input_handler) = lock.input_handler.take() {
|
||||
drop(lock);
|
||||
match ime {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
mod dispatcher;
|
||||
mod display;
|
||||
mod platform;
|
||||
mod text_system;
|
||||
mod window;
|
||||
|
||||
pub(crate) use dispatcher::*;
|
||||
|
||||
50
crates/gpui/src/platform/test/text_system.rs
Normal file
50
crates/gpui/src/platform/test/text_system.rs
Normal file
@@ -0,0 +1,50 @@
|
||||
use crate::{
|
||||
Bounds, DevicePixels, Font, FontId, FontMetrics, FontRun, GlyphId, LineLayout, Pixels,
|
||||
PlatformTextSystem, RenderGlyphParams, Size,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use std::borrow::Cow;
|
||||
|
||||
pub(crate) struct TestTextSystem {}
|
||||
|
||||
// todo(linux)
|
||||
#[allow(unused)]
|
||||
impl PlatformTextSystem for TestTextSystem {
|
||||
fn add_fonts(&self, fonts: Vec<Cow<'static, [u8]>>) -> Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn all_font_names(&self) -> Vec<String> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn all_font_families(&self) -> Vec<String> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn font_id(&self, descriptor: &Font) -> Result<FontId> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn font_metrics(&self, font_id: FontId) -> FontMetrics {
|
||||
unimplemented!()
|
||||
}
|
||||
fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Bounds<f32>> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn glyph_raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn rasterize_glyph(
|
||||
&self,
|
||||
params: &RenderGlyphParams,
|
||||
raster_bounds: Bounds<DevicePixels>,
|
||||
) -> Result<(Size<DevicePixels>, Vec<u8>)> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> LineLayout {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
@@ -132,8 +132,8 @@ impl PlatformWindow for TestWindow {
|
||||
WindowAppearance::Light
|
||||
}
|
||||
|
||||
fn display(&self) -> Option<std::rc::Rc<dyn crate::PlatformDisplay>> {
|
||||
Some(self.0.lock().display.clone())
|
||||
fn display(&self) -> std::rc::Rc<dyn crate::PlatformDisplay> {
|
||||
self.0.lock().display.clone()
|
||||
}
|
||||
|
||||
fn mouse_position(&self) -> Point<Pixels> {
|
||||
|
||||
@@ -379,8 +379,8 @@ impl PlatformWindow for WindowsWindow {
|
||||
WindowAppearance::Dark
|
||||
}
|
||||
|
||||
fn display(&self) -> Option<Rc<dyn PlatformDisplay>> {
|
||||
Some(Rc::new(self.0.state.borrow().display))
|
||||
fn display(&self) -> Rc<dyn PlatformDisplay> {
|
||||
Rc::new(self.0.state.borrow().display)
|
||||
}
|
||||
|
||||
fn mouse_position(&self) -> Point<Pixels> {
|
||||
|
||||
@@ -488,7 +488,7 @@ pub struct Window {
|
||||
pub(crate) handle: AnyWindowHandle,
|
||||
pub(crate) removed: bool,
|
||||
pub(crate) platform_window: Box<dyn PlatformWindow>,
|
||||
display_id: Option<DisplayId>,
|
||||
display_id: DisplayId,
|
||||
sprite_atlas: Arc<dyn PlatformAtlas>,
|
||||
text_system: Arc<WindowTextSystem>,
|
||||
rem_size: Pixels,
|
||||
@@ -634,7 +634,7 @@ impl Window {
|
||||
window_background,
|
||||
},
|
||||
);
|
||||
let display_id = platform_window.display().map(|display| display.id());
|
||||
let display_id = platform_window.display().id();
|
||||
let sprite_atlas = platform_window.sprite_atlas();
|
||||
let mouse_position = platform_window.mouse_position();
|
||||
let modifiers = platform_window.modifiers();
|
||||
@@ -1099,12 +1099,7 @@ impl<'a> WindowContext<'a> {
|
||||
fn bounds_changed(&mut self) {
|
||||
self.window.scale_factor = self.window.platform_window.scale_factor();
|
||||
self.window.viewport_size = self.window.platform_window.content_size();
|
||||
self.window.display_id = self
|
||||
.window
|
||||
.platform_window
|
||||
.display()
|
||||
.map(|display| display.id());
|
||||
|
||||
self.window.display_id = self.window.platform_window.display().id();
|
||||
self.refresh();
|
||||
|
||||
self.window
|
||||
@@ -1196,7 +1191,7 @@ impl<'a> WindowContext<'a> {
|
||||
self.platform
|
||||
.displays()
|
||||
.into_iter()
|
||||
.find(|display| Some(display.id()) == self.window.display_id)
|
||||
.find(|display| display.id() == self.window.display_id)
|
||||
}
|
||||
|
||||
/// Show the platform character palette.
|
||||
|
||||
@@ -1,135 +0,0 @@
|
||||
use crate::html_element::HtmlElement;
|
||||
use crate::markdown_writer::{HandleTag, MarkdownWriter, StartTagOutcome};
|
||||
|
||||
pub struct ParagraphHandler;
|
||||
|
||||
impl HandleTag for ParagraphHandler {
|
||||
fn should_handle(&self, _tag: &str) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn handle_tag_start(
|
||||
&mut self,
|
||||
tag: &HtmlElement,
|
||||
writer: &mut MarkdownWriter,
|
||||
) -> StartTagOutcome {
|
||||
if tag.is_inline() && writer.is_inside("p") {
|
||||
if let Some(parent) = writer.current_element_stack().iter().last() {
|
||||
if !parent.is_inline() {
|
||||
if !(writer.markdown.ends_with(' ') || writer.markdown.ends_with('\n')) {
|
||||
writer.push_str(" ");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match tag.tag.as_str() {
|
||||
"p" => writer.push_blank_line(),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
StartTagOutcome::Continue
|
||||
}
|
||||
}
|
||||
|
||||
pub struct HeadingHandler;
|
||||
|
||||
impl HandleTag for HeadingHandler {
|
||||
fn should_handle(&self, tag: &str) -> bool {
|
||||
match tag {
|
||||
"h1" | "h2" | "h3" | "h4" | "h5" | "h6" => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_tag_start(
|
||||
&mut self,
|
||||
tag: &HtmlElement,
|
||||
writer: &mut MarkdownWriter,
|
||||
) -> StartTagOutcome {
|
||||
match tag.tag.as_str() {
|
||||
"h1" => writer.push_str("\n\n# "),
|
||||
"h2" => writer.push_str("\n\n## "),
|
||||
"h3" => writer.push_str("\n\n### "),
|
||||
"h4" => writer.push_str("\n\n#### "),
|
||||
"h5" => writer.push_str("\n\n##### "),
|
||||
"h6" => writer.push_str("\n\n###### "),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
StartTagOutcome::Continue
|
||||
}
|
||||
|
||||
fn handle_tag_end(&mut self, tag: &HtmlElement, writer: &mut MarkdownWriter) {
|
||||
match tag.tag.as_str() {
|
||||
"h1" | "h2" | "h3" | "h4" | "h5" | "h6" => writer.push_blank_line(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ListHandler;
|
||||
|
||||
impl HandleTag for ListHandler {
|
||||
fn should_handle(&self, tag: &str) -> bool {
|
||||
match tag {
|
||||
"ul" | "ol" | "li" => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_tag_start(
|
||||
&mut self,
|
||||
tag: &HtmlElement,
|
||||
writer: &mut MarkdownWriter,
|
||||
) -> StartTagOutcome {
|
||||
match tag.tag.as_str() {
|
||||
"ul" | "ol" => writer.push_newline(),
|
||||
"li" => writer.push_str("- "),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
StartTagOutcome::Continue
|
||||
}
|
||||
|
||||
fn handle_tag_end(&mut self, tag: &HtmlElement, writer: &mut MarkdownWriter) {
|
||||
match tag.tag.as_str() {
|
||||
"ul" | "ol" => writer.push_newline(),
|
||||
"li" => writer.push_newline(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct StyledTextHandler;
|
||||
|
||||
impl HandleTag for StyledTextHandler {
|
||||
fn should_handle(&self, tag: &str) -> bool {
|
||||
match tag {
|
||||
"strong" | "em" => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_tag_start(
|
||||
&mut self,
|
||||
tag: &HtmlElement,
|
||||
writer: &mut MarkdownWriter,
|
||||
) -> StartTagOutcome {
|
||||
match tag.tag.as_str() {
|
||||
"strong" => writer.push_str("**"),
|
||||
"em" => writer.push_str("_"),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
StartTagOutcome::Continue
|
||||
}
|
||||
|
||||
fn handle_tag_end(&mut self, tag: &HtmlElement, writer: &mut MarkdownWriter) {
|
||||
match tag.tag.as_str() {
|
||||
"strong" => writer.push_str("**"),
|
||||
"em" => writer.push_str("_"),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,198 +0,0 @@
|
||||
use std::collections::VecDeque;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use anyhow::Result;
|
||||
use markup5ever_rcdom::{Handle, NodeData};
|
||||
use regex::Regex;
|
||||
|
||||
use crate::html_element::HtmlElement;
|
||||
|
||||
fn empty_line_regex() -> &'static Regex {
|
||||
static REGEX: OnceLock<Regex> = OnceLock::new();
|
||||
REGEX.get_or_init(|| Regex::new(r"^\s*$").unwrap())
|
||||
}
|
||||
|
||||
fn more_than_three_newlines_regex() -> &'static Regex {
|
||||
static REGEX: OnceLock<Regex> = OnceLock::new();
|
||||
REGEX.get_or_init(|| Regex::new(r"\n{3,}").unwrap())
|
||||
}
|
||||
|
||||
pub enum StartTagOutcome {
|
||||
Continue,
|
||||
Skip,
|
||||
}
|
||||
|
||||
pub struct MarkdownWriter {
|
||||
current_element_stack: VecDeque<HtmlElement>,
|
||||
pub(crate) markdown: String,
|
||||
}
|
||||
|
||||
impl MarkdownWriter {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
current_element_stack: VecDeque::new(),
|
||||
markdown: String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn current_element_stack(&self) -> &VecDeque<HtmlElement> {
|
||||
&self.current_element_stack
|
||||
}
|
||||
|
||||
pub fn is_inside(&self, tag: &str) -> bool {
|
||||
self.current_element_stack
|
||||
.iter()
|
||||
.any(|parent_element| parent_element.tag == tag)
|
||||
}
|
||||
|
||||
/// Appends the given string slice onto the end of the Markdown output.
|
||||
pub fn push_str(&mut self, str: &str) {
|
||||
self.markdown.push_str(str);
|
||||
}
|
||||
|
||||
/// Appends a newline to the end of the Markdown output.
|
||||
pub fn push_newline(&mut self) {
|
||||
self.push_str("\n");
|
||||
}
|
||||
|
||||
/// Appends a blank line to the end of the Markdown output.
|
||||
pub fn push_blank_line(&mut self) {
|
||||
self.push_str("\n\n");
|
||||
}
|
||||
|
||||
pub fn run(
|
||||
mut self,
|
||||
root_node: &Handle,
|
||||
mut handlers: Vec<Box<dyn HandleTag>>,
|
||||
) -> Result<String> {
|
||||
self.visit_node(&root_node, &mut handlers)?;
|
||||
Ok(Self::prettify_markdown(self.markdown))
|
||||
}
|
||||
|
||||
fn prettify_markdown(markdown: String) -> String {
|
||||
let markdown = empty_line_regex().replace_all(&markdown, "");
|
||||
let markdown = more_than_three_newlines_regex().replace_all(&markdown, "\n\n");
|
||||
|
||||
markdown.trim().to_string()
|
||||
}
|
||||
|
||||
fn visit_node(&mut self, node: &Handle, handlers: &mut [Box<dyn HandleTag>]) -> Result<()> {
|
||||
let mut current_element = None;
|
||||
|
||||
match node.data {
|
||||
NodeData::Document
|
||||
| NodeData::Doctype { .. }
|
||||
| NodeData::ProcessingInstruction { .. }
|
||||
| NodeData::Comment { .. } => {
|
||||
// Currently left unimplemented, as we're not interested in this data
|
||||
// at this time.
|
||||
}
|
||||
NodeData::Element {
|
||||
ref name,
|
||||
ref attrs,
|
||||
..
|
||||
} => {
|
||||
let tag_name = name.local.to_string();
|
||||
if !tag_name.is_empty() {
|
||||
current_element = Some(HtmlElement {
|
||||
tag: tag_name,
|
||||
attrs: attrs.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
NodeData::Text { ref contents } => {
|
||||
let text = contents.borrow().to_string();
|
||||
self.visit_text(text, handlers)?;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(current_element) = current_element.as_ref() {
|
||||
match self.start_tag(¤t_element, handlers) {
|
||||
StartTagOutcome::Continue => {}
|
||||
StartTagOutcome::Skip => return Ok(()),
|
||||
}
|
||||
|
||||
self.current_element_stack
|
||||
.push_back(current_element.clone());
|
||||
}
|
||||
|
||||
for child in node.children.borrow().iter() {
|
||||
self.visit_node(child, handlers)?;
|
||||
}
|
||||
|
||||
if let Some(current_element) = current_element {
|
||||
self.current_element_stack.pop_back();
|
||||
self.end_tag(¤t_element, handlers);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn start_tag(
|
||||
&mut self,
|
||||
tag: &HtmlElement,
|
||||
handlers: &mut [Box<dyn HandleTag>],
|
||||
) -> StartTagOutcome {
|
||||
for handler in handlers {
|
||||
if handler.should_handle(tag.tag.as_str()) {
|
||||
match handler.handle_tag_start(tag, self) {
|
||||
StartTagOutcome::Continue => {}
|
||||
StartTagOutcome::Skip => return StartTagOutcome::Skip,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StartTagOutcome::Continue
|
||||
}
|
||||
|
||||
fn end_tag(&mut self, tag: &HtmlElement, handlers: &mut [Box<dyn HandleTag>]) {
|
||||
for handler in handlers {
|
||||
if handler.should_handle(tag.tag.as_str()) {
|
||||
handler.handle_tag_end(tag, self);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_text(&mut self, text: String, handlers: &mut [Box<dyn HandleTag>]) -> Result<()> {
|
||||
for handler in handlers {
|
||||
match handler.handle_text(&text, self) {
|
||||
HandlerOutcome::Handled => return Ok(()),
|
||||
HandlerOutcome::NoOp => {}
|
||||
}
|
||||
}
|
||||
|
||||
let text = text
|
||||
.trim_matches(|char| char == '\n' || char == '\r')
|
||||
.replace('\n', " ");
|
||||
|
||||
self.push_str(&text);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub enum HandlerOutcome {
|
||||
Handled,
|
||||
NoOp,
|
||||
}
|
||||
|
||||
pub trait HandleTag {
|
||||
/// Returns whether this handler should handle the given tag.
|
||||
fn should_handle(&self, tag: &str) -> bool;
|
||||
|
||||
/// Handles the start of the given tag.
|
||||
fn handle_tag_start(
|
||||
&mut self,
|
||||
_tag: &HtmlElement,
|
||||
_writer: &mut MarkdownWriter,
|
||||
) -> StartTagOutcome {
|
||||
StartTagOutcome::Continue
|
||||
}
|
||||
|
||||
/// Handles the end of the given tag.
|
||||
fn handle_tag_end(&mut self, _tag: &HtmlElement, _writer: &mut MarkdownWriter) {}
|
||||
|
||||
fn handle_text(&mut self, _text: &str, _writer: &mut MarkdownWriter) -> HandlerOutcome {
|
||||
HandlerOutcome::NoOp
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
pub mod rustdoc;
|
||||
@@ -1,286 +0,0 @@
|
||||
use crate::html_element::HtmlElement;
|
||||
use crate::markdown_writer::{HandleTag, HandlerOutcome, MarkdownWriter, StartTagOutcome};
|
||||
|
||||
pub struct RustdocHeadingHandler;
|
||||
|
||||
impl HandleTag for RustdocHeadingHandler {
|
||||
fn should_handle(&self, _tag: &str) -> bool {
|
||||
// We're only handling text, so we don't need to visit any tags.
|
||||
false
|
||||
}
|
||||
|
||||
fn handle_text(&mut self, text: &str, writer: &mut MarkdownWriter) -> HandlerOutcome {
|
||||
if writer.is_inside("h1")
|
||||
|| writer.is_inside("h2")
|
||||
|| writer.is_inside("h3")
|
||||
|| writer.is_inside("h4")
|
||||
|| writer.is_inside("h5")
|
||||
|| writer.is_inside("h6")
|
||||
{
|
||||
let text = text
|
||||
.trim_matches(|char| char == '\n' || char == '\r' || char == '§')
|
||||
.replace('\n', " ");
|
||||
writer.push_str(&text);
|
||||
|
||||
return HandlerOutcome::Handled;
|
||||
}
|
||||
|
||||
HandlerOutcome::NoOp
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RustdocCodeHandler;
|
||||
|
||||
impl HandleTag for RustdocCodeHandler {
|
||||
fn should_handle(&self, tag: &str) -> bool {
|
||||
match tag {
|
||||
"pre" | "code" => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_tag_start(
|
||||
&mut self,
|
||||
tag: &HtmlElement,
|
||||
writer: &mut MarkdownWriter,
|
||||
) -> StartTagOutcome {
|
||||
match tag.tag.as_str() {
|
||||
"code" => {
|
||||
if !writer.is_inside("pre") {
|
||||
writer.push_str("`");
|
||||
}
|
||||
}
|
||||
"pre" => {
|
||||
let classes = tag.classes();
|
||||
let is_rust = classes.iter().any(|class| class == "rust");
|
||||
let language = is_rust
|
||||
.then(|| "rs")
|
||||
.or_else(|| {
|
||||
classes.iter().find_map(|class| {
|
||||
if let Some((_, language)) = class.split_once("language-") {
|
||||
Some(language.trim())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
})
|
||||
.unwrap_or("");
|
||||
|
||||
writer.push_str(&format!("\n\n```{language}\n"));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
StartTagOutcome::Continue
|
||||
}
|
||||
|
||||
fn handle_tag_end(&mut self, tag: &HtmlElement, writer: &mut MarkdownWriter) {
|
||||
match tag.tag.as_str() {
|
||||
"code" => {
|
||||
if !writer.is_inside("pre") {
|
||||
writer.push_str("`");
|
||||
}
|
||||
}
|
||||
"pre" => writer.push_str("\n```\n"),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_text(&mut self, text: &str, writer: &mut MarkdownWriter) -> HandlerOutcome {
|
||||
if writer.is_inside("pre") {
|
||||
writer.push_str(&text);
|
||||
return HandlerOutcome::Handled;
|
||||
}
|
||||
|
||||
HandlerOutcome::NoOp
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RustdocTableHandler {
|
||||
/// The number of columns in the current `<table>`.
|
||||
current_table_columns: usize,
|
||||
is_first_th: bool,
|
||||
is_first_td: bool,
|
||||
}
|
||||
|
||||
impl RustdocTableHandler {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
current_table_columns: 0,
|
||||
is_first_th: true,
|
||||
is_first_td: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl HandleTag for RustdocTableHandler {
|
||||
fn should_handle(&self, tag: &str) -> bool {
|
||||
match tag {
|
||||
"table" | "thead" | "tbody" | "tr" | "th" | "td" => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_tag_start(
|
||||
&mut self,
|
||||
tag: &HtmlElement,
|
||||
writer: &mut MarkdownWriter,
|
||||
) -> StartTagOutcome {
|
||||
match tag.tag.as_str() {
|
||||
"thead" => writer.push_blank_line(),
|
||||
"tr" => writer.push_newline(),
|
||||
"th" => {
|
||||
self.current_table_columns += 1;
|
||||
if self.is_first_th {
|
||||
self.is_first_th = false;
|
||||
} else {
|
||||
writer.push_str(" ");
|
||||
}
|
||||
writer.push_str("| ");
|
||||
}
|
||||
"td" => {
|
||||
if self.is_first_td {
|
||||
self.is_first_td = false;
|
||||
} else {
|
||||
writer.push_str(" ");
|
||||
}
|
||||
writer.push_str("| ");
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
StartTagOutcome::Continue
|
||||
}
|
||||
|
||||
fn handle_tag_end(&mut self, tag: &HtmlElement, writer: &mut MarkdownWriter) {
|
||||
match tag.tag.as_str() {
|
||||
"thead" => {
|
||||
writer.push_newline();
|
||||
for ix in 0..self.current_table_columns {
|
||||
if ix > 0 {
|
||||
writer.push_str(" ");
|
||||
}
|
||||
writer.push_str("| ---");
|
||||
}
|
||||
writer.push_str(" |");
|
||||
self.is_first_th = true;
|
||||
}
|
||||
"tr" => {
|
||||
writer.push_str(" |");
|
||||
self.is_first_td = true;
|
||||
}
|
||||
"table" => {
|
||||
self.current_table_columns = 0;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const RUSTDOC_ITEM_NAME_CLASS: &str = "item-name";
|
||||
|
||||
pub struct RustdocItemHandler;
|
||||
|
||||
impl RustdocItemHandler {
|
||||
/// Returns whether we're currently inside of an `.item-name` element, which
|
||||
/// rustdoc uses to display Rust items in a list.
|
||||
fn is_inside_item_name(writer: &MarkdownWriter) -> bool {
|
||||
writer
|
||||
.current_element_stack()
|
||||
.iter()
|
||||
.any(|element| element.has_class(RUSTDOC_ITEM_NAME_CLASS))
|
||||
}
|
||||
}
|
||||
|
||||
impl HandleTag for RustdocItemHandler {
|
||||
fn should_handle(&self, tag: &str) -> bool {
|
||||
match tag {
|
||||
"div" | "span" => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_tag_start(
|
||||
&mut self,
|
||||
tag: &HtmlElement,
|
||||
writer: &mut MarkdownWriter,
|
||||
) -> StartTagOutcome {
|
||||
match tag.tag.as_str() {
|
||||
"div" | "span" => {
|
||||
if Self::is_inside_item_name(writer) && tag.has_class("stab") {
|
||||
writer.push_str(" [");
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
StartTagOutcome::Continue
|
||||
}
|
||||
|
||||
fn handle_tag_end(&mut self, tag: &HtmlElement, writer: &mut MarkdownWriter) {
|
||||
match tag.tag.as_str() {
|
||||
"div" | "span" => {
|
||||
if tag.has_class(RUSTDOC_ITEM_NAME_CLASS) {
|
||||
writer.push_str(": ");
|
||||
}
|
||||
|
||||
if Self::is_inside_item_name(writer) && tag.has_class("stab") {
|
||||
writer.push_str("]");
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_text(&mut self, text: &str, writer: &mut MarkdownWriter) -> HandlerOutcome {
|
||||
if Self::is_inside_item_name(writer)
|
||||
&& !writer.is_inside("span")
|
||||
&& !writer.is_inside("code")
|
||||
{
|
||||
writer.push_str(&format!("`{text}`"));
|
||||
return HandlerOutcome::Handled;
|
||||
}
|
||||
|
||||
HandlerOutcome::NoOp
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RustdocChromeRemover;
|
||||
|
||||
impl HandleTag for RustdocChromeRemover {
|
||||
fn should_handle(&self, tag: &str) -> bool {
|
||||
match tag {
|
||||
"head" | "script" | "nav" | "summary" | "button" | "div" | "span" => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_tag_start(
|
||||
&mut self,
|
||||
tag: &HtmlElement,
|
||||
_writer: &mut MarkdownWriter,
|
||||
) -> StartTagOutcome {
|
||||
match tag.tag.as_str() {
|
||||
"head" | "script" | "nav" => return StartTagOutcome::Skip,
|
||||
"summary" => {
|
||||
if tag.has_class("hideme") {
|
||||
return StartTagOutcome::Skip;
|
||||
}
|
||||
}
|
||||
"button" => {
|
||||
if tag.attr("id").as_deref() == Some("copy-path") {
|
||||
return StartTagOutcome::Skip;
|
||||
}
|
||||
}
|
||||
"div" | "span" => {
|
||||
let classes_to_skip = ["nav-container", "sidebar-elems", "out-of-band"];
|
||||
if tag.has_any_classes(&classes_to_skip) {
|
||||
return StartTagOutcome::Skip;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
StartTagOutcome::Continue
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ pub use crate::{
|
||||
};
|
||||
use crate::{
|
||||
diagnostic_set::{DiagnosticEntry, DiagnosticGroup},
|
||||
language_settings::{language_settings, IndentGuideSettings, LanguageSettings},
|
||||
language_settings::{language_settings, LanguageSettings},
|
||||
markdown::parse_markdown,
|
||||
outline::OutlineItem,
|
||||
syntax_map::{
|
||||
@@ -542,10 +542,25 @@ pub struct IndentGuide {
|
||||
pub end_row: BufferRow,
|
||||
pub depth: u32,
|
||||
pub tab_size: u32,
|
||||
pub settings: IndentGuideSettings,
|
||||
}
|
||||
|
||||
impl IndentGuide {
|
||||
pub fn new(
|
||||
buffer_id: BufferId,
|
||||
start_row: BufferRow,
|
||||
end_row: BufferRow,
|
||||
depth: u32,
|
||||
tab_size: u32,
|
||||
) -> Self {
|
||||
Self {
|
||||
buffer_id,
|
||||
start_row,
|
||||
end_row,
|
||||
depth,
|
||||
tab_size,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn indent_level(&self) -> u32 {
|
||||
self.depth * self.tab_size
|
||||
}
|
||||
@@ -3136,15 +3151,9 @@ impl BufferSnapshot {
|
||||
pub fn indent_guides_in_range(
|
||||
&self,
|
||||
range: Range<Anchor>,
|
||||
ignore_disabled_for_language: bool,
|
||||
cx: &AppContext,
|
||||
) -> Vec<IndentGuide> {
|
||||
let language_settings = language_settings(self.language(), self.file.as_ref(), cx);
|
||||
let settings = language_settings.indent_guides;
|
||||
if !ignore_disabled_for_language && !settings.enabled {
|
||||
return Vec::new();
|
||||
}
|
||||
let tab_size = language_settings.tab_size.get() as u32;
|
||||
let tab_size = language_settings(self.language(), None, cx).tab_size.get() as u32;
|
||||
|
||||
let start_row = range.start.to_point(self).row;
|
||||
let end_row = range.end.to_point(self).row;
|
||||
@@ -3225,7 +3234,6 @@ impl BufferSnapshot {
|
||||
end_row: last_row,
|
||||
depth: next_depth,
|
||||
tab_size,
|
||||
settings,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use editor::{scroll::Autoscroll, Anchor, Editor, ExcerptId};
|
||||
use gpui::{
|
||||
actions, div, rems, uniform_list, AnyElement, AppContext, Div, EventEmitter, FocusHandle,
|
||||
FocusableView, Hsla, InteractiveElement, IntoElement, Model, MouseButton, MouseDownEvent,
|
||||
MouseMoveEvent, ParentElement, Render, Styled, UniformListScrollHandle, View, ViewContext,
|
||||
VisualContext, WeakView, WindowContext,
|
||||
actions, canvas, div, rems, uniform_list, AnyElement, AppContext, Div, EventEmitter,
|
||||
FocusHandle, FocusableView, Hsla, InteractiveElement, IntoElement, Model, MouseButton,
|
||||
MouseDownEvent, MouseMoveEvent, ParentElement, Render, Styled, UniformListScrollHandle, View,
|
||||
ViewContext, VisualContext, WeakView, WindowContext,
|
||||
};
|
||||
use language::{Buffer, OwnedSyntaxLayer};
|
||||
use std::{mem, ops::Range};
|
||||
@@ -281,7 +281,7 @@ impl Render for SyntaxTreeView {
|
||||
.and_then(|buffer| buffer.active_layer.as_ref())
|
||||
{
|
||||
let layer = layer.clone();
|
||||
rendered = rendered.child(uniform_list(
|
||||
let mut list = uniform_list(
|
||||
cx.view().clone(),
|
||||
"SyntaxTreeView",
|
||||
layer.node().descendant_count(),
|
||||
@@ -360,7 +360,18 @@ impl Render for SyntaxTreeView {
|
||||
)
|
||||
.size_full()
|
||||
.track_scroll(self.list_scroll_handle.clone())
|
||||
.text_bg(cx.theme().colors().background).into_any_element());
|
||||
.text_bg(cx.theme().colors().background).into_any_element();
|
||||
|
||||
rendered = rendered.child(
|
||||
canvas(
|
||||
move |bounds, cx| {
|
||||
list.prepaint_as_root(bounds.origin, bounds.size.into(), cx);
|
||||
list
|
||||
},
|
||||
|_, mut list, cx| list.paint(cx),
|
||||
)
|
||||
.size_full(),
|
||||
);
|
||||
}
|
||||
|
||||
rendered
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
; Properties
|
||||
|
||||
(property_identifier) @property
|
||||
(shorthand_property_identifier) @property
|
||||
(shorthand_property_identifier_pattern) @property
|
||||
|
||||
; Function and method calls
|
||||
|
||||
|
||||
@@ -60,14 +60,3 @@
|
||||
(accessibility_modifier)
|
||||
]* @context
|
||||
name: (_) @name) @item
|
||||
|
||||
; Add support for (node:test, bun:test and Jest) runnable
|
||||
(call_expression
|
||||
function: (_) @context
|
||||
(#any-of? @context "it" "test" "describe")
|
||||
arguments: (
|
||||
arguments . (string
|
||||
(string_fragment) @name
|
||||
)
|
||||
)
|
||||
) @item
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
; Add support for (node:test, bun:test and Jest) runnable
|
||||
; Function expression that has `it`, `test` or `describe` as the function name
|
||||
(
|
||||
(call_expression
|
||||
function: (_) @_name
|
||||
(#any-of? @_name "it" "test" "describe")
|
||||
arguments: (
|
||||
arguments . (string
|
||||
(string_fragment) @run
|
||||
)
|
||||
)
|
||||
) @js-test
|
||||
(#set! tag js-test)
|
||||
)
|
||||
@@ -15,7 +15,6 @@ use std::{
|
||||
any::Any,
|
||||
ffi::OsString,
|
||||
path::{Path, PathBuf},
|
||||
str::FromStr,
|
||||
sync::{Arc, OnceLock},
|
||||
};
|
||||
use task::{TaskTemplate, TaskTemplates, VariableName};
|
||||
@@ -23,8 +22,6 @@ use util::{maybe, paths, ResultExt};
|
||||
|
||||
const SERVER_PATH: &str = "node_modules/vscode-json-languageserver/bin/vscode-json-languageserver";
|
||||
|
||||
// Origin: https://github.com/SchemaStore/schemastore
|
||||
const TSCONFIG_SCHEMA: &str = include_str!("json/schemas/tsconfig.json");
|
||||
pub(super) fn json_task_context() -> ContextProviderWithTasks {
|
||||
ContextProviderWithTasks::new(TaskTemplates(vec![
|
||||
TaskTemplate {
|
||||
@@ -77,17 +74,12 @@ impl JsonLspAdapter {
|
||||
cx,
|
||||
);
|
||||
let tasks_schema = task::TaskTemplates::generate_json_schema();
|
||||
let tsconfig_schema = serde_json::Value::from_str(TSCONFIG_SCHEMA).unwrap();
|
||||
serde_json::json!({
|
||||
"json": {
|
||||
"format": {
|
||||
"enable": true,
|
||||
},
|
||||
"schemas": [
|
||||
{
|
||||
"fileMatch": ["tsconfig.json"],
|
||||
"schema":tsconfig_schema
|
||||
},
|
||||
{
|
||||
"fileMatch": [
|
||||
schema_file_match(&paths::SETTINGS),
|
||||
@@ -106,7 +98,6 @@ impl JsonLspAdapter {
|
||||
],
|
||||
"schema": tasks_schema,
|
||||
}
|
||||
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,15 +3,16 @@ use gpui::{AppContext, UpdateGlobal};
|
||||
use json::json_task_context;
|
||||
pub use language::*;
|
||||
use node_runtime::NodeRuntime;
|
||||
use python::PythonContextProvider;
|
||||
use rust_embed::RustEmbed;
|
||||
use settings::SettingsStore;
|
||||
use smol::stream::StreamExt;
|
||||
use std::{str, sync::Arc};
|
||||
use typescript::typescript_task_context;
|
||||
use util::{asset_str, ResultExt};
|
||||
|
||||
use crate::{bash::bash_task_context, go::GoContextProvider, rust::RustContextProvider};
|
||||
use crate::{
|
||||
bash::bash_task_context, go::GoContextProvider, python::python_task_context,
|
||||
rust::RustContextProvider,
|
||||
};
|
||||
|
||||
mod bash;
|
||||
mod c;
|
||||
@@ -22,7 +23,6 @@ mod python;
|
||||
mod rust;
|
||||
mod tailwind;
|
||||
mod typescript;
|
||||
mod vtsls;
|
||||
mod yaml;
|
||||
|
||||
#[derive(RustEmbed)]
|
||||
@@ -129,7 +129,7 @@ pub fn init(
|
||||
vec![Arc::new(python::PythonLspAdapter::new(
|
||||
node_runtime.clone(),
|
||||
))],
|
||||
PythonContextProvider
|
||||
python_task_context()
|
||||
);
|
||||
language!(
|
||||
"rust",
|
||||
@@ -138,33 +138,27 @@ pub fn init(
|
||||
);
|
||||
language!(
|
||||
"tsx",
|
||||
vec![
|
||||
Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone())),
|
||||
Arc::new(vtsls::VtslsLspAdapter::new(node_runtime.clone()))
|
||||
]
|
||||
vec![Arc::new(typescript::TypeScriptLspAdapter::new(
|
||||
node_runtime.clone()
|
||||
))]
|
||||
);
|
||||
language!(
|
||||
"typescript",
|
||||
vec![
|
||||
Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone())),
|
||||
Arc::new(vtsls::VtslsLspAdapter::new(node_runtime.clone()))
|
||||
],
|
||||
typescript_task_context()
|
||||
vec![Arc::new(typescript::TypeScriptLspAdapter::new(
|
||||
node_runtime.clone()
|
||||
))]
|
||||
);
|
||||
language!(
|
||||
"javascript",
|
||||
vec![
|
||||
Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone())),
|
||||
Arc::new(vtsls::VtslsLspAdapter::new(node_runtime.clone()))
|
||||
],
|
||||
typescript_task_context()
|
||||
vec![Arc::new(typescript::TypeScriptLspAdapter::new(
|
||||
node_runtime.clone()
|
||||
))]
|
||||
);
|
||||
language!(
|
||||
"jsdoc",
|
||||
vec![
|
||||
Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone(),)),
|
||||
Arc::new(vtsls::VtslsLspAdapter::new(node_runtime.clone()))
|
||||
]
|
||||
vec![Arc::new(typescript::TypeScriptLspAdapter::new(
|
||||
node_runtime.clone(),
|
||||
))]
|
||||
);
|
||||
language!("regex");
|
||||
language!(
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use language::{ContextProvider, LanguageServerName, LspAdapter, LspAdapterDelegate};
|
||||
use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
|
||||
use lsp::LanguageServerBinary;
|
||||
use node_runtime::NodeRuntime;
|
||||
use project::ContextProviderWithTasks;
|
||||
use std::{
|
||||
any::Any,
|
||||
borrow::Cow,
|
||||
ffi::OsString,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
@@ -182,92 +182,21 @@ async fn get_cached_server_binary(
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct PythonContextProvider;
|
||||
|
||||
const PYTHON_UNITTEST_TARGET_TASK_VARIABLE: VariableName =
|
||||
VariableName::Custom(Cow::Borrowed("PYTHON_UNITTEST_TARGET"));
|
||||
|
||||
impl ContextProvider for PythonContextProvider {
|
||||
fn build_context(
|
||||
&self,
|
||||
variables: &task::TaskVariables,
|
||||
_location: &project::Location,
|
||||
_cx: &mut gpui::AppContext,
|
||||
) -> Result<task::TaskVariables> {
|
||||
let python_module_name = python_module_name_from_relative_path(
|
||||
variables.get(&VariableName::RelativeFile).unwrap_or(""),
|
||||
);
|
||||
let unittest_class_name =
|
||||
variables.get(&VariableName::Custom(Cow::Borrowed("_unittest_class_name")));
|
||||
let unittest_method_name = variables.get(&VariableName::Custom(Cow::Borrowed(
|
||||
"_unittest_method_name",
|
||||
)));
|
||||
|
||||
let unittest_target_str = match (unittest_class_name, unittest_method_name) {
|
||||
(Some(class_name), Some(method_name)) => {
|
||||
format!("{}.{}.{}", python_module_name, class_name, method_name)
|
||||
}
|
||||
(Some(class_name), None) => format!("{}.{}", python_module_name, class_name),
|
||||
(None, None) => python_module_name,
|
||||
(None, Some(_)) => return Ok(task::TaskVariables::default()), // should never happen, a TestCase class is the unit of testing
|
||||
};
|
||||
|
||||
let unittest_target = (
|
||||
PYTHON_UNITTEST_TARGET_TASK_VARIABLE.clone(),
|
||||
unittest_target_str,
|
||||
);
|
||||
|
||||
Ok(task::TaskVariables::from_iter([unittest_target]))
|
||||
}
|
||||
|
||||
fn associated_tasks(&self) -> Option<TaskTemplates> {
|
||||
Some(TaskTemplates(vec![
|
||||
TaskTemplate {
|
||||
label: "execute selection".to_owned(),
|
||||
command: "python3".to_owned(),
|
||||
args: vec!["-c".to_owned(), VariableName::SelectedText.template_value()],
|
||||
..TaskTemplate::default()
|
||||
},
|
||||
TaskTemplate {
|
||||
label: format!("run '{}'", VariableName::File.template_value()),
|
||||
command: "python3".to_owned(),
|
||||
args: vec![VariableName::File.template_value()],
|
||||
..TaskTemplate::default()
|
||||
},
|
||||
TaskTemplate {
|
||||
label: format!("unittest '{}'", VariableName::File.template_value()),
|
||||
command: "python3".to_owned(),
|
||||
args: vec![
|
||||
"-m".to_owned(),
|
||||
"unittest".to_owned(),
|
||||
VariableName::File.template_value(),
|
||||
],
|
||||
..TaskTemplate::default()
|
||||
},
|
||||
TaskTemplate {
|
||||
label: "unittest $ZED_CUSTOM_PYTHON_UNITTEST_TARGET".to_owned(),
|
||||
command: "python3".to_owned(),
|
||||
args: vec![
|
||||
"-m".to_owned(),
|
||||
"unittest".to_owned(),
|
||||
"$ZED_CUSTOM_PYTHON_UNITTEST_TARGET".to_owned(),
|
||||
],
|
||||
tags: vec![
|
||||
"python-unittest-class".to_owned(),
|
||||
"python-unittest-method".to_owned(),
|
||||
],
|
||||
..TaskTemplate::default()
|
||||
},
|
||||
]))
|
||||
}
|
||||
}
|
||||
|
||||
fn python_module_name_from_relative_path(relative_path: &str) -> String {
|
||||
let path_with_dots = relative_path.replace('/', ".");
|
||||
path_with_dots
|
||||
.strip_suffix(".py")
|
||||
.unwrap_or(&path_with_dots)
|
||||
.to_string()
|
||||
pub(super) fn python_task_context() -> ContextProviderWithTasks {
|
||||
ContextProviderWithTasks::new(TaskTemplates(vec![
|
||||
TaskTemplate {
|
||||
label: "execute selection".to_owned(),
|
||||
command: "python3".to_owned(),
|
||||
args: vec!["-c".to_owned(), VariableName::SelectedText.template_value()],
|
||||
..TaskTemplate::default()
|
||||
},
|
||||
TaskTemplate {
|
||||
label: format!("run '{}'", VariableName::File.template_value()),
|
||||
command: "python3".to_owned(),
|
||||
args: vec![VariableName::File.template_value()],
|
||||
..TaskTemplate::default()
|
||||
},
|
||||
]))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
; subclasses of unittest.TestCase or TestCase
|
||||
(
|
||||
(class_definition
|
||||
name: (identifier) @run @_unittest_class_name
|
||||
superclasses: (argument_list
|
||||
[(identifier) @_superclass
|
||||
(attribute (identifier) @_superclass)]
|
||||
)
|
||||
(#eq? @_superclass "TestCase")
|
||||
) @python-unittest-class
|
||||
(#set! tag python-unittest-class)
|
||||
)
|
||||
|
||||
; test methods whose names start with `test` in a TestCase
|
||||
(
|
||||
(class_definition
|
||||
name: (identifier) @_unittest_class_name
|
||||
superclasses: (argument_list
|
||||
[(identifier) @_superclass
|
||||
(attribute (identifier) @_superclass)]
|
||||
)
|
||||
(#eq? @_superclass "TestCase")
|
||||
body: (block
|
||||
(function_definition
|
||||
name: (identifier) @run @_unittest_method_name
|
||||
(#match? @_unittest_method_name "^test.*")
|
||||
) @python-unittest-method
|
||||
(#set! tag python-unittest-method)
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -5,8 +5,6 @@
|
||||
; Properties
|
||||
|
||||
(property_identifier) @property
|
||||
(shorthand_property_identifier) @property
|
||||
(shorthand_property_identifier_pattern) @property
|
||||
|
||||
; Function and method calls
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
|
||||
use lsp::{CodeActionKind, LanguageServerBinary};
|
||||
use node_runtime::NodeRuntime;
|
||||
use project::project_settings::ProjectSettings;
|
||||
use project::ContextProviderWithTasks;
|
||||
use serde_json::{json, Value};
|
||||
use settings::Settings;
|
||||
use smol::{fs, io::BufReader, stream::StreamExt};
|
||||
@@ -19,36 +18,8 @@ use std::{
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
use task::{TaskTemplate, TaskTemplates, VariableName};
|
||||
use util::{fs::remove_matching, maybe, ResultExt};
|
||||
|
||||
pub(super) fn typescript_task_context() -> ContextProviderWithTasks {
|
||||
ContextProviderWithTasks::new(TaskTemplates(vec![
|
||||
TaskTemplate {
|
||||
label: "jest file test".to_owned(),
|
||||
command: "npx jest".to_owned(),
|
||||
args: vec![VariableName::File.template_value()],
|
||||
..TaskTemplate::default()
|
||||
},
|
||||
TaskTemplate {
|
||||
label: "jest test $ZED_SYMBOL".to_owned(),
|
||||
command: "npx jest".to_owned(),
|
||||
args: vec![
|
||||
VariableName::Symbol.template_value(),
|
||||
VariableName::File.template_value(),
|
||||
],
|
||||
tags: vec!["ts-test".into(), "js-test".into()],
|
||||
..TaskTemplate::default()
|
||||
},
|
||||
TaskTemplate {
|
||||
label: "execute selection $ZED_SELECTED_TEXT".to_owned(),
|
||||
command: "node".to_owned(),
|
||||
args: vec!["-e".into(), VariableName::SelectedText.template_value()],
|
||||
..TaskTemplate::default()
|
||||
},
|
||||
]))
|
||||
}
|
||||
|
||||
fn typescript_server_binary_arguments(server_path: &Path) -> Vec<OsString> {
|
||||
vec![server_path.into(), "--stdio".into()]
|
||||
}
|
||||
|
||||
@@ -63,14 +63,3 @@
|
||||
(accessibility_modifier)
|
||||
]* @context
|
||||
name: (_) @name) @item
|
||||
|
||||
; Add support for (node:test, bun:test and Jest) runnable
|
||||
(call_expression
|
||||
function: (_) @context
|
||||
(#any-of? @context "it" "test" "describe")
|
||||
arguments: (
|
||||
arguments . (string
|
||||
(string_fragment) @name
|
||||
)
|
||||
)
|
||||
) @item
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
; Add support for (node:test, bun:test and Jest) runnable
|
||||
; Function expression that has `it`, `test` or `describe` as the function name
|
||||
(
|
||||
(call_expression
|
||||
function: (_) @_name
|
||||
(#any-of? @_name "it" "test" "describe")
|
||||
arguments: (
|
||||
arguments . (string
|
||||
(string_fragment) @run
|
||||
)
|
||||
)
|
||||
) @ts-test
|
||||
(#set! tag ts-test)
|
||||
)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user