Compare commits

..

2 Commits

Author SHA1 Message Date
Thorsten Ball
804b00c12a WIP: Try to drop outdated key press events 2024-06-07 15:58:46 +02:00
Thorsten Ball
f724b2c171 DEBUG 2024-06-06 10:46:18 +02:00
233 changed files with 5927 additions and 13070 deletions

View File

@@ -4,7 +4,3 @@ rustflags = ["-C", "symbol-mangling-version=v0", "--cfg", "tokio_unstable"]
[alias]
xtask = "run --package xtask --"
[target.x86_64-unknown-linux-gnu]
linker = "/usr/bin/clang"
rustflags = ["-C", "link-arg=-fuse-ld=mold"]

View File

@@ -74,8 +74,8 @@ jobs:
version: v1.29.0
- uses: bufbuild/buf-breaking-action@v1
with:
input: "crates/proto/proto/"
against: "https://github.com/${GITHUB_REPOSITORY}.git#branch=${BUF_BASE_BRANCH},subdir=crates/proto/proto/"
input: "crates/rpc/proto/"
against: "https://github.com/${GITHUB_REPOSITORY}.git#branch=${BUF_BASE_BRANCH},subdir=crates/rpc/proto/"
macos_tests:
timeout-minutes: 60
@@ -305,6 +305,9 @@ jobs:
exit 1
fi
- name: Generate license file
run: script/generate-licenses
- name: Create and upload Linux .tar.gz bundle
run: script/bundle-linux

114
Cargo.lock generated
View File

@@ -359,7 +359,6 @@ dependencies = [
"log",
"menu",
"multi_buffer",
"ollama",
"open_ai",
"ordered-float 2.10.0",
"parking_lot",
@@ -368,14 +367,12 @@ dependencies = [
"rand 0.8.5",
"regex",
"rope",
"rustdoc",
"schemars",
"search",
"semantic_index",
"serde",
"serde_json",
"settings",
"similar",
"smol",
"strsim 0.11.1",
"strum",
@@ -1514,7 +1511,7 @@ dependencies = [
[[package]]
name = "blade-graphics"
version = "0.4.0"
source = "git+https://github.com/zed-industries/blade?rev=33fd51359d113c03b785e28f4a6cf75bacb0b26d#33fd51359d113c03b785e28f4a6cf75bacb0b26d"
source = "git+https://github.com/kvark/blade?rev=bdaf8c534fbbc9fbca71d1cf272f45640b3a068d#bdaf8c534fbbc9fbca71d1cf272f45640b3a068d"
dependencies = [
"ash",
"ash-window",
@@ -1544,7 +1541,7 @@ dependencies = [
[[package]]
name = "blade-macros"
version = "0.2.1"
source = "git+https://github.com/zed-industries/blade?rev=33fd51359d113c03b785e28f4a6cf75bacb0b26d#33fd51359d113c03b785e28f4a6cf75bacb0b26d"
source = "git+https://github.com/kvark/blade?rev=bdaf8c534fbbc9fbca71d1cf272f45640b3a068d#bdaf8c534fbbc9fbca71d1cf272f45640b3a068d"
dependencies = [
"proc-macro2",
"quote",
@@ -1554,7 +1551,7 @@ dependencies = [
[[package]]
name = "blade-util"
version = "0.1.0"
source = "git+https://github.com/zed-industries/blade?rev=33fd51359d113c03b785e28f4a6cf75bacb0b26d#33fd51359d113c03b785e28f4a6cf75bacb0b26d"
source = "git+https://github.com/kvark/blade?rev=bdaf8c534fbbc9fbca71d1cf272f45640b3a068d#bdaf8c534fbbc9fbca71d1cf272f45640b3a068d"
dependencies = [
"blade-graphics",
"bytemuck",
@@ -2151,6 +2148,7 @@ dependencies = [
"exec",
"fork",
"ipc-channel",
"libc",
"once_cell",
"plist",
"release_channel",
@@ -2211,10 +2209,8 @@ dependencies = [
"async-tungstenite",
"chrono",
"clock",
"cocoa",
"collections",
"feature_flags",
"fs",
"futures 0.3.28",
"gpui",
"http 0.1.0",
@@ -2241,7 +2237,6 @@ dependencies = [
"tiny_http",
"url",
"util",
"windows 0.56.0",
]
[[package]]
@@ -2368,7 +2363,6 @@ dependencies = [
"prometheus",
"prost",
"rand 0.8.5",
"recent_projects",
"release_channel",
"reqwest",
"rpc",
@@ -2459,6 +2453,13 @@ dependencies = [
"rustc-hash",
]
[[package]]
name = "color"
version = "0.1.0"
dependencies = [
"palette",
]
[[package]]
name = "color_quant"
version = "1.1.0"
@@ -3451,7 +3452,6 @@ version = "0.1.0"
dependencies = [
"aho-corasick",
"anyhow",
"assets",
"client",
"clock",
"collections",
@@ -3833,7 +3833,6 @@ dependencies = [
"fuzzy",
"gpui",
"language",
"num-format",
"picker",
"project",
"release_channel",
@@ -6736,16 +6735,6 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "num-format"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a652d9771a63711fd3c3deb670acfbe5c30a4072e664d7a3bf5a9e1056ac72c3"
dependencies = [
"arrayvec",
"itoa",
]
[[package]]
name = "num-integer"
version = "0.1.45"
@@ -6920,19 +6909,6 @@ dependencies = [
"cc",
]
[[package]]
name = "ollama"
version = "0.1.0"
dependencies = [
"anyhow",
"futures 0.3.28",
"http 0.1.0",
"isahc",
"schemars",
"serde",
"serde_json",
]
[[package]]
name = "once_cell"
version = "1.19.0"
@@ -7132,6 +7108,7 @@ dependencies = [
"project",
"rope",
"serde_json",
"settings",
"smol",
"theme",
"tree-sitter-rust",
@@ -7141,31 +7118,6 @@ dependencies = [
"workspace",
]
[[package]]
name = "outline_panel"
version = "0.1.0"
dependencies = [
"anyhow",
"collections",
"db",
"editor",
"file_icons",
"git",
"gpui",
"language",
"log",
"menu",
"project",
"schemars",
"serde",
"serde_json",
"settings",
"unicase",
"util",
"workspace",
"worktree",
]
[[package]]
name = "outref"
version = "0.5.1"
@@ -7968,17 +7920,6 @@ dependencies = [
"prost",
]
[[package]]
name = "proto"
version = "0.1.0"
dependencies = [
"anyhow",
"collections",
"prost",
"prost-build",
"serde",
]
[[package]]
name = "protobuf"
version = "2.28.0"
@@ -8562,7 +8503,8 @@ dependencies = [
"futures 0.3.28",
"gpui",
"parking_lot",
"proto",
"prost",
"prost-build",
"rand 0.8.5",
"rsa 0.4.0",
"serde",
@@ -8687,26 +8629,6 @@ dependencies = [
"semver",
]
[[package]]
name = "rustdoc"
version = "0.1.0"
dependencies = [
"anyhow",
"async-trait",
"collections",
"fs",
"futures 0.3.28",
"fuzzy",
"gpui",
"html_to_markdown",
"http 0.1.0",
"indexmap 1.9.3",
"indoc",
"parking_lot",
"pretty_assertions",
"strum",
]
[[package]]
name = "rustix"
version = "0.37.23"
@@ -10485,6 +10407,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"collections",
"color",
"derive_more",
"fs",
"futures 0.3.28",
@@ -12964,6 +12887,7 @@ name = "worktree"
version = "0.1.0"
dependencies = [
"anyhow",
"client",
"clock",
"collections",
"env_logger",
@@ -12975,8 +12899,10 @@ dependencies = [
"gpui",
"http 0.1.0",
"ignore",
"itertools 0.11.0",
"language",
"log",
"lsp",
"parking_lot",
"postage",
"pretty_assertions",
@@ -13225,11 +13151,10 @@ dependencies = [
[[package]]
name = "zed"
version = "0.141.0"
version = "0.140.0"
dependencies = [
"activity_indicator",
"anyhow",
"ashpd",
"assets",
"assistant",
"audio",
@@ -13282,7 +13207,6 @@ dependencies = [
"node_runtime",
"notifications",
"outline",
"outline_panel",
"parking_lot",
"profiling",
"project",

View File

@@ -61,16 +61,13 @@ members = [
"crates/multi_buffer",
"crates/node_runtime",
"crates/notifications",
"crates/ollama",
"crates/open_ai",
"crates/outline",
"crates/outline_panel",
"crates/picker",
"crates/prettier",
"crates/project",
"crates/project_panel",
"crates/project_symbols",
"crates/proto",
"crates/quick_action_bar",
"crates/recent_projects",
"crates/refineable",
@@ -80,7 +77,6 @@ members = [
"crates/rich_text",
"crates/rope",
"crates/rpc",
"crates/rustdoc",
"crates/task",
"crates/tasks_ui",
"crates/search",
@@ -167,6 +163,7 @@ clock = { path = "crates/clock" }
collab = { path = "crates/collab" }
collab_ui = { path = "crates/collab_ui" }
collections = { path = "crates/collections" }
color = { path = "crates/color" }
command_palette = { path = "crates/command_palette" }
command_palette_hooks = { path = "crates/command_palette_hooks" }
copilot = { path = "crates/copilot" }
@@ -210,16 +207,13 @@ menu = { path = "crates/menu" }
multi_buffer = { path = "crates/multi_buffer" }
node_runtime = { path = "crates/node_runtime" }
notifications = { path = "crates/notifications" }
ollama = { path = "crates/ollama" }
open_ai = { path = "crates/open_ai" }
outline = { path = "crates/outline" }
outline_panel = { path = "crates/outline_panel" }
picker = { path = "crates/picker" }
plugin = { path = "crates/plugin" }
plugin_macros = { path = "crates/plugin_macros" }
prettier = { path = "crates/prettier" }
project = { path = "crates/project" }
proto = { path = "crates/proto" }
worktree = { path = "crates/worktree" }
project_panel = { path = "crates/project_panel" }
project_symbols = { path = "crates/project_symbols" }
@@ -230,7 +224,6 @@ dev_server_projects = { path = "crates/dev_server_projects" }
rich_text = { path = "crates/rich_text" }
rope = { path = "crates/rope" }
rpc = { path = "crates/rpc" }
rustdoc = { path = "crates/rustdoc" }
task = { path = "crates/task" }
tasks_ui = { path = "crates/tasks_ui" }
search = { path = "crates/search" }
@@ -274,15 +267,14 @@ async-tar = "0.4.2"
async-trait = "0.1"
async_zip = { version = "0.0.17", features = ["deflate", "deflate64"] }
bitflags = "2.4.2"
blade-graphics = { git = "https://github.com/zed-industries/blade", rev = "33fd51359d113c03b785e28f4a6cf75bacb0b26d" }
blade-macros = { git = "https://github.com/zed-industries/blade", rev = "33fd51359d113c03b785e28f4a6cf75bacb0b26d" }
blade-util = { git = "https://github.com/zed-industries/blade", rev = "33fd51359d113c03b785e28f4a6cf75bacb0b26d" }
blade-graphics = { git = "https://github.com/kvark/blade", rev = "bdaf8c534fbbc9fbca71d1cf272f45640b3a068d" }
blade-macros = { git = "https://github.com/kvark/blade", rev = "bdaf8c534fbbc9fbca71d1cf272f45640b3a068d" }
blade-util = { git = "https://github.com/kvark/blade", rev = "bdaf8c534fbbc9fbca71d1cf272f45640b3a068d" }
cap-std = "3.0"
cargo_toml = "0.20"
chrono = { version = "0.4", features = ["serde"] }
clap = { version = "4.4", features = ["derive"] }
clickhouse = { version = "0.11.6" }
cocoa = "0.25"
ctor = "0.2.6"
signal-hook = "0.3.17"
core-foundation = { version = "0.9.3" }
@@ -301,7 +293,6 @@ heed = { version = "0.20.1", features = ["read-txn-no-tls"] }
hex = "0.4.3"
html5ever = "0.27.0"
ignore = "0.4.22"
indexmap = { version = "1.6.2", features = ["serde"] }
indoc = "1"
# We explicitly disable http2 support in isahc.
isahc = { version = "1.7.2", default-features = false, features = [
@@ -316,7 +307,6 @@ log = { version = "0.4.16", features = ["kv_unstable_serde"] }
markup5ever_rcdom = "0.3.0"
nanoid = "0.4"
nix = "0.28"
num-format = "0.4.4"
once_cell = "1.19.0"
ordered-float = "2.1.1"
palette = { version = "0.7.5", default-features = false, features = ["std"] }
@@ -348,7 +338,6 @@ serde_repr = "0.1"
sha2 = "0.10"
shellexpand = "2.1.0"
shlex = "1.3.0"
similar = "1.3"
smallvec = { version = "1.6", features = ["union"] }
smol = "1.2"
strum = { version = "0.25.0", features = ["derive"] }
@@ -474,6 +463,12 @@ codegen-units = 1
[profile.release.package]
zed = { codegen-units = 16 }
[profile.profiling]
inherits = "release"
debug = true
lto = false
codegen-units = 16
[workspace.lints.clippy]
dbg_macro = "deny"
todo = "deny"

View File

@@ -1,6 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.6667 2H3.33333C2.59695 2 2 2.59695 2 3.33333V12.6667C2 13.403 2.59695 14 3.33333 14H12.6667C13.403 14 14 13.403 14 12.6667V3.33333C14 2.59695 13.403 2 12.6667 2Z" stroke="#888888" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9 5H5" stroke="#888888" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10.5 8H5" stroke="#888888" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9 10.9502H5" stroke="#888888" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 683 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-list-tree"><path d="M21 12h-8"/><path d="M21 6H8"/><path d="M21 18h-8"/><path d="M3 6v4c0 1.1.9 2 2 2h3"/><path d="M3 10v6c0 1.1.9 2 2 2h3"/></svg>

Before

Width:  |  Height:  |  Size: 349 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-rotate-cw"><path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/></svg>

Before

Width:  |  Height:  |  Size: 303 B

View File

@@ -1,3 +0,0 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.88889 1H2.11111C1.49746 1 1 1.49746 1 2.11111V9.88889C1 10.5025 1.49746 11 2.11111 11H9.88889C10.5025 11 11 10.5025 11 9.88889V2.11111C11 1.49746 10.5025 1 9.88889 1Z" stroke="#C56757" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 369 B

View File

@@ -439,7 +439,6 @@
"ctrl-shift-p": "command_palette::Toggle",
"ctrl-shift-m": "diagnostics::Deploy",
"ctrl-shift-e": "project_panel::ToggleFocus",
"ctrl-shift-b": "outline_panel::ToggleFocus",
"ctrl-?": "assistant::ToggleFocus",
"ctrl-alt-s": "workspace::SaveAll",
"ctrl-k m": "language_selector::Toggle",
@@ -546,7 +545,7 @@
}
},
{
"context": "ContextEditor > Editor",
"context": "ConversationEditor > Editor",
"bindings": {
"ctrl-enter": "assistant::Assist",
"ctrl-s": "workspace::Save",
@@ -563,18 +562,6 @@
"ctrl-enter": "project_search::SearchInNew"
}
},
{
"context": "OutlinePanel",
"bindings": {
"left": "project_panel::CollapseSelectedEntry",
"right": "project_panel::ExpandSelectedEntry",
"ctrl-alt-c": "project_panel::CopyPath",
"alt-ctrl-shift-c": "project_panel::CopyRelativePath",
"alt-ctrl-r": "project_panel::RevealInFinder",
"shift-down": "menu::SelectNext",
"shift-up": "menu::SelectPrev"
}
},
{
"context": "ProjectPanel",
"bindings": {
@@ -596,10 +583,7 @@
"ctrl-backspace": ["project_panel::Delete", { "skip_prompt": false }],
"ctrl-delete": ["project_panel::Delete", { "skip_prompt": false }],
"alt-ctrl-r": "project_panel::RevealInFinder",
"alt-shift-f": "project_panel::NewSearchInDirectory",
"shift-down": "menu::SelectNext",
"shift-up": "menu::SelectPrev",
"escape": "menu::Cancel"
"alt-shift-f": "project_panel::NewSearchInDirectory"
}
},
{

View File

@@ -228,7 +228,7 @@
}
},
{
"context": "ContextEditor > Editor",
"context": "ConversationEditor > Editor",
"bindings": {
"cmd-enter": "assistant::Assist",
"cmd-s": "workspace::Save",
@@ -475,7 +475,6 @@
"cmd-shift-p": "command_palette::Toggle",
"cmd-shift-m": "diagnostics::Deploy",
"cmd-shift-e": "project_panel::ToggleFocus",
"cmd-shift-b": "outline_panel::ToggleFocus",
"cmd-?": "assistant::ToggleFocus",
"cmd-alt-s": "workspace::SaveAll",
"cmd-k m": "language_selector::Toggle",
@@ -585,18 +584,6 @@
"cmd-enter": "project_search::SearchInNew"
}
},
{
"context": "OutlinePanel",
"bindings": {
"left": "outline_panel::CollapseSelectedEntry",
"right": "outline_panel::ExpandSelectedEntry",
"cmd-alt-c": "outline_panel::CopyPath",
"alt-cmd-shift-c": "outline_panel::CopyRelativePath",
"alt-cmd-r": "outline_panel::RevealInFinder",
"shift-down": "menu::SelectNext",
"shift-up": "menu::SelectPrev"
}
},
{
"context": "ProjectPanel",
"bindings": {

View File

@@ -80,7 +80,6 @@
"g shift-e": ["vim::PreviousWordEnd", { "ignorePunctuation": true }],
"/": "vim::Search",
"g /": "pane::DeploySearch",
"?": [
"vim::Search",
{
@@ -382,10 +381,6 @@
"shift-s": "vim::SubstituteLine",
">": ["vim::PushOperator", "Indent"],
"<": ["vim::PushOperator", "Outdent"],
"g u": ["vim::PushOperator", "Lowercase"],
"g shift-u": ["vim::PushOperator", "Uppercase"],
"g ~": ["vim::PushOperator", "OppositeCase"],
"\"": ["vim::PushOperator", "Register"],
"ctrl-pagedown": "pane::ActivateNextItem",
"ctrl-pageup": "pane::ActivatePrevItem",
// tree-sitter related commands
@@ -400,7 +395,6 @@
{
"context": "Editor && vim_mode == visual && vim_operator == none && !VimWaiting",
"bindings": {
"\"": ["vim::PushOperator", "Register"],
// tree-sitter related commands
"[ x": "editor::SelectLargerSyntaxNode",
"] x": "editor::SelectSmallerSyntaxNode"
@@ -436,27 +430,6 @@
"d": "vim::CurrentLine"
}
},
{
"context": "Editor && vim_operator == gu",
"bindings": {
"g u": "vim::CurrentLine",
"u": "vim::CurrentLine"
}
},
{
"context": "Editor && vim_operator == gU",
"bindings": {
"g shift-u": "vim::CurrentLine",
"shift-u": "vim::CurrentLine"
}
},
{
"context": "Editor && vim_operator == g~",
"bindings": {
"g ~": "vim::CurrentLine",
"~": "vim::CurrentLine"
}
},
{
"context": "Editor && vim_mode == normal && vim_operator == d",
"bindings": {

View File

@@ -131,14 +131,7 @@
// The default number of lines to expand excerpts in the multibuffer by.
"expand_excerpt_lines": 3,
// Globs to match against file paths to determine if a file is private.
"private_files": [
"**/.env*",
"**/*.pem",
"**/*.key",
"**/*.cert",
"**/*.crt",
"**/secrets.yml"
],
"private_files": ["**/.env*", "**/*.pem", "**/*.key", "**/*.cert", "**/*.crt", "**/secrets.yml"],
// Whether to use additional LSP queries to format (and amend) the code after
// every "trigger" symbol input, defined by LSP server capabilities.
"use_on_type_format": true,
@@ -302,29 +295,6 @@
/// when a directory has only one directory inside.
"auto_fold_dirs": false
},
"outline_panel": {
// Whether to show the outline panel button in the status bar
"button": true,
// Default width of the outline panel.
"default_width": 240,
// Where to dock the outline panel. Can be 'left' or 'right'.
"dock": "left",
// Whether to show file icons in the outline panel.
"file_icons": true,
// Whether to show folder icons or chevrons for directories in the outline panel.
"folder_icons": true,
// Whether to show the git status in the outline panel.
"git_status": true,
// Amount of indentation for nested items.
"indent_size": 20,
// Whether to reveal it in the outline panel automatically,
// when a corresponding outline entry becomes active.
// Gitignored entries are never auto revealed.
"auto_reveal_entries": true,
/// Whether to fold directories automatically
/// when a directory has only one directory inside.
"auto_fold_dirs": true
},
"collaboration_panel": {
// Whether to show the collaboration panel button in the status bar.
"button": true,
@@ -384,9 +354,6 @@
"show_call_status_icon": true,
// Whether to use language servers to provide code intelligence.
"enable_language_server": true,
// Whether to perform linked edits of associated ranges, if the language server supports it.
// For example, when editing opening <html> tag, the contents of the closing </html> tag will be edited as well.
"linked_edits": true,
// The list of language servers to use (or disable) for all languages.
//
// This is typically customized on a per-language basis.

View File

@@ -62,16 +62,16 @@ impl ActivityIndicator {
this.update(&mut cx, |this, cx| {
this.statuses.retain(|s| s.name != name);
this.statuses.push(LspStatus { name, status });
cx.notify();
cx.notify(); // commented back in
})?;
}
anyhow::Ok(())
})
.detach();
cx.observe(&project, |_, _, cx| cx.notify()).detach();
cx.observe(&project, |_, _, cx| cx.notify()).detach(); // commented back in
if let Some(auto_updater) = auto_updater.as_ref() {
cx.observe(auto_updater, |_, _, cx| cx.notify()).detach();
cx.observe(auto_updater, |_, _, cx| cx.notify()).detach(); // commented back in
}
Self {
@@ -285,10 +285,10 @@ impl ActivityIndicator {
icon: None,
message: "Click to restart and update Zed".to_string(),
on_click: Some(Arc::new({
let reload = workspace::Reload {
let restart = workspace::Restart {
binary_path: Some(binary_path.clone()),
};
move |_, cx| workspace::reload(&reload, cx)
move |_, cx| workspace::restart(&restart, cx)
})),
},
AutoUpdateStatus::Errored => Content {

View File

@@ -35,21 +35,18 @@ language.workspace = true
log.workspace = true
menu.workspace = true
multi_buffer.workspace = true
ollama = { workspace = true, features = ["schemars"] }
open_ai = { workspace = true, features = ["schemars"] }
ordered-float.workspace = true
parking_lot.workspace = true
project.workspace = true
regex.workspace = true
rope.workspace = true
rustdoc.workspace = true
schemars.workspace = true
search.workspace = true
semantic_index.workspace = true
serde.workspace = true
serde_json.workspace = true
settings.workspace = true
similar.workspace = true
smol.workspace = true
strsim = "0.11"
strum.workspace = true

View File

@@ -1,7 +1,7 @@
pub mod assistant_panel;
pub mod assistant_settings;
mod completion_provider;
mod context_store;
mod conversation_store;
mod inline_assistant;
mod model_selector;
mod prompt_library;
@@ -12,22 +12,21 @@ mod streaming_diff;
pub use assistant_panel::AssistantPanel;
use assistant_settings::{AnthropicModel, AssistantSettings, CloudModel, OllamaModel, OpenAiModel};
use assistant_settings::{AnthropicModel, AssistantSettings, CloudModel, OpenAiModel};
use assistant_slash_command::SlashCommandRegistry;
use client::{proto, Client};
use command_palette_hooks::CommandPaletteFilter;
pub(crate) use completion_provider::*;
pub(crate) use context_store::*;
pub(crate) use conversation_store::*;
use gpui::{actions, AppContext, Global, SharedString, UpdateGlobal};
pub(crate) use inline_assistant::*;
pub(crate) use model_selector::*;
use rustdoc::RustdocStore;
use semantic_index::{CloudEmbeddingProvider, SemanticIndex};
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsStore};
use slash_command::{
active_command, default_command, fetch_command, file_command, now_command, project_command,
prompt_command, rustdoc_command, search_command, tabs_command,
active_command, default_command, fetch_command, file_command, project_command, prompt_command,
rustdoc_command, search_command, tabs_command,
};
use std::{
fmt::{self, Display},
@@ -92,7 +91,6 @@ pub enum LanguageModel {
Cloud(CloudModel),
OpenAi(OpenAiModel),
Anthropic(AnthropicModel),
Ollama(OllamaModel),
}
impl Default for LanguageModel {
@@ -107,7 +105,6 @@ impl LanguageModel {
LanguageModel::OpenAi(model) => format!("openai/{}", model.id()),
LanguageModel::Anthropic(model) => format!("anthropic/{}", model.id()),
LanguageModel::Cloud(model) => format!("zed.dev/{}", model.id()),
LanguageModel::Ollama(model) => format!("ollama/{}", model.id()),
}
}
@@ -116,7 +113,6 @@ impl LanguageModel {
LanguageModel::OpenAi(model) => model.display_name().into(),
LanguageModel::Anthropic(model) => model.display_name().into(),
LanguageModel::Cloud(model) => model.display_name().into(),
LanguageModel::Ollama(model) => model.display_name().into(),
}
}
@@ -125,7 +121,6 @@ impl LanguageModel {
LanguageModel::OpenAi(model) => model.max_token_count(),
LanguageModel::Anthropic(model) => model.max_token_count(),
LanguageModel::Cloud(model) => model.max_token_count(),
LanguageModel::Ollama(model) => model.max_token_count(),
}
}
@@ -134,7 +129,6 @@ impl LanguageModel {
LanguageModel::OpenAi(model) => model.id(),
LanguageModel::Anthropic(model) => model.id(),
LanguageModel::Cloud(model) => model.id(),
LanguageModel::Ollama(model) => model.id(),
}
}
}
@@ -185,7 +179,6 @@ impl LanguageModelRequest {
match &self.model {
LanguageModel::OpenAi(_) => {}
LanguageModel::Anthropic(_) => {}
LanguageModel::Ollama(_) => {}
LanguageModel::Cloud(model) => match model {
CloudModel::Claude3Opus | CloudModel::Claude3Sonnet | CloudModel::Claude3Haiku => {
preprocess_anthropic_request(self);
@@ -287,7 +280,6 @@ pub fn init(client: Arc<Client>, cx: &mut AppContext) {
register_slash_commands(cx);
assistant_panel::init(cx);
inline_assistant::init(client.telemetry().clone(), cx);
RustdocStore::init_global(cx);
CommandPaletteFilter::update_global(cx, |filter, _cx| {
filter.hide_namespace(Assistant::NAMESPACE);
@@ -315,7 +307,6 @@ fn register_slash_commands(cx: &mut AppContext) {
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(now_command::NowSlashCommand, true);
slash_command_registry.register_command(rustdoc_command::RustdocSlashCommand, false);
slash_command_registry.register_command(fetch_command::FetchSlashCommand, false);
}

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,6 @@ use std::fmt;
pub use anthropic::Model as AnthropicModel;
use gpui::Pixels;
pub use ollama::Model as OllamaModel;
pub use open_ai::Model as OpenAiModel;
use schemars::{
schema::{InstanceType, Metadata, Schema, SchemaObject},
@@ -169,11 +168,6 @@ pub enum AssistantProvider {
api_url: String,
low_speed_timeout_in_seconds: Option<u64>,
},
Ollama {
model: OllamaModel,
api_url: String,
low_speed_timeout_in_seconds: Option<u64>,
},
}
impl Default for AssistantProvider {
@@ -203,12 +197,6 @@ pub enum AssistantProviderContent {
api_url: Option<String>,
low_speed_timeout_in_seconds: Option<u64>,
},
#[serde(rename = "ollama")]
Ollama {
default_model: Option<OllamaModel>,
api_url: Option<String>,
low_speed_timeout_in_seconds: Option<u64>,
},
}
#[derive(Debug, Default)]
@@ -340,13 +328,6 @@ impl AssistantSettingsContent {
low_speed_timeout_in_seconds: None,
})
}
LanguageModel::Ollama(model) => {
*provider = Some(AssistantProviderContent::Ollama {
default_model: Some(model),
api_url: None,
low_speed_timeout_in_seconds: None,
})
}
},
},
},
@@ -491,27 +472,6 @@ impl Settings for AssistantSettings {
Some(low_speed_timeout_in_seconds_override);
}
}
(
AssistantProvider::Ollama {
model,
api_url,
low_speed_timeout_in_seconds,
},
AssistantProviderContent::Ollama {
default_model: model_override,
api_url: api_url_override,
low_speed_timeout_in_seconds: low_speed_timeout_in_seconds_override,
},
) => {
merge(model, model_override);
merge(api_url, api_url_override);
if let Some(low_speed_timeout_in_seconds_override) =
low_speed_timeout_in_seconds_override
{
*low_speed_timeout_in_seconds =
Some(low_speed_timeout_in_seconds_override);
}
}
(
AssistantProvider::Anthropic {
model,
@@ -559,15 +519,6 @@ impl Settings for AssistantSettings {
.unwrap_or_else(|| anthropic::ANTHROPIC_API_URL.into()),
low_speed_timeout_in_seconds,
},
AssistantProviderContent::Ollama {
default_model: model,
api_url,
low_speed_timeout_in_seconds,
} => AssistantProvider::Ollama {
model: model.unwrap_or_default(),
api_url: api_url.unwrap_or_else(|| ollama::OLLAMA_API_URL.into()),
low_speed_timeout_in_seconds,
},
};
}
}

View File

@@ -2,14 +2,12 @@ mod anthropic;
mod cloud;
#[cfg(test)]
mod fake;
mod ollama;
mod open_ai;
pub use anthropic::*;
pub use cloud::*;
#[cfg(test)]
pub use fake::*;
pub use ollama::*;
pub use open_ai::*;
use crate::{
@@ -52,18 +50,6 @@ pub fn init(client: Arc<Client>, cx: &mut AppContext) {
low_speed_timeout_in_seconds.map(Duration::from_secs),
settings_version,
)),
AssistantProvider::Ollama {
model,
api_url,
low_speed_timeout_in_seconds,
} => CompletionProvider::Ollama(OllamaCompletionProvider::new(
model.clone(),
api_url.clone(),
client.http_client(),
low_speed_timeout_in_seconds.map(Duration::from_secs),
settings_version,
cx,
)),
};
cx.set_global(provider);
@@ -101,24 +87,6 @@ pub fn init(client: Arc<Client>, cx: &mut AppContext) {
settings_version,
);
}
(
CompletionProvider::Ollama(provider),
AssistantProvider::Ollama {
model,
api_url,
low_speed_timeout_in_seconds,
},
) => {
provider.update(
model.clone(),
api_url.clone(),
low_speed_timeout_in_seconds.map(Duration::from_secs),
settings_version,
cx,
);
}
(CompletionProvider::Cloud(provider), AssistantProvider::ZedDotDev { model }) => {
provider.update(model.clone(), settings_version);
}
@@ -162,23 +130,6 @@ pub fn init(client: Arc<Client>, cx: &mut AppContext) {
settings_version,
));
}
(
_,
AssistantProvider::Ollama {
model,
api_url,
low_speed_timeout_in_seconds,
},
) => {
*provider = CompletionProvider::Ollama(OllamaCompletionProvider::new(
model.clone(),
api_url.clone(),
client.http_client(),
low_speed_timeout_in_seconds.map(Duration::from_secs),
settings_version,
cx,
));
}
}
})
})
@@ -191,7 +142,6 @@ pub enum CompletionProvider {
Cloud(CloudCompletionProvider),
#[cfg(test)]
Fake(FakeCompletionProvider),
Ollama(OllamaCompletionProvider),
}
impl gpui::Global for CompletionProvider {}
@@ -215,10 +165,6 @@ impl CompletionProvider {
.available_models()
.map(LanguageModel::Cloud)
.collect(),
CompletionProvider::Ollama(provider) => provider
.available_models()
.map(|model| LanguageModel::Ollama(model.clone()))
.collect(),
#[cfg(test)]
CompletionProvider::Fake(_) => unimplemented!(),
}
@@ -229,7 +175,6 @@ impl CompletionProvider {
CompletionProvider::OpenAi(provider) => provider.settings_version(),
CompletionProvider::Anthropic(provider) => provider.settings_version(),
CompletionProvider::Cloud(provider) => provider.settings_version(),
CompletionProvider::Ollama(provider) => provider.settings_version(),
#[cfg(test)]
CompletionProvider::Fake(_) => unimplemented!(),
}
@@ -240,7 +185,6 @@ impl CompletionProvider {
CompletionProvider::OpenAi(provider) => provider.is_authenticated(),
CompletionProvider::Anthropic(provider) => provider.is_authenticated(),
CompletionProvider::Cloud(provider) => provider.is_authenticated(),
CompletionProvider::Ollama(provider) => provider.is_authenticated(),
#[cfg(test)]
CompletionProvider::Fake(_) => true,
}
@@ -251,7 +195,6 @@ impl CompletionProvider {
CompletionProvider::OpenAi(provider) => provider.authenticate(cx),
CompletionProvider::Anthropic(provider) => provider.authenticate(cx),
CompletionProvider::Cloud(provider) => provider.authenticate(cx),
CompletionProvider::Ollama(provider) => provider.authenticate(cx),
#[cfg(test)]
CompletionProvider::Fake(_) => Task::ready(Ok(())),
}
@@ -262,7 +205,6 @@ impl CompletionProvider {
CompletionProvider::OpenAi(provider) => provider.authentication_prompt(cx),
CompletionProvider::Anthropic(provider) => provider.authentication_prompt(cx),
CompletionProvider::Cloud(provider) => provider.authentication_prompt(cx),
CompletionProvider::Ollama(provider) => provider.authentication_prompt(cx),
#[cfg(test)]
CompletionProvider::Fake(_) => unimplemented!(),
}
@@ -273,7 +215,6 @@ impl CompletionProvider {
CompletionProvider::OpenAi(provider) => provider.reset_credentials(cx),
CompletionProvider::Anthropic(provider) => provider.reset_credentials(cx),
CompletionProvider::Cloud(_) => Task::ready(Ok(())),
CompletionProvider::Ollama(provider) => provider.reset_credentials(cx),
#[cfg(test)]
CompletionProvider::Fake(_) => Task::ready(Ok(())),
}
@@ -284,7 +225,6 @@ impl CompletionProvider {
CompletionProvider::OpenAi(provider) => LanguageModel::OpenAi(provider.model()),
CompletionProvider::Anthropic(provider) => LanguageModel::Anthropic(provider.model()),
CompletionProvider::Cloud(provider) => LanguageModel::Cloud(provider.model()),
CompletionProvider::Ollama(provider) => LanguageModel::Ollama(provider.model()),
#[cfg(test)]
CompletionProvider::Fake(_) => LanguageModel::default(),
}
@@ -299,7 +239,6 @@ impl CompletionProvider {
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::Ollama(provider) => provider.count_tokens(request, cx),
#[cfg(test)]
CompletionProvider::Fake(_) => futures::FutureExt::boxed(futures::future::ready(Ok(0))),
}
@@ -313,7 +252,6 @@ impl CompletionProvider {
CompletionProvider::OpenAi(provider) => provider.complete(request),
CompletionProvider::Anthropic(provider) => provider.complete(request),
CompletionProvider::Cloud(provider) => provider.complete(request),
CompletionProvider::Ollama(provider) => provider.complete(request),
#[cfg(test)]
CompletionProvider::Fake(provider) => provider.complete(),
}

View File

@@ -349,7 +349,7 @@ impl Render for AuthenticationPrompt {
h_flex()
.gap_2()
.child(Label::new("Click on").size(LabelSize::Small))
.child(Icon::new(IconName::ZedAssistant).size(IconSize::XSmall))
.child(Icon::new(IconName::Ai).size(IconSize::XSmall))
.child(
Label::new("in the status bar to close this panel.").size(LabelSize::Small),
),

View File

@@ -1,267 +0,0 @@
use crate::{
assistant_settings::OllamaModel, CompletionProvider, LanguageModel, LanguageModelRequest, Role,
};
use anyhow::Result;
use futures::StreamExt as _;
use futures::{future::BoxFuture, stream::BoxStream, FutureExt};
use gpui::{AnyView, AppContext, Task};
use http::HttpClient;
use ollama::{
get_models, preload_model, stream_chat_completion, ChatMessage, ChatOptions, ChatRequest,
Role as OllamaRole,
};
use std::sync::Arc;
use std::time::Duration;
use ui::{prelude::*, ButtonLike, ElevationIndex};
const OLLAMA_DOWNLOAD_URL: &str = "https://ollama.com/download";
pub struct OllamaCompletionProvider {
api_url: String,
model: OllamaModel,
http_client: Arc<dyn HttpClient>,
low_speed_timeout: Option<Duration>,
settings_version: usize,
available_models: Vec<OllamaModel>,
}
impl OllamaCompletionProvider {
pub fn new(
model: OllamaModel,
api_url: String,
http_client: Arc<dyn HttpClient>,
low_speed_timeout: Option<Duration>,
settings_version: usize,
cx: &AppContext,
) -> Self {
cx.spawn({
let api_url = api_url.clone();
let client = http_client.clone();
let model = model.name.clone();
|_| async move { preload_model(client.as_ref(), &api_url, &model).await }
})
.detach_and_log_err(cx);
Self {
api_url,
model,
http_client,
low_speed_timeout,
settings_version,
available_models: Default::default(),
}
}
pub fn update(
&mut self,
model: OllamaModel,
api_url: String,
low_speed_timeout: Option<Duration>,
settings_version: usize,
cx: &AppContext,
) {
cx.spawn({
let api_url = api_url.clone();
let client = self.http_client.clone();
let model = model.name.clone();
|_| async move { preload_model(client.as_ref(), &api_url, &model).await }
})
.detach_and_log_err(cx);
self.model = model;
self.api_url = api_url;
self.low_speed_timeout = low_speed_timeout;
self.settings_version = settings_version;
}
pub fn available_models(&self) -> impl Iterator<Item = &OllamaModel> {
self.available_models.iter()
}
pub fn settings_version(&self) -> usize {
self.settings_version
}
pub fn is_authenticated(&self) -> bool {
!self.available_models.is_empty()
}
pub fn authenticate(&self, cx: &AppContext) -> Task<Result<()>> {
if self.is_authenticated() {
Task::ready(Ok(()))
} else {
self.fetch_models(cx)
}
}
pub fn reset_credentials(&self, cx: &AppContext) -> Task<Result<()>> {
self.fetch_models(cx)
}
pub fn fetch_models(&self, cx: &AppContext) -> Task<Result<()>> {
let http_client = self.http_client.clone();
let api_url = self.api_url.clone();
// As a proxy for the server being "authenticated", we'll check if its up by fetching the models
cx.spawn(|mut cx| async move {
let models = get_models(http_client.as_ref(), &api_url, None).await?;
let mut models: Vec<OllamaModel> = models
.into_iter()
// Since there is no metadata from the Ollama API
// indicating which models are embedding models,
// simply filter out models with "-embed" in their name
.filter(|model| !model.name.contains("-embed"))
.map(|model| OllamaModel::new(&model.name))
.collect();
models.sort_by(|a, b| a.name.cmp(&b.name));
cx.update_global::<CompletionProvider, _>(|provider, _cx| {
if let CompletionProvider::Ollama(provider) = provider {
provider.available_models = models;
}
})
})
}
pub fn authentication_prompt(&self, cx: &mut WindowContext) -> AnyView {
cx.new_view(|cx| DownloadOllamaMessage::new(cx)).into()
}
pub fn model(&self) -> OllamaModel {
self.model.clone()
}
pub fn count_tokens(
&self,
request: LanguageModelRequest,
_cx: &AppContext,
) -> BoxFuture<'static, Result<usize>> {
// There is no endpoint for this _yet_ in Ollama
// see: https://github.com/ollama/ollama/issues/1716 and https://github.com/ollama/ollama/issues/3582
let token_count = request
.messages
.iter()
.map(|msg| msg.content.chars().count())
.sum::<usize>()
/ 4;
async move { Ok(token_count) }.boxed()
}
pub fn complete(
&self,
request: LanguageModelRequest,
) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>> {
let request = self.to_ollama_request(request);
let http_client = self.http_client.clone();
let api_url = self.api_url.clone();
let low_speed_timeout = self.low_speed_timeout;
async move {
let request =
stream_chat_completion(http_client.as_ref(), &api_url, request, low_speed_timeout);
let response = request.await?;
let stream = response
.filter_map(|response| async move {
match response {
Ok(delta) => {
let content = match delta.message {
ChatMessage::User { content } => content,
ChatMessage::Assistant { content } => content,
ChatMessage::System { content } => content,
};
Some(Ok(content))
}
Err(error) => Some(Err(error)),
}
})
.boxed();
Ok(stream)
}
.boxed()
}
fn to_ollama_request(&self, request: LanguageModelRequest) -> ChatRequest {
let model = match request.model {
LanguageModel::Ollama(model) => model,
_ => self.model(),
};
ChatRequest {
model: model.name,
messages: request
.messages
.into_iter()
.map(|msg| match msg.role {
Role::User => ChatMessage::User {
content: msg.content,
},
Role::Assistant => ChatMessage::Assistant {
content: msg.content,
},
Role::System => ChatMessage::System {
content: msg.content,
},
})
.collect(),
keep_alive: model.keep_alive,
stream: true,
options: Some(ChatOptions {
num_ctx: Some(model.max_tokens),
stop: Some(request.stop),
temperature: Some(request.temperature),
..Default::default()
}),
}
}
}
impl From<Role> for ollama::Role {
fn from(val: Role) -> Self {
match val {
Role::User => OllamaRole::User,
Role::Assistant => OllamaRole::Assistant,
Role::System => OllamaRole::System,
}
}
}
struct DownloadOllamaMessage {}
impl DownloadOllamaMessage {
pub fn new(_cx: &mut ViewContext<Self>) -> Self {
Self {}
}
fn render_download_button(&self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
ButtonLike::new("download_ollama_button")
.style(ButtonStyle::Filled)
.size(ButtonSize::Large)
.layer(ElevationIndex::ModalSurface)
.child(Label::new("Get Ollama"))
.on_click(move |_, cx| cx.open_url(OLLAMA_DOWNLOAD_URL))
}
}
impl Render for DownloadOllamaMessage {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
v_flex()
.p_4()
.size_full()
.child(Label::new("To use Ollama models via the assistant, Ollama must be running on your machine.").size(LabelSize::Large))
.child(
h_flex()
.w_full()
.p_4()
.justify_center()
.child(
self.render_download_button(cx)
)
)
.into_any()
}
}

View File

@@ -336,7 +336,7 @@ impl Render for AuthenticationPrompt {
h_flex()
.gap_2()
.child(Label::new("Click on").size(LabelSize::Small))
.child(Icon::new(IconName::ZedAssistant).size(IconSize::XSmall))
.child(Icon::new(IconName::Ai).size(IconSize::XSmall))
.child(
Label::new("in the status bar to close this panel.").size(LabelSize::Small),
),

View File

@@ -9,7 +9,7 @@ use regex::Regex;
use serde::{Deserialize, Serialize};
use std::{cmp::Reverse, ffi::OsStr, path::PathBuf, sync::Arc, time::Duration};
use ui::Context;
use util::{paths::CONTEXTS_DIR, ResultExt, TryFutureExt};
use util::{paths::CONVERSATIONS_DIR, ResultExt, TryFutureExt};
#[derive(Serialize, Deserialize)]
pub struct SavedMessage {
@@ -18,7 +18,7 @@ pub struct SavedMessage {
}
#[derive(Serialize, Deserialize)]
pub struct SavedContext {
pub struct SavedConversation {
pub id: Option<String>,
pub zed: String,
pub version: String,
@@ -28,12 +28,12 @@ pub struct SavedContext {
pub summary: String,
}
impl SavedContext {
impl SavedConversation {
pub const VERSION: &'static str = "0.2.0";
}
#[derive(Serialize, Deserialize)]
struct SavedContextV0_1_0 {
struct SavedConversationV0_1_0 {
id: Option<String>,
zed: String,
version: String,
@@ -46,26 +46,28 @@ struct SavedContextV0_1_0 {
}
#[derive(Clone)]
pub struct SavedContextMetadata {
pub struct SavedConversationMetadata {
pub title: String,
pub path: PathBuf,
pub mtime: chrono::DateTime<chrono::Local>,
}
pub struct ContextStore {
contexts_metadata: Vec<SavedContextMetadata>,
pub struct ConversationStore {
conversations_metadata: Vec<SavedConversationMetadata>,
fs: Arc<dyn Fs>,
_watch_updates: Task<Option<()>>,
}
impl ContextStore {
impl ConversationStore {
pub fn new(fs: Arc<dyn Fs>, cx: &mut AppContext) -> Task<Result<Model<Self>>> {
cx.spawn(|mut cx| async move {
const CONTEXT_WATCH_DURATION: Duration = Duration::from_millis(100);
let (mut events, _) = fs.watch(&CONTEXTS_DIR, CONTEXT_WATCH_DURATION).await;
const CONVERSATION_WATCH_DURATION: Duration = Duration::from_millis(100);
let (mut events, _) = fs
.watch(&CONVERSATIONS_DIR, CONVERSATION_WATCH_DURATION)
.await;
let this = cx.new_model(|cx: &mut ModelContext<Self>| Self {
contexts_metadata: Vec::new(),
conversations_metadata: Vec::new(),
fs,
_watch_updates: cx.spawn(|this, mut cx| {
async move {
@@ -86,41 +88,46 @@ impl ContextStore {
})
}
pub fn load(&self, path: PathBuf, cx: &AppContext) -> Task<Result<SavedContext>> {
pub fn load(&self, path: PathBuf, cx: &AppContext) -> Task<Result<SavedConversation>> {
let fs = self.fs.clone();
cx.background_executor().spawn(async move {
let saved_context = fs.load(&path).await?;
let saved_context_json = serde_json::from_str::<serde_json::Value>(&saved_context)?;
match saved_context_json
let saved_conversation = fs.load(&path).await?;
let saved_conversation_json =
serde_json::from_str::<serde_json::Value>(&saved_conversation)?;
match saved_conversation_json
.get("version")
.ok_or_else(|| anyhow!("version not found"))?
{
serde_json::Value::String(version) => match version.as_str() {
SavedContext::VERSION => {
Ok(serde_json::from_value::<SavedContext>(saved_context_json)?)
}
SavedConversation::VERSION => Ok(serde_json::from_value::<SavedConversation>(
saved_conversation_json,
)?),
"0.1.0" => {
let saved_context =
serde_json::from_value::<SavedContextV0_1_0>(saved_context_json)?;
Ok(SavedContext {
id: saved_context.id,
zed: saved_context.zed,
version: saved_context.version,
text: saved_context.text,
messages: saved_context.messages,
message_metadata: saved_context.message_metadata,
summary: saved_context.summary,
let saved_conversation = serde_json::from_value::<SavedConversationV0_1_0>(
saved_conversation_json,
)?;
Ok(SavedConversation {
id: saved_conversation.id,
zed: saved_conversation.zed,
version: saved_conversation.version,
text: saved_conversation.text,
messages: saved_conversation.messages,
message_metadata: saved_conversation.message_metadata,
summary: saved_conversation.summary,
})
}
_ => Err(anyhow!("unrecognized saved context version: {}", version)),
_ => Err(anyhow!(
"unrecognized saved conversation version: {}",
version
)),
},
_ => Err(anyhow!("version not found on saved context")),
_ => Err(anyhow!("version not found on saved conversation")),
}
})
}
pub fn search(&self, query: String, cx: &AppContext) -> Task<Vec<SavedContextMetadata>> {
let metadata = self.contexts_metadata.clone();
pub fn search(&self, query: String, cx: &AppContext) -> Task<Vec<SavedConversationMetadata>> {
let metadata = self.conversations_metadata.clone();
let executor = cx.background_executor().clone();
cx.background_executor().spawn(async move {
if query.is_empty() {
@@ -152,10 +159,10 @@ impl ContextStore {
fn reload(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
let fs = self.fs.clone();
cx.spawn(|this, mut cx| async move {
fs.create_dir(&CONTEXTS_DIR).await?;
fs.create_dir(&CONVERSATIONS_DIR).await?;
let mut paths = fs.read_dir(&CONTEXTS_DIR).await?;
let mut contexts = Vec::<SavedContextMetadata>::new();
let mut paths = fs.read_dir(&CONVERSATIONS_DIR).await?;
let mut conversations = Vec::<SavedConversationMetadata>::new();
while let Some(path) = paths.next().await {
let path = path?;
if path.extension() != Some(OsStr::new("json")) {
@@ -171,13 +178,13 @@ impl ContextStore {
.and_then(|name| name.to_str())
.zip(metadata)
{
// This is used to filter out contexts saved by the new assistant.
// This is used to filter out conversations saved by the new assistant.
if !re.is_match(file_name) {
continue;
}
if let Some(title) = re.replace(file_name, "").lines().next() {
contexts.push(SavedContextMetadata {
conversations.push(SavedConversationMetadata {
title: title.to_string(),
path,
mtime: metadata.mtime.into(),
@@ -185,10 +192,10 @@ impl ContextStore {
}
}
}
contexts.sort_unstable_by_key(|context| Reverse(context.mtime));
conversations.sort_unstable_by_key(|conversation| Reverse(conversation.mtime));
this.update(&mut cx, |this, cx| {
this.contexts_metadata = contexts;
this.conversations_metadata = conversations;
cx.notify();
})
})

File diff suppressed because it is too large Load Diff

View File

@@ -13,9 +13,10 @@ use futures::{
};
use fuzzy::StringMatchCandidate;
use gpui::{
actions, percentage, point, size, Animation, AnimationExt, AppContext, BackgroundExecutor,
Bounds, EventEmitter, Global, PromptLevel, ReadGlobal, Subscription, Task, TitlebarOptions,
Transformation, UpdateGlobal, View, WindowBounds, WindowHandle, WindowOptions,
actions, percentage, point, size, Animation, AnimationExt, AnyElement, AppContext,
BackgroundExecutor, Bounds, DevicePixels, EventEmitter, Global, PromptLevel, ReadGlobal,
Subscription, Task, TitlebarOptions, Transformation, UpdateGlobal, View, WindowBounds,
WindowHandle, WindowOptions,
};
use heed::{types::SerdeBincode, Database, RoTxn};
use language::{language_settings::SoftWrap, Buffer, LanguageRegistry};
@@ -25,7 +26,6 @@ use rope::Rope;
use serde::{Deserialize, Serialize};
use settings::Settings;
use std::{
cmp::Reverse,
future::Future,
path::PathBuf,
sync::{atomic::AtomicBool, Arc},
@@ -33,8 +33,8 @@ use std::{
};
use theme::ThemeSettings;
use ui::{
div, prelude::*, IconButtonShape, ListItem, ListItemSpacing, ParentElement, Render,
SharedString, Styled, TitleBar, Tooltip, ViewContext, VisualContext,
div, prelude::*, IconButtonShape, ListHeader, ListItem, ListItemSpacing, ListSubHeader,
ParentElement, Render, SharedString, Styled, TitleBar, Tooltip, ViewContext, VisualContext,
};
use util::{paths::PROMPTS_DIR, ResultExt, TryFutureExt};
use uuid::Uuid;
@@ -80,7 +80,11 @@ pub fn open_prompt_library(
cx.spawn(|cx| async move {
let store = store.await?;
cx.update(|cx| {
let bounds = Bounds::centered(None, size(px(1024.0), px(768.0)), cx);
let bounds = Bounds::centered(
None,
size(DevicePixels::from(1024), DevicePixels::from(768)),
cx,
);
cx.open_window(
WindowOptions {
titlebar: Some(TitlebarOptions {
@@ -93,7 +97,7 @@ pub fn open_prompt_library(
},
|cx| cx.new_view(|cx| PromptLibrary::new(store, language_registry, cx)),
)
})?
})
})
}
}
@@ -120,23 +124,41 @@ struct PromptEditor {
struct PromptPickerDelegate {
store: Arc<PromptStore>,
selected_index: usize,
matches: Vec<PromptMetadata>,
entries: Vec<PromptPickerEntry>,
}
enum PromptPickerEvent {
Selected { prompt_id: PromptId },
Selected { prompt_id: Option<PromptId> },
Confirmed { prompt_id: PromptId },
Deleted { prompt_id: PromptId },
ToggledDefault { prompt_id: PromptId },
}
#[derive(Debug)]
enum PromptPickerEntry {
DefaultPromptsHeader,
DefaultPromptsEmpty,
AllPromptsHeader,
AllPromptsEmpty,
Prompt(PromptMetadata),
}
impl PromptPickerEntry {
fn prompt_id(&self) -> Option<PromptId> {
match self {
PromptPickerEntry::Prompt(metadata) => Some(metadata.id),
_ => None,
}
}
}
impl EventEmitter<PromptPickerEvent> for Picker<PromptPickerDelegate> {}
impl PickerDelegate for PromptPickerDelegate {
type ListItem = ListItem;
type ListItem = AnyElement;
fn match_count(&self) -> usize {
self.matches.len()
self.entries.len()
}
fn selected_index(&self) -> usize {
@@ -145,11 +167,14 @@ impl PickerDelegate for PromptPickerDelegate {
fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>) {
self.selected_index = ix;
if let Some(prompt) = self.matches.get(self.selected_index) {
cx.emit(PromptPickerEvent::Selected {
prompt_id: prompt.id,
});
}
let prompt_id = if let Some(PromptPickerEntry::Prompt(prompt)) =
self.entries.get(self.selected_index)
{
Some(prompt.id)
} else {
None
};
cx.emit(PromptPickerEvent::Selected { prompt_id });
}
fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
@@ -158,24 +183,48 @@ impl PickerDelegate for PromptPickerDelegate {
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
let search = self.store.search(query);
let prev_prompt_id = self.matches.get(self.selected_index).map(|mat| mat.id);
let prev_prompt_id = self
.entries
.get(self.selected_index)
.and_then(|mat| mat.prompt_id());
cx.spawn(|this, mut cx| async move {
let (matches, selected_index) = cx
let (entries, selected_index) = cx
.background_executor()
.spawn(async move {
let matches = search.await;
let prompts = search.await;
let (default_prompts, prompts) = prompts
.into_iter()
.partition::<Vec<_>, _>(|prompt| prompt.default);
let mut entries = Vec::new();
entries.push(PromptPickerEntry::DefaultPromptsHeader);
if default_prompts.is_empty() {
entries.push(PromptPickerEntry::DefaultPromptsEmpty);
} else {
entries.extend(default_prompts.into_iter().map(PromptPickerEntry::Prompt));
}
entries.push(PromptPickerEntry::AllPromptsHeader);
if prompts.is_empty() {
entries.push(PromptPickerEntry::AllPromptsEmpty);
} else {
entries.extend(prompts.into_iter().map(PromptPickerEntry::Prompt));
}
let selected_index = prev_prompt_id
.and_then(|prev_prompt_id| {
matches.iter().position(|entry| entry.id == prev_prompt_id)
entries
.iter()
.position(|entry| entry.prompt_id() == Some(prev_prompt_id))
})
.or_else(|| entries.iter().position(|entry| entry.prompt_id().is_some()))
.unwrap_or(0);
(matches, selected_index)
(entries, selected_index)
})
.await;
this.update(&mut cx, |this, cx| {
this.delegate.matches = matches;
this.delegate.entries = entries;
this.delegate.set_selected_index(selected_index, cx);
cx.notify();
})
@@ -184,7 +233,7 @@ impl PickerDelegate for PromptPickerDelegate {
}
fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
if let Some(prompt) = self.matches.get(self.selected_index) {
if let Some(PromptPickerEntry::Prompt(prompt)) = self.entries.get(self.selected_index) {
cx.emit(PromptPickerEvent::Confirmed {
prompt_id: prompt.id,
});
@@ -199,59 +248,82 @@ impl PickerDelegate for PromptPickerDelegate {
selected: bool,
cx: &mut ViewContext<Picker<Self>>,
) -> Option<Self::ListItem> {
let prompt = self.matches.get(ix)?;
let default = prompt.default;
let prompt_id = prompt.id;
let element = ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.selected(selected)
.child(h_flex().h_5().line_height(relative(1.)).child(Label::new(
prompt.title.clone().unwrap_or("Untitled".into()),
)))
.end_slot::<IconButton>(default.then(|| {
IconButton::new("toggle-default-prompt", IconName::SparkleFilled)
.selected(true)
.icon_color(Color::Accent)
.shape(IconButtonShape::Square)
.tooltip(move |cx| Tooltip::text("Remove from Default Prompt", cx))
.on_click(cx.listener(move |_, _, cx| {
cx.emit(PromptPickerEvent::ToggledDefault { prompt_id })
}))
}))
.end_hover_slot(
h_flex()
.gap_2()
.child(
IconButton::new("delete-prompt", IconName::Trash)
.icon_color(Color::Muted)
.shape(IconButtonShape::Square)
.tooltip(move |cx| Tooltip::text("Delete Prompt", cx))
.on_click(cx.listener(move |_, _, cx| {
cx.emit(PromptPickerEvent::Deleted { prompt_id })
})),
let prompt = self.entries.get(ix)?;
let element = match prompt {
PromptPickerEntry::DefaultPromptsHeader => ListHeader::new("Default Prompts")
.inset(true)
.start_slot(
Icon::new(IconName::Sparkle)
.color(Color::Muted)
.size(IconSize::XSmall),
)
.selected(selected)
.into_any_element(),
PromptPickerEntry::DefaultPromptsEmpty => {
ListSubHeader::new("Star a prompt to add it to the default context")
.inset(true)
.selected(selected)
.into_any_element()
}
PromptPickerEntry::AllPromptsHeader => ListHeader::new("All Prompts")
.inset(true)
.start_slot(
Icon::new(IconName::Library)
.color(Color::Muted)
.size(IconSize::XSmall),
)
.selected(selected)
.into_any_element(),
PromptPickerEntry::AllPromptsEmpty => ListSubHeader::new("No prompts")
.inset(true)
.selected(selected)
.into_any_element(),
PromptPickerEntry::Prompt(prompt) => {
let default = prompt.default;
let prompt_id = prompt.id;
ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.selected(selected)
.child(h_flex().h_5().line_height(relative(1.)).child(Label::new(
prompt.title.clone().unwrap_or("Untitled".into()),
)))
.end_hover_slot(
h_flex()
.gap_2()
.child(
IconButton::new("delete-prompt", IconName::Trash)
.icon_color(Color::Muted)
.shape(IconButtonShape::Square)
.tooltip(move |cx| Tooltip::text("Delete Prompt", cx))
.on_click(cx.listener(move |_, _, cx| {
cx.emit(PromptPickerEvent::Deleted { prompt_id })
})),
)
.child(
IconButton::new("toggle-default-prompt", IconName::Sparkle)
.selected(default)
.selected_icon(IconName::SparkleFilled)
.icon_color(if default { Color::Accent } else { Color::Muted })
.shape(IconButtonShape::Square)
.tooltip(move |cx| {
Tooltip::text(
if default {
"Remove from Default Prompt"
} else {
"Add to Default Prompt"
},
cx,
)
})
.on_click(cx.listener(move |_, _, cx| {
cx.emit(PromptPickerEvent::ToggledDefault { prompt_id })
})),
),
)
.child(
IconButton::new("toggle-default-prompt", IconName::Sparkle)
.selected(default)
.selected_icon(IconName::SparkleFilled)
.icon_color(if default { Color::Accent } else { Color::Muted })
.shape(IconButtonShape::Square)
.tooltip(move |cx| {
Tooltip::text(
if default {
"Remove from Default Prompt"
} else {
"Add to Default Prompt"
},
cx,
)
})
.on_click(cx.listener(move |_, _, cx| {
cx.emit(PromptPickerEvent::ToggledDefault { prompt_id })
})),
),
);
.into_any_element()
}
};
Some(element)
}
@@ -277,13 +349,11 @@ impl PromptLibrary {
let delegate = PromptPickerDelegate {
store: store.clone(),
selected_index: 0,
matches: Vec::new(),
entries: Vec::new(),
};
let picker = cx.new_view(|cx| {
let picker = Picker::uniform_list(delegate, cx)
.modal(false)
.max_height(None);
let picker = Picker::list(delegate, cx).modal(false).max_height(None);
picker.focus(cx);
picker
});
@@ -306,7 +376,11 @@ impl PromptLibrary {
) {
match event {
PromptPickerEvent::Selected { prompt_id } => {
self.load_prompt(*prompt_id, false, cx);
if let Some(prompt_id) = *prompt_id {
self.load_prompt(prompt_id, false, cx);
} else {
self.focus_picker(&Default::default(), cx);
}
}
PromptPickerEvent::Confirmed { prompt_id } => {
self.load_prompt(*prompt_id, true, cx);
@@ -493,23 +567,21 @@ impl PromptLibrary {
if let Some(prompt_id) = prompt_id {
if picker
.delegate
.matches
.entries
.get(picker.delegate.selected_index())
.map_or(true, |old_selected_prompt| {
old_selected_prompt.id != prompt_id
old_selected_prompt.prompt_id() != Some(prompt_id)
})
{
if let Some(ix) = picker
.delegate
.matches
.entries
.iter()
.position(|mat| mat.id == prompt_id)
.position(|mat| mat.prompt_id() == Some(prompt_id))
{
picker.set_selected_index(ix, true, cx);
}
}
} else {
picker.focus(cx);
}
});
cx.notify();
@@ -1033,7 +1105,7 @@ impl PromptStore {
let cached_metadata = self.metadata_cache.read().metadata.clone();
let executor = self.executor.clone();
self.executor.spawn(async move {
let mut matches = if query.is_empty() {
if query.is_empty() {
cached_metadata
} else {
let candidates = cached_metadata
@@ -1059,9 +1131,7 @@ impl PromptStore {
.into_iter()
.map(|mat| cached_metadata[mat.candidate_id].clone())
.collect()
};
matches.sort_by_key(|metadata| Reverse(metadata.default));
matches
}
})
}

View File

@@ -1,4 +1,4 @@
use crate::assistant_panel::ContextEditor;
use crate::assistant_panel::ConversationEditor;
use anyhow::Result;
pub use assistant_slash_command::{SlashCommand, SlashCommandOutput, SlashCommandRegistry};
use editor::{CompletionProvider, Editor};
@@ -20,7 +20,6 @@ pub mod active_command;
pub mod default_command;
pub mod fetch_command;
pub mod file_command;
pub mod now_command;
pub mod project_command;
pub mod prompt_command;
pub mod rustdoc_command;
@@ -30,7 +29,7 @@ pub mod tabs_command;
pub(crate) struct SlashCommandCompletionProvider {
commands: Arc<SlashCommandRegistry>,
cancel_flag: Mutex<Arc<AtomicBool>>,
editor: Option<WeakView<ContextEditor>>,
editor: Option<WeakView<ConversationEditor>>,
workspace: Option<WeakView<Workspace>>,
}
@@ -44,7 +43,7 @@ pub(crate) struct SlashCommandLine {
impl SlashCommandCompletionProvider {
pub fn new(
commands: Arc<SlashCommandRegistry>,
editor: Option<WeakView<ContextEditor>>,
editor: Option<WeakView<ConversationEditor>>,
workspace: Option<WeakView<Workspace>>,
) -> Self {
Self {

View File

@@ -1,5 +1,3 @@
use std::cell::RefCell;
use std::rc::Rc;
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
@@ -7,19 +5,12 @@ 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, markdown, TagHandler};
use html_to_markdown::convert_html_to_markdown;
use http::{AsyncBody, HttpClient, HttpClientWithUrl};
use language::LspAdapterDelegate;
use ui::{prelude::*, ButtonLike, ElevationIndex};
use workspace::Workspace;
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
enum ContentType {
Html,
Plaintext,
Json,
}
pub(crate) struct FetchSlashCommand;
impl FetchSlashCommand {
@@ -46,52 +37,7 @@ impl FetchSlashCommand {
);
}
let Some(content_type) = response.headers().get("content-type") else {
bail!("missing Content-Type header");
};
let content_type = content_type
.to_str()
.context("invalid Content-Type header")?;
let content_type = match content_type {
"text/html" => ContentType::Html,
"text/plain" => ContentType::Plaintext,
"application/json" => ContentType::Json,
_ => ContentType::Html,
};
match content_type {
ContentType::Html => {
let mut handlers: Vec<TagHandler> = vec![
Rc::new(RefCell::new(markdown::ParagraphHandler)),
Rc::new(RefCell::new(markdown::HeadingHandler)),
Rc::new(RefCell::new(markdown::ListHandler)),
Rc::new(RefCell::new(markdown::TableHandler::new())),
Rc::new(RefCell::new(markdown::StyledTextHandler)),
];
if url.contains("wikipedia.org") {
use html_to_markdown::structure::wikipedia;
handlers.push(Rc::new(RefCell::new(wikipedia::WikipediaChromeRemover)));
handlers.push(Rc::new(RefCell::new(wikipedia::WikipediaInfoboxHandler)));
handlers.push(Rc::new(
RefCell::new(wikipedia::WikipediaCodeHandler::new()),
));
} else {
handlers.push(Rc::new(RefCell::new(markdown::CodeHandler)));
}
convert_html_to_markdown(&body[..], &mut handlers)
}
ContentType::Plaintext => Ok(std::str::from_utf8(&body)?.to_owned()),
ContentType::Json => {
let json: serde_json::Value = serde_json::from_slice(&body)?;
Ok(format!(
"```json\n{}\n```",
serde_json::to_string_pretty(&json)?
))
}
}
convert_html_to_markdown(&body[..])
}
}

View File

@@ -1,83 +0,0 @@
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use anyhow::Result;
use assistant_slash_command::{SlashCommand, SlashCommandOutput, SlashCommandOutputSection};
use chrono::{DateTime, Local};
use gpui::{AppContext, Task, WeakView};
use language::LspAdapterDelegate;
use ui::{prelude::*, ButtonLike, ElevationIndex};
use workspace::Workspace;
pub(crate) struct NowSlashCommand;
impl SlashCommand for NowSlashCommand {
fn name(&self) -> String {
"now".into()
}
fn description(&self) -> String {
"insert the current date and time".into()
}
fn menu_text(&self) -> String {
"Insert current date and time".into()
}
fn requires_argument(&self) -> bool {
false
}
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 now = Local::now();
let text = format!("Today is {now}.", now = now.to_rfc3339());
let range = 0..text.len();
Task::ready(Ok(SlashCommandOutput {
text,
sections: vec![SlashCommandOutputSection {
range,
render_placeholder: Arc::new(move |id, unfold, _cx| {
NowPlaceholder { id, unfold, now }.into_any_element()
}),
}],
run_commands_in_text: false,
}))
}
}
#[derive(IntoElement)]
struct NowPlaceholder {
pub id: ElementId,
pub unfold: Arc<dyn Fn(&mut WindowContext)>,
pub now: DateTime<Local>,
}
impl RenderOnce for NowPlaceholder {
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::CountdownTimer))
.child(Label::new(self.now.to_rfc3339()))
.on_click(move |_, cx| unfold(cx))
}
}

View File

@@ -7,11 +7,10 @@ use assistant_slash_command::{SlashCommand, SlashCommandOutput, SlashCommandOutp
use fs::Fs;
use futures::AsyncReadExt;
use gpui::{AppContext, Model, Task, WeakView};
use html_to_markdown::convert_rustdoc_to_markdown;
use http::{AsyncBody, HttpClient, HttpClientWithUrl};
use language::LspAdapterDelegate;
use project::{Project, ProjectPath};
use rustdoc::crawler::LocalProvider;
use rustdoc::{convert_rustdoc_to_markdown, RustdocStore};
use ui::{prelude::*, ButtonLike, ElevationIndex};
use workspace::Workspace;
@@ -43,9 +42,10 @@ impl RustdocSlashCommand {
local_cargo_doc_path.push("index.html");
if let Ok(contents) = fs.load(&local_cargo_doc_path).await {
let (markdown, _items) = convert_rustdoc_to_markdown(contents.as_bytes())?;
return Ok((RustdocSource::Local, markdown));
return Ok((
RustdocSource::Local,
convert_rustdoc_to_markdown(contents.as_bytes())?,
));
}
}
@@ -78,9 +78,10 @@ impl RustdocSlashCommand {
);
}
let (markdown, _items) = convert_rustdoc_to_markdown(&body[..])?;
Ok((RustdocSource::DocsDotRs, markdown))
Ok((
RustdocSource::DocsDotRs,
convert_rustdoc_to_markdown(&body[..])?,
))
}
fn path_to_cargo_toml(project: Model<Project>, cx: &mut AppContext) -> Option<Arc<Path>> {
@@ -116,19 +117,12 @@ impl SlashCommand for RustdocSlashCommand {
fn complete_argument(
&self,
query: String,
_query: String,
_cancel: Arc<AtomicBool>,
_workspace: Option<WeakView<Workspace>>,
cx: &mut AppContext,
_cx: &mut AppContext,
) -> Task<Result<Vec<String>>> {
let store = RustdocStore::global(cx);
cx.background_executor().spawn(async move {
let items = store.search(query).await;
Ok(items
.into_iter()
.map(|(crate_name, item)| format!("{crate_name}::{}", item.display()))
.collect())
})
Task::ready(Ok(Vec::new()))
}
fn run(
@@ -148,67 +142,7 @@ impl SlashCommand for RustdocSlashCommand {
let project = workspace.read(cx).project().clone();
let fs = project.read(cx).fs().clone();
let http_client = workspace.read(cx).client().http_client();
let path_to_cargo_toml = Self::path_to_cargo_toml(project, cx);
let mut item_path = String::new();
let mut crate_name_to_index = None;
let mut args = argument.split(' ').map(|word| word.trim());
while let Some(arg) = args.next() {
if arg == "--index" {
let Some(crate_name) = args.next() else {
return Task::ready(Err(anyhow!("no crate name provided to --index")));
};
crate_name_to_index = Some(crate_name.to_string());
continue;
}
item_path.push_str(arg);
}
if let Some(crate_name_to_index) = crate_name_to_index {
let index_task = cx.background_executor().spawn({
let rustdoc_store = RustdocStore::global(cx);
let fs = fs.clone();
let crate_name_to_index = crate_name_to_index.clone();
async move {
let cargo_workspace_root = path_to_cargo_toml
.and_then(|path| path.parent().map(|path| path.to_path_buf()))
.ok_or_else(|| anyhow!("no Cargo workspace root found"))?;
let provider = Box::new(LocalProvider::new(fs, cargo_workspace_root));
rustdoc_store
.index(crate_name_to_index.clone(), provider)
.await?;
anyhow::Ok(format!("Indexed {crate_name_to_index}"))
}
});
return cx.foreground_executor().spawn(async move {
let text = index_task.await?;
let range = 0..text.len();
Ok(SlashCommandOutput {
text,
sections: vec![SlashCommandOutputSection {
range,
render_placeholder: Arc::new(move |id, unfold, _cx| {
RustdocIndexPlaceholder {
id,
unfold,
source: RustdocSource::Local,
crate_name: SharedString::from(crate_name_to_index.clone()),
}
.into_any_element()
}),
}],
run_commands_in_text: false,
})
});
}
let mut path_components = item_path.split("::");
let mut path_components = argument.split("::");
let crate_name = match path_components
.next()
.ok_or_else(|| anyhow!("missing crate name"))
@@ -216,37 +150,29 @@ impl SlashCommand for RustdocSlashCommand {
Ok(crate_name) => crate_name.to_string(),
Err(err) => return Task::ready(Err(err)),
};
let item_path = path_components.map(ToString::to_string).collect::<Vec<_>>();
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 rustdoc_store = RustdocStore::global(cx);
let crate_name = crate_name.clone();
let item_path = item_path.clone();
let module_path = module_path.clone();
async move {
let item_docs = rustdoc_store
.load(crate_name.clone(), Some(item_path.join("::")))
.await;
if let Ok(item_docs) = item_docs {
anyhow::Ok((RustdocSource::Local, item_docs))
} else {
Self::build_message(
fs,
http_client,
crate_name,
item_path,
path_to_cargo_toml.as_deref(),
)
.await
}
Self::build_message(
fs,
http_client,
crate_name,
module_path,
path_to_cargo_toml.as_deref(),
)
.await
}
});
let crate_name = SharedString::from(crate_name);
let module_path = if item_path.is_empty() {
let module_path = if module_path.is_empty() {
None
} else {
Some(SharedString::from(item_path.join("::")))
Some(SharedString::from(module_path.join("::")))
};
cx.foreground_executor().spawn(async move {
let (source, text) = text.await?;
@@ -304,31 +230,3 @@ impl RenderOnce for RustdocPlaceholder {
.on_click(move |_, cx| unfold(cx))
}
}
#[derive(IntoElement)]
struct RustdocIndexPlaceholder {
pub id: ElementId,
pub unfold: Arc<dyn Fn(&mut WindowContext)>,
pub source: RustdocSource,
pub crate_name: SharedString,
}
impl RenderOnce for RustdocIndexPlaceholder {
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::FileRust))
.child(Label::new(format!(
"rustdoc index ({source}): {crate_name}",
crate_name = self.crate_name,
source = match self.source {
RustdocSource::Local => "local",
RustdocSource::DocsDotRs => "docs.rs",
}
)))
.on_click(move |_, cx| unfold(cx))
}
}

View File

@@ -3,7 +3,7 @@ use crate::channel_chat::ChannelChatEvent;
use super::*;
use client::{test::FakeServer, Client, UserStore};
use clock::FakeSystemClock;
use gpui::{AppContext, Context, Model, SemanticVersion, TestAppContext};
use gpui::{AppContext, Context, Model, TestAppContext};
use http::FakeHttpClient;
use rpc::proto::{self};
use settings::SettingsStore;
@@ -340,7 +340,7 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
fn init_test(cx: &mut AppContext) -> Model<ChannelStore> {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
release_channel::init(SemanticVersion::default(), cx);
release_channel::init("0.0.0", cx);
client::init_settings(cx);
let clock = Arc::new(FakeSystemClock::default());

View File

@@ -19,6 +19,7 @@ path = "src/main.rs"
[dependencies]
anyhow.workspace = true
clap.workspace = true
libc.workspace = true
ipc-channel = "0.18"
once_cell.workspace = true
release_channel.workspace = true

View File

@@ -161,7 +161,10 @@ mod linux {
env,
ffi::OsString,
io,
os::unix::net::{SocketAddr, UnixDatagram},
os::{
linux::net::SocketAddrExt,
unix::net::{SocketAddr, UnixDatagram},
},
path::{Path, PathBuf},
process::{self, ExitStatus},
thread,
@@ -172,7 +175,6 @@ mod linux {
use cli::FORCE_CLI_MODE_ENV_VAR_NAME;
use fork::Fork;
use once_cell::sync::Lazy;
use util::paths;
use crate::{Detect, InstalledApp};
@@ -221,9 +223,12 @@ mod linux {
}
fn launch(&self, ipc_url: String) -> anyhow::Result<()> {
let sock_path = paths::SUPPORT_DIR.join(format!("zed-{}.sock", *RELEASE_CHANNEL));
let uid: u32 = unsafe { libc::getuid() };
let sock_addr =
SocketAddr::from_abstract_name(format!("zed-{}-{}", *RELEASE_CHANNEL, uid))?;
let sock = UnixDatagram::unbound()?;
if sock.connect(&sock_path).is_err() {
if sock.connect_addr(&sock_addr).is_err() {
self.boot_background(ipc_url)?;
} else {
sock.send(ipc_url.as_bytes())?;

View File

@@ -24,7 +24,6 @@ chrono = { workspace = true, features = ["serde"] }
clock.workspace = true
collections.workspace = true
feature_flags.workspace = true
fs.workspace = true
futures.workspace = true
gpui.workspace = true
http.workspace = true
@@ -61,12 +60,6 @@ settings = { workspace = true, features = ["test-support"] }
util = { workspace = true, features = ["test-support"] }
http = { workspace = true, features = ["test-support"] }
[target.'cfg(target_os = "windows")'.dependencies]
windows.workspace = true
[target.'cfg(target_os = "macos")'.dependencies]
cocoa.workspace = true
[target.'cfg(target_os = "linux")'.dependencies]
async-native-tls = {"version" = "0.5.0", features = ["vendored"]}
# This is an indirect dependency of async-tungstenite that is included

View File

@@ -1429,31 +1429,6 @@ impl Client {
}
}
pub fn request_dynamic(
&self,
envelope: proto::Envelope,
request_type: &'static str,
) -> impl Future<Output = Result<proto::Envelope>> {
let client_id = self.id();
log::debug!(
"rpc request start. client_id:{}. name:{}",
client_id,
request_type
);
let response = self
.connection_id()
.map(|conn_id| self.peer.request_dynamic(conn_id, envelope, request_type));
async move {
let response = response?.await;
log::debug!(
"rpc request finish. client_id:{}. name:{}",
client_id,
request_type
);
Ok(response?.0)
}
}
fn respond<T: RequestMessage>(&self, receipt: Receipt<T>, response: T::Response) -> Result<()> {
log::debug!("rpc respond. client_id:{}. name:{}", self.id(), T::NAME);
self.peer.respond(receipt, response)
@@ -1729,7 +1704,6 @@ mod tests {
use gpui::{BackgroundExecutor, Context, TestAppContext};
use http::FakeHttpClient;
use parking_lot::Mutex;
use proto::TypedEnvelope;
use settings::SettingsStore;
use std::future;

View File

@@ -4,7 +4,7 @@ use crate::{ChannelId, TelemetrySettings};
use chrono::{DateTime, Utc};
use clock::SystemClock;
use futures::Future;
use gpui::{AppContext, BackgroundExecutor, Task};
use gpui::{AppContext, AppMetadata, BackgroundExecutor, Task};
use http::{self, HttpClient, HttpClientWithUrl, Method};
use once_cell::sync::Lazy;
use parking_lot::Mutex;
@@ -39,6 +39,7 @@ struct TelemetryState {
installation_id: Option<Arc<str>>, // Per app installation (different for dev, nightly, preview, and stable)
session_id: Option<String>, // Per app launch
release_channel: Option<&'static str>,
app_metadata: AppMetadata,
architecture: &'static str,
events_queue: Vec<EventWrapper>,
flush_events_task: Option<Task<()>>,
@@ -47,10 +48,6 @@ struct TelemetryState {
first_event_date_time: Option<DateTime<Utc>>,
event_coalescer: EventCoalescer,
max_queue_size: usize,
os_name: String,
app_version: String,
os_version: Option<String>,
}
#[cfg(debug_assertions)]
@@ -74,87 +71,6 @@ static ZED_CLIENT_CHECKSUM_SEED: Lazy<Option<Vec<u8>>> = Lazy::new(|| {
})
});
pub fn os_name() -> String {
#[cfg(target_os = "macos")]
{
"macOS".to_string()
}
#[cfg(target_os = "linux")]
{
format!("Linux {}", gpui::guess_compositor())
}
#[cfg(target_os = "windows")]
{
"Windows".to_string()
}
}
/// Note: This might do blocking IO! Only call from background threads
pub fn os_version() -> String {
#[cfg(target_os = "macos")]
{
use cocoa::base::nil;
use cocoa::foundation::NSProcessInfo;
unsafe {
let process_info = cocoa::foundation::NSProcessInfo::processInfo(nil);
let version = process_info.operatingSystemVersion();
gpui::SemanticVersion::new(
version.majorVersion as usize,
version.minorVersion as usize,
version.patchVersion as usize,
)
.to_string()
}
}
#[cfg(target_os = "linux")]
{
use std::path::Path;
let content = if let Ok(file) = std::fs::read_to_string(&Path::new("/etc/os-release")) {
file
} else if let Ok(file) = std::fs::read_to_string(&Path::new("/usr/lib/os-release")) {
file
} else {
log::error!("Failed to load /etc/os-release, /usr/lib/os-release");
"".to_string()
};
let mut name = "unknown".to_string();
let mut version = "unknown".to_string();
for line in content.lines() {
if line.starts_with("ID=") {
name = line.trim_start_matches("ID=").trim_matches('"').to_string();
}
if line.starts_with("VERSION_ID=") {
version = line
.trim_start_matches("VERSION_ID=")
.trim_matches('"')
.to_string();
}
}
format!("{} {}", name, version)
}
#[cfg(target_os = "windows")]
{
let mut info = unsafe { std::mem::zeroed() };
let status = unsafe { windows::Wdk::System::SystemServices::RtlGetVersion(&mut info) };
if status.is_ok() {
gpui::SemanticVersion::new(
info.dwMajorVersion as _,
info.dwMinorVersion as _,
info.dwBuildNumber as _,
)
.to_string()
} else {
"unknown".to_string()
}
}
}
impl Telemetry {
pub fn new(
clock: Arc<dyn SystemClock>,
@@ -168,6 +84,7 @@ impl Telemetry {
let state = Arc::new(Mutex::new(TelemetryState {
settings: *TelemetrySettings::get_global(cx),
app_metadata: cx.app_metadata(),
architecture: env::consts::ARCH,
release_channel,
installation_id: None,
@@ -180,10 +97,6 @@ impl Telemetry {
first_event_date_time: None,
event_coalescer: EventCoalescer::new(clock.clone()),
max_queue_size: MAX_QUEUE_LEN,
os_version: None,
os_name: os_name(),
app_version: release_channel::AppVersion::global(cx).to_string(),
}));
#[cfg(not(debug_assertions))]
@@ -255,9 +168,6 @@ impl Telemetry {
let mut state = self.state.lock();
state.installation_id = installation_id.map(|id| id.into());
state.session_id = Some(session_id);
state.app_version = release_channel::AppVersion::global(cx).to_string();
state.os_name = os_version();
drop(state);
let this = self.clone();
@@ -535,14 +445,20 @@ impl Telemetry {
{
let state = this.state.lock();
let request_body = EventRequestBody {
installation_id: state.installation_id.as_deref().map(Into::into),
session_id: state.session_id.clone(),
is_staff: state.is_staff,
app_version: state.app_version.clone(),
os_name: state.os_name.clone(),
os_version: state.os_version.clone(),
app_version: state
.app_metadata
.app_version
.unwrap_or_default()
.to_string(),
os_name: state.app_metadata.os_name.to_string(),
os_version: state
.app_metadata
.os_version
.map(|version| version.to_string()),
architecture: state.architecture.to_string(),
release_channel: state.release_channel.map(Into::into),

View File

@@ -96,7 +96,6 @@ node_runtime.workspace = true
notifications = { workspace = true, features = ["test-support"] }
pretty_assertions.workspace = true
project = { workspace = true, features = ["test-support"] }
recent_projects = { workspace = true }
release_channel.workspace = true
dev_server_projects.workspace = true
rpc = { workspace = true, features = ["test-support"] }

View File

@@ -308,14 +308,6 @@ pub async fn post_panic(
.map_err(|_| Error::Http(StatusCode::BAD_REQUEST, "invalid json".into()))?;
let panic = report.panic;
// better OS reporting for linux (because linux is hard):
// - Remove os_version/app_version/os_name from the gpui platform trait
// - Move platform processing data into client/telemetry
// - Duplicate some small code in macOS platform for a version check
// - Add GPUI API for reporting the selected platform integration
// - macos-blade, macos-metal, linux-X11, linux-headless
// if cfg(macos( { "Macos" } else { "Linux-{cx.compositor_name()"} ))
tracing::error!(
service = "client",
version = %panic.app_version,

View File

@@ -548,9 +548,6 @@ impl Server {
.add_request_handler(user_handler(
forward_mutating_project_request::<proto::RestartLanguageServers>,
))
.add_request_handler(user_handler(
forward_mutating_project_request::<proto::LinkedEditingRange>,
))
.add_message_handler(create_buffer_for_peer)
.add_request_handler(update_buffer)
.add_message_handler(broadcast_project_message_from_host::<proto::RefreshInlayHints>)

View File

@@ -68,7 +68,6 @@ async fn test_dev_server(cx: &mut gpui::TestAppContext, cx2: &mut gpui::TestAppC
assert_eq!(projects.len(), 1);
assert_eq!(projects[0].path, "/remote");
workspace::join_dev_server_project(
projects[0].id,
projects[0].project_id.unwrap(),
client.app_state.clone(),
None,
@@ -208,7 +207,6 @@ async fn create_dev_server_project(
assert_eq!(projects.len(), 1);
assert_eq!(projects[0].path, "/remote");
workspace::join_dev_server_project(
projects[0].id,
projects[0].project_id.unwrap(),
client_app_state,
None,
@@ -493,7 +491,6 @@ async fn test_dev_server_reconnect(
.update(cx2, |store, cx| {
let projects = store.dev_server_projects();
workspace::join_dev_server_project(
projects[0].id,
projects[0].project_id.unwrap(),
client2.app_state.clone(),
None,

View File

@@ -30,7 +30,6 @@ use project::{
project_settings::{InlineBlameSettings, ProjectSettings},
SERVER_PROGRESS_DEBOUNCE_TIMEOUT,
};
use recent_projects::disconnected_overlay::DisconnectedOverlay;
use rpc::RECEIVE_TIMEOUT;
use serde_json::json;
use settings::SettingsStore;
@@ -60,7 +59,6 @@ async fn test_host_disconnect(
.await;
cx_b.update(editor::init);
cx_b.update(recent_projects::init);
client_a
.fs()
@@ -85,7 +83,7 @@ async fn test_host_disconnect(
let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
cx_a.background_executor.run_until_parked();
assert!(worktree_a.read_with(cx_a, |tree, _| tree.has_update_observer()));
assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared()));
let workspace_b = cx_b
.add_window(|cx| Workspace::new(None, project_b.clone(), client_b.app_state.clone(), cx));
@@ -122,13 +120,14 @@ async fn test_host_disconnect(
project_b.read_with(cx_b, |project, _| project.is_read_only());
assert!(worktree_a.read_with(cx_a, |tree, _| !tree.has_update_observer()));
assert!(worktree_a.read_with(cx_a, |tree, _| !tree.as_local().unwrap().is_shared()));
// Ensure client B's edited state is reset and that the whole window is blurred.
workspace_b
.update(cx_b, |workspace, cx| {
assert!(workspace.active_modal::<DisconnectedOverlay>(cx).is_some());
assert!(!workspace.is_edited());
assert_eq!(cx.focused(), None);
assert!(!workspace.is_edited())
})
.unwrap();

View File

@@ -1378,7 +1378,7 @@ async fn test_unshare_project(
let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
executor.run_until_parked();
assert!(worktree_a.read_with(cx_a, |tree, _| tree.has_update_observer()));
assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared()));
project_b
.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
@@ -1403,7 +1403,7 @@ async fn test_unshare_project(
.unwrap();
executor.run_until_parked();
assert!(worktree_a.read_with(cx_a, |tree, _| !tree.has_update_observer()));
assert!(worktree_a.read_with(cx_a, |tree, _| !tree.as_local().unwrap().is_shared()));
assert!(project_c.read_with(cx_c, |project, _| project.is_disconnected()));
@@ -1415,7 +1415,7 @@ async fn test_unshare_project(
let project_c2 = client_c.build_dev_server_project(project_id, cx_c).await;
executor.run_until_parked();
assert!(worktree_a.read_with(cx_a, |tree, _| tree.has_update_observer()));
assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared()));
project_c2
.update(cx_c, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
.await
@@ -1522,7 +1522,7 @@ async fn test_project_reconnect(
executor.run_until_parked();
let worktree1_id = worktree_a1.read_with(cx_a, |worktree, _| {
assert!(worktree.has_update_observer());
assert!(worktree.as_local().unwrap().is_shared());
worktree.id()
});
let (worktree_a2, _) = project_a1
@@ -1534,7 +1534,7 @@ async fn test_project_reconnect(
executor.run_until_parked();
let worktree2_id = worktree_a2.read_with(cx_a, |tree, _| {
assert!(tree.has_update_observer());
assert!(tree.as_local().unwrap().is_shared());
tree.id()
});
executor.run_until_parked();
@@ -1567,7 +1567,9 @@ async fn test_project_reconnect(
assert_eq!(project.collaborators().len(), 1);
});
worktree_a1.read_with(cx_a, |tree, _| assert!(tree.has_update_observer()));
worktree_a1.read_with(cx_a, |tree, _| {
assert!(tree.as_local().unwrap().is_shared())
});
// While client A is disconnected, add and remove files from client A's project.
client_a
@@ -1609,7 +1611,7 @@ async fn test_project_reconnect(
.await;
let worktree3_id = worktree_a3.read_with(cx_a, |tree, _| {
assert!(!tree.has_update_observer());
assert!(!tree.as_local().unwrap().is_shared());
tree.id()
});
executor.run_until_parked();
@@ -1632,7 +1634,7 @@ async fn test_project_reconnect(
project_a1.read_with(cx_a, |project, cx| {
assert!(project.is_shared());
assert!(worktree_a1.read(cx).has_update_observer());
assert!(worktree_a1.read(cx).as_local().unwrap().is_shared());
assert_eq!(
worktree_a1
.read(cx)
@@ -1650,7 +1652,7 @@ async fn test_project_reconnect(
"subdir2/i.txt"
]
);
assert!(worktree_a3.read(cx).has_update_observer());
assert!(worktree_a3.read(cx).as_local().unwrap().is_shared());
assert_eq!(
worktree_a3
.read(cx)
@@ -1731,7 +1733,7 @@ async fn test_project_reconnect(
executor.run_until_parked();
let worktree4_id = worktree_a4.read_with(cx_a, |tree, _| {
assert!(tree.has_update_observer());
assert!(tree.as_local().unwrap().is_shared());
tree.id()
});
project_a1.update(cx_a, |project, cx| {

View File

@@ -69,6 +69,7 @@ struct TestPlan<T: RandomizedTest> {
pub struct UserTestPlan {
pub user_id: UserId,
pub username: String,
pub allow_client_reconnection: bool,
pub allow_client_disconnection: bool,
next_root_id: usize,
operation_ix: usize,
@@ -236,6 +237,7 @@ impl<T: RandomizedTest> TestPlan<T> {
next_root_id: 0,
operation_ix: 0,
allow_client_disconnection,
allow_client_reconnection,
});
}

View File

@@ -161,7 +161,7 @@ impl TestServer {
}
let settings = SettingsStore::test(cx);
cx.set_global(settings);
release_channel::init(SemanticVersion::default(), cx);
release_channel::init("0.0.0", cx);
client::init_settings(cx);
});
@@ -327,7 +327,7 @@ impl TestServer {
}
let settings = SettingsStore::test(cx);
cx.set_global(settings);
release_channel::init(SemanticVersion::default(), cx);
release_channel::init("0.0.0", cx);
client::init_settings(cx);
});
let (dev_server_id, _) = split_dev_server_token(&access_token).unwrap();

View File

@@ -413,17 +413,6 @@ impl CollabTitlebarItem {
);
}
if self.project.read(cx).is_disconnected() {
return Some(
Button::new("disconnected", "Disconnected")
.disabled(true)
.color(Color::Disabled)
.style(ButtonStyle::Subtle)
.label_size(LabelSize::Small)
.into_any_element(),
);
}
let host = self.project.read(cx).host()?;
let host_user = self.user_store.read(cx).get_cached_user(host.user_id)?;
let participant_index = self
@@ -697,7 +686,7 @@ impl CollabTitlebarItem {
.on_click(|_, cx| {
if let Some(auto_updater) = auto_update::AutoUpdater::get(cx) {
if auto_updater.read(cx).status().is_updated() {
workspace::reload(&Default::default(), cx);
workspace::restart(&Default::default(), cx);
return;
}
}

View File

@@ -13,8 +13,8 @@ use call::{report_call_event_for_room, ActiveCall};
pub use collab_panel::CollabPanel;
pub use collab_titlebar_item::CollabTitlebarItem;
use gpui::{
actions, point, AppContext, Pixels, PlatformDisplay, Size, Task, WindowBackgroundAppearance,
WindowBounds, WindowContext, WindowKind, WindowOptions,
actions, point, AppContext, DevicePixels, Pixels, PlatformDisplay, Size, Task,
WindowBackgroundAppearance, WindowBounds, WindowContext, WindowKind, WindowOptions,
};
use panel_settings::MessageEditorSettings;
pub use panel_settings::{
@@ -22,7 +22,6 @@ pub use panel_settings::{
};
use release_channel::ReleaseChannel;
use settings::Settings;
use ui::px;
use workspace::{notifications::DetachAndPromptErr, AppState};
actions!(
@@ -97,19 +96,22 @@ pub fn toggle_deafen(_: &ToggleDeafen, cx: &mut AppContext) {
fn notification_window_options(
screen: Rc<dyn PlatformDisplay>,
size: Size<Pixels>,
window_size: Size<Pixels>,
cx: &AppContext,
) -> WindowOptions {
let notification_margin_width = px(16.);
let notification_margin_height = px(-48.);
let notification_margin_width = DevicePixels::from(16);
let notification_margin_height = DevicePixels::from(-0) - DevicePixels::from(48);
let bounds = gpui::Bounds::<Pixels> {
origin: screen.bounds().upper_right()
let screen_bounds = screen.bounds();
let size: Size<DevicePixels> = window_size.into();
let bounds = gpui::Bounds::<DevicePixels> {
origin: screen_bounds.upper_right()
- point(
size.width + notification_margin_width,
notification_margin_height,
),
size,
size: window_size.into(),
};
let app_id = ReleaseChannel::global(cx).app_id();

View File

@@ -8,7 +8,6 @@ use settings::Settings;
use std::sync::{Arc, Weak};
use theme::ThemeSettings;
use ui::{prelude::*, Button, Label};
use util::ResultExt;
use workspace::AppState;
pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
@@ -28,21 +27,16 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
for screen in cx.displays() {
let options = notification_window_options(screen, window_size, cx);
let Some(window) = cx
.open_window(options, |cx| {
cx.new_view(|_| {
ProjectSharedNotification::new(
owner.clone(),
*project_id,
worktree_root_names.clone(),
app_state.clone(),
)
})
let window = cx.open_window(options, |cx| {
cx.new_view(|_| {
ProjectSharedNotification::new(
owner.clone(),
*project_id,
worktree_root_names.clone(),
app_state.clone(),
)
})
.log_err()
else {
continue;
};
});
notification_windows
.entry(*project_id)
.or_insert(Vec::new())

19
crates/color/Cargo.toml Normal file
View File

@@ -0,0 +1,19 @@
[package]
name = "color"
version = "0.1.0"
edition = "2021"
publish = false
license = "GPL-3.0-or-later"
[lints]
workspace = true
[features]
default = []
[lib]
path = "src/color.rs"
doctest = true
[dependencies]
palette.workspace = true

227
crates/color/src/color.rs Normal file
View File

@@ -0,0 +1,227 @@
//! # Color
//!
//! The `color` crate provides a set utilities for working with colors. It is a wrapper around the [`palette`](https://docs.rs/palette) crate with some additional functionality.
//!
//! It is used to create a manipulate colors when building themes.
//!
//! === In development note ===
//!
//! This crate is meant to sit between gpui and the theme/ui for all the color related stuff.
//!
//! It could be folded into gpui, ui or theme potentially but for now we'll continue
//! to develop it in isolation.
//!
//! Once we have a good idea of the needs of the theme system and color in gpui in general I see 3 paths:
//! 1. Use `palette` (or another color library) directly in gpui and everywhere else, rather than rolling our own color system.
//! 2. Keep this crate as a thin wrapper around `palette` and use it everywhere except gpui, and convert to gpui's color system when needed.
//! 3. Build the needed functionality into gpui and keep using its color system everywhere.
//!
//! I'm leaning towards 2 in the short term and 1 in the long term, but we'll need to discuss it more.
//!
//! === End development note ===
use palette::{
blend::Blend, convert::FromColorUnclamped, encoding, rgb::Rgb, Clamp, Mix, Srgb, WithAlpha,
};
/// The types of blend modes supported
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum BlendMode {
/// Multiplies the colors, resulting in a darker color. This mode is useful for creating shadows.
Multiply,
/// Lightens the color by adding the source and destination colors. It results in a lighter color.
Screen,
/// Combines Multiply and Screen blend modes. Parts of the image that are lighter than 50% gray are lightened, and parts that are darker are darkened.
Overlay,
/// Selects the darker of the base or blend color as the resulting color. Useful for darkening images without affecting the overall contrast.
Darken,
/// Selects the lighter of the base or blend color as the resulting color. Useful for lightening images without affecting the overall contrast.
Lighten,
/// Brightens the base color to reflect the blend color. The result is a lightened image.
Dodge,
/// Darkens the base color to reflect the blend color. The result is a darkened image.
Burn,
/// Similar to Overlay, but with a stronger effect. Hard Light can either multiply or screen colors, depending on the blend color.
HardLight,
/// A softer version of Hard Light. Soft Light either darkens or lightens colors, depending on the blend color.
SoftLight,
/// Subtracts the darker of the two constituent colors from the lighter color. Difference mode is useful for creating more vivid colors.
Difference,
/// Similar to Difference, but with a lower contrast. Exclusion mode produces an effect similar to Difference but with less intensity.
Exclusion,
}
/// Converts a hexadecimal color string to a `palette::Hsla` color.
///
/// This function supports the following hex formats:
/// `#RGB`, `#RGBA`, `#RRGGBB`, `#RRGGBBAA`.
pub fn hex_to_hsla(s: &str) -> Result<RGBAColor, String> {
let hex = s.trim_start_matches('#');
// Expand shorthand formats #RGB and #RGBA to #RRGGBB and #RRGGBBAA
let h = hex.as_bytes();
let arr: [u8; 8] = match h.len() {
// #RGB => #RRGGBBAA
3 => [h[0], h[0], h[1], h[1], h[2], h[2], b'f', b'f'],
// #RGBA => #RRGGBBAA
4 => [h[0], h[0], h[1], h[1], h[2], h[2], h[3], h[3]],
// #RRGGBB => #RRGGBBAA
6 => [h[0], h[1], h[2], h[3], h[4], h[5], b'f', b'f'],
// Already in #RRGGBBAA
8 => h.try_into().unwrap(),
_ => return Err("Invalid hexadecimal string length".to_string()),
};
let hex =
std::str::from_utf8(&arr).map_err(|_| format!("Invalid hexadecimal string: {}", s))?;
let hex_val =
u32::from_str_radix(hex, 16).map_err(|_| format!("Invalid hexadecimal string: {}", s))?;
Ok(RGBAColor {
r: ((hex_val >> 24) & 0xFF) as f32 / 255.0,
g: ((hex_val >> 16) & 0xFF) as f32 / 255.0,
b: ((hex_val >> 8) & 0xFF) as f32 / 255.0,
a: (hex_val & 0xFF) as f32 / 255.0,
})
}
// These derives implement to and from palette's color types.
#[derive(FromColorUnclamped, WithAlpha, Debug, Clone)]
#[palette(skip_derives(Rgb), rgb_standard = "encoding::Srgb")]
pub struct RGBAColor {
r: f32,
g: f32,
b: f32,
// Let Palette know this is our alpha channel.
#[palette(alpha)]
a: f32,
}
impl FromColorUnclamped<RGBAColor> for RGBAColor {
fn from_color_unclamped(color: RGBAColor) -> RGBAColor {
color
}
}
impl<S> FromColorUnclamped<Rgb<S, f32>> for RGBAColor
where
Srgb: FromColorUnclamped<Rgb<S, f32>>,
{
fn from_color_unclamped(color: Rgb<S, f32>) -> RGBAColor {
let srgb = Srgb::from_color_unclamped(color);
RGBAColor {
r: srgb.red,
g: srgb.green,
b: srgb.blue,
a: 1.0,
}
}
}
impl<S> FromColorUnclamped<RGBAColor> for Rgb<S, f32>
where
Rgb<S, f32>: FromColorUnclamped<Srgb>,
{
fn from_color_unclamped(color: RGBAColor) -> Self {
Self::from_color_unclamped(Srgb::new(color.r, color.g, color.b))
}
}
impl Clamp for RGBAColor {
fn clamp(self) -> Self {
RGBAColor {
r: self.r.min(1.0).max(0.0),
g: self.g.min(1.0).max(0.0),
b: self.b.min(1.0).max(0.0),
a: self.a.min(1.0).max(0.0),
}
}
}
impl RGBAColor {
/// Creates a new color from the given RGBA values.
///
/// This color can be used to convert to any [`palette::Color`] type.
pub fn new(r: f32, g: f32, b: f32, a: f32) -> Self {
RGBAColor { r, g, b, a }
}
/// Returns a set of states for this color.
pub fn states(self, is_light: bool) -> ColorStates {
states_for_color(self, is_light)
}
/// Mixes this color with another [`palette::Hsl`] color at the given `mix_ratio`.
pub fn mixed(&self, other: RGBAColor, mix_ratio: f32) -> Self {
let srgb_self = Srgb::new(self.r, self.g, self.b);
let srgb_other = Srgb::new(other.r, other.g, other.b);
// Directly mix the colors as sRGB values
let mixed = srgb_self.mix(srgb_other, mix_ratio);
RGBAColor::from_color_unclamped(mixed)
}
pub fn blend(&self, other: RGBAColor, blend_mode: BlendMode) -> Self {
let srgb_self = Srgb::new(self.r, self.g, self.b);
let srgb_other = Srgb::new(other.r, other.g, other.b);
let blended = match blend_mode {
// replace hsl methods with the respective sRGB methods
BlendMode::Multiply => srgb_self.multiply(srgb_other),
_ => unimplemented!(),
};
Self {
r: blended.red,
g: blended.green,
b: blended.blue,
a: self.a,
}
}
}
/// A set of colors for different states of an element.
#[derive(Debug, Clone)]
pub struct ColorStates {
/// The default color.
pub default: RGBAColor,
/// The color when the mouse is hovering over the element.
pub hover: RGBAColor,
/// The color when the mouse button is held down on the element.
pub active: RGBAColor,
/// The color when the element is focused with the keyboard.
pub focused: RGBAColor,
/// The color when the element is disabled.
pub disabled: RGBAColor,
}
/// Returns a set of colors for different states of an element.
///
/// todo("This should take a theme and use appropriate colors from it")
pub fn states_for_color(color: RGBAColor, is_light: bool) -> ColorStates {
let adjustment_factor = if is_light { 0.1 } else { -0.1 };
let hover_adjustment = 1.0 - adjustment_factor;
let active_adjustment = 1.0 - 2.0 * adjustment_factor;
let focused_adjustment = 1.0 - 3.0 * adjustment_factor;
let disabled_adjustment = 1.0 - 4.0 * adjustment_factor;
let make_adjustment = |color: RGBAColor, adjustment: f32| -> RGBAColor {
// Adjust lightness for each state
// Note: Adjustment logic may differ; simplify as needed for sRGB
RGBAColor::new(
color.r * adjustment,
color.g * adjustment,
color.b * adjustment,
color.a,
)
};
let color = color.clamp();
ColorStates {
default: color.clone(),
hover: make_adjustment(color.clone(), hover_adjustment),
active: make_adjustment(color.clone(), active_adjustment),
focused: make_adjustment(color.clone(), focused_adjustment),
disabled: make_adjustment(color.clone(), disabled_adjustment),
}
}

View File

@@ -1044,6 +1044,7 @@ async fn get_copilot_lsp(http: Arc<dyn HttpClient>) -> anyhow::Result<PathBuf> {
mod tests {
use super::*;
use gpui::TestAppContext;
use language::BufferId;
#[gpui::test(iterations = 10)]
async fn test_buffer_management(cx: &mut TestAppContext) {
@@ -1257,5 +1258,16 @@ mod tests {
fn load(&self, _: &AppContext) -> Task<Result<String>> {
unimplemented!()
}
fn buffer_reloaded(
&self,
_: BufferId,
_: &clock::Global,
_: language::LineEnding,
_: Option<std::time::SystemTime>,
_: &mut AppContext,
) {
unimplemented!()
}
}
}

View File

@@ -867,12 +867,10 @@ fn compare_diagnostics(
snapshot: &language::BufferSnapshot,
) -> Ordering {
use language::ToOffset;
// The diagnostics may point to a previously open Buffer for this file.
if !old.range.start.is_valid(snapshot) || !new.range.start.is_valid(snapshot) {
// The old diagnostics may point to a previously open Buffer for this file.
if !old.range.start.is_valid(snapshot) {
return Ordering::Greater;
}
old.range
.start
.to_offset(snapshot)

View File

@@ -30,7 +30,6 @@ test-support = [
[dependencies]
aho-corasick = "1.1"
anyhow.workspace = true
assets.workspace = true
client.workspace = true
clock.workspace = true
collections.workspace = true

View File

@@ -52,14 +52,8 @@ use multi_buffer::{
ToOffset, ToPoint,
};
use serde::Deserialize;
use std::{
any::TypeId,
borrow::Cow,
fmt::Debug,
num::NonZeroU32,
ops::{Add, Range, Sub},
sync::Arc,
};
use std::ops::Add;
use std::{any::TypeId, borrow::Cow, fmt::Debug, num::NonZeroU32, ops::Range, sync::Arc};
use sum_tree::{Bias, TreeMap};
use tab_map::{TabMap, TabSnapshot};
use text::LineIndent;
@@ -1033,14 +1027,6 @@ impl Add for DisplayRow {
}
}
impl Sub for DisplayRow {
type Output = Self;
fn sub(self, other: Self) -> Self::Output {
DisplayRow(self.0 - other.0)
}
}
impl DisplayPoint {
pub fn new(row: DisplayRow, column: u32) -> Self {
Self(BlockPoint(Point::new(row.0, column)))

View File

@@ -28,7 +28,6 @@ mod indent_guides;
mod inlay_hint_cache;
mod inline_completion_provider;
pub mod items;
mod linked_editing_ranges;
mod mouse_context_menu;
pub mod movement;
mod persistence;
@@ -89,7 +88,6 @@ use language::{
Point, Selection, SelectionGoal, TransactionId,
};
use language::{BufferRow, Runnable, RunnableRange};
use linked_editing_ranges::refresh_linked_ranges;
use task::{ResolvedTask, TaskTemplate, TaskVariables};
use hover_links::{HoverLink, HoveredLinkState, InlayHighlight};
@@ -149,9 +147,6 @@ use workspace::{OpenInTerminal, OpenTerminal, TabBarSettings, Toast};
use crate::hover_links::find_url;
pub const FILE_HEADER_HEIGHT: u8 = 1;
pub const MULTI_BUFFER_EXCERPT_HEADER_HEIGHT: u8 = 1;
pub const MULTI_BUFFER_EXCERPT_FOOTER_HEIGHT: u8 = 1;
pub const DEFAULT_MULTIBUFFER_CONTEXT: u32 = 2;
const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500);
const MAX_LINE_LEN: usize = 1024;
@@ -381,7 +376,6 @@ type CompletionId = usize;
// type OverrideTextStyle = dyn Fn(&EditorStyle) -> Option<HighlightStyle>;
type BackgroundHighlight = (fn(&ThemeColors) -> Hsla, Arc<[Range<Anchor>]>);
type GutterHighlight = (fn(&AppContext) -> Hsla, Arc<[Range<Anchor>]>);
struct ScrollbarMarkerState {
scrollbar_size: Size<Pixels>,
@@ -470,7 +464,6 @@ pub struct Editor {
highlight_order: usize,
highlighted_rows: HashMap<TypeId, Vec<RowHighlight>>,
background_highlights: TreeMap<TypeId, BackgroundHighlight>,
gutter_highlights: TreeMap<TypeId, GutterHighlight>,
scrollbar_marker_state: ScrollbarMarkerState,
active_indent_guides_state: ActiveIndentGuidesState,
nav_history: Option<ItemNavHistory>,
@@ -483,8 +476,6 @@ pub struct Editor {
available_code_actions: Option<(Location, Arc<[CodeAction]>)>,
code_actions_task: Option<Task<()>>,
document_highlights_task: Option<Task<()>>,
linked_editing_range_task: Option<Task<Option<()>>>,
linked_edit_ranges: linked_editing_ranges::LinkedEditingRanges,
pending_rename: Option<RenameState>,
searchable: bool,
cursor_shape: CursorShape,
@@ -532,7 +523,6 @@ pub struct Editor {
tasks: BTreeMap<(BufferId, BufferRow), RunnableTasks>,
tasks_update_task: Option<Task<()>>,
previous_search_ranges: Option<Arc<[Range<Anchor>]>>,
file_header_size: u8,
}
#[derive(Clone)]
@@ -1506,7 +1496,7 @@ struct ActiveDiagnosticGroup {
is_valid: bool,
}
#[derive(Serialize, Deserialize, Clone)]
#[derive(Serialize, Deserialize)]
pub struct ClipboardSelection {
pub len: usize,
pub is_entire_line: bool,
@@ -1655,8 +1645,9 @@ impl Editor {
}),
merge_adjacent: true,
};
let file_header_size = if show_excerpt_controls { 3 } else { 2 };
let display_map = cx.new_model(|cx| {
let file_header_size = if show_excerpt_controls { 3 } else { 2 };
DisplayMap::new(
buffer.clone(),
style.font(),
@@ -1664,8 +1655,8 @@ impl Editor {
None,
show_excerpt_controls,
file_header_size,
MULTI_BUFFER_EXCERPT_HEADER_HEIGHT,
MULTI_BUFFER_EXCERPT_FOOTER_HEIGHT,
1,
1,
fold_placeholder,
cx,
)
@@ -1761,7 +1752,6 @@ impl Editor {
highlight_order: 0,
highlighted_rows: HashMap::default(),
background_highlights: Default::default(),
gutter_highlights: TreeMap::default(),
scrollbar_marker_state: ScrollbarMarkerState::default(),
active_indent_guides_state: ActiveIndentGuidesState::default(),
nav_history: None,
@@ -1775,7 +1765,6 @@ impl Editor {
available_code_actions: Default::default(),
code_actions_task: Default::default(),
document_highlights_task: Default::default(),
linked_editing_range_task: Default::default(),
pending_rename: Default::default(),
searchable: true,
cursor_shape: Default::default(),
@@ -1815,7 +1804,6 @@ impl Editor {
git_blame_inline_enabled: ProjectSettings::get_global(cx).git.inline_blame_enabled(),
blame: None,
blame_subscription: None,
file_header_size,
tasks: Default::default(),
_subscriptions: vec![
cx.observe(&buffer, Self::on_buffer_changed),
@@ -1837,7 +1825,6 @@ impl Editor {
}),
],
tasks_update_task: None,
linked_edit_ranges: Default::default(),
previous_search_ranges: None,
};
this.tasks_update_task = Some(this.refresh_runnables(cx));
@@ -2218,6 +2205,7 @@ impl Editor {
)
});
}
let display_map = self
.display_map
.update(cx, |display_map, cx| display_map.snapshot(cx));
@@ -2305,7 +2293,6 @@ impl Editor {
self.refresh_document_highlights(cx);
refresh_matching_bracket_highlights(self, cx);
self.discard_inline_completion(false, cx);
linked_editing_ranges::refresh_linked_ranges(self, cx);
if self.git_blame_inline_enabled {
self.start_inline_blame_timer(cx);
}
@@ -2317,6 +2304,7 @@ impl Editor {
if self.selections.disjoint_anchors().len() == 1 {
cx.emit(SearchEvent::ActiveMatchChanged)
}
cx.notify();
}
@@ -2786,49 +2774,6 @@ impl Editor {
false
}
fn linked_editing_ranges_for(
&self,
selection: Range<text::Anchor>,
cx: &AppContext,
) -> Option<HashMap<Model<Buffer>, Vec<Range<text::Anchor>>>> {
if self.linked_edit_ranges.is_empty() {
return None;
}
let ((base_range, linked_ranges), buffer_snapshot, buffer) =
selection.end.buffer_id.and_then(|end_buffer_id| {
if selection.start.buffer_id != Some(end_buffer_id) {
return None;
}
let buffer = self.buffer.read(cx).buffer(end_buffer_id)?;
let snapshot = buffer.read(cx).snapshot();
self.linked_edit_ranges
.get(end_buffer_id, selection.start..selection.end, &snapshot)
.map(|ranges| (ranges, snapshot, buffer))
})?;
use text::ToOffset as TO;
// find offset from the start of current range to current cursor position
let start_byte_offset = TO::to_offset(&base_range.start, &buffer_snapshot);
let start_offset = TO::to_offset(&selection.start, &buffer_snapshot);
let start_difference = start_offset - start_byte_offset;
let end_offset = TO::to_offset(&selection.end, &buffer_snapshot);
let end_difference = end_offset - start_byte_offset;
// Current range has associated linked ranges.
let mut linked_edits = HashMap::<_, Vec<_>>::default();
for range in linked_ranges.iter() {
let start_offset = TO::to_offset(&range.start, &buffer_snapshot);
let end_offset = start_offset + end_difference;
let start_offset = start_offset + start_difference;
let start = buffer_snapshot.anchor_after(start_offset);
let end = buffer_snapshot.anchor_after(end_offset);
linked_edits
.entry(buffer.clone())
.or_default()
.push(start..end);
}
Some(linked_edits)
}
pub fn handle_input(&mut self, text: &str, cx: &mut ViewContext<Self>) {
let text: Arc<str> = text.into();
@@ -2839,7 +2784,6 @@ impl Editor {
let selections = self.selections.all_adjusted(cx);
let mut brace_inserted = false;
let mut edits = Vec::new();
let mut linked_edits = HashMap::<_, Vec<_>>::default();
let mut new_selections = Vec::with_capacity(selections.len());
let mut new_autoclose_regions = Vec::new();
let snapshot = self.buffer.read(cx).read(cx);
@@ -3020,46 +2964,16 @@ impl Editor {
// text with the given input and move the selection to the end of the
// newly inserted text.
let anchor = snapshot.anchor_after(selection.end);
if !self.linked_edit_ranges.is_empty() {
let start_anchor = snapshot.anchor_before(selection.start);
if let Some(ranges) =
self.linked_editing_ranges_for(start_anchor.text_anchor..anchor.text_anchor, cx)
{
for (buffer, edits) in ranges {
linked_edits
.entry(buffer.clone())
.or_default()
.extend(edits.into_iter().map(|range| (range, text.clone())));
}
}
}
new_selections.push((selection.map(|_| anchor), 0));
edits.push((selection.start..selection.end, text.clone()));
}
drop(snapshot);
self.transact(cx, |this, cx| {
this.buffer.update(cx, |buffer, cx| {
buffer.edit(edits, this.autoindent_mode.clone(), cx);
});
for (buffer, edits) in linked_edits {
buffer.update(cx, |buffer, cx| {
let snapshot = buffer.snapshot();
let edits = edits
.into_iter()
.map(|(range, text)| {
use text::ToPoint as TP;
let end_point = TP::to_point(&range.end, &snapshot);
let start_point = TP::to_point(&range.start, &snapshot);
(start_point..end_point, text)
})
.sorted_by_key(|(range, _)| range.start)
.collect::<Vec<_>>();
buffer.edit(edits, None, cx);
})
}
let new_anchor_selections = new_selections.iter().map(|e| &e.0);
let new_selection_deltas = new_selections.iter().map(|e| e.1);
let snapshot = this.buffer.read(cx).read(cx);
@@ -3116,7 +3030,6 @@ impl Editor {
let trigger_in_words = !had_active_inline_completion;
this.trigger_completion_on_input(&text, trigger_in_words, cx);
linked_editing_ranges::refresh_linked_ranges(this, cx);
this.refresh_inline_completion(true, cx);
});
}
@@ -4054,7 +3967,6 @@ impl Editor {
let snapshot = self.buffer.read(cx).snapshot(cx);
let mut range_to_replace: Option<Range<isize>> = None;
let mut ranges = Vec::new();
let mut linked_edits = HashMap::<_, Vec<_>>::default();
for selection in &selections {
if snapshot.contains_str_at(selection.start.saturating_sub(lookbehind), &old_text) {
let start = selection.start.saturating_sub(lookbehind);
@@ -4084,21 +3996,6 @@ impl Editor {
}));
break;
}
if !self.linked_edit_ranges.is_empty() {
let start_anchor = snapshot.anchor_before(selection.head());
let end_anchor = snapshot.anchor_after(selection.tail());
if let Some(ranges) = self
.linked_editing_ranges_for(start_anchor.text_anchor..end_anchor.text_anchor, cx)
{
for (buffer, edits) in ranges {
linked_edits.entry(buffer.clone()).or_default().extend(
edits
.into_iter()
.map(|range| (range, text[common_prefix_len..].to_owned())),
);
}
}
}
}
let text = &text[common_prefix_len..];
@@ -4125,22 +4022,6 @@ impl Editor {
);
});
}
for (buffer, edits) in linked_edits {
buffer.update(cx, |buffer, cx| {
let snapshot = buffer.snapshot();
let edits = edits
.into_iter()
.map(|(range, text)| {
use text::ToPoint as TP;
let end_point = TP::to_point(&range.end, &snapshot);
let start_point = TP::to_point(&range.start, &snapshot);
(start_point..end_point, text)
})
.sorted_by_key(|(range, _)| range.start)
.collect::<Vec<_>>();
buffer.edit(edits, None, cx);
})
}
this.refresh_inline_completion(true, cx);
});
@@ -5125,27 +5006,6 @@ impl Editor {
pub fn backspace(&mut self, _: &Backspace, cx: &mut ViewContext<Self>) {
self.transact(cx, |this, cx| {
this.select_autoclose_pair(cx);
let mut linked_ranges = HashMap::<_, Vec<_>>::default();
if !this.linked_edit_ranges.is_empty() {
let selections = this.selections.all::<MultiBufferPoint>(cx);
let snapshot = this.buffer.read(cx).snapshot(cx);
for selection in selections.iter() {
let selection_start = snapshot.anchor_before(selection.start).text_anchor;
let selection_end = snapshot.anchor_after(selection.end).text_anchor;
if selection_start.buffer_id != selection_end.buffer_id {
continue;
}
if let Some(ranges) =
this.linked_editing_ranges_for(selection_start..selection_end, cx)
{
for (buffer, entries) in ranges {
linked_ranges.entry(buffer).or_default().extend(entries);
}
}
}
}
let mut selections = this.selections.all::<MultiBufferPoint>(cx);
if !this.selections.line_mode {
let display_map = this.display_map.update(cx, |map, cx| map.snapshot(cx));
@@ -5186,33 +5046,7 @@ impl Editor {
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections));
this.insert("", cx);
let empty_str: Arc<str> = Arc::from("");
for (buffer, edits) in linked_ranges {
let snapshot = buffer.read(cx).snapshot();
use text::ToPoint as TP;
let edits = edits
.into_iter()
.map(|range| {
let end_point = TP::to_point(&range.end, &snapshot);
let mut start_point = TP::to_point(&range.start, &snapshot);
if end_point == start_point {
let offset = text::ToOffset::to_offset(&range.start, &snapshot)
.saturating_sub(1);
start_point = TP::to_point(&offset, &snapshot);
};
(start_point..end_point, empty_str.clone())
})
.sorted_by_key(|(range, _)| range.start)
.collect::<Vec<_>>();
buffer.update(cx, |this, cx| {
this.edit(edits, None, cx);
})
}
this.refresh_inline_completion(true, cx);
linked_editing_ranges::refresh_linked_ranges(this, cx);
});
}
@@ -6544,8 +6378,6 @@ impl Editor {
}
let text_layout_details = &self.text_layout_details(cx);
let selection_count = self.selections.count();
let first_selection = self.selections.first_anchor();
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
let line_mode = s.line_mode;
@@ -6562,12 +6394,7 @@ impl Editor {
);
selection.collapse_to(cursor, goal);
});
});
if selection_count == 1 && first_selection.range() == self.selections.first_anchor().range()
{
cx.propagate();
}
})
}
pub fn move_up_by_lines(&mut self, action: &MoveUpByLines, cx: &mut ViewContext<Self>) {
@@ -6711,9 +6538,6 @@ impl Editor {
}
let text_layout_details = &self.text_layout_details(cx);
let selection_count = self.selections.count();
let first_selection = self.selections.first_anchor();
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
let line_mode = s.line_mode;
s.move_with(|map, selection| {
@@ -6730,11 +6554,6 @@ impl Editor {
selection.collapse_to(cursor, goal);
});
});
if selection_count == 1 && first_selection.range() == self.selections.first_anchor().range()
{
cx.propagate();
}
}
pub fn move_page_down(&mut self, action: &MovePageDown, cx: &mut ViewContext<Self>) {
@@ -10444,25 +10263,6 @@ impl Editor {
Some(text_highlights)
}
pub fn highlight_gutter<T: 'static>(
&mut self,
ranges: &[Range<Anchor>],
color_fetcher: fn(&AppContext) -> Hsla,
cx: &mut ViewContext<Self>,
) {
self.gutter_highlights
.insert(TypeId::of::<T>(), (color_fetcher, Arc::from(ranges)));
cx.notify();
}
pub fn clear_gutter_highlights<T: 'static>(
&mut self,
cx: &mut ViewContext<Self>,
) -> Option<GutterHighlight> {
cx.notify();
self.gutter_highlights.remove(&TypeId::of::<T>())
}
#[cfg(feature = "test-support")]
pub fn all_text_background_highlights(
&mut self,
@@ -10652,44 +10452,6 @@ impl Editor {
results
}
pub fn gutter_highlights_in_range(
&self,
search_range: Range<Anchor>,
display_snapshot: &DisplaySnapshot,
cx: &AppContext,
) -> Vec<(Range<DisplayPoint>, Hsla)> {
let mut results = Vec::new();
for (color_fetcher, ranges) in self.gutter_highlights.values() {
let color = color_fetcher(cx);
let start_ix = match ranges.binary_search_by(|probe| {
let cmp = probe
.end
.cmp(&search_range.start, &display_snapshot.buffer_snapshot);
if cmp.is_gt() {
Ordering::Greater
} else {
Ordering::Less
}
}) {
Ok(i) | Err(i) => i,
};
for range in &ranges[start_ix..] {
if range
.start
.cmp(&search_range.end, &display_snapshot.buffer_snapshot)
.is_ge()
{
break;
}
let start = range.start.to_display_point(&display_snapshot);
let end = range.end.to_display_point(&display_snapshot);
results.push((start..end, color))
}
}
results
}
/// Get the text ranges corresponding to the redaction query
pub fn redacted_ranges(
&self,
@@ -10782,6 +10544,7 @@ impl Editor {
}
cx.emit(EditorEvent::BufferEdited);
cx.emit(SearchEvent::MatchesInvalidated);
if *singleton_buffer_edited {
if let Some(project) = &self.project {
let project = project.read(cx);
@@ -10813,7 +10576,6 @@ impl Editor {
let Some(project) = &self.project else { return };
let telemetry = project.read(cx).client().telemetry().clone();
refresh_linked_ranges(self, cx);
telemetry.log_edit_event("editor");
}
multi_buffer::Event::ExcerptsAdded {
@@ -10833,19 +10595,12 @@ impl Editor {
self.refresh_inlay_hints(InlayHintRefreshReason::ExcerptsRemoved(ids.clone()), cx);
cx.emit(EditorEvent::ExcerptsRemoved { ids: ids.clone() })
}
multi_buffer::Event::ExcerptsEdited { ids } => {
cx.emit(EditorEvent::ExcerptsEdited { ids: ids.clone() })
}
multi_buffer::Event::ExcerptsExpanded { ids } => {
cx.emit(EditorEvent::ExcerptsExpanded { ids: ids.clone() })
}
multi_buffer::Event::Reparsed => {
self.tasks_update_task = Some(self.refresh_runnables(cx));
cx.emit(EditorEvent::Reparsed);
}
multi_buffer::Event::LanguageChanged => {
linked_editing_ranges::refresh_linked_ranges(self, cx);
cx.emit(EditorEvent::Reparsed);
cx.notify();
}
@@ -11309,10 +11064,6 @@ impl Editor {
}));
self
}
pub fn file_header_size(&self) -> u8 {
self.file_header_size
}
}
fn hunks_for_selections(
@@ -11757,12 +11508,6 @@ pub enum EditorEvent {
ExcerptsRemoved {
ids: Vec<ExcerptId>,
},
ExcerptsEdited {
ids: Vec<ExcerptId>,
},
ExcerptsExpanded {
ids: Vec<ExcerptId>,
},
BufferEdited,
Edited,
Reparsed,
@@ -11963,6 +11708,7 @@ impl ViewInputHandler for Editor {
cx: &mut ViewContext<Self>,
) {
if !self.input_enabled {
cx.emit(EditorEvent::InputIgnored { text: text.into() });
return;
}

View File

@@ -9,10 +9,7 @@ use crate::{
JoinLines,
};
use futures::StreamExt;
use gpui::{
div, AssetSource, SemanticVersion, TestAppContext, UpdateGlobal, VisualTestContext,
WindowBounds, WindowOptions,
};
use gpui::{div, TestAppContext, UpdateGlobal, VisualTestContext, WindowBounds, WindowOptions};
use indoc::indoc;
use language::{
language_settings::{
@@ -512,7 +509,6 @@ fn test_clone(cx: &mut TestAppContext) {
.update(cx, |editor, cx| {
cx.open_window(Default::default(), |cx| cx.new_view(|cx| editor.clone(cx)))
})
.unwrap()
.unwrap();
let snapshot = editor.update(cx, |e, cx| e.snapshot(cx)).unwrap();
@@ -7654,14 +7650,13 @@ async fn test_following(cx: &mut gpui::TestAppContext) {
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
gpui::Point::new(px(0.), px(0.)),
gpui::Point::new(px(10.), px(80.)),
gpui::Point::new(0.into(), 0.into()),
gpui::Point::new(10.into(), 80.into()),
))),
..Default::default()
},
|cx| cx.new_view(|cx| build_editor(buffer.clone(), cx)),
)
.unwrap()
});
let is_still_following = Rc::new(RefCell::new(true));
@@ -12189,16 +12184,10 @@ pub(crate) fn update_test_project_settings(
pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
_ = cx.update(|cx| {
cx.text_system()
.add_fonts(vec![assets::Assets
.load("fonts/zed-mono/zed-mono-extended.ttf")
.unwrap()
.unwrap()])
.unwrap();
let store = SettingsStore::test(cx);
cx.set_global(store);
theme::init(theme::LoadThemes::JustBase, cx);
release_channel::init(SemanticVersion::default(), cx);
release_channel::init("0.0.0", cx);
client::init_settings(cx);
language::init(cx);
Project::init_settings(cx);

View File

@@ -2837,8 +2837,6 @@ impl EditorElement {
Self::paint_diff_hunks(layout.gutter_hitbox.bounds, layout, cx)
}
self.paint_gutter_highlights(layout, cx);
if layout.blamed_display_rows.is_some() {
self.paint_blamed_display_rows(layout, cx);
}
@@ -3008,37 +3006,6 @@ impl EditorElement {
}
}
fn paint_gutter_highlights(&self, layout: &EditorLayout, cx: &mut WindowContext) {
let highlight_width = 0.275 * layout.position_map.line_height;
let highlight_corner_radii = Corners::all(0.05 * layout.position_map.line_height);
cx.paint_layer(layout.gutter_hitbox.bounds, |cx| {
for (range, color) in &layout.highlighted_gutter_ranges {
let start_row = if range.start.row() < layout.visible_display_row_range.start {
layout.visible_display_row_range.start - DisplayRow(1)
} else {
range.start.row()
};
let end_row = if range.end.row() > layout.visible_display_row_range.end {
layout.visible_display_row_range.end + DisplayRow(1)
} else {
range.end.row()
};
let start_y = layout.gutter_hitbox.top()
+ start_row.0 as f32 * layout.position_map.line_height
- layout.position_map.scroll_pixel_position.y;
let end_y = layout.gutter_hitbox.top()
+ (end_row.0 + 1) as f32 * layout.position_map.line_height
- layout.position_map.scroll_pixel_position.y;
let bounds = Bounds::from_corners(
point(layout.gutter_hitbox.left(), start_y),
point(layout.gutter_hitbox.left() + highlight_width, end_y),
);
cx.paint_quad(fill(bounds, *color).corner_radii(highlight_corner_radii));
}
});
}
fn paint_blamed_display_rows(&self, layout: &mut EditorLayout, cx: &mut WindowContext) {
let Some(blamed_display_rows) = layout.blamed_display_rows.take() else {
return;
@@ -4664,12 +4631,6 @@ impl Element for EditorElement {
&snapshot.display_snapshot,
cx.theme().colors(),
);
let highlighted_gutter_ranges =
self.editor.read(cx).gutter_highlights_in_range(
start_anchor..end_anchor,
&snapshot.display_snapshot,
cx,
);
let redacted_ranges = self.editor.read(cx).redacted_ranges(
start_anchor..end_anchor,
@@ -5030,7 +4991,6 @@ impl Element for EditorElement {
active_rows,
highlighted_rows,
highlighted_ranges,
highlighted_gutter_ranges,
redacted_ranges,
line_elements,
line_numbers,
@@ -5161,7 +5121,6 @@ pub struct EditorLayout {
inline_blame: Option<AnyElement>,
blocks: Vec<BlockLayout>,
highlighted_ranges: Vec<(Range<DisplayPoint>, Hsla)>,
highlighted_gutter_ranges: Vec<(Range<DisplayPoint>, Hsla)>,
redacted_ranges: Vec<Range<DisplayPoint>>,
cursors: Vec<(DisplayPoint, Hsla)>,
visible_cursors: Vec<CursorLayout>,

View File

@@ -322,6 +322,7 @@ pub fn update_inlay_link_and_hover_points(
hover_popover::hover_at_inlay(
editor,
InlayHover {
excerpt: excerpt_id,
tooltip: match tooltip {
InlayHintTooltip::String(text) => HoverBlock {
text,
@@ -369,6 +370,7 @@ pub fn update_inlay_link_and_hover_points(
hover_popover::hover_at_inlay(
editor,
InlayHover {
excerpt: excerpt_id,
tooltip: match tooltip {
InlayHintLabelPartTooltip::String(text) => {
HoverBlock {

View File

@@ -3,7 +3,7 @@ use crate::{
hover_links::{InlayHighlight, RangeInEditor},
scroll::ScrollAmount,
Anchor, AnchorRangeExt, DisplayPoint, DisplayRow, Editor, EditorSettings, EditorSnapshot,
EditorStyle, Hover, RangeToAnchorExt,
EditorStyle, ExcerptId, Hover, RangeToAnchorExt,
};
use futures::{stream::FuturesUnordered, FutureExt};
use gpui::{
@@ -49,6 +49,7 @@ pub fn hover_at(editor: &mut Editor, anchor: Option<Anchor>, cx: &mut ViewContex
}
pub struct InlayHover {
pub excerpt: ExcerptId,
pub range: InlayHighlight,
pub tooltip: HoverBlock,
}

View File

@@ -1268,7 +1268,7 @@ pub mod tests {
ExcerptRange,
};
use futures::StreamExt;
use gpui::{Context, SemanticVersion, TestAppContext, WindowHandle};
use gpui::{Context, TestAppContext, WindowHandle};
use itertools::Itertools;
use language::{
language_settings::AllLanguageSettingsContent, Capability, FakeLspAdapter, Language,
@@ -3361,7 +3361,7 @@ pub mod tests {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
theme::init(theme::LoadThemes::JustBase, cx);
release_channel::init(SemanticVersion::default(), cx);
release_channel::init("0.0.0", cx);
client::init_settings(cx);
language::init(cx);
Project::init_settings(cx);

View File

@@ -1,150 +0,0 @@
use std::ops::Range;
use collections::HashMap;
use itertools::Itertools;
use text::{AnchorRangeExt, BufferId, ToPoint};
use ui::ViewContext;
use util::ResultExt;
use crate::Editor;
#[derive(Clone, Default)]
pub(super) struct LinkedEditingRanges(
/// Ranges are non-overlapping and sorted by .0 (thus, [x + 1].start > [x].end must hold)
pub HashMap<BufferId, Vec<(Range<text::Anchor>, Vec<Range<text::Anchor>>)>>,
);
impl LinkedEditingRanges {
pub(super) fn get(
&self,
id: BufferId,
anchor: Range<text::Anchor>,
snapshot: &text::BufferSnapshot,
) -> Option<&(Range<text::Anchor>, Vec<Range<text::Anchor>>)> {
let ranges_for_buffer = self.0.get(&id)?;
let lower_bound = ranges_for_buffer
.partition_point(|(range, _)| range.start.cmp(&anchor.start, snapshot).is_le());
if lower_bound == 0 {
// None of the linked ranges contains `anchor`.
return None;
}
ranges_for_buffer
.get(lower_bound - 1)
.filter(|(range, _)| range.end.cmp(&anchor.end, snapshot).is_ge())
}
pub(super) fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
pub(super) fn refresh_linked_ranges(this: &mut Editor, cx: &mut ViewContext<Editor>) -> Option<()> {
if this.pending_rename.is_some() {
return None;
}
let project = this.project.clone()?;
let buffer = this.buffer.read(cx);
let mut applicable_selections = vec![];
let selections = this.selections.all::<usize>(cx);
let snapshot = buffer.snapshot(cx);
for selection in selections {
let cursor_position = selection.head();
let start_position = snapshot.anchor_before(cursor_position);
let end_position = snapshot.anchor_after(selection.tail());
if start_position.buffer_id != end_position.buffer_id || end_position.buffer_id.is_none() {
// Throw away selections spanning multiple buffers.
continue;
}
if let Some(buffer) = end_position.buffer_id.and_then(|id| buffer.buffer(id)) {
applicable_selections.push((
buffer,
start_position.text_anchor,
end_position.text_anchor,
));
}
}
if applicable_selections.is_empty() {
return None;
}
this.linked_editing_range_task = Some(cx.spawn(|this, mut cx| async move {
let highlights = project
.update(&mut cx, |project, cx| {
let mut linked_edits_tasks = vec![];
for (buffer, start, end) in &applicable_selections {
let snapshot = buffer.read(cx).snapshot();
let buffer_id = buffer.read(cx).remote_id();
let linked_edits_task = project.linked_edit(&buffer, *start, cx);
let highlights = move || async move {
let edits = linked_edits_task.await.log_err()?;
// Find the range containing our current selection.
// We might not find one, because the selection contains both the start and end of the contained range
// (think of selecting <`html>foo`</html> - even though there's a matching closing tag, the selection goes beyond the range of the opening tag)
// or the language server may not have returned any ranges.
let start_point = start.to_point(&snapshot);
let end_point = end.to_point(&snapshot);
let _current_selection_contains_range = edits.iter().find(|range| {
range.start.to_point(&snapshot) <= start_point
&& range.end.to_point(&snapshot) >= end_point
});
if _current_selection_contains_range.is_none() {
return None;
}
// Now link every range as each-others sibling.
let mut siblings: HashMap<Range<text::Anchor>, Vec<_>> = Default::default();
let mut insert_sorted_anchor =
|key: &Range<text::Anchor>, value: &Range<text::Anchor>| {
siblings.entry(key.clone()).or_default().push(value.clone());
};
for items in edits.into_iter().combinations(2) {
let Ok([first, second]): Result<[_; 2], _> = items.try_into() else {
unreachable!()
};
insert_sorted_anchor(&first, &second);
insert_sorted_anchor(&second, &first);
}
let mut siblings: Vec<(_, _)> = siblings.into_iter().collect();
siblings.sort_by(|lhs, rhs| lhs.0.cmp(&rhs.0, &snapshot));
Some((buffer_id, siblings))
};
linked_edits_tasks.push(highlights());
}
linked_edits_tasks
})
.log_err()?;
let highlights = futures::future::join_all(highlights).await;
this.update(&mut cx, |this, cx| {
this.linked_edit_ranges.0.clear();
if this.pending_rename.is_some() {
return;
}
for (buffer_id, ranges) in highlights.into_iter().flatten() {
this.linked_edit_ranges
.0
.entry(buffer_id)
.or_default()
.extend(ranges);
}
for (buffer_id, values) in this.linked_edit_ranges.0.iter_mut() {
let Some(snapshot) = this
.buffer
.read(cx)
.buffer(*buffer_id)
.map(|buffer| buffer.read(cx).snapshot())
else {
continue;
};
values.sort_by(|lhs, rhs| lhs.0.cmp(&rhs.0, &snapshot));
}
cx.notify();
})
.log_err();
Some(())
}));
None
}

View File

@@ -10,7 +10,7 @@ use serde_json::json;
use crate::{Editor, ToPoint};
use collections::HashSet;
use futures::Future;
use gpui::{AssetSource, View, ViewContext, VisualTestContext};
use gpui::{View, ViewContext, VisualTestContext};
use indoc::indoc;
use language::{
point_to_lsp, FakeLspAdapter, Language, LanguageConfig, LanguageMatcher, LanguageQueries,
@@ -39,12 +39,6 @@ impl EditorLspTestContext {
let app_state = cx.update(AppState::test);
cx.update(|cx| {
cx.text_system()
.add_fonts(vec![assets::Assets
.load("fonts/zed-mono/zed-mono-extended.ttf")
.unwrap()
.unwrap()])
.unwrap();
language::init(cx);
crate::init(cx);
workspace::init(app_state.clone(), cx);

View File

@@ -10,7 +10,7 @@ use async_compression::futures::bufread::GzipEncoder;
use collections::BTreeMap;
use fs::{FakeFs, Fs, RealFs};
use futures::{io::BufReader, AsyncReadExt, StreamExt};
use gpui::{Context, SemanticVersion, TestAppContext};
use gpui::{Context, TestAppContext};
use http::{FakeHttpClient, Response};
use language::{LanguageMatcher, LanguageRegistry, LanguageServerBinaryStatus, LanguageServerName};
use node_runtime::FakeNodeRuntime;
@@ -723,7 +723,7 @@ fn init_test(cx: &mut TestAppContext) {
cx.update(|cx| {
let store = SettingsStore::test(cx);
cx.set_global(store);
release_channel::init(SemanticVersion::default(), cx);
release_channel::init("0.0.0", cx);
theme::init(theme::LoadThemes::JustBase, cx);
Project::init_settings(cx);
ExtensionSettings::register(cx);

View File

@@ -8,10 +8,6 @@ keywords = ["zed", "extension"]
edition = "2021"
license = "Apache-2.0"
# Don't publish v0.0.7 until we're ready to commit to the breaking API changes
# Marshall is DRI on this.
publish = false
[lints]
workspace = true

View File

@@ -24,7 +24,6 @@ fs.workspace = true
fuzzy.workspace = true
gpui.workspace = true
language.workspace = true
num-format.workspace = true
picker.workspace = true
project.workspace = true
release_channel.workspace = true

View File

@@ -16,7 +16,6 @@ use gpui::{
InteractiveElement, KeyContext, ParentElement, Render, Styled, Task, TextStyle,
UniformListScrollHandle, View, ViewContext, VisualContext, WeakView, WhiteSpace, WindowContext,
};
use num_format::{Locale, ToFormattedString};
use release_channel::ReleaseChannel;
use settings::Settings;
use std::ops::DerefMut;
@@ -488,11 +487,8 @@ impl ExtensionsPage {
.size(LabelSize::Small),
)
.child(
Label::new(format!(
"Downloads: {}",
extension.download_count.to_formatted_string(&Locale::en)
))
.size(LabelSize::Small),
Label::new(format!("Downloads: {}", extension.download_count))
.size(LabelSize::Small),
),
)
.child(

View File

@@ -1,6 +1,5 @@
use gpui::{actions, AppContext, ClipboardItem, PromptLevel};
use system_specs::SystemSpecs;
use util::ResultExt;
use workspace::Workspace;
pub mod feedback_modal;
@@ -39,38 +38,25 @@ pub fn init(cx: &mut AppContext) {
feedback_modal::FeedbackModal::register(workspace, cx);
workspace
.register_action(|_, _: &CopySystemSpecsIntoClipboard, cx| {
let specs = SystemSpecs::new(&cx);
let specs = SystemSpecs::new(&cx).to_string();
cx.spawn(|_, mut cx| async move {
let specs = specs.await.to_string();
cx.update(|cx| cx.write_to_clipboard(ClipboardItem::new(specs.clone())))
.log_err();
cx.prompt(
PromptLevel::Info,
"Copied into clipboard",
Some(&specs),
&["OK"],
)
.await
.ok();
let prompt = cx.prompt(
PromptLevel::Info,
"Copied into clipboard",
Some(&specs),
&["OK"],
);
cx.spawn(|_, _cx| async move {
prompt.await.ok();
})
.detach();
cx.write_to_clipboard(ClipboardItem::new(specs.clone()));
})
.register_action(|_, _: &RequestFeature, cx| {
cx.open_url(request_feature_url());
})
.register_action(move |_, _: &FileBugReport, cx| {
let specs = SystemSpecs::new(&cx);
cx.spawn(|_, mut cx| async move {
let specs = specs.await;
cx.update(|cx| {
cx.open_url(&file_bug_report_url(&specs));
})
.log_err();
})
.detach();
cx.open_url(&file_bug_report_url(&SystemSpecs::new(&cx)));
})
.register_action(move |_, _: &OpenZedRepo, cx| {
cx.open_url(zed_repo_url());

View File

@@ -141,15 +141,15 @@ impl FeedbackModal {
return;
}
let system_specs = SystemSpecs::new(cx);
cx.spawn(|workspace, mut cx| async move {
let markdown = markdown.await.log_err();
let buffer = project.update(&mut cx, |project, cx| {
project.create_local_buffer("", markdown, cx)
})?;
let system_specs = system_specs.await;
workspace.update(&mut cx, |workspace, cx| {
let system_specs = SystemSpecs::new(cx);
workspace.toggle_modal(cx, move |cx| {
FeedbackModal::new(system_specs, project, buffer, cx)
});

View File

@@ -1,5 +1,4 @@
use client::telemetry;
use gpui::{AppContext, Task};
use gpui::AppContext;
use human_bytes::human_bytes;
use release_channel::{AppCommitSha, AppVersion, ReleaseChannel};
use serde::Serialize;
@@ -10,23 +9,27 @@ use sysinfo::{MemoryRefreshKind, RefreshKind, System};
pub struct SystemSpecs {
app_version: String,
release_channel: &'static str,
os_name: String,
os_version: String,
os_name: &'static str,
os_version: Option<String>,
memory: u64,
architecture: &'static str,
commit_sha: Option<String>,
}
impl SystemSpecs {
pub fn new(cx: &AppContext) -> Task<Self> {
pub fn new(cx: &AppContext) -> Self {
let app_version = AppVersion::global(cx).to_string();
let release_channel = ReleaseChannel::global(cx);
let os_name = telemetry::os_name();
let os_name = cx.app_metadata().os_name;
let system = System::new_with_specifics(
RefreshKind::new().with_memory(MemoryRefreshKind::everything()),
);
let memory = system.total_memory();
let architecture = env::consts::ARCH;
let os_version = cx
.app_metadata()
.os_version
.map(|os_version| os_version.to_string());
let commit_sha = match release_channel {
ReleaseChannel::Dev | ReleaseChannel::Nightly => {
AppCommitSha::try_global(cx).map(|sha| sha.0.clone())
@@ -34,24 +37,24 @@ impl SystemSpecs {
_ => None,
};
cx.background_executor().spawn(async move {
let os_version = telemetry::os_version();
SystemSpecs {
app_version,
release_channel: release_channel.display_name(),
os_name,
os_version,
memory,
architecture,
commit_sha,
}
})
SystemSpecs {
app_version,
release_channel: release_channel.display_name(),
os_name,
os_version,
memory,
architecture,
commit_sha,
}
}
}
impl Display for SystemSpecs {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let os_information = format!("OS: {} {}", self.os_name, self.os_version);
let os_information = match &self.os_version {
Some(os_version) => format!("OS: {} {}", self.os_name, os_version),
None => format!("OS: {}", self.os_name),
};
let app_version_information = format!(
"Zed: v{} ({})",
self.app_version,

View File

@@ -1,6 +1,6 @@
use std::iter::FromIterator;
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
pub struct CharBag(u64);
impl CharBag {

View File

@@ -316,7 +316,7 @@ fn check_path_to_repo_path_errors(relative_file_path: &Path) -> Result<()> {
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum GitFileStatus {
Added,
Modified,

View File

@@ -87,7 +87,7 @@ cbindgen = "0.26.0"
[target.'cfg(target_os = "macos")'.dependencies]
block = "0.1"
cocoa.workspace = true
cocoa = "0.25"
core-foundation.workspace = true
core-graphics = "0.23"
core-text = "20.1"

View File

@@ -76,7 +76,6 @@ fn main() {
cx.open_window(options, |cx| {
cx.activate(false);
cx.new_view(|_cx| AnimationExample {})
})
.unwrap();
});
});
}

View File

@@ -34,7 +34,6 @@ fn main() {
text: "World".into(),
})
},
)
.unwrap();
);
});
}

View File

@@ -80,8 +80,8 @@ fn main() {
}),
window_bounds: Some(WindowBounds::Windowed(Bounds {
size: size(px(1100.), px(600.)),
origin: Point::new(px(200.), px(200.)),
size: size(px(1100.), px(600.)).into(),
origin: Point::new(DevicePixels::from(200), DevicePixels::from(200)),
})),
..Default::default()
@@ -93,7 +93,6 @@ fn main() {
local_resource: Arc::new(PathBuf::from_str("examples/image/app-icon.png").unwrap()),
remote_resource: "https://picsum.photos/512/512".into(),
})
})
.unwrap();
});
});
}

View File

@@ -29,8 +29,7 @@ fn main() {
}]);
cx.open_window(WindowOptions::default(), |cx| {
cx.new_view(|_cx| SetMenus {})
})
.unwrap();
});
});
}

View File

@@ -24,18 +24,21 @@ fn main() {
for screen in cx.displays() {
let options = {
let margin_right = px(16.);
let margin_height = px(-48.);
let popup_margin_width = DevicePixels::from(16);
let popup_margin_height = DevicePixels::from(-0) - DevicePixels::from(48);
let size = Size {
let window_size = Size {
width: px(400.),
height: px(72.),
};
let bounds = gpui::Bounds::<Pixels> {
origin: screen.bounds().upper_right()
- point(size.width + margin_right, margin_height),
size,
let screen_bounds = screen.bounds();
let size: Size<DevicePixels> = window_size.into();
let bounds = gpui::Bounds::<DevicePixels> {
origin: screen_bounds.upper_right()
- point(size.width + popup_margin_width, popup_margin_height),
size: window_size.into(),
};
WindowOptions {
@@ -58,8 +61,7 @@ fn main() {
cx.new_view(|_| WindowContent {
text: format!("{:?}", screen.id()).into(),
})
})
.unwrap();
});
}
});
}

View File

@@ -27,12 +27,13 @@ use util::ResultExt;
use crate::{
current_platform, init_app_menus, Action, ActionRegistry, Any, AnyView, AnyWindowHandle,
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,
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,
};
mod async_context;
@@ -168,6 +169,11 @@ impl App {
self
}
/// Returns metadata associated with the application
pub fn metadata(&self) -> AppMetadata {
self.0.borrow().app_metadata.clone()
}
/// Returns a handle to the [`BackgroundExecutor`] associated with this app, which can be used to spawn futures in the background.
pub fn background_executor(&self) -> BackgroundExecutor {
self.0.borrow().background_executor.clone()
@@ -202,6 +208,7 @@ type NewViewListener = Box<dyn FnMut(AnyView, &mut WindowContext) + 'static>;
pub struct AppContext {
pub(crate) this: Weak<AppCell>,
pub(crate) platform: Rc<dyn Platform>,
app_metadata: AppMetadata,
text_system: Arc<TextSystem>,
flushing_effects: bool,
pending_updates: usize,
@@ -254,10 +261,17 @@ impl AppContext {
let text_system = Arc::new(TextSystem::new(platform.text_system()));
let entities = EntityMap::new();
let app_metadata = AppMetadata {
os_name: platform.os_name(),
os_version: platform.os_version().ok(),
app_version: platform.app_version().ok(),
};
let app = Rc::new_cyclic(|this| AppCell {
app: RefCell::new(AppContext {
this: this.clone(),
platform: platform.clone(),
app_metadata,
text_system,
actions: Rc::new(ActionRegistry::default()),
flushing_effects: false,
@@ -332,6 +346,11 @@ impl AppContext {
self.platform.quit();
}
/// Get metadata about the app and platform.
pub fn app_metadata(&self) -> AppMetadata {
self.app_metadata.clone()
}
/// Schedules all windows in the application to be redrawn. This can be called
/// multiple times in an update cycle and still result in a single redraw.
pub fn refresh(&mut self) {
@@ -471,26 +490,26 @@ impl AppContext {
&mut self,
options: crate::WindowOptions,
build_root_view: impl FnOnce(&mut WindowContext) -> View<V>,
) -> anyhow::Result<WindowHandle<V>> {
) -> WindowHandle<V> {
self.update(|cx| {
let id = cx.windows.insert(None);
let handle = WindowHandle::new(id);
match Window::new(handle.into(), options, cx) {
Ok(mut window) => {
let root_view = build_root_view(&mut WindowContext::new(cx, &mut window));
window.root_view.replace(root_view.into());
cx.window_handles.insert(id, window.handle);
cx.windows.get_mut(id).unwrap().replace(window);
Ok(handle)
}
Err(e) => {
cx.windows.remove(id);
return Err(e);
}
}
let mut window = Window::new(handle.into(), options, cx);
let root_view = build_root_view(&mut WindowContext::new(cx, &mut window));
window.root_view.replace(root_view.into());
cx.window_handles.insert(id, window.handle);
cx.windows.get_mut(id).unwrap().replace(window);
handle
})
}
/// Returns Ok() if the platform supports opening windows.
/// This returns false (for example) on linux when we could
/// not establish a connection to X or Wayland.
pub fn can_open_windows(&self) -> anyhow::Result<()> {
self.platform.can_open_windows()
}
/// Instructs the platform to activate the application by bringing it to the foreground.
pub fn activate(&self, ignoring_other_apps: bool) {
self.platform.activate(ignoring_other_apps);
@@ -597,12 +616,6 @@ impl AppContext {
self.platform.app_path()
}
/// On Linux, returns the name of the compositor in use.
/// Is blank on other platforms.
pub fn compositor_name(&self) -> &'static str {
self.platform.compositor_name()
}
/// Returns the file URL of the executable with the specified name in the application bundle
pub fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf> {
self.platform.path_for_auxiliary_executable(name)
@@ -722,7 +735,11 @@ impl AppContext {
})
.collect::<Vec<_>>()
{
self.update_window(window, |_, cx| cx.draw()).unwrap();
self.update_window(window, |_, cx| {
println!("flush_effects. cx.draw()");
cx.draw()
})
.unwrap();
}
if self.pending_effects.is_empty() {

View File

@@ -151,7 +151,7 @@ impl AsyncAppContext {
.upgrade()
.ok_or_else(|| anyhow!("app was released"))?;
let mut lock = app.borrow_mut();
lock.open_window(options, build_root_view)
Ok(lock.open_window(options, build_root_view))
}
/// Schedule a future to be polled in the background.

View File

@@ -193,22 +193,19 @@ impl TestAppContext {
},
|cx| cx.new_view(build_window),
)
.unwrap()
}
/// Adds a new window with no content.
pub fn add_empty_window(&mut self) -> &mut VisualTestContext {
let mut cx = self.app.borrow_mut();
let bounds = Bounds::maximized(None, &mut cx);
let window = cx
.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
..Default::default()
},
|cx| cx.new_view(|_| Empty),
)
.unwrap();
let window = cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
..Default::default()
},
|cx| cx.new_view(|_| Empty),
);
drop(cx);
let cx = VisualTestContext::from_window(*window.deref(), self).as_mut();
cx.run_until_parked();
@@ -225,15 +222,13 @@ impl TestAppContext {
{
let mut cx = self.app.borrow_mut();
let bounds = Bounds::maximized(None, &mut cx);
let window = cx
.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
..Default::default()
},
|cx| cx.new_view(build_root_view),
)
.unwrap();
let window = cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
..Default::default()
},
|cx| cx.new_view(build_root_view),
);
drop(cx);
let view = window.root_view(self).unwrap();
let cx = VisualTestContext::from_window(*window.deref(), self).as_mut();

View File

@@ -1,9 +1,6 @@
use anyhow::{bail, Context};
use serde::de::{self, Deserialize, Deserializer, Visitor};
use std::{
fmt,
hash::{Hash, Hasher},
};
use std::fmt;
/// Convert an RGB hex color code number to a color type
pub fn rgb(hex: u32) -> Rgba {
@@ -270,15 +267,6 @@ impl Ord for Hsla {
impl Eq for Hsla {}
impl Hash for Hsla {
fn hash<H: Hasher>(&self, state: &mut H) {
state.write_u32(u32::from_be_bytes(self.h.to_be_bytes()));
state.write_u32(u32::from_be_bytes(self.s.to_be_bytes()));
state.write_u32(u32::from_be_bytes(self.l.to_be_bytes()));
state.write_u32(u32::from_be_bytes(self.a.to_be_bytes()));
}
}
/// Construct an [`Hsla`] object from plain values
pub fn hsla(h: f32, s: f32, l: f32, a: f32) -> Hsla {
Hsla {

View File

@@ -363,6 +363,15 @@ pub struct Size<T: Clone + Default + Debug> {
pub height: T,
}
impl From<Size<DevicePixels>> for Size<Pixels> {
fn from(size: Size<DevicePixels>) -> Self {
Size {
width: Pixels(size.width.0 as f32),
height: Pixels(size.height.0 as f32),
}
}
}
/// Constructs a new `Size<T>` with the provided width and height.
///
/// # Arguments
@@ -624,6 +633,15 @@ impl<T: Clone + Default + Debug> From<Point<T>> for Size<T> {
}
}
impl From<Size<Pixels>> for Size<DevicePixels> {
fn from(size: Size<Pixels>) -> Self {
Size {
width: DevicePixels(size.width.0 as i32),
height: DevicePixels(size.height.0 as i32),
}
}
}
impl From<Size<Pixels>> for Size<DefiniteLength> {
fn from(size: Size<Pixels>) -> Self {
Size {
@@ -704,27 +722,28 @@ pub struct Bounds<T: Clone + Default + Debug> {
pub size: Size<T>,
}
impl Bounds<Pixels> {
impl Bounds<DevicePixels> {
/// Generate a centered bounds for the given display or primary display if none is provided
pub fn centered(
display_id: Option<DisplayId>,
size: Size<Pixels>,
size: impl Into<Size<DevicePixels>>,
cx: &mut AppContext,
) -> Self {
let display = display_id
.and_then(|id| cx.find_display(id))
.or_else(|| cx.primary_display());
let size = size.into();
display
.map(|display| {
let center = display.bounds().center();
Bounds {
origin: point(center.x - size.width / 2., center.y - size.height / 2.),
origin: point(center.x - size.width / 2, center.y - size.height / 2),
size,
}
})
.unwrap_or_else(|| Bounds {
origin: point(px(0.), px(0.)),
origin: point(DevicePixels(0), DevicePixels(0)),
size,
})
}
@@ -738,8 +757,8 @@ impl Bounds<Pixels> {
display
.map(|display| display.bounds())
.unwrap_or_else(|| Bounds {
origin: point(px(0.), px(0.)),
size: size(px(1024.), px(768.)),
origin: point(DevicePixels(0), DevicePixels(0)),
size: size(DevicePixels(1024), DevicePixels(768)),
})
}
}
@@ -1290,16 +1309,6 @@ impl<T: PartialOrd + Default + Debug + Clone> Bounds<T> {
}
}
impl Size<DevicePixels> {
/// Converts the size from physical to logical pixels.
pub(crate) fn to_pixels(self, scale_factor: f32) -> Size<Pixels> {
size(
px(self.width.0 as f32 / scale_factor),
px(self.height.0 as f32 / scale_factor),
)
}
}
impl Bounds<Pixels> {
/// Scales the bounds by a given factor, typically used to adjust for display scaling.
///
@@ -1337,33 +1346,6 @@ impl Bounds<Pixels> {
size: self.size.scale(factor),
}
}
/// Convert the bounds from logical pixels to physical pixels
pub fn to_device_pixels(&self, factor: f32) -> Bounds<DevicePixels> {
Bounds {
origin: point(
DevicePixels((self.origin.x.0 * factor) as i32),
DevicePixels((self.origin.y.0 * factor) as i32),
),
size: size(
DevicePixels((self.size.width.0 * factor) as i32),
DevicePixels((self.size.height.0 * factor) as i32),
),
}
}
}
impl Bounds<DevicePixels> {
/// Convert the bounds from physical pixels to logical pixels
pub fn to_pixels(self, scale_factor: f32) -> Bounds<Pixels> {
Bounds {
origin: point(
px(self.origin.x.0 as f32 / scale_factor),
px(self.origin.y.0 as f32 / scale_factor),
),
size: self.size.to_pixels(scale_factor),
}
}
}
impl<T: Clone + Debug + Copy + Default> Copy for Bounds<T> {}

View File

@@ -486,7 +486,6 @@ mod test {
focus_handle: cx.focus_handle(),
})
})
.unwrap()
});
cx.update(|cx| {

View File

@@ -295,7 +295,7 @@ impl KeyBindingContextPredicate {
}
_ if is_identifier_char(next) => {
let len = source
.find(|c: char| !is_identifier_char(c) && !is_vim_operator_char(c))
.find(|c: char| !is_identifier_char(c))
.unwrap_or(source.len());
let (identifier, rest) = source.split_at(len);
source = skip_whitespace(rest);
@@ -356,7 +356,7 @@ fn is_identifier_char(c: char) -> bool {
}
fn is_vim_operator_char(c: char) -> bool {
c == '>' || c == '<' || c == '~' || c == '"'
c == '>' || c == '<'
}
fn skip_whitespace(source: &str) -> &str {

View File

@@ -70,19 +70,6 @@ pub(crate) fn current_platform() -> Rc<dyn Platform> {
}
#[cfg(target_os = "linux")]
pub(crate) fn current_platform() -> Rc<dyn Platform> {
match guess_compositor() {
"Wayland" => Rc::new(WaylandClient::new()),
"X11" => Rc::new(X11Client::new()),
"Headless" => Rc::new(HeadlessClient::new()),
_ => unreachable!(),
}
}
/// Return which compositor we're guessing we'll use.
/// Does not attempt to connect to the given compositor
#[cfg(target_os = "linux")]
#[inline]
pub fn guess_compositor() -> &'static str {
let wayland_display = std::env::var_os("WAYLAND_DISPLAY");
let x11_display = std::env::var_os("DISPLAY");
@@ -90,14 +77,13 @@ pub fn guess_compositor() -> &'static str {
let use_x11 = x11_display.is_some_and(|display| !display.is_empty());
if use_wayland {
"Wayland"
Rc::new(WaylandClient::new())
} else if use_x11 {
"X11"
Rc::new(X11Client::new())
} else {
"Headless"
Rc::new(HeadlessClient::new())
}
}
// todo("windows")
#[cfg(target_os = "windows")]
pub(crate) fn current_platform() -> Rc<dyn Platform> {
@@ -120,12 +106,14 @@ pub(crate) trait Platform: 'static {
fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>>;
fn primary_display(&self) -> Option<Rc<dyn PlatformDisplay>>;
fn active_window(&self) -> Option<AnyWindowHandle>;
fn can_open_windows(&self) -> anyhow::Result<()> {
Ok(())
}
fn open_window(
&self,
handle: AnyWindowHandle,
options: WindowParams,
) -> anyhow::Result<Box<dyn PlatformWindow>>;
) -> Box<dyn PlatformWindow>;
/// Returns the appearance of the application's windows.
fn window_appearance(&self) -> WindowAppearance;
@@ -155,9 +143,9 @@ pub(crate) trait Platform: 'static {
fn on_will_open_app_menu(&self, callback: Box<dyn FnMut()>);
fn on_validate_app_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>);
fn compositor_name(&self) -> &'static str {
""
}
fn os_name(&self) -> &'static str;
fn os_version(&self) -> Result<SemanticVersion>;
fn app_version(&self) -> Result<SemanticVersion>;
fn app_path(&self) -> Result<PathBuf>;
fn local_timezone(&self) -> UtcOffset;
fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf>;
@@ -187,12 +175,12 @@ pub trait PlatformDisplay: Send + Sync + Debug {
fn uuid(&self) -> Result<Uuid>;
/// Get the bounds for this display
fn bounds(&self) -> Bounds<Pixels>;
fn bounds(&self) -> Bounds<DevicePixels>;
/// Get the default bounds for this display to place a window
fn default_bounds(&self) -> Bounds<Pixels> {
fn default_bounds(&self) -> Bounds<DevicePixels> {
let center = self.bounds().center();
let offset = DEFAULT_WINDOW_SIZE / 2.0;
let offset = DEFAULT_WINDOW_SIZE / 2;
let origin = point(center.x - offset.width, center.y - offset.height);
Bounds::new(origin, DEFAULT_WINDOW_SIZE)
}
@@ -211,7 +199,7 @@ impl Debug for DisplayId {
unsafe impl Send for DisplayId {}
pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
fn bounds(&self) -> Bounds<Pixels>;
fn bounds(&self) -> Bounds<DevicePixels>;
fn is_maximized(&self) -> bool;
fn window_bounds(&self) -> WindowBounds;
fn content_size(&self) -> Size<Pixels>;
@@ -300,6 +288,19 @@ pub(crate) trait PlatformTextSystem: Send + Sync {
fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> LineLayout;
}
/// Basic metadata about the current application and operating system.
#[derive(Clone, Debug)]
pub struct AppMetadata {
/// The name of the current operating system
pub os_name: &'static str,
/// The operating system's version
pub os_version: Option<SemanticVersion>,
/// The current version of the application
pub app_version: Option<SemanticVersion>,
}
#[derive(PartialEq, Eq, Hash, Clone)]
pub(crate) enum AtlasKey {
Glyph(RenderGlyphParams),
@@ -569,7 +570,7 @@ pub struct WindowOptions {
/// The variables that can be configured when creating a new window
#[derive(Debug)]
pub(crate) struct WindowParams {
pub bounds: Bounds<Pixels>,
pub bounds: Bounds<DevicePixels>,
/// The titlebar configuration of the window
pub titlebar: Option<TitlebarOptions>,
@@ -597,13 +598,13 @@ pub(crate) struct WindowParams {
#[derive(Debug, Copy, Clone, PartialEq)]
pub enum WindowBounds {
/// Indicates that the window should open in a windowed state with the given bounds.
Windowed(Bounds<Pixels>),
Windowed(Bounds<DevicePixels>),
/// Indicates that the window should open in a maximized state.
/// The bounds provided here represent the restore size of the window.
Maximized(Bounds<Pixels>),
Maximized(Bounds<DevicePixels>),
/// Indicates that the window should open in fullscreen mode.
/// The bounds provided here represent the restore size of the window.
Fullscreen(Bounds<Pixels>),
Fullscreen(Bounds<DevicePixels>),
}
impl Default for WindowBounds {
@@ -614,7 +615,7 @@ impl Default for WindowBounds {
impl WindowBounds {
/// Retrieve the inner bounds
pub fn get_bounds(&self) -> Bounds<Pixels> {
pub fn get_bounds(&self) -> Bounds<DevicePixels> {
match self {
WindowBounds::Windowed(bounds) => *bounds,
WindowBounds::Maximized(bounds) => *bounds,

View File

@@ -4,7 +4,7 @@
use super::{BladeAtlas, PATH_TEXTURE_FORMAT};
use crate::{
AtlasTextureKind, AtlasTile, Bounds, ContentMask, Hsla, MonochromeSprite, Path, PathId,
PathVertex, DevicePixels, PolychromeSprite, PrimitiveBatch, Quad, ScaledPixels, Scene, Shadow, Size,
PathVertex, PolychromeSprite, PrimitiveBatch, Quad, ScaledPixels, Scene, Shadow, Size,
Underline,
};
use bytemuck::{Pod, Zeroable};
@@ -417,10 +417,10 @@ impl BladeRenderer {
}
}
pub fn update_drawable_size(&mut self, size: Size<DevicePixels>) {
pub fn update_drawable_size(&mut self, size: Size<f64>) {
let gpu_size = gpu::Extent {
width: size.width.0 as u32,
height: size.height.0 as u32,
width: size.width as u32,
height: size.height as u32,
depth: 1,
};

View File

@@ -59,6 +59,10 @@ impl LinuxClient for HeadlessClient {
None
}
fn can_open_windows(&self) -> anyhow::Result<()> {
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
}
@@ -67,14 +71,8 @@ impl LinuxClient for HeadlessClient {
&self,
_handle: AnyWindowHandle,
_params: WindowParams,
) -> anyhow::Result<Box<dyn PlatformWindow>> {
Err(anyhow::anyhow!(
"neither DISPLAY nor WAYLAND_DISPLAY is set. You can run in headless mode"
))
}
fn compositor_name(&self) -> &'static str {
"headless"
) -> Box<dyn PlatformWindow> {
unimplemented!()
}
fn set_cursor_style(&self, _style: CursorStyle) {}

View File

@@ -39,8 +39,8 @@ 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, SharedString,
Size, Task, WindowAppearance, WindowOptions, WindowParams,
PlatformTextSystem, PlatformWindow, Point, PromptLevel, Result, SemanticVersion, Size, Task,
WindowAppearance, WindowOptions, WindowParams,
};
use super::x11::X11Client;
@@ -54,17 +54,18 @@ pub(crate) const DOUBLE_CLICK_DISTANCE: Pixels = px(5.0);
pub(crate) const KEYRING_LABEL: &str = "zed-github-account";
pub trait LinuxClient {
fn compositor_name(&self) -> &'static str;
fn with_common<R>(&self, f: impl FnOnce(&mut LinuxCommon) -> R) -> R;
fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>>;
fn primary_display(&self) -> Option<Rc<dyn PlatformDisplay>>;
fn display(&self, id: DisplayId) -> Option<Rc<dyn PlatformDisplay>>;
fn can_open_windows(&self) -> anyhow::Result<()> {
Ok(())
}
fn open_window(
&self,
handle: AnyWindowHandle,
options: WindowParams,
) -> anyhow::Result<Box<dyn PlatformWindow>>;
) -> Box<dyn PlatformWindow>;
fn set_cursor_style(&self, style: CursorStyle);
fn open_uri(&self, uri: &str);
fn write_to_primary(&self, item: ClipboardItem);
@@ -151,12 +152,12 @@ impl<P: LinuxClient + 'static> Platform for P {
});
}
fn quit(&self) {
self.with_common(|common| common.signal.stop());
fn can_open_windows(&self) -> anyhow::Result<()> {
self.can_open_windows()
}
fn compositor_name(&self) -> &'static str {
self.compositor_name()
fn quit(&self) {
self.with_common(|common| common.signal.stop());
}
fn restart(&self, binary_path: Option<PathBuf>) {
@@ -244,7 +245,7 @@ impl<P: LinuxClient + 'static> Platform for P {
&self,
handle: AnyWindowHandle,
options: WindowParams,
) -> anyhow::Result<Box<dyn PlatformWindow>> {
) -> Box<dyn PlatformWindow> {
self.open_window(handle, options)
}
@@ -368,6 +369,23 @@ impl<P: LinuxClient + 'static> Platform for P {
});
}
fn os_name(&self) -> &'static str {
"Linux"
}
fn os_version(&self) -> Result<SemanticVersion> {
Ok(SemanticVersion::new(1, 0, 0))
}
fn app_version(&self) -> Result<SemanticVersion> {
const VERSION: Option<&str> = option_env!("RELEASE_VERSION");
if let Some(version) = VERSION {
version.parse()
} else {
Ok(SemanticVersion::new(1, 0, 0))
}
}
fn app_path(&self) -> Result<PathBuf> {
// get the path of the executable of the current process
let exe_path = std::env::current_exe()?;
@@ -492,8 +510,6 @@ impl<P: LinuxClient + 'static> Platform for P {
fn read_from_clipboard(&self) -> Option<ClipboardItem> {
self.read_from_clipboard()
}
fn add_recent_document(&self, _path: &Path) {}
}
pub(super) fn open_uri_internal(uri: &str, activation_token: Option<&str>) {

View File

@@ -63,9 +63,7 @@ use crate::platform::linux::is_within_click_distance;
use crate::platform::linux::wayland::cursor::Cursor;
use crate::platform::linux::wayland::serial::{SerialKind, SerialTracker};
use crate::platform::linux::wayland::window::WaylandWindow;
use crate::platform::linux::xdg_desktop_portal::{
cursor_settings, Event as XDPEvent, XDPEventSource,
};
use crate::platform::linux::xdg_desktop_portal::{Event as XDPEvent, XDPEventSource};
use crate::platform::linux::LinuxClient;
use crate::platform::PlatformWindow;
use crate::{
@@ -405,14 +403,9 @@ impl WaylandClient {
let handle = event_loop.handle();
handle
.insert_source(main_receiver, {
let handle = handle.clone();
move |event, _, _: &mut WaylandClientStatePtr| {
if let calloop::channel::Event::Msg(runnable) = event {
handle.insert_idle(|_| {
runnable.run();
});
}
.insert_source(main_receiver, |event, _, _: &mut WaylandClientStatePtr| {
if let calloop::channel::Event::Msg(runnable) = event {
runnable.run();
}
})
.unwrap();
@@ -432,7 +425,7 @@ impl WaylandClient {
let (primary, clipboard) = unsafe { create_clipboards_from_external(display) };
let mut cursor = Cursor::new(&conn, &globals, 24);
let cursor = Cursor::new(&conn, &globals, 24);
handle
.insert_source(XDPEventSource::new(&common.background_executor), {
@@ -448,24 +441,10 @@ impl WaylandClient {
}
}
}
XDPEvent::CursorTheme(theme) => {
if let Some(client) = client.0.upgrade() {
let mut client = client.borrow_mut();
client.cursor.set_theme(theme.as_str(), None);
}
}
XDPEvent::CursorSize(size) => {
if let Some(client) = client.0.upgrade() {
let mut client = client.borrow_mut();
client.cursor.set_size(size);
}
}
}
})
.unwrap();
let foreground = common.foreground_executor.clone();
let mut state = Rc::new(RefCell::new(WaylandClientState {
serial_tracker: SerialTracker::new(),
globals,
@@ -527,18 +506,6 @@ impl WaylandClient {
pending_open_uri: None,
}));
foreground
.spawn({
let state = state.clone();
async move {
if let Ok((theme, size)) = cursor_settings().await {
let mut state = state.borrow_mut();
state.cursor.set_theme(theme.as_str(), size);
}
}
})
.detach();
WaylandSource::new(conn, event_queue)
.insert(handle)
.unwrap();
@@ -557,7 +524,7 @@ impl LinuxClient for WaylandClient {
Rc::new(WaylandDisplay {
id: id.clone(),
name: output.name.clone(),
bounds: output.bounds.to_pixels(output.scale as f32),
bounds: output.bounds,
}) as Rc<dyn PlatformDisplay>
})
.collect()
@@ -573,7 +540,7 @@ impl LinuxClient for WaylandClient {
Rc::new(WaylandDisplay {
id: object_id.clone(),
name: output.name.clone(),
bounds: output.bounds.to_pixels(output.scale as f32),
bounds: output.bounds,
}) as Rc<dyn PlatformDisplay>
})
})
@@ -587,7 +554,7 @@ impl LinuxClient for WaylandClient {
&self,
handle: AnyWindowHandle,
params: WindowParams,
) -> anyhow::Result<Box<dyn PlatformWindow>> {
) -> Box<dyn PlatformWindow> {
let mut state = self.0.borrow_mut();
let (window, surface_id) = WaylandWindow::new(
@@ -596,10 +563,10 @@ impl LinuxClient for WaylandClient {
WaylandClientStatePtr(Rc::downgrade(&self.0)),
params,
state.common.appearance,
)?;
);
state.windows.insert(surface_id, window.0.clone());
Ok(Box::new(window))
Box::new(window)
}
fn set_cursor_style(&self, style: CursorStyle) {
@@ -721,10 +688,6 @@ impl LinuxClient for WaylandClient {
.as_ref()
.map(|window| window.handle())
}
fn compositor_name(&self) -> &'static str {
"Wayland"
}
}
impl Dispatch<wl_registry::WlRegistry, GlobalListContents> for WaylandClientStatePtr {
@@ -1109,15 +1072,14 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr {
..
} => {
let focused_window = state.keyboard_focused_window.clone();
let Some(focused_window) = focused_window else {
return;
};
let keymap_state = state.keymap_state.as_mut().unwrap();
keymap_state.update_mask(mods_depressed, mods_latched, mods_locked, 0, 0, group);
state.modifiers = Modifiers::from_xkb(keymap_state);
let Some(focused_window) = focused_window else {
return;
};
let input = PlatformInput::ModifiersChanged(ModifiersChangedEvent {
modifiers: state.modifiers,
});

View File

@@ -1,18 +1,14 @@
use crate::Globals;
use util::ResultExt;
use wayland_client::protocol::wl_pointer::WlPointer;
use wayland_client::protocol::wl_surface::WlSurface;
use wayland_client::protocol::{wl_pointer::WlPointer, wl_shm::WlShm};
use wayland_client::Connection;
use wayland_cursor::{CursorImageBuffer, CursorTheme};
pub(crate) struct Cursor {
theme: Option<CursorTheme>,
theme_name: Option<String>,
surface: WlSurface,
size: u32,
shm: WlShm,
connection: Connection,
}
impl Drop for Cursor {
@@ -26,49 +22,10 @@ impl Cursor {
pub fn new(connection: &Connection, globals: &Globals, size: u32) -> Self {
Self {
theme: CursorTheme::load(&connection, globals.shm.clone(), size).log_err(),
theme_name: None,
surface: globals.compositor.create_surface(&globals.qh, ()),
shm: globals.shm.clone(),
connection: connection.clone(),
size,
}
}
pub fn set_theme(&mut self, theme_name: &str, size: Option<u32>) {
if let Some(size) = size {
self.size = size;
}
if let Some(theme) =
CursorTheme::load_from_name(&self.connection, self.shm.clone(), theme_name, self.size)
.log_err()
{
self.theme = Some(theme);
self.theme_name = Some(theme_name.to_string());
} else if let Some(theme) =
CursorTheme::load(&self.connection, self.shm.clone(), self.size).log_err()
{
self.theme = Some(theme);
self.theme_name = None;
}
}
pub fn set_size(&mut self, size: u32) {
self.size = size;
self.theme = self
.theme_name
.as_ref()
.and_then(|name| {
CursorTheme::load_from_name(
&self.connection,
self.shm.clone(),
name.as_str(),
self.size,
)
.log_err()
})
.or_else(|| CursorTheme::load(&self.connection, self.shm.clone(), self.size).log_err());
}
pub fn set_icon(&mut self, wl_pointer: &WlPointer, serial_id: u32, mut cursor_icon_name: &str) {
if let Some(theme) = &mut self.theme {
let mut buffer: Option<&CursorImageBuffer>;

View File

@@ -6,14 +6,14 @@ use std::{
use uuid::Uuid;
use wayland_backend::client::ObjectId;
use crate::{Bounds, DisplayId, Pixels, PlatformDisplay};
use crate::{Bounds, DevicePixels, DisplayId, PlatformDisplay};
#[derive(Debug, Clone)]
pub(crate) struct WaylandDisplay {
/// The ID of the wl_output object
pub id: ObjectId,
pub name: Option<String>,
pub bounds: Bounds<Pixels>,
pub bounds: Bounds<DevicePixels>,
}
impl Hash for WaylandDisplay {
@@ -35,7 +35,7 @@ impl PlatformDisplay for WaylandDisplay {
}
}
fn bounds(&self) -> Bounds<Pixels> {
fn bounds(&self) -> Bounds<DevicePixels> {
self.bounds
}
}

View File

@@ -1,5 +1,6 @@
use std::cell::{Ref, RefCell, RefMut};
use std::ffi::c_void;
use std::num::NonZeroU32;
use std::ptr::NonNull;
use std::rc::Rc;
use std::sync::Arc;
@@ -25,9 +26,9 @@ use crate::platform::linux::wayland::serial::SerialKind;
use crate::platform::{PlatformAtlas, PlatformInputHandler, PlatformWindow};
use crate::scene::Scene;
use crate::{
px, size, AnyWindowHandle, Bounds, Globals, Modifiers, Output, Pixels, PlatformDisplay,
PlatformInput, Point, PromptLevel, Size, WaylandClientStatePtr, WindowAppearance,
WindowBackgroundAppearance, WindowBounds, WindowParams,
px, size, AnyWindowHandle, Bounds, DevicePixels, Globals, Modifiers, Output, Pixels,
PlatformDisplay, PlatformInput, Point, PromptLevel, Size, WaylandClientStatePtr,
WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowParams,
};
#[derive(Default)]
@@ -75,13 +76,13 @@ pub struct WaylandWindowState {
display: Option<(ObjectId, Output)>,
globals: Globals,
renderer: BladeRenderer,
bounds: Bounds<Pixels>,
bounds: Bounds<u32>,
scale: f32,
input_handler: Option<PlatformInputHandler>,
decoration_state: WaylandDecorationState,
fullscreen: bool,
maximized: bool,
windowed_bounds: Bounds<Pixels>,
windowed_bounds: Bounds<DevicePixels>,
client: WaylandClientStatePtr,
handle: AnyWindowHandle,
active: bool,
@@ -106,7 +107,9 @@ impl WaylandWindowState {
client: WaylandClientStatePtr,
globals: Globals,
options: WindowParams,
) -> anyhow::Result<Self> {
) -> Self {
let bounds = options.bounds.map(|p| p.0 as u32);
let raw = RawWindow {
window: surface.id().as_ptr().cast::<c_void>(),
display: surface
@@ -127,18 +130,18 @@ impl WaylandWindowState {
},
)
}
.map_err(|e| anyhow::anyhow!("{:?}", e))?,
.unwrap(),
);
let config = BladeSurfaceConfig {
size: gpu::Extent {
width: options.bounds.size.width.0 as u32,
height: options.bounds.size.height.0 as u32,
width: bounds.size.width,
height: bounds.size.height,
depth: 1,
},
transparent: options.window_background != WindowBackgroundAppearance::Opaque,
};
Ok(Self {
Self {
xdg_surface,
acknowledged_first_configure: false,
surface,
@@ -150,7 +153,7 @@ impl WaylandWindowState {
outputs: HashMap::default(),
display: None,
renderer: BladeRenderer::new(gpu, config),
bounds: options.bounds,
bounds,
scale: 1.0,
input_handler: None,
decoration_state: WaylandDecorationState::Client,
@@ -161,7 +164,7 @@ impl WaylandWindowState {
appearance,
handle,
active: false,
})
}
}
}
@@ -221,7 +224,7 @@ impl WaylandWindow {
client: WaylandClientStatePtr,
params: WindowParams,
appearance: WindowAppearance,
) -> anyhow::Result<(Self, ObjectId)> {
) -> (Self, ObjectId) {
let surface = globals.compositor.create_surface(&globals.qh, ());
let xdg_surface = globals
.wm_base
@@ -264,14 +267,14 @@ impl WaylandWindow {
client,
globals,
params,
)?)),
))),
callbacks: Rc::new(RefCell::new(Callbacks::default())),
});
// Kick things off
surface.commit();
Ok((this, surface.id()))
(this, surface.id())
}
}
@@ -346,16 +349,10 @@ impl WaylandWindowStatePtr {
pub fn handle_toplevel_event(&self, event: xdg_toplevel::Event) -> bool {
match event {
xdg_toplevel::Event::Configure {
width,
height,
mut width,
mut height,
states,
} => {
let mut size = if width == 0 || height == 0 {
None
} else {
Some(size(px(width as f32), px(height as f32)))
};
let fullscreen = states.contains(&(xdg_toplevel::State::Fullscreen as u8));
let maximized = states.contains(&(xdg_toplevel::State::Maximized as u8));
@@ -365,20 +362,19 @@ impl WaylandWindowStatePtr {
state.maximized = maximized;
if got_unmaximized {
size = Some(state.windowed_bounds.size);
} else if !fullscreen && !maximized {
if let Some(size) = size {
state.windowed_bounds = Bounds {
origin: Point::default(),
size,
};
}
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);
drop(state);
if let Some(size) = size {
self.resize(size);
}
self.resize(width, height);
false
}
@@ -487,43 +483,63 @@ impl WaylandWindowStatePtr {
bounds
}
pub fn set_size_and_scale(&self, size: Option<Size<Pixels>>, scale: Option<f32>) {
let (size, scale) = {
pub fn set_size_and_scale(
&self,
width: Option<NonZeroU32>,
height: Option<NonZeroU32>,
scale: Option<f32>,
) {
let (width, height, scale) = {
let mut state = self.state.borrow_mut();
if size.map_or(true, |size| size == state.bounds.size)
if width.map_or(true, |width| width.get() == state.bounds.size.width)
&& height.map_or(true, |height| height.get() == state.bounds.size.height)
&& scale.map_or(true, |scale| scale == state.scale)
{
return;
}
if let Some(size) = size {
state.bounds.size = size;
if let Some(width) = width {
state.bounds.size.width = width.get();
}
if let Some(height) = height {
state.bounds.size.height = height.get();
}
if let Some(scale) = scale {
state.scale = scale;
}
let device_bounds = state.bounds.to_device_pixels(state.scale);
state.renderer.update_drawable_size(device_bounds.size);
(state.bounds.size, state.scale)
let width = state.bounds.size.width;
let height = state.bounds.size.height;
let scale = state.scale;
state.renderer.update_drawable_size(size(
width as f64 * scale as f64,
height as f64 * scale as f64,
));
(width, height, scale)
};
if let Some(ref mut fun) = self.callbacks.borrow_mut().resize {
fun(size, scale);
fun(
Size {
width: px(width as f32),
height: px(height as f32),
},
scale,
);
}
{
let state = self.state.borrow();
if let Some(viewport) = &state.viewport {
viewport.set_destination(size.width.0 as i32, size.height.0 as i32);
viewport.set_destination(width as i32, height as i32);
}
}
}
pub fn resize(&self, size: Size<Pixels>) {
self.set_size_and_scale(Some(size), None);
pub fn resize(&self, width: Option<NonZeroU32>, height: Option<NonZeroU32>) {
self.set_size_and_scale(width, height, None);
}
pub fn rescale(&self, scale: f32) {
self.set_size_and_scale(None, Some(scale));
self.set_size_and_scale(None, None, Some(scale));
}
/// Notifies the window of the state of the decorations.
@@ -609,8 +625,8 @@ impl rwh::HasDisplayHandle for WaylandWindow {
}
impl PlatformWindow for WaylandWindow {
fn bounds(&self) -> Bounds<Pixels> {
self.borrow().bounds
fn bounds(&self) -> Bounds<DevicePixels> {
self.borrow().bounds.map(|p| DevicePixels(p as i32))
}
fn is_maximized(&self) -> bool {
@@ -624,13 +640,16 @@ impl PlatformWindow for WaylandWindow {
} else if state.maximized {
WindowBounds::Maximized(state.windowed_bounds)
} else {
drop(state);
WindowBounds::Windowed(self.bounds())
WindowBounds::Windowed(state.bounds.map(|p| DevicePixels(p as i32)))
}
}
fn content_size(&self) -> Size<Pixels> {
self.borrow().bounds.size
let state = self.borrow();
Size {
width: Pixels(state.bounds.size.width as f32),
height: Pixels(state.bounds.size.height as f32),
}
}
fn scale_factor(&self) -> f32 {
@@ -642,12 +661,11 @@ impl PlatformWindow for WaylandWindow {
}
fn display(&self) -> Option<Rc<dyn PlatformDisplay>> {
let state = self.borrow();
state.display.as_ref().map(|(id, display)| {
self.borrow().display.as_ref().map(|(id, display)| {
Rc::new(WaylandDisplay {
id: id.clone(),
name: display.name.clone(),
bounds: display.bounds.to_pixels(state.scale),
bounds: display.bounds,
}) as Rc<dyn PlatformDisplay>
})
}

View File

@@ -1,4 +1,4 @@
use std::cell::RefCell;
use std::cell::{Cell, RefCell};
use std::ffi::OsString;
use std::ops::Deref;
use std::rc::{Rc, Weak};
@@ -16,9 +16,9 @@ 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, KeyCode};
use x11rb::protocol::xkb::ConnectionExt as _;
use x11rb::protocol::xproto::{ChangeWindowAttributesAux, ConnectionExt as _};
use x11rb::protocol::xproto::{ChangeWindowAttributesAux, ConnectionExt as _, KeyButMask};
use x11rb::protocol::{randr, render, xinput, xkb, xproto, Event};
use x11rb::resource_manager::Database;
use x11rb::xcb_ffi::XCBConnection;
@@ -32,7 +32,7 @@ use crate::platform::{LinuxCommon, PlatformWindow};
use crate::{
modifiers_from_xinput_info, point, px, AnyWindowHandle, Bounds, CursorStyle, DisplayId,
Keystroke, Modifiers, ModifiersChangedEvent, Pixels, PlatformDisplay, PlatformInput, Point,
ScrollDelta, Size, TouchPhase, WindowParams, X11Window,
ScrollDelta, Size, TouchPhase, WindowParams, X11Window, XimXCBConnection,
};
use super::{
@@ -96,6 +96,13 @@ impl From<xim::ClientError> for EventHandlerError {
}
}
#[derive(Clone)]
struct KeyEvent {
time: Instant,
state: KeyButMask,
code: KeyCode,
}
pub struct X11ClientState {
pub(crate) loop_handle: LoopHandle<'static, X11Client>,
pub(crate) event_loop: Option<calloop::EventLoop<'static, X11Client>>,
@@ -113,7 +120,7 @@ pub struct X11ClientState {
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) ximc: Option<X11rbClient<XimXCBConnection>>,
pub(crate) xim_handler: Option<XimHandler>,
pub modifiers: Modifiers,
@@ -131,6 +138,8 @@ pub struct X11ClientState {
pub(crate) common: LinuxCommon,
pub(crate) clipboard: X11ClipboardContext<Clipboard>,
pub(crate) primary: X11ClipboardContext<Primary>,
last_key_event: Option<KeyEvent>,
}
#[derive(Clone)]
@@ -165,17 +174,9 @@ impl X11Client {
let handle = event_loop.handle();
handle
.insert_source(main_receiver, {
let handle = handle.clone();
move |event, _, _: &mut X11Client| {
if let calloop::channel::Event::Msg(runnable) = event {
// Insert the runnables as idle callbacks, so we make sure that user-input and X11
// events have higher priority and runnables are only worked off after the event
// callbacks.
handle.insert_idle(|_| {
runnable.run();
});
}
.insert_source(main_receiver, |event, _, _: &mut X11Client| {
if let calloop::channel::Event::Msg(runnable) = event {
runnable.run();
}
})
.unwrap();
@@ -282,7 +283,9 @@ impl X11Client {
let xcb_connection = Rc::new(xcb_connection);
let ximc = X11rbClient::init(Rc::clone(&xcb_connection), x_root_index, None).ok();
let xim_xcb_connection =
XimXCBConnection::new(Rc::clone(&xcb_connection), Rc::new(Cell::new(true)));
let ximc = X11rbClient::init(xim_xcb_connection.clone(), x_root_index, None).ok();
let xim_handler = if ximc.is_some() {
Some(XimHandler::new())
} else {
@@ -302,8 +305,42 @@ impl X11Client {
{
let xcb_connection = xcb_connection.clone();
move |_readiness, _, client| {
// println!("---------------------------------------------------");
// let mut events_count = 0;
// let start = Instant::now();
xim_xcb_connection.set_can_flush(false);
while let Some(event) = xcb_connection.poll_for_event()? {
// events_count += 1;
// let event_start = Instant::now();
// println!(
// "--> event {}: {} (sequnce: {:?})",
// events_count,
// ev_name,
// event.wire_sequence_number()
// );
// let _drop = util::defer(move || {
// println!(
// "<-- event {}: {}, took: {:?}",
// events_count,
// ev_name,
// event_start.elapsed()
// );
// });
let mut state = client.0.borrow_mut();
let last_key_event = match event {
Event::KeyPress(ev) | Event::KeyRelease(ev) => {
state.last_key_event.replace(KeyEvent { time: Instant::now(), state: ev.state, code: ev.detail });
None
}
_ => state.last_key_event.clone()
};
// let last_key_event_time = state.last_key_event.clone();
if state.ximc.is_none() || state.xim_handler.is_none() {
drop(state);
client.handle_event(event);
@@ -312,9 +349,9 @@ impl X11Client {
let mut ximc = state.ximc.take().unwrap();
let mut xim_handler = state.xim_handler.take().unwrap();
let xim_connected = xim_handler.connected;
drop(state);
let xim_filtered = match ximc.filter_event(&event, &mut xim_handler) {
Ok(handled) => handled,
Err(err) => {
@@ -322,21 +359,63 @@ impl X11Client {
false
}
};
let xim_callback_event = xim_handler.last_callback_event.take();
if let Some(xim_event) = xim_handler.last_callback_event.take() {
match xim_event {
XimCallbackEvent::XimXEvent(
event @ Event::KeyPress(key_event),
)
| XimCallbackEvent::XimXEvent(
event @ Event::KeyRelease(key_event),
) => {
// let ev_name = event_name(&event);
// println!("---> XimXEvent: {}", ev_name);
if let Some(last_key_event) = last_key_event {
// println!(
// "last_key_event_time: {}",
// last_key_event_time
// );
// println!(
// "event.time: {} (sequence: {})",
// key_event.time, key_event.sequence
// );
// let lag_ms = last_key_event_time.saturating_sub(key_event.time);
// if lag_ms > 100
let lag_ms = last_key_event.time.elapsed();
if (lag_ms > Duration::from_millis(100) && last_key_event.state == key_event.state && last_key_event.code == key_event.detail) ||
(lag_ms > Duration::from_millis(50) && last_key_event.state != key_event.state && last_key_event.code != key_event.detail)
{
let name = event_name(&event);
println!(">>>>>>>>>>>>>> dropping old event (name: {}, sequence: {}, lag_ms: {:?}) <<<<<<<<<<<<<<<<<<<<<", name, key_event.sequence, lag_ms);
// drop(state);
// continue;
} else {
client.handle_event(event);
}
} else {
client.handle_event(event);
}
}
XimCallbackEvent::XimXEvent(event) => {
client.handle_event(event);
}
XimCallbackEvent::XimCommitEvent(window, text) => {
client.xim_handle_commit(window, text);
}
XimCallbackEvent::XimPreeditEvent(window, text) => {
client.xim_handle_preedit(window, text);
}
};
}
let mut state = client.0.borrow_mut();
state.ximc = Some(ximc);
state.xim_handler = Some(xim_handler);
drop(state);
if let Some(event) = xim_callback_event {
client.handle_xim_callback_event(event);
}
if xim_filtered {
// println!(" -> xim filtered");
continue;
}
if xim_connected {
client.xim_handle_event(event);
} else {
@@ -344,12 +423,45 @@ impl X11Client {
}
}
// println!(
// "-------------- events_count: {:?}, took: {:?} ----------\n\n",
// events_count,
// start.elapsed()
// );
xim_xcb_connection.set_can_flush(true);
xim_xcb_connection.flush()?;
Ok(calloop::PostAction::Continue)
}
},
)
.expect("Failed to initialize x11 event source");
// handle
// .insert_source(xim_rx, {
// move |chan_event, _, client| match chan_event {
// channel::Event::Msg(xim_event) => {
// // println!(".......... XimCallBackEvent ...............");
// match xim_event {
// XimCallbackEvent::XimXEvent(event) => {
// println!("--> XimXEvent");
// client.handle_event(event);
// }
// XimCallbackEvent::XimCommitEvent(window, text) => {
// client.xim_handle_commit(window, text);
// }
// XimCallbackEvent::XimPreeditEvent(window, text) => {
// client.xim_handle_preedit(window, text);
// }
// };
// // println!("......... DONE: XimCallBackEvent ...............");
// }
// channel::Event::Closed => {
// log::error!("XIM Event Sender dropped")
// }
// }
// })
// .expect("Failed to initialize XIM event source");
handle
.insert_source(XDPEventSource::new(&common.background_executor), {
move |event, _, client| match event {
@@ -359,9 +471,6 @@ impl X11Client {
window.window.set_appearance(appearance);
}
}
XDPEvent::CursorTheme(_) | XDPEvent::CursorSize(_) => {
// noop, X11 manages this for us.
}
}
})
.unwrap();
@@ -400,6 +509,8 @@ impl X11Client {
clipboard,
primary,
last_key_event: None,
})))
}
@@ -461,11 +572,12 @@ impl X11Client {
state
.windows
.get(&win)
.filter(|window_reference| !window_reference.window.state.borrow().destroyed)
.map(|window_reference| window_reference.window.clone())
}
fn handle_event(&self, event: Event) -> Option<()> {
// println!("handle_event: {:?}", event_name(&event));
match event {
Event::ClientMessage(event) => {
let window = self.get_window(event.window)?;
@@ -498,6 +610,7 @@ impl X11Client {
Event::Expose(event) => {
let window = self.get_window(event.window)?;
window.refresh();
std::thread::sleep(Duration::from_millis(20));
}
Event::FocusIn(event) => {
let window = self.get_window(event.event)?;
@@ -528,20 +641,15 @@ impl X11Client {
0,
event.locked_group.into(),
);
let modifiers = Modifiers::from_xkb(&state.xkb);
if state.modifiers == modifiers {
drop(state);
} else {
let focused_window_id = state.focused_window?;
state.modifiers = modifiers;
drop(state);
let focused_window_id = state.focused_window?;
state.modifiers = modifiers;
drop(state);
let focused_window = self.get_window(focused_window_id)?;
focused_window.handle_input(PlatformInput::ModifiersChanged(
ModifiersChangedEvent { modifiers },
));
}
let focused_window = self.get_window(focused_window_id)?;
focused_window.handle_input(PlatformInput::ModifiersChanged(
ModifiersChangedEvent { modifiers },
));
}
Event::KeyPress(event) => {
let window = self.get_window(event.event)?;
@@ -593,6 +701,7 @@ impl X11Client {
keystroke
};
drop(state);
// println!("keydown. keystroke.key: {:?}", keystroke.key);
window.handle_input(PlatformInput::KeyDown(crate::KeyDownEvent {
keystroke,
is_held: false,
@@ -798,28 +907,21 @@ impl X11Client {
Some(())
}
fn handle_xim_callback_event(&self, event: XimCallbackEvent) {
match event {
XimCallbackEvent::XimXEvent(event) => {
self.handle_event(event);
}
XimCallbackEvent::XimCommitEvent(window, text) => {
self.xim_handle_commit(window, text);
}
XimCallbackEvent::XimPreeditEvent(window, text) => {
self.xim_handle_preedit(window, text);
}
};
}
fn xim_handle_event(&self, event: Event) -> Option<()> {
let name = event_name(&event);
match event {
Event::KeyPress(event) | Event::KeyRelease(event) => {
let mut state = self.0.borrow_mut();
let mut ximc = state.ximc.take().unwrap();
let mut xim_handler = state.xim_handler.take().unwrap();
drop(state);
xim_handler.window = event.event;
println!(
"--> ximc.forward_event({}), sequence: {}",
name, event.sequence
);
ximc.forward_event(
xim_handler.im_id,
xim_handler.ic_id,
@@ -892,10 +994,6 @@ impl X11Client {
}
impl LinuxClient for X11Client {
fn compositor_name(&self) -> &'static str {
"X11"
}
fn with_common<R>(&self, f: impl FnOnce(&mut LinuxCommon) -> R) -> R {
f(&mut self.0.borrow_mut().common)
}
@@ -908,11 +1006,8 @@ impl LinuxClient for X11Client {
.iter()
.enumerate()
.filter_map(|(root_id, _)| {
Some(Rc::new(X11Display::new(
&state.xcb_connection,
state.scale_factor,
root_id,
)?) as Rc<dyn PlatformDisplay>)
Some(Rc::new(X11Display::new(&state.xcb_connection, root_id)?)
as Rc<dyn PlatformDisplay>)
})
.collect()
}
@@ -921,12 +1016,8 @@ impl LinuxClient for X11Client {
let state = self.0.borrow();
Some(Rc::new(
X11Display::new(
&state.xcb_connection,
state.scale_factor,
state.x_root_index,
)
.expect("There should always be a root index"),
X11Display::new(&state.xcb_connection, state.x_root_index)
.expect("There should always be a root index"),
))
}
@@ -935,7 +1026,6 @@ impl LinuxClient for X11Client {
Some(Rc::new(X11Display::new(
&state.xcb_connection,
state.scale_factor,
id.0 as usize,
)?))
}
@@ -944,7 +1034,7 @@ impl LinuxClient for X11Client {
&self,
handle: AnyWindowHandle,
params: WindowParams,
) -> anyhow::Result<Box<dyn PlatformWindow>> {
) -> Box<dyn PlatformWindow> {
let mut state = self.0.borrow_mut();
let x_window = state.xcb_connection.generate_id().unwrap();
@@ -959,7 +1049,7 @@ impl LinuxClient for X11Client {
&state.atoms,
state.scale_factor,
state.common.appearance,
)?;
);
let screen_resources = state
.xcb_connection
@@ -1027,7 +1117,7 @@ impl LinuxClient for X11Client {
};
state.windows.insert(x_window, window_ref);
Ok(Box::new(window))
Box::new(window)
}
fn set_cursor_style(&self, style: CursorStyle) {
@@ -1138,3 +1228,111 @@ pub fn mode_refresh_rate(mode: &randr::ModeInfo) -> Duration {
fn fp3232_to_f32(value: xinput::Fp3232) -> f32 {
value.integral as f32 + value.frac as f32 / u32::MAX as f32
}
fn event_name(event: &Event) -> &'static str {
match event {
Event::Unknown(_) => "Event::Unknown",
Event::Error(_) => "Event::Error",
Event::ButtonPress(_) => "Event::ButtonPress",
Event::ButtonRelease(_) => "Event::ButtonRelease",
Event::CirculateNotify(_) => "Event::CirculateNotify",
Event::CirculateRequest(_) => "Event::CirculateRequest",
Event::ClientMessage(_) => "Event::ClientMessage",
Event::ColormapNotify(_) => "Event::ColormapNotify",
Event::ConfigureNotify(_) => "Event::ConfigureNotify",
Event::ConfigureRequest(_) => "Event::ConfigureRequest",
Event::CreateNotify(_) => "Event::CreateNotify",
Event::DestroyNotify(_) => "Event::DestroyNotify",
Event::EnterNotify(_) => "Event::EnterNotify",
Event::Expose(_) => "Event::Expose",
Event::FocusIn(_) => "Event::FocusIn",
Event::FocusOut(_) => "Event::FocusOut",
Event::GeGeneric(_) => "Event::GeGeneric",
Event::GraphicsExposure(_) => "Event::GraphicsExposure",
Event::GravityNotify(_) => "Event::GravityNotify",
Event::KeyPress(_) => "Event::KeyPress",
Event::KeyRelease(_) => "Event::KeyRelease",
Event::KeymapNotify(_) => "Event::KeymapNotify",
Event::LeaveNotify(_) => "Event::LeaveNotify",
Event::MapNotify(_) => "Event::MapNotify",
Event::MapRequest(_) => "Event::MapRequest",
Event::MappingNotify(_) => "Event::MappingNotify",
Event::MotionNotify(_) => "Event::MotionNotify",
Event::NoExposure(_) => "Event::NoExposure",
Event::PropertyNotify(_) => "Event::PropertyNotify",
Event::ReparentNotify(_) => "Event::ReparentNotify",
Event::ResizeRequest(_) => "Event::ResizeRequest",
Event::SelectionClear(_) => "Event::SelectionClear",
Event::SelectionNotify(_) => "Event::SelectionNotify",
Event::SelectionRequest(_) => "Event::SelectionRequest",
Event::UnmapNotify(_) => "Event::UnmapNotify",
Event::VisibilityNotify(_) => "Event::VisibilityNotify",
Event::RandrNotify(_) => "Event::RandrNotify",
Event::RandrScreenChangeNotify(_) => "Event::RandrScreenChangeNotify",
Event::ShapeNotify(_) => "Event::ShapeNotify",
Event::XfixesCursorNotify(_) => "Event::XfixesCursorNotify",
Event::XfixesSelectionNotify(_) => "Event::XfixesSelectionNotify",
Event::XinputBarrierHit(_) => "Event::XinputBarrierHit",
Event::XinputBarrierLeave(_) => "Event::XinputBarrierLeave",
Event::XinputButtonPress(_) => "Event::XinputButtonPress",
Event::XinputButtonRelease(_) => "Event::XinputButtonRelease",
Event::XinputChangeDeviceNotify(_) => "Event::XinputChangeDeviceNotify",
Event::XinputDeviceButtonPress(_) => "Event::XinputDeviceButtonPress",
Event::XinputDeviceButtonRelease(_) => "Event::XinputDeviceButtonRelease",
Event::XinputDeviceButtonStateNotify(_) => "Event::XinputDeviceButtonStateNotify",
Event::XinputDeviceChanged(_) => "Event::XinputDeviceChanged",
Event::XinputDeviceFocusIn(_) => "Event::XinputDeviceFocusIn",
Event::XinputDeviceFocusOut(_) => "Event::XinputDeviceFocusOut",
Event::XinputDeviceKeyPress(_) => "Event::XinputDeviceKeyPress",
Event::XinputDeviceKeyRelease(_) => "Event::XinputDeviceKeyRelease",
Event::XinputDeviceKeyStateNotify(_) => "Event::XinputDeviceKeyStateNotify",
Event::XinputDeviceMappingNotify(_) => "Event::XinputDeviceMappingNotify",
Event::XinputDeviceMotionNotify(_) => "Event::XinputDeviceMotionNotify",
Event::XinputDevicePresenceNotify(_) => "Event::XinputDevicePresenceNotify",
Event::XinputDevicePropertyNotify(_) => "Event::XinputDevicePropertyNotify",
Event::XinputDeviceStateNotify(_) => "Event::XinputDeviceStateNotify",
Event::XinputDeviceValuator(_) => "Event::XinputDeviceValuator",
Event::XinputEnter(_) => "Event::XinputEnter",
Event::XinputFocusIn(_) => "Event::XinputFocusIn",
Event::XinputFocusOut(_) => "Event::XinputFocusOut",
Event::XinputGesturePinchBegin(_) => "Event::XinputGesturePinchBegin",
Event::XinputGesturePinchEnd(_) => "Event::XinputGesturePinchEnd",
Event::XinputGesturePinchUpdate(_) => "Event::XinputGesturePinchUpdate",
Event::XinputGestureSwipeBegin(_) => "Event::XinputGestureSwipeBegin",
Event::XinputGestureSwipeEnd(_) => "Event::XinputGestureSwipeEnd",
Event::XinputGestureSwipeUpdate(_) => "Event::XinputGestureSwipeUpdate",
Event::XinputHierarchy(_) => "Event::XinputHierarchy",
Event::XinputKeyPress(_) => "Event::XinputKeyPress",
Event::XinputKeyRelease(_) => "Event::XinputKeyRelease",
Event::XinputLeave(_) => "Event::XinputLeave",
Event::XinputMotion(_) => "Event::XinputMotion",
Event::XinputProperty(_) => "Event::XinputProperty",
Event::XinputProximityIn(_) => "Event::XinputProximityIn",
Event::XinputProximityOut(_) => "Event::XinputProximityOut",
Event::XinputRawButtonPress(_) => "Event::XinputRawButtonPress",
Event::XinputRawButtonRelease(_) => "Event::XinputRawButtonRelease",
Event::XinputRawKeyPress(_) => "Event::XinputRawKeyPress",
Event::XinputRawKeyRelease(_) => "Event::XinputRawKeyRelease",
Event::XinputRawMotion(_) => "Event::XinputRawMotion",
Event::XinputRawTouchBegin(_) => "Event::XinputRawTouchBegin",
Event::XinputRawTouchEnd(_) => "Event::XinputRawTouchEnd",
Event::XinputRawTouchUpdate(_) => "Event::XinputRawTouchUpdate",
Event::XinputTouchBegin(_) => "Event::XinputTouchBegin",
Event::XinputTouchEnd(_) => "Event::XinputTouchEnd",
Event::XinputTouchOwnership(_) => "Event::XinputTouchOwnership",
Event::XinputTouchUpdate(_) => "Event::XinputTouchUpdate",
Event::XkbAccessXNotify(_) => "Event::XkbAccessXNotify",
Event::XkbActionMessage(_) => "Event::XkbActionMessage",
Event::XkbBellNotify(_) => "Event::XkbBellNotify",
Event::XkbCompatMapNotify(_) => "Event::XkbCompatMapNotify",
Event::XkbControlsNotify(_) => "Event::XkbControlsNotify",
Event::XkbExtensionDeviceNotify(_) => "Event::XkbExtensionDeviceNotify",
Event::XkbIndicatorMapNotify(_) => "Event::XkbIndicatorMapNotify",
Event::XkbIndicatorStateNotify(_) => "Event::XkbIndicatorStateNotify",
Event::XkbMapNotify(_) => "Event::XkbMapNotify",
Event::XkbNamesNotify(_) => "Event::XkbNamesNotify",
Event::XkbNewKeyboardNotify(_) => "Event::XkbNewKeyboardNotify",
Event::XkbStateNotify(_) => "Event::XkbStateNotify",
_ => "unknown",
}
}

View File

@@ -2,29 +2,25 @@ use anyhow::Result;
use uuid::Uuid;
use x11rb::{connection::Connection as _, xcb_ffi::XCBConnection};
use crate::{px, Bounds, DisplayId, Pixels, PlatformDisplay, Size};
use crate::{Bounds, DevicePixels, DisplayId, PlatformDisplay, Size};
#[derive(Debug)]
pub(crate) struct X11Display {
x_screen_index: usize,
bounds: Bounds<Pixels>,
bounds: Bounds<DevicePixels>,
uuid: Uuid,
}
impl X11Display {
pub(crate) fn new(
xc: &XCBConnection,
scale_factor: f32,
x_screen_index: usize,
) -> Option<Self> {
pub(crate) fn new(xc: &XCBConnection, x_screen_index: usize) -> Option<Self> {
let screen = xc.setup().roots.get(x_screen_index).unwrap();
Some(Self {
x_screen_index,
x_screen_index: x_screen_index,
bounds: Bounds {
origin: Default::default(),
size: Size {
width: px(screen.width_in_pixels as f32 / scale_factor),
height: px(screen.height_in_pixels as f32 / scale_factor),
width: DevicePixels(screen.width_in_pixels as i32),
height: DevicePixels(screen.height_in_pixels as i32),
},
},
uuid: Uuid::from_bytes([0; 16]),
@@ -41,7 +37,7 @@ impl PlatformDisplay for X11Display {
Ok(self.uuid)
}
fn bounds(&self) -> Bounds<Pixels> {
fn bounds(&self) -> Bounds<DevicePixels> {
self.bounds
}
}

View File

@@ -1,9 +1,9 @@
use crate::{
platform::blade::{BladeRenderer, BladeSurfaceConfig},
px, size, AnyWindowHandle, Bounds, DevicePixels, ForegroundExecutor, Modifiers, Pixels,
size, AnyWindowHandle, Bounds, DevicePixels, ForegroundExecutor, Modifiers, Pixels,
PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow, Point,
PromptLevel, Scene, Size, WindowAppearance, WindowBackgroundAppearance, WindowBounds,
WindowKind, WindowParams, X11ClientStatePtr,
WindowParams, X11ClientStatePtr,
};
use blade_graphics as gpu;
@@ -29,6 +29,7 @@ use std::{
ptr::NonNull,
rc::Rc,
sync::{self, Arc},
time::Instant,
};
use super::{X11Display, XINPUT_MASTER_DEVICE};
@@ -47,8 +48,6 @@ x11rb::atom_manager! {
_NET_WM_STATE_HIDDEN,
_NET_WM_STATE_FOCUSED,
_NET_WM_MOVERESIZE,
_NET_WM_WINDOW_TYPE,
_NET_WM_WINDOW_TYPE_NOTIFICATION,
_GTK_SHOW_WINDOW_MENU,
}
}
@@ -156,13 +155,12 @@ pub struct Callbacks {
}
pub struct X11WindowState {
pub destroyed: bool,
client: X11ClientStatePtr,
executor: ForegroundExecutor,
atoms: XcbAtoms,
x_root_window: xproto::Window,
_raw: RawWindow,
bounds: Bounds<Pixels>,
bounds: Bounds<i32>,
scale_factor: f32,
renderer: BladeRenderer,
display: Rc<dyn PlatformDisplay>,
@@ -219,7 +217,7 @@ impl X11WindowState {
atoms: &XcbAtoms,
scale_factor: f32,
appearance: WindowAppearance,
) -> anyhow::Result<Self> {
) -> Self {
let x_screen_index = params
.display_id
.map_or(x_main_screen_index, |did| did.0 as usize);
@@ -251,7 +249,8 @@ impl X11WindowState {
xcb_connection
.create_colormap(xproto::ColormapAlloc::NONE, id, visual_set.root, visual.id)
.unwrap()
.check()?;
.check()
.unwrap();
id
};
@@ -273,17 +272,18 @@ impl X11WindowState {
visual.depth,
x_window,
visual_set.root,
(params.bounds.origin.x.0 * scale_factor) as i16,
(params.bounds.origin.y.0 * scale_factor) as i16,
(params.bounds.size.width.0 * scale_factor) as u16,
(params.bounds.size.height.0 * scale_factor) as u16,
params.bounds.origin.x.0 as i16,
params.bounds.origin.y.0 as i16,
params.bounds.size.width.0 as u16,
params.bounds.size.height.0 as u16,
0,
xproto::WindowClass::INPUT_OUTPUT,
visual.id,
&win_aux,
)
.unwrap()
.check()?;
.check()
.unwrap();
if let Some(titlebar) = params.titlebar {
if let Some(title) = titlebar.title {
@@ -298,17 +298,6 @@ impl X11WindowState {
.unwrap();
}
}
if params.kind == WindowKind::PopUp {
xcb_connection
.change_property32(
xproto::PropMode::REPLACE,
x_window,
atoms._NET_WM_WINDOW_TYPE,
xproto::AtomEnum::ATOM,
&[atoms._NET_WM_WINDOW_TYPE_NOTIFICATION],
)
.unwrap();
}
xcb_connection
.change_property32(
@@ -357,7 +346,7 @@ impl X11WindowState {
},
)
}
.map_err(|e| anyhow::anyhow!("{:?}", e))?,
.unwrap(),
);
let config = BladeSurfaceConfig {
@@ -367,23 +356,20 @@ impl X11WindowState {
transparent: params.window_background != WindowBackgroundAppearance::Opaque,
};
Ok(Self {
Self {
client,
executor,
display: Rc::new(
X11Display::new(xcb_connection, scale_factor, x_screen_index).unwrap(),
),
display: Rc::new(X11Display::new(xcb_connection, x_screen_index).unwrap()),
_raw: raw,
x_root_window: visual_set.root,
bounds: params.bounds,
bounds: params.bounds.map(|v| v.0),
scale_factor,
renderer: BladeRenderer::new(gpu, config),
atoms: *atoms,
input_handler: None,
appearance,
handle,
destroyed: false,
})
}
}
fn content_size(&self) -> Size<Pixels> {
@@ -409,10 +395,6 @@ impl Drop for X11Window {
.unwrap();
self.0.xcb_connection.flush().unwrap();
// Mark window as destroyed so that we can filter out when X11 events
// for it still come in.
state.destroyed = true;
let this_ptr = self.0.clone();
let client_ptr = state.client.clone();
state
@@ -445,8 +427,8 @@ impl X11Window {
atoms: &XcbAtoms,
scale_factor: f32,
appearance: WindowAppearance,
) -> anyhow::Result<Self> {
Ok(Self(X11WindowStatePtr {
) -> Self {
Self(X11WindowStatePtr {
state: Rc::new(RefCell::new(X11WindowState::new(
handle,
client,
@@ -458,11 +440,11 @@ impl X11Window {
atoms,
scale_factor,
appearance,
)?)),
))),
callbacks: Rc::new(RefCell::new(Callbacks::default())),
xcb_connection: xcb_connection.clone(),
x_window,
}))
})
}
fn set_wm_hints(&self, wm_hint_property_state: WmHintPropertyState, prop1: u32, prop2: u32) {
@@ -550,8 +532,15 @@ impl X11WindowStatePtr {
}
pub fn handle_input(&self, input: PlatformInput) {
// println!("handle_input called");
// let start = Instant::now();
if let Some(ref mut fun) = self.callbacks.borrow_mut().input {
// println!(
// "handle_input. got input callback. elapsed: {:?}",
// start.elapsed()
// );
if !fun(input.clone()).propagate {
// println!("handle_input. return here. elapsed: {:?}", start.elapsed());
return;
}
}
@@ -629,7 +618,6 @@ impl X11WindowStatePtr {
let is_resize;
{
let mut state = self.state.borrow_mut();
let bounds = bounds.map(|f| px(f as f32 / state.scale_factor));
is_resize = bounds.size.width != state.bounds.size.width
|| bounds.size.height != state.bounds.size.height;
@@ -644,10 +632,9 @@ impl X11WindowStatePtr {
let gpu_size = query_render_extent(&self.xcb_connection, self.x_window);
if state.renderer.viewport_size() != gpu_size {
state.renderer.update_drawable_size(size(
DevicePixels(gpu_size.width as i32),
DevicePixels(gpu_size.height as i32),
));
state
.renderer
.update_drawable_size(size(gpu_size.width as f64, gpu_size.height as f64));
resize_args = Some((state.content_size(), state.scale_factor));
}
}
@@ -682,8 +669,8 @@ impl X11WindowStatePtr {
}
impl PlatformWindow for X11Window {
fn bounds(&self) -> Bounds<Pixels> {
self.0.state.borrow().bounds
fn bounds(&self) -> Bounds<DevicePixels> {
self.0.state.borrow().bounds.map(|v| v.into())
}
fn is_maximized(&self) -> bool {
@@ -697,11 +684,7 @@ impl PlatformWindow for X11Window {
fn window_bounds(&self) -> WindowBounds {
let state = self.0.state.borrow();
if self.is_maximized() {
WindowBounds::Maximized(state.bounds)
} else {
WindowBounds::Windowed(state.bounds)
}
WindowBounds::Windowed(state.bounds.map(|p| DevicePixels(p)))
}
fn content_size(&self) -> Size<Pixels> {

View File

@@ -1,7 +1,16 @@
use std::default::Default;
use std::{cell::Cell, default::Default, rc::Rc};
use x11rb::protocol::{xproto, Event};
use xim::{AHashMap, AttributeName, Client, ClientError, ClientHandler, InputStyle};
use calloop::channel;
use x11rb::{
connection::{Connection, RequestConnection},
cookie::{CookieWithFds, VoidCookie},
protocol::{xproto, Event},
xcb_ffi::XCBConnection,
};
use xim::{
x11rb::HasConnection, AHashMap, AttributeName, Client, ClientError, ClientHandler, InputStyle,
};
pub enum XimCallbackEvent {
XimXEvent(x11rb::protocol::Event),
@@ -12,6 +21,7 @@ pub enum XimCallbackEvent {
pub struct XimHandler {
pub im_id: u16,
pub ic_id: u16,
// pub xim_tx: channel::Sender<XimCallbackEvent>,
pub connected: bool,
pub window: xproto::Window,
pub last_callback_event: Option<XimCallbackEvent>,
@@ -22,6 +32,7 @@ impl XimHandler {
Self {
im_id: Default::default(),
ic_id: Default::default(),
// xim_tx,
connected: false,
window: Default::default(),
last_callback_event: None,
@@ -78,10 +89,12 @@ impl<C: Client<XEvent = xproto::KeyPressEvent>> ClientHandler<C> for XimHandler
_input_context_id: u16,
text: &str,
) -> Result<(), ClientError> {
self.last_callback_event = Some(XimCallbackEvent::XimCommitEvent(
self.window,
String::from(text),
));
self.last_callback_event
.replace(XimCallbackEvent::XimCommitEvent(
self.window,
String::from(text),
));
// .ok();
Ok(())
}
@@ -95,11 +108,20 @@ impl<C: Client<XEvent = xproto::KeyPressEvent>> ClientHandler<C> for XimHandler
) -> Result<(), ClientError> {
match xev.response_type {
x11rb::protocol::xproto::KEY_PRESS_EVENT => {
self.last_callback_event = Some(XimCallbackEvent::XimXEvent(Event::KeyPress(xev)));
// println!(
// "XimHandler. handle_forward_event(KeyPress). sequence: {}",
// xev.sequence
// );
self.last_callback_event
.replace(XimCallbackEvent::XimXEvent(Event::KeyPress(xev)));
}
x11rb::protocol::xproto::KEY_RELEASE_EVENT => {
self.last_callback_event =
Some(XimCallbackEvent::XimXEvent(Event::KeyRelease(xev)));
// println!(
// "XimHandler. handle_forward_event(KeyRelease), sequence: {}",
// xev.sequence
// );
self.last_callback_event
.replace(XimCallbackEvent::XimXEvent(Event::KeyRelease(xev)));
}
_ => {}
}
@@ -138,10 +160,185 @@ 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.last_callback_event = Some(XimCallbackEvent::XimPreeditEvent(
self.window,
String::from(preedit_string),
));
self.last_callback_event
.replace(XimCallbackEvent::XimPreeditEvent(
self.window,
String::from(preedit_string),
));
// self.xim_tx
// .send()
// .ok();
Ok(())
}
}
#[derive(Clone)]
pub struct XimXCBConnection(Rc<XCBConnection>, Rc<Cell<bool>>);
impl XimXCBConnection {
pub fn new(connection: Rc<XCBConnection>, can_flush: Rc<Cell<bool>>) -> Self {
Self(connection, can_flush)
}
pub fn set_can_flush(&self, can_flush: bool) {
self.1.set(can_flush);
}
}
impl HasConnection for XimXCBConnection {
type Connection = Self;
fn conn(&self) -> &Self::Connection {
self
}
}
impl Connection for XimXCBConnection {
fn wait_for_raw_event_with_sequence(
&self,
) -> Result<x11rb::connection::RawEventAndSeqNumber<Self::Buf>, x11rb::errors::ConnectionError>
{
self.0.wait_for_raw_event_with_sequence()
}
fn poll_for_raw_event_with_sequence(
&self,
) -> Result<
Option<x11rb::connection::RawEventAndSeqNumber<Self::Buf>>,
x11rb::errors::ConnectionError,
> {
self.0.poll_for_raw_event_with_sequence()
}
fn flush(&self) -> Result<(), x11rb::errors::ConnectionError> {
if self.1.get() {
// println!("*real* flush");
self.0.flush()
} else {
// println!("fake flush");
Ok(())
}
}
fn setup(&self) -> &xproto::Setup {
self.0.setup()
}
fn generate_id(&self) -> Result<u32, x11rb::errors::ReplyOrIdError> {
self.0.generate_id()
}
}
impl RequestConnection for XimXCBConnection {
type Buf = <XCBConnection as RequestConnection>::Buf;
fn send_request_with_reply<R>(
&self,
bufs: &[std::io::IoSlice<'_>],
fds: Vec<x11rb::utils::RawFdContainer>,
) -> Result<x11rb::cookie::Cookie<'_, Self, R>, x11rb::errors::ConnectionError>
where
R: x11rb::x11_utils::TryParse,
{
self.0
.send_request_with_reply::<R>(bufs, fds)
.map(|cookie| x11rb::cookie::Cookie::new(self, cookie.sequence_number()))
}
fn send_request_with_reply_with_fds<R>(
&self,
bufs: &[std::io::IoSlice<'_>],
fds: Vec<x11rb::utils::RawFdContainer>,
) -> Result<CookieWithFds<'_, Self, R>, x11rb::errors::ConnectionError>
where
R: x11rb::x11_utils::TryParseFd,
{
self.0
.send_request_with_reply_with_fds::<R>(bufs, fds)
.map(|cookie| CookieWithFds::new(self, cookie.sequence_number()))
}
fn send_request_without_reply(
&self,
bufs: &[std::io::IoSlice<'_>],
fds: Vec<x11rb::utils::RawFdContainer>,
) -> Result<VoidCookie<'_, Self>, x11rb::errors::ConnectionError> {
self.0
.send_request_without_reply(bufs, fds)
.map(|cookie| VoidCookie::new(self, cookie.sequence_number()))
}
fn discard_reply(
&self,
sequence: x11rb::connection::SequenceNumber,
kind: x11rb::connection::RequestKind,
mode: x11rb::connection::DiscardMode,
) {
self.0.discard_reply(sequence, kind, mode)
}
fn prefetch_extension_information(
&self,
extension_name: &'static str,
) -> Result<(), x11rb::errors::ConnectionError> {
self.0.prefetch_extension_information(extension_name)
}
fn extension_information(
&self,
extension_name: &'static str,
) -> Result<Option<x11rb::x11_utils::ExtensionInformation>, x11rb::errors::ConnectionError>
{
self.0.extension_information(extension_name)
}
fn wait_for_reply_or_raw_error(
&self,
sequence: x11rb::connection::SequenceNumber,
) -> Result<x11rb::connection::ReplyOrError<Self::Buf>, x11rb::errors::ConnectionError> {
self.0.wait_for_reply_or_raw_error(sequence)
}
fn wait_for_reply(
&self,
sequence: x11rb::connection::SequenceNumber,
) -> Result<Option<Self::Buf>, x11rb::errors::ConnectionError> {
self.0.wait_for_reply(sequence)
}
fn wait_for_reply_with_fds_raw(
&self,
sequence: x11rb::connection::SequenceNumber,
) -> Result<
x11rb::connection::ReplyOrError<x11rb::connection::BufWithFds<Self::Buf>, Self::Buf>,
x11rb::errors::ConnectionError,
> {
self.0.wait_for_reply_with_fds_raw(sequence)
}
fn check_for_raw_error(
&self,
sequence: x11rb::connection::SequenceNumber,
) -> Result<Option<Self::Buf>, x11rb::errors::ConnectionError> {
self.0.check_for_raw_error(sequence)
}
fn prefetch_maximum_request_bytes(&self) {
self.0.prefetch_maximum_request_bytes()
}
fn maximum_request_bytes(&self) -> usize {
self.0.maximum_request_bytes()
}
fn parse_error(
&self,
error: &[u8],
) -> Result<x11rb::x11_utils::X11Error, x11rb::errors::ParseError> {
self.0.parse_error(error)
}
fn parse_event(&self, event: &[u8]) -> Result<Event, x11rb::errors::ParseError> {
self.0.parse_event(event)
}
}

View File

@@ -1,18 +1,18 @@
//! Provides a [calloop] event source from [XDG Desktop Portal] events
//!
//! This module uses the [ashpd] crate
//! This module uses the [ashpd] crate and handles many async loop
use std::future::Future;
use ashpd::desktop::settings::{ColorScheme, Settings};
use calloop::channel::Channel;
use calloop::channel::{Channel, Sender};
use calloop::{EventSource, Poll, PostAction, Readiness, Token, TokenFactory};
use smol::stream::StreamExt;
use util::ResultExt;
use crate::{BackgroundExecutor, WindowAppearance};
pub enum Event {
WindowAppearance(WindowAppearance),
CursorTheme(String),
CursorSize(u32),
}
pub struct XDPEventSource {
@@ -23,63 +23,35 @@ impl XDPEventSource {
pub fn new(executor: &BackgroundExecutor) -> Self {
let (sender, channel) = calloop::channel::channel();
let background = executor.clone();
executor
.spawn(async move {
let settings = Settings::new().await?;
if let Ok(mut cursor_theme_changed) = settings
.receive_setting_changed_with_args(
"org.gnome.desktop.interface",
"cursor-theme",
)
.await
{
let sender = sender.clone();
background
.spawn(async move {
while let Some(theme) = cursor_theme_changed.next().await {
let theme = theme?;
sender.send(Event::CursorTheme(theme))?;
}
anyhow::Ok(())
})
.detach();
}
if let Ok(mut cursor_size_changed) = settings
.receive_setting_changed_with_args::<u32>(
"org.gnome.desktop.interface",
"cursor-size",
)
.await
{
let sender = sender.clone();
background
.spawn(async move {
while let Some(size) = cursor_size_changed.next().await {
let size = size?;
sender.send(Event::CursorSize(size))?;
}
anyhow::Ok(())
})
.detach();
}
let mut appearance_changed = settings.receive_color_scheme_changed().await?;
while let Some(scheme) = appearance_changed.next().await {
sender.send(Event::WindowAppearance(WindowAppearance::from_native(
scheme,
)))?;
}
anyhow::Ok(())
})
.detach();
Self::spawn_observer(executor, Self::appearance_observer(sender.clone()));
Self { channel }
}
fn spawn_observer(
executor: &BackgroundExecutor,
to_spawn: impl Future<Output = Result<(), anyhow::Error>> + Send + 'static,
) {
executor
.spawn(async move {
to_spawn.await.log_err();
})
.detach()
}
async fn appearance_observer(sender: Sender<Event>) -> Result<(), anyhow::Error> {
let settings = Settings::new().await?;
// We observe the color change during the execution of the application
let mut stream = settings.receive_color_scheme_changed().await?;
while let Some(scheme) = stream.next().await {
sender.send(Event::WindowAppearance(WindowAppearance::from_native(
scheme,
)))?;
}
Ok(())
}
}
impl EventSource for XDPEventSource {
@@ -170,16 +142,3 @@ pub fn should_auto_hide_scrollbars(executor: &BackgroundExecutor) -> Result<bool
Ok(auto_hide)
})
}
pub async fn cursor_settings() -> Result<(String, Option<u32>), anyhow::Error> {
let settings = Settings::new().await?;
let cursor_theme = settings
.read::<String>("org.gnome.desktop.interface", "cursor-theme")
.await?;
let cursor_size = settings
.read::<u32>("org.gnome.desktop.interface", "cursor-size")
.await
.ok();
Ok((cursor_theme, cursor_size))
}

View File

@@ -1,4 +1,4 @@
use crate::{px, size, Bounds, DisplayId, Pixels, PlatformDisplay};
use crate::{point, size, Bounds, DevicePixels, DisplayId, PlatformDisplay};
use anyhow::Result;
use cocoa::{
appkit::NSScreen,
@@ -102,15 +102,18 @@ impl PlatformDisplay for MacDisplay {
]))
}
fn bounds(&self) -> Bounds<Pixels> {
fn bounds(&self) -> Bounds<DevicePixels> {
unsafe {
// CGDisplayBounds is in "global display" coordinates, where 0 is
// the top left of the primary display.
let bounds = CGDisplayBounds(self.0);
Bounds {
origin: Default::default(),
size: size(px(bounds.size.width as f32), px(bounds.size.height as f32)),
origin: point(DevicePixels(0), DevicePixels(0)),
size: size(
DevicePixels(bounds.size.width as i32),
DevicePixels(bounds.size.height as i32),
),
}
}
}

View File

@@ -201,7 +201,7 @@ mod sys {
#[link(name = "CoreFoundation", kind = "framework")]
#[link(name = "CoreVideo", kind = "framework")]
#[allow(improper_ctypes, unknown_lints, clippy::duplicated_attributes)]
#[allow(improper_ctypes)]
extern "C" {
pub fn CVDisplayLinkCreateWithActiveCGDisplays(
display_link_out: *mut *mut CVDisplayLink,

View File

@@ -5,7 +5,7 @@ use crate::{
Platform, PlatformDisplay, PlatformTextSystem, PlatformWindow, Result, SemanticVersion, Task,
WindowAppearance, WindowParams,
};
use anyhow::anyhow;
use anyhow::{anyhow, bail};
use block::ConcreteBlock;
use cocoa::{
appkit::{
@@ -367,18 +367,6 @@ impl MacPlatform {
}
}
}
fn os_version(&self) -> Result<SemanticVersion> {
unsafe {
let process_info = NSProcessInfo::processInfo(nil);
let version = process_info.operatingSystemVersion();
Ok(SemanticVersion::new(
version.majorVersion as usize,
version.minorVersion as usize,
version.patchVersion as usize,
))
}
}
}
impl Platform for MacPlatform {
@@ -516,16 +504,16 @@ impl Platform for MacPlatform {
&self,
handle: AnyWindowHandle,
options: WindowParams,
) -> Result<Box<dyn PlatformWindow>> {
) -> Box<dyn PlatformWindow> {
// Clippy thinks that this evaluates to `()`, for some reason.
#[allow(clippy::unit_arg, clippy::clone_on_copy)]
let renderer_context = self.0.lock().renderer_context.clone();
Ok(Box::new(MacWindow::open(
Box::new(MacWindow::open(
handle,
options,
self.foreground_executor(),
renderer_context,
)))
))
}
fn window_appearance(&self) -> WindowAppearance {
@@ -717,6 +705,40 @@ impl Platform for MacPlatform {
self.0.lock().validate_menu_command = Some(callback);
}
fn os_name(&self) -> &'static str {
"macOS"
}
fn os_version(&self) -> Result<SemanticVersion> {
unsafe {
let process_info = NSProcessInfo::processInfo(nil);
let version = process_info.operatingSystemVersion();
Ok(SemanticVersion::new(
version.majorVersion as usize,
version.minorVersion as usize,
version.patchVersion as usize,
))
}
}
fn app_version(&self) -> Result<SemanticVersion> {
unsafe {
let bundle: id = NSBundle::mainBundle();
if bundle.is_null() {
Err(anyhow!("app is not running inside a bundle"))
} else {
let version: id = msg_send![bundle, objectForInfoDictionaryKey: ns_string("CFBundleShortVersionString")];
if version.is_null() {
bail!("bundle does not have version");
}
let len = msg_send![version, lengthOfBytesUsingEncoding: NSUTF8StringEncoding];
let bytes = version.UTF8String() as *const u8;
let version = str::from_utf8(slice::from_raw_parts(bytes, len)).unwrap();
version.parse()
}
}
}
fn app_path(&self) -> Result<PathBuf> {
unsafe {
let bundle: id = NSBundle::mainBundle();

View File

@@ -1,10 +1,11 @@
use super::{ns_string, renderer, MacDisplay, NSRange, NSStringExt};
use crate::{
platform::PlatformInputHandler, point, px, size, AnyWindowHandle, Bounds, DisplayLink,
ExternalPaths, FileDropEvent, ForegroundExecutor, KeyDownEvent, Keystroke, Modifiers,
ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels,
PlatformAtlas, PlatformDisplay, PlatformInput, PlatformWindow, Point, PromptLevel, Size, Timer,
WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowKind, WindowParams,
platform::PlatformInputHandler, point, px, size, AnyWindowHandle, Bounds, DevicePixels,
DisplayLink, ExternalPaths, FileDropEvent, ForegroundExecutor, KeyDownEvent, Keystroke,
Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent,
Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, PlatformWindow, Point, PromptLevel,
Size, Timer, WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowKind,
WindowParams,
};
use block::ConcreteBlock;
use cocoa::{
@@ -344,7 +345,7 @@ struct MacWindowState {
external_files_dragged: bool,
// Whether the next left-mouse click is also the focusing click.
first_mouse: bool,
fullscreen_restore_bounds: Bounds<Pixels>,
fullscreen_restore_bounds: Bounds<DevicePixels>,
}
impl MacWindowState {
@@ -438,7 +439,7 @@ impl MacWindowState {
}
}
fn bounds(&self) -> Bounds<Pixels> {
fn bounds(&self) -> Bounds<DevicePixels> {
let mut window_frame = unsafe { NSWindow::frame(self.native_window) };
let screen_frame = unsafe {
let screen = NSWindow::screen(self.native_window);
@@ -451,12 +452,12 @@ impl MacWindowState {
let bounds = Bounds::new(
point(
px((window_frame.origin.x - screen_frame.origin.x) as f32),
px((window_frame.origin.y - screen_frame.origin.y) as f32),
((window_frame.origin.x - screen_frame.origin.x) as i32).into(),
((window_frame.origin.y - screen_frame.origin.y) as i32).into(),
),
size(
px(window_frame.size.width as f32),
px(window_frame.size.height as f32),
(window_frame.size.width as i32).into(),
(window_frame.size.height as i32).into(),
),
);
bounds
@@ -598,6 +599,14 @@ impl MacWindow {
let native_view = NSView::init(native_view);
assert!(!native_view.is_null());
let window_size = {
let scale = get_scale_factor(native_window);
size(
bounds.size.width.0 as f32 * scale,
bounds.size.height.0 as f32 * scale,
)
};
let mut window = Self(Arc::new(Mutex::new(MacWindowState {
handle,
executor,
@@ -608,7 +617,7 @@ impl MacWindow {
renderer_context,
native_window as *mut _,
native_view as *mut _,
bounds.size.map(|pixels| pixels.0),
window_size,
window_background != WindowBackgroundAppearance::Opaque,
),
request_frame_callback: None,
@@ -763,7 +772,7 @@ impl Drop for MacWindow {
}
impl PlatformWindow for MacWindow {
fn bounds(&self) -> Bounds<Pixels> {
fn bounds(&self) -> Bounds<DevicePixels> {
self.0.as_ref().lock().bounds()
}

View File

@@ -1,11 +1,12 @@
use crate::{px, Bounds, DisplayId, Pixels, PlatformDisplay, Point};
use anyhow::{Ok, Result};
use crate::{Bounds, DevicePixels, DisplayId, PlatformDisplay, Point};
#[derive(Debug)]
pub(crate) struct TestDisplay {
id: DisplayId,
uuid: uuid::Uuid,
bounds: Bounds<Pixels>,
bounds: Bounds<DevicePixels>,
}
impl TestDisplay {
@@ -13,7 +14,10 @@ impl TestDisplay {
TestDisplay {
id: DisplayId(1),
uuid: uuid::Uuid::new_v4(),
bounds: Bounds::from_corners(Point::default(), Point::new(px(1920.), px(1080.))),
bounds: Bounds::from_corners(
Point::default(),
Point::new(DevicePixels(1920), DevicePixels(1080)),
),
}
}
}
@@ -27,7 +31,7 @@ impl PlatformDisplay for TestDisplay {
Ok(self.uuid)
}
fn bounds(&self) -> crate::Bounds<crate::Pixels> {
fn bounds(&self) -> crate::Bounds<crate::DevicePixels> {
self.bounds
}
}

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