Compare commits
81 Commits
testing-in
...
adjust_yss
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e2ae0125bd | ||
|
|
1e86d012a3 | ||
|
|
4adc1f16cb | ||
|
|
6af5c650a2 | ||
|
|
004e77e723 | ||
|
|
6e9cfec24f | ||
|
|
d743c19fe2 | ||
|
|
73d0600ad2 | ||
|
|
f96cab286c | ||
|
|
8152e0676f | ||
|
|
62c12cd549 | ||
|
|
4fad96b179 | ||
|
|
ad44237467 | ||
|
|
bc736265be | ||
|
|
0697b417a0 | ||
|
|
04cd8dd0f2 | ||
|
|
ce643e6bef | ||
|
|
6ab9c3c3ab | ||
|
|
a765535557 | ||
|
|
089ea7852d | ||
|
|
ae650342ce | ||
|
|
1c09b69384 | ||
|
|
f842d19b0b | ||
|
|
d633a0da78 | ||
|
|
91b3c24ed3 | ||
|
|
20625e98ad | ||
|
|
9ff847753e | ||
|
|
ec95605fec | ||
|
|
ff8e7f91c1 | ||
|
|
2614215090 | ||
|
|
2386ae9f0e | ||
|
|
5674ba2a49 | ||
|
|
95118c6568 | ||
|
|
35c3af7fd0 | ||
|
|
5ef75919f0 | ||
|
|
9746f4f267 | ||
|
|
9693e394f7 | ||
|
|
45d217f6e0 | ||
|
|
ca187c8386 | ||
|
|
f72cf2afe3 | ||
|
|
8a79535b84 | ||
|
|
d4ec68b9ab | ||
|
|
c8a496ec4b | ||
|
|
c826ad2f82 | ||
|
|
f458f90673 | ||
|
|
39fb1d567d | ||
|
|
8b55494351 | ||
|
|
32e6424543 | ||
|
|
d2569afe66 | ||
|
|
5dbd23f6b0 | ||
|
|
b7d9aeb29d | ||
|
|
1aa9c868d4 | ||
|
|
848bb97ba7 | ||
|
|
8e925bf58f | ||
|
|
adcaa211ec | ||
|
|
393b16d226 | ||
|
|
7bd18fa653 | ||
|
|
11dc3c2582 | ||
|
|
268cb948a7 | ||
|
|
6a915e349c | ||
|
|
70d03e4841 | ||
|
|
b1eb0291dc | ||
|
|
e0644de90e | ||
|
|
9329ef1d78 | ||
|
|
664f779eb4 | ||
|
|
314b723292 | ||
|
|
1af1a9e8b3 | ||
|
|
8006f69513 | ||
|
|
bacc92333a | ||
|
|
eb7bd0b98a | ||
|
|
7f229dc202 | ||
|
|
03d0b68f0c | ||
|
|
5c2f27a501 | ||
|
|
d9d509a2bb | ||
|
|
a4ad3bcc08 | ||
|
|
6d7332e80c | ||
|
|
1b614ef63b | ||
|
|
604857ed2e | ||
|
|
d9eb3c4b35 | ||
|
|
f8beda0704 | ||
|
|
40fe5275cf |
2
.github/workflows/bump_patch_version.yml
vendored
2
.github/workflows/bump_patch_version.yml
vendored
@@ -9,7 +9,7 @@ on:
|
||||
|
||||
concurrency:
|
||||
# Allow only one workflow per any non-`main` branch.
|
||||
group: ${{ github.workflow }}-${{ github.event.input.branch }}
|
||||
group: ${{ github.workflow }}-${{ inputs.branch }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
|
||||
19
.github/workflows/ci.yml
vendored
19
.github/workflows/ci.yml
vendored
@@ -260,15 +260,13 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
bundle-deb:
|
||||
name: Create a *.deb Linux bundle
|
||||
bundle-linux:
|
||||
name: Create a Linux bundle
|
||||
runs-on: ubuntu-22.04 # keep the version fixed to avoid libc and dynamic linked library issues
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
|
||||
needs: [linux_tests]
|
||||
env:
|
||||
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
|
||||
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
|
||||
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
@@ -312,19 +310,18 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# TODO linux : Find a way to add licenses to the final bundle
|
||||
# - name: Generate license file
|
||||
# run: script/generate-licenses
|
||||
- name: Generate license file
|
||||
run: script/generate-licenses
|
||||
|
||||
- name: Create Linux *.deb bundle
|
||||
- name: Create and upload Linux .tar.gz bundle
|
||||
run: script/bundle-linux
|
||||
|
||||
- name: Upload app bundle to workflow run if main branch or specific label
|
||||
- name: Upload Linux bundle to workflow run if main branch or specific label
|
||||
uses: actions/upload-artifact@v4
|
||||
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
|
||||
with:
|
||||
name: Zed_${{ github.event.pull_request.head.sha || github.sha }}.deb
|
||||
path: target/release/*.deb
|
||||
name: zed-${{ github.event.pull_request.head.sha || github.sha }}-x86_64-unknown-linux-gnu.tar.gz
|
||||
path: zed-*.tar.gz
|
||||
|
||||
# TODO linux : make it stable enough to be uploaded as a release
|
||||
# - uses: softprops/action-gh-release@v1
|
||||
|
||||
11
.github/workflows/release_nightly.yml
vendored
11
.github/workflows/release_nightly.yml
vendored
@@ -94,7 +94,7 @@ jobs:
|
||||
run: script/upload-nightly macos
|
||||
|
||||
bundle-deb:
|
||||
name: Create a *.deb Linux bundle
|
||||
name: Create a Linux *.tar.gz bundle
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
runs-on: ubuntu-22.04 # keep the version fixed to avoid libc and dynamic linked library issues
|
||||
needs: tests
|
||||
@@ -125,12 +125,11 @@ jobs:
|
||||
echo "Publishing version: ${version} on release channel nightly"
|
||||
echo "nightly" > crates/zed/RELEASE_CHANNEL
|
||||
|
||||
# TODO linux : find a way to add licenses to the final bundle
|
||||
# - name: Generate license file
|
||||
# run: script/generate-licenses
|
||||
- name: Generate license file
|
||||
run: script/generate-licenses
|
||||
|
||||
- name: Create Linux *.deb bundle
|
||||
- name: Create Linux .tar.gz bundle
|
||||
run: script/bundle-linux
|
||||
|
||||
- name: Upload Zed Nightly
|
||||
run: script/upload-nightly linux-deb
|
||||
run: script/upload-nightly linux-targz
|
||||
|
||||
69
Cargo.lock
generated
69
Cargo.lock
generated
@@ -379,9 +379,11 @@ dependencies = [
|
||||
"assets",
|
||||
"assistant_tooling",
|
||||
"client",
|
||||
"collections",
|
||||
"editor",
|
||||
"env_logger",
|
||||
"feature_flags",
|
||||
"fs",
|
||||
"futures 0.3.28",
|
||||
"gpui",
|
||||
"language",
|
||||
@@ -3181,13 +3183,17 @@ dependencies = [
|
||||
"anyhow",
|
||||
"client",
|
||||
"collections",
|
||||
"ctor",
|
||||
"editor",
|
||||
"env_logger",
|
||||
"futures 0.3.28",
|
||||
"gpui",
|
||||
"language",
|
||||
"log",
|
||||
"lsp",
|
||||
"pretty_assertions",
|
||||
"project",
|
||||
"rand 0.8.5",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -3433,6 +3439,20 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "embed-resource"
|
||||
version = "2.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c6985554d0688b687c5cb73898a34fbe3ad6c24c58c238a4d91d5e840670ee9d"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"memchr",
|
||||
"rustc_version",
|
||||
"toml 0.8.10",
|
||||
"vswhom",
|
||||
"winreg 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "emojis"
|
||||
version = "0.6.1"
|
||||
@@ -3810,6 +3830,7 @@ dependencies = [
|
||||
"ctor",
|
||||
"editor",
|
||||
"env_logger",
|
||||
"futures 0.3.28",
|
||||
"fuzzy",
|
||||
"gpui",
|
||||
"itertools 0.11.0",
|
||||
@@ -4045,6 +4066,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"async-tar",
|
||||
"async-trait",
|
||||
"cocoa",
|
||||
"collections",
|
||||
"fsevent",
|
||||
"futures 0.3.28",
|
||||
@@ -4054,6 +4076,7 @@ dependencies = [
|
||||
"lazy_static",
|
||||
"libc",
|
||||
"notify",
|
||||
"objc",
|
||||
"parking_lot",
|
||||
"rope",
|
||||
"serde",
|
||||
@@ -4530,6 +4553,7 @@ dependencies = [
|
||||
"cosmic-text",
|
||||
"ctor",
|
||||
"derive_more",
|
||||
"embed-resource",
|
||||
"env_logger",
|
||||
"etagere",
|
||||
"filedescriptor",
|
||||
@@ -5896,6 +5920,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion 1.0.5",
|
||||
"collections",
|
||||
"editor",
|
||||
"gpui",
|
||||
"language",
|
||||
@@ -5952,9 +5977,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.6.3"
|
||||
version = "2.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c"
|
||||
checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d"
|
||||
|
||||
[[package]]
|
||||
name = "memfd"
|
||||
@@ -7963,7 +7988,7 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
"winreg",
|
||||
"winreg 0.50.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8667,6 +8692,7 @@ dependencies = [
|
||||
"languages",
|
||||
"log",
|
||||
"open_ai",
|
||||
"parking_lot",
|
||||
"project",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -9492,7 +9518,6 @@ dependencies = [
|
||||
"strum",
|
||||
"theme",
|
||||
"ui",
|
||||
"winresource",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -10508,7 +10533,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "tree-sitter"
|
||||
version = "0.20.100"
|
||||
source = "git+https://github.com/tree-sitter/tree-sitter?rev=528bcd2274814ca53711a57d71d1e3cf7abd73fe#528bcd2274814ca53711a57d71d1e3cf7abd73fe"
|
||||
source = "git+https://github.com/tree-sitter/tree-sitter?rev=7b4894ba2ae81b988846676f54c0988d4027ef4f#7b4894ba2ae81b988846676f54c0988d4027ef4f"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"regex",
|
||||
@@ -10620,7 +10645,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "tree-sitter-jsdoc"
|
||||
version = "0.20.0"
|
||||
source = "git+https://github.com/tree-sitter/tree-sitter-jsdoc#6a6cf9e7341af32d8e2b2e24a37fbfebefc3dc55"
|
||||
source = "git+https://github.com/tree-sitter/tree-sitter-jsdoc?rev=6a6cf9e7341af32d8e2b2e24a37fbfebefc3dc55#6a6cf9e7341af32d8e2b2e24a37fbfebefc3dc55"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"tree-sitter",
|
||||
@@ -11126,6 +11151,26 @@ version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64"
|
||||
|
||||
[[package]]
|
||||
name = "vswhom"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"vswhom-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vswhom-sys"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3b17ae1f6c8a2b28506cd96d412eebf83b4a0ff2cbefeeb952f2f9dfa44ba18"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vte"
|
||||
version = "0.13.0"
|
||||
@@ -12199,6 +12244,16 @@ dependencies = [
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winreg"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winresource"
|
||||
version = "0.1.17"
|
||||
@@ -12646,6 +12701,7 @@ dependencies = [
|
||||
"markdown_preview",
|
||||
"menu",
|
||||
"mimalloc",
|
||||
"nix 0.28.0",
|
||||
"node_runtime",
|
||||
"notifications",
|
||||
"outline",
|
||||
@@ -12668,6 +12724,7 @@ dependencies = [
|
||||
"tab_switcher",
|
||||
"task",
|
||||
"tasks_ui",
|
||||
"telemetry_events",
|
||||
"terminal_view",
|
||||
"theme",
|
||||
"theme_selector",
|
||||
|
||||
@@ -283,6 +283,8 @@ itertools = "0.11.0"
|
||||
lazy_static = "1.4.0"
|
||||
linkify = "0.10.0"
|
||||
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
|
||||
nanoid = "0.4"
|
||||
nix = "0.28"
|
||||
ordered-float = "2.1.1"
|
||||
palette = { version = "0.7.5", default-features = false, features = ["std"] }
|
||||
parking_lot = "0.12.1"
|
||||
@@ -341,7 +343,7 @@ tree-sitter-gowork = { git = "https://github.com/d1y/tree-sitter-go-work" }
|
||||
rustc-demangle = "0.1.23"
|
||||
tree-sitter-heex = { git = "https://github.com/phoenixframework/tree-sitter-heex", rev = "2e1348c3cf2c9323e87c2744796cf3f3868aa82a" }
|
||||
tree-sitter-html = "0.19.0"
|
||||
tree-sitter-jsdoc = { git = "https://github.com/tree-sitter/tree-sitter-jsdoc", ref = "6a6cf9e7341af32d8e2b2e24a37fbfebefc3dc55" }
|
||||
tree-sitter-jsdoc = { git = "https://github.com/tree-sitter/tree-sitter-jsdoc", rev = "6a6cf9e7341af32d8e2b2e24a37fbfebefc3dc55" }
|
||||
tree-sitter-json = { git = "https://github.com/tree-sitter/tree-sitter-json", rev = "40a81c01a40ac48744e0c8ccabbaba1920441199" }
|
||||
tree-sitter-markdown = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "330ecab87a3e3a7211ac69bbadc19eabecdb1cca" }
|
||||
tree-sitter-proto = { git = "https://github.com/rewinfrey/tree-sitter-proto", rev = "36d54f288aee112f13a67b550ad32634d0c2cb52" }
|
||||
@@ -407,7 +409,7 @@ features = [
|
||||
]
|
||||
|
||||
[patch.crates-io]
|
||||
tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "528bcd2274814ca53711a57d71d1e3cf7abd73fe" }
|
||||
tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "7b4894ba2ae81b988846676f54c0988d4027ef4f" }
|
||||
# Workaround for a broken nightly build of gpui: See #7644 and revisit once 0.5.3 is released.
|
||||
pathfinder_simd = { git = "https://github.com/servo/pathfinder.git", rev = "30419d07660dc11a21e42ef4a7fa329600cff152" }
|
||||
|
||||
|
||||
102
README.md
102
README.md
@@ -1,51 +1,51 @@
|
||||
# Zed
|
||||
|
||||
[](https://github.com/zed-industries/zed/actions/workflows/ci.yml)
|
||||
|
||||
Welcome to Zed, a high-performance, multiplayer code editor from the creators of [Atom](https://github.com/atom/atom) and [Tree-sitter](https://github.com/tree-sitter/tree-sitter).
|
||||
|
||||
## Installation
|
||||
|
||||
You can [download](https://zed.dev/download) Zed today for macOS (v10.15+).
|
||||
|
||||
Support for additional platforms is on our [roadmap](https://zed.dev/roadmap):
|
||||
|
||||
- Linux ([tracking issue](https://github.com/zed-industries/zed/issues/7015))
|
||||
- Windows ([tracking issue](https://github.com/zed-industries/zed/issues/5394))
|
||||
- Web ([tracking issue](https://github.com/zed-industries/zed/issues/5396))
|
||||
|
||||
For macOS users, you can also install Zed using [Homebrew](https://brew.sh/):
|
||||
|
||||
```sh
|
||||
brew install --cask zed
|
||||
```
|
||||
|
||||
Alternatively, to install the Preview release:
|
||||
|
||||
```sh
|
||||
brew tap homebrew/cask-versions
|
||||
brew install zed-preview
|
||||
```
|
||||
|
||||
## Developing Zed
|
||||
|
||||
- [Building Zed for macOS](./docs/src/developing_zed__building_zed_macos.md)
|
||||
- [Building Zed for Linux](./docs/src/developing_zed__building_zed_linux.md)
|
||||
- [Building Zed for Windows](./docs/src/developing_zed__building_zed_windows.md)
|
||||
- [Running Collaboration Locally](./docs/src/developing_zed__local_collaboration.md)
|
||||
|
||||
## Contributing
|
||||
|
||||
See [CONTRIBUTING.md](./CONTRIBUTING.md) for ways you can contribute to Zed.
|
||||
|
||||
Also... we're hiring! Check out our [jobs](https://zed.dev/jobs) page for open roles.
|
||||
|
||||
## Licensing
|
||||
|
||||
License information for third party dependencies must be correctly provided for CI to pass.
|
||||
|
||||
We use [`cargo-about`](https://github.com/EmbarkStudios/cargo-about) to automatically comply with open source licenses. If CI is failing, check the following:
|
||||
|
||||
- Is it showing a `no license specified` error for a crate you've created? If so, add `publish = false` under `[package]` in your crate's Cargo.toml.
|
||||
- Is the error `failed to satisfy license requirements` for a dependency? If so, first determine what license the project has and whether this system is sufficient to comply with this license's requirements. If you're unsure, ask a lawyer. Once you've verified that this system is acceptable add the license's SPDX identifier to the `accepted` array in `script/licenses/zed-licenses.toml`.
|
||||
- Is `cargo-about` unable to find the license for a dependency? If so, add a clarification field at the end of `script/licenses/zed-licenses.toml`, as specified in the [cargo-about book](https://embarkstudios.github.io/cargo-about/cli/generate/config.html#crate-configuration).
|
||||
# Zed
|
||||
|
||||
[](https://github.com/zed-industries/zed/actions/workflows/ci.yml)
|
||||
|
||||
Welcome to Zed, a high-performance, multiplayer code editor from the creators of [Atom](https://github.com/atom/atom) and [Tree-sitter](https://github.com/tree-sitter/tree-sitter).
|
||||
|
||||
## Installation
|
||||
|
||||
You can [download](https://zed.dev/download) Zed today for macOS (v10.15+).
|
||||
|
||||
Support for additional platforms is on our [roadmap](https://zed.dev/roadmap):
|
||||
|
||||
- Linux ([tracking issue](https://github.com/zed-industries/zed/issues/7015))
|
||||
- Windows ([tracking issue](https://github.com/zed-industries/zed/issues/5394))
|
||||
- Web ([tracking issue](https://github.com/zed-industries/zed/issues/5396))
|
||||
|
||||
For macOS users, you can also install Zed using [Homebrew](https://brew.sh/):
|
||||
|
||||
```sh
|
||||
brew install --cask zed
|
||||
```
|
||||
|
||||
Alternatively, to install the Preview release:
|
||||
|
||||
```sh
|
||||
brew tap homebrew/cask-versions
|
||||
brew install zed-preview
|
||||
```
|
||||
|
||||
## Developing Zed
|
||||
|
||||
- [Building Zed for macOS](./docs/src/developing_zed__building_zed_macos.md)
|
||||
- [Building Zed for Linux](./docs/src/developing_zed__building_zed_linux.md)
|
||||
- [Building Zed for Windows](./docs/src/developing_zed__building_zed_windows.md)
|
||||
- [Running Collaboration Locally](./docs/src/developing_zed__local_collaboration.md)
|
||||
|
||||
## Contributing
|
||||
|
||||
See [CONTRIBUTING.md](./CONTRIBUTING.md) for ways you can contribute to Zed.
|
||||
|
||||
Also... we're hiring! Check out our [jobs](https://zed.dev/jobs) page for open roles.
|
||||
|
||||
## Licensing
|
||||
|
||||
License information for third party dependencies must be correctly provided for CI to pass.
|
||||
|
||||
We use [`cargo-about`](https://github.com/EmbarkStudios/cargo-about) to automatically comply with open source licenses. If CI is failing, check the following:
|
||||
|
||||
- Is it showing a `no license specified` error for a crate you've created? If so, add `publish = false` under `[package]` in your crate's Cargo.toml.
|
||||
- Is the error `failed to satisfy license requirements` for a dependency? If so, first determine what license the project has and whether this system is sufficient to comply with this license's requirements. If you're unsure, ask a lawyer. Once you've verified that this system is acceptable add the license's SPDX identifier to the `accepted` array in `script/licenses/zed-licenses.toml`.
|
||||
- Is `cargo-about` unable to find the license for a dependency? If so, add a clarification field at the end of `script/licenses/zed-licenses.toml`, as specified in the [cargo-about book](https://embarkstudios.github.io/cargo-about/cli/generate/config.html#crate-configuration).
|
||||
|
||||
10
assets/icons/file_icons/c.svg
Normal file
10
assets/icons/file_icons/c.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_1791_43)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.8473 4.79901C13.7531 4.63587 13.6232 4.4934 13.4803 4.4109L8.51958 1.54683C8.23382 1.38181 7.76613 1.38181 7.48037 1.54683L2.51961 4.4109C2.2338 4.57587 2 4.98089 2 5.31089V11.0391C2 11.2041 2.05864 11.3879 2.15283 11.551C2.24699 11.7141 2.37691 11.8566 2.51977 11.939L7.48053 14.8031C7.7663 14.9681 8.23398 14.9681 8.51974 14.8031L13.4805 11.939C13.6234 11.8565 13.7532 11.714 13.8473 11.5509C13.9415 11.3878 14 11.204 14 11.039V5.31085C14 5.14583 13.9415 4.96211 13.8473 4.79901ZM4 8.175C4 10.3806 5.79438 12.175 7.99998 12.175C9.42327 12.175 10.7506 11.4091 11.464 10.1761L9.73295 9.17441C9.37586 9.79162 8.71182 10.175 7.99998 10.175C6.89716 10.175 5.99999 9.27778 5.99999 8.175C5.99999 7.07218 6.89716 6.17501 7.99998 6.17501C8.71174 6.17501 9.37578 6.55838 9.73284 7.17548L11.4639 6.17375C10.7505 4.9409 9.42319 4.17502 7.99998 4.17502C5.79438 4.17502 4 5.9694 4 8.175Z" fill="black"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_1791_43">
|
||||
<rect width="11.9999" height="13.5039" fill="white" transform="translate(2 1.42307)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
10
assets/icons/file_icons/cpp.svg
Normal file
10
assets/icons/file_icons/cpp.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_1791_60)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.4803 4.4109C13.6232 4.4934 13.7531 4.63587 13.8473 4.79901C13.9415 4.96211 14 5.14583 14 5.31085V5.78958H12.8036V7.50745H11.0857V8.99892H12.8036V10.7168H14V11.039C14 11.204 13.9415 11.3878 13.8473 11.5509C13.7532 11.714 13.6234 11.8565 13.4805 11.939L8.51974 14.8031C8.23398 14.9681 7.7663 14.9681 7.48053 14.8031L2.51977 11.939C2.37691 11.8566 2.24699 11.7141 2.15283 11.551C2.05864 11.3879 2 11.2041 2 11.0391V5.31089C2 4.98089 2.2338 4.57587 2.51961 4.4109L7.48037 1.54683C7.76613 1.38181 8.23382 1.38181 8.51958 1.54683L13.4803 4.4109ZM7.99998 12.175C5.79438 12.175 4 10.3806 4 8.175C4 5.9694 5.79438 4.17502 7.99998 4.17502C9.42319 4.17502 10.7505 4.9409 11.4639 6.17375L9.73284 7.17548C9.37578 6.55838 8.71174 6.17501 7.99998 6.17501C6.89716 6.17501 5.99999 7.07218 5.99999 8.175C5.99999 9.27778 6.89716 10.175 7.99998 10.175C8.71182 10.175 9.37586 9.79162 9.73295 9.17441L11.464 10.1761C10.7506 11.4091 9.42327 12.175 7.99998 12.175Z" fill="black"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_1791_60">
|
||||
<rect width="11.9999" height="13.5039" fill="white" transform="translate(2 1.42307)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -19,11 +19,11 @@
|
||||
"bash_profile": "terminal",
|
||||
"bashrc": "terminal",
|
||||
"bmp": "image",
|
||||
"c": "code",
|
||||
"cc": "code",
|
||||
"cjs": "code",
|
||||
"c": "c",
|
||||
"cc": "cpp",
|
||||
"cjs": "javascript",
|
||||
"conf": "settings",
|
||||
"cpp": "code",
|
||||
"cpp": "cpp",
|
||||
"css": "css",
|
||||
"csv": "storage",
|
||||
"cts": "typescript",
|
||||
@@ -58,7 +58,8 @@
|
||||
"gitmodules": "vcs",
|
||||
"go": "go",
|
||||
"graphql": "graphql",
|
||||
"h": "code",
|
||||
"h": "c",
|
||||
"hpp": "cpp",
|
||||
"handlebars": "code",
|
||||
"hbs": "template",
|
||||
"heex": "elixir",
|
||||
@@ -77,7 +78,8 @@
|
||||
"jp2": "image",
|
||||
"jpeg": "image",
|
||||
"jpg": "image",
|
||||
"js": "code",
|
||||
"js": "javascript",
|
||||
"jsx": "react",
|
||||
"json": "storage",
|
||||
"jsonc": "storage",
|
||||
"jxl": "image",
|
||||
@@ -95,7 +97,7 @@
|
||||
"mdx": "document",
|
||||
"metadata": "code",
|
||||
"mkv": "video",
|
||||
"mjs": "code",
|
||||
"mjs": "javascript",
|
||||
"mka": "audio",
|
||||
"ml": "ocaml",
|
||||
"mli": "ocaml",
|
||||
@@ -152,7 +154,7 @@
|
||||
"ts": "typescript",
|
||||
"tsv": "storage",
|
||||
"ttf": "font",
|
||||
"tsx": "code",
|
||||
"tsx": "react",
|
||||
"txt": "document",
|
||||
"tcl": "tcl",
|
||||
"vue": "vue",
|
||||
@@ -195,6 +197,12 @@
|
||||
"collapsed_folder": {
|
||||
"icon": "icons/file_icons/folder.svg"
|
||||
},
|
||||
"c": {
|
||||
"icon": "icons/file_icons/c.svg"
|
||||
},
|
||||
"cpp": {
|
||||
"icon": "icons/file_icons/cpp.svg"
|
||||
},
|
||||
"css": {
|
||||
"icon": "icons/file_icons/css.svg"
|
||||
},
|
||||
@@ -255,6 +263,9 @@
|
||||
"java": {
|
||||
"icon": "icons/file_icons/java.svg"
|
||||
},
|
||||
"javascript": {
|
||||
"icon": "icons/file_icons/javascript.svg"
|
||||
},
|
||||
"kotlin": {
|
||||
"icon": "icons/file_icons/kotlin.svg"
|
||||
},
|
||||
@@ -291,15 +302,18 @@
|
||||
"python": {
|
||||
"icon": "icons/file_icons/python.svg"
|
||||
},
|
||||
"r": {
|
||||
"icon": "icons/file_icons/r.svg"
|
||||
},
|
||||
"react": {
|
||||
"icon": "icons/file_icons/react.svg"
|
||||
},
|
||||
"ruby": {
|
||||
"icon": "icons/file_icons/ruby.svg"
|
||||
},
|
||||
"rust": {
|
||||
"icon": "icons/file_icons/rust.svg"
|
||||
},
|
||||
"r": {
|
||||
"icon": "icons/file_icons/r.svg"
|
||||
},
|
||||
"settings": {
|
||||
"icon": "icons/file_icons/settings.svg"
|
||||
},
|
||||
|
||||
3
assets/icons/file_icons/javascript.svg
Normal file
3
assets/icons/file_icons/javascript.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M4 2C2.89543 2 2 2.89543 2 4V12C2 13.1046 2.89543 14 4 14H12C13.1046 14 14 13.1046 14 12V4C14 2.89543 13.1046 2 12 2H4ZM7.26917 6.80584H6.04784V10.8808C6.04784 11.0672 6.02025 11.2241 5.96508 11.3516C5.90991 11.4791 5.82906 11.5761 5.72253 11.6427C5.61789 11.7074 5.48948 11.7397 5.33729 11.7397C5.19271 11.7397 5.06715 11.7112 4.96062 11.6541C4.85599 11.5951 4.77323 11.5114 4.71235 11.403C4.65338 11.2926 4.62199 11.1604 4.61819 11.0063H3.38829C3.38639 11.3944 3.46914 11.7169 3.63655 11.9737C3.80396 12.2286 4.03035 12.4189 4.31571 12.5444C4.60297 12.6681 4.92257 12.7299 5.27451 12.7299C5.67021 12.7299 6.0174 12.6547 6.31607 12.5045C6.61475 12.3542 6.84779 12.1402 7.0152 11.8624C7.18452 11.5847 7.26917 11.2574 7.26917 10.8808V6.80584ZM11.1672 7.95013C11.3403 8.07759 11.4383 8.25641 11.4611 8.4866H12.6453C12.6396 8.13846 12.5464 7.83218 12.3657 7.56775C12.185 7.30331 11.9319 7.0969 11.6066 6.94852C11.2832 6.80013 10.9046 6.72594 10.4709 6.72594C10.0448 6.72594 9.66429 6.80013 9.32947 6.94852C8.99464 7.0969 8.73116 7.30331 8.53902 7.56775C8.34878 7.83218 8.25461 8.14132 8.25652 8.49516C8.25461 8.92701 8.39634 9.27039 8.6817 9.52531C8.96706 9.78023 9.3561 9.96762 9.84882 10.0875L10.4852 10.2473C10.6982 10.2986 10.878 10.3557 11.0245 10.4185C11.1729 10.4813 11.2851 10.5574 11.3612 10.6468C11.4392 10.7362 11.4782 10.8465 11.4782 10.9778C11.4782 11.1186 11.4354 11.2432 11.3498 11.3516C11.2642 11.46 11.1434 11.5447 10.9874 11.6056C10.8333 11.6665 10.6516 11.6969 10.4424 11.6969C10.2293 11.6969 10.0381 11.6646 9.86879 11.5999C9.70138 11.5333 9.56727 11.4353 9.46644 11.306C9.36751 11.1747 9.31139 11.0111 9.29808 10.8151H8.10242C8.11193 11.2356 8.21371 11.5885 8.40776 11.8738C8.6037 12.1573 8.87574 12.3713 9.22388 12.5159C9.57392 12.6605 9.98484 12.7327 10.4566 12.7327C10.9322 12.7327 11.3384 12.6614 11.6751 12.5187C12.0137 12.3741 12.2725 12.1715 12.4513 11.9109C12.632 11.6484 12.7233 11.3383 12.7252 10.9806C12.7233 10.7371 12.6786 10.5212 12.5911 10.3329C12.5055 10.1445 12.3847 9.98093 12.2287 9.84206C12.0727 9.70319 11.8882 9.58619 11.6751 9.49107C11.4621 9.39595 11.2281 9.31985 10.9731 9.26278L10.4481 9.13722C10.3206 9.10869 10.2008 9.07444 10.0885 9.03449C9.97628 8.99264 9.87736 8.94413 9.79175 8.88896C9.70614 8.83189 9.63861 8.76435 9.58914 8.68635C9.54158 8.60836 9.51971 8.51704 9.52351 8.41241C9.52351 8.28685 9.55966 8.17461 9.63195 8.07569C9.70614 7.97676 9.81267 7.89971 9.95155 7.84455C10.0904 7.78747 10.2607 7.75894 10.4623 7.75894C10.7591 7.75894 10.9941 7.82267 11.1672 7.95013Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
4
assets/icons/file_icons/react.svg
Normal file
4
assets/icons/file_icons/react.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7.99752 9.14577C8.62961 9.14577 9.14201 8.63336 9.14201 8.00127C9.14201 7.36919 8.62961 6.85678 7.99752 6.85678C7.36543 6.85678 6.85303 7.36919 6.85303 8.00127C6.85303 8.63336 7.36543 9.14577 7.99752 9.14577Z" fill="black"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.37507 12.5467C5.35849 12.5371 5.26876 12.4764 5.21215 12.1996C5.15576 11.924 5.15423 11.5219 5.24075 11.0018C5.25336 10.926 5.2677 10.8485 5.28376 10.7695C5.61535 10.8289 5.96088 10.8775 6.31735 10.9146C6.52765 11.2048 6.74254 11.4797 6.9598 11.7371C6.8994 11.7906 6.83947 11.8417 6.7801 11.8906C6.37292 12.2255 6.02397 12.4252 5.75706 12.5142C5.48905 12.6036 5.39166 12.5562 5.37507 12.5467ZM4.63463 8.00002C4.48846 7.67278 4.35781 7.34921 4.24347 7.03232C4.16699 7.05793 4.09271 7.08426 4.0207 7.11126C3.52701 7.29639 3.17959 7.49875 2.96906 7.6854C2.75767 7.87282 2.75 7.98085 2.75 8C2.75 8.01916 2.75767 8.12719 2.96906 8.31461C3.17959 8.50126 3.52701 8.70361 4.0207 8.88875C4.09271 8.91575 4.167 8.94208 4.24348 8.96769C4.35782 8.65081 4.48846 8.32725 4.63463 8.00002ZM3.49402 5.70677C3.6016 5.66643 3.71247 5.6276 3.82645 5.59035C3.80173 5.47305 3.77992 5.35765 3.76108 5.24434C3.65732 4.62055 3.63633 4.01923 3.74257 3.49981C3.84858 2.98153 4.10358 2.45543 4.62507 2.15435C5.14656 1.85326 5.72968 1.89548 6.23152 2.06281C6.73448 2.23051 7.24474 2.54935 7.73308 2.95111C7.82179 3.02409 7.91084 3.10068 8.00007 3.18075C8.0893 3.10068 8.17835 3.02409 8.26706 2.9511C8.7554 2.54935 9.26566 2.23051 9.76862 2.06281C10.2705 1.89548 10.8536 1.85326 11.3751 2.15435C11.8966 2.45543 12.1516 2.98153 12.2576 3.49981C12.3638 4.01923 12.3428 4.62055 12.2391 5.24434C12.2202 5.35766 12.1984 5.47308 12.1737 5.59039C12.2876 5.62763 12.3984 5.66644 12.506 5.70677C13.0981 5.9288 13.6293 6.21129 14.026 6.56301C14.4219 6.91396 14.75 7.39783 14.75 8C14.75 8.60218 14.4219 9.08605 14.026 9.437C13.6293 9.78872 13.0981 10.0712 12.506 10.2932C12.3984 10.3336 12.2876 10.3724 12.1737 10.4096C12.1984 10.5269 12.2202 10.6424 12.2391 10.7557C12.3428 11.3795 12.3638 11.9808 12.2576 12.5002C12.1516 13.0185 11.8966 13.5446 11.3751 13.8457C10.8536 14.1468 10.2705 14.1046 9.76862 13.9372C9.26566 13.7695 8.7554 13.4507 8.26706 13.0489C8.17835 12.976 8.08931 12.8994 8.00007 12.8193C7.91084 12.8994 7.82179 12.976 7.73308 13.0489C7.24474 13.4507 6.73448 13.7695 6.23152 13.9372C5.72968 14.1046 5.14657 14.1468 4.62507 13.8457C4.10358 13.5446 3.84859 13.0185 3.74257 12.5002C3.63633 11.9808 3.65732 11.3795 3.76108 10.7557C3.77993 10.6424 3.80174 10.527 3.82646 10.4097C3.71248 10.3724 3.6016 10.3336 3.49402 10.2932C2.90192 10.0712 2.37066 9.78872 1.97395 9.437C1.57812 9.08605 1.25 8.60218 1.25 8C1.25 7.39783 1.57812 6.91396 1.97395 6.56301C2.37066 6.21129 2.90192 5.9288 3.49402 5.70677ZM9.22005 11.8906C9.16067 11.8417 9.10075 11.7906 9.04034 11.7371C9.25761 11.4797 9.4725 11.2048 9.68281 10.9145C10.0393 10.8775 10.3848 10.8289 10.7164 10.7695C10.7324 10.8485 10.7468 10.926 10.7594 11.0018C10.8459 11.5219 10.8444 11.924 10.788 12.1996C10.7314 12.4764 10.6417 12.5371 10.6251 12.5467C10.6085 12.5562 10.5111 12.6036 10.2431 12.5142C9.97617 12.4252 9.62722 12.2255 9.22005 11.8906ZM6.31737 5.08544C6.52766 4.79525 6.74254 4.52034 6.9598 4.26289C6.89939 4.20948 6.83947 4.15832 6.7801 4.10948C6.37292 3.77449 6.02397 3.57479 5.75706 3.4858C5.48905 3.39644 5.39165 3.44381 5.37507 3.45339C5.35849 3.46296 5.26876 3.52362 5.21214 3.80041C5.15576 4.07605 5.15423 4.4781 5.24075 4.99822C5.25336 5.07405 5.2677 5.15152 5.28375 5.23053C5.61535 5.1711 5.96089 5.12247 6.31737 5.08544ZM9.04034 4.26289C9.2576 4.52034 9.47249 4.79526 9.68278 5.08546C10.0393 5.12248 10.3848 5.17112 10.7164 5.23055C10.7324 5.15154 10.7468 5.07406 10.7594 4.99822C10.8459 4.4781 10.8444 4.07605 10.788 3.80041C10.7314 3.52362 10.6417 3.46296 10.6251 3.45339C10.6085 3.44381 10.5111 3.39644 10.2431 3.4858C9.97617 3.57479 9.62722 3.77449 9.22005 4.10947C9.16067 4.15832 9.10074 4.20948 9.04034 4.26289ZM11.3655 8.00002C11.5117 8.32723 11.6423 8.65078 11.7566 8.96765C11.8331 8.94205 11.9073 8.91574 11.9793 8.88875C12.473 8.70361 12.8204 8.50126 13.0309 8.31461C13.2423 8.12719 13.25 8.01916 13.25 8C13.25 7.98085 13.2423 7.87282 13.0309 7.6854C12.8204 7.49875 12.473 7.29639 11.9793 7.11126C11.9073 7.08427 11.8331 7.05796 11.7567 7.03237C11.6423 7.34924 11.5117 7.6728 11.3655 8.00002ZM7.99752 10.1458C9.18189 10.1458 10.142 9.18565 10.142 8.00127C10.142 6.8169 9.18189 5.85678 7.99752 5.85678C6.81315 5.85678 5.85303 6.8169 5.85303 8.00127C5.85303 9.18564 6.81315 10.1458 7.99752 10.1458Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.5 KiB |
@@ -549,8 +549,8 @@
|
||||
"alt-ctrl-shift-c": "project_panel::CopyRelativePath",
|
||||
"f2": "project_panel::Rename",
|
||||
"enter": "project_panel::Rename",
|
||||
"backspace": "project_panel::Delete",
|
||||
"delete": "project_panel::Delete",
|
||||
"backspace": "project_panel::Trash",
|
||||
"delete": "project_panel::Trash",
|
||||
"ctrl-backspace": ["project_panel::Delete", { "skip_prompt": true }],
|
||||
"ctrl-delete": ["project_panel::Delete", { "skip_prompt": true }],
|
||||
"alt-ctrl-r": "project_panel::RevealInFinder",
|
||||
|
||||
@@ -576,8 +576,8 @@
|
||||
"alt-cmd-shift-c": "project_panel::CopyRelativePath",
|
||||
"f2": "project_panel::Rename",
|
||||
"enter": "project_panel::Rename",
|
||||
"backspace": "project_panel::Delete",
|
||||
"delete": "project_panel::Delete",
|
||||
"backspace": "project_panel::Trash",
|
||||
"delete": "project_panel::Trash",
|
||||
"cmd-backspace": ["project_panel::Delete", { "skip_prompt": true }],
|
||||
"cmd-delete": ["project_panel::Delete", { "skip_prompt": true }],
|
||||
"alt-cmd-r": "project_panel::RevealInFinder",
|
||||
|
||||
@@ -435,6 +435,12 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_operator == ys",
|
||||
"bindings": {
|
||||
"s": "vim::CurrentLine"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && VimObject",
|
||||
"bindings": {
|
||||
@@ -625,7 +631,10 @@
|
||||
"t": "project_panel::OpenPermanent",
|
||||
"v": "project_panel::OpenPermanent",
|
||||
"p": "project_panel::Open",
|
||||
"x": "project_panel::RevealInFinder"
|
||||
"x": "project_panel::RevealInFinder",
|
||||
"shift-g": "menu::SelectLast",
|
||||
"g g": "menu::SelectFirst",
|
||||
"-": "project_panel::SelectParent"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -155,6 +155,8 @@
|
||||
// 4. Never show the scrollbar:
|
||||
// "never"
|
||||
"show": "auto",
|
||||
// Whether to show cursor positions in the scrollbar.
|
||||
"cursors": true,
|
||||
// Whether to show git diff indicators in the scrollbar.
|
||||
"git_diff": true,
|
||||
// Whether to show buffer search results in the scrollbar.
|
||||
@@ -329,8 +331,10 @@
|
||||
// when you switch to another file unless you explicitly pin them.
|
||||
// This is useful for quickly viewing files without cluttering your workspace.
|
||||
"enabled": true,
|
||||
// Whether to open files in preview mode when selected from the file finder.
|
||||
"enable_preview_from_file_finder": false
|
||||
// Whether to open tabs in preview mode when selected from the file finder.
|
||||
"enable_preview_from_file_finder": false,
|
||||
// Whether a preview tab gets replaced when code navigation is used to navigate away from the tab.
|
||||
"enable_preview_from_code_navigation": false
|
||||
},
|
||||
// Whether or not to remove any trailing whitespace from lines of a buffer
|
||||
// before saving it.
|
||||
|
||||
@@ -99,6 +99,7 @@ impl ActivityIndicator {
|
||||
Box::new(
|
||||
cx.new_view(|cx| Editor::for_buffer(buffer, Some(project.clone()), cx)),
|
||||
),
|
||||
None,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2873,7 +2873,7 @@ impl InlineAssistant {
|
||||
cx.theme().colors().text
|
||||
},
|
||||
font_family: settings.ui_font.family.clone(),
|
||||
font_features: settings.ui_font.features,
|
||||
font_features: settings.ui_font.features.clone(),
|
||||
font_size: rems(0.875).into(),
|
||||
font_weight: FontWeight::NORMAL,
|
||||
font_style: FontStyle::Normal,
|
||||
|
||||
@@ -241,7 +241,7 @@ impl AuthenticationPrompt {
|
||||
let text_style = TextStyle {
|
||||
color: cx.theme().colors().text,
|
||||
font_family: settings.ui_font.family.clone(),
|
||||
font_features: settings.ui_font.features,
|
||||
font_features: settings.ui_font.features.clone(),
|
||||
font_size: rems(0.875).into(),
|
||||
font_weight: FontWeight::NORMAL,
|
||||
font_style: FontStyle::Normal,
|
||||
|
||||
@@ -8,26 +8,24 @@ license = "GPL-3.0-or-later"
|
||||
[lib]
|
||||
path = "src/assistant2.rs"
|
||||
|
||||
[[example]]
|
||||
name = "assistant_example"
|
||||
path = "examples/assistant_example.rs"
|
||||
crate-type = ["bin"]
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
assistant_tooling.workspace = true
|
||||
client.workspace = true
|
||||
collections.workspace = true
|
||||
editor.workspace = true
|
||||
feature_flags.workspace = true
|
||||
fs.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
language.workspace = true
|
||||
log.workspace = true
|
||||
nanoid.workspace = true
|
||||
open_ai.workspace = true
|
||||
project.workspace = true
|
||||
rich_text.workspace = true
|
||||
semantic_index.workspace = true
|
||||
schemars.workspace = true
|
||||
semantic_index.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
@@ -35,7 +33,6 @@ theme.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
workspace.workspace = true
|
||||
nanoid = "0.4"
|
||||
|
||||
[dev-dependencies]
|
||||
assets.workspace = true
|
||||
|
||||
@@ -1,129 +0,0 @@
|
||||
use anyhow::Context as _;
|
||||
use assets::Assets;
|
||||
use assistant2::{tools::ProjectIndexTool, AssistantPanel};
|
||||
use assistant_tooling::ToolRegistry;
|
||||
use client::Client;
|
||||
use gpui::{actions, App, AppContext, KeyBinding, Task, View, WindowOptions};
|
||||
use language::LanguageRegistry;
|
||||
use project::Project;
|
||||
use semantic_index::{OpenAiEmbeddingModel, OpenAiEmbeddingProvider, SemanticIndex};
|
||||
use settings::{KeymapFile, DEFAULT_KEYMAP_PATH};
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
use theme::LoadThemes;
|
||||
use ui::{div, prelude::*, Render};
|
||||
use util::{http::HttpClientWithUrl, ResultExt as _};
|
||||
|
||||
actions!(example, [Quit]);
|
||||
|
||||
fn main() {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
|
||||
env_logger::init();
|
||||
App::new().with_assets(Assets).run(|cx| {
|
||||
cx.bind_keys(Some(KeyBinding::new("cmd-q", Quit, None)));
|
||||
cx.on_action(|_: &Quit, cx: &mut AppContext| {
|
||||
cx.quit();
|
||||
});
|
||||
|
||||
if args.len() < 2 {
|
||||
eprintln!(
|
||||
"Usage: cargo run --example assistant_example -p assistant2 -- <project_path>"
|
||||
);
|
||||
cx.quit();
|
||||
return;
|
||||
}
|
||||
|
||||
settings::init(cx);
|
||||
language::init(cx);
|
||||
Project::init_settings(cx);
|
||||
editor::init(cx);
|
||||
theme::init(LoadThemes::JustBase, cx);
|
||||
Assets.load_fonts(cx).unwrap();
|
||||
KeymapFile::load_asset(DEFAULT_KEYMAP_PATH, cx).unwrap();
|
||||
client::init_settings(cx);
|
||||
release_channel::init("0.130.0", cx);
|
||||
|
||||
let client = Client::production(cx);
|
||||
{
|
||||
let client = client.clone();
|
||||
cx.spawn(|cx| async move { client.authenticate_and_connect(false, &cx).await })
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
assistant2::init(client.clone(), cx);
|
||||
|
||||
let language_registry = Arc::new(LanguageRegistry::new(
|
||||
Task::ready(()),
|
||||
cx.background_executor().clone(),
|
||||
));
|
||||
let node_runtime = node_runtime::RealNodeRuntime::new(client.http_client());
|
||||
languages::init(language_registry.clone(), node_runtime, cx);
|
||||
|
||||
let http = Arc::new(HttpClientWithUrl::new("http://localhost:11434"));
|
||||
|
||||
let api_key = std::env::var("OPENAI_API_KEY").expect("OPENAI_API_KEY not set");
|
||||
let embedding_provider = OpenAiEmbeddingProvider::new(
|
||||
http.clone(),
|
||||
OpenAiEmbeddingModel::TextEmbedding3Small,
|
||||
open_ai::OPEN_AI_API_URL.to_string(),
|
||||
api_key,
|
||||
);
|
||||
|
||||
cx.spawn(|mut cx| async move {
|
||||
let mut semantic_index = SemanticIndex::new(
|
||||
PathBuf::from("/tmp/semantic-index-db.mdb"),
|
||||
Arc::new(embedding_provider),
|
||||
&mut cx,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let project_path = Path::new(&args[1]);
|
||||
let project = Project::example([project_path], &mut cx).await;
|
||||
|
||||
cx.update(|cx| {
|
||||
let fs = project.read(cx).fs().clone();
|
||||
|
||||
let project_index = semantic_index.project_index(project.clone(), cx);
|
||||
|
||||
let mut tool_registry = ToolRegistry::new();
|
||||
tool_registry
|
||||
.register(ProjectIndexTool::new(project_index.clone(), fs.clone()))
|
||||
.context("failed to register ProjectIndexTool")
|
||||
.log_err();
|
||||
|
||||
let tool_registry = Arc::new(tool_registry);
|
||||
|
||||
cx.open_window(WindowOptions::default(), |cx| {
|
||||
cx.new_view(|cx| Example::new(language_registry, tool_registry, cx))
|
||||
});
|
||||
cx.activate(true);
|
||||
})
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
})
|
||||
}
|
||||
|
||||
struct Example {
|
||||
assistant_panel: View<AssistantPanel>,
|
||||
}
|
||||
|
||||
impl Example {
|
||||
fn new(
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
tool_registry: Arc<ToolRegistry>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
Self {
|
||||
assistant_panel: cx
|
||||
.new_view(|cx| AssistantPanel::new(language_registry, tool_registry, cx)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for Example {
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl ui::prelude::IntoElement {
|
||||
div().size_full().child(self.assistant_panel.clone())
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,20 @@
|
||||
/// This example creates a basic Chat UI with a function for rolling a die.
|
||||
//! This example creates a basic Chat UI with a function for rolling a die.
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use assets::Assets;
|
||||
use assistant2::AssistantPanel;
|
||||
use assistant_tooling::{LanguageModelTool, ToolRegistry};
|
||||
use client::Client;
|
||||
use gpui::{actions, AnyElement, App, AppContext, KeyBinding, Task, View, WindowOptions};
|
||||
use client::{Client, UserStore};
|
||||
use fs::Fs;
|
||||
use futures::StreamExt as _;
|
||||
use gpui::{actions, AnyElement, App, AppContext, KeyBinding, Model, Task, View, WindowOptions};
|
||||
use language::LanguageRegistry;
|
||||
use project::Project;
|
||||
use rand::Rng;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{KeymapFile, DEFAULT_KEYMAP_PATH};
|
||||
use std::sync::Arc;
|
||||
use std::{path::PathBuf, sync::Arc};
|
||||
use theme::LoadThemes;
|
||||
use ui::{div, prelude::*, Render};
|
||||
use util::ResultExt as _;
|
||||
@@ -134,7 +137,7 @@ impl LanguageModelTool for RollDiceTool {
|
||||
return Task::ready(Ok(DiceRoll { rolls }));
|
||||
}
|
||||
|
||||
fn new_view(
|
||||
fn output_view(
|
||||
_tool_call_id: String,
|
||||
_input: Self::Input,
|
||||
result: Result<Self::Output>,
|
||||
@@ -158,6 +161,121 @@ impl LanguageModelTool for RollDiceTool {
|
||||
}
|
||||
}
|
||||
|
||||
struct FileBrowserTool {
|
||||
fs: Arc<dyn Fs>,
|
||||
root_dir: PathBuf,
|
||||
}
|
||||
|
||||
impl FileBrowserTool {
|
||||
fn new(fs: Arc<dyn Fs>, root_dir: PathBuf) -> Self {
|
||||
Self { fs, root_dir }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
struct FileBrowserParams {
|
||||
command: FileBrowserCommand,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
enum FileBrowserCommand {
|
||||
Ls { path: PathBuf },
|
||||
Cat { path: PathBuf },
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
enum FileBrowserOutput {
|
||||
Ls { entries: Vec<String> },
|
||||
Cat { content: String },
|
||||
}
|
||||
|
||||
pub struct FileBrowserView {
|
||||
result: Result<FileBrowserOutput>,
|
||||
}
|
||||
|
||||
impl Render for FileBrowserView {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let Ok(output) = self.result.as_ref() else {
|
||||
return h_flex().child("Failed to perform operation");
|
||||
};
|
||||
|
||||
match output {
|
||||
FileBrowserOutput::Ls { entries } => v_flex().children(
|
||||
entries
|
||||
.into_iter()
|
||||
.map(|entry| h_flex().text_ui(cx).child(entry.clone())),
|
||||
),
|
||||
FileBrowserOutput::Cat { content } => h_flex().child(content.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LanguageModelTool for FileBrowserTool {
|
||||
type Input = FileBrowserParams;
|
||||
type Output = FileBrowserOutput;
|
||||
type View = FileBrowserView;
|
||||
|
||||
fn name(&self) -> String {
|
||||
"file_browser".to_string()
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"A tool for browsing the filesystem.".to_string()
|
||||
}
|
||||
|
||||
fn execute(&self, input: &Self::Input, cx: &AppContext) -> Task<gpui::Result<Self::Output>> {
|
||||
cx.spawn({
|
||||
let fs = self.fs.clone();
|
||||
let root_dir = self.root_dir.clone();
|
||||
let input = input.clone();
|
||||
|_cx| async move {
|
||||
match input.command {
|
||||
FileBrowserCommand::Ls { path } => {
|
||||
let path = root_dir.join(path);
|
||||
|
||||
let mut output = fs.read_dir(&path).await?;
|
||||
|
||||
let mut entries = Vec::new();
|
||||
while let Some(entry) = output.next().await {
|
||||
let entry = entry?;
|
||||
entries.push(entry.display().to_string());
|
||||
}
|
||||
|
||||
Ok(FileBrowserOutput::Ls { entries })
|
||||
}
|
||||
FileBrowserCommand::Cat { path } => {
|
||||
let path = root_dir.join(path);
|
||||
|
||||
let output = fs.load(&path).await?;
|
||||
|
||||
Ok(FileBrowserOutput::Cat { content: output })
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn output_view(
|
||||
_tool_call_id: String,
|
||||
_input: Self::Input,
|
||||
result: Result<Self::Output>,
|
||||
cx: &mut WindowContext,
|
||||
) -> gpui::View<Self::View> {
|
||||
cx.new_view(|_cx| FileBrowserView { result })
|
||||
}
|
||||
|
||||
fn format(_input: &Self::Input, output: &Result<Self::Output>) -> String {
|
||||
let Ok(output) = output else {
|
||||
return "Failed to perform command: {input:?}".to_string();
|
||||
};
|
||||
|
||||
match output {
|
||||
FileBrowserOutput::Ls { entries } => entries.join("\n"),
|
||||
FileBrowserOutput::Cat { content } => content.to_owned(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
env_logger::init();
|
||||
App::new().with_assets(Assets).run(|cx| {
|
||||
@@ -188,26 +306,36 @@ fn main() {
|
||||
Task::ready(()),
|
||||
cx.background_executor().clone(),
|
||||
));
|
||||
|
||||
let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx));
|
||||
let node_runtime = node_runtime::RealNodeRuntime::new(client.http_client());
|
||||
languages::init(language_registry.clone(), node_runtime, cx);
|
||||
|
||||
cx.spawn(|cx| async move {
|
||||
cx.update(|cx| {
|
||||
let mut tool_registry = ToolRegistry::new();
|
||||
tool_registry
|
||||
.register(RollDiceTool::new())
|
||||
.context("failed to register DummyTool")
|
||||
.log_err();
|
||||
|
||||
let tool_registry = Arc::new(tool_registry);
|
||||
|
||||
println!("Tools registered");
|
||||
for definition in tool_registry.definitions() {
|
||||
println!("{}", definition);
|
||||
}
|
||||
let fs = Arc::new(fs::RealFs::new(None));
|
||||
let cwd = std::env::current_dir().expect("Failed to get current working directory");
|
||||
|
||||
cx.open_window(WindowOptions::default(), |cx| {
|
||||
cx.new_view(|cx| Example::new(language_registry, tool_registry, cx))
|
||||
let mut tool_registry = ToolRegistry::new();
|
||||
tool_registry
|
||||
.register(RollDiceTool::new(), cx)
|
||||
.context("failed to register DummyTool")
|
||||
.log_err();
|
||||
|
||||
tool_registry
|
||||
.register(FileBrowserTool::new(fs, cwd), cx)
|
||||
.context("failed to register FileBrowserTool")
|
||||
.log_err();
|
||||
|
||||
let tool_registry = Arc::new(tool_registry);
|
||||
|
||||
println!("Tools registered");
|
||||
for definition in tool_registry.definitions() {
|
||||
println!("{}", definition);
|
||||
}
|
||||
|
||||
cx.new_view(|cx| Example::new(language_registry, tool_registry, user_store, cx))
|
||||
});
|
||||
cx.activate(true);
|
||||
})
|
||||
@@ -224,11 +352,13 @@ impl Example {
|
||||
fn new(
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
tool_registry: Arc<ToolRegistry>,
|
||||
user_store: Model<UserStore>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
Self {
|
||||
assistant_panel: cx
|
||||
.new_view(|cx| AssistantPanel::new(language_registry, tool_registry, cx)),
|
||||
assistant_panel: cx.new_view(|cx| {
|
||||
AssistantPanel::new(language_registry, tool_registry, user_store, cx)
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,29 +1,30 @@
|
||||
mod assistant_settings;
|
||||
mod completion_provider;
|
||||
pub mod tools;
|
||||
mod ui;
|
||||
|
||||
use ::ui::{div, prelude::*, Color, ViewContext};
|
||||
use anyhow::{Context, Result};
|
||||
use assistant_tooling::{ToolFunctionCall, ToolRegistry};
|
||||
use client::{proto, Client};
|
||||
use client::{proto, Client, UserStore};
|
||||
use collections::HashMap;
|
||||
use completion_provider::*;
|
||||
use editor::Editor;
|
||||
use feature_flags::FeatureFlagAppExt as _;
|
||||
use futures::{channel::oneshot, future::join_all, Future, FutureExt, StreamExt};
|
||||
use futures::{future::join_all, StreamExt};
|
||||
use gpui::{
|
||||
list, prelude::*, AnyElement, AppContext, AsyncWindowContext, EventEmitter, FocusHandle,
|
||||
FocusableView, Global, ListAlignment, ListState, Model, Render, Task, View, WeakView,
|
||||
list, AnyElement, AppContext, AsyncWindowContext, EventEmitter, FocusHandle, FocusableView,
|
||||
ListAlignment, ListState, Model, Render, Task, View, WeakView,
|
||||
};
|
||||
use language::{language_settings::SoftWrap, LanguageRegistry};
|
||||
use open_ai::{FunctionContent, ToolCall, ToolCallContent};
|
||||
use project::Fs;
|
||||
use rich_text::RichText;
|
||||
use semantic_index::{CloudEmbeddingProvider, ProjectIndex, SemanticIndex};
|
||||
use semantic_index::{CloudEmbeddingProvider, SemanticIndex};
|
||||
use serde::Deserialize;
|
||||
use settings::Settings;
|
||||
use std::{cmp, sync::Arc};
|
||||
use theme::ThemeSettings;
|
||||
use std::sync::Arc;
|
||||
use tools::ProjectIndexTool;
|
||||
use ui::{popover_menu, prelude::*, ButtonLike, CollapsibleContainer, Color, ContextMenu, Tooltip};
|
||||
use ui::Composer;
|
||||
use util::{paths::EMBEDDINGS_DIR, ResultExt};
|
||||
use workspace::{
|
||||
dock::{DockPosition, Panel, PanelEvent},
|
||||
@@ -32,6 +33,8 @@ use workspace::{
|
||||
|
||||
pub use assistant_settings::AssistantSettings;
|
||||
|
||||
use crate::ui::UserOrAssistant;
|
||||
|
||||
const MAX_COMPLETION_CALLS_PER_SUBMISSION: usize = 5;
|
||||
|
||||
#[derive(Eq, PartialEq, Copy, Clone, Deserialize)]
|
||||
@@ -102,6 +105,8 @@ impl AssistantPanel {
|
||||
(workspace.app_state().clone(), workspace.project().clone())
|
||||
})?;
|
||||
|
||||
let user_store = app_state.user_store.clone();
|
||||
|
||||
cx.new_view(|cx| {
|
||||
// todo!("this will panic if the semantic index failed to load or has not loaded yet")
|
||||
let project_index = cx.update_global(|semantic_index: &mut SemanticIndex, cx| {
|
||||
@@ -110,16 +115,16 @@ impl AssistantPanel {
|
||||
|
||||
let mut tool_registry = ToolRegistry::new();
|
||||
tool_registry
|
||||
.register(ProjectIndexTool::new(
|
||||
project_index.clone(),
|
||||
app_state.fs.clone(),
|
||||
))
|
||||
.register(
|
||||
ProjectIndexTool::new(project_index.clone(), app_state.fs.clone()),
|
||||
cx,
|
||||
)
|
||||
.context("failed to register ProjectIndexTool")
|
||||
.log_err();
|
||||
|
||||
let tool_registry = Arc::new(tool_registry);
|
||||
|
||||
Self::new(app_state.languages.clone(), tool_registry, cx)
|
||||
Self::new(app_state.languages.clone(), tool_registry, user_store, cx)
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -127,10 +132,16 @@ impl AssistantPanel {
|
||||
pub fn new(
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
tool_registry: Arc<ToolRegistry>,
|
||||
user_store: Model<UserStore>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let chat = cx.new_view(|cx| {
|
||||
AssistantChat::new(language_registry.clone(), tool_registry.clone(), cx)
|
||||
AssistantChat::new(
|
||||
language_registry.clone(),
|
||||
tool_registry.clone(),
|
||||
user_store,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
Self { width: None, chat }
|
||||
@@ -175,7 +186,7 @@ impl Panel for AssistantPanel {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn icon(&self, _cx: &WindowContext) -> Option<ui::IconName> {
|
||||
fn icon(&self, _cx: &WindowContext) -> Option<::ui::IconName> {
|
||||
Some(IconName::Ai)
|
||||
}
|
||||
|
||||
@@ -192,13 +203,7 @@ impl EventEmitter<PanelEvent> for AssistantPanel {}
|
||||
|
||||
impl FocusableView for AssistantPanel {
|
||||
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
|
||||
self.chat
|
||||
.read(cx)
|
||||
.messages
|
||||
.iter()
|
||||
.rev()
|
||||
.find_map(|msg| msg.focus_handle(cx))
|
||||
.expect("no user message in chat")
|
||||
self.chat.read(cx).composer_editor.read(cx).focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,7 +212,10 @@ struct AssistantChat {
|
||||
messages: Vec<ChatMessage>,
|
||||
list_state: ListState,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
composer_editor: View<Editor>,
|
||||
user_store: Model<UserStore>,
|
||||
next_message_id: MessageId,
|
||||
collapsed_messages: HashMap<MessageId, bool>,
|
||||
pending_completion: Option<Task<()>>,
|
||||
tool_registry: Arc<ToolRegistry>,
|
||||
}
|
||||
@@ -216,6 +224,7 @@ impl AssistantChat {
|
||||
fn new(
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
tool_registry: Arc<ToolRegistry>,
|
||||
user_store: Model<UserStore>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let model = CompletionProvider::get(cx).default_model();
|
||||
@@ -230,17 +239,23 @@ impl AssistantChat {
|
||||
},
|
||||
);
|
||||
|
||||
let mut this = Self {
|
||||
Self {
|
||||
model,
|
||||
messages: Vec::new(),
|
||||
composer_editor: cx.new_view(|cx| {
|
||||
let mut editor = Editor::auto_height(80, cx);
|
||||
editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
|
||||
editor.set_placeholder_text("Type a message to the assistant", cx);
|
||||
editor
|
||||
}),
|
||||
list_state,
|
||||
user_store,
|
||||
language_registry,
|
||||
next_message_id: MessageId(0),
|
||||
collapsed_messages: HashMap::default(),
|
||||
pending_completion: None,
|
||||
tool_registry,
|
||||
};
|
||||
this.push_new_user_message(true, cx);
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
fn focused_message_id(&self, cx: &WindowContext) -> Option<MessageId> {
|
||||
@@ -263,19 +278,37 @@ impl AssistantChat {
|
||||
if let Some(ChatMessage::Assistant(message)) = self.messages.last() {
|
||||
if message.body.text.is_empty() {
|
||||
self.pop_message(cx);
|
||||
} else {
|
||||
self.push_new_user_message(false, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn submit(&mut self, Submit(mode): &Submit, cx: &mut ViewContext<Self>) {
|
||||
let Some(focused_message_id) = self.focused_message_id(cx) else {
|
||||
// Don't allow multiple concurrent completions.
|
||||
if self.pending_completion.is_some() {
|
||||
cx.propagate();
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(focused_message_id) = self.focused_message_id(cx) {
|
||||
self.truncate_messages(focused_message_id, cx);
|
||||
} else if self.composer_editor.focus_handle(cx).is_focused(cx) {
|
||||
let message = self.composer_editor.update(cx, |composer_editor, cx| {
|
||||
let text = composer_editor.text(cx);
|
||||
let id = self.next_message_id.post_inc();
|
||||
let body = cx.new_view(|cx| {
|
||||
let mut editor = Editor::auto_height(80, cx);
|
||||
editor.set_text(text, cx);
|
||||
editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
|
||||
editor
|
||||
});
|
||||
composer_editor.clear(cx);
|
||||
ChatMessage::User(UserMessage { id, body })
|
||||
});
|
||||
self.push_message(message, cx);
|
||||
} else {
|
||||
log::error!("unexpected state: no user message editor is focused.");
|
||||
return;
|
||||
};
|
||||
|
||||
self.truncate_messages(focused_message_id, cx);
|
||||
}
|
||||
|
||||
let mode = *mode;
|
||||
self.pending_completion = Some(cx.spawn(move |this, mut cx| async move {
|
||||
@@ -289,12 +322,8 @@ impl AssistantChat {
|
||||
.log_err();
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
let focus = this
|
||||
.user_message(focused_message_id)
|
||||
.body
|
||||
.focus_handle(cx)
|
||||
.contains_focused(cx);
|
||||
this.push_new_user_message(focus, cx);
|
||||
let composer_focus_handle = this.composer_editor.focus_handle(cx);
|
||||
cx.focus(&composer_focus_handle);
|
||||
this.pending_completion = None;
|
||||
})
|
||||
.context("Failed to push new user message")
|
||||
@@ -302,6 +331,10 @@ impl AssistantChat {
|
||||
}));
|
||||
}
|
||||
|
||||
fn can_submit(&self) -> bool {
|
||||
self.pending_completion.is_none()
|
||||
}
|
||||
|
||||
async fn request_completion(
|
||||
this: WeakView<Self>,
|
||||
mode: SubmitMode,
|
||||
@@ -425,36 +458,6 @@ impl AssistantChat {
|
||||
}
|
||||
}
|
||||
|
||||
fn user_message(&mut self, message_id: MessageId) -> &mut UserMessage {
|
||||
self.messages
|
||||
.iter_mut()
|
||||
.find_map(|message| match message {
|
||||
ChatMessage::User(user_message) if user_message.id == message_id => {
|
||||
Some(user_message)
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.expect("User message not found")
|
||||
}
|
||||
|
||||
fn push_new_user_message(&mut self, focus: bool, cx: &mut ViewContext<Self>) {
|
||||
let id = self.next_message_id.post_inc();
|
||||
let body = cx.new_view(|cx| {
|
||||
let mut editor = Editor::auto_height(80, cx);
|
||||
editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
|
||||
if focus {
|
||||
cx.focus_self();
|
||||
}
|
||||
editor
|
||||
});
|
||||
let message = ChatMessage::User(UserMessage {
|
||||
id,
|
||||
body,
|
||||
contexts: Vec::new(),
|
||||
});
|
||||
self.push_message(message, cx);
|
||||
}
|
||||
|
||||
fn push_new_assistant_message(&mut self, cx: &mut ViewContext<Self>) {
|
||||
let message = ChatMessage::Assistant(AssistantMessage {
|
||||
id: self.next_message_id.post_inc(),
|
||||
@@ -496,6 +499,15 @@ impl AssistantChat {
|
||||
}
|
||||
}
|
||||
|
||||
fn is_message_collapsed(&self, id: &MessageId) -> bool {
|
||||
self.collapsed_messages.get(id).copied().unwrap_or_default()
|
||||
}
|
||||
|
||||
fn toggle_message_collapsed(&mut self, id: MessageId) {
|
||||
let entry = self.collapsed_messages.entry(id).or_insert(false);
|
||||
*entry = !*entry;
|
||||
}
|
||||
|
||||
fn render_error(
|
||||
&self,
|
||||
error: Option<SharedString>,
|
||||
@@ -525,22 +537,20 @@ impl AssistantChat {
|
||||
let is_last = ix == self.messages.len() - 1;
|
||||
|
||||
match &self.messages[ix] {
|
||||
ChatMessage::User(UserMessage {
|
||||
body,
|
||||
contexts: _contexts,
|
||||
..
|
||||
}) => div()
|
||||
ChatMessage::User(UserMessage { id, body }) => div()
|
||||
.when(!is_last, |element| element.mb_2())
|
||||
.child(div().p_2().child(Label::new("You").color(Color::Default)))
|
||||
.child(
|
||||
div()
|
||||
.on_action(cx.listener(Self::submit))
|
||||
.p_2()
|
||||
.text_color(cx.theme().colors().editor_foreground)
|
||||
.font(ThemeSettings::get_global(cx).buffer_font.clone())
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.child(body.clone()), // .children(contexts.iter().map(|context| context.render(cx))),
|
||||
)
|
||||
.child(crate::ui::ChatMessage::new(
|
||||
*id,
|
||||
UserOrAssistant::User(self.user_store.read(cx).current_user()),
|
||||
body.clone().into_any_element(),
|
||||
self.is_message_collapsed(id),
|
||||
Box::new(cx.listener({
|
||||
let id = *id;
|
||||
move |assistant_chat, _event, _cx| {
|
||||
assistant_chat.toggle_message_collapsed(id)
|
||||
}
|
||||
})),
|
||||
))
|
||||
.into_any(),
|
||||
ChatMessage::Assistant(AssistantMessage {
|
||||
id,
|
||||
@@ -557,12 +567,19 @@ impl AssistantChat {
|
||||
|
||||
div()
|
||||
.when(!is_last, |element| element.mb_2())
|
||||
.child(
|
||||
div()
|
||||
.p_2()
|
||||
.child(Label::new("Assistant").color(Color::Modified)),
|
||||
)
|
||||
.child(assistant_body)
|
||||
.child(crate::ui::ChatMessage::new(
|
||||
*id,
|
||||
UserOrAssistant::Assistant,
|
||||
assistant_body.into_any_element(),
|
||||
self.is_message_collapsed(id),
|
||||
Box::new(cx.listener({
|
||||
let id = *id;
|
||||
move |assistant_chat, _event, _cx| {
|
||||
assistant_chat.toggle_message_collapsed(id)
|
||||
}
|
||||
})),
|
||||
))
|
||||
// TODO: Should the errors and tool calls get passed into `ChatMessage`?
|
||||
.child(self.render_error(error.clone(), ix, cx))
|
||||
.children(tool_calls.iter().map(|tool_call| {
|
||||
let result = &tool_call.result;
|
||||
@@ -588,11 +605,11 @@ impl AssistantChat {
|
||||
|
||||
for message in &self.messages {
|
||||
match message {
|
||||
ChatMessage::User(UserMessage { body, contexts, .. }) => {
|
||||
// setup context for model
|
||||
contexts.iter().for_each(|context| {
|
||||
completion_messages.extend(context.completion_messages(cx))
|
||||
});
|
||||
ChatMessage::User(UserMessage { body, .. }) => {
|
||||
// When we re-introduce contexts like active file, we'll inject them here instead of relying on the model to request them
|
||||
// contexts.iter().for_each(|context| {
|
||||
// completion_messages.extend(context.completion_messages(cx))
|
||||
// });
|
||||
|
||||
// Show user's message last so that the assistant is grounded in the user's request
|
||||
completion_messages.push(CompletionMessage::User {
|
||||
@@ -646,59 +663,6 @@ impl AssistantChat {
|
||||
|
||||
completion_messages
|
||||
}
|
||||
|
||||
fn render_model_dropdown(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let this = cx.view().downgrade();
|
||||
div().h_flex().justify_end().child(
|
||||
div().w_32().child(
|
||||
popover_menu("user-menu")
|
||||
.menu(move |cx| {
|
||||
ContextMenu::build(cx, |mut menu, cx| {
|
||||
for model in CompletionProvider::get(cx).available_models() {
|
||||
menu = menu.custom_entry(
|
||||
{
|
||||
let model = model.clone();
|
||||
move |_| Label::new(model.clone()).into_any_element()
|
||||
},
|
||||
{
|
||||
let this = this.clone();
|
||||
move |cx| {
|
||||
_ = this.update(cx, |this, cx| {
|
||||
this.model = model.clone();
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
menu
|
||||
})
|
||||
.into()
|
||||
})
|
||||
.trigger(
|
||||
ButtonLike::new("active-model")
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
div()
|
||||
.overflow_x_hidden()
|
||||
.flex_grow()
|
||||
.whitespace_nowrap()
|
||||
.child(Label::new(self.model.clone())),
|
||||
)
|
||||
.child(div().child(
|
||||
Icon::new(IconName::ChevronDown).color(Color::Muted),
|
||||
)),
|
||||
)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.tooltip(move |cx| Tooltip::text("Change Model", cx)),
|
||||
)
|
||||
.anchor(gpui::AnchorCorner::TopRight),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for AssistantChat {
|
||||
@@ -708,14 +672,22 @@ impl Render for AssistantChat {
|
||||
.flex_1()
|
||||
.v_flex()
|
||||
.key_context("AssistantChat")
|
||||
.on_action(cx.listener(Self::submit))
|
||||
.on_action(cx.listener(Self::cancel))
|
||||
.text_color(Color::Default.color(cx))
|
||||
.child(self.render_model_dropdown(cx))
|
||||
.child(list(self.list_state.clone()).flex_1())
|
||||
.child(Composer::new(
|
||||
cx.view().downgrade(),
|
||||
self.model.clone(),
|
||||
self.composer_editor.clone(),
|
||||
self.user_store.read(cx).current_user(),
|
||||
self.can_submit(),
|
||||
self.tool_registry.clone(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Eq, PartialEq)]
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
|
||||
struct MessageId(usize);
|
||||
|
||||
impl MessageId {
|
||||
@@ -743,7 +715,6 @@ impl ChatMessage {
|
||||
struct UserMessage {
|
||||
id: MessageId,
|
||||
body: View<Editor>,
|
||||
contexts: Vec<AssistantContext>,
|
||||
}
|
||||
|
||||
struct AssistantMessage {
|
||||
@@ -752,211 +723,3 @@ struct AssistantMessage {
|
||||
tool_calls: Vec<ToolFunctionCall>,
|
||||
error: Option<SharedString>,
|
||||
}
|
||||
|
||||
// Since we're swapping out for direct query usage, we might not need to use this injected context
|
||||
// It will be useful though for when the user _definitely_ wants the model to see a specific file,
|
||||
// query, error, etc.
|
||||
#[allow(dead_code)]
|
||||
enum AssistantContext {
|
||||
Codebase(View<CodebaseContext>),
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
struct CodebaseExcerpt {
|
||||
element_id: ElementId,
|
||||
path: SharedString,
|
||||
text: SharedString,
|
||||
score: f32,
|
||||
expanded: bool,
|
||||
}
|
||||
|
||||
impl AssistantContext {
|
||||
#[allow(dead_code)]
|
||||
fn render(&self, _cx: &mut ViewContext<AssistantChat>) -> AnyElement {
|
||||
match self {
|
||||
AssistantContext::Codebase(context) => context.clone().into_any_element(),
|
||||
}
|
||||
}
|
||||
|
||||
fn completion_messages(&self, cx: &WindowContext) -> Vec<CompletionMessage> {
|
||||
match self {
|
||||
AssistantContext::Codebase(context) => context.read(cx).completion_messages(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum CodebaseContext {
|
||||
Pending { _task: Task<()> },
|
||||
Done(Result<Vec<CodebaseExcerpt>>),
|
||||
}
|
||||
|
||||
impl CodebaseContext {
|
||||
fn toggle_expanded(&mut self, element_id: ElementId, cx: &mut ViewContext<Self>) {
|
||||
if let CodebaseContext::Done(Ok(excerpts)) = self {
|
||||
if let Some(excerpt) = excerpts
|
||||
.iter_mut()
|
||||
.find(|excerpt| excerpt.element_id == element_id)
|
||||
{
|
||||
excerpt.expanded = !excerpt.expanded;
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for CodebaseContext {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
match self {
|
||||
CodebaseContext::Pending { .. } => div()
|
||||
.h_flex()
|
||||
.items_center()
|
||||
.gap_1()
|
||||
.child(Icon::new(IconName::Ai).color(Color::Muted).into_element())
|
||||
.child("Searching codebase..."),
|
||||
CodebaseContext::Done(Ok(excerpts)) => {
|
||||
div()
|
||||
.v_flex()
|
||||
.gap_2()
|
||||
.children(excerpts.iter().map(|excerpt| {
|
||||
let expanded = excerpt.expanded;
|
||||
let element_id = excerpt.element_id.clone();
|
||||
|
||||
CollapsibleContainer::new(element_id.clone(), expanded)
|
||||
.start_slot(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(Icon::new(IconName::File).color(Color::Muted))
|
||||
.child(Label::new(excerpt.path.clone()).color(Color::Muted)),
|
||||
)
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
this.toggle_expanded(element_id.clone(), cx);
|
||||
}))
|
||||
.child(
|
||||
div()
|
||||
.p_2()
|
||||
.rounded_md()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.child(
|
||||
excerpt.text.clone(), // todo!(): Show as an editor block
|
||||
),
|
||||
)
|
||||
}))
|
||||
}
|
||||
CodebaseContext::Done(Err(error)) => div().child(error.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CodebaseContext {
|
||||
#[allow(dead_code)]
|
||||
fn new(
|
||||
query: impl 'static + Future<Output = Result<String>>,
|
||||
populated: oneshot::Sender<bool>,
|
||||
project_index: Model<ProjectIndex>,
|
||||
fs: Arc<dyn Fs>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let query = query.boxed_local();
|
||||
let _task = cx.spawn(|this, mut cx| async move {
|
||||
let result = async {
|
||||
let query = query.await?;
|
||||
let results = this
|
||||
.update(&mut cx, |_this, cx| {
|
||||
project_index.read(cx).search(&query, 16, cx)
|
||||
})?
|
||||
.await;
|
||||
|
||||
let excerpts = results.into_iter().map(|result| {
|
||||
let abs_path = result
|
||||
.worktree
|
||||
.read_with(&cx, |worktree, _| worktree.abs_path().join(&result.path));
|
||||
let fs = fs.clone();
|
||||
|
||||
async move {
|
||||
let path = result.path.clone();
|
||||
let text = fs.load(&abs_path?).await?;
|
||||
// todo!("what should we do with stale ranges?");
|
||||
let range = cmp::min(result.range.start, text.len())
|
||||
..cmp::min(result.range.end, text.len());
|
||||
|
||||
let text = SharedString::from(text[range].to_string());
|
||||
|
||||
anyhow::Ok(CodebaseExcerpt {
|
||||
element_id: ElementId::Name(nanoid::nanoid!().into()),
|
||||
path: path.to_string_lossy().to_string().into(),
|
||||
text,
|
||||
score: result.score,
|
||||
expanded: false,
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
anyhow::Ok(
|
||||
futures::future::join_all(excerpts)
|
||||
.await
|
||||
.into_iter()
|
||||
.filter_map(|result| result.log_err())
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
.await;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.populate(result, populated, cx);
|
||||
})
|
||||
.ok();
|
||||
});
|
||||
|
||||
Self::Pending { _task }
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn populate(
|
||||
&mut self,
|
||||
result: Result<Vec<CodebaseExcerpt>>,
|
||||
populated: oneshot::Sender<bool>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let success = result.is_ok();
|
||||
*self = Self::Done(result);
|
||||
populated.send(success).ok();
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn completion_messages(&self) -> Vec<CompletionMessage> {
|
||||
// One system message for the whole batch of excerpts:
|
||||
|
||||
// Semantic search results for user query:
|
||||
//
|
||||
// Excerpt from $path:
|
||||
// ~~~
|
||||
// `text`
|
||||
// ~~~
|
||||
//
|
||||
// Excerpt from $path:
|
||||
|
||||
match self {
|
||||
CodebaseContext::Done(Ok(excerpts)) => {
|
||||
if excerpts.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let mut body = "Semantic search results for user query:\n".to_string();
|
||||
|
||||
for excerpt in excerpts {
|
||||
body.push_str("Excerpt from ");
|
||||
body.push_str(excerpt.path.as_ref());
|
||||
body.push_str(", score ");
|
||||
body.push_str(&excerpt.score.to_string());
|
||||
body.push_str(":\n");
|
||||
body.push_str("~~~\n");
|
||||
body.push_str(excerpt.text.as_ref());
|
||||
body.push_str("~~~\n");
|
||||
}
|
||||
|
||||
vec![CompletionMessage::System { content: body }]
|
||||
}
|
||||
_ => vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ use anyhow::Result;
|
||||
use assistant_tooling::ToolFunctionDefinition;
|
||||
use client::{proto, Client};
|
||||
use futures::{future::BoxFuture, stream::BoxStream, FutureExt, StreamExt};
|
||||
use gpui::Global;
|
||||
use gpui::{AppContext, Global};
|
||||
use std::sync::Arc;
|
||||
|
||||
pub use open_ai::RequestMessage as CompletionMessage;
|
||||
@@ -11,6 +11,10 @@ pub use open_ai::RequestMessage as CompletionMessage;
|
||||
pub struct CompletionProvider(Arc<dyn CompletionProviderBackend>);
|
||||
|
||||
impl CompletionProvider {
|
||||
pub fn get(cx: &AppContext) -> &Self {
|
||||
cx.global::<CompletionProvider>()
|
||||
}
|
||||
|
||||
pub fn new(backend: impl CompletionProviderBackend) -> Self {
|
||||
Self(Arc::new(backend))
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use anyhow::Result;
|
||||
use assistant_tooling::LanguageModelTool;
|
||||
use gpui::{prelude::*, AppContext, Model, Task};
|
||||
use gpui::{prelude::*, AnyView, AppContext, Model, Task};
|
||||
use project::Fs;
|
||||
use schemars::JsonSchema;
|
||||
use semantic_index::ProjectIndex;
|
||||
use semantic_index::{ProjectIndex, Status};
|
||||
use serde::Deserialize;
|
||||
use std::sync::Arc;
|
||||
use ui::{
|
||||
@@ -36,13 +36,14 @@ pub struct CodebaseQuery {
|
||||
|
||||
pub struct ProjectIndexView {
|
||||
input: CodebaseQuery,
|
||||
output: Result<Vec<CodebaseExcerpt>>,
|
||||
output: Result<ProjectIndexOutput>,
|
||||
}
|
||||
|
||||
impl ProjectIndexView {
|
||||
fn toggle_expanded(&mut self, element_id: ElementId, cx: &mut ViewContext<Self>) {
|
||||
if let Ok(excerpts) = &mut self.output {
|
||||
if let Some(excerpt) = excerpts
|
||||
if let Ok(output) = &mut self.output {
|
||||
if let Some(excerpt) = output
|
||||
.excerpts
|
||||
.iter_mut()
|
||||
.find(|excerpt| excerpt.element_id == element_id)
|
||||
{
|
||||
@@ -59,11 +60,11 @@ impl Render for ProjectIndexView {
|
||||
|
||||
let result = &self.output;
|
||||
|
||||
let excerpts = match result {
|
||||
let output = match result {
|
||||
Err(err) => {
|
||||
return div().child(Label::new(format!("Error: {}", err)).color(Color::Error));
|
||||
}
|
||||
Ok(excerpts) => excerpts,
|
||||
Ok(output) => output,
|
||||
};
|
||||
|
||||
div()
|
||||
@@ -80,7 +81,7 @@ impl Render for ProjectIndexView {
|
||||
.child(Label::new(query).color(Color::Muted)),
|
||||
),
|
||||
)
|
||||
.children(excerpts.iter().map(|excerpt| {
|
||||
.children(output.excerpts.iter().map(|excerpt| {
|
||||
let element_id = excerpt.element_id.clone();
|
||||
let expanded = excerpt.expanded;
|
||||
|
||||
@@ -99,9 +100,7 @@ impl Render for ProjectIndexView {
|
||||
.p_2()
|
||||
.rounded_md()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.child(
|
||||
excerpt.text.clone(), // todo!(): Show as an editor block
|
||||
),
|
||||
.child(excerpt.text.clone()),
|
||||
)
|
||||
}))
|
||||
}
|
||||
@@ -112,8 +111,15 @@ pub struct ProjectIndexTool {
|
||||
fs: Arc<dyn Fs>,
|
||||
}
|
||||
|
||||
pub struct ProjectIndexOutput {
|
||||
excerpts: Vec<CodebaseExcerpt>,
|
||||
status: Status,
|
||||
}
|
||||
|
||||
impl ProjectIndexTool {
|
||||
pub fn new(project_index: Model<ProjectIndex>, fs: Arc<dyn Fs>) -> Self {
|
||||
// Listen for project index status and update the ProjectIndexTool directly
|
||||
|
||||
// TODO: setup a better description based on the user's current codebase.
|
||||
Self { project_index, fs }
|
||||
}
|
||||
@@ -121,7 +127,7 @@ impl ProjectIndexTool {
|
||||
|
||||
impl LanguageModelTool for ProjectIndexTool {
|
||||
type Input = CodebaseQuery;
|
||||
type Output = Vec<CodebaseExcerpt>;
|
||||
type Output = ProjectIndexOutput;
|
||||
type View = ProjectIndexView;
|
||||
|
||||
fn name(&self) -> String {
|
||||
@@ -135,6 +141,7 @@ impl LanguageModelTool for ProjectIndexTool {
|
||||
fn execute(&self, query: &Self::Input, cx: &AppContext) -> Task<Result<Self::Output>> {
|
||||
let project_index = self.project_index.read(cx);
|
||||
|
||||
let status = project_index.status();
|
||||
let results = project_index.search(
|
||||
query.query.as_str(),
|
||||
query.limit.unwrap_or(DEFAULT_SEARCH_LIMIT),
|
||||
@@ -180,11 +187,11 @@ impl LanguageModelTool for ProjectIndexTool {
|
||||
.into_iter()
|
||||
.filter_map(|result| result.log_err())
|
||||
.collect();
|
||||
anyhow::Ok(excerpts)
|
||||
anyhow::Ok(ProjectIndexOutput { excerpts, status })
|
||||
})
|
||||
}
|
||||
|
||||
fn new_view(
|
||||
fn output_view(
|
||||
_tool_call_id: String,
|
||||
input: Self::Input,
|
||||
output: Result<Self::Output>,
|
||||
@@ -193,16 +200,28 @@ impl LanguageModelTool for ProjectIndexTool {
|
||||
cx.new_view(|_cx| ProjectIndexView { input, output })
|
||||
}
|
||||
|
||||
fn status_view(&self, cx: &mut WindowContext) -> Option<AnyView> {
|
||||
Some(
|
||||
cx.new_view(|cx| ProjectIndexStatusView::new(self.project_index.clone(), cx))
|
||||
.into(),
|
||||
)
|
||||
}
|
||||
|
||||
fn format(_input: &Self::Input, output: &Result<Self::Output>) -> String {
|
||||
match &output {
|
||||
Ok(excerpts) => {
|
||||
if excerpts.len() == 0 {
|
||||
return "No results found".to_string();
|
||||
}
|
||||
|
||||
Ok(output) => {
|
||||
let mut body = "Semantic search results:\n".to_string();
|
||||
|
||||
for excerpt in excerpts {
|
||||
if output.status != Status::Idle {
|
||||
body.push_str("Still indexing. Results may be incomplete.\n");
|
||||
}
|
||||
|
||||
if output.excerpts.is_empty() {
|
||||
body.push_str("No results found");
|
||||
return body;
|
||||
}
|
||||
|
||||
for excerpt in &output.excerpts {
|
||||
body.push_str("Excerpt from ");
|
||||
body.push_str(excerpt.path.as_ref());
|
||||
body.push_str(", score ");
|
||||
@@ -218,3 +237,31 @@ impl LanguageModelTool for ProjectIndexTool {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ProjectIndexStatusView {
|
||||
project_index: Model<ProjectIndex>,
|
||||
}
|
||||
|
||||
impl ProjectIndexStatusView {
|
||||
pub fn new(project_index: Model<ProjectIndex>, cx: &mut ViewContext<Self>) -> Self {
|
||||
cx.subscribe(&project_index, |_this, _, _status: &Status, cx| {
|
||||
cx.notify();
|
||||
})
|
||||
.detach();
|
||||
Self { project_index }
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ProjectIndexStatusView {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let status = self.project_index.read(cx).status();
|
||||
|
||||
h_flex().gap_2().map(|element| match status {
|
||||
Status::Idle => element.child(Label::new("Project index ready")),
|
||||
Status::Loading => element.child(Label::new("Project index loading...")),
|
||||
Status::Scanning { remaining_count } => element.child(Label::new(format!(
|
||||
"Project index scanning: {remaining_count} remaining..."
|
||||
))),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
5
crates/assistant2/src/ui.rs
Normal file
5
crates/assistant2/src/ui.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
mod chat_message;
|
||||
mod composer;
|
||||
|
||||
pub use chat_message::*;
|
||||
pub use composer::*;
|
||||
131
crates/assistant2/src/ui/chat_message.rs
Normal file
131
crates/assistant2/src/ui/chat_message.rs
Normal file
@@ -0,0 +1,131 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use client::User;
|
||||
use gpui::{AnyElement, ClickEvent};
|
||||
use ui::{prelude::*, Avatar};
|
||||
|
||||
use crate::MessageId;
|
||||
|
||||
pub enum UserOrAssistant {
|
||||
User(Option<Arc<User>>),
|
||||
Assistant,
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct ChatMessage {
|
||||
id: MessageId,
|
||||
player: UserOrAssistant,
|
||||
message: AnyElement,
|
||||
collapsed: bool,
|
||||
on_collapse_handle_click: Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>,
|
||||
}
|
||||
|
||||
impl ChatMessage {
|
||||
pub fn new(
|
||||
id: MessageId,
|
||||
player: UserOrAssistant,
|
||||
message: AnyElement,
|
||||
collapsed: bool,
|
||||
on_collapse_handle_click: Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>,
|
||||
) -> Self {
|
||||
Self {
|
||||
id,
|
||||
player,
|
||||
message,
|
||||
collapsed,
|
||||
on_collapse_handle_click,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for ChatMessage {
|
||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||
// TODO: This should be top padding + 1.5x line height
|
||||
// Set the message height to cut off at exactly 1.5 lines when collapsed
|
||||
let collapsed_height = rems(2.875);
|
||||
|
||||
let collapse_handle_id = SharedString::from(format!("{}_collapse_handle", self.id.0));
|
||||
let collapse_handle = h_flex()
|
||||
.id(collapse_handle_id.clone())
|
||||
.group(collapse_handle_id.clone())
|
||||
.flex_none()
|
||||
.justify_center()
|
||||
.w_1()
|
||||
.mx_2()
|
||||
.h_full()
|
||||
.on_click(self.on_collapse_handle_click)
|
||||
.child(
|
||||
div()
|
||||
.w_px()
|
||||
.h_full()
|
||||
.rounded_lg()
|
||||
.overflow_hidden()
|
||||
.bg(cx.theme().colors().element_background)
|
||||
.group_hover(collapse_handle_id, |this| {
|
||||
this.bg(cx.theme().colors().element_hover)
|
||||
}),
|
||||
);
|
||||
let content = div()
|
||||
.overflow_hidden()
|
||||
.w_full()
|
||||
.p_4()
|
||||
.rounded_lg()
|
||||
.when(self.collapsed, |this| this.h(collapsed_height))
|
||||
.bg(cx.theme().colors().surface_background)
|
||||
.child(self.message);
|
||||
|
||||
v_flex()
|
||||
.gap_1()
|
||||
.child(ChatMessageHeader::new(self.player))
|
||||
.child(h_flex().gap_3().child(collapse_handle).child(content))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
struct ChatMessageHeader {
|
||||
player: UserOrAssistant,
|
||||
contexts: Vec<()>,
|
||||
}
|
||||
|
||||
impl ChatMessageHeader {
|
||||
fn new(player: UserOrAssistant) -> Self {
|
||||
Self {
|
||||
player,
|
||||
contexts: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for ChatMessageHeader {
|
||||
fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
|
||||
let (username, avatar_uri) = match self.player {
|
||||
UserOrAssistant::Assistant => (
|
||||
"Assistant".into(),
|
||||
Some("https://zed.dev/assistant_avatar.png".into()),
|
||||
),
|
||||
UserOrAssistant::User(Some(user)) => {
|
||||
(user.github_login.clone(), Some(user.avatar_uri.clone()))
|
||||
}
|
||||
UserOrAssistant::User(None) => ("You".into(), None),
|
||||
};
|
||||
|
||||
h_flex()
|
||||
.justify_between()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_3()
|
||||
.map(|this| {
|
||||
let avatar_size = rems(20.0 / 16.0);
|
||||
if let Some(avatar_uri) = avatar_uri {
|
||||
this.child(Avatar::new(avatar_uri).size(avatar_size))
|
||||
} else {
|
||||
this.child(div().size(avatar_size))
|
||||
}
|
||||
})
|
||||
.child(Label::new(username).color(Color::Default)),
|
||||
)
|
||||
.child(div().when(!self.contexts.is_empty(), |this| {
|
||||
this.child(Label::new(self.contexts.len().to_string()).color(Color::Muted))
|
||||
}))
|
||||
}
|
||||
}
|
||||
222
crates/assistant2/src/ui/composer.rs
Normal file
222
crates/assistant2/src/ui/composer.rs
Normal file
@@ -0,0 +1,222 @@
|
||||
use assistant_tooling::ToolRegistry;
|
||||
use client::User;
|
||||
use editor::{Editor, EditorElement, EditorStyle};
|
||||
use gpui::{FontStyle, FontWeight, TextStyle, View, WeakView, WhiteSpace};
|
||||
use settings::Settings;
|
||||
use std::sync::Arc;
|
||||
use theme::ThemeSettings;
|
||||
use ui::{popover_menu, prelude::*, Avatar, ButtonLike, ContextMenu, Tooltip};
|
||||
|
||||
use crate::{AssistantChat, CompletionProvider, Submit, SubmitMode};
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct Composer {
|
||||
assistant_chat: WeakView<AssistantChat>,
|
||||
model: String,
|
||||
editor: View<Editor>,
|
||||
player: Option<Arc<User>>,
|
||||
can_submit: bool,
|
||||
tool_registry: Arc<ToolRegistry>,
|
||||
}
|
||||
|
||||
impl Composer {
|
||||
pub fn new(
|
||||
assistant_chat: WeakView<AssistantChat>,
|
||||
model: String,
|
||||
editor: View<Editor>,
|
||||
player: Option<Arc<User>>,
|
||||
can_submit: bool,
|
||||
tool_registry: Arc<ToolRegistry>,
|
||||
) -> Self {
|
||||
Self {
|
||||
assistant_chat,
|
||||
model,
|
||||
editor,
|
||||
player,
|
||||
can_submit,
|
||||
tool_registry,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for Composer {
|
||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||
let mut player_avatar = div().size(rems(20.0 / 16.0)).into_any_element();
|
||||
if let Some(player) = self.player.clone() {
|
||||
player_avatar = Avatar::new(player.avatar_uri.clone())
|
||||
.size(rems(20.0 / 16.0))
|
||||
.into_any_element();
|
||||
}
|
||||
|
||||
let font_size = rems(0.875);
|
||||
let line_height = font_size.to_pixels(cx.rem_size()) * 1.3;
|
||||
|
||||
h_flex()
|
||||
.w_full()
|
||||
.items_start()
|
||||
.mt_4()
|
||||
.gap_3()
|
||||
.child(player_avatar)
|
||||
.child(
|
||||
v_flex()
|
||||
.size_full()
|
||||
.gap_1()
|
||||
.child(
|
||||
v_flex()
|
||||
.w_full()
|
||||
.p_4()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.rounded_lg()
|
||||
.child(
|
||||
v_flex()
|
||||
.justify_between()
|
||||
.w_full()
|
||||
.gap_1()
|
||||
.min_h(line_height * 4 + px(74.0))
|
||||
.child({
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
let text_style = TextStyle {
|
||||
color: cx.theme().colors().editor_foreground,
|
||||
font_family: settings.buffer_font.family.clone(),
|
||||
font_features: settings.buffer_font.features.clone(),
|
||||
font_size: font_size.into(),
|
||||
font_weight: FontWeight::NORMAL,
|
||||
font_style: FontStyle::Normal,
|
||||
line_height: line_height.into(),
|
||||
background_color: None,
|
||||
underline: None,
|
||||
strikethrough: None,
|
||||
white_space: WhiteSpace::Normal,
|
||||
};
|
||||
|
||||
EditorElement::new(
|
||||
&self.editor,
|
||||
EditorStyle {
|
||||
background: cx.theme().colors().editor_background,
|
||||
local_player: cx.theme().players().local(),
|
||||
text: text_style,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
})
|
||||
.child(
|
||||
h_flex()
|
||||
.flex_none()
|
||||
.gap_2()
|
||||
.justify_between()
|
||||
.w_full()
|
||||
.child(
|
||||
h_flex().gap_1().child(
|
||||
// IconButton/button
|
||||
// Toggle - if enabled, .selected(true).selected_style(IconButtonStyle::Filled)
|
||||
//
|
||||
// match status
|
||||
// Tooltip::with_meta("some label explaining project index + status", "click to enable")
|
||||
IconButton::new(
|
||||
"add-context",
|
||||
IconName::FileDoc,
|
||||
)
|
||||
.icon_color(Color::Muted),
|
||||
), // .child(
|
||||
// IconButton::new(
|
||||
// "add-context",
|
||||
// IconName::Plus,
|
||||
// )
|
||||
// .icon_color(Color::Muted),
|
||||
// ),
|
||||
)
|
||||
.child(
|
||||
Button::new("send-button", "Send")
|
||||
.style(ButtonStyle::Filled)
|
||||
.disabled(!self.can_submit)
|
||||
.on_click(|_, cx| {
|
||||
cx.dispatch_action(Box::new(Submit(
|
||||
SubmitMode::Codebase,
|
||||
)))
|
||||
})
|
||||
.tooltip(|cx| {
|
||||
Tooltip::for_action(
|
||||
"Submit message",
|
||||
&Submit(SubmitMode::Codebase),
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.justify_between()
|
||||
.child(ModelSelector::new(self.assistant_chat, self.model))
|
||||
.children(self.tool_registry.status_views().iter().cloned()),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
struct ModelSelector {
|
||||
assistant_chat: WeakView<AssistantChat>,
|
||||
model: String,
|
||||
}
|
||||
|
||||
impl ModelSelector {
|
||||
pub fn new(assistant_chat: WeakView<AssistantChat>, model: String) -> Self {
|
||||
Self {
|
||||
assistant_chat,
|
||||
model,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for ModelSelector {
|
||||
fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
|
||||
popover_menu("model-switcher")
|
||||
.menu(move |cx| {
|
||||
ContextMenu::build(cx, |mut menu, cx| {
|
||||
for model in CompletionProvider::get(cx).available_models() {
|
||||
menu = menu.custom_entry(
|
||||
{
|
||||
let model = model.clone();
|
||||
move |_| Label::new(model.clone()).into_any_element()
|
||||
},
|
||||
{
|
||||
let assistant_chat = self.assistant_chat.clone();
|
||||
move |cx| {
|
||||
_ = assistant_chat.update(cx, |assistant_chat, cx| {
|
||||
assistant_chat.model = model.clone();
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
menu
|
||||
})
|
||||
.into()
|
||||
})
|
||||
.trigger(
|
||||
ButtonLike::new("active-model")
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
div()
|
||||
.overflow_x_hidden()
|
||||
.flex_grow()
|
||||
.whitespace_nowrap()
|
||||
.child(Label::new(self.model)),
|
||||
)
|
||||
.child(
|
||||
div().child(Icon::new(IconName::ChevronDown).color(Color::Muted)),
|
||||
),
|
||||
)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.tooltip(move |cx| Tooltip::text("Change Model", cx)),
|
||||
)
|
||||
.anchor(gpui::AnchorCorner::BottomRight)
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use gpui::{Task, WindowContext};
|
||||
use gpui::{AnyView, Task, WindowContext};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::tool::{
|
||||
@@ -12,6 +12,7 @@ pub struct ToolRegistry {
|
||||
Box<dyn Fn(&ToolFunctionCall, &mut WindowContext) -> Task<Result<ToolFunctionCall>>>,
|
||||
>,
|
||||
definitions: Vec<ToolFunctionDefinition>,
|
||||
status_views: Vec<AnyView>,
|
||||
}
|
||||
|
||||
impl ToolRegistry {
|
||||
@@ -19,6 +20,7 @@ impl ToolRegistry {
|
||||
Self {
|
||||
tools: HashMap::new(),
|
||||
definitions: Vec::new(),
|
||||
status_views: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,8 +28,17 @@ impl ToolRegistry {
|
||||
&self.definitions
|
||||
}
|
||||
|
||||
pub fn register<T: 'static + LanguageModelTool>(&mut self, tool: T) -> Result<()> {
|
||||
pub fn register<T: 'static + LanguageModelTool>(
|
||||
&mut self,
|
||||
tool: T,
|
||||
cx: &mut WindowContext,
|
||||
) -> Result<()> {
|
||||
self.definitions.push(tool.definition());
|
||||
|
||||
if let Some(tool_view) = tool.status_view(cx) {
|
||||
self.status_views.push(tool_view);
|
||||
}
|
||||
|
||||
let name = tool.name();
|
||||
let previous = self.tools.insert(
|
||||
name.clone(),
|
||||
@@ -52,7 +63,7 @@ impl ToolRegistry {
|
||||
cx.spawn(move |mut cx| async move {
|
||||
let result: Result<T::Output> = result.await;
|
||||
let for_model = T::format(&input, &result);
|
||||
let view = cx.update(|cx| T::new_view(id.clone(), input, result, cx))?;
|
||||
let view = cx.update(|cx| T::output_view(id.clone(), input, result, cx))?;
|
||||
|
||||
Ok(ToolFunctionCall {
|
||||
id,
|
||||
@@ -100,6 +111,10 @@ impl ToolRegistry {
|
||||
|
||||
tool(tool_call, cx)
|
||||
}
|
||||
|
||||
pub fn status_views(&self) -> &[AnyView] {
|
||||
&self.status_views
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -165,7 +180,7 @@ mod test {
|
||||
Task::ready(Ok(weather))
|
||||
}
|
||||
|
||||
fn new_view(
|
||||
fn output_view(
|
||||
_tool_call_id: String,
|
||||
_input: Self::Input,
|
||||
result: Result<Self::Output>,
|
||||
@@ -182,46 +197,6 @@ mod test {
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_function_registry(cx: &mut TestAppContext) {
|
||||
cx.background_executor.run_until_parked();
|
||||
|
||||
let mut registry = ToolRegistry::new();
|
||||
|
||||
let tool = WeatherTool {
|
||||
current_weather: WeatherResult {
|
||||
location: "San Francisco".to_string(),
|
||||
temperature: 21.0,
|
||||
unit: "Celsius".to_string(),
|
||||
},
|
||||
};
|
||||
|
||||
registry.register(tool).unwrap();
|
||||
|
||||
// let _result = cx
|
||||
// .update(|cx| {
|
||||
// registry.call(
|
||||
// &ToolFunctionCall {
|
||||
// name: "get_current_weather".to_string(),
|
||||
// arguments: r#"{ "location": "San Francisco", "unit": "Celsius" }"#
|
||||
// .to_string(),
|
||||
// id: "test-123".to_string(),
|
||||
// result: None,
|
||||
// },
|
||||
// cx,
|
||||
// )
|
||||
// })
|
||||
// .await;
|
||||
|
||||
// assert!(result.is_ok());
|
||||
// let result = result.unwrap();
|
||||
|
||||
// let expected = r#"{"location":"San Francisco","temperature":21.0,"unit":"Celsius"}"#;
|
||||
|
||||
// todo!(): Put this back in after the interface is stabilized
|
||||
// assert_eq!(result, expected);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_openai_weather_example(cx: &mut TestAppContext) {
|
||||
cx.background_executor.run_until_parked();
|
||||
|
||||
@@ -95,10 +95,14 @@ pub trait LanguageModelTool {
|
||||
|
||||
fn format(input: &Self::Input, output: &Result<Self::Output>) -> String;
|
||||
|
||||
fn new_view(
|
||||
fn output_view(
|
||||
tool_call_id: String,
|
||||
input: Self::Input,
|
||||
output: Result<Self::Output>,
|
||||
cx: &mut WindowContext,
|
||||
) -> View<Self::View>;
|
||||
|
||||
fn status_view(&self, _cx: &mut WindowContext) -> Option<AnyView> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
@@ -243,7 +243,7 @@ fn view_release_notes_locally(workspace: &mut Workspace, cx: &mut ViewContext<Wo
|
||||
Some(tab_description),
|
||||
cx,
|
||||
);
|
||||
workspace.add_item_to_active_pane(Box::new(view.clone()), cx);
|
||||
workspace.add_item_to_active_pane(Box::new(view.clone()), None, cx);
|
||||
cx.notify();
|
||||
})
|
||||
.log_err();
|
||||
|
||||
@@ -421,7 +421,7 @@ impl Telemetry {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(checksum_seed) = &*ZED_CLIENT_CHECKSUM_SEED else {
|
||||
if ZED_CLIENT_CHECKSUM_SEED.is_none() {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -466,15 +466,9 @@ impl Telemetry {
|
||||
serde_json::to_writer(&mut json_bytes, &request_body)?;
|
||||
}
|
||||
|
||||
let mut summer = Sha256::new();
|
||||
summer.update(checksum_seed);
|
||||
summer.update(&json_bytes);
|
||||
summer.update(checksum_seed);
|
||||
let mut checksum = String::new();
|
||||
for byte in summer.finalize().as_slice() {
|
||||
use std::fmt::Write;
|
||||
write!(&mut checksum, "{:02x}", byte).unwrap();
|
||||
}
|
||||
let Some(checksum) = calculate_json_checksum(&json_bytes) else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let request = http::Request::builder()
|
||||
.method(Method::POST)
|
||||
@@ -657,3 +651,21 @@ mod tests {
|
||||
&& telemetry.state.lock().first_event_date_time.is_none()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn calculate_json_checksum(json: &impl AsRef<[u8]>) -> Option<String> {
|
||||
let Some(checksum_seed) = &*ZED_CLIENT_CHECKSUM_SEED else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let mut summer = Sha256::new();
|
||||
summer.update(checksum_seed);
|
||||
summer.update(&json);
|
||||
summer.update(checksum_seed);
|
||||
let mut checksum = String::new();
|
||||
for byte in summer.finalize().as_slice() {
|
||||
use std::fmt::Write;
|
||||
write!(&mut checksum, "{:02x}", byte).unwrap();
|
||||
}
|
||||
|
||||
Some(checksum)
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ google_ai.workspace = true
|
||||
hex.workspace = true
|
||||
live_kit_server.workspace = true
|
||||
log.workspace = true
|
||||
nanoid = "0.4"
|
||||
nanoid.workspace = true
|
||||
open_ai.workspace = true
|
||||
parking_lot.workspace = true
|
||||
prometheus = "0.13"
|
||||
|
||||
@@ -18,11 +18,15 @@ use telemetry_events::{
|
||||
ActionEvent, AppEvent, AssistantEvent, CallEvent, CopilotEvent, CpuEvent, EditEvent,
|
||||
EditorEvent, Event, EventRequestBody, EventWrapper, ExtensionEvent, MemoryEvent, SettingEvent,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
static CRASH_REPORTS_BUCKET: &str = "zed-crash-reports";
|
||||
|
||||
pub fn router() -> Router {
|
||||
Router::new()
|
||||
.route("/telemetry/events", post(post_events))
|
||||
.route("/telemetry/crashes", post(post_crash))
|
||||
.route("/telemetry/hangs", post(post_hang))
|
||||
}
|
||||
|
||||
pub struct ZedChecksumHeader(Vec<u8>);
|
||||
@@ -85,8 +89,6 @@ pub async fn post_crash(
|
||||
headers: HeaderMap,
|
||||
body: Bytes,
|
||||
) -> Result<()> {
|
||||
static CRASH_REPORTS_BUCKET: &str = "zed-crash-reports";
|
||||
|
||||
let report = IpsFile::parse(&body)?;
|
||||
let version_threshold = SemanticVersion::new(0, 123, 0);
|
||||
|
||||
@@ -136,6 +138,13 @@ pub async fn post_crash(
|
||||
.get("x-zed-panicked-on")
|
||||
.and_then(|h| h.to_str().ok())
|
||||
.and_then(|s| s.parse().ok());
|
||||
|
||||
let installation_id = headers
|
||||
.get("x-zed-installation-id")
|
||||
.and_then(|h| h.to_str().ok())
|
||||
.map(|s| s.to_string())
|
||||
.unwrap_or_default();
|
||||
|
||||
let mut recent_panic = None;
|
||||
|
||||
if let Some(recent_panic_on) = recent_panic_on {
|
||||
@@ -160,6 +169,7 @@ pub async fn post_crash(
|
||||
os_version = %report.header.os_version,
|
||||
bundle_id = %report.header.bundle_id,
|
||||
incident_id = %report.header.incident_id,
|
||||
installation_id = %installation_id,
|
||||
description = %description,
|
||||
backtrace = %summary,
|
||||
"crash report");
|
||||
@@ -214,6 +224,107 @@ pub async fn post_crash(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn post_hang(
|
||||
Extension(app): Extension<Arc<AppState>>,
|
||||
TypedHeader(ZedChecksumHeader(checksum)): TypedHeader<ZedChecksumHeader>,
|
||||
body: Bytes,
|
||||
) -> Result<()> {
|
||||
let Some(expected) = calculate_json_checksum(app.clone(), &body) else {
|
||||
return Err(Error::Http(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"events not enabled".into(),
|
||||
))?;
|
||||
};
|
||||
|
||||
if checksum != expected {
|
||||
return Err(Error::Http(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"invalid checksum".into(),
|
||||
))?;
|
||||
}
|
||||
|
||||
let incident_id = Uuid::new_v4().to_string();
|
||||
|
||||
// dump JSON into S3 so we can get frame offsets if we need to.
|
||||
if let Some(blob_store_client) = app.blob_store_client.as_ref() {
|
||||
blob_store_client
|
||||
.put_object()
|
||||
.bucket(CRASH_REPORTS_BUCKET)
|
||||
.key(incident_id.clone() + ".hang.json")
|
||||
.acl(aws_sdk_s3::types::ObjectCannedAcl::PublicRead)
|
||||
.body(ByteStream::from(body.to_vec()))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| log::error!("Failed to upload crash: {}", e))
|
||||
.ok();
|
||||
}
|
||||
|
||||
let report: telemetry_events::HangReport = serde_json::from_slice(&body).map_err(|err| {
|
||||
log::error!("can't parse report json: {err}");
|
||||
Error::Internal(anyhow!(err))
|
||||
})?;
|
||||
|
||||
let mut backtrace = "Possible hang detected on main threadL".to_string();
|
||||
let unknown = "<unknown>".to_string();
|
||||
for frame in report.backtrace.iter() {
|
||||
backtrace.push_str(&format!("\n{}", frame.symbols.first().unwrap_or(&unknown)));
|
||||
}
|
||||
|
||||
tracing::error!(
|
||||
service = "client",
|
||||
version = %report.app_version.unwrap_or_default().to_string(),
|
||||
os_name = %report.os_name,
|
||||
os_version = report.os_version.unwrap_or_default().to_string(),
|
||||
incident_id = %incident_id,
|
||||
installation_id = %report.installation_id.unwrap_or_default(),
|
||||
backtrace = %backtrace,
|
||||
"hang report");
|
||||
|
||||
if let Some(slack_panics_webhook) = app.config.slack_panics_webhook.clone() {
|
||||
let payload = slack::WebhookBody::new(|w| {
|
||||
w.add_section(|s| s.text(slack::Text::markdown("Possible Hang".to_string())))
|
||||
.add_section(|s| {
|
||||
s.add_field(slack::Text::markdown(format!(
|
||||
"*Version:*\n {} ",
|
||||
report.app_version.unwrap_or_default()
|
||||
)))
|
||||
.add_field({
|
||||
let hostname = app.config.blob_store_url.clone().unwrap_or_default();
|
||||
let hostname = hostname.strip_prefix("https://").unwrap_or_else(|| {
|
||||
hostname.strip_prefix("http://").unwrap_or_default()
|
||||
});
|
||||
|
||||
slack::Text::markdown(format!(
|
||||
"*Incident:*\n<https://{}.{}/{}.hang.json|{}…>",
|
||||
CRASH_REPORTS_BUCKET,
|
||||
hostname,
|
||||
incident_id,
|
||||
incident_id.chars().take(8).collect::<String>(),
|
||||
))
|
||||
})
|
||||
})
|
||||
.add_rich_text(|r| r.add_preformatted(|p| p.add_text(backtrace)))
|
||||
});
|
||||
let payload_json = serde_json::to_string(&payload).map_err(|err| {
|
||||
log::error!("Failed to serialize payload to JSON: {err}");
|
||||
Error::Internal(anyhow!(err))
|
||||
})?;
|
||||
|
||||
reqwest::Client::new()
|
||||
.post(slack_panics_webhook)
|
||||
.header("Content-Type", "application/json")
|
||||
.body(payload_json)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|err| {
|
||||
log::error!("Failed to send payload to Slack: {err}");
|
||||
Error::Internal(anyhow!(err))
|
||||
})?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn post_events(
|
||||
Extension(app): Extension<Arc<AppState>>,
|
||||
TypedHeader(ZedChecksumHeader(checksum)): TypedHeader<ZedChecksumHeader>,
|
||||
@@ -227,19 +338,14 @@ pub async fn post_events(
|
||||
))?
|
||||
};
|
||||
|
||||
let Some(checksum_seed) = app.config.zed_client_checksum_seed.as_ref() else {
|
||||
let Some(expected) = calculate_json_checksum(app.clone(), &body) else {
|
||||
return Err(Error::Http(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"events not enabled".into(),
|
||||
))?;
|
||||
};
|
||||
|
||||
let mut summer = Sha256::new();
|
||||
summer.update(checksum_seed);
|
||||
summer.update(&body);
|
||||
summer.update(checksum_seed);
|
||||
|
||||
if &checksum != &summer.finalize()[..] {
|
||||
if checksum != expected {
|
||||
return Err(Error::Http(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"invalid checksum".into(),
|
||||
@@ -1053,3 +1159,15 @@ impl ActionEventRow {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn calculate_json_checksum(app: Arc<AppState>, json: &impl AsRef<[u8]>) -> Option<Vec<u8>> {
|
||||
let Some(checksum_seed) = app.config.zed_client_checksum_seed.as_ref() else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let mut summer = Sha256::new();
|
||||
summer.update(checksum_seed);
|
||||
summer.update(&json);
|
||||
summer.update(checksum_seed);
|
||||
Some(summer.finalize().into_iter().collect())
|
||||
}
|
||||
|
||||
@@ -70,6 +70,7 @@ async fn test_dev_server(cx: &mut gpui::TestAppContext, cx2: &mut gpui::TestAppC
|
||||
workspace::join_remote_project(
|
||||
projects[0].project_id.unwrap(),
|
||||
client.app_state.clone(),
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
@@ -205,7 +206,12 @@ async fn create_remote_project(
|
||||
let projects = store.remote_projects();
|
||||
assert_eq!(projects.len(), 1);
|
||||
assert_eq!(projects[0].path, "/remote");
|
||||
workspace::join_remote_project(projects[0].project_id.unwrap(), client_app_state, cx)
|
||||
workspace::join_remote_project(
|
||||
projects[0].project_id.unwrap(),
|
||||
client_app_state,
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -301,6 +307,7 @@ async fn test_dev_server_reconnect(
|
||||
workspace::join_remote_project(
|
||||
projects[0].project_id.unwrap(),
|
||||
client2.app_state.clone(),
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
@@ -359,3 +366,35 @@ async fn test_create_remote_project_path_validation(
|
||||
ErrorCode::RemoteProjectPathDoesNotExist
|
||||
));
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_save_as_remote(cx1: &mut gpui::TestAppContext, cx2: &mut gpui::TestAppContext) {
|
||||
let (server, client1) = TestServer::start1(cx1).await;
|
||||
|
||||
// Creating a project with a path that does exist should not fail
|
||||
let (dev_server, remote_workspace) =
|
||||
create_remote_project(&server, client1.app_state.clone(), cx1, cx2).await;
|
||||
|
||||
let mut cx = VisualTestContext::from_window(remote_workspace.into(), cx1);
|
||||
|
||||
cx.simulate_keystrokes("cmd-p 1 enter");
|
||||
cx.simulate_keystrokes("cmd-shift-s");
|
||||
cx.simulate_input("2.txt");
|
||||
cx.simulate_keystrokes("enter");
|
||||
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
let title = remote_workspace
|
||||
.update(&mut cx, |ws, cx| {
|
||||
ws.active_item(cx).unwrap().tab_description(0, &cx).unwrap()
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(title, "2.txt");
|
||||
|
||||
let path = Path::new("/remote/2.txt");
|
||||
assert_eq!(
|
||||
dev_server.fs().load(&path).await.unwrap(),
|
||||
"remote\nremote\nremote"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -310,7 +310,7 @@ async fn test_basic_following(
|
||||
let multibuffer_editor_a = workspace_a.update(cx_a, |workspace, cx| {
|
||||
let editor =
|
||||
cx.new_view(|cx| Editor::for_multibuffer(multibuffer_a, Some(project_a.clone()), cx));
|
||||
workspace.add_item_to_active_pane(Box::new(editor.clone()), cx);
|
||||
workspace.add_item_to_active_pane(Box::new(editor.clone()), None, cx);
|
||||
editor
|
||||
});
|
||||
executor.run_until_parked();
|
||||
|
||||
@@ -2468,7 +2468,12 @@ async fn test_propagate_saves_and_fs_changes(
|
||||
});
|
||||
project_a
|
||||
.update(cx_a, |project, cx| {
|
||||
project.save_buffer_as(new_buffer_a.clone(), "/a/file3.rs".into(), cx)
|
||||
let path = ProjectPath {
|
||||
path: Arc::from(Path::new("file3.rs")),
|
||||
worktree_id: worktree_a.read(cx).id(),
|
||||
};
|
||||
|
||||
project.save_buffer_as(new_buffer_a.clone(), path, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -3185,7 +3190,7 @@ async fn test_fs_operations(
|
||||
|
||||
project_b
|
||||
.update(cx_b, |project, cx| {
|
||||
project.delete_entry(dir_entry.id, cx).unwrap()
|
||||
project.delete_entry(dir_entry.id, false, cx).unwrap()
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -3213,7 +3218,7 @@ async fn test_fs_operations(
|
||||
|
||||
project_b
|
||||
.update(cx_b, |project, cx| {
|
||||
project.delete_entry(entry.id, cx).unwrap()
|
||||
project.delete_entry(entry.id, false, cx).unwrap()
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -522,7 +522,7 @@ impl Render for MessageEditor {
|
||||
cx.theme().colors().text
|
||||
},
|
||||
font_family: settings.ui_font.family.clone(),
|
||||
font_features: settings.ui_font.features,
|
||||
font_features: settings.ui_font.features.clone(),
|
||||
font_size: TextSize::Small.rems(cx).into(),
|
||||
font_weight: FontWeight::NORMAL,
|
||||
font_style: FontStyle::Normal,
|
||||
|
||||
@@ -2171,7 +2171,7 @@ impl CollabPanel {
|
||||
cx.theme().colors().text
|
||||
},
|
||||
font_family: settings.ui_font.family.clone(),
|
||||
font_features: settings.ui_font.features,
|
||||
font_features: settings.ui_font.features.clone(),
|
||||
font_size: rems(0.875).into(),
|
||||
font_weight: FontWeight::NORMAL,
|
||||
font_style: FontStyle::Normal,
|
||||
@@ -2970,6 +2970,7 @@ impl Render for DraggedChannelView {
|
||||
struct JoinChannelTooltip {
|
||||
channel_store: Model<ChannelStore>,
|
||||
channel_id: ChannelId,
|
||||
#[allow(unused)]
|
||||
has_notes_notification: bool,
|
||||
}
|
||||
|
||||
@@ -2983,12 +2984,6 @@ impl Render for JoinChannelTooltip {
|
||||
|
||||
container
|
||||
.child(Label::new("Join channel"))
|
||||
.children(self.has_notes_notification.then(|| {
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(Indicator::dot().color(Color::Info))
|
||||
.child(Label::new("Unread notes"))
|
||||
}))
|
||||
.children(participants.iter().map(|participant| {
|
||||
h_flex()
|
||||
.gap_2()
|
||||
|
||||
@@ -122,5 +122,6 @@ fn notification_window_options(
|
||||
display_id: Some(screen.id()),
|
||||
fullscreen: false,
|
||||
window_background: WindowBackgroundAppearance::default(),
|
||||
app_id: Some("dev.zed.Zed".to_owned()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -477,7 +477,7 @@ mod tests {
|
||||
});
|
||||
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.add_item_to_active_pane(Box::new(editor.clone()), cx);
|
||||
workspace.add_item_to_active_pane(Box::new(editor.clone()), None, cx);
|
||||
editor.update(cx, |editor, cx| editor.focus(cx))
|
||||
});
|
||||
|
||||
|
||||
@@ -15,13 +15,16 @@ doctest = false
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
collections.workspace = true
|
||||
ctor.workspace = true
|
||||
editor.workspace = true
|
||||
env_logger.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
language.workspace = true
|
||||
log.workspace = true
|
||||
lsp.workspace = true
|
||||
project.workspace = true
|
||||
rand.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
settings.workspace = true
|
||||
@@ -40,3 +43,4 @@ serde_json.workspace = true
|
||||
theme = { workspace = true, features = ["test-support"] }
|
||||
unindent.workspace = true
|
||||
workspace = { workspace = true, features = ["test-support"] }
|
||||
pretty_assertions.workspace = true
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
1011
crates/diagnostics/src/diagnostics_tests.rs
Normal file
1011
crates/diagnostics/src/diagnostics_tests.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,13 +1,11 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use collections::HashSet;
|
||||
use editor::Editor;
|
||||
use gpui::{
|
||||
percentage, rems, Animation, AnimationExt, EventEmitter, IntoElement, ParentElement, Render,
|
||||
Styled, Subscription, Transformation, View, ViewContext, WeakView,
|
||||
};
|
||||
use language::Diagnostic;
|
||||
use lsp::LanguageServerId;
|
||||
use ui::{h_flex, prelude::*, Button, ButtonLike, Color, Icon, IconName, Label, Tooltip};
|
||||
use workspace::{item::ItemHandle, StatusItemView, ToolbarItemEvent, Workspace};
|
||||
|
||||
@@ -18,7 +16,6 @@ pub struct DiagnosticIndicator {
|
||||
active_editor: Option<WeakView<Editor>>,
|
||||
workspace: WeakView<Workspace>,
|
||||
current_diagnostic: Option<Diagnostic>,
|
||||
in_progress_checks: HashSet<LanguageServerId>,
|
||||
_observe_active_editor: Option<Subscription>,
|
||||
}
|
||||
|
||||
@@ -64,7 +61,20 @@ impl Render for DiagnosticIndicator {
|
||||
.child(Label::new(warning_count.to_string()).size(LabelSize::Small)),
|
||||
};
|
||||
|
||||
let status = if !self.in_progress_checks.is_empty() {
|
||||
let has_in_progress_checks = self
|
||||
.workspace
|
||||
.upgrade()
|
||||
.and_then(|workspace| {
|
||||
workspace
|
||||
.read(cx)
|
||||
.project()
|
||||
.read(cx)
|
||||
.language_servers_running_disk_based_diagnostics()
|
||||
.next()
|
||||
})
|
||||
.is_some();
|
||||
|
||||
let status = if has_in_progress_checks {
|
||||
Some(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
@@ -126,15 +136,13 @@ impl DiagnosticIndicator {
|
||||
pub fn new(workspace: &Workspace, cx: &mut ViewContext<Self>) -> Self {
|
||||
let project = workspace.project();
|
||||
cx.subscribe(project, |this, project, event, cx| match event {
|
||||
project::Event::DiskBasedDiagnosticsStarted { language_server_id } => {
|
||||
this.in_progress_checks.insert(*language_server_id);
|
||||
project::Event::DiskBasedDiagnosticsStarted { .. } => {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
project::Event::DiskBasedDiagnosticsFinished { language_server_id }
|
||||
| project::Event::LanguageServerRemoved(language_server_id) => {
|
||||
project::Event::DiskBasedDiagnosticsFinished { .. }
|
||||
| project::Event::LanguageServerRemoved(_) => {
|
||||
this.summary = project.read(cx).diagnostic_summary(false, cx);
|
||||
this.in_progress_checks.remove(language_server_id);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -149,10 +157,6 @@ impl DiagnosticIndicator {
|
||||
|
||||
Self {
|
||||
summary: project.read(cx).diagnostic_summary(false, cx),
|
||||
in_progress_checks: project
|
||||
.read(cx)
|
||||
.language_servers_running_disk_based_diagnostics()
|
||||
.collect(),
|
||||
active_editor: None,
|
||||
workspace: workspace.weak_handle(),
|
||||
current_diagnostic: None,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::ProjectDiagnosticsEditor;
|
||||
use gpui::{div, EventEmitter, ParentElement, Render, ViewContext, WeakView};
|
||||
use gpui::{EventEmitter, ParentElement, Render, ViewContext, WeakView};
|
||||
use ui::prelude::*;
|
||||
use ui::{IconButton, IconName, Tooltip};
|
||||
use workspace::{item::ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView};
|
||||
@@ -10,12 +10,23 @@ pub struct ToolbarControls {
|
||||
|
||||
impl Render for ToolbarControls {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let include_warnings = self
|
||||
.editor
|
||||
.as_ref()
|
||||
.and_then(|editor| editor.upgrade())
|
||||
.map(|editor| editor.read(cx).include_warnings)
|
||||
.unwrap_or(false);
|
||||
let mut include_warnings = false;
|
||||
let mut has_stale_excerpts = false;
|
||||
let mut is_updating = false;
|
||||
|
||||
if let Some(editor) = self.editor.as_ref().and_then(|editor| editor.upgrade()) {
|
||||
let editor = editor.read(cx);
|
||||
|
||||
include_warnings = editor.include_warnings;
|
||||
has_stale_excerpts = !editor.paths_to_update.is_empty();
|
||||
is_updating = editor.update_paths_tx.len() > 0
|
||||
|| editor
|
||||
.project
|
||||
.read(cx)
|
||||
.language_servers_running_disk_based_diagnostics()
|
||||
.next()
|
||||
.is_some();
|
||||
}
|
||||
|
||||
let tooltip = if include_warnings {
|
||||
"Exclude Warnings"
|
||||
@@ -23,17 +34,37 @@ impl Render for ToolbarControls {
|
||||
"Include Warnings"
|
||||
};
|
||||
|
||||
div().child(
|
||||
IconButton::new("toggle-warnings", IconName::ExclamationTriangle)
|
||||
.tooltip(move |cx| Tooltip::text(tooltip, cx))
|
||||
.on_click(cx.listener(|this, _, cx| {
|
||||
if let Some(editor) = this.editor.as_ref().and_then(|editor| editor.upgrade()) {
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.toggle_warnings(&Default::default(), cx);
|
||||
});
|
||||
}
|
||||
})),
|
||||
)
|
||||
h_flex()
|
||||
.when(has_stale_excerpts, |div| {
|
||||
div.child(
|
||||
IconButton::new("update-excerpts", IconName::Update)
|
||||
.icon_color(Color::Info)
|
||||
.disabled(is_updating)
|
||||
.tooltip(move |cx| Tooltip::text("Update excerpts", cx))
|
||||
.on_click(cx.listener(|this, _, cx| {
|
||||
if let Some(editor) =
|
||||
this.editor.as_ref().and_then(|editor| editor.upgrade())
|
||||
{
|
||||
editor.update(cx, |editor, _| {
|
||||
editor.enqueue_update_stale_excerpts(None);
|
||||
});
|
||||
}
|
||||
})),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
IconButton::new("toggle-warnings", IconName::ExclamationTriangle)
|
||||
.tooltip(move |cx| Tooltip::text(tooltip, cx))
|
||||
.on_click(cx.listener(|this, _, cx| {
|
||||
if let Some(editor) =
|
||||
this.editor.as_ref().and_then(|editor| editor.upgrade())
|
||||
{
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.toggle_warnings(&Default::default(), cx);
|
||||
});
|
||||
}
|
||||
})),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -130,7 +130,7 @@ use ui::{
|
||||
Tooltip,
|
||||
};
|
||||
use util::{defer, maybe, post_inc, RangeExt, ResultExt, TryFutureExt};
|
||||
use workspace::item::ItemHandle;
|
||||
use workspace::item::{ItemHandle, PreviewTabsSettings};
|
||||
use workspace::notifications::NotificationId;
|
||||
use workspace::{
|
||||
searchable::SearchEvent, ItemNavHistory, SplitDirection, ViewId, Workspace, WorkspaceId,
|
||||
@@ -278,6 +278,8 @@ pub fn init(cx: &mut AppContext) {
|
||||
});
|
||||
}
|
||||
|
||||
pub struct SearchWithinRange;
|
||||
|
||||
trait InvalidationRegion {
|
||||
fn ranges(&self) -> &[Range<Anchor>];
|
||||
}
|
||||
@@ -1610,6 +1612,7 @@ impl Editor {
|
||||
{
|
||||
workspace.add_item_to_active_pane(
|
||||
Box::new(cx.new_view(|cx| Editor::for_buffer(buffer, Some(project.clone()), cx))),
|
||||
None,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
@@ -3781,7 +3784,7 @@ impl Editor {
|
||||
let project = workspace.project().clone();
|
||||
let editor =
|
||||
cx.new_view(|cx| Editor::for_multibuffer(excerpt_buffer, Some(project), cx));
|
||||
workspace.add_item_to_active_pane(Box::new(editor.clone()), cx);
|
||||
workspace.add_item_to_active_pane(Box::new(editor.clone()), None, cx);
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.highlight_background::<Self>(
|
||||
&ranges_to_highlight,
|
||||
@@ -7526,13 +7529,14 @@ impl Editor {
|
||||
} else {
|
||||
selection.head()
|
||||
};
|
||||
|
||||
let snapshot = self.snapshot(cx);
|
||||
loop {
|
||||
let mut diagnostics = if direction == Direction::Prev {
|
||||
buffer.diagnostics_in_range::<_, usize>(0..search_start, true)
|
||||
} else {
|
||||
buffer.diagnostics_in_range::<_, usize>(search_start..buffer.len(), false)
|
||||
};
|
||||
}
|
||||
.filter(|diagnostic| !snapshot.intersects_fold(diagnostic.range.start));
|
||||
let group = diagnostics.find_map(|entry| {
|
||||
if entry.diagnostic.is_primary
|
||||
&& entry.diagnostic.severity <= DiagnosticSeverity::WARNING
|
||||
@@ -8102,14 +8106,23 @@ impl Editor {
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
let item = Box::new(editor);
|
||||
let item_id = item.item_id();
|
||||
|
||||
if split {
|
||||
workspace.split_item(SplitDirection::Right, item.clone(), cx);
|
||||
} else {
|
||||
workspace.add_item_to_active_pane(item.clone(), cx);
|
||||
let destination_index = workspace.active_pane().update(cx, |pane, cx| {
|
||||
if PreviewTabsSettings::get_global(cx).enable_preview_from_code_navigation {
|
||||
pane.close_current_preview_item(cx)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
workspace.add_item_to_active_pane(item.clone(), destination_index, cx);
|
||||
}
|
||||
workspace.active_pane().clone().update(cx, |pane, cx| {
|
||||
let item_id = item.item_id();
|
||||
workspace.active_pane().update(cx, |pane, cx| {
|
||||
pane.set_preview_item_id(Some(item_id), cx);
|
||||
});
|
||||
}
|
||||
@@ -8206,9 +8219,13 @@ impl Editor {
|
||||
cursor_offset_in_rename_range_end..cursor_offset_in_rename_range
|
||||
}
|
||||
};
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.select_ranges([rename_selection_range]);
|
||||
});
|
||||
if rename_selection_range.end > old_name.len() {
|
||||
editor.select_all(&SelectAll, cx);
|
||||
} else {
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.select_ranges([rename_selection_range]);
|
||||
});
|
||||
}
|
||||
editor
|
||||
});
|
||||
|
||||
@@ -8479,6 +8496,7 @@ impl Editor {
|
||||
|
||||
fn activate_diagnostics(&mut self, group_id: usize, cx: &mut ViewContext<Self>) -> bool {
|
||||
self.dismiss_diagnostics(cx);
|
||||
let snapshot = self.snapshot(cx);
|
||||
self.active_diagnostics = self.display_map.update(cx, |display_map, cx| {
|
||||
let buffer = self.buffer.read(cx).snapshot(cx);
|
||||
|
||||
@@ -8487,7 +8505,13 @@ impl Editor {
|
||||
let mut group_end = Point::zero();
|
||||
let diagnostic_group = buffer
|
||||
.diagnostic_group::<Point>(group_id)
|
||||
.map(|entry| {
|
||||
.filter_map(|entry| {
|
||||
if snapshot.is_line_folded(entry.range.start.row)
|
||||
&& (entry.range.start.row == entry.range.end.row
|
||||
|| snapshot.is_line_folded(entry.range.end.row))
|
||||
{
|
||||
return None;
|
||||
}
|
||||
if entry.range.end > group_end {
|
||||
group_end = entry.range.end;
|
||||
}
|
||||
@@ -8495,7 +8519,7 @@ impl Editor {
|
||||
primary_range = Some(entry.range.clone());
|
||||
primary_message = Some(entry.diagnostic.message.clone());
|
||||
}
|
||||
entry
|
||||
Some(entry)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let primary_range = primary_range?;
|
||||
@@ -8724,6 +8748,18 @@ impl Editor {
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
|
||||
if let Some(active_diagnostics) = self.active_diagnostics.take() {
|
||||
// Clear diagnostics block when folding a range that contains it.
|
||||
let snapshot = self.snapshot(cx);
|
||||
if snapshot.intersects_fold(active_diagnostics.primary_range.start) {
|
||||
drop(snapshot);
|
||||
self.active_diagnostics = Some(active_diagnostics);
|
||||
self.dismiss_diagnostics(cx);
|
||||
} else {
|
||||
self.active_diagnostics = Some(active_diagnostics);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9230,6 +9266,18 @@ impl Editor {
|
||||
)
|
||||
}
|
||||
|
||||
pub fn set_search_within_ranges(
|
||||
&mut self,
|
||||
ranges: &[Range<Anchor>],
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.highlight_background::<SearchWithinRange>(
|
||||
ranges,
|
||||
|colors| colors.editor_document_highlight_read_background,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn highlight_background<T: 'static>(
|
||||
&mut self,
|
||||
ranges: &[Range<Anchor>],
|
||||
@@ -10336,7 +10384,7 @@ impl Render for Editor {
|
||||
EditorMode::SingleLine | EditorMode::AutoHeight { .. } => TextStyle {
|
||||
color: cx.theme().colors().editor_foreground,
|
||||
font_family: settings.ui_font.family.clone(),
|
||||
font_features: settings.ui_font.features,
|
||||
font_features: settings.ui_font.features.clone(),
|
||||
font_size: rems(0.875).into(),
|
||||
font_weight: FontWeight::NORMAL,
|
||||
font_style: FontStyle::Normal,
|
||||
@@ -10349,7 +10397,7 @@ impl Render for Editor {
|
||||
EditorMode::Full => TextStyle {
|
||||
color: cx.theme().colors().editor_foreground,
|
||||
font_family: settings.buffer_font.family.clone(),
|
||||
font_features: settings.buffer_font.features,
|
||||
font_features: settings.buffer_font.features.clone(),
|
||||
font_size: settings.buffer_font_size(cx).into(),
|
||||
font_weight: FontWeight::NORMAL,
|
||||
font_style: FontStyle::Normal,
|
||||
@@ -10774,7 +10822,7 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, _is_valid: bool) -> Ren
|
||||
let theme_settings = ThemeSettings::get_global(cx);
|
||||
text_style.font_family = theme_settings.buffer_font.family.clone();
|
||||
text_style.font_style = theme_settings.buffer_font.style;
|
||||
text_style.font_features = theme_settings.buffer_font.features;
|
||||
text_style.font_features = theme_settings.buffer_font.features.clone();
|
||||
text_style.font_weight = theme_settings.buffer_font.weight;
|
||||
|
||||
let multi_line_diagnostic = diagnostic.message.contains('\n');
|
||||
|
||||
@@ -61,6 +61,7 @@ pub struct Scrollbar {
|
||||
pub selected_symbol: bool,
|
||||
pub search_results: bool,
|
||||
pub diagnostics: bool,
|
||||
pub cursors: bool,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
|
||||
@@ -206,6 +207,10 @@ pub struct ScrollbarContent {
|
||||
///
|
||||
/// Default: true
|
||||
pub diagnostics: Option<bool>,
|
||||
/// Whether to show cursor positions in the scrollbar.
|
||||
///
|
||||
/// Default: true
|
||||
pub cursors: Option<bool>,
|
||||
}
|
||||
|
||||
/// Gutter related settings
|
||||
|
||||
@@ -9161,7 +9161,7 @@ async fn test_mutlibuffer_in_navigation_history(cx: &mut gpui::TestAppContext) {
|
||||
workspace.active_item(cx).is_none(),
|
||||
"active item should be None before the first item is added"
|
||||
);
|
||||
workspace.add_item_to_active_pane(Box::new(multi_buffer_editor.clone()), cx);
|
||||
workspace.add_item_to_active_pane(Box::new(multi_buffer_editor.clone()), None, cx);
|
||||
let active_item = workspace
|
||||
.active_item(cx)
|
||||
.expect("should have an active item after adding the multi buffer");
|
||||
|
||||
@@ -18,16 +18,18 @@ use crate::{
|
||||
SelectPhase, Selection, SoftWrap, ToPoint, CURSORS_VISIBLE_FOR, MAX_LINE_LEN,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use client::ParticipantIndex;
|
||||
use collections::{BTreeMap, HashMap};
|
||||
use git::{blame::BlameEntry, diff::DiffHunkStatus, Oid};
|
||||
use gpui::{
|
||||
anchored, deferred, div, fill, outline, point, px, quad, relative, size, svg,
|
||||
transparent_black, Action, AnchorCorner, AnyElement, AvailableSpace, Bounds, ClipboardItem,
|
||||
ContentMask, Corners, CursorStyle, DispatchPhase, Edges, Element, ElementInputHandler, Entity,
|
||||
Hitbox, Hsla, InteractiveElement, IntoElement, ModifiersChangedEvent, MouseButton,
|
||||
MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, ParentElement, Pixels, ScrollDelta,
|
||||
ScrollWheelEvent, ShapedLine, SharedString, Size, Stateful, StatefulInteractiveElement, Style,
|
||||
Styled, TextRun, TextStyle, TextStyleRefinement, View, ViewContext, WeakView, WindowContext,
|
||||
GlobalElementId, Hitbox, Hsla, InteractiveElement, IntoElement, ModifiersChangedEvent,
|
||||
MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, ParentElement, Pixels,
|
||||
ScrollDelta, ScrollWheelEvent, ShapedLine, SharedString, Size, Stateful,
|
||||
StatefulInteractiveElement, Style, Styled, TextRun, TextStyle, TextStyleRefinement, View,
|
||||
ViewContext, WeakView, WindowContext,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use language::language_settings::ShowWhitespaceSetting;
|
||||
@@ -45,7 +47,7 @@ use std::{
|
||||
cmp::{self, max, Ordering},
|
||||
fmt::Write,
|
||||
iter, mem,
|
||||
ops::Range,
|
||||
ops::{Deref, Range},
|
||||
sync::Arc,
|
||||
};
|
||||
use sum_tree::Bias;
|
||||
@@ -89,7 +91,10 @@ impl SelectionLayout {
|
||||
}
|
||||
|
||||
// any vim visual mode (including line mode)
|
||||
if cursor_shape == CursorShape::Block && !range.is_empty() && !selection.reversed {
|
||||
if (cursor_shape == CursorShape::Block || cursor_shape == CursorShape::Hollow)
|
||||
&& !range.is_empty()
|
||||
&& !selection.reversed
|
||||
{
|
||||
if head.column() > 0 {
|
||||
head = map.clip_point(DisplayPoint::new(head.row(), head.column() - 1), Bias::Left)
|
||||
} else if head.row() > 0 && head != map.max_point() {
|
||||
@@ -767,13 +772,7 @@ impl EditorElement {
|
||||
collaboration_hub.as_ref(),
|
||||
cx,
|
||||
) {
|
||||
let selection_style = if let Some(participant_index) = selection.participant_index {
|
||||
cx.theme()
|
||||
.players()
|
||||
.color_for_participant(participant_index.0)
|
||||
} else {
|
||||
cx.theme().players().absent()
|
||||
};
|
||||
let selection_style = Self::get_participant_color(selection.participant_index, cx);
|
||||
|
||||
// Don't re-render the leader's selections, since the local selections
|
||||
// match theirs.
|
||||
@@ -872,8 +871,42 @@ impl EditorElement {
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn collect_cursors(
|
||||
&self,
|
||||
snapshot: &EditorSnapshot,
|
||||
cx: &mut WindowContext,
|
||||
) -> Vec<(Anchor, Hsla)> {
|
||||
let editor = self.editor.read(cx);
|
||||
let mut cursors = Vec::<(Anchor, Hsla)>::new();
|
||||
let mut skip_local = false;
|
||||
// Remote cursors
|
||||
if let Some(collaboration_hub) = &editor.collaboration_hub {
|
||||
for remote_selection in snapshot.remote_selections_in_range(
|
||||
&(Anchor::min()..Anchor::max()),
|
||||
collaboration_hub.deref(),
|
||||
cx,
|
||||
) {
|
||||
let color = Self::get_participant_color(remote_selection.participant_index, cx);
|
||||
cursors.push((remote_selection.selection.head(), color.cursor));
|
||||
if Some(remote_selection.peer_id) == editor.leader_peer_id {
|
||||
skip_local = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Local cursors
|
||||
if !skip_local {
|
||||
editor.selections.disjoint.iter().for_each(|selection| {
|
||||
cursors.push((selection.head(), cx.theme().players().local().cursor));
|
||||
});
|
||||
if let Some(ref selection) = editor.selections.pending_anchor() {
|
||||
cursors.push((selection.head(), cx.theme().players().local().cursor));
|
||||
}
|
||||
}
|
||||
cursors
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn layout_cursors(
|
||||
fn layout_visible_cursors(
|
||||
&self,
|
||||
snapshot: &EditorSnapshot,
|
||||
selections: &[(PlayerColor, Vec<SelectionLayout>)],
|
||||
@@ -887,16 +920,21 @@ impl EditorElement {
|
||||
em_width: Pixels,
|
||||
autoscroll_containing_element: bool,
|
||||
cx: &mut WindowContext,
|
||||
) -> Vec<CursorLayout> {
|
||||
) -> (Vec<CursorLayout>, bool) {
|
||||
let mut autoscroll_bounds = None;
|
||||
let mut non_visible_cursors = false;
|
||||
let cursor_layouts = self.editor.update(cx, |editor, cx| {
|
||||
let mut cursors = Vec::new();
|
||||
for (player_color, selections) in selections {
|
||||
for selection in selections {
|
||||
let cursor_position = selection.head;
|
||||
if (selection.is_local && !editor.show_local_cursors(cx))
|
||||
|| !visible_display_row_range.contains(&cursor_position.row())
|
||||
{
|
||||
|
||||
let in_range = visible_display_row_range.contains(&cursor_position.row());
|
||||
if !in_range {
|
||||
non_visible_cursors |= true;
|
||||
}
|
||||
|
||||
if (selection.is_local && !editor.show_local_cursors(cx)) || !in_range {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1003,7 +1041,7 @@ impl EditorElement {
|
||||
cx.request_autoscroll(bounds);
|
||||
}
|
||||
|
||||
cursor_layouts
|
||||
(cursor_layouts, non_visible_cursors)
|
||||
}
|
||||
|
||||
fn layout_scrollbar(
|
||||
@@ -1012,6 +1050,7 @@ impl EditorElement {
|
||||
bounds: Bounds<Pixels>,
|
||||
scroll_position: gpui::Point<f32>,
|
||||
rows_per_page: f32,
|
||||
non_visible_cursors: bool,
|
||||
cx: &mut WindowContext,
|
||||
) -> Option<ScrollbarLayout> {
|
||||
let scrollbar_settings = EditorSettings::get_global(cx).scrollbar;
|
||||
@@ -1031,6 +1070,9 @@ impl EditorElement {
|
||||
// Diagnostics
|
||||
(is_singleton && scrollbar_settings.diagnostics && snapshot.buffer_snapshot.has_diagnostics())
|
||||
||
|
||||
// Cursors out of sight
|
||||
non_visible_cursors
|
||||
||
|
||||
// Scrollmanager
|
||||
editor.scroll_manager.scrollbars_visible()
|
||||
}
|
||||
@@ -1320,9 +1362,20 @@ impl EditorElement {
|
||||
Some(button)
|
||||
}
|
||||
|
||||
fn get_participant_color(
|
||||
participant_index: Option<ParticipantIndex>,
|
||||
cx: &WindowContext,
|
||||
) -> PlayerColor {
|
||||
if let Some(index) = participant_index {
|
||||
cx.theme().players().color_for_participant(index.0)
|
||||
} else {
|
||||
cx.theme().players().absent()
|
||||
}
|
||||
}
|
||||
|
||||
fn calculate_relative_line_numbers(
|
||||
&self,
|
||||
buffer_rows: Vec<Option<u32>>,
|
||||
snapshot: &EditorSnapshot,
|
||||
rows: &Range<u32>,
|
||||
relative_to: Option<u32>,
|
||||
) -> HashMap<u32, u32> {
|
||||
@@ -1332,6 +1385,12 @@ impl EditorElement {
|
||||
};
|
||||
|
||||
let start = rows.start.min(relative_to);
|
||||
let end = rows.end.max(relative_to);
|
||||
|
||||
let buffer_rows = snapshot
|
||||
.buffer_rows(start)
|
||||
.take(1 + (end - start) as usize)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let head_idx = relative_to - start;
|
||||
let mut delta = 1;
|
||||
@@ -1406,9 +1465,7 @@ impl EditorElement {
|
||||
None
|
||||
};
|
||||
|
||||
let buffer_rows = buffer_rows.collect::<Vec<_>>();
|
||||
let relative_rows =
|
||||
self.calculate_relative_line_numbers(buffer_rows.clone(), &rows, relative_to);
|
||||
let relative_rows = self.calculate_relative_line_numbers(snapshot, &rows, relative_to);
|
||||
|
||||
for (ix, row) in buffer_rows.into_iter().enumerate() {
|
||||
let display_row = rows.start + ix as u32;
|
||||
@@ -2223,7 +2280,7 @@ impl EditorElement {
|
||||
}
|
||||
|
||||
cx.paint_layer(layout.gutter_hitbox.bounds, |cx| {
|
||||
cx.with_element_id(Some("gutter_fold_indicators"), |cx| {
|
||||
cx.with_element_namespace("gutter_fold_indicators", |cx| {
|
||||
for fold_indicator in layout.fold_indicators.iter_mut().flatten() {
|
||||
fold_indicator.paint(cx);
|
||||
}
|
||||
@@ -2372,7 +2429,7 @@ impl EditorElement {
|
||||
};
|
||||
cx.set_cursor_style(cursor_style, &layout.text_hitbox);
|
||||
|
||||
cx.with_element_id(Some("folds"), |cx| self.paint_folds(layout, cx));
|
||||
cx.with_element_namespace("folds", |cx| self.paint_folds(layout, cx));
|
||||
let invisible_display_ranges = self.paint_highlights(layout, cx);
|
||||
self.paint_lines(&invisible_display_ranges, layout, cx);
|
||||
self.paint_redactions(layout, cx);
|
||||
@@ -2475,7 +2532,7 @@ impl EditorElement {
|
||||
}
|
||||
|
||||
fn paint_cursors(&mut self, layout: &mut EditorLayout, cx: &mut WindowContext) {
|
||||
for cursor in &mut layout.cursors {
|
||||
for cursor in &mut layout.visible_cursors {
|
||||
cursor.paint(layout.content_origin, cx);
|
||||
}
|
||||
}
|
||||
@@ -2501,11 +2558,13 @@ impl EditorElement {
|
||||
cx.theme().colors().scrollbar_track_border,
|
||||
));
|
||||
|
||||
// Refresh scrollbar markers in the background. Below, we paint whatever markers have already been computed.
|
||||
self.refresh_scrollbar_markers(layout, scrollbar_layout, cx);
|
||||
let fast_markers =
|
||||
self.collect_fast_scrollbar_markers(layout, scrollbar_layout, cx);
|
||||
// Refresh slow scrollbar markers in the background. Below, we paint whatever markers have already been computed.
|
||||
self.refresh_slow_scrollbar_markers(layout, scrollbar_layout, cx);
|
||||
|
||||
let markers = self.editor.read(cx).scrollbar_marker_state.markers.clone();
|
||||
for marker in markers.iter() {
|
||||
for marker in markers.iter().chain(&fast_markers) {
|
||||
let mut marker = marker.clone();
|
||||
marker.bounds.origin += scrollbar_layout.hitbox.origin;
|
||||
cx.paint_quad(marker);
|
||||
@@ -2612,7 +2671,34 @@ impl EditorElement {
|
||||
}
|
||||
}
|
||||
|
||||
fn refresh_scrollbar_markers(
|
||||
fn collect_fast_scrollbar_markers(
|
||||
&self,
|
||||
layout: &EditorLayout,
|
||||
scrollbar_layout: &ScrollbarLayout,
|
||||
cx: &mut WindowContext,
|
||||
) -> Vec<PaintQuad> {
|
||||
const LIMIT: usize = 100;
|
||||
if !EditorSettings::get_global(cx).scrollbar.cursors || layout.cursors.len() > LIMIT {
|
||||
return vec![];
|
||||
}
|
||||
let cursor_ranges = layout
|
||||
.cursors
|
||||
.iter()
|
||||
.map(|cursor| {
|
||||
let point = cursor
|
||||
.0
|
||||
.to_display_point(&layout.position_map.snapshot.display_snapshot);
|
||||
ColoredRange {
|
||||
start: point.row(),
|
||||
end: point.row(),
|
||||
color: cursor.1,
|
||||
}
|
||||
})
|
||||
.collect_vec();
|
||||
scrollbar_layout.marker_quads_for_ranges(cursor_ranges, None)
|
||||
}
|
||||
|
||||
fn refresh_slow_scrollbar_markers(
|
||||
&self,
|
||||
layout: &EditorLayout,
|
||||
scrollbar_layout: &ScrollbarLayout,
|
||||
@@ -2672,7 +2758,8 @@ impl EditorElement {
|
||||
});
|
||||
|
||||
marker_quads.extend(
|
||||
scrollbar_layout.marker_quads_for_ranges(marker_row_ranges, 0),
|
||||
scrollbar_layout
|
||||
.marker_quads_for_ranges(marker_row_ranges, Some(0)),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2688,6 +2775,10 @@ impl EditorElement {
|
||||
if (is_search_highlights && scrollbar_settings.search_results)
|
||||
|| (is_symbol_occurrences && scrollbar_settings.selected_symbol)
|
||||
{
|
||||
let mut color = theme.status().info;
|
||||
if is_symbol_occurrences {
|
||||
color.fade_out(0.5);
|
||||
}
|
||||
let marker_row_ranges =
|
||||
background_ranges.into_iter().map(|range| {
|
||||
let display_start = range
|
||||
@@ -2699,12 +2790,12 @@ impl EditorElement {
|
||||
ColoredRange {
|
||||
start: display_start.row(),
|
||||
end: display_end.row(),
|
||||
color: theme.status().info,
|
||||
color,
|
||||
}
|
||||
});
|
||||
marker_quads.extend(
|
||||
scrollbar_layout
|
||||
.marker_quads_for_ranges(marker_row_ranges, 1),
|
||||
.marker_quads_for_ranges(marker_row_ranges, Some(1)),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2746,7 +2837,8 @@ impl EditorElement {
|
||||
}
|
||||
});
|
||||
marker_quads.extend(
|
||||
scrollbar_layout.marker_quads_for_ranges(marker_row_ranges, 2),
|
||||
scrollbar_layout
|
||||
.marker_quads_for_ranges(marker_row_ranges, Some(2)),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3364,7 +3456,15 @@ impl Element for EditorElement {
|
||||
type RequestLayoutState = ();
|
||||
type PrepaintState = EditorLayout;
|
||||
|
||||
fn request_layout(&mut self, cx: &mut WindowContext) -> (gpui::LayoutId, ()) {
|
||||
fn id(&self) -> Option<ElementId> {
|
||||
None
|
||||
}
|
||||
|
||||
fn request_layout(
|
||||
&mut self,
|
||||
_: Option<&GlobalElementId>,
|
||||
cx: &mut WindowContext,
|
||||
) -> (gpui::LayoutId, ()) {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.set_style(self.style.clone(), cx);
|
||||
|
||||
@@ -3408,6 +3508,7 @@ impl Element for EditorElement {
|
||||
|
||||
fn prepaint(
|
||||
&mut self,
|
||||
_: Option<&GlobalElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
_: &mut Self::RequestLayoutState,
|
||||
cx: &mut WindowContext,
|
||||
@@ -3511,10 +3612,10 @@ impl Element for EditorElement {
|
||||
let start_row = scroll_position.y as u32;
|
||||
let height_in_lines = bounds.size.height / line_height;
|
||||
let max_row = snapshot.max_point().row();
|
||||
|
||||
// Add 1 to ensure selections bleed off screen
|
||||
let end_row =
|
||||
1 + cmp::min((scroll_position.y + height_in_lines).ceil() as u32, max_row);
|
||||
let end_row = cmp::min(
|
||||
(scroll_position.y + height_in_lines).ceil() as u32,
|
||||
max_row + 1,
|
||||
);
|
||||
|
||||
let buffer_rows = snapshot
|
||||
.buffer_rows(start_row)
|
||||
@@ -3584,19 +3685,22 @@ impl Element for EditorElement {
|
||||
.width;
|
||||
let mut scroll_width =
|
||||
longest_line_width.max(max_visible_line_width) + overscroll.width;
|
||||
let mut blocks = self.build_blocks(
|
||||
start_row..end_row,
|
||||
&snapshot,
|
||||
&hitbox,
|
||||
&text_hitbox,
|
||||
&mut scroll_width,
|
||||
&gutter_dimensions,
|
||||
em_width,
|
||||
gutter_dimensions.width + gutter_dimensions.margin,
|
||||
line_height,
|
||||
&line_layouts,
|
||||
cx,
|
||||
);
|
||||
|
||||
let mut blocks = cx.with_element_namespace("blocks", |cx| {
|
||||
self.build_blocks(
|
||||
start_row..end_row,
|
||||
&snapshot,
|
||||
&hitbox,
|
||||
&text_hitbox,
|
||||
&mut scroll_width,
|
||||
&gutter_dimensions,
|
||||
em_width,
|
||||
gutter_dimensions.width + gutter_dimensions.margin,
|
||||
line_height,
|
||||
&line_layouts,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let scroll_pixel_position = point(
|
||||
scroll_position.x * em_width,
|
||||
@@ -3658,7 +3762,7 @@ impl Element for EditorElement {
|
||||
}
|
||||
});
|
||||
|
||||
cx.with_element_id(Some("blocks"), |cx| {
|
||||
cx.with_element_namespace("blocks", |cx| {
|
||||
self.layout_blocks(
|
||||
&mut blocks,
|
||||
&hitbox,
|
||||
@@ -3668,7 +3772,9 @@ impl Element for EditorElement {
|
||||
);
|
||||
});
|
||||
|
||||
let cursors = self.layout_cursors(
|
||||
let cursors = self.collect_cursors(&snapshot, cx);
|
||||
|
||||
let (visible_cursors, non_visible_cursors) = self.layout_visible_cursors(
|
||||
&snapshot,
|
||||
&selections,
|
||||
start_row..end_row,
|
||||
@@ -3683,10 +3789,16 @@ impl Element for EditorElement {
|
||||
cx,
|
||||
);
|
||||
|
||||
let scrollbar_layout =
|
||||
self.layout_scrollbar(&snapshot, bounds, scroll_position, height_in_lines, cx);
|
||||
let scrollbar_layout = self.layout_scrollbar(
|
||||
&snapshot,
|
||||
bounds,
|
||||
scroll_position,
|
||||
height_in_lines,
|
||||
non_visible_cursors,
|
||||
cx,
|
||||
);
|
||||
|
||||
let folds = cx.with_element_id(Some("folds"), |cx| {
|
||||
let folds = cx.with_element_namespace("folds", |cx| {
|
||||
self.layout_folds(
|
||||
&snapshot,
|
||||
content_origin,
|
||||
@@ -3747,7 +3859,7 @@ impl Element for EditorElement {
|
||||
let mouse_context_menu = self.layout_mouse_context_menu(cx);
|
||||
|
||||
let fold_indicators = if gutter_settings.folds {
|
||||
cx.with_element_id(Some("gutter_fold_indicators"), |cx| {
|
||||
cx.with_element_namespace("gutter_fold_indicators", |cx| {
|
||||
self.layout_gutter_fold_indicators(
|
||||
fold_statuses,
|
||||
line_height,
|
||||
@@ -3826,6 +3938,7 @@ impl Element for EditorElement {
|
||||
folds,
|
||||
blocks,
|
||||
cursors,
|
||||
visible_cursors,
|
||||
selections,
|
||||
mouse_context_menu,
|
||||
code_actions_indicator,
|
||||
@@ -3839,6 +3952,7 @@ impl Element for EditorElement {
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
_: Option<&GlobalElementId>,
|
||||
bounds: Bounds<gpui::Pixels>,
|
||||
_: &mut Self::RequestLayoutState,
|
||||
layout: &mut Self::PrepaintState,
|
||||
@@ -3871,7 +3985,7 @@ impl Element for EditorElement {
|
||||
self.paint_text(layout, cx);
|
||||
|
||||
if !layout.blocks.is_empty() {
|
||||
cx.with_element_id(Some("blocks"), |cx| {
|
||||
cx.with_element_namespace("blocks", |cx| {
|
||||
self.paint_blocks(layout, cx);
|
||||
});
|
||||
}
|
||||
@@ -3914,7 +4028,8 @@ pub struct EditorLayout {
|
||||
blocks: Vec<BlockLayout>,
|
||||
highlighted_ranges: Vec<(Range<DisplayPoint>, Hsla)>,
|
||||
redacted_ranges: Vec<Range<DisplayPoint>>,
|
||||
cursors: Vec<CursorLayout>,
|
||||
cursors: Vec<(Anchor, Hsla)>,
|
||||
visible_cursors: Vec<CursorLayout>,
|
||||
selections: Vec<(PlayerColor, Vec<SelectionLayout>)>,
|
||||
max_row: u32,
|
||||
code_actions_indicator: Option<AnyElement>,
|
||||
@@ -3947,7 +4062,8 @@ struct ScrollbarLayout {
|
||||
|
||||
impl ScrollbarLayout {
|
||||
const BORDER_WIDTH: Pixels = px(1.0);
|
||||
const MIN_MARKER_HEIGHT: Pixels = px(2.0);
|
||||
const LINE_MARKER_HEIGHT: Pixels = px(2.0);
|
||||
const MIN_MARKER_HEIGHT: Pixels = px(5.0);
|
||||
const MIN_THUMB_HEIGHT: Pixels = px(20.0);
|
||||
|
||||
fn thumb_bounds(&self) -> Bounds<Pixels> {
|
||||
@@ -3966,19 +4082,43 @@ impl ScrollbarLayout {
|
||||
fn marker_quads_for_ranges(
|
||||
&self,
|
||||
row_ranges: impl IntoIterator<Item = ColoredRange<u32>>,
|
||||
column: usize,
|
||||
column: Option<usize>,
|
||||
) -> Vec<PaintQuad> {
|
||||
let column_width =
|
||||
px(((self.hitbox.size.width - ScrollbarLayout::BORDER_WIDTH).0 / 3.0).floor());
|
||||
struct MinMax {
|
||||
min: Pixels,
|
||||
max: Pixels,
|
||||
}
|
||||
let (x_range, height_limit) = if let Some(column) = column {
|
||||
let column_width = px(((self.hitbox.size.width - Self::BORDER_WIDTH).0 / 3.0).floor());
|
||||
let start = Self::BORDER_WIDTH + (column as f32 * column_width);
|
||||
let end = start + column_width;
|
||||
(
|
||||
Range { start, end },
|
||||
MinMax {
|
||||
min: Self::MIN_MARKER_HEIGHT,
|
||||
max: px(f32::MAX),
|
||||
},
|
||||
)
|
||||
} else {
|
||||
(
|
||||
Range {
|
||||
start: Self::BORDER_WIDTH,
|
||||
end: self.hitbox.size.width,
|
||||
},
|
||||
MinMax {
|
||||
min: Self::LINE_MARKER_HEIGHT,
|
||||
max: Self::LINE_MARKER_HEIGHT,
|
||||
},
|
||||
)
|
||||
};
|
||||
|
||||
let left_x = ScrollbarLayout::BORDER_WIDTH + (column as f32 * column_width);
|
||||
let right_x = left_x + column_width;
|
||||
|
||||
let mut background_pixel_ranges = row_ranges
|
||||
let row_to_y = |row: u32| row as f32 * self.row_height;
|
||||
let mut pixel_ranges = row_ranges
|
||||
.into_iter()
|
||||
.map(|range| {
|
||||
let start_y = range.start as f32 * self.row_height;
|
||||
let end_y = (range.end + 1) as f32 * self.row_height;
|
||||
let start_y = row_to_y(range.start);
|
||||
let end_y = row_to_y(range.end)
|
||||
+ self.row_height.max(height_limit.min).min(height_limit.max);
|
||||
ColoredRange {
|
||||
start: start_y,
|
||||
end: end_y,
|
||||
@@ -3988,24 +4128,21 @@ impl ScrollbarLayout {
|
||||
.peekable();
|
||||
|
||||
let mut quads = Vec::new();
|
||||
while let Some(mut pixel_range) = background_pixel_ranges.next() {
|
||||
pixel_range.end = pixel_range
|
||||
.end
|
||||
.max(pixel_range.start + Self::MIN_MARKER_HEIGHT);
|
||||
while let Some(next_pixel_range) = background_pixel_ranges.peek() {
|
||||
if pixel_range.end >= next_pixel_range.start
|
||||
while let Some(mut pixel_range) = pixel_ranges.next() {
|
||||
while let Some(next_pixel_range) = pixel_ranges.peek() {
|
||||
if pixel_range.end >= next_pixel_range.start - px(1.0)
|
||||
&& pixel_range.color == next_pixel_range.color
|
||||
{
|
||||
pixel_range.end = next_pixel_range.end.max(pixel_range.end);
|
||||
background_pixel_ranges.next();
|
||||
pixel_ranges.next();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let bounds = Bounds::from_corners(
|
||||
point(left_x, pixel_range.start),
|
||||
point(right_x, pixel_range.end),
|
||||
point(x_range.start, pixel_range.start),
|
||||
point(x_range.end, pixel_range.end),
|
||||
);
|
||||
quads.push(quad(
|
||||
bounds,
|
||||
@@ -4445,8 +4582,12 @@ mod tests {
|
||||
.unwrap();
|
||||
assert_eq!(layouts.len(), 6);
|
||||
|
||||
let relative_rows =
|
||||
element.calculate_relative_line_numbers((0..6).map(Some).collect(), &(0..6), Some(3));
|
||||
let relative_rows = window
|
||||
.update(cx, |editor, cx| {
|
||||
let snapshot = editor.snapshot(cx);
|
||||
element.calculate_relative_line_numbers(&snapshot, &(0..6), Some(3))
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(relative_rows[&0], 3);
|
||||
assert_eq!(relative_rows[&1], 2);
|
||||
assert_eq!(relative_rows[&2], 1);
|
||||
@@ -4455,16 +4596,24 @@ mod tests {
|
||||
assert_eq!(relative_rows[&5], 2);
|
||||
|
||||
// works if cursor is before screen
|
||||
let relative_rows =
|
||||
element.calculate_relative_line_numbers((0..6).map(Some).collect(), &(3..6), Some(1));
|
||||
let relative_rows = window
|
||||
.update(cx, |editor, cx| {
|
||||
let snapshot = editor.snapshot(cx);
|
||||
element.calculate_relative_line_numbers(&snapshot, &(3..6), Some(1))
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(relative_rows.len(), 3);
|
||||
assert_eq!(relative_rows[&3], 2);
|
||||
assert_eq!(relative_rows[&4], 3);
|
||||
assert_eq!(relative_rows[&5], 4);
|
||||
|
||||
// works if cursor is after screen
|
||||
let relative_rows =
|
||||
element.calculate_relative_line_numbers((0..6).map(Some).collect(), &(0..3), Some(6));
|
||||
let relative_rows = window
|
||||
.update(cx, |editor, cx| {
|
||||
let snapshot = editor.snapshot(cx);
|
||||
element.calculate_relative_line_numbers(&snapshot, &(0..3), Some(6))
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(relative_rows.len(), 3);
|
||||
assert_eq!(relative_rows[&0], 5);
|
||||
assert_eq!(relative_rows[&1], 4);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::{
|
||||
editor_settings::SeedQuerySetting, persistence::DB, scroll::ScrollAnchor, Anchor, Autoscroll,
|
||||
Editor, EditorEvent, EditorSettings, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot,
|
||||
NavigationData, ToPoint as _,
|
||||
NavigationData, SearchWithinRange, ToPoint as _,
|
||||
};
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use collections::HashSet;
|
||||
@@ -16,17 +16,19 @@ use language::{
|
||||
proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, CharKind, OffsetRangeExt,
|
||||
Point, SelectionGoal,
|
||||
};
|
||||
use multi_buffer::AnchorRangeExt;
|
||||
use project::{search::SearchQuery, FormatTrigger, Item as _, Project, ProjectPath};
|
||||
use rpc::proto::{self, update_view, PeerId};
|
||||
use settings::Settings;
|
||||
use workspace::item::{ItemSettings, TabContentParams};
|
||||
|
||||
use std::{
|
||||
any::TypeId,
|
||||
borrow::Cow,
|
||||
cmp::{self, Ordering},
|
||||
iter,
|
||||
ops::Range,
|
||||
path::{Path, PathBuf},
|
||||
path::Path,
|
||||
sync::Arc,
|
||||
};
|
||||
use text::{BufferId, Selection};
|
||||
@@ -750,7 +752,7 @@ impl Item for Editor {
|
||||
fn save_as(
|
||||
&mut self,
|
||||
project: Model<Project>,
|
||||
abs_path: PathBuf,
|
||||
path: ProjectPath,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let buffer = self
|
||||
@@ -759,14 +761,13 @@ impl Item for Editor {
|
||||
.as_singleton()
|
||||
.expect("cannot call save_as on an excerpt list");
|
||||
|
||||
let file_extension = abs_path
|
||||
let file_extension = path
|
||||
.path
|
||||
.extension()
|
||||
.map(|a| a.to_string_lossy().to_string());
|
||||
self.report_editor_event("save", file_extension, cx);
|
||||
|
||||
project.update(cx, |project, cx| {
|
||||
project.save_buffer_as(buffer, abs_path, cx)
|
||||
})
|
||||
project.update(cx, |project, cx| project.save_buffer_as(buffer, path, cx))
|
||||
}
|
||||
|
||||
fn reload(&mut self, project: Model<Project>, cx: &mut ViewContext<Self>) -> Task<Result<()>> {
|
||||
@@ -1000,6 +1001,10 @@ impl SearchableItem for Editor {
|
||||
);
|
||||
}
|
||||
|
||||
fn has_filtered_search_ranges(&mut self) -> bool {
|
||||
self.has_background_highlights::<SearchWithinRange>()
|
||||
}
|
||||
|
||||
fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> String {
|
||||
let setting = EditorSettings::get_global(cx).seed_search_query_from_cursor;
|
||||
let snapshot = &self.snapshot(cx).buffer_snapshot;
|
||||
@@ -1124,18 +1129,37 @@ impl SearchableItem for Editor {
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Task<Vec<Range<Anchor>>> {
|
||||
let buffer = self.buffer().read(cx).snapshot(cx);
|
||||
let search_within_ranges = self
|
||||
.background_highlights
|
||||
.get(&TypeId::of::<SearchWithinRange>())
|
||||
.map(|(_color, ranges)| {
|
||||
ranges
|
||||
.iter()
|
||||
.map(|range| range.to_offset(&buffer))
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
cx.background_executor().spawn(async move {
|
||||
let mut ranges = Vec::new();
|
||||
if let Some((_, _, excerpt_buffer)) = buffer.as_singleton() {
|
||||
ranges.extend(
|
||||
query
|
||||
.search(excerpt_buffer, None)
|
||||
.await
|
||||
.into_iter()
|
||||
.map(|range| {
|
||||
buffer.anchor_after(range.start)..buffer.anchor_before(range.end)
|
||||
}),
|
||||
);
|
||||
if let Some(search_within_ranges) = search_within_ranges {
|
||||
for range in search_within_ranges {
|
||||
let offset = range.start;
|
||||
ranges.extend(
|
||||
query
|
||||
.search(excerpt_buffer, Some(range))
|
||||
.await
|
||||
.into_iter()
|
||||
.map(|range| {
|
||||
buffer.anchor_after(range.start + offset)
|
||||
..buffer.anchor_before(range.end + offset)
|
||||
}),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
ranges.extend(query.search(excerpt_buffer, None).await.into_iter().map(
|
||||
|range| buffer.anchor_after(range.start)..buffer.anchor_before(range.end),
|
||||
));
|
||||
}
|
||||
} else {
|
||||
for excerpt in buffer.excerpt_boundaries_in_range(0..buffer.len()) {
|
||||
let excerpt_range = excerpt.range.context.to_offset(&excerpt.buffer);
|
||||
|
||||
@@ -106,6 +106,7 @@ pub fn expand_macro_recursively(
|
||||
});
|
||||
workspace.add_item_to_active_pane(
|
||||
Box::new(cx.new_view(|cx| Editor::for_multibuffer(buffer, Some(project), cx))),
|
||||
None,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
|
||||
@@ -45,7 +45,7 @@ pub fn init(cx: &mut AppContext) {
|
||||
workspace.activate_item(&existing, cx);
|
||||
} else {
|
||||
let extensions_page = ExtensionsPage::new(workspace, cx);
|
||||
workspace.add_item_to_active_pane(Box::new(extensions_page), cx)
|
||||
workspace.add_item_to_active_pane(Box::new(extensions_page), None, cx)
|
||||
}
|
||||
})
|
||||
.register_action(move |_, _: &InstallDevExtension, cx| {
|
||||
@@ -739,7 +739,7 @@ impl ExtensionsPage {
|
||||
cx.theme().colors().text
|
||||
},
|
||||
font_family: settings.ui_font.family.clone(),
|
||||
font_features: settings.ui_font.features,
|
||||
font_features: settings.ui_font.features.clone(),
|
||||
font_size: rems(0.875).into(),
|
||||
font_weight: FontWeight::NORMAL,
|
||||
font_style: FontStyle::Normal,
|
||||
|
||||
@@ -16,6 +16,7 @@ doctest = false
|
||||
anyhow.workspace = true
|
||||
collections.workspace = true
|
||||
editor.workspace = true
|
||||
futures.workspace = true
|
||||
fuzzy.workspace = true
|
||||
gpui.workspace = true
|
||||
itertools = "0.11"
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#[cfg(test)]
|
||||
mod file_finder_tests;
|
||||
|
||||
mod new_path_prompt;
|
||||
|
||||
use collections::{HashMap, HashSet};
|
||||
use editor::{scroll::Autoscroll, Bias, Editor};
|
||||
use fuzzy::{CharBag, PathMatch, PathMatchCandidate};
|
||||
@@ -10,6 +12,7 @@ use gpui::{
|
||||
ViewContext, VisualContext, WeakView,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use new_path_prompt::NewPathPrompt;
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use project::{PathMatchCandidateSet, Project, ProjectPath, WorktreeId};
|
||||
use settings::Settings;
|
||||
@@ -37,6 +40,7 @@ pub struct FileFinder {
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.observe_new_views(FileFinder::register).detach();
|
||||
cx.observe_new_views(NewPathPrompt::register).detach();
|
||||
}
|
||||
|
||||
impl FileFinder {
|
||||
@@ -454,6 +458,7 @@ impl FileFinderDelegate {
|
||||
.root_entry()
|
||||
.map_or(false, |entry| entry.is_ignored),
|
||||
include_root_name,
|
||||
directories_only: false,
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
463
crates/file_finder/src/new_path_prompt.rs
Normal file
463
crates/file_finder/src/new_path_prompt.rs
Normal file
@@ -0,0 +1,463 @@
|
||||
use futures::channel::oneshot;
|
||||
use fuzzy::PathMatch;
|
||||
use gpui::{HighlightStyle, Model, StyledText};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use project::{Entry, PathMatchCandidateSet, Project, ProjectPath, WorktreeId};
|
||||
use std::{
|
||||
path::PathBuf,
|
||||
sync::{
|
||||
atomic::{self, AtomicBool},
|
||||
Arc,
|
||||
},
|
||||
};
|
||||
use ui::{highlight_ranges, prelude::*, LabelLike, ListItemSpacing};
|
||||
use ui::{ListItem, ViewContext};
|
||||
use util::ResultExt;
|
||||
use workspace::Workspace;
|
||||
|
||||
pub(crate) struct NewPathPrompt;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct Match {
|
||||
path_match: Option<PathMatch>,
|
||||
suffix: Option<String>,
|
||||
}
|
||||
|
||||
impl Match {
|
||||
fn entry<'a>(&'a self, project: &'a Project, cx: &'a WindowContext) -> Option<&'a Entry> {
|
||||
if let Some(suffix) = &self.suffix {
|
||||
let (worktree, path) = if let Some(path_match) = &self.path_match {
|
||||
(
|
||||
project.worktree_for_id(WorktreeId::from_usize(path_match.worktree_id), cx),
|
||||
path_match.path.join(suffix),
|
||||
)
|
||||
} else {
|
||||
(project.worktrees().next(), PathBuf::from(suffix))
|
||||
};
|
||||
|
||||
worktree.and_then(|worktree| worktree.read(cx).entry_for_path(path))
|
||||
} else if let Some(path_match) = &self.path_match {
|
||||
let worktree =
|
||||
project.worktree_for_id(WorktreeId::from_usize(path_match.worktree_id), cx)?;
|
||||
worktree.read(cx).entry_for_path(path_match.path.as_ref())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn is_dir(&self, project: &Project, cx: &WindowContext) -> bool {
|
||||
self.entry(project, cx).is_some_and(|e| e.is_dir())
|
||||
|| self.suffix.as_ref().is_some_and(|s| s.ends_with('/'))
|
||||
}
|
||||
|
||||
fn relative_path(&self) -> String {
|
||||
if let Some(path_match) = &self.path_match {
|
||||
if let Some(suffix) = &self.suffix {
|
||||
format!(
|
||||
"{}/{}",
|
||||
path_match.path.to_string_lossy(),
|
||||
suffix.trim_end_matches('/')
|
||||
)
|
||||
} else {
|
||||
path_match.path.to_string_lossy().to_string()
|
||||
}
|
||||
} else if let Some(suffix) = &self.suffix {
|
||||
suffix.trim_end_matches('/').to_string()
|
||||
} else {
|
||||
"".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn project_path(&self, project: &Project, cx: &WindowContext) -> Option<ProjectPath> {
|
||||
let worktree_id = if let Some(path_match) = &self.path_match {
|
||||
WorktreeId::from_usize(path_match.worktree_id)
|
||||
} else {
|
||||
project.worktrees().next()?.read(cx).id()
|
||||
};
|
||||
|
||||
let path = PathBuf::from(self.relative_path());
|
||||
|
||||
Some(ProjectPath {
|
||||
worktree_id,
|
||||
path: Arc::from(path),
|
||||
})
|
||||
}
|
||||
|
||||
fn existing_prefix(&self, project: &Project, cx: &WindowContext) -> Option<PathBuf> {
|
||||
let worktree = project.worktrees().next()?.read(cx);
|
||||
let mut prefix = PathBuf::new();
|
||||
let parts = self.suffix.as_ref()?.split('/');
|
||||
for part in parts {
|
||||
if worktree.entry_for_path(prefix.join(&part)).is_none() {
|
||||
return Some(prefix);
|
||||
}
|
||||
prefix = prefix.join(part);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn styled_text(&self, project: &Project, cx: &WindowContext) -> StyledText {
|
||||
let mut text = "./".to_string();
|
||||
let mut highlights = Vec::new();
|
||||
let mut offset = text.as_bytes().len();
|
||||
|
||||
let separator = '/';
|
||||
let dir_indicator = "[…]";
|
||||
|
||||
if let Some(path_match) = &self.path_match {
|
||||
text.push_str(&path_match.path.to_string_lossy());
|
||||
for (range, style) in highlight_ranges(
|
||||
&path_match.path.to_string_lossy(),
|
||||
&path_match.positions,
|
||||
gpui::HighlightStyle::color(Color::Accent.color(cx)),
|
||||
) {
|
||||
highlights.push((range.start + offset..range.end + offset, style))
|
||||
}
|
||||
text.push(separator);
|
||||
offset = text.as_bytes().len();
|
||||
|
||||
if let Some(suffix) = &self.suffix {
|
||||
text.push_str(suffix);
|
||||
let entry = self.entry(project, cx);
|
||||
let color = if let Some(entry) = entry {
|
||||
if entry.is_dir() {
|
||||
Color::Accent
|
||||
} else {
|
||||
Color::Conflict
|
||||
}
|
||||
} else {
|
||||
Color::Created
|
||||
};
|
||||
highlights.push((
|
||||
offset..offset + suffix.as_bytes().len(),
|
||||
HighlightStyle::color(color.color(cx)),
|
||||
));
|
||||
offset += suffix.as_bytes().len();
|
||||
if entry.is_some_and(|e| e.is_dir()) {
|
||||
text.push(separator);
|
||||
offset += separator.len_utf8();
|
||||
|
||||
text.push_str(dir_indicator);
|
||||
highlights.push((
|
||||
offset..offset + dir_indicator.bytes().len(),
|
||||
HighlightStyle::color(Color::Muted.color(cx)),
|
||||
));
|
||||
}
|
||||
} else {
|
||||
text.push_str(dir_indicator);
|
||||
highlights.push((
|
||||
offset..offset + dir_indicator.bytes().len(),
|
||||
HighlightStyle::color(Color::Muted.color(cx)),
|
||||
))
|
||||
}
|
||||
} else if let Some(suffix) = &self.suffix {
|
||||
text.push_str(suffix);
|
||||
let existing_prefix_len = self
|
||||
.existing_prefix(project, cx)
|
||||
.map(|prefix| prefix.to_string_lossy().as_bytes().len())
|
||||
.unwrap_or(0);
|
||||
|
||||
if existing_prefix_len > 0 {
|
||||
highlights.push((
|
||||
offset..offset + existing_prefix_len,
|
||||
HighlightStyle::color(Color::Accent.color(cx)),
|
||||
));
|
||||
}
|
||||
highlights.push((
|
||||
offset + existing_prefix_len..offset + suffix.as_bytes().len(),
|
||||
HighlightStyle::color(if self.entry(project, cx).is_some() {
|
||||
Color::Conflict.color(cx)
|
||||
} else {
|
||||
Color::Created.color(cx)
|
||||
}),
|
||||
));
|
||||
offset += suffix.as_bytes().len();
|
||||
if suffix.ends_with('/') {
|
||||
text.push_str(dir_indicator);
|
||||
highlights.push((
|
||||
offset..offset + dir_indicator.bytes().len(),
|
||||
HighlightStyle::color(Color::Muted.color(cx)),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
StyledText::new(text).with_highlights(&cx.text_style().clone(), highlights)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct NewPathDelegate {
|
||||
project: Model<Project>,
|
||||
tx: Option<oneshot::Sender<Option<ProjectPath>>>,
|
||||
selected_index: usize,
|
||||
matches: Vec<Match>,
|
||||
last_selected_dir: Option<String>,
|
||||
cancel_flag: Arc<AtomicBool>,
|
||||
should_dismiss: bool,
|
||||
}
|
||||
|
||||
impl NewPathPrompt {
|
||||
pub(crate) fn register(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
|
||||
if workspace.project().read(cx).is_remote() {
|
||||
workspace.set_prompt_for_new_path(Box::new(|workspace, cx| {
|
||||
let (tx, rx) = futures::channel::oneshot::channel();
|
||||
Self::prompt_for_new_path(workspace, tx, cx);
|
||||
rx
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_for_new_path(
|
||||
workspace: &mut Workspace,
|
||||
tx: oneshot::Sender<Option<ProjectPath>>,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) {
|
||||
let project = workspace.project().clone();
|
||||
workspace.toggle_modal(cx, |cx| {
|
||||
let delegate = NewPathDelegate {
|
||||
project,
|
||||
tx: Some(tx),
|
||||
selected_index: 0,
|
||||
matches: vec![],
|
||||
cancel_flag: Arc::new(AtomicBool::new(false)),
|
||||
last_selected_dir: None,
|
||||
should_dismiss: true,
|
||||
};
|
||||
|
||||
Picker::uniform_list(delegate, cx).width(rems(34.))
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl PickerDelegate for NewPathDelegate {
|
||||
type ListItem = ui::ListItem;
|
||||
|
||||
fn match_count(&self) -> usize {
|
||||
self.matches.len()
|
||||
}
|
||||
|
||||
fn selected_index(&self) -> usize {
|
||||
self.selected_index
|
||||
}
|
||||
|
||||
fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<picker::Picker<Self>>) {
|
||||
self.selected_index = ix;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn update_matches(
|
||||
&mut self,
|
||||
query: String,
|
||||
cx: &mut ViewContext<picker::Picker<Self>>,
|
||||
) -> gpui::Task<()> {
|
||||
let query = query.trim().trim_start_matches('/');
|
||||
let (dir, suffix) = if let Some(index) = query.rfind('/') {
|
||||
let suffix = if index + 1 < query.len() {
|
||||
Some(query[index + 1..].to_string())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
(query[0..index].to_string(), suffix)
|
||||
} else {
|
||||
(query.to_string(), None)
|
||||
};
|
||||
|
||||
let worktrees = self
|
||||
.project
|
||||
.read(cx)
|
||||
.visible_worktrees(cx)
|
||||
.collect::<Vec<_>>();
|
||||
let include_root_name = worktrees.len() > 1;
|
||||
let candidate_sets = worktrees
|
||||
.into_iter()
|
||||
.map(|worktree| {
|
||||
let worktree = worktree.read(cx);
|
||||
PathMatchCandidateSet {
|
||||
snapshot: worktree.snapshot(),
|
||||
include_ignored: worktree
|
||||
.root_entry()
|
||||
.map_or(false, |entry| entry.is_ignored),
|
||||
include_root_name,
|
||||
directories_only: true,
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
self.cancel_flag.store(true, atomic::Ordering::Relaxed);
|
||||
self.cancel_flag = Arc::new(AtomicBool::new(false));
|
||||
|
||||
let cancel_flag = self.cancel_flag.clone();
|
||||
let query = query.to_string();
|
||||
let prefix = dir.clone();
|
||||
cx.spawn(|picker, mut cx| async move {
|
||||
let matches = fuzzy::match_path_sets(
|
||||
candidate_sets.as_slice(),
|
||||
&dir,
|
||||
None,
|
||||
false,
|
||||
100,
|
||||
&cancel_flag,
|
||||
cx.background_executor().clone(),
|
||||
)
|
||||
.await;
|
||||
let did_cancel = cancel_flag.load(atomic::Ordering::Relaxed);
|
||||
if did_cancel {
|
||||
return;
|
||||
}
|
||||
picker
|
||||
.update(&mut cx, |picker, cx| {
|
||||
picker
|
||||
.delegate
|
||||
.set_search_matches(query, prefix, suffix, matches, cx)
|
||||
})
|
||||
.log_err();
|
||||
})
|
||||
}
|
||||
|
||||
fn confirm_update_query(&mut self, cx: &mut ViewContext<Picker<Self>>) -> Option<String> {
|
||||
let m = self.matches.get(self.selected_index)?;
|
||||
if m.is_dir(self.project.read(cx), cx) {
|
||||
let path = m.relative_path();
|
||||
self.last_selected_dir = Some(path.clone());
|
||||
Some(format!("{}/", path))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _: bool, cx: &mut ViewContext<picker::Picker<Self>>) {
|
||||
let Some(m) = self.matches.get(self.selected_index) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let exists = m.entry(self.project.read(cx), cx).is_some();
|
||||
if exists {
|
||||
self.should_dismiss = false;
|
||||
let answer = cx.prompt(
|
||||
gpui::PromptLevel::Destructive,
|
||||
&format!("{} already exists. Do you want to replace it?", m.relative_path()),
|
||||
Some(
|
||||
"A file or folder with the same name already eixsts. Replacing it will overwrite its current contents.",
|
||||
),
|
||||
&["Replace", "Cancel"],
|
||||
);
|
||||
let m = m.clone();
|
||||
cx.spawn(|picker, mut cx| async move {
|
||||
let answer = answer.await.ok();
|
||||
picker
|
||||
.update(&mut cx, |picker, cx| {
|
||||
picker.delegate.should_dismiss = true;
|
||||
if answer != Some(0) {
|
||||
return;
|
||||
}
|
||||
if let Some(path) = m.project_path(picker.delegate.project.read(cx), cx) {
|
||||
if let Some(tx) = picker.delegate.tx.take() {
|
||||
tx.send(Some(path)).ok();
|
||||
}
|
||||
}
|
||||
cx.emit(gpui::DismissEvent);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.detach();
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(path) = m.project_path(self.project.read(cx), cx) {
|
||||
if let Some(tx) = self.tx.take() {
|
||||
tx.send(Some(path)).ok();
|
||||
}
|
||||
}
|
||||
cx.emit(gpui::DismissEvent);
|
||||
}
|
||||
|
||||
fn should_dismiss(&self) -> bool {
|
||||
self.should_dismiss
|
||||
}
|
||||
|
||||
fn dismissed(&mut self, cx: &mut ViewContext<picker::Picker<Self>>) {
|
||||
if let Some(tx) = self.tx.take() {
|
||||
tx.send(None).ok();
|
||||
}
|
||||
cx.emit(gpui::DismissEvent)
|
||||
}
|
||||
|
||||
fn render_match(
|
||||
&self,
|
||||
ix: usize,
|
||||
selected: bool,
|
||||
cx: &mut ViewContext<picker::Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
let m = self.matches.get(ix)?;
|
||||
|
||||
Some(
|
||||
ListItem::new(ix)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.inset(true)
|
||||
.selected(selected)
|
||||
.child(LabelLike::new().child(m.styled_text(self.project.read(cx), cx))),
|
||||
)
|
||||
}
|
||||
|
||||
fn no_matches_text(&self, _cx: &mut WindowContext) -> SharedString {
|
||||
"Type a path...".into()
|
||||
}
|
||||
|
||||
fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
|
||||
Arc::from("[directory/]filename.ext")
|
||||
}
|
||||
}
|
||||
|
||||
impl NewPathDelegate {
|
||||
fn set_search_matches(
|
||||
&mut self,
|
||||
query: String,
|
||||
prefix: String,
|
||||
suffix: Option<String>,
|
||||
matches: Vec<PathMatch>,
|
||||
cx: &mut ViewContext<Picker<Self>>,
|
||||
) {
|
||||
cx.notify();
|
||||
if query.is_empty() {
|
||||
self.matches = vec![];
|
||||
return;
|
||||
}
|
||||
|
||||
let mut directory_exists = false;
|
||||
|
||||
self.matches = matches
|
||||
.into_iter()
|
||||
.map(|m| {
|
||||
if m.path.as_ref().to_string_lossy() == prefix {
|
||||
directory_exists = true
|
||||
}
|
||||
Match {
|
||||
path_match: Some(m),
|
||||
suffix: suffix.clone(),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
if !directory_exists {
|
||||
if suffix.is_none()
|
||||
|| self
|
||||
.last_selected_dir
|
||||
.as_ref()
|
||||
.is_some_and(|d| query.starts_with(d))
|
||||
{
|
||||
self.matches.insert(
|
||||
0,
|
||||
Match {
|
||||
path_match: None,
|
||||
suffix: Some(query.clone()),
|
||||
},
|
||||
)
|
||||
} else {
|
||||
self.matches.push(Match {
|
||||
path_match: None,
|
||||
suffix: Some(query.clone()),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,10 @@ pub fn init(assets: impl AssetSource, cx: &mut AppContext) {
|
||||
}
|
||||
|
||||
impl FileIcons {
|
||||
pub fn get(cx: &AppContext) -> &Self {
|
||||
cx.global::<FileIcons>()
|
||||
}
|
||||
|
||||
pub fn new(assets: impl AssetSource) -> Self {
|
||||
assets
|
||||
.load("icons/file_icons/file_types.json")
|
||||
|
||||
@@ -36,6 +36,9 @@ gpui = { workspace = true, optional = true }
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
fsevent.workspace = true
|
||||
objc = "0.2"
|
||||
cocoa = "0.25"
|
||||
|
||||
|
||||
[target.'cfg(not(target_os = "macos"))'.dependencies]
|
||||
notify = "6.1.1"
|
||||
|
||||
@@ -49,7 +49,13 @@ pub trait Fs: Send + Sync {
|
||||
async fn copy_file(&self, source: &Path, target: &Path, options: CopyOptions) -> Result<()>;
|
||||
async fn rename(&self, source: &Path, target: &Path, options: RenameOptions) -> Result<()>;
|
||||
async fn remove_dir(&self, path: &Path, options: RemoveOptions) -> Result<()>;
|
||||
async fn trash_dir(&self, path: &Path, options: RemoveOptions) -> Result<()> {
|
||||
self.remove_dir(path, options).await
|
||||
}
|
||||
async fn remove_file(&self, path: &Path, options: RemoveOptions) -> Result<()>;
|
||||
async fn trash_file(&self, path: &Path, options: RemoveOptions) -> Result<()> {
|
||||
self.remove_file(path, options).await
|
||||
}
|
||||
async fn open_sync(&self, path: &Path) -> Result<Box<dyn io::Read>>;
|
||||
async fn load(&self, path: &Path) -> Result<String>;
|
||||
async fn atomic_write(&self, path: PathBuf, text: String) -> Result<()>;
|
||||
@@ -237,6 +243,33 @@ impl Fs for RealFs {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
async fn trash_file(&self, path: &Path, _options: RemoveOptions) -> Result<()> {
|
||||
use cocoa::{
|
||||
base::{id, nil},
|
||||
foundation::{NSAutoreleasePool, NSString},
|
||||
};
|
||||
use objc::{class, msg_send, sel, sel_impl};
|
||||
|
||||
unsafe {
|
||||
unsafe fn ns_string(string: &str) -> id {
|
||||
NSString::alloc(nil).init_str(string).autorelease()
|
||||
}
|
||||
|
||||
let url: id = msg_send![class!(NSURL), fileURLWithPath: ns_string(path.to_string_lossy().as_ref())];
|
||||
let array: id = msg_send![class!(NSArray), arrayWithObject: url];
|
||||
let workspace: id = msg_send![class!(NSWorkspace), sharedWorkspace];
|
||||
|
||||
let _: id = msg_send![workspace, recycleURLs: array completionHandler: nil];
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
async fn trash_dir(&self, path: &Path, options: RemoveOptions) -> Result<()> {
|
||||
self.trash_file(path, options).await
|
||||
}
|
||||
|
||||
async fn open_sync(&self, path: &Path) -> Result<Box<dyn io::Read>> {
|
||||
Ok(Box::new(std::fs::File::open(path)?))
|
||||
}
|
||||
@@ -714,6 +747,15 @@ impl FakeFs {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn read_file_sync(&self, path: impl AsRef<Path>) -> Result<Vec<u8>> {
|
||||
let path = path.as_ref();
|
||||
let path = normalize_path(path);
|
||||
let state = self.state.lock();
|
||||
let entry = state.read_path(&path)?;
|
||||
let entry = entry.lock();
|
||||
entry.file_content(&path).cloned()
|
||||
}
|
||||
|
||||
async fn load_internal(&self, path: impl AsRef<Path>) -> Result<Vec<u8>> {
|
||||
let path = path.as_ref();
|
||||
let path = normalize_path(path);
|
||||
|
||||
@@ -8,6 +8,10 @@ use std::process::Command;
|
||||
use std::os::windows::process::CommandExt;
|
||||
|
||||
pub fn get_messages(working_directory: &Path, shas: &[Oid]) -> Result<HashMap<Oid, String>> {
|
||||
if shas.is_empty() {
|
||||
return Ok(HashMap::default());
|
||||
}
|
||||
|
||||
const MARKER: &'static str = "<MARKER>";
|
||||
|
||||
let mut command = Command::new("git");
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::{ops::Range, sync::Arc};
|
||||
|
||||
use anyhow::Result;
|
||||
use url::Url;
|
||||
use util::{github, http::HttpClient};
|
||||
use util::{codeberg, github, http::HttpClient};
|
||||
|
||||
use crate::Oid;
|
||||
|
||||
@@ -59,7 +59,7 @@ impl HostingProvider {
|
||||
|
||||
pub fn supports_avatars(&self) -> bool {
|
||||
match self {
|
||||
HostingProvider::Github => true,
|
||||
HostingProvider::Github | HostingProvider::Codeberg => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
@@ -71,24 +71,27 @@ impl HostingProvider {
|
||||
commit: Oid,
|
||||
client: Arc<dyn HttpClient>,
|
||||
) -> Result<Option<Url>> {
|
||||
match self {
|
||||
Ok(match self {
|
||||
HostingProvider::Github => {
|
||||
let commit = commit.to_string();
|
||||
|
||||
let author =
|
||||
github::fetch_github_commit_author(repo_owner, repo, &commit, &client).await?;
|
||||
|
||||
let url = if let Some(author) = author {
|
||||
let mut url = Url::parse(&author.avatar_url)?;
|
||||
url.set_query(Some("size=128"));
|
||||
Some(url)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Ok(url)
|
||||
github::fetch_github_commit_author(repo_owner, repo, &commit, &client)
|
||||
.await?
|
||||
.map(|author| -> Result<Url, url::ParseError> {
|
||||
let mut url = Url::parse(&author.avatar_url)?;
|
||||
url.set_query(Some("size=128"));
|
||||
Ok(url)
|
||||
})
|
||||
.transpose()
|
||||
}
|
||||
HostingProvider::Codeberg => {
|
||||
let commit = commit.to_string();
|
||||
codeberg::fetch_codeberg_commit_author(repo_owner, repo, &commit, &client)
|
||||
.await?
|
||||
.map(|author| Url::parse(&author.avatar_url))
|
||||
.transpose()
|
||||
}
|
||||
_ => Ok(None),
|
||||
}
|
||||
}?)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -114,12 +114,21 @@ wayland-protocols = { version = "0.31.2", features = [
|
||||
oo7 = "0.3.0"
|
||||
open = "5.1.2"
|
||||
filedescriptor = "0.8.2"
|
||||
x11rb = { version = "0.13.0", features = ["allow-unsafe-code", "xkb", "randr"] }
|
||||
x11rb = { version = "0.13.0", features = [
|
||||
"allow-unsafe-code",
|
||||
"xkb",
|
||||
"randr",
|
||||
"xinput",
|
||||
"resource_manager",
|
||||
] }
|
||||
xkbcommon = { version = "0.7", features = ["wayland", "x11"] }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows.workspace = true
|
||||
|
||||
[target.'cfg(windows)'.build-dependencies]
|
||||
embed-resource = "2.4"
|
||||
|
||||
[[example]]
|
||||
name = "hello_world"
|
||||
path = "examples/hello_world.rs"
|
||||
|
||||
@@ -6,6 +6,15 @@
|
||||
fn main() {
|
||||
#[cfg(target_os = "macos")]
|
||||
macos::build();
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let manifest = std::path::Path::new("resources/windows/gpui.manifest.xml");
|
||||
let rc_file = std::path::Path::new("resources/windows/gpui.rc");
|
||||
println!("cargo:rerun-if-changed={}", manifest.display());
|
||||
println!("cargo:rerun-if-changed={}", rc_file.display());
|
||||
embed_resource::compile(rc_file, embed_resource::NONE);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
|
||||
@@ -58,11 +58,36 @@ impl Render for ImageShowcase {
|
||||
}
|
||||
}
|
||||
|
||||
actions!(image, [Quit]);
|
||||
|
||||
fn main() {
|
||||
env_logger::init();
|
||||
|
||||
App::new().run(|cx: &mut AppContext| {
|
||||
cx.open_window(WindowOptions::default(), |cx| {
|
||||
cx.activate(true);
|
||||
cx.on_action(|_: &Quit, cx| cx.quit());
|
||||
cx.bind_keys([KeyBinding::new("cmd-q", Quit, None)]);
|
||||
cx.set_menus(vec![Menu {
|
||||
name: "Image",
|
||||
items: vec![MenuItem::action("Quit", Quit)],
|
||||
}]);
|
||||
|
||||
let window_options = WindowOptions {
|
||||
titlebar: Some(TitlebarOptions {
|
||||
title: Some(SharedString::from("Image Example")),
|
||||
appears_transparent: false,
|
||||
..Default::default()
|
||||
}),
|
||||
|
||||
bounds: Some(Bounds {
|
||||
size: size(px(1100.), px(600.)).into(),
|
||||
origin: Point::new(DevicePixels::from(200), DevicePixels::from(200)),
|
||||
}),
|
||||
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
cx.open_window(window_options, |cx| {
|
||||
cx.new_view(|_cx| ImageShowcase {
|
||||
// Relative path to your root project path
|
||||
local_resource: Arc::new(PathBuf::from_str("examples/image/app-icon.png").unwrap()),
|
||||
|
||||
@@ -54,6 +54,7 @@ fn main() {
|
||||
kind: WindowKind::PopUp,
|
||||
is_movable: false,
|
||||
fullscreen: false,
|
||||
app_id: None,
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
2
crates/gpui/resources/windows/gpui.rc
Normal file
2
crates/gpui/resources/windows/gpui.rc
Normal file
@@ -0,0 +1,2 @@
|
||||
#define RT_MANIFEST 24
|
||||
1 RT_MANIFEST "resources/windows/gpui.manifest.xml"
|
||||
@@ -20,7 +20,7 @@ pub trait AssetSource: 'static + Send + Sync {
|
||||
impl AssetSource for () {
|
||||
fn load(&self, path: &str) -> Result<Cow<'static, [u8]>> {
|
||||
Err(anyhow!(
|
||||
"get called on empty asset provider with \"{}\"",
|
||||
"load called on empty asset provider with \"{}\"",
|
||||
path
|
||||
))
|
||||
}
|
||||
|
||||
@@ -52,14 +52,26 @@ pub trait Element: 'static + IntoElement {
|
||||
/// provided to [`Element::paint`].
|
||||
type PrepaintState: 'static;
|
||||
|
||||
/// If this element has a unique identifier, return it here. This is used to track elements across frames, and
|
||||
/// will cause a GlobalElementId to be passed to the request_layout, prepaint, and paint methods.
|
||||
///
|
||||
/// The global id can in turn be used to access state that's connected to an element with the same id across
|
||||
/// frames. This id must be unique among children of the first containing element with an id.
|
||||
fn id(&self) -> Option<ElementId>;
|
||||
|
||||
/// Before an element can be painted, we need to know where it's going to be and how big it is.
|
||||
/// Use this method to request a layout from Taffy and initialize the element's state.
|
||||
fn request_layout(&mut self, cx: &mut WindowContext) -> (LayoutId, Self::RequestLayoutState);
|
||||
fn request_layout(
|
||||
&mut self,
|
||||
id: Option<&GlobalElementId>,
|
||||
cx: &mut WindowContext,
|
||||
) -> (LayoutId, Self::RequestLayoutState);
|
||||
|
||||
/// After laying out an element, we need to commit its bounds to the current frame for hitbox
|
||||
/// purposes. The state argument is the same state that was returned from [`Element::request_layout()`].
|
||||
fn prepaint(
|
||||
&mut self,
|
||||
id: Option<&GlobalElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
request_layout: &mut Self::RequestLayoutState,
|
||||
cx: &mut WindowContext,
|
||||
@@ -69,6 +81,7 @@ pub trait Element: 'static + IntoElement {
|
||||
/// The state argument is the same state that was returned from [`Element::request_layout()`].
|
||||
fn paint(
|
||||
&mut self,
|
||||
id: Option<&GlobalElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
request_layout: &mut Self::RequestLayoutState,
|
||||
prepaint: &mut Self::PrepaintState,
|
||||
@@ -164,18 +177,33 @@ impl<C: RenderOnce> Element for Component<C> {
|
||||
type RequestLayoutState = AnyElement;
|
||||
type PrepaintState = ();
|
||||
|
||||
fn request_layout(&mut self, cx: &mut WindowContext) -> (LayoutId, Self::RequestLayoutState) {
|
||||
fn id(&self) -> Option<ElementId> {
|
||||
None
|
||||
}
|
||||
|
||||
fn request_layout(
|
||||
&mut self,
|
||||
_id: Option<&GlobalElementId>,
|
||||
cx: &mut WindowContext,
|
||||
) -> (LayoutId, Self::RequestLayoutState) {
|
||||
let mut element = self.0.take().unwrap().render(cx).into_any_element();
|
||||
let layout_id = element.request_layout(cx);
|
||||
(layout_id, element)
|
||||
}
|
||||
|
||||
fn prepaint(&mut self, _: Bounds<Pixels>, element: &mut AnyElement, cx: &mut WindowContext) {
|
||||
fn prepaint(
|
||||
&mut self,
|
||||
_id: Option<&GlobalElementId>,
|
||||
_: Bounds<Pixels>,
|
||||
element: &mut AnyElement,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
element.prepaint(cx);
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
_id: Option<&GlobalElementId>,
|
||||
_: Bounds<Pixels>,
|
||||
element: &mut Self::RequestLayoutState,
|
||||
_: &mut Self::PrepaintState,
|
||||
@@ -194,8 +222,8 @@ impl<C: RenderOnce> IntoElement for Component<C> {
|
||||
}
|
||||
|
||||
/// A globally unique identifier for an element, used to track state across frames.
|
||||
#[derive(Deref, DerefMut, Default, Clone, Debug, Eq, PartialEq, Hash)]
|
||||
pub(crate) struct GlobalElementId(SmallVec<[ElementId; 32]>);
|
||||
#[derive(Deref, DerefMut, Default, Debug, Eq, PartialEq, Hash)]
|
||||
pub struct GlobalElementId(pub(crate) SmallVec<[ElementId; 32]>);
|
||||
|
||||
trait ElementObject {
|
||||
fn inner_element(&mut self) -> &mut dyn Any;
|
||||
@@ -224,17 +252,20 @@ pub struct Drawable<E: Element> {
|
||||
enum ElementDrawPhase<RequestLayoutState, PrepaintState> {
|
||||
#[default]
|
||||
Start,
|
||||
RequestLayoutState {
|
||||
RequestLayout {
|
||||
layout_id: LayoutId,
|
||||
global_id: Option<GlobalElementId>,
|
||||
request_layout: RequestLayoutState,
|
||||
},
|
||||
LayoutComputed {
|
||||
layout_id: LayoutId,
|
||||
global_id: Option<GlobalElementId>,
|
||||
available_space: Size<AvailableSpace>,
|
||||
request_layout: RequestLayoutState,
|
||||
},
|
||||
PrepaintState {
|
||||
Prepaint {
|
||||
node_id: DispatchNodeId,
|
||||
global_id: Option<GlobalElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
request_layout: RequestLayoutState,
|
||||
prepaint: PrepaintState,
|
||||
@@ -254,9 +285,21 @@ impl<E: Element> Drawable<E> {
|
||||
fn request_layout(&mut self, cx: &mut WindowContext) -> LayoutId {
|
||||
match mem::take(&mut self.phase) {
|
||||
ElementDrawPhase::Start => {
|
||||
let (layout_id, request_layout) = self.element.request_layout(cx);
|
||||
self.phase = ElementDrawPhase::RequestLayoutState {
|
||||
let global_id = self.element.id().map(|element_id| {
|
||||
cx.window.element_id_stack.push(element_id);
|
||||
GlobalElementId(cx.window.element_id_stack.clone())
|
||||
});
|
||||
|
||||
let (layout_id, request_layout) =
|
||||
self.element.request_layout(global_id.as_ref(), cx);
|
||||
|
||||
if global_id.is_some() {
|
||||
cx.window.element_id_stack.pop();
|
||||
}
|
||||
|
||||
self.phase = ElementDrawPhase::RequestLayout {
|
||||
layout_id,
|
||||
global_id,
|
||||
request_layout,
|
||||
};
|
||||
layout_id
|
||||
@@ -267,25 +310,40 @@ impl<E: Element> Drawable<E> {
|
||||
|
||||
pub(crate) fn prepaint(&mut self, cx: &mut WindowContext) {
|
||||
match mem::take(&mut self.phase) {
|
||||
ElementDrawPhase::RequestLayoutState {
|
||||
ElementDrawPhase::RequestLayout {
|
||||
layout_id,
|
||||
global_id,
|
||||
mut request_layout,
|
||||
}
|
||||
| ElementDrawPhase::LayoutComputed {
|
||||
layout_id,
|
||||
global_id,
|
||||
mut request_layout,
|
||||
..
|
||||
} => {
|
||||
if let Some(element_id) = self.element.id() {
|
||||
cx.window.element_id_stack.push(element_id);
|
||||
debug_assert_eq!(global_id.as_ref().unwrap().0, cx.window.element_id_stack);
|
||||
}
|
||||
|
||||
let bounds = cx.layout_bounds(layout_id);
|
||||
let node_id = cx.window.next_frame.dispatch_tree.push_node();
|
||||
let prepaint = self.element.prepaint(bounds, &mut request_layout, cx);
|
||||
self.phase = ElementDrawPhase::PrepaintState {
|
||||
let prepaint =
|
||||
self.element
|
||||
.prepaint(global_id.as_ref(), bounds, &mut request_layout, cx);
|
||||
cx.window.next_frame.dispatch_tree.pop_node();
|
||||
|
||||
if global_id.is_some() {
|
||||
cx.window.element_id_stack.pop();
|
||||
}
|
||||
|
||||
self.phase = ElementDrawPhase::Prepaint {
|
||||
node_id,
|
||||
global_id,
|
||||
bounds,
|
||||
request_layout,
|
||||
prepaint,
|
||||
};
|
||||
cx.window.next_frame.dispatch_tree.pop_node();
|
||||
}
|
||||
_ => panic!("must call request_layout before prepaint"),
|
||||
}
|
||||
@@ -296,16 +354,32 @@ impl<E: Element> Drawable<E> {
|
||||
cx: &mut WindowContext,
|
||||
) -> (E::RequestLayoutState, E::PrepaintState) {
|
||||
match mem::take(&mut self.phase) {
|
||||
ElementDrawPhase::PrepaintState {
|
||||
ElementDrawPhase::Prepaint {
|
||||
node_id,
|
||||
global_id,
|
||||
bounds,
|
||||
mut request_layout,
|
||||
mut prepaint,
|
||||
..
|
||||
} => {
|
||||
if let Some(element_id) = self.element.id() {
|
||||
cx.window.element_id_stack.push(element_id);
|
||||
debug_assert_eq!(global_id.as_ref().unwrap().0, cx.window.element_id_stack);
|
||||
}
|
||||
|
||||
cx.window.next_frame.dispatch_tree.set_active_node(node_id);
|
||||
self.element
|
||||
.paint(bounds, &mut request_layout, &mut prepaint, cx);
|
||||
self.element.paint(
|
||||
global_id.as_ref(),
|
||||
bounds,
|
||||
&mut request_layout,
|
||||
&mut prepaint,
|
||||
cx,
|
||||
);
|
||||
|
||||
if global_id.is_some() {
|
||||
cx.window.element_id_stack.pop();
|
||||
}
|
||||
|
||||
self.phase = ElementDrawPhase::Painted;
|
||||
(request_layout, prepaint)
|
||||
}
|
||||
@@ -323,13 +397,15 @@ impl<E: Element> Drawable<E> {
|
||||
}
|
||||
|
||||
let layout_id = match mem::take(&mut self.phase) {
|
||||
ElementDrawPhase::RequestLayoutState {
|
||||
ElementDrawPhase::RequestLayout {
|
||||
layout_id,
|
||||
global_id,
|
||||
request_layout,
|
||||
} => {
|
||||
cx.compute_layout(layout_id, available_space);
|
||||
self.phase = ElementDrawPhase::LayoutComputed {
|
||||
layout_id,
|
||||
global_id,
|
||||
available_space,
|
||||
request_layout,
|
||||
};
|
||||
@@ -337,6 +413,7 @@ impl<E: Element> Drawable<E> {
|
||||
}
|
||||
ElementDrawPhase::LayoutComputed {
|
||||
layout_id,
|
||||
global_id,
|
||||
available_space: prev_available_space,
|
||||
request_layout,
|
||||
} => {
|
||||
@@ -345,6 +422,7 @@ impl<E: Element> Drawable<E> {
|
||||
}
|
||||
self.phase = ElementDrawPhase::LayoutComputed {
|
||||
layout_id,
|
||||
global_id,
|
||||
available_space,
|
||||
request_layout,
|
||||
};
|
||||
@@ -454,13 +532,22 @@ impl Element for AnyElement {
|
||||
type RequestLayoutState = ();
|
||||
type PrepaintState = ();
|
||||
|
||||
fn request_layout(&mut self, cx: &mut WindowContext) -> (LayoutId, Self::RequestLayoutState) {
|
||||
fn id(&self) -> Option<ElementId> {
|
||||
None
|
||||
}
|
||||
|
||||
fn request_layout(
|
||||
&mut self,
|
||||
_: Option<&GlobalElementId>,
|
||||
cx: &mut WindowContext,
|
||||
) -> (LayoutId, Self::RequestLayoutState) {
|
||||
let layout_id = self.request_layout(cx);
|
||||
(layout_id, ())
|
||||
}
|
||||
|
||||
fn prepaint(
|
||||
&mut self,
|
||||
_: Option<&GlobalElementId>,
|
||||
_: Bounds<Pixels>,
|
||||
_: &mut Self::RequestLayoutState,
|
||||
cx: &mut WindowContext,
|
||||
@@ -470,6 +557,7 @@ impl Element for AnyElement {
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
_: Option<&GlobalElementId>,
|
||||
_: Bounds<Pixels>,
|
||||
_: &mut Self::RequestLayoutState,
|
||||
_: &mut Self::PrepaintState,
|
||||
@@ -506,12 +594,21 @@ impl Element for Empty {
|
||||
type RequestLayoutState = ();
|
||||
type PrepaintState = ();
|
||||
|
||||
fn request_layout(&mut self, cx: &mut WindowContext) -> (LayoutId, Self::RequestLayoutState) {
|
||||
fn id(&self) -> Option<ElementId> {
|
||||
None
|
||||
}
|
||||
|
||||
fn request_layout(
|
||||
&mut self,
|
||||
_id: Option<&GlobalElementId>,
|
||||
cx: &mut WindowContext,
|
||||
) -> (LayoutId, Self::RequestLayoutState) {
|
||||
(cx.request_layout(&Style::default(), None), ())
|
||||
}
|
||||
|
||||
fn prepaint(
|
||||
&mut self,
|
||||
_id: Option<&GlobalElementId>,
|
||||
_bounds: Bounds<Pixels>,
|
||||
_state: &mut Self::RequestLayoutState,
|
||||
_cx: &mut WindowContext,
|
||||
@@ -520,6 +617,7 @@ impl Element for Empty {
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
_id: Option<&GlobalElementId>,
|
||||
_bounds: Bounds<Pixels>,
|
||||
_request_layout: &mut Self::RequestLayoutState,
|
||||
_prepaint: &mut Self::PrepaintState,
|
||||
|
||||
@@ -2,8 +2,8 @@ use smallvec::SmallVec;
|
||||
use taffy::style::{Display, Position};
|
||||
|
||||
use crate::{
|
||||
point, AnyElement, Bounds, Element, IntoElement, LayoutId, ParentElement, Pixels, Point, Size,
|
||||
Style, WindowContext,
|
||||
point, AnyElement, Bounds, Element, GlobalElementId, IntoElement, LayoutId, ParentElement,
|
||||
Pixels, Point, Size, Style, WindowContext,
|
||||
};
|
||||
|
||||
/// The state that the anchored element element uses to track its children.
|
||||
@@ -72,8 +72,13 @@ impl Element for Anchored {
|
||||
type RequestLayoutState = AnchoredState;
|
||||
type PrepaintState = ();
|
||||
|
||||
fn id(&self) -> Option<crate::ElementId> {
|
||||
None
|
||||
}
|
||||
|
||||
fn request_layout(
|
||||
&mut self,
|
||||
_id: Option<&GlobalElementId>,
|
||||
cx: &mut WindowContext,
|
||||
) -> (crate::LayoutId, Self::RequestLayoutState) {
|
||||
let child_layout_ids = self
|
||||
@@ -95,6 +100,7 @@ impl Element for Anchored {
|
||||
|
||||
fn prepaint(
|
||||
&mut self,
|
||||
_id: Option<&GlobalElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
request_layout: &mut Self::RequestLayoutState,
|
||||
cx: &mut WindowContext,
|
||||
@@ -177,6 +183,7 @@ impl Element for Anchored {
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
_id: Option<&GlobalElementId>,
|
||||
_bounds: crate::Bounds<crate::Pixels>,
|
||||
_request_layout: &mut Self::RequestLayoutState,
|
||||
_prepaint: &mut Self::PrepaintState,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use crate::{AnyElement, Element, ElementId, IntoElement};
|
||||
use crate::{AnyElement, Element, ElementId, GlobalElementId, IntoElement};
|
||||
|
||||
pub use easing::*;
|
||||
|
||||
@@ -86,15 +86,19 @@ struct AnimationState {
|
||||
|
||||
impl<E: IntoElement + 'static> Element for AnimationElement<E> {
|
||||
type RequestLayoutState = AnyElement;
|
||||
|
||||
type PrepaintState = ();
|
||||
|
||||
fn id(&self) -> Option<ElementId> {
|
||||
Some(self.id.clone())
|
||||
}
|
||||
|
||||
fn request_layout(
|
||||
&mut self,
|
||||
global_id: Option<&GlobalElementId>,
|
||||
cx: &mut crate::WindowContext,
|
||||
) -> (crate::LayoutId, Self::RequestLayoutState) {
|
||||
cx.with_element_state(Some(self.id.clone()), |state, cx| {
|
||||
let state = state.unwrap().unwrap_or_else(|| AnimationState {
|
||||
cx.with_element_state(global_id.unwrap(), |state, cx| {
|
||||
let state = state.unwrap_or_else(|| AnimationState {
|
||||
start: Instant::now(),
|
||||
});
|
||||
let mut delta =
|
||||
@@ -130,12 +134,13 @@ impl<E: IntoElement + 'static> Element for AnimationElement<E> {
|
||||
})
|
||||
}
|
||||
|
||||
((element.request_layout(cx), element), Some(state))
|
||||
((element.request_layout(cx), element), state)
|
||||
})
|
||||
}
|
||||
|
||||
fn prepaint(
|
||||
&mut self,
|
||||
_id: Option<&GlobalElementId>,
|
||||
_bounds: crate::Bounds<crate::Pixels>,
|
||||
element: &mut Self::RequestLayoutState,
|
||||
cx: &mut crate::WindowContext,
|
||||
@@ -145,6 +150,7 @@ impl<E: IntoElement + 'static> Element for AnimationElement<E> {
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
_id: Option<&GlobalElementId>,
|
||||
_bounds: crate::Bounds<crate::Pixels>,
|
||||
element: &mut Self::RequestLayoutState,
|
||||
_: &mut Self::PrepaintState,
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
use refineable::Refineable as _;
|
||||
|
||||
use crate::{Bounds, Element, IntoElement, Pixels, Style, StyleRefinement, Styled, WindowContext};
|
||||
use crate::{
|
||||
Bounds, Element, ElementId, GlobalElementId, IntoElement, Pixels, Style, StyleRefinement,
|
||||
Styled, WindowContext,
|
||||
};
|
||||
|
||||
/// Construct a canvas element with the given paint callback.
|
||||
/// Useful for adding short term custom drawing to a view.
|
||||
@@ -35,8 +38,13 @@ impl<T: 'static> Element for Canvas<T> {
|
||||
type RequestLayoutState = Style;
|
||||
type PrepaintState = Option<T>;
|
||||
|
||||
fn id(&self) -> Option<ElementId> {
|
||||
None
|
||||
}
|
||||
|
||||
fn request_layout(
|
||||
&mut self,
|
||||
_id: Option<&GlobalElementId>,
|
||||
cx: &mut WindowContext,
|
||||
) -> (crate::LayoutId, Self::RequestLayoutState) {
|
||||
let mut style = Style::default();
|
||||
@@ -47,6 +55,7 @@ impl<T: 'static> Element for Canvas<T> {
|
||||
|
||||
fn prepaint(
|
||||
&mut self,
|
||||
_id: Option<&GlobalElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
_request_layout: &mut Style,
|
||||
cx: &mut WindowContext,
|
||||
@@ -56,6 +65,7 @@ impl<T: 'static> Element for Canvas<T> {
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
_id: Option<&GlobalElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
style: &mut Style,
|
||||
prepaint: &mut Self::PrepaintState,
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use crate::{AnyElement, Bounds, Element, IntoElement, LayoutId, Pixels, WindowContext};
|
||||
use crate::{
|
||||
AnyElement, Bounds, Element, GlobalElementId, IntoElement, LayoutId, Pixels, WindowContext,
|
||||
};
|
||||
|
||||
/// Builds a `Deferred` element, which delays the layout and paint of its child.
|
||||
pub fn deferred(child: impl IntoElement) -> Deferred {
|
||||
@@ -29,13 +31,22 @@ impl Element for Deferred {
|
||||
type RequestLayoutState = ();
|
||||
type PrepaintState = ();
|
||||
|
||||
fn request_layout(&mut self, cx: &mut WindowContext) -> (LayoutId, ()) {
|
||||
fn id(&self) -> Option<crate::ElementId> {
|
||||
None
|
||||
}
|
||||
|
||||
fn request_layout(
|
||||
&mut self,
|
||||
_id: Option<&GlobalElementId>,
|
||||
cx: &mut WindowContext,
|
||||
) -> (LayoutId, ()) {
|
||||
let layout_id = self.child.as_mut().unwrap().request_layout(cx);
|
||||
(layout_id, ())
|
||||
}
|
||||
|
||||
fn prepaint(
|
||||
&mut self,
|
||||
_id: Option<&GlobalElementId>,
|
||||
_bounds: Bounds<Pixels>,
|
||||
_request_layout: &mut Self::RequestLayoutState,
|
||||
cx: &mut WindowContext,
|
||||
@@ -47,6 +58,7 @@ impl Element for Deferred {
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
_id: Option<&GlobalElementId>,
|
||||
_bounds: Bounds<Pixels>,
|
||||
_request_layout: &mut Self::RequestLayoutState,
|
||||
_prepaint: &mut Self::PrepaintState,
|
||||
|
||||
@@ -17,11 +17,11 @@
|
||||
|
||||
use crate::{
|
||||
point, px, size, Action, AnyDrag, AnyElement, AnyTooltip, AnyView, AppContext, Bounds,
|
||||
ClickEvent, DispatchPhase, Element, ElementId, FocusHandle, Global, Hitbox, HitboxId,
|
||||
IntoElement, IsZero, KeyContext, KeyDownEvent, KeyUpEvent, LayoutId, ModifiersChangedEvent,
|
||||
MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, Point,
|
||||
Render, ScrollWheelEvent, SharedString, Size, Style, StyleRefinement, Styled, Task, TooltipId,
|
||||
View, Visibility, WindowContext,
|
||||
ClickEvent, DispatchPhase, Element, ElementId, FocusHandle, Global, GlobalElementId, Hitbox,
|
||||
HitboxId, IntoElement, IsZero, KeyContext, KeyDownEvent, KeyUpEvent, LayoutId,
|
||||
ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent,
|
||||
ParentElement, Pixels, Point, Render, ScrollWheelEvent, SharedString, Size, Style,
|
||||
StyleRefinement, Styled, Task, TooltipId, View, Visibility, WindowContext,
|
||||
};
|
||||
use collections::HashMap;
|
||||
use refineable::Refineable;
|
||||
@@ -1123,23 +1123,34 @@ impl Element for Div {
|
||||
type RequestLayoutState = DivFrameState;
|
||||
type PrepaintState = Option<Hitbox>;
|
||||
|
||||
fn request_layout(&mut self, cx: &mut WindowContext) -> (LayoutId, Self::RequestLayoutState) {
|
||||
fn id(&self) -> Option<ElementId> {
|
||||
self.interactivity.element_id.clone()
|
||||
}
|
||||
|
||||
fn request_layout(
|
||||
&mut self,
|
||||
global_id: Option<&GlobalElementId>,
|
||||
cx: &mut WindowContext,
|
||||
) -> (LayoutId, Self::RequestLayoutState) {
|
||||
let mut child_layout_ids = SmallVec::new();
|
||||
let layout_id = self.interactivity.request_layout(cx, |style, cx| {
|
||||
cx.with_text_style(style.text_style().cloned(), |cx| {
|
||||
child_layout_ids = self
|
||||
.children
|
||||
.iter_mut()
|
||||
.map(|child| child.request_layout(cx))
|
||||
.collect::<SmallVec<_>>();
|
||||
cx.request_layout(&style, child_layout_ids.iter().copied())
|
||||
})
|
||||
});
|
||||
let layout_id = self
|
||||
.interactivity
|
||||
.request_layout(global_id, cx, |style, cx| {
|
||||
cx.with_text_style(style.text_style().cloned(), |cx| {
|
||||
child_layout_ids = self
|
||||
.children
|
||||
.iter_mut()
|
||||
.map(|child| child.request_layout(cx))
|
||||
.collect::<SmallVec<_>>();
|
||||
cx.request_layout(&style, child_layout_ids.iter().copied())
|
||||
})
|
||||
});
|
||||
(layout_id, DivFrameState { child_layout_ids })
|
||||
}
|
||||
|
||||
fn prepaint(
|
||||
&mut self,
|
||||
global_id: Option<&GlobalElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
request_layout: &mut Self::RequestLayoutState,
|
||||
cx: &mut WindowContext,
|
||||
@@ -1178,6 +1189,7 @@ impl Element for Div {
|
||||
};
|
||||
|
||||
self.interactivity.prepaint(
|
||||
global_id,
|
||||
bounds,
|
||||
content_size,
|
||||
cx,
|
||||
@@ -1194,13 +1206,14 @@ impl Element for Div {
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
global_id: Option<&GlobalElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
_request_layout: &mut Self::RequestLayoutState,
|
||||
hitbox: &mut Option<Hitbox>,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
self.interactivity
|
||||
.paint(bounds, hitbox.as_ref(), cx, |_style, cx| {
|
||||
.paint(global_id, bounds, hitbox.as_ref(), cx, |_style, cx| {
|
||||
for child in &mut self.children {
|
||||
child.paint(cx);
|
||||
}
|
||||
@@ -1220,7 +1233,7 @@ impl IntoElement for Div {
|
||||
/// interactivity in the `Div` element.
|
||||
#[derive(Default)]
|
||||
pub struct Interactivity {
|
||||
/// The element ID of the element
|
||||
/// The element ID of the element. In id is required to support a stateful subset of the interactivity such as on_click.
|
||||
pub element_id: Option<ElementId>,
|
||||
/// Whether the element was clicked. This will only be present after layout.
|
||||
pub active: Option<bool>,
|
||||
@@ -1276,11 +1289,12 @@ impl Interactivity {
|
||||
/// Layout this element according to this interactivity state's configured styles
|
||||
pub fn request_layout(
|
||||
&mut self,
|
||||
global_id: Option<&GlobalElementId>,
|
||||
cx: &mut WindowContext,
|
||||
f: impl FnOnce(Style, &mut WindowContext) -> LayoutId,
|
||||
) -> LayoutId {
|
||||
cx.with_element_state::<InteractiveElementState, _>(
|
||||
self.element_id.clone(),
|
||||
cx.with_optional_element_state::<InteractiveElementState, _>(
|
||||
global_id,
|
||||
|element_state, cx| {
|
||||
let mut element_state =
|
||||
element_state.map(|element_state| element_state.unwrap_or_default());
|
||||
@@ -1339,14 +1353,15 @@ impl Interactivity {
|
||||
/// Commit the bounds of this element according to this interactivity state's configured styles.
|
||||
pub fn prepaint<R>(
|
||||
&mut self,
|
||||
global_id: Option<&GlobalElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
content_size: Size<Pixels>,
|
||||
cx: &mut WindowContext,
|
||||
f: impl FnOnce(&Style, Point<Pixels>, Option<Hitbox>, &mut WindowContext) -> R,
|
||||
) -> R {
|
||||
self.content_size = content_size;
|
||||
cx.with_element_state::<InteractiveElementState, _>(
|
||||
self.element_id.clone(),
|
||||
cx.with_optional_element_state::<InteractiveElementState, _>(
|
||||
global_id,
|
||||
|element_state, cx| {
|
||||
let mut element_state =
|
||||
element_state.map(|element_state| element_state.unwrap_or_default());
|
||||
@@ -1454,14 +1469,15 @@ impl Interactivity {
|
||||
/// with the current scroll offset
|
||||
pub fn paint(
|
||||
&mut self,
|
||||
global_id: Option<&GlobalElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
hitbox: Option<&Hitbox>,
|
||||
cx: &mut WindowContext,
|
||||
f: impl FnOnce(&Style, &mut WindowContext),
|
||||
) {
|
||||
self.hovered = hitbox.map(|hitbox| hitbox.is_hovered(cx));
|
||||
cx.with_element_state::<InteractiveElementState, _>(
|
||||
self.element_id.clone(),
|
||||
cx.with_optional_element_state::<InteractiveElementState, _>(
|
||||
global_id,
|
||||
|element_state, cx| {
|
||||
let mut element_state =
|
||||
element_state.map(|element_state| element_state.unwrap_or_default());
|
||||
@@ -1487,7 +1503,7 @@ impl Interactivity {
|
||||
cx.with_content_mask(style.overflow_mask(bounds, cx.rem_size()), |cx| {
|
||||
if let Some(hitbox) = hitbox {
|
||||
#[cfg(debug_assertions)]
|
||||
self.paint_debug_info(hitbox, &style, cx);
|
||||
self.paint_debug_info(global_id, hitbox, &style, cx);
|
||||
|
||||
if !cx.has_active_drag() {
|
||||
if let Some(mouse_cursor) = style.mouse_cursor {
|
||||
@@ -1521,13 +1537,19 @@ impl Interactivity {
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
fn paint_debug_info(&mut self, hitbox: &Hitbox, style: &Style, cx: &mut WindowContext) {
|
||||
if self.element_id.is_some()
|
||||
fn paint_debug_info(
|
||||
&mut self,
|
||||
global_id: Option<&GlobalElementId>,
|
||||
hitbox: &Hitbox,
|
||||
style: &Style,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
if global_id.is_some()
|
||||
&& (style.debug || style.debug_below || cx.has_global::<crate::DebugBelow>())
|
||||
&& hitbox.is_hovered(cx)
|
||||
{
|
||||
const FONT_SIZE: crate::Pixels = crate::Pixels(10.);
|
||||
let element_id = format!("{:?}", self.element_id.as_ref().unwrap());
|
||||
let element_id = format!("{:?}", global_id.unwrap());
|
||||
let str_len = element_id.len();
|
||||
|
||||
let render_debug_text = |cx: &mut WindowContext| {
|
||||
@@ -2064,8 +2086,13 @@ impl Interactivity {
|
||||
}
|
||||
|
||||
/// Compute the visual style for this element, based on the current bounds and the element's state.
|
||||
pub fn compute_style(&self, hitbox: Option<&Hitbox>, cx: &mut WindowContext) -> Style {
|
||||
cx.with_element_state(self.element_id.clone(), |element_state, cx| {
|
||||
pub fn compute_style(
|
||||
&self,
|
||||
global_id: Option<&GlobalElementId>,
|
||||
hitbox: Option<&Hitbox>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Style {
|
||||
cx.with_optional_element_state(global_id, |element_state, cx| {
|
||||
let mut element_state =
|
||||
element_state.map(|element_state| element_state.unwrap_or_default());
|
||||
let style = self.compute_style_internal(hitbox, element_state.as_mut(), cx);
|
||||
@@ -2264,27 +2291,37 @@ where
|
||||
type RequestLayoutState = E::RequestLayoutState;
|
||||
type PrepaintState = E::PrepaintState;
|
||||
|
||||
fn request_layout(&mut self, cx: &mut WindowContext) -> (LayoutId, Self::RequestLayoutState) {
|
||||
self.element.request_layout(cx)
|
||||
fn id(&self) -> Option<ElementId> {
|
||||
self.element.id()
|
||||
}
|
||||
|
||||
fn request_layout(
|
||||
&mut self,
|
||||
id: Option<&GlobalElementId>,
|
||||
cx: &mut WindowContext,
|
||||
) -> (LayoutId, Self::RequestLayoutState) {
|
||||
self.element.request_layout(id, cx)
|
||||
}
|
||||
|
||||
fn prepaint(
|
||||
&mut self,
|
||||
id: Option<&GlobalElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
state: &mut Self::RequestLayoutState,
|
||||
cx: &mut WindowContext,
|
||||
) -> E::PrepaintState {
|
||||
self.element.prepaint(bounds, state, cx)
|
||||
self.element.prepaint(id, bounds, state, cx)
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
id: Option<&GlobalElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
request_layout: &mut Self::RequestLayoutState,
|
||||
prepaint: &mut Self::PrepaintState,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
self.element.paint(bounds, request_layout, prepaint, cx)
|
||||
self.element.paint(id, bounds, request_layout, prepaint, cx)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2347,27 +2384,37 @@ where
|
||||
type RequestLayoutState = E::RequestLayoutState;
|
||||
type PrepaintState = E::PrepaintState;
|
||||
|
||||
fn request_layout(&mut self, cx: &mut WindowContext) -> (LayoutId, Self::RequestLayoutState) {
|
||||
self.element.request_layout(cx)
|
||||
fn id(&self) -> Option<ElementId> {
|
||||
self.element.id()
|
||||
}
|
||||
|
||||
fn request_layout(
|
||||
&mut self,
|
||||
id: Option<&GlobalElementId>,
|
||||
cx: &mut WindowContext,
|
||||
) -> (LayoutId, Self::RequestLayoutState) {
|
||||
self.element.request_layout(id, cx)
|
||||
}
|
||||
|
||||
fn prepaint(
|
||||
&mut self,
|
||||
id: Option<&GlobalElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
state: &mut Self::RequestLayoutState,
|
||||
cx: &mut WindowContext,
|
||||
) -> E::PrepaintState {
|
||||
self.element.prepaint(bounds, state, cx)
|
||||
self.element.prepaint(id, bounds, state, cx)
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
id: Option<&GlobalElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
request_layout: &mut Self::RequestLayoutState,
|
||||
prepaint: &mut Self::PrepaintState,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
self.element.paint(bounds, request_layout, prepaint, cx);
|
||||
self.element.paint(id, bounds, request_layout, prepaint, cx);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,9 +3,10 @@ use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{
|
||||
point, px, size, AbsoluteLength, Asset, Bounds, DefiniteLength, DevicePixels, Element, Hitbox,
|
||||
ImageData, InteractiveElement, Interactivity, IntoElement, LayoutId, Length, Pixels, SharedUri,
|
||||
Size, StyleRefinement, Styled, SvgSize, UriOrPath, WindowContext,
|
||||
point, px, size, AbsoluteLength, Asset, Bounds, DefiniteLength, DevicePixels, Element,
|
||||
ElementId, GlobalElementId, Hitbox, ImageData, InteractiveElement, Interactivity, IntoElement,
|
||||
LayoutId, Length, Pixels, SharedUri, Size, StyleRefinement, Styled, SvgSize, UriOrPath,
|
||||
WindowContext,
|
||||
};
|
||||
use futures::{AsyncReadExt, Future};
|
||||
use image::{ImageBuffer, ImageError};
|
||||
@@ -232,42 +233,54 @@ impl Element for Img {
|
||||
type RequestLayoutState = ();
|
||||
type PrepaintState = Option<Hitbox>;
|
||||
|
||||
fn request_layout(&mut self, cx: &mut WindowContext) -> (LayoutId, Self::RequestLayoutState) {
|
||||
let layout_id = self.interactivity.request_layout(cx, |mut style, cx| {
|
||||
if let Some(data) = self.source.data(cx) {
|
||||
let image_size = data.size();
|
||||
match (style.size.width, style.size.height) {
|
||||
(Length::Auto, Length::Auto) => {
|
||||
style.size = Size {
|
||||
width: Length::Definite(DefiniteLength::Absolute(
|
||||
AbsoluteLength::Pixels(px(image_size.width.0 as f32)),
|
||||
)),
|
||||
height: Length::Definite(DefiniteLength::Absolute(
|
||||
AbsoluteLength::Pixels(px(image_size.height.0 as f32)),
|
||||
)),
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
fn id(&self) -> Option<ElementId> {
|
||||
self.interactivity.element_id.clone()
|
||||
}
|
||||
|
||||
cx.request_layout(&style, [])
|
||||
});
|
||||
fn request_layout(
|
||||
&mut self,
|
||||
global_id: Option<&GlobalElementId>,
|
||||
cx: &mut WindowContext,
|
||||
) -> (LayoutId, Self::RequestLayoutState) {
|
||||
let layout_id = self
|
||||
.interactivity
|
||||
.request_layout(global_id, cx, |mut style, cx| {
|
||||
if let Some(data) = self.source.data(cx) {
|
||||
let image_size = data.size();
|
||||
match (style.size.width, style.size.height) {
|
||||
(Length::Auto, Length::Auto) => {
|
||||
style.size = Size {
|
||||
width: Length::Definite(DefiniteLength::Absolute(
|
||||
AbsoluteLength::Pixels(px(image_size.width.0 as f32)),
|
||||
)),
|
||||
height: Length::Definite(DefiniteLength::Absolute(
|
||||
AbsoluteLength::Pixels(px(image_size.height.0 as f32)),
|
||||
)),
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
cx.request_layout(&style, [])
|
||||
});
|
||||
(layout_id, ())
|
||||
}
|
||||
|
||||
fn prepaint(
|
||||
&mut self,
|
||||
global_id: Option<&GlobalElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
_request_layout: &mut Self::RequestLayoutState,
|
||||
cx: &mut WindowContext,
|
||||
) -> Option<Hitbox> {
|
||||
self.interactivity
|
||||
.prepaint(bounds, bounds.size, cx, |_, _, hitbox, _| hitbox)
|
||||
.prepaint(global_id, bounds, bounds.size, cx, |_, _, hitbox, _| hitbox)
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
global_id: Option<&GlobalElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
_: &mut Self::RequestLayoutState,
|
||||
hitbox: &mut Self::PrepaintState,
|
||||
@@ -275,7 +288,7 @@ impl Element for Img {
|
||||
) {
|
||||
let source = self.source.clone();
|
||||
self.interactivity
|
||||
.paint(bounds, hitbox.as_ref(), cx, |style, cx| {
|
||||
.paint(global_id, bounds, hitbox.as_ref(), cx, |style, cx| {
|
||||
let corner_radii = style.corner_radii.to_pixels(bounds.size, cx.rem_size());
|
||||
|
||||
if let Some(data) = source.data(cx) {
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
|
||||
use crate::{
|
||||
point, px, size, AnyElement, AvailableSpace, Bounds, ContentMask, DispatchPhase, Edges,
|
||||
Element, FocusHandle, Hitbox, IntoElement, Pixels, Point, ScrollWheelEvent, Size, Style,
|
||||
StyleRefinement, Styled, WindowContext,
|
||||
Element, FocusHandle, GlobalElementId, Hitbox, IntoElement, Pixels, Point, ScrollWheelEvent,
|
||||
Size, Style, StyleRefinement, Styled, WindowContext,
|
||||
};
|
||||
use collections::VecDeque;
|
||||
use refineable::Refineable as _;
|
||||
@@ -704,8 +704,13 @@ impl Element for List {
|
||||
type RequestLayoutState = ();
|
||||
type PrepaintState = ListPrepaintState;
|
||||
|
||||
fn id(&self) -> Option<crate::ElementId> {
|
||||
None
|
||||
}
|
||||
|
||||
fn request_layout(
|
||||
&mut self,
|
||||
_id: Option<&GlobalElementId>,
|
||||
cx: &mut crate::WindowContext,
|
||||
) -> (crate::LayoutId, Self::RequestLayoutState) {
|
||||
let layout_id = match self.sizing_behavior {
|
||||
@@ -770,6 +775,7 @@ impl Element for List {
|
||||
|
||||
fn prepaint(
|
||||
&mut self,
|
||||
_id: Option<&GlobalElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
_: &mut Self::RequestLayoutState,
|
||||
cx: &mut WindowContext,
|
||||
@@ -812,6 +818,7 @@ impl Element for List {
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
_id: Option<&GlobalElementId>,
|
||||
bounds: Bounds<crate::Pixels>,
|
||||
_: &mut Self::RequestLayoutState,
|
||||
prepaint: &mut Self::PrepaintState,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::{
|
||||
geometry::Negate as _, point, px, radians, size, Bounds, Element, Hitbox, InteractiveElement,
|
||||
Interactivity, IntoElement, LayoutId, Pixels, Point, Radians, SharedString, Size,
|
||||
StyleRefinement, Styled, TransformationMatrix, WindowContext,
|
||||
geometry::Negate as _, point, px, radians, size, Bounds, Element, GlobalElementId, Hitbox,
|
||||
InteractiveElement, Interactivity, IntoElement, LayoutId, Pixels, Point, Radians, SharedString,
|
||||
Size, StyleRefinement, Styled, TransformationMatrix, WindowContext,
|
||||
};
|
||||
use util::ResultExt;
|
||||
|
||||
@@ -40,25 +40,35 @@ impl Element for Svg {
|
||||
type RequestLayoutState = ();
|
||||
type PrepaintState = Option<Hitbox>;
|
||||
|
||||
fn request_layout(&mut self, cx: &mut WindowContext) -> (LayoutId, Self::RequestLayoutState) {
|
||||
fn id(&self) -> Option<crate::ElementId> {
|
||||
self.interactivity.element_id.clone()
|
||||
}
|
||||
|
||||
fn request_layout(
|
||||
&mut self,
|
||||
global_id: Option<&GlobalElementId>,
|
||||
cx: &mut WindowContext,
|
||||
) -> (LayoutId, Self::RequestLayoutState) {
|
||||
let layout_id = self
|
||||
.interactivity
|
||||
.request_layout(cx, |style, cx| cx.request_layout(&style, None));
|
||||
.request_layout(global_id, cx, |style, cx| cx.request_layout(&style, None));
|
||||
(layout_id, ())
|
||||
}
|
||||
|
||||
fn prepaint(
|
||||
&mut self,
|
||||
global_id: Option<&GlobalElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
_request_layout: &mut Self::RequestLayoutState,
|
||||
cx: &mut WindowContext,
|
||||
) -> Option<Hitbox> {
|
||||
self.interactivity
|
||||
.prepaint(bounds, bounds.size, cx, |_, _, hitbox, _| hitbox)
|
||||
.prepaint(global_id, bounds, bounds.size, cx, |_, _, hitbox, _| hitbox)
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
global_id: Option<&GlobalElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
_request_layout: &mut Self::RequestLayoutState,
|
||||
hitbox: &mut Option<Hitbox>,
|
||||
@@ -67,7 +77,7 @@ impl Element for Svg {
|
||||
Self: Sized,
|
||||
{
|
||||
self.interactivity
|
||||
.paint(bounds, hitbox.as_ref(), cx, |style, cx| {
|
||||
.paint(global_id, bounds, hitbox.as_ref(), cx, |style, cx| {
|
||||
if let Some((path, color)) = self.path.as_ref().zip(style.text.color) {
|
||||
let transformation = self
|
||||
.transformation
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use crate::{
|
||||
ActiveTooltip, AnyTooltip, AnyView, Bounds, DispatchPhase, Element, ElementId, HighlightStyle,
|
||||
Hitbox, IntoElement, LayoutId, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Point,
|
||||
SharedString, Size, TextRun, TextStyle, WhiteSpace, WindowContext, WrappedLine, TOOLTIP_DELAY,
|
||||
ActiveTooltip, AnyTooltip, AnyView, Bounds, DispatchPhase, Element, ElementId, GlobalElementId,
|
||||
HighlightStyle, Hitbox, IntoElement, LayoutId, MouseDownEvent, MouseMoveEvent, MouseUpEvent,
|
||||
Pixels, Point, SharedString, Size, TextRun, TextStyle, WhiteSpace, WindowContext, WrappedLine,
|
||||
TOOLTIP_DELAY,
|
||||
};
|
||||
use anyhow::anyhow;
|
||||
use parking_lot::{Mutex, MutexGuard};
|
||||
@@ -19,7 +20,15 @@ impl Element for &'static str {
|
||||
type RequestLayoutState = TextState;
|
||||
type PrepaintState = ();
|
||||
|
||||
fn request_layout(&mut self, cx: &mut WindowContext) -> (LayoutId, Self::RequestLayoutState) {
|
||||
fn id(&self) -> Option<ElementId> {
|
||||
None
|
||||
}
|
||||
|
||||
fn request_layout(
|
||||
&mut self,
|
||||
_id: Option<&GlobalElementId>,
|
||||
cx: &mut WindowContext,
|
||||
) -> (LayoutId, Self::RequestLayoutState) {
|
||||
let mut state = TextState::default();
|
||||
let layout_id = state.layout(SharedString::from(*self), None, cx);
|
||||
(layout_id, state)
|
||||
@@ -27,6 +36,7 @@ impl Element for &'static str {
|
||||
|
||||
fn prepaint(
|
||||
&mut self,
|
||||
_id: Option<&GlobalElementId>,
|
||||
_bounds: Bounds<Pixels>,
|
||||
_text_state: &mut Self::RequestLayoutState,
|
||||
_cx: &mut WindowContext,
|
||||
@@ -35,6 +45,7 @@ impl Element for &'static str {
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
_id: Option<&GlobalElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
text_state: &mut TextState,
|
||||
_: &mut (),
|
||||
@@ -64,7 +75,17 @@ impl Element for SharedString {
|
||||
type RequestLayoutState = TextState;
|
||||
type PrepaintState = ();
|
||||
|
||||
fn request_layout(&mut self, cx: &mut WindowContext) -> (LayoutId, Self::RequestLayoutState) {
|
||||
fn id(&self) -> Option<ElementId> {
|
||||
None
|
||||
}
|
||||
|
||||
fn request_layout(
|
||||
&mut self,
|
||||
|
||||
_id: Option<&GlobalElementId>,
|
||||
|
||||
cx: &mut WindowContext,
|
||||
) -> (LayoutId, Self::RequestLayoutState) {
|
||||
let mut state = TextState::default();
|
||||
let layout_id = state.layout(self.clone(), None, cx);
|
||||
(layout_id, state)
|
||||
@@ -72,6 +93,7 @@ impl Element for SharedString {
|
||||
|
||||
fn prepaint(
|
||||
&mut self,
|
||||
_id: Option<&GlobalElementId>,
|
||||
_bounds: Bounds<Pixels>,
|
||||
_text_state: &mut Self::RequestLayoutState,
|
||||
_cx: &mut WindowContext,
|
||||
@@ -80,6 +102,7 @@ impl Element for SharedString {
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
_id: Option<&GlobalElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
text_state: &mut Self::RequestLayoutState,
|
||||
_: &mut Self::PrepaintState,
|
||||
@@ -150,7 +173,17 @@ impl Element for StyledText {
|
||||
type RequestLayoutState = TextState;
|
||||
type PrepaintState = ();
|
||||
|
||||
fn request_layout(&mut self, cx: &mut WindowContext) -> (LayoutId, Self::RequestLayoutState) {
|
||||
fn id(&self) -> Option<ElementId> {
|
||||
None
|
||||
}
|
||||
|
||||
fn request_layout(
|
||||
&mut self,
|
||||
|
||||
_id: Option<&GlobalElementId>,
|
||||
|
||||
cx: &mut WindowContext,
|
||||
) -> (LayoutId, Self::RequestLayoutState) {
|
||||
let mut state = TextState::default();
|
||||
let layout_id = state.layout(self.text.clone(), self.runs.take(), cx);
|
||||
(layout_id, state)
|
||||
@@ -158,6 +191,7 @@ impl Element for StyledText {
|
||||
|
||||
fn prepaint(
|
||||
&mut self,
|
||||
_id: Option<&GlobalElementId>,
|
||||
_bounds: Bounds<Pixels>,
|
||||
_state: &mut Self::RequestLayoutState,
|
||||
_cx: &mut WindowContext,
|
||||
@@ -166,6 +200,7 @@ impl Element for StyledText {
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
_id: Option<&GlobalElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
text_state: &mut Self::RequestLayoutState,
|
||||
_: &mut Self::PrepaintState,
|
||||
@@ -404,18 +439,27 @@ impl Element for InteractiveText {
|
||||
type RequestLayoutState = TextState;
|
||||
type PrepaintState = Hitbox;
|
||||
|
||||
fn request_layout(&mut self, cx: &mut WindowContext) -> (LayoutId, Self::RequestLayoutState) {
|
||||
self.text.request_layout(cx)
|
||||
fn id(&self) -> Option<ElementId> {
|
||||
Some(self.element_id.clone())
|
||||
}
|
||||
|
||||
fn request_layout(
|
||||
&mut self,
|
||||
_id: Option<&GlobalElementId>,
|
||||
cx: &mut WindowContext,
|
||||
) -> (LayoutId, Self::RequestLayoutState) {
|
||||
self.text.request_layout(None, cx)
|
||||
}
|
||||
|
||||
fn prepaint(
|
||||
&mut self,
|
||||
global_id: Option<&GlobalElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
state: &mut Self::RequestLayoutState,
|
||||
cx: &mut WindowContext,
|
||||
) -> Hitbox {
|
||||
cx.with_element_state::<InteractiveTextState, _>(
|
||||
Some(self.element_id.clone()),
|
||||
cx.with_optional_element_state::<InteractiveTextState, _>(
|
||||
global_id,
|
||||
|interactive_state, cx| {
|
||||
let interactive_state = interactive_state
|
||||
.map(|interactive_state| interactive_state.unwrap_or_default());
|
||||
@@ -429,7 +473,7 @@ impl Element for InteractiveText {
|
||||
}
|
||||
}
|
||||
|
||||
self.text.prepaint(bounds, state, cx);
|
||||
self.text.prepaint(None, bounds, state, cx);
|
||||
let hitbox = cx.insert_hitbox(bounds, false);
|
||||
(hitbox, interactive_state)
|
||||
},
|
||||
@@ -438,15 +482,16 @@ impl Element for InteractiveText {
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
global_id: Option<&GlobalElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
text_state: &mut Self::RequestLayoutState,
|
||||
hitbox: &mut Hitbox,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
cx.with_element_state::<InteractiveTextState, _>(
|
||||
Some(self.element_id.clone()),
|
||||
global_id.unwrap(),
|
||||
|interactive_state, cx| {
|
||||
let mut interactive_state = interactive_state.unwrap().unwrap_or_default();
|
||||
let mut interactive_state = interactive_state.unwrap_or_default();
|
||||
if let Some(click_listener) = self.click_listener.take() {
|
||||
let mouse_position = cx.mouse_position();
|
||||
if let Some(ix) = text_state.index_for_position(bounds, mouse_position) {
|
||||
@@ -576,9 +621,9 @@ impl Element for InteractiveText {
|
||||
});
|
||||
}
|
||||
|
||||
self.text.paint(bounds, text_state, &mut (), cx);
|
||||
self.text.paint(None, bounds, text_state, &mut (), cx);
|
||||
|
||||
((), Some(interactive_state))
|
||||
((), interactive_state)
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
//! elements with uniform height.
|
||||
|
||||
use crate::{
|
||||
point, px, size, AnyElement, AvailableSpace, Bounds, ContentMask, Element, ElementId, Hitbox,
|
||||
InteractiveElement, Interactivity, IntoElement, LayoutId, Pixels, Render, ScrollHandle, Size,
|
||||
StyleRefinement, Styled, View, ViewContext, WindowContext,
|
||||
point, px, size, AnyElement, AvailableSpace, Bounds, ContentMask, Element, ElementId,
|
||||
GlobalElementId, Hitbox, InteractiveElement, Interactivity, IntoElement, LayoutId, Pixels,
|
||||
Render, ScrollHandle, Size, StyleRefinement, Styled, View, ViewContext, WindowContext,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
use std::{cell::RefCell, cmp, ops::Range, rc::Rc};
|
||||
@@ -107,26 +107,38 @@ impl Element for UniformList {
|
||||
type RequestLayoutState = UniformListFrameState;
|
||||
type PrepaintState = Option<Hitbox>;
|
||||
|
||||
fn request_layout(&mut self, cx: &mut WindowContext) -> (LayoutId, Self::RequestLayoutState) {
|
||||
fn id(&self) -> Option<ElementId> {
|
||||
self.interactivity.element_id.clone()
|
||||
}
|
||||
|
||||
fn request_layout(
|
||||
&mut self,
|
||||
global_id: Option<&GlobalElementId>,
|
||||
cx: &mut WindowContext,
|
||||
) -> (LayoutId, Self::RequestLayoutState) {
|
||||
let max_items = self.item_count;
|
||||
let item_size = self.measure_item(None, cx);
|
||||
let layout_id = self.interactivity.request_layout(cx, |style, cx| {
|
||||
cx.request_measured_layout(style, move |known_dimensions, available_space, _cx| {
|
||||
let desired_height = item_size.height * max_items;
|
||||
let width = known_dimensions
|
||||
.width
|
||||
.unwrap_or(match available_space.width {
|
||||
AvailableSpace::Definite(x) => x,
|
||||
AvailableSpace::MinContent | AvailableSpace::MaxContent => item_size.width,
|
||||
});
|
||||
let layout_id = self
|
||||
.interactivity
|
||||
.request_layout(global_id, cx, |style, cx| {
|
||||
cx.request_measured_layout(style, move |known_dimensions, available_space, _cx| {
|
||||
let desired_height = item_size.height * max_items;
|
||||
let width = known_dimensions
|
||||
.width
|
||||
.unwrap_or(match available_space.width {
|
||||
AvailableSpace::Definite(x) => x,
|
||||
AvailableSpace::MinContent | AvailableSpace::MaxContent => {
|
||||
item_size.width
|
||||
}
|
||||
});
|
||||
|
||||
let height = match available_space.height {
|
||||
AvailableSpace::Definite(height) => desired_height.min(height),
|
||||
AvailableSpace::MinContent | AvailableSpace::MaxContent => desired_height,
|
||||
};
|
||||
size(width, height)
|
||||
})
|
||||
});
|
||||
let height = match available_space.height {
|
||||
AvailableSpace::Definite(height) => desired_height.min(height),
|
||||
AvailableSpace::MinContent | AvailableSpace::MaxContent => desired_height,
|
||||
};
|
||||
size(width, height)
|
||||
})
|
||||
});
|
||||
|
||||
(
|
||||
layout_id,
|
||||
@@ -139,11 +151,12 @@ impl Element for UniformList {
|
||||
|
||||
fn prepaint(
|
||||
&mut self,
|
||||
global_id: Option<&GlobalElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
frame_state: &mut Self::RequestLayoutState,
|
||||
cx: &mut WindowContext,
|
||||
) -> Option<Hitbox> {
|
||||
let style = self.interactivity.compute_style(None, cx);
|
||||
let style = self.interactivity.compute_style(global_id, None, cx);
|
||||
let border = style.border_widths.to_pixels(cx.rem_size());
|
||||
let padding = style.padding.to_pixels(bounds.size.into(), cx.rem_size());
|
||||
|
||||
@@ -167,6 +180,7 @@ impl Element for UniformList {
|
||||
.and_then(|handle| handle.deferred_scroll_to_item.take());
|
||||
|
||||
self.interactivity.prepaint(
|
||||
global_id,
|
||||
bounds,
|
||||
content_size,
|
||||
cx,
|
||||
@@ -236,13 +250,14 @@ impl Element for UniformList {
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
global_id: Option<&GlobalElementId>,
|
||||
bounds: Bounds<crate::Pixels>,
|
||||
request_layout: &mut Self::RequestLayoutState,
|
||||
hitbox: &mut Option<Hitbox>,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
self.interactivity
|
||||
.paint(bounds, hitbox.as_ref(), cx, |_, cx| {
|
||||
.paint(global_id, bounds, hitbox.as_ref(), cx, |_, cx| {
|
||||
for item in &mut request_layout.items {
|
||||
item.paint(cx);
|
||||
}
|
||||
|
||||
@@ -330,19 +330,12 @@ impl<T> Flatten<T> for Result<T> {
|
||||
|
||||
/// A marker trait for types that can be stored in GPUI's global state.
|
||||
///
|
||||
/// This trait exists to provide type-safe access to globals by restricting
|
||||
/// the scope from which they can be accessed. For instance, the actual type
|
||||
/// that implements [`Global`] can be private, with public accessor functions
|
||||
/// that enforce correct usage.
|
||||
///
|
||||
/// Implement this on types you want to store in the context as a global.
|
||||
pub trait Global: 'static + Sized {
|
||||
/// Access the global of the implementing type. Panics if a global for that type has not been assigned.
|
||||
fn get(cx: &AppContext) -> &Self {
|
||||
cx.global()
|
||||
}
|
||||
|
||||
/// Updates the global of the implementing type with a closure. Unlike `global_mut`, this method provides
|
||||
/// your closure with mutable access to the `AppContext` and the global simultaneously.
|
||||
fn update<C, R>(cx: &mut C, f: impl FnOnce(&mut Self, &mut C) -> R) -> R
|
||||
where
|
||||
C: BorrowAppContext,
|
||||
{
|
||||
cx.update_global(f)
|
||||
}
|
||||
pub trait Global: 'static {
|
||||
// This trait is intentionally left empty, by virtue of being a marker trait.
|
||||
}
|
||||
|
||||
@@ -209,6 +209,7 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
|
||||
fn activate(&self);
|
||||
fn is_active(&self) -> bool;
|
||||
fn set_title(&mut self, title: &str);
|
||||
fn set_app_id(&mut self, app_id: &str);
|
||||
fn set_background_appearance(&mut self, background_appearance: WindowBackgroundAppearance);
|
||||
fn set_edited(&mut self, edited: bool);
|
||||
fn show_character_palette(&self);
|
||||
@@ -557,6 +558,9 @@ pub struct WindowOptions {
|
||||
|
||||
/// The appearance of the window background.
|
||||
pub window_background: WindowBackgroundAppearance,
|
||||
|
||||
/// Application identifier of the window. Can by used by desktop environments to group applications together.
|
||||
pub app_id: Option<String>,
|
||||
}
|
||||
|
||||
/// The variables that can be configured when creating a new window
|
||||
@@ -599,6 +603,7 @@ impl Default for WindowOptions {
|
||||
display_id: None,
|
||||
fullscreen: false,
|
||||
window_background: WindowBackgroundAppearance::default(),
|
||||
app_id: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -693,7 +698,7 @@ pub struct PathPromptOptions {
|
||||
}
|
||||
|
||||
/// What kind of prompt styling to show
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||
pub enum PromptLevel {
|
||||
/// A prompt that is shown when the user should be notified of something
|
||||
Info,
|
||||
@@ -703,10 +708,14 @@ pub enum PromptLevel {
|
||||
|
||||
/// A prompt that is shown when a critical problem has occurred
|
||||
Critical,
|
||||
|
||||
/// A prompt that is shown when asking the user to confirm a potentially destructive action
|
||||
/// (overwriting a file for example)
|
||||
Destructive,
|
||||
}
|
||||
|
||||
/// The style of the cursor (pointer)
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum CursorStyle {
|
||||
/// The default cursor
|
||||
Arrow,
|
||||
|
||||
@@ -90,7 +90,7 @@ impl PlatformTextSystem for CosmicTextSystem {
|
||||
let candidates = if let Some(font_ids) = state.font_ids_by_family_cache.get(&font.family) {
|
||||
font_ids.as_slice()
|
||||
} else {
|
||||
let font_ids = state.load_family(&font.family, font.features)?;
|
||||
let font_ids = state.load_family(&font.family, &font.features)?;
|
||||
state
|
||||
.font_ids_by_family_cache
|
||||
.insert(font.family.clone(), font_ids);
|
||||
@@ -211,7 +211,7 @@ impl CosmicTextSystemState {
|
||||
fn load_family(
|
||||
&mut self,
|
||||
name: &str,
|
||||
_features: FontFeatures,
|
||||
_features: &FontFeatures,
|
||||
) -> Result<SmallVec<[FontId; 4]>> {
|
||||
// TODO: Determine the proper system UI font.
|
||||
let name = if name == ".SystemUIFont" {
|
||||
|
||||
@@ -28,6 +28,7 @@ use futures::channel::oneshot;
|
||||
use parking_lot::Mutex;
|
||||
use time::UtcOffset;
|
||||
use wayland_client::Connection;
|
||||
use wayland_protocols::wp::cursor_shape::v1::client::wp_cursor_shape_device_v1::Shape;
|
||||
use xkbcommon::xkb::{self, Keycode, Keysym, State};
|
||||
|
||||
use crate::platform::linux::wayland::WaylandClient;
|
||||
@@ -501,6 +502,58 @@ pub(super) unsafe fn read_fd(mut fd: FileDescriptor) -> Result<String> {
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
impl CursorStyle {
|
||||
pub(super) fn to_shape(&self) -> Shape {
|
||||
match self {
|
||||
CursorStyle::Arrow => Shape::Default,
|
||||
CursorStyle::IBeam => Shape::Text,
|
||||
CursorStyle::Crosshair => Shape::Crosshair,
|
||||
CursorStyle::ClosedHand => Shape::Grabbing,
|
||||
CursorStyle::OpenHand => Shape::Grab,
|
||||
CursorStyle::PointingHand => Shape::Pointer,
|
||||
CursorStyle::ResizeLeft => Shape::WResize,
|
||||
CursorStyle::ResizeRight => Shape::EResize,
|
||||
CursorStyle::ResizeLeftRight => Shape::EwResize,
|
||||
CursorStyle::ResizeUp => Shape::NResize,
|
||||
CursorStyle::ResizeDown => Shape::SResize,
|
||||
CursorStyle::ResizeUpDown => Shape::NsResize,
|
||||
CursorStyle::DisappearingItem => Shape::Grabbing, // todo(linux) - couldn't find equivalent icon in linux
|
||||
CursorStyle::IBeamCursorForVerticalLayout => Shape::VerticalText,
|
||||
CursorStyle::OperationNotAllowed => Shape::NotAllowed,
|
||||
CursorStyle::DragLink => Shape::Alias,
|
||||
CursorStyle::DragCopy => Shape::Copy,
|
||||
CursorStyle::ContextualMenu => Shape::ContextMenu,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn to_icon_name(&self) -> String {
|
||||
// Based on cursor names from https://gitlab.gnome.org/GNOME/adwaita-icon-theme (GNOME)
|
||||
// and https://github.com/KDE/breeze (KDE). Both of them seem to be also derived from
|
||||
// Web CSS cursor names: https://developer.mozilla.org/en-US/docs/Web/CSS/cursor#values
|
||||
match self {
|
||||
CursorStyle::Arrow => "arrow",
|
||||
CursorStyle::IBeam => "text",
|
||||
CursorStyle::Crosshair => "crosshair",
|
||||
CursorStyle::ClosedHand => "grabbing",
|
||||
CursorStyle::OpenHand => "grab",
|
||||
CursorStyle::PointingHand => "pointer",
|
||||
CursorStyle::ResizeLeft => "w-resize",
|
||||
CursorStyle::ResizeRight => "e-resize",
|
||||
CursorStyle::ResizeLeftRight => "ew-resize",
|
||||
CursorStyle::ResizeUp => "n-resize",
|
||||
CursorStyle::ResizeDown => "s-resize",
|
||||
CursorStyle::ResizeUpDown => "ns-resize",
|
||||
CursorStyle::DisappearingItem => "grabbing", // todo(linux) - couldn't find equivalent icon in linux
|
||||
CursorStyle::IBeamCursorForVerticalLayout => "vertical-text",
|
||||
CursorStyle::OperationNotAllowed => "not-allowed",
|
||||
CursorStyle::DragLink => "alias",
|
||||
CursorStyle::DragCopy => "copy",
|
||||
CursorStyle::ContextualMenu => "context-menu",
|
||||
}
|
||||
.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
impl Keystroke {
|
||||
pub(super) fn from_xkb(state: &State, modifiers: Modifiers, keycode: Keycode) -> Self {
|
||||
let mut modifiers = modifiers;
|
||||
|
||||
@@ -34,6 +34,10 @@ use wayland_client::{
|
||||
},
|
||||
Connection, Dispatch, Proxy, QueueHandle,
|
||||
};
|
||||
use wayland_protocols::wp::cursor_shape::v1::client::wp_cursor_shape_device_v1::Shape;
|
||||
use wayland_protocols::wp::cursor_shape::v1::client::{
|
||||
wp_cursor_shape_device_v1, wp_cursor_shape_manager_v1,
|
||||
};
|
||||
use wayland_protocols::wp::fractional_scale::v1::client::{
|
||||
wp_fractional_scale_manager_v1, wp_fractional_scale_v1,
|
||||
};
|
||||
@@ -68,6 +72,7 @@ const MIN_KEYCODE: u32 = 8;
|
||||
pub struct Globals {
|
||||
pub qh: QueueHandle<WaylandClientStatePtr>,
|
||||
pub compositor: wl_compositor::WlCompositor,
|
||||
pub cursor_shape_manager: Option<wp_cursor_shape_manager_v1::WpCursorShapeManagerV1>,
|
||||
pub data_device_manager: Option<wl_data_device_manager::WlDataDeviceManager>,
|
||||
pub wm_base: xdg_wm_base::XdgWmBase,
|
||||
pub shm: wl_shm::WlShm,
|
||||
@@ -93,6 +98,7 @@ impl Globals {
|
||||
(),
|
||||
)
|
||||
.unwrap(),
|
||||
cursor_shape_manager: globals.bind(&qh, 1..=1, ()).ok(),
|
||||
data_device_manager: globals
|
||||
.bind(
|
||||
&qh,
|
||||
@@ -112,9 +118,11 @@ impl Globals {
|
||||
}
|
||||
|
||||
pub(crate) struct WaylandClientState {
|
||||
serial: u32,
|
||||
serial: u32, // todo(linux): storing a general serial is wrong
|
||||
pointer_serial: u32,
|
||||
globals: Globals,
|
||||
wl_pointer: Option<wl_pointer::WlPointer>,
|
||||
cursor_shape_device: Option<wp_cursor_shape_device_v1::WpCursorShapeDeviceV1>,
|
||||
data_device: Option<wl_data_device::WlDataDevice>,
|
||||
// Surface to Window mapping
|
||||
windows: HashMap<ObjectId, WaylandWindowStatePtr>,
|
||||
@@ -137,7 +145,7 @@ pub(crate) struct WaylandClientState {
|
||||
mouse_focused_window: Option<WaylandWindowStatePtr>,
|
||||
keyboard_focused_window: Option<WaylandWindowStatePtr>,
|
||||
loop_handle: LoopHandle<'static, WaylandClientStatePtr>,
|
||||
cursor_icon_name: String,
|
||||
cursor_style: Option<CursorStyle>,
|
||||
cursor: Cursor,
|
||||
clipboard: Option<Clipboard>,
|
||||
primary: Option<Primary>,
|
||||
@@ -161,7 +169,7 @@ pub(crate) struct KeyRepeat {
|
||||
characters_per_second: u32,
|
||||
delay: Duration,
|
||||
current_id: u64,
|
||||
current_keysym: Option<xkb::Keysym>,
|
||||
current_keycode: Option<xkb::Keycode>,
|
||||
}
|
||||
|
||||
/// This struct is required to conform to Rust's orphan rules, so we can dispatch on the state but hand the
|
||||
@@ -197,6 +205,9 @@ impl WaylandClientStatePtr {
|
||||
if let Some(wl_pointer) = &state.wl_pointer {
|
||||
wl_pointer.release();
|
||||
}
|
||||
if let Some(cursor_shape_device) = &state.cursor_shape_device {
|
||||
cursor_shape_device.destroy();
|
||||
}
|
||||
if let Some(data_device) = &state.data_device {
|
||||
data_device.release();
|
||||
}
|
||||
@@ -289,8 +300,10 @@ impl WaylandClient {
|
||||
|
||||
let mut state = Rc::new(RefCell::new(WaylandClientState {
|
||||
serial: 0,
|
||||
pointer_serial: 0,
|
||||
globals,
|
||||
wl_pointer: None,
|
||||
cursor_shape_device: None,
|
||||
data_device,
|
||||
output_scales: outputs,
|
||||
windows: HashMap::default(),
|
||||
@@ -310,7 +323,7 @@ impl WaylandClient {
|
||||
characters_per_second: 16,
|
||||
delay: Duration::from_millis(500),
|
||||
current_id: 0,
|
||||
current_keysym: None,
|
||||
current_keycode: None,
|
||||
},
|
||||
modifiers: Modifiers {
|
||||
shift: false,
|
||||
@@ -330,8 +343,8 @@ impl WaylandClient {
|
||||
mouse_focused_window: None,
|
||||
keyboard_focused_window: None,
|
||||
loop_handle: handle.clone(),
|
||||
cursor_icon_name: "arrow".to_string(),
|
||||
enter_token: None,
|
||||
cursor_style: None,
|
||||
cursor,
|
||||
clipboard: Some(clipboard),
|
||||
primary: Some(primary),
|
||||
@@ -375,39 +388,28 @@ impl LinuxClient for WaylandClient {
|
||||
}
|
||||
|
||||
fn set_cursor_style(&self, style: CursorStyle) {
|
||||
// Based on cursor names from https://gitlab.gnome.org/GNOME/adwaita-icon-theme (GNOME)
|
||||
// and https://github.com/KDE/breeze (KDE). Both of them seem to be also derived from
|
||||
// Web CSS cursor names: https://developer.mozilla.org/en-US/docs/Web/CSS/cursor#values
|
||||
let cursor_icon_name = match style {
|
||||
CursorStyle::Arrow => "arrow",
|
||||
CursorStyle::IBeam => "text",
|
||||
CursorStyle::Crosshair => "crosshair",
|
||||
CursorStyle::ClosedHand => "grabbing",
|
||||
CursorStyle::OpenHand => "grab",
|
||||
CursorStyle::PointingHand => "pointer",
|
||||
CursorStyle::ResizeLeft => "w-resize",
|
||||
CursorStyle::ResizeRight => "e-resize",
|
||||
CursorStyle::ResizeLeftRight => "ew-resize",
|
||||
CursorStyle::ResizeUp => "n-resize",
|
||||
CursorStyle::ResizeDown => "s-resize",
|
||||
CursorStyle::ResizeUpDown => "ns-resize",
|
||||
CursorStyle::DisappearingItem => "grabbing", // todo(linux) - couldn't find equivalent icon in linux
|
||||
CursorStyle::IBeamCursorForVerticalLayout => "vertical-text",
|
||||
CursorStyle::OperationNotAllowed => "not-allowed",
|
||||
CursorStyle::DragLink => "alias",
|
||||
CursorStyle::DragCopy => "copy",
|
||||
CursorStyle::ContextualMenu => "context-menu",
|
||||
}
|
||||
.to_string();
|
||||
|
||||
let mut state = self.0.borrow_mut();
|
||||
state.cursor_icon_name = cursor_icon_name.clone();
|
||||
if state.mouse_focused_window.is_some() {
|
||||
let wl_pointer = state
|
||||
.wl_pointer
|
||||
.clone()
|
||||
.expect("window is focused by pointer");
|
||||
state.cursor.set_icon(&wl_pointer, &cursor_icon_name);
|
||||
|
||||
let need_update = state
|
||||
.cursor_style
|
||||
.map_or(true, |current_style| current_style != style);
|
||||
|
||||
if need_update {
|
||||
let serial = state.pointer_serial;
|
||||
state.cursor_style = Some(style);
|
||||
|
||||
if let Some(cursor_shape_device) = &state.cursor_shape_device {
|
||||
cursor_shape_device.set_shape(serial, style.to_shape());
|
||||
} else if state.mouse_focused_window.is_some() {
|
||||
// cursor-shape-v1 isn't supported, set the cursor using a surface.
|
||||
let wl_pointer = state
|
||||
.wl_pointer
|
||||
.clone()
|
||||
.expect("window is focused by pointer");
|
||||
state
|
||||
.cursor
|
||||
.set_icon(&wl_pointer, serial, &style.to_icon_name());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -516,6 +518,8 @@ impl Dispatch<wl_registry::WlRegistry, GlobalListContents> for WaylandClientStat
|
||||
}
|
||||
|
||||
delegate_noop!(WaylandClientStatePtr: ignore wl_compositor::WlCompositor);
|
||||
delegate_noop!(WaylandClientStatePtr: ignore wp_cursor_shape_device_v1::WpCursorShapeDeviceV1);
|
||||
delegate_noop!(WaylandClientStatePtr: ignore wp_cursor_shape_manager_v1::WpCursorShapeManagerV1);
|
||||
delegate_noop!(WaylandClientStatePtr: ignore wl_data_device_manager::WlDataDeviceManager);
|
||||
delegate_noop!(WaylandClientStatePtr: ignore wl_shm::WlShm);
|
||||
delegate_noop!(WaylandClientStatePtr: ignore wl_shm_pool::WlShmPool);
|
||||
@@ -684,7 +688,13 @@ impl Dispatch<wl_seat::WlSeat, ()> for WaylandClientStatePtr {
|
||||
if capabilities.contains(wl_seat::Capability::Pointer) {
|
||||
let client = state.get_client();
|
||||
let mut state = client.borrow_mut();
|
||||
state.wl_pointer = Some(seat.get_pointer(qh, ()));
|
||||
let pointer = seat.get_pointer(qh, ());
|
||||
state.cursor_shape_device = state
|
||||
.globals
|
||||
.cursor_shape_manager
|
||||
.as_ref()
|
||||
.map(|cursor_shape_manager| cursor_shape_manager.get_pointer(&pointer, qh, ()));
|
||||
state.wl_pointer = Some(pointer);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -805,7 +815,7 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr {
|
||||
});
|
||||
|
||||
state.repeat.current_id += 1;
|
||||
state.repeat.current_keysym = Some(keysym);
|
||||
state.repeat.current_keycode = Some(keycode);
|
||||
|
||||
let rate = state.repeat.characters_per_second;
|
||||
let id = state.repeat.current_id;
|
||||
@@ -817,7 +827,7 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr {
|
||||
let mut client = this.get_client();
|
||||
let mut state = client.borrow_mut();
|
||||
let is_repeating = id == state.repeat.current_id
|
||||
&& state.repeat.current_keysym.is_some()
|
||||
&& state.repeat.current_keycode.is_some()
|
||||
&& state.keyboard_focused_window.is_some();
|
||||
|
||||
if !is_repeating {
|
||||
@@ -843,7 +853,9 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr {
|
||||
keystroke: Keystroke::from_xkb(keymap_state, state.modifiers, keycode),
|
||||
});
|
||||
|
||||
state.repeat.current_keysym = None;
|
||||
if state.repeat.current_keycode == Some(keycode) {
|
||||
state.repeat.current_keycode = None;
|
||||
}
|
||||
|
||||
drop(state);
|
||||
focused_window.handle_input(input);
|
||||
@@ -887,7 +899,6 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr {
|
||||
) {
|
||||
let mut client = this.get_client();
|
||||
let mut state = client.borrow_mut();
|
||||
let cursor_icon_name = state.cursor_icon_name.clone();
|
||||
|
||||
match event {
|
||||
wl_pointer::Event::Enter {
|
||||
@@ -897,17 +908,21 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr {
|
||||
surface_y,
|
||||
..
|
||||
} => {
|
||||
state.serial = serial;
|
||||
state.pointer_serial = serial;
|
||||
state.mouse_location = Some(point(px(surface_x as f32), px(surface_y as f32)));
|
||||
|
||||
if let Some(window) = get_window(&mut state, &surface.id()) {
|
||||
state.enter_token = Some(());
|
||||
state.mouse_focused_window = Some(window.clone());
|
||||
state.cursor.mark_dirty();
|
||||
state.cursor.set_serial_id(serial);
|
||||
state
|
||||
.cursor
|
||||
.set_icon(&wl_pointer, cursor_icon_name.as_str());
|
||||
if let Some(style) = state.cursor_style {
|
||||
if let Some(cursor_shape_device) = &state.cursor_shape_device {
|
||||
cursor_shape_device.set_shape(serial, style.to_shape());
|
||||
} else {
|
||||
state
|
||||
.cursor
|
||||
.set_icon(&wl_pointer, serial, &style.to_icon_name());
|
||||
}
|
||||
}
|
||||
drop(state);
|
||||
window.set_focused(true);
|
||||
}
|
||||
|
||||
@@ -8,9 +8,7 @@ use wayland_cursor::{CursorImageBuffer, CursorTheme};
|
||||
|
||||
pub(crate) struct Cursor {
|
||||
theme: Option<CursorTheme>,
|
||||
current_icon_name: Option<String>,
|
||||
surface: WlSurface,
|
||||
serial_id: u32,
|
||||
}
|
||||
|
||||
impl Drop for Cursor {
|
||||
@@ -24,65 +22,39 @@ impl Cursor {
|
||||
pub fn new(connection: &Connection, globals: &Globals, size: u32) -> Self {
|
||||
Self {
|
||||
theme: CursorTheme::load(&connection, globals.shm.clone(), size).log_err(),
|
||||
current_icon_name: None,
|
||||
surface: globals.compositor.create_surface(&globals.qh, ()),
|
||||
serial_id: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mark_dirty(&mut self) {
|
||||
self.current_icon_name = None;
|
||||
}
|
||||
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>;
|
||||
|
||||
pub fn set_serial_id(&mut self, serial_id: u32) {
|
||||
self.serial_id = serial_id;
|
||||
}
|
||||
|
||||
pub fn set_icon(&mut self, wl_pointer: &WlPointer, mut cursor_icon_name: &str) {
|
||||
let need_update = self
|
||||
.current_icon_name
|
||||
.as_ref()
|
||||
.map_or(true, |current_icon_name| {
|
||||
current_icon_name != cursor_icon_name
|
||||
});
|
||||
|
||||
if need_update {
|
||||
if let Some(theme) = &mut self.theme {
|
||||
let mut buffer: Option<&CursorImageBuffer>;
|
||||
|
||||
if let Some(cursor) = theme.get_cursor(&cursor_icon_name) {
|
||||
buffer = Some(&cursor[0]);
|
||||
} else if let Some(cursor) = theme.get_cursor("default") {
|
||||
buffer = Some(&cursor[0]);
|
||||
cursor_icon_name = "default";
|
||||
log::warn!(
|
||||
"Linux: Wayland: Unable to get cursor icon: {}. Using default cursor icon",
|
||||
cursor_icon_name
|
||||
);
|
||||
} else {
|
||||
buffer = None;
|
||||
log::warn!("Linux: Wayland: Unable to get default cursor too!");
|
||||
}
|
||||
|
||||
if let Some(buffer) = &mut buffer {
|
||||
let (width, height) = buffer.dimensions();
|
||||
let (hot_x, hot_y) = buffer.hotspot();
|
||||
|
||||
wl_pointer.set_cursor(
|
||||
self.serial_id,
|
||||
Some(&self.surface),
|
||||
hot_x as i32,
|
||||
hot_y as i32,
|
||||
);
|
||||
self.surface.attach(Some(&buffer), 0, 0);
|
||||
self.surface.damage(0, 0, width as i32, height as i32);
|
||||
self.surface.commit();
|
||||
|
||||
self.current_icon_name = Some(cursor_icon_name.to_string());
|
||||
}
|
||||
if let Some(cursor) = theme.get_cursor(&cursor_icon_name) {
|
||||
buffer = Some(&cursor[0]);
|
||||
} else if let Some(cursor) = theme.get_cursor("default") {
|
||||
buffer = Some(&cursor[0]);
|
||||
cursor_icon_name = "default";
|
||||
log::warn!(
|
||||
"Linux: Wayland: Unable to get cursor icon: {}. Using default cursor icon",
|
||||
cursor_icon_name
|
||||
);
|
||||
} else {
|
||||
log::warn!("Linux: Wayland: Unable to load cursor themes");
|
||||
buffer = None;
|
||||
log::warn!("Linux: Wayland: Unable to get default cursor too!");
|
||||
}
|
||||
|
||||
if let Some(buffer) = &mut buffer {
|
||||
let (width, height) = buffer.dimensions();
|
||||
let (hot_x, hot_y) = buffer.hotspot();
|
||||
|
||||
wl_pointer.set_cursor(serial_id, Some(&self.surface), hot_x as i32, hot_y as i32);
|
||||
self.surface.attach(Some(&buffer), 0, 0);
|
||||
self.surface.damage(0, 0, width as i32, height as i32);
|
||||
self.surface.commit();
|
||||
}
|
||||
} else {
|
||||
log::warn!("Linux: Wayland: Unable to load cursor themes");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,6 +64,7 @@ impl rwh::HasDisplayHandle for RawWindow {
|
||||
|
||||
pub struct WaylandWindowState {
|
||||
xdg_surface: xdg_surface::XdgSurface,
|
||||
acknowledged_first_configure: bool,
|
||||
pub surface: wl_surface::WlSurface,
|
||||
decoration: Option<zxdg_toplevel_decoration_v1::ZxdgToplevelDecorationV1>,
|
||||
toplevel: xdg_toplevel::XdgToplevel,
|
||||
@@ -131,6 +132,7 @@ impl WaylandWindowState {
|
||||
|
||||
Self {
|
||||
xdg_surface,
|
||||
acknowledged_first_configure: false,
|
||||
surface,
|
||||
decoration,
|
||||
toplevel,
|
||||
@@ -227,8 +229,6 @@ impl WaylandWindow {
|
||||
.as_ref()
|
||||
.map(|viewporter| viewporter.get_viewport(&surface, &globals.qh, ()));
|
||||
|
||||
surface.frame(&globals.qh, surface.id());
|
||||
|
||||
let this = Self(WaylandWindowStatePtr {
|
||||
state: Rc::new(RefCell::new(WaylandWindowState::new(
|
||||
surface.clone(),
|
||||
@@ -255,8 +255,8 @@ impl WaylandWindowStatePtr {
|
||||
Rc::ptr_eq(&self.state, &other.state)
|
||||
}
|
||||
|
||||
pub fn frame(&self, from_frame_callback: bool) {
|
||||
if from_frame_callback {
|
||||
pub fn frame(&self, request_frame_callback: bool) {
|
||||
if request_frame_callback {
|
||||
let state = self.state.borrow_mut();
|
||||
state.surface.frame(&state.globals.qh, state.surface.id());
|
||||
drop(state);
|
||||
@@ -270,10 +270,12 @@ impl WaylandWindowStatePtr {
|
||||
pub fn handle_xdg_surface_event(&self, event: xdg_surface::Event) {
|
||||
match event {
|
||||
xdg_surface::Event::Configure { serial } => {
|
||||
let state = self.state.borrow();
|
||||
let mut state = self.state.borrow_mut();
|
||||
state.xdg_surface.ack_configure(serial);
|
||||
let request_frame_callback = !state.acknowledged_first_configure;
|
||||
state.acknowledged_first_configure = true;
|
||||
drop(state);
|
||||
self.frame(false);
|
||||
self.frame(request_frame_callback);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -322,7 +324,7 @@ impl WaylandWindowStatePtr {
|
||||
self.resize(width, height);
|
||||
self.set_fullscreen(fullscreen);
|
||||
let mut state = self.state.borrow_mut();
|
||||
state.maximized = true;
|
||||
state.maximized = maximized;
|
||||
|
||||
false
|
||||
}
|
||||
@@ -609,6 +611,10 @@ impl PlatformWindow for WaylandWindow {
|
||||
self.borrow_mut().toplevel.set_title(title.to_string());
|
||||
}
|
||||
|
||||
fn set_app_id(&mut self, app_id: &str) {
|
||||
self.borrow_mut().toplevel.set_app_id(app_id.to_owned());
|
||||
}
|
||||
|
||||
fn set_background_appearance(&mut self, _background_appearance: WindowBackgroundAppearance) {
|
||||
// todo(linux)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::cell::RefCell;
|
||||
use std::ops::Deref;
|
||||
use std::rc::Rc;
|
||||
use std::rc::{Rc, Weak};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use calloop::{EventLoop, LoopHandle};
|
||||
@@ -12,9 +12,11 @@ use util::ResultExt;
|
||||
use x11rb::connection::{Connection, RequestConnection};
|
||||
use x11rb::errors::ConnectionError;
|
||||
use x11rb::protocol::randr::ConnectionExt as _;
|
||||
use x11rb::protocol::xinput::{ConnectionExt, ScrollClass};
|
||||
use x11rb::protocol::xkb::ConnectionExt as _;
|
||||
use x11rb::protocol::xproto::ConnectionExt as _;
|
||||
use x11rb::protocol::{randr, xkb, xproto, Event};
|
||||
use x11rb::protocol::{randr, xinput, xkb, xproto, Event};
|
||||
use x11rb::resource_manager::Database;
|
||||
use x11rb::xcb_ffi::XCBConnection;
|
||||
use xkbc::x11::ffi::{XKB_X11_MIN_MAJOR_XKB_VERSION, XKB_X11_MIN_MINOR_XKB_VERSION};
|
||||
use xkbcommon::xkb as xkbc;
|
||||
@@ -22,11 +24,12 @@ use xkbcommon::xkb as xkbc;
|
||||
use crate::platform::linux::LinuxClient;
|
||||
use crate::platform::{LinuxCommon, PlatformWindow};
|
||||
use crate::{
|
||||
px, AnyWindowHandle, Bounds, CursorStyle, DisplayId, Modifiers, ModifiersChangedEvent, Pixels,
|
||||
PlatformDisplay, PlatformInput, Point, ScrollDelta, Size, TouchPhase, WindowParams,
|
||||
modifiers_from_xinput_info, point, px, AnyWindowHandle, Bounds, CursorStyle, DisplayId,
|
||||
Modifiers, ModifiersChangedEvent, Pixels, PlatformDisplay, PlatformInput, Point, ScrollDelta,
|
||||
Size, TouchPhase, WindowParams, X11Window,
|
||||
};
|
||||
|
||||
use super::{super::SCROLL_LINES, X11Display, X11Window, XcbAtoms};
|
||||
use super::{super::SCROLL_LINES, X11Display, X11WindowStatePtr, XcbAtoms};
|
||||
use super::{button_of_key, modifiers_from_state};
|
||||
use crate::platform::linux::is_within_click_distance;
|
||||
use crate::platform::linux::platform::DOUBLE_CLICK_INTERVAL;
|
||||
@@ -36,12 +39,12 @@ use calloop::{
|
||||
};
|
||||
|
||||
pub(crate) struct WindowRef {
|
||||
window: X11Window,
|
||||
window: X11WindowStatePtr,
|
||||
refresh_event_token: RegistrationToken,
|
||||
}
|
||||
|
||||
impl Deref for WindowRef {
|
||||
type Target = X11Window;
|
||||
type Target = X11WindowStatePtr;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.window
|
||||
@@ -56,18 +59,43 @@ pub struct X11ClientState {
|
||||
pub(crate) last_location: Point<Pixels>,
|
||||
pub(crate) current_count: usize,
|
||||
|
||||
pub(crate) scale_factor: f32,
|
||||
|
||||
pub(crate) xcb_connection: Rc<XCBConnection>,
|
||||
pub(crate) x_root_index: usize,
|
||||
pub(crate) resource_database: Database,
|
||||
pub(crate) atoms: XcbAtoms,
|
||||
pub(crate) windows: HashMap<xproto::Window, WindowRef>,
|
||||
pub(crate) focused_window: Option<xproto::Window>,
|
||||
pub(crate) xkb: xkbc::State,
|
||||
|
||||
pub(crate) scroll_class_data: Vec<xinput::DeviceClassDataScroll>,
|
||||
pub(crate) scroll_x: Option<f32>,
|
||||
pub(crate) scroll_y: Option<f32>,
|
||||
|
||||
pub(crate) common: LinuxCommon,
|
||||
pub(crate) clipboard: X11ClipboardContext<Clipboard>,
|
||||
pub(crate) primary: X11ClipboardContext<Primary>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct X11ClientStatePtr(pub Weak<RefCell<X11ClientState>>);
|
||||
|
||||
impl X11ClientStatePtr {
|
||||
pub fn drop_window(&self, x_window: u32) {
|
||||
let client = X11Client(self.0.upgrade().expect("client already dropped"));
|
||||
let mut state = client.0.borrow_mut();
|
||||
|
||||
if let Some(window_ref) = state.windows.remove(&x_window) {
|
||||
state.loop_handle.remove(window_ref.refresh_event_token);
|
||||
}
|
||||
|
||||
if state.windows.is_empty() {
|
||||
state.common.signal.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct X11Client(Rc<RefCell<X11ClientState>>);
|
||||
|
||||
@@ -92,6 +120,35 @@ impl X11Client {
|
||||
xcb_connection
|
||||
.prefetch_extension_information(randr::X11_EXTENSION_NAME)
|
||||
.unwrap();
|
||||
xcb_connection
|
||||
.prefetch_extension_information(xinput::X11_EXTENSION_NAME)
|
||||
.unwrap();
|
||||
|
||||
let xinput_version = xcb_connection
|
||||
.xinput_xi_query_version(2, 0)
|
||||
.unwrap()
|
||||
.reply()
|
||||
.unwrap();
|
||||
assert!(
|
||||
xinput_version.major_version >= 2,
|
||||
"XInput Extension v2 not supported."
|
||||
);
|
||||
|
||||
let master_device_query = xcb_connection
|
||||
.xinput_xi_query_device(1_u16)
|
||||
.unwrap()
|
||||
.reply()
|
||||
.unwrap();
|
||||
let scroll_class_data = master_device_query
|
||||
.infos
|
||||
.iter()
|
||||
.find(|info| info.type_ == xinput::DeviceType::MASTER_POINTER)
|
||||
.unwrap()
|
||||
.classes
|
||||
.iter()
|
||||
.filter_map(|class| class.data.as_scroll())
|
||||
.map(|class| *class)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let atoms = XcbAtoms::new(&xcb_connection).unwrap();
|
||||
let xkb = xcb_connection
|
||||
@@ -125,6 +182,31 @@ impl X11Client {
|
||||
xkbc::x11::state_new_from_device(&xkb_keymap, &xcb_connection, xkb_device_id)
|
||||
};
|
||||
|
||||
let screen = xcb_connection.setup().roots.get(x_root_index).unwrap();
|
||||
|
||||
// Values from `Database::GET_RESOURCE_DATABASE`
|
||||
let resource_manager = xcb_connection
|
||||
.get_property(
|
||||
false,
|
||||
screen.root,
|
||||
xproto::AtomEnum::RESOURCE_MANAGER,
|
||||
xproto::AtomEnum::STRING,
|
||||
0,
|
||||
100_000_000,
|
||||
)
|
||||
.unwrap();
|
||||
let resource_manager = resource_manager.reply().unwrap();
|
||||
|
||||
// todo(linux): read hostname
|
||||
let resource_database = Database::new_from_default(&resource_manager, "HOSTNAME".into());
|
||||
|
||||
let scale_factor = resource_database
|
||||
.get_value("Xft.dpi", "Xft.dpi")
|
||||
.ok()
|
||||
.flatten()
|
||||
.map(|dpi: f32| dpi / 96.0)
|
||||
.unwrap_or(1.0);
|
||||
|
||||
let clipboard = X11ClipboardContext::<Clipboard>::new().unwrap();
|
||||
let primary = X11ClipboardContext::<Primary>::new().unwrap();
|
||||
|
||||
@@ -159,19 +241,26 @@ impl X11Client {
|
||||
last_click: Instant::now(),
|
||||
last_location: Point::new(px(0.0), px(0.0)),
|
||||
current_count: 0,
|
||||
scale_factor,
|
||||
|
||||
xcb_connection,
|
||||
x_root_index,
|
||||
resource_database,
|
||||
atoms,
|
||||
windows: HashMap::default(),
|
||||
focused_window: None,
|
||||
xkb: xkb_state,
|
||||
|
||||
scroll_class_data,
|
||||
scroll_x: None,
|
||||
scroll_y: None,
|
||||
|
||||
clipboard,
|
||||
primary,
|
||||
})))
|
||||
}
|
||||
|
||||
fn get_window(&self, win: xproto::Window) -> Option<X11Window> {
|
||||
fn get_window(&self, win: xproto::Window) -> Option<X11WindowStatePtr> {
|
||||
let state = self.0.borrow();
|
||||
state
|
||||
.windows
|
||||
@@ -182,18 +271,16 @@ impl X11Client {
|
||||
fn handle_event(&self, event: Event) -> Option<()> {
|
||||
match event {
|
||||
Event::ClientMessage(event) => {
|
||||
let window = self.get_window(event.window)?;
|
||||
let [atom, ..] = event.data.as_data32();
|
||||
let mut state = self.0.borrow_mut();
|
||||
|
||||
if atom == state.atoms.WM_DELETE_WINDOW {
|
||||
// window "x" button clicked by user, we gracefully exit
|
||||
let window_ref = state.windows.remove(&event.window)?;
|
||||
|
||||
state.loop_handle.remove(window_ref.refresh_event_token);
|
||||
window_ref.window.destroy();
|
||||
|
||||
if state.windows.is_empty() {
|
||||
state.common.signal.stop();
|
||||
// window "x" button clicked by user
|
||||
if window.should_close() {
|
||||
let window_ref = state.windows.remove(&event.window)?;
|
||||
state.loop_handle.remove(window_ref.refresh_event_token);
|
||||
// Rest of the close logic is handled in drop_window()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -289,8 +376,10 @@ impl X11Client {
|
||||
let mut state = self.0.borrow_mut();
|
||||
|
||||
let modifiers = modifiers_from_state(event.state);
|
||||
let position =
|
||||
Point::new((event.event_x as f32).into(), (event.event_y as f32).into());
|
||||
let position = point(
|
||||
px(event.event_x as f32 / state.scale_factor),
|
||||
px(event.event_y as f32 / state.scale_factor),
|
||||
);
|
||||
if let Some(button) = button_of_key(event.detail) {
|
||||
let click_elapsed = state.last_click.elapsed();
|
||||
|
||||
@@ -314,18 +403,6 @@ impl X11Client {
|
||||
click_count: current_count,
|
||||
first_mouse: false,
|
||||
}));
|
||||
} else if event.detail >= 4 && event.detail <= 5 {
|
||||
// https://stackoverflow.com/questions/15510472/scrollwheel-event-in-x11
|
||||
let scroll_direction = if event.detail == 4 { 1.0 } else { -1.0 };
|
||||
let scroll_y = SCROLL_LINES * scroll_direction;
|
||||
|
||||
drop(state);
|
||||
window.handle_input(PlatformInput::ScrollWheel(crate::ScrollWheelEvent {
|
||||
position,
|
||||
delta: ScrollDelta::Lines(Point::new(0.0, scroll_y as f32)),
|
||||
modifiers,
|
||||
touch_phase: TouchPhase::Moved,
|
||||
}));
|
||||
} else {
|
||||
log::warn!("Unknown button press: {event:?}");
|
||||
}
|
||||
@@ -334,8 +411,10 @@ impl X11Client {
|
||||
let window = self.get_window(event.event)?;
|
||||
let state = self.0.borrow();
|
||||
let modifiers = modifiers_from_state(event.state);
|
||||
let position =
|
||||
Point::new((event.event_x as f32).into(), (event.event_y as f32).into());
|
||||
let position = point(
|
||||
px(event.event_x as f32 / state.scale_factor),
|
||||
px(event.event_y as f32 / state.scale_factor),
|
||||
);
|
||||
if let Some(button) = button_of_key(event.detail) {
|
||||
let click_count = state.current_count;
|
||||
drop(state);
|
||||
@@ -347,12 +426,95 @@ impl X11Client {
|
||||
}));
|
||||
}
|
||||
}
|
||||
Event::XinputMotion(event) => {
|
||||
let window = self.get_window(event.event)?;
|
||||
let state = self.0.borrow();
|
||||
let position = point(
|
||||
px(event.event_x as f32 / u16::MAX as f32 / state.scale_factor),
|
||||
px(event.event_y as f32 / u16::MAX as f32 / state.scale_factor),
|
||||
);
|
||||
drop(state);
|
||||
let modifiers = modifiers_from_xinput_info(event.mods);
|
||||
|
||||
let axisvalues = event
|
||||
.axisvalues
|
||||
.iter()
|
||||
.map(|axisvalue| fp3232_to_f32(*axisvalue))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if event.valuator_mask[0] & 3 != 0 {
|
||||
window.handle_input(PlatformInput::MouseMove(crate::MouseMoveEvent {
|
||||
position,
|
||||
pressed_button: None,
|
||||
modifiers,
|
||||
}));
|
||||
}
|
||||
|
||||
let mut valuator_idx = 0;
|
||||
let scroll_class_data = self.0.borrow().scroll_class_data.clone();
|
||||
for shift in 0..32 {
|
||||
if (event.valuator_mask[0] >> shift) & 1 == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
for scroll_class in &scroll_class_data {
|
||||
if scroll_class.scroll_type == xinput::ScrollType::HORIZONTAL
|
||||
&& scroll_class.number == shift
|
||||
{
|
||||
let new_scroll = axisvalues[valuator_idx]
|
||||
/ fp3232_to_f32(scroll_class.increment)
|
||||
* SCROLL_LINES as f32;
|
||||
let old_scroll = self.0.borrow().scroll_x;
|
||||
self.0.borrow_mut().scroll_x = Some(new_scroll);
|
||||
|
||||
if let Some(old_scroll) = old_scroll {
|
||||
let delta_scroll = old_scroll - new_scroll;
|
||||
window.handle_input(PlatformInput::ScrollWheel(
|
||||
crate::ScrollWheelEvent {
|
||||
position,
|
||||
delta: ScrollDelta::Lines(Point::new(delta_scroll, 0.0)),
|
||||
modifiers,
|
||||
touch_phase: TouchPhase::default(),
|
||||
},
|
||||
));
|
||||
}
|
||||
} else if scroll_class.scroll_type == xinput::ScrollType::VERTICAL
|
||||
&& scroll_class.number == shift
|
||||
{
|
||||
// the `increment` is the valuator delta equivalent to one positive unit of scrolling. Here that means SCROLL_LINES lines.
|
||||
let new_scroll = axisvalues[valuator_idx]
|
||||
/ fp3232_to_f32(scroll_class.increment)
|
||||
* SCROLL_LINES as f32;
|
||||
let old_scroll = self.0.borrow().scroll_y;
|
||||
self.0.borrow_mut().scroll_y = Some(new_scroll);
|
||||
|
||||
if let Some(old_scroll) = old_scroll {
|
||||
let delta_scroll = old_scroll - new_scroll;
|
||||
window.handle_input(PlatformInput::ScrollWheel(
|
||||
crate::ScrollWheelEvent {
|
||||
position,
|
||||
delta: ScrollDelta::Lines(Point::new(0.0, delta_scroll)),
|
||||
modifiers,
|
||||
touch_phase: TouchPhase::default(),
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
valuator_idx += 1;
|
||||
}
|
||||
}
|
||||
Event::MotionNotify(event) => {
|
||||
let window = self.get_window(event.event)?;
|
||||
let state = self.0.borrow();
|
||||
let pressed_button = super::button_from_state(event.state);
|
||||
let position =
|
||||
Point::new((event.event_x as f32).into(), (event.event_y as f32).into());
|
||||
let position = point(
|
||||
px(event.event_x as f32 / state.scale_factor),
|
||||
px(event.event_y as f32 / state.scale_factor),
|
||||
);
|
||||
let modifiers = modifiers_from_state(event.state);
|
||||
drop(state);
|
||||
window.handle_input(PlatformInput::MouseMove(crate::MouseMoveEvent {
|
||||
pressed_button,
|
||||
position,
|
||||
@@ -361,10 +523,14 @@ impl X11Client {
|
||||
}
|
||||
Event::LeaveNotify(event) => {
|
||||
let window = self.get_window(event.event)?;
|
||||
let state = self.0.borrow();
|
||||
let pressed_button = super::button_from_state(event.state);
|
||||
let position =
|
||||
Point::new((event.event_x as f32).into(), (event.event_y as f32).into());
|
||||
let position = point(
|
||||
px(event.event_x as f32 / state.scale_factor),
|
||||
px(event.event_y as f32 / state.scale_factor),
|
||||
);
|
||||
let modifiers = modifiers_from_state(event.state);
|
||||
drop(state);
|
||||
window.handle_input(PlatformInput::MouseExited(crate::MouseExitEvent {
|
||||
pressed_button,
|
||||
position,
|
||||
@@ -424,11 +590,14 @@ impl LinuxClient for X11Client {
|
||||
let x_window = state.xcb_connection.generate_id().unwrap();
|
||||
|
||||
let window = X11Window::new(
|
||||
X11ClientStatePtr(Rc::downgrade(&self.0)),
|
||||
state.common.foreground_executor.clone(),
|
||||
params,
|
||||
&state.xcb_connection,
|
||||
state.x_root_index,
|
||||
x_window,
|
||||
&state.atoms,
|
||||
state.scale_factor,
|
||||
);
|
||||
|
||||
let screen_resources = state
|
||||
@@ -492,7 +661,7 @@ impl LinuxClient for X11Client {
|
||||
.expect("Failed to initialize refresh timer");
|
||||
|
||||
let window_ref = WindowRef {
|
||||
window: window.clone(),
|
||||
window: window.0.clone(),
|
||||
refresh_event_token,
|
||||
};
|
||||
|
||||
@@ -555,3 +724,7 @@ pub fn mode_refresh_rate(mode: &randr::ModeInfo) -> Duration {
|
||||
log::info!("Refreshing at {} micros", micros);
|
||||
Duration::from_micros(micros)
|
||||
}
|
||||
|
||||
fn fp3232_to_f32(value: xinput::Fp3232) -> f32 {
|
||||
value.integral as f32 + value.frac as f32 / u32::MAX as f32
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
use x11rb::protocol::xproto;
|
||||
use x11rb::protocol::{
|
||||
xinput,
|
||||
xproto::{self, ModMask},
|
||||
};
|
||||
|
||||
use crate::{Modifiers, MouseButton, NavigationDirection};
|
||||
|
||||
@@ -23,6 +26,17 @@ pub(crate) fn modifiers_from_state(state: xproto::KeyButMask) -> Modifiers {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn modifiers_from_xinput_info(modifier_info: xinput::ModifierInfo) -> Modifiers {
|
||||
Modifiers {
|
||||
control: modifier_info.effective as u16 & ModMask::CONTROL.bits()
|
||||
== ModMask::CONTROL.bits(),
|
||||
alt: modifier_info.effective as u16 & ModMask::M1.bits() == ModMask::M1.bits(),
|
||||
shift: modifier_info.effective as u16 & ModMask::SHIFT.bits() == ModMask::SHIFT.bits(),
|
||||
platform: modifier_info.effective as u16 & ModMask::M4.bits() == ModMask::M4.bits(),
|
||||
function: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn button_from_state(state: xproto::KeyButMask) -> Option<MouseButton> {
|
||||
Some(if state.contains(xproto::KeyButMask::BUTTON1) {
|
||||
MouseButton::Left
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
#![allow(unused)]
|
||||
|
||||
use crate::{
|
||||
platform::blade::BladeRenderer, size, Bounds, DevicePixels, Modifiers, Pixels, PlatformAtlas,
|
||||
PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow, Point, PromptLevel,
|
||||
Scene, Size, WindowAppearance, WindowBackgroundAppearance, WindowOptions, WindowParams,
|
||||
X11Client, X11ClientState,
|
||||
platform::blade::BladeRenderer, size, Bounds, DevicePixels, ForegroundExecutor, Modifiers,
|
||||
Pixels, Platform, PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler,
|
||||
PlatformWindow, Point, PromptLevel, Scene, Size, WindowAppearance, WindowBackgroundAppearance,
|
||||
WindowOptions, WindowParams, X11Client, X11ClientState, X11ClientStatePtr,
|
||||
};
|
||||
use blade_graphics as gpu;
|
||||
use parking_lot::Mutex;
|
||||
@@ -13,7 +13,11 @@ use raw_window_handle as rwh;
|
||||
use util::ResultExt;
|
||||
use x11rb::{
|
||||
connection::Connection,
|
||||
protocol::xproto::{self, ConnectionExt as _, CreateWindowAux},
|
||||
protocol::{
|
||||
xinput,
|
||||
xproto::{self, ConnectionExt as _, CreateWindowAux},
|
||||
},
|
||||
resource_manager::Database,
|
||||
wrapper::ConnectionExt,
|
||||
xcb_ffi::XCBConnection,
|
||||
};
|
||||
@@ -24,6 +28,7 @@ use std::{
|
||||
iter::Zip,
|
||||
mem,
|
||||
num::NonZeroU32,
|
||||
ops::Div,
|
||||
ptr::NonNull,
|
||||
rc::Rc,
|
||||
sync::{self, Arc},
|
||||
@@ -77,6 +82,8 @@ pub struct Callbacks {
|
||||
}
|
||||
|
||||
pub(crate) struct X11WindowState {
|
||||
client: X11ClientStatePtr,
|
||||
executor: ForegroundExecutor,
|
||||
atoms: XcbAtoms,
|
||||
raw: RawWindow,
|
||||
bounds: Bounds<i32>,
|
||||
@@ -88,7 +95,7 @@ pub(crate) struct X11WindowState {
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct X11Window {
|
||||
pub(crate) struct X11WindowStatePtr {
|
||||
pub(crate) state: Rc<RefCell<X11WindowState>>,
|
||||
pub(crate) callbacks: Rc<RefCell<Callbacks>>,
|
||||
xcb_connection: Rc<XCBConnection>,
|
||||
@@ -123,12 +130,16 @@ impl rwh::HasDisplayHandle for X11Window {
|
||||
}
|
||||
|
||||
impl X11WindowState {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new(
|
||||
client: X11ClientStatePtr,
|
||||
executor: ForegroundExecutor,
|
||||
params: WindowParams,
|
||||
xcb_connection: &Rc<XCBConnection>,
|
||||
x_main_screen_index: usize,
|
||||
x_window: xproto::Window,
|
||||
atoms: &XcbAtoms,
|
||||
scale_factor: f32,
|
||||
) -> Self {
|
||||
let x_screen_index = params
|
||||
.display_id
|
||||
@@ -149,8 +160,6 @@ impl X11WindowState {
|
||||
| xproto::EventMask::BUTTON1_MOTION
|
||||
| xproto::EventMask::BUTTON2_MOTION
|
||||
| xproto::EventMask::BUTTON3_MOTION
|
||||
| xproto::EventMask::BUTTON4_MOTION
|
||||
| xproto::EventMask::BUTTON5_MOTION
|
||||
| xproto::EventMask::BUTTON_MOTION,
|
||||
);
|
||||
|
||||
@@ -170,6 +179,18 @@ impl X11WindowState {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
xinput::ConnectionExt::xinput_xi_select_events(
|
||||
&xcb_connection,
|
||||
x_window,
|
||||
&[xinput::EventMask {
|
||||
deviceid: 1,
|
||||
mask: vec![xinput::XIEventMask::MOTION],
|
||||
}],
|
||||
)
|
||||
.unwrap()
|
||||
.check()
|
||||
.unwrap();
|
||||
|
||||
if let Some(titlebar) = params.titlebar {
|
||||
if let Some(title) = titlebar.title {
|
||||
xcb_connection
|
||||
@@ -224,10 +245,12 @@ impl X11WindowState {
|
||||
let gpu_extent = query_render_extent(xcb_connection, x_window);
|
||||
|
||||
Self {
|
||||
client,
|
||||
executor,
|
||||
display: Rc::new(X11Display::new(xcb_connection, x_screen_index).unwrap()),
|
||||
raw,
|
||||
bounds: params.bounds.map(|v| v.0),
|
||||
scale_factor: 1.0,
|
||||
scale_factor,
|
||||
renderer: BladeRenderer::new(gpu, gpu_extent),
|
||||
atoms: *atoms,
|
||||
|
||||
@@ -244,39 +267,80 @@ impl X11WindowState {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct X11Window(pub X11WindowStatePtr);
|
||||
|
||||
impl Drop for X11Window {
|
||||
fn drop(&mut self) {
|
||||
let mut state = self.0.state.borrow_mut();
|
||||
state.renderer.destroy();
|
||||
|
||||
self.0.xcb_connection.unmap_window(self.0.x_window).unwrap();
|
||||
self.0
|
||||
.xcb_connection
|
||||
.destroy_window(self.0.x_window)
|
||||
.unwrap();
|
||||
self.0.xcb_connection.flush().unwrap();
|
||||
|
||||
let this_ptr = self.0.clone();
|
||||
let client_ptr = state.client.clone();
|
||||
state
|
||||
.executor
|
||||
.spawn(async move {
|
||||
this_ptr.close();
|
||||
client_ptr.drop_window(this_ptr.x_window);
|
||||
})
|
||||
.detach();
|
||||
drop(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl X11Window {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new(
|
||||
client: X11ClientStatePtr,
|
||||
executor: ForegroundExecutor,
|
||||
params: WindowParams,
|
||||
xcb_connection: &Rc<XCBConnection>,
|
||||
x_main_screen_index: usize,
|
||||
x_window: xproto::Window,
|
||||
atoms: &XcbAtoms,
|
||||
scale_factor: f32,
|
||||
) -> Self {
|
||||
X11Window {
|
||||
Self(X11WindowStatePtr {
|
||||
state: Rc::new(RefCell::new(X11WindowState::new(
|
||||
client,
|
||||
executor,
|
||||
params,
|
||||
xcb_connection,
|
||||
x_main_screen_index,
|
||||
x_window,
|
||||
atoms,
|
||||
scale_factor,
|
||||
))),
|
||||
callbacks: Rc::new(RefCell::new(Callbacks::default())),
|
||||
xcb_connection: xcb_connection.clone(),
|
||||
x_window,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl X11WindowStatePtr {
|
||||
pub fn should_close(&self) -> bool {
|
||||
let mut cb = self.callbacks.borrow_mut();
|
||||
if let Some(mut should_close) = cb.should_close.take() {
|
||||
let result = (should_close)();
|
||||
cb.should_close = Some(should_close);
|
||||
result
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
pub fn destroy(&self) {
|
||||
let mut state = self.state.borrow_mut();
|
||||
state.renderer.destroy();
|
||||
drop(state);
|
||||
|
||||
self.xcb_connection.unmap_window(self.x_window).unwrap();
|
||||
self.xcb_connection.destroy_window(self.x_window).unwrap();
|
||||
if let Some(fun) = self.callbacks.borrow_mut().close.take() {
|
||||
fun();
|
||||
pub fn close(&self) {
|
||||
let mut callbacks = self.callbacks.borrow_mut();
|
||||
if let Some(fun) = callbacks.close.take() {
|
||||
fun()
|
||||
}
|
||||
self.xcb_connection.flush().unwrap();
|
||||
}
|
||||
|
||||
pub fn refresh(&self) {
|
||||
@@ -345,7 +409,7 @@ impl X11Window {
|
||||
|
||||
impl PlatformWindow for X11Window {
|
||||
fn bounds(&self) -> Bounds<DevicePixels> {
|
||||
self.state.borrow_mut().bounds.map(|v| v.into())
|
||||
self.0.state.borrow().bounds.map(|v| v.into())
|
||||
}
|
||||
|
||||
// todo(linux)
|
||||
@@ -359,11 +423,16 @@ impl PlatformWindow for X11Window {
|
||||
}
|
||||
|
||||
fn content_size(&self) -> Size<Pixels> {
|
||||
self.state.borrow_mut().content_size()
|
||||
// We divide by the scale factor here because this value is queried to determine how much to draw,
|
||||
// but it will be multiplied later by the scale to adjust for scaling.
|
||||
let state = self.0.state.borrow();
|
||||
state
|
||||
.content_size()
|
||||
.map(|size| size.div(state.scale_factor))
|
||||
}
|
||||
|
||||
fn scale_factor(&self) -> f32 {
|
||||
self.state.borrow_mut().scale_factor
|
||||
self.0.state.borrow().scale_factor
|
||||
}
|
||||
|
||||
// todo(linux)
|
||||
@@ -372,13 +441,14 @@ impl PlatformWindow for X11Window {
|
||||
}
|
||||
|
||||
fn display(&self) -> Rc<dyn PlatformDisplay> {
|
||||
self.state.borrow().display.clone()
|
||||
self.0.state.borrow().display.clone()
|
||||
}
|
||||
|
||||
fn mouse_position(&self) -> Point<Pixels> {
|
||||
let reply = self
|
||||
.0
|
||||
.xcb_connection
|
||||
.query_pointer(self.x_window)
|
||||
.query_pointer(self.0.x_window)
|
||||
.unwrap()
|
||||
.reply()
|
||||
.unwrap();
|
||||
@@ -395,11 +465,11 @@ impl PlatformWindow for X11Window {
|
||||
}
|
||||
|
||||
fn set_input_handler(&mut self, input_handler: PlatformInputHandler) {
|
||||
self.state.borrow_mut().input_handler = Some(input_handler);
|
||||
self.0.state.borrow_mut().input_handler = Some(input_handler);
|
||||
}
|
||||
|
||||
fn take_input_handler(&mut self) -> Option<PlatformInputHandler> {
|
||||
self.state.borrow_mut().input_handler.take()
|
||||
self.0.state.borrow_mut().input_handler.take()
|
||||
}
|
||||
|
||||
fn prompt(
|
||||
@@ -414,8 +484,9 @@ impl PlatformWindow for X11Window {
|
||||
|
||||
fn activate(&self) {
|
||||
let win_aux = xproto::ConfigureWindowAux::new().stack_mode(xproto::StackMode::ABOVE);
|
||||
self.xcb_connection
|
||||
.configure_window(self.x_window, &win_aux)
|
||||
self.0
|
||||
.xcb_connection
|
||||
.configure_window(self.0.x_window, &win_aux)
|
||||
.log_err();
|
||||
}
|
||||
|
||||
@@ -425,27 +496,44 @@ impl PlatformWindow for X11Window {
|
||||
}
|
||||
|
||||
fn set_title(&mut self, title: &str) {
|
||||
self.xcb_connection
|
||||
self.0
|
||||
.xcb_connection
|
||||
.change_property8(
|
||||
xproto::PropMode::REPLACE,
|
||||
self.x_window,
|
||||
self.0.x_window,
|
||||
xproto::AtomEnum::WM_NAME,
|
||||
xproto::AtomEnum::STRING,
|
||||
title.as_bytes(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
self.xcb_connection
|
||||
self.0
|
||||
.xcb_connection
|
||||
.change_property8(
|
||||
xproto::PropMode::REPLACE,
|
||||
self.x_window,
|
||||
self.state.borrow().atoms._NET_WM_NAME,
|
||||
self.state.borrow().atoms.UTF8_STRING,
|
||||
self.0.x_window,
|
||||
self.0.state.borrow().atoms._NET_WM_NAME,
|
||||
self.0.state.borrow().atoms.UTF8_STRING,
|
||||
title.as_bytes(),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn set_app_id(&mut self, app_id: &str) {
|
||||
let mut data = Vec::with_capacity(app_id.len() * 2 + 1);
|
||||
data.extend(app_id.bytes()); // instance https://unix.stackexchange.com/a/494170
|
||||
data.push(b'\0');
|
||||
data.extend(app_id.bytes()); // class
|
||||
|
||||
self.0.xcb_connection.change_property8(
|
||||
xproto::PropMode::REPLACE,
|
||||
self.0.x_window,
|
||||
xproto::AtomEnum::WM_CLASS,
|
||||
xproto::AtomEnum::STRING,
|
||||
&data,
|
||||
);
|
||||
}
|
||||
|
||||
// todo(linux)
|
||||
fn set_edited(&mut self, edited: bool) {}
|
||||
|
||||
@@ -484,39 +572,39 @@ impl PlatformWindow for X11Window {
|
||||
}
|
||||
|
||||
fn on_request_frame(&self, callback: Box<dyn FnMut()>) {
|
||||
self.callbacks.borrow_mut().request_frame = Some(callback);
|
||||
self.0.callbacks.borrow_mut().request_frame = Some(callback);
|
||||
}
|
||||
|
||||
fn on_input(&self, callback: Box<dyn FnMut(PlatformInput) -> crate::DispatchEventResult>) {
|
||||
self.callbacks.borrow_mut().input = Some(callback);
|
||||
self.0.callbacks.borrow_mut().input = Some(callback);
|
||||
}
|
||||
|
||||
fn on_active_status_change(&self, callback: Box<dyn FnMut(bool)>) {
|
||||
self.callbacks.borrow_mut().active_status_change = Some(callback);
|
||||
self.0.callbacks.borrow_mut().active_status_change = Some(callback);
|
||||
}
|
||||
|
||||
fn on_resize(&self, callback: Box<dyn FnMut(Size<Pixels>, f32)>) {
|
||||
self.callbacks.borrow_mut().resize = Some(callback);
|
||||
self.0.callbacks.borrow_mut().resize = Some(callback);
|
||||
}
|
||||
|
||||
fn on_fullscreen(&self, callback: Box<dyn FnMut(bool)>) {
|
||||
self.callbacks.borrow_mut().fullscreen = Some(callback);
|
||||
self.0.callbacks.borrow_mut().fullscreen = Some(callback);
|
||||
}
|
||||
|
||||
fn on_moved(&self, callback: Box<dyn FnMut()>) {
|
||||
self.callbacks.borrow_mut().moved = Some(callback);
|
||||
self.0.callbacks.borrow_mut().moved = Some(callback);
|
||||
}
|
||||
|
||||
fn on_should_close(&self, callback: Box<dyn FnMut() -> bool>) {
|
||||
self.callbacks.borrow_mut().should_close = Some(callback);
|
||||
self.0.callbacks.borrow_mut().should_close = Some(callback);
|
||||
}
|
||||
|
||||
fn on_close(&self, callback: Box<dyn FnOnce()>) {
|
||||
self.callbacks.borrow_mut().close = Some(callback);
|
||||
self.0.callbacks.borrow_mut().close = Some(callback);
|
||||
}
|
||||
|
||||
fn on_appearance_changed(&self, callback: Box<dyn FnMut()>) {
|
||||
self.callbacks.borrow_mut().appearance_changed = Some(callback);
|
||||
self.0.callbacks.borrow_mut().appearance_changed = Some(callback);
|
||||
}
|
||||
|
||||
// todo(linux)
|
||||
@@ -525,12 +613,12 @@ impl PlatformWindow for X11Window {
|
||||
}
|
||||
|
||||
fn draw(&self, scene: &Scene) {
|
||||
let mut inner = self.state.borrow_mut();
|
||||
let mut inner = self.0.state.borrow_mut();
|
||||
inner.renderer.draw(scene);
|
||||
}
|
||||
|
||||
fn sprite_atlas(&self) -> sync::Arc<dyn PlatformAtlas> {
|
||||
let inner = self.state.borrow();
|
||||
let inner = self.0.state.borrow();
|
||||
inner.renderer.sprite_atlas().clone()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,7 +107,7 @@ const kTypographicExtrasType: i32 = 14;
|
||||
const kVerticalFractionsSelector: i32 = 1;
|
||||
const kVerticalPositionType: i32 = 10;
|
||||
|
||||
pub fn apply_features(font: &mut Font, features: FontFeatures) {
|
||||
pub fn apply_features(font: &mut Font, features: &FontFeatures) {
|
||||
// See https://chromium.googlesource.com/chromium/src/+/66.0.3359.158/third_party/harfbuzz-ng/src/hb-coretext.cc
|
||||
// for a reference implementation.
|
||||
toggle_open_type_feature(
|
||||
|
||||
@@ -123,12 +123,12 @@ impl PlatformTextSystem for MacTextSystem {
|
||||
let mut lock = RwLockUpgradableReadGuard::upgrade(lock);
|
||||
let font_key = FontKey {
|
||||
font_family: font.family.clone(),
|
||||
font_features: font.features,
|
||||
font_features: font.features.clone(),
|
||||
};
|
||||
let candidates = if let Some(font_ids) = lock.font_ids_by_font_key.get(&font_key) {
|
||||
font_ids.as_slice()
|
||||
} else {
|
||||
let font_ids = lock.load_family(&font.family, font.features)?;
|
||||
let font_ids = lock.load_family(&font.family, &font.features)?;
|
||||
lock.font_ids_by_font_key.insert(font_key.clone(), font_ids);
|
||||
lock.font_ids_by_font_key[&font_key].as_ref()
|
||||
};
|
||||
@@ -219,7 +219,11 @@ impl MacTextSystemState {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn load_family(&mut self, name: &str, features: FontFeatures) -> Result<SmallVec<[FontId; 4]>> {
|
||||
fn load_family(
|
||||
&mut self,
|
||||
name: &str,
|
||||
features: &FontFeatures,
|
||||
) -> Result<SmallVec<[FontId; 4]>> {
|
||||
let name = if name == ".SystemUIFont" {
|
||||
".AppleSystemUIFont"
|
||||
} else {
|
||||
|
||||
@@ -904,7 +904,7 @@ impl PlatformWindow for MacWindow {
|
||||
let alert_style = match level {
|
||||
PromptLevel::Info => 1,
|
||||
PromptLevel::Warning => 0,
|
||||
PromptLevel::Critical => 2,
|
||||
PromptLevel::Critical | PromptLevel::Destructive => 2,
|
||||
};
|
||||
let _: () = msg_send![alert, setAlertStyle: alert_style];
|
||||
let _: () = msg_send![alert, setMessageText: ns_string(msg)];
|
||||
@@ -919,10 +919,16 @@ impl PlatformWindow for MacWindow {
|
||||
{
|
||||
let button: id = msg_send![alert, addButtonWithTitle: ns_string(answer)];
|
||||
let _: () = msg_send![button, setTag: ix as NSInteger];
|
||||
if level == PromptLevel::Destructive && answer != &"Cancel" {
|
||||
let _: () = msg_send![button, setHasDestructiveAction: YES];
|
||||
}
|
||||
}
|
||||
if let Some((ix, answer)) = latest_non_cancel_label {
|
||||
let button: id = msg_send![alert, addButtonWithTitle: ns_string(answer)];
|
||||
let _: () = msg_send![button, setTag: ix as NSInteger];
|
||||
if level == PromptLevel::Destructive {
|
||||
let _: () = msg_send![button, setHasDestructiveAction: YES];
|
||||
}
|
||||
}
|
||||
|
||||
let (done_tx, done_rx) = oneshot::channel();
|
||||
@@ -976,6 +982,8 @@ impl PlatformWindow for MacWindow {
|
||||
}
|
||||
}
|
||||
|
||||
fn set_app_id(&mut self, _app_id: &str) {}
|
||||
|
||||
fn set_background_appearance(&mut self, background_appearance: WindowBackgroundAppearance) {
|
||||
let this = self.0.as_ref().lock();
|
||||
let blur_radius = if background_appearance == WindowBackgroundAppearance::Blurred {
|
||||
@@ -990,6 +998,8 @@ impl PlatformWindow for MacWindow {
|
||||
};
|
||||
unsafe {
|
||||
this.native_window.setOpaque_(opaque);
|
||||
// Shadows for transparent windows cause artifacts and performance issues
|
||||
this.native_window.setHasShadow_(opaque);
|
||||
let clear_color = if opaque == YES {
|
||||
NSColor::colorWithSRGBRed_green_blue_alpha_(nil, 0f64, 0f64, 0f64, 1f64)
|
||||
} else {
|
||||
|
||||
@@ -190,6 +190,8 @@ impl PlatformWindow for TestWindow {
|
||||
self.0.lock().title = Some(title.to_owned());
|
||||
}
|
||||
|
||||
fn set_app_id(&mut self, _app_id: &str) {}
|
||||
|
||||
fn set_background_appearance(&mut self, _background: WindowBackgroundAppearance) {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
@@ -367,7 +367,10 @@ impl DirectWriteState {
|
||||
|
||||
fn layout_line(&mut self, text: &str, font_size: Pixels, font_runs: &[FontRun]) -> LineLayout {
|
||||
if font_runs.is_empty() {
|
||||
return LineLayout::default();
|
||||
return LineLayout {
|
||||
font_size,
|
||||
..Default::default()
|
||||
};
|
||||
}
|
||||
unsafe {
|
||||
let text_renderer = self.components.text_renderer.clone();
|
||||
|
||||
@@ -25,7 +25,7 @@ use windows::{
|
||||
Win32::{
|
||||
Foundation::*,
|
||||
Graphics::Gdi::*,
|
||||
System::{Com::*, Ole::*, SystemServices::*},
|
||||
System::{Com::*, LibraryLoader::*, Ole::*, SystemServices::*},
|
||||
UI::{
|
||||
Controls::*,
|
||||
HiDpi::*,
|
||||
@@ -82,7 +82,9 @@ impl WindowsWindowInner {
|
||||
fn window_handle(&self) -> Result<rwh::WindowHandle<'_>, rwh::HandleError> {
|
||||
Ok(unsafe {
|
||||
let hwnd = NonZeroIsize::new_unchecked(self.hwnd);
|
||||
let handle = rwh::Win32WindowHandle::new(hwnd);
|
||||
let mut handle = rwh::Win32WindowHandle::new(hwnd);
|
||||
let hinstance = get_window_long(HWND(self.hwnd), GWLP_HINSTANCE);
|
||||
handle.hinstance = NonZeroIsize::new(hinstance);
|
||||
rwh::WindowHandle::borrow_raw(handle.into())
|
||||
})
|
||||
}
|
||||
@@ -1269,7 +1271,7 @@ impl WindowsWindow {
|
||||
let nheight = options.bounds.size.height.0;
|
||||
let hwndparent = HWND::default();
|
||||
let hmenu = HMENU::default();
|
||||
let hinstance = HINSTANCE::default();
|
||||
let hinstance = get_module_handle();
|
||||
let mut context = WindowCreateContext {
|
||||
inner: None,
|
||||
platform_inner: platform_inner.clone(),
|
||||
@@ -1455,7 +1457,7 @@ impl PlatformWindow for WindowsWindow {
|
||||
title = windows::core::w!("Warning");
|
||||
main_icon = TD_WARNING_ICON;
|
||||
}
|
||||
crate::PromptLevel::Critical => {
|
||||
crate::PromptLevel::Critical | crate::PromptLevel::Destructive => {
|
||||
title = windows::core::w!("Critical");
|
||||
main_icon = TD_ERROR_ICON;
|
||||
}
|
||||
@@ -1512,6 +1514,8 @@ impl PlatformWindow for WindowsWindow {
|
||||
.ok();
|
||||
}
|
||||
|
||||
fn set_app_id(&mut self, _app_id: &str) {}
|
||||
|
||||
fn set_background_appearance(&mut self, _background_appearance: WindowBackgroundAppearance) {
|
||||
// todo(windows)
|
||||
}
|
||||
@@ -1767,6 +1771,7 @@ fn register_wnd_class(icon_handle: HICON) -> PCWSTR {
|
||||
hIcon: icon_handle,
|
||||
lpszClassName: PCWSTR(CLASS_NAME.as_ptr()),
|
||||
style: CS_HREDRAW | CS_VREDRAW,
|
||||
hInstance: get_module_handle().into(),
|
||||
..Default::default()
|
||||
};
|
||||
unsafe { RegisterClassW(&wc) };
|
||||
@@ -1907,6 +1912,20 @@ struct StyleAndBounds {
|
||||
cy: i32,
|
||||
}
|
||||
|
||||
fn get_module_handle() -> HMODULE {
|
||||
unsafe {
|
||||
let mut h_module = std::mem::zeroed();
|
||||
GetModuleHandleExW(
|
||||
GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
|
||||
windows::core::w!("ZedModule"),
|
||||
&mut h_module,
|
||||
)
|
||||
.expect("Unable to get module handle"); // this should never fail
|
||||
|
||||
h_module
|
||||
}
|
||||
}
|
||||
|
||||
// https://learn.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-dragqueryfilew
|
||||
const DRAGDROP_GET_FILES_COUNT: u32 = 0xFFFFFFFF;
|
||||
// https://learn.microsoft.com/en-us/windows/win32/controls/ttm-setdelaytime?redirectedfrom=MSDN
|
||||
|
||||
@@ -262,7 +262,7 @@ impl TextStyle {
|
||||
pub fn font(&self) -> Font {
|
||||
Font {
|
||||
family: self.font_family.clone(),
|
||||
features: self.font_features,
|
||||
features: self.font_features.clone(),
|
||||
weight: self.font_weight,
|
||||
style: self.font_style,
|
||||
}
|
||||
@@ -628,6 +628,13 @@ impl From<&TextStyle> for HighlightStyle {
|
||||
}
|
||||
|
||||
impl HighlightStyle {
|
||||
/// Create a highlight style with just a color
|
||||
pub fn color(color: Hsla) -> Self {
|
||||
Self {
|
||||
color: Some(color),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
/// Blend this highlight style with another.
|
||||
/// Non-continuous properties, like font_weight and font_style, are overwritten.
|
||||
pub fn highlight(&mut self, other: HighlightStyle) {
|
||||
|
||||
@@ -55,11 +55,12 @@ impl SvgRenderer {
|
||||
};
|
||||
|
||||
// Render the SVG to a pixmap with the specified width and height.
|
||||
let mut pixmap =
|
||||
resvg::tiny_skia::Pixmap::new(size.width.into(), size.height.into()).unwrap();
|
||||
let mut pixmap = resvg::tiny_skia::Pixmap::new(size.width.into(), size.height.into())
|
||||
.ok_or(usvg::Error::InvalidSize)?;
|
||||
|
||||
let transform = tree.view_box().to_transform(
|
||||
resvg::tiny_skia::Size::from_wh(size.width.0 as f32, size.height.0 as f32).unwrap(),
|
||||
resvg::tiny_skia::Size::from_wh(size.width.0 as f32, size.height.0 as f32)
|
||||
.ok_or(usvg::Error::InvalidSize)?,
|
||||
);
|
||||
|
||||
resvg::render(&tree, transform, &mut pixmap.as_mut());
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
#[cfg(target_os = "windows")]
|
||||
use crate::SharedString;
|
||||
#[cfg(target_os = "windows")]
|
||||
use itertools::Itertools;
|
||||
use schemars::{
|
||||
schema::{InstanceType, Schema, SchemaObject, SingleOrVec},
|
||||
JsonSchema,
|
||||
@@ -7,10 +11,14 @@ macro_rules! create_definitions {
|
||||
($($(#[$meta:meta])* ($name:ident, $idx:expr)),* $(,)?) => {
|
||||
|
||||
/// The OpenType features that can be configured for a given font.
|
||||
#[derive(Default, Copy, Clone, Eq, PartialEq, Hash)]
|
||||
#[derive(Default, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct FontFeatures {
|
||||
enabled: u64,
|
||||
disabled: u64,
|
||||
#[cfg(target_os = "windows")]
|
||||
other_enabled: SharedString,
|
||||
#[cfg(target_os = "windows")]
|
||||
other_disabled: SharedString,
|
||||
}
|
||||
|
||||
impl FontFeatures {
|
||||
@@ -47,6 +55,14 @@ macro_rules! create_definitions {
|
||||
}
|
||||
}
|
||||
)*
|
||||
{
|
||||
for name in self.other_enabled.as_ref().chars().chunks(4).into_iter() {
|
||||
result.push((name.collect::<String>(), true));
|
||||
}
|
||||
for name in self.other_disabled.as_ref().chars().chunks(4).into_iter() {
|
||||
result.push((name.collect::<String>(), false));
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
@@ -59,6 +75,15 @@ macro_rules! create_definitions {
|
||||
debug.field(stringify!($name), &value);
|
||||
};
|
||||
)*
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
for name in self.other_enabled.as_ref().chars().chunks(4).into_iter() {
|
||||
debug.field(name.collect::<String>().as_str(), &true);
|
||||
}
|
||||
for name in self.other_disabled.as_ref().chars().chunks(4).into_iter() {
|
||||
debug.field(name.collect::<String>().as_str(), &false);
|
||||
}
|
||||
}
|
||||
debug.finish()
|
||||
}
|
||||
}
|
||||
@@ -80,6 +105,7 @@ macro_rules! create_definitions {
|
||||
formatter.write_str("a map of font features")
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
fn visit_map<M>(self, mut access: M) -> Result<Self::Value, M::Error>
|
||||
where
|
||||
M: MapAccess<'de>,
|
||||
@@ -100,6 +126,54 @@ macro_rules! create_definitions {
|
||||
}
|
||||
Ok(FontFeatures { enabled, disabled })
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn visit_map<M>(self, mut access: M) -> Result<Self::Value, M::Error>
|
||||
where
|
||||
M: MapAccess<'de>,
|
||||
{
|
||||
let mut enabled: u64 = 0;
|
||||
let mut disabled: u64 = 0;
|
||||
let mut other_enabled = "".to_owned();
|
||||
let mut other_disabled = "".to_owned();
|
||||
|
||||
while let Some((key, value)) = access.next_entry::<String, Option<bool>>()? {
|
||||
let idx = match key.as_str() {
|
||||
$(stringify!($name) => Some($idx),)*
|
||||
other_feature => {
|
||||
if other_feature.len() != 4 || !other_feature.is_ascii() {
|
||||
log::error!("Incorrect feature name: {}", other_feature);
|
||||
continue;
|
||||
}
|
||||
None
|
||||
},
|
||||
};
|
||||
if let Some(idx) = idx {
|
||||
match value {
|
||||
Some(true) => enabled |= 1 << idx,
|
||||
Some(false) => disabled |= 1 << idx,
|
||||
None => {}
|
||||
};
|
||||
} else {
|
||||
match value {
|
||||
Some(true) => other_enabled.push_str(key.as_str()),
|
||||
Some(false) => other_disabled.push_str(key.as_str()),
|
||||
None => {}
|
||||
};
|
||||
}
|
||||
}
|
||||
let other_enabled = if other_enabled.is_empty() {
|
||||
"".into()
|
||||
} else {
|
||||
other_enabled.into()
|
||||
};
|
||||
let other_disabled = if other_disabled.is_empty() {
|
||||
"".into()
|
||||
} else {
|
||||
other_disabled.into()
|
||||
};
|
||||
Ok(FontFeatures { enabled, disabled, other_enabled, other_disabled })
|
||||
}
|
||||
}
|
||||
|
||||
let features = deserializer.deserialize_map(FontFeaturesVisitor)?;
|
||||
@@ -125,6 +199,16 @@ macro_rules! create_definitions {
|
||||
}
|
||||
)*
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
for name in self.other_enabled.as_ref().chars().chunks(4).into_iter() {
|
||||
map.serialize_entry(name.collect::<String>().as_str(), &true)?;
|
||||
}
|
||||
for name in self.other_disabled.as_ref().chars().chunks(4).into_iter() {
|
||||
map.serialize_entry(name.collect::<String>().as_str(), &false)?;
|
||||
}
|
||||
}
|
||||
|
||||
map.end()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use crate::{
|
||||
seal::Sealed, AnyElement, AnyModel, AnyWeakModel, AppContext, Bounds, ContentMask, Element,
|
||||
ElementId, Entity, EntityId, Flatten, FocusHandle, FocusableView, IntoElement, LayoutId, Model,
|
||||
PaintIndex, Pixels, PrepaintStateIndex, Render, Style, StyleRefinement, TextStyle, ViewContext,
|
||||
VisualContext, WeakModel, WindowContext,
|
||||
ElementId, Entity, EntityId, Flatten, FocusHandle, FocusableView, GlobalElementId, IntoElement,
|
||||
LayoutId, Model, PaintIndex, Pixels, PrepaintStateIndex, Render, Style, StyleRefinement,
|
||||
TextStyle, ViewContext, VisualContext, WeakModel, WindowContext,
|
||||
};
|
||||
use anyhow::{Context, Result};
|
||||
use refineable::Refineable;
|
||||
@@ -93,36 +93,40 @@ impl<V: Render> Element for View<V> {
|
||||
type RequestLayoutState = AnyElement;
|
||||
type PrepaintState = ();
|
||||
|
||||
fn request_layout(&mut self, cx: &mut WindowContext) -> (LayoutId, Self::RequestLayoutState) {
|
||||
cx.with_element_id(Some(ElementId::View(self.entity_id())), |cx| {
|
||||
let mut element = self.update(cx, |view, cx| view.render(cx).into_any_element());
|
||||
let layout_id = element.request_layout(cx);
|
||||
(layout_id, element)
|
||||
})
|
||||
fn id(&self) -> Option<ElementId> {
|
||||
Some(ElementId::View(self.entity_id()))
|
||||
}
|
||||
|
||||
fn request_layout(
|
||||
&mut self,
|
||||
_id: Option<&GlobalElementId>,
|
||||
cx: &mut WindowContext,
|
||||
) -> (LayoutId, Self::RequestLayoutState) {
|
||||
let mut element = self.update(cx, |view, cx| view.render(cx).into_any_element());
|
||||
let layout_id = element.request_layout(cx);
|
||||
(layout_id, element)
|
||||
}
|
||||
|
||||
fn prepaint(
|
||||
&mut self,
|
||||
_id: Option<&GlobalElementId>,
|
||||
_: Bounds<Pixels>,
|
||||
element: &mut Self::RequestLayoutState,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
cx.set_view_id(self.entity_id());
|
||||
cx.with_element_id(Some(ElementId::View(self.entity_id())), |cx| {
|
||||
element.prepaint(cx)
|
||||
})
|
||||
element.prepaint(cx);
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
_id: Option<&GlobalElementId>,
|
||||
_: Bounds<Pixels>,
|
||||
element: &mut Self::RequestLayoutState,
|
||||
_: &mut Self::PrepaintState,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
cx.with_element_id(Some(ElementId::View(self.entity_id())), |cx| {
|
||||
element.paint(cx)
|
||||
})
|
||||
element.paint(cx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -279,112 +283,108 @@ impl Element for AnyView {
|
||||
type RequestLayoutState = Option<AnyElement>;
|
||||
type PrepaintState = Option<AnyElement>;
|
||||
|
||||
fn request_layout(&mut self, cx: &mut WindowContext) -> (LayoutId, Self::RequestLayoutState) {
|
||||
fn id(&self) -> Option<ElementId> {
|
||||
Some(ElementId::View(self.entity_id()))
|
||||
}
|
||||
|
||||
fn request_layout(
|
||||
&mut self,
|
||||
_id: Option<&GlobalElementId>,
|
||||
cx: &mut WindowContext,
|
||||
) -> (LayoutId, Self::RequestLayoutState) {
|
||||
if let Some(style) = self.cached_style.as_ref() {
|
||||
let mut root_style = Style::default();
|
||||
root_style.refine(style);
|
||||
let layout_id = cx.request_layout(&root_style, None);
|
||||
(layout_id, None)
|
||||
} else {
|
||||
cx.with_element_id(Some(ElementId::View(self.entity_id())), |cx| {
|
||||
let mut element = (self.render)(self, cx);
|
||||
let layout_id = element.request_layout(cx);
|
||||
(layout_id, Some(element))
|
||||
})
|
||||
let mut element = (self.render)(self, cx);
|
||||
let layout_id = element.request_layout(cx);
|
||||
(layout_id, Some(element))
|
||||
}
|
||||
}
|
||||
|
||||
fn prepaint(
|
||||
&mut self,
|
||||
global_id: Option<&GlobalElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
element: &mut Self::RequestLayoutState,
|
||||
cx: &mut WindowContext,
|
||||
) -> Option<AnyElement> {
|
||||
cx.set_view_id(self.entity_id());
|
||||
if self.cached_style.is_some() {
|
||||
cx.with_element_state::<AnyViewState, _>(
|
||||
Some(ElementId::View(self.entity_id())),
|
||||
|element_state, cx| {
|
||||
let mut element_state = element_state.unwrap();
|
||||
cx.with_element_state::<AnyViewState, _>(global_id.unwrap(), |element_state, cx| {
|
||||
let content_mask = cx.content_mask();
|
||||
let text_style = cx.text_style();
|
||||
|
||||
let content_mask = cx.content_mask();
|
||||
let text_style = cx.text_style();
|
||||
|
||||
if let Some(mut element_state) = element_state {
|
||||
if element_state.cache_key.bounds == bounds
|
||||
&& element_state.cache_key.content_mask == content_mask
|
||||
&& element_state.cache_key.text_style == text_style
|
||||
&& !cx.window.dirty_views.contains(&self.entity_id())
|
||||
&& !cx.window.refreshing
|
||||
{
|
||||
let prepaint_start = cx.prepaint_index();
|
||||
cx.reuse_prepaint(element_state.prepaint_range.clone());
|
||||
let prepaint_end = cx.prepaint_index();
|
||||
element_state.prepaint_range = prepaint_start..prepaint_end;
|
||||
return (None, Some(element_state));
|
||||
}
|
||||
if let Some(mut element_state) = element_state {
|
||||
if element_state.cache_key.bounds == bounds
|
||||
&& element_state.cache_key.content_mask == content_mask
|
||||
&& element_state.cache_key.text_style == text_style
|
||||
&& !cx.window.dirty_views.contains(&self.entity_id())
|
||||
&& !cx.window.refreshing
|
||||
{
|
||||
let prepaint_start = cx.prepaint_index();
|
||||
cx.reuse_prepaint(element_state.prepaint_range.clone());
|
||||
let prepaint_end = cx.prepaint_index();
|
||||
element_state.prepaint_range = prepaint_start..prepaint_end;
|
||||
return (None, element_state);
|
||||
}
|
||||
}
|
||||
|
||||
let prepaint_start = cx.prepaint_index();
|
||||
let mut element = (self.render)(self, cx);
|
||||
element.layout_as_root(bounds.size.into(), cx);
|
||||
element.prepaint_at(bounds.origin, cx);
|
||||
let prepaint_end = cx.prepaint_index();
|
||||
let prepaint_start = cx.prepaint_index();
|
||||
let mut element = (self.render)(self, cx);
|
||||
element.layout_as_root(bounds.size.into(), cx);
|
||||
element.prepaint_at(bounds.origin, cx);
|
||||
let prepaint_end = cx.prepaint_index();
|
||||
|
||||
(
|
||||
Some(element),
|
||||
Some(AnyViewState {
|
||||
prepaint_range: prepaint_start..prepaint_end,
|
||||
paint_range: PaintIndex::default()..PaintIndex::default(),
|
||||
cache_key: ViewCacheKey {
|
||||
bounds,
|
||||
content_mask,
|
||||
text_style,
|
||||
},
|
||||
}),
|
||||
)
|
||||
},
|
||||
)
|
||||
} else {
|
||||
cx.with_element_id(Some(ElementId::View(self.entity_id())), |cx| {
|
||||
let mut element = element.take().unwrap();
|
||||
element.prepaint(cx);
|
||||
Some(element)
|
||||
(
|
||||
Some(element),
|
||||
AnyViewState {
|
||||
prepaint_range: prepaint_start..prepaint_end,
|
||||
paint_range: PaintIndex::default()..PaintIndex::default(),
|
||||
cache_key: ViewCacheKey {
|
||||
bounds,
|
||||
content_mask,
|
||||
text_style,
|
||||
},
|
||||
},
|
||||
)
|
||||
})
|
||||
} else {
|
||||
let mut element = element.take().unwrap();
|
||||
element.prepaint(cx);
|
||||
Some(element)
|
||||
}
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
global_id: Option<&GlobalElementId>,
|
||||
_bounds: Bounds<Pixels>,
|
||||
_: &mut Self::RequestLayoutState,
|
||||
element: &mut Self::PrepaintState,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
if self.cached_style.is_some() {
|
||||
cx.with_element_state::<AnyViewState, _>(
|
||||
Some(ElementId::View(self.entity_id())),
|
||||
|element_state, cx| {
|
||||
let mut element_state = element_state.unwrap().unwrap();
|
||||
cx.with_element_state::<AnyViewState, _>(global_id.unwrap(), |element_state, cx| {
|
||||
let mut element_state = element_state.unwrap();
|
||||
|
||||
let paint_start = cx.paint_index();
|
||||
let paint_start = cx.paint_index();
|
||||
|
||||
if let Some(element) = element {
|
||||
element.paint(cx);
|
||||
} else {
|
||||
cx.reuse_paint(element_state.paint_range.clone());
|
||||
}
|
||||
if let Some(element) = element {
|
||||
element.paint(cx);
|
||||
} else {
|
||||
cx.reuse_paint(element_state.paint_range.clone());
|
||||
}
|
||||
|
||||
let paint_end = cx.paint_index();
|
||||
element_state.paint_range = paint_start..paint_end;
|
||||
let paint_end = cx.paint_index();
|
||||
element_state.paint_range = paint_start..paint_end;
|
||||
|
||||
((), Some(element_state))
|
||||
},
|
||||
)
|
||||
} else {
|
||||
cx.with_element_id(Some(ElementId::View(self.entity_id())), |cx| {
|
||||
element.as_mut().unwrap().paint(cx);
|
||||
((), element_state)
|
||||
})
|
||||
} else {
|
||||
element.as_mut().unwrap().paint(cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -353,7 +353,7 @@ pub(crate) struct TooltipRequest {
|
||||
pub(crate) struct DeferredDraw {
|
||||
priority: usize,
|
||||
parent_node: DispatchNodeId,
|
||||
element_id_stack: GlobalElementId,
|
||||
element_id_stack: SmallVec<[ElementId; 32]>,
|
||||
text_style_stack: Vec<TextStyleRefinement>,
|
||||
element: Option<AnyElement>,
|
||||
absolute_offset: Point<Pixels>,
|
||||
@@ -454,9 +454,10 @@ impl Frame {
|
||||
|
||||
pub(crate) fn finish(&mut self, prev_frame: &mut Self) {
|
||||
for element_state_key in &self.accessed_element_states {
|
||||
if let Some(element_state) = prev_frame.element_states.remove(element_state_key) {
|
||||
self.element_states
|
||||
.insert(element_state_key.clone(), element_state);
|
||||
if let Some((element_state_key, element_state)) =
|
||||
prev_frame.element_states.remove_entry(element_state_key)
|
||||
{
|
||||
self.element_states.insert(element_state_key, element_state);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -477,7 +478,7 @@ pub struct Window {
|
||||
pub(crate) viewport_size: Size<Pixels>,
|
||||
layout_engine: Option<TaffyLayoutEngine>,
|
||||
pub(crate) root_view: Option<AnyView>,
|
||||
pub(crate) element_id_stack: GlobalElementId,
|
||||
pub(crate) element_id_stack: SmallVec<[ElementId; 32]>,
|
||||
pub(crate) text_style_stack: Vec<TextStyleRefinement>,
|
||||
pub(crate) element_offset_stack: Vec<Point<Pixels>>,
|
||||
pub(crate) content_mask_stack: Vec<ContentMask<Pixels>>,
|
||||
@@ -599,10 +600,11 @@ impl Window {
|
||||
display_id,
|
||||
fullscreen,
|
||||
window_background,
|
||||
app_id,
|
||||
} = options;
|
||||
|
||||
let bounds = bounds.unwrap_or_else(|| default_bounds(display_id, cx));
|
||||
let platform_window = cx.platform.open_window(
|
||||
let mut platform_window = cx.platform.open_window(
|
||||
handle,
|
||||
WindowParams {
|
||||
bounds,
|
||||
@@ -734,6 +736,10 @@ impl Window {
|
||||
})
|
||||
});
|
||||
|
||||
if let Some(app_id) = app_id {
|
||||
platform_window.set_app_id(&app_id);
|
||||
}
|
||||
|
||||
Window {
|
||||
handle,
|
||||
removed: false,
|
||||
@@ -745,7 +751,7 @@ impl Window {
|
||||
viewport_size: content_size,
|
||||
layout_engine: Some(TaffyLayoutEngine::new()),
|
||||
root_view: None,
|
||||
element_id_stack: GlobalElementId::default(),
|
||||
element_id_stack: SmallVec::default(),
|
||||
text_style_stack: Vec::new(),
|
||||
element_offset_stack: Vec::new(),
|
||||
content_mask_stack: Vec::new(),
|
||||
@@ -1124,6 +1130,11 @@ impl<'a> WindowContext<'a> {
|
||||
self.window.platform_window.set_title(title);
|
||||
}
|
||||
|
||||
/// Sets the application identifier.
|
||||
pub fn set_app_id(&mut self, app_id: &str) {
|
||||
self.window.platform_window.set_app_id(app_id);
|
||||
}
|
||||
|
||||
/// Sets the window background appearance.
|
||||
pub fn set_background_appearance(&mut self, background_appearance: WindowBackgroundAppearance) {
|
||||
self.window
|
||||
@@ -1499,7 +1510,7 @@ impl<'a> WindowContext<'a> {
|
||||
window.rendered_frame.accessed_element_states[range.start.accessed_element_states_index
|
||||
..range.end.accessed_element_states_index]
|
||||
.iter()
|
||||
.cloned(),
|
||||
.map(|(id, type_id)| (GlobalElementId(id.0.clone()), *type_id)),
|
||||
);
|
||||
window
|
||||
.text_system
|
||||
@@ -1562,7 +1573,7 @@ impl<'a> WindowContext<'a> {
|
||||
window.rendered_frame.accessed_element_states[range.start.accessed_element_states_index
|
||||
..range.end.accessed_element_states_index]
|
||||
.iter()
|
||||
.cloned(),
|
||||
.map(|(id, type_id)| (GlobalElementId(id.0.clone()), *type_id)),
|
||||
);
|
||||
window
|
||||
.text_system
|
||||
@@ -1630,35 +1641,6 @@ impl<'a> WindowContext<'a> {
|
||||
id
|
||||
}
|
||||
|
||||
/// Pushes the given element id onto the global stack and invokes the given closure
|
||||
/// with a `GlobalElementId`, which disambiguates the given id in the context of its ancestor
|
||||
/// ids. Because elements are discarded and recreated on each frame, the `GlobalElementId` is
|
||||
/// used to associate state with identified elements across separate frames. This method should
|
||||
/// only be called as part of element drawing.
|
||||
pub fn with_element_id<R>(
|
||||
&mut self,
|
||||
id: Option<impl Into<ElementId>>,
|
||||
f: impl FnOnce(&mut Self) -> R,
|
||||
) -> R {
|
||||
debug_assert!(
|
||||
matches!(
|
||||
self.window.draw_phase,
|
||||
DrawPhase::Prepaint | DrawPhase::Paint
|
||||
),
|
||||
"this method can only be called during request_layout, prepaint, or paint"
|
||||
);
|
||||
if let Some(id) = id.map(Into::into) {
|
||||
let window = self.window_mut();
|
||||
window.element_id_stack.push(id);
|
||||
let result = f(self);
|
||||
let window: &mut Window = self.borrow_mut();
|
||||
window.element_id_stack.pop();
|
||||
result
|
||||
} else {
|
||||
f(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// Invoke the given function with the given content mask after intersecting it
|
||||
/// with the current mask. This method should only be called during element drawing.
|
||||
pub fn with_content_mask<R>(
|
||||
@@ -1903,13 +1885,114 @@ impl<'a> WindowContext<'a> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Provide elements in the called function with a new namespace in which their identiers must be unique.
|
||||
/// This can be used within a custom element to distinguish multiple sets of child elements.
|
||||
pub fn with_element_namespace<R>(
|
||||
&mut self,
|
||||
element_id: impl Into<ElementId>,
|
||||
f: impl FnOnce(&mut Self) -> R,
|
||||
) -> R {
|
||||
self.window.element_id_stack.push(element_id.into());
|
||||
let result = f(self);
|
||||
self.window.element_id_stack.pop();
|
||||
result
|
||||
}
|
||||
|
||||
/// Updates or initializes state for an element with the given id that lives across multiple
|
||||
/// frames. If an element with this ID existed in the rendered frame, its state will be passed
|
||||
/// to the given closure. The state returned by the closure will be stored so it can be referenced
|
||||
/// when drawing the next frame. This method should only be called as part of element drawing.
|
||||
pub fn with_element_state<S, R>(
|
||||
&mut self,
|
||||
element_id: Option<ElementId>,
|
||||
global_id: &GlobalElementId,
|
||||
f: impl FnOnce(Option<S>, &mut Self) -> (R, S),
|
||||
) -> R
|
||||
where
|
||||
S: 'static,
|
||||
{
|
||||
debug_assert!(
|
||||
matches!(
|
||||
self.window.draw_phase,
|
||||
DrawPhase::Prepaint | DrawPhase::Paint
|
||||
),
|
||||
"this method can only be called during request_layout, prepaint, or paint"
|
||||
);
|
||||
|
||||
let key = (GlobalElementId(global_id.0.clone()), TypeId::of::<S>());
|
||||
self.window
|
||||
.next_frame
|
||||
.accessed_element_states
|
||||
.push((GlobalElementId(key.0.clone()), TypeId::of::<S>()));
|
||||
|
||||
if let Some(any) = self
|
||||
.window
|
||||
.next_frame
|
||||
.element_states
|
||||
.remove(&key)
|
||||
.or_else(|| self.window.rendered_frame.element_states.remove(&key))
|
||||
{
|
||||
let ElementStateBox {
|
||||
inner,
|
||||
#[cfg(debug_assertions)]
|
||||
type_name,
|
||||
} = any;
|
||||
// Using the extra inner option to avoid needing to reallocate a new box.
|
||||
let mut state_box = inner
|
||||
.downcast::<Option<S>>()
|
||||
.map_err(|_| {
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
anyhow::anyhow!(
|
||||
"invalid element state type for id, requested {:?}, actual: {:?}",
|
||||
std::any::type_name::<S>(),
|
||||
type_name
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
{
|
||||
anyhow::anyhow!(
|
||||
"invalid element state type for id, requested {:?}",
|
||||
std::any::type_name::<S>(),
|
||||
)
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let state = state_box.take().expect(
|
||||
"reentrant call to with_element_state for the same state type and element id",
|
||||
);
|
||||
let (result, state) = f(Some(state), self);
|
||||
state_box.replace(state);
|
||||
self.window.next_frame.element_states.insert(
|
||||
key,
|
||||
ElementStateBox {
|
||||
inner: state_box,
|
||||
#[cfg(debug_assertions)]
|
||||
type_name,
|
||||
},
|
||||
);
|
||||
result
|
||||
} else {
|
||||
let (result, state) = f(None, self);
|
||||
self.window.next_frame.element_states.insert(
|
||||
key,
|
||||
ElementStateBox {
|
||||
inner: Box::new(Some(state)),
|
||||
#[cfg(debug_assertions)]
|
||||
type_name: std::any::type_name::<S>(),
|
||||
},
|
||||
);
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
/// A variant of `with_element_state` that allows the element's id to be optional. This is a convenience
|
||||
/// method for elements where the element id may or may not be assigned. Prefer using `with_element_state`
|
||||
/// when the element is guaranteed to have an id.
|
||||
pub fn with_optional_element_state<S, R>(
|
||||
&mut self,
|
||||
global_id: Option<&GlobalElementId>,
|
||||
f: impl FnOnce(Option<Option<S>>, &mut Self) -> (R, Option<S>),
|
||||
) -> R
|
||||
where
|
||||
@@ -1922,90 +2005,22 @@ impl<'a> WindowContext<'a> {
|
||||
),
|
||||
"this method can only be called during request_layout, prepaint, or paint"
|
||||
);
|
||||
let id_is_none = element_id.is_none();
|
||||
self.with_element_id(element_id, |cx| {
|
||||
if id_is_none {
|
||||
let (result, state) = f(None, cx);
|
||||
debug_assert!(state.is_none(), "you must not return an element state when passing None for the element id");
|
||||
result
|
||||
} else {
|
||||
let global_id = cx.window().element_id_stack.clone();
|
||||
let key = (global_id, TypeId::of::<S>());
|
||||
cx.window.next_frame.accessed_element_states.push(key.clone());
|
||||
|
||||
if let Some(any) = cx
|
||||
.window_mut()
|
||||
.next_frame
|
||||
.element_states
|
||||
.remove(&key)
|
||||
.or_else(|| {
|
||||
cx.window_mut()
|
||||
.rendered_frame
|
||||
.element_states
|
||||
.remove(&key)
|
||||
})
|
||||
{
|
||||
let ElementStateBox {
|
||||
inner,
|
||||
#[cfg(debug_assertions)]
|
||||
type_name
|
||||
} = any;
|
||||
// Using the extra inner option to avoid needing to reallocate a new box.
|
||||
let mut state_box = inner
|
||||
.downcast::<Option<S>>()
|
||||
.map_err(|_| {
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
anyhow::anyhow!(
|
||||
"invalid element state type for id, requested_type {:?}, actual type: {:?}",
|
||||
std::any::type_name::<S>(),
|
||||
type_name
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
{
|
||||
anyhow::anyhow!(
|
||||
"invalid element state type for id, requested_type {:?}",
|
||||
std::any::type_name::<S>(),
|
||||
)
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
// Actual: Option<AnyElement> <- View
|
||||
// Requested: () <- AnyElement
|
||||
let state = state_box
|
||||
.take()
|
||||
.expect("reentrant call to with_element_state for the same state type and element id");
|
||||
let (result, state) = f(Some(Some(state)), cx);
|
||||
state_box.replace(state.expect("you must return "));
|
||||
cx.window_mut()
|
||||
.next_frame
|
||||
.element_states
|
||||
.insert(key, ElementStateBox {
|
||||
inner: state_box,
|
||||
#[cfg(debug_assertions)]
|
||||
type_name
|
||||
});
|
||||
result
|
||||
} else {
|
||||
let (result, state) = f(Some(None), cx);
|
||||
cx.window_mut()
|
||||
.next_frame
|
||||
.element_states
|
||||
.insert(key,
|
||||
ElementStateBox {
|
||||
inner: Box::new(Some(state.expect("you must return Some<State> when you pass some element id"))),
|
||||
#[cfg(debug_assertions)]
|
||||
type_name: std::any::type_name::<S>()
|
||||
}
|
||||
|
||||
);
|
||||
result
|
||||
}
|
||||
}
|
||||
if let Some(global_id) = global_id {
|
||||
self.with_element_state(global_id, |state, cx| {
|
||||
let (result, state) = f(Some(state), cx);
|
||||
let state =
|
||||
state.expect("you must return some state when you pass some element id");
|
||||
(result, state)
|
||||
})
|
||||
} else {
|
||||
let (result, state) = f(None, self);
|
||||
debug_assert!(
|
||||
state.is_none(),
|
||||
"you must not return an element state when passing None for the global id"
|
||||
);
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
/// Defers the drawing of the given element, scheduling it to be painted on top of the currently-drawn tree
|
||||
|
||||
@@ -606,13 +606,21 @@ impl SyntaxSnapshot {
|
||||
LogIncludedRanges(&included_ranges),
|
||||
);
|
||||
|
||||
tree = parse_text(
|
||||
let result = parse_text(
|
||||
grammar,
|
||||
text.as_rope(),
|
||||
step_start_byte,
|
||||
included_ranges,
|
||||
Some(old_tree.clone()),
|
||||
);
|
||||
match result {
|
||||
Ok(t) => tree = t,
|
||||
Err(e) => {
|
||||
log::error!("error parsing text: {:?}", e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
changed_ranges = join_ranges(
|
||||
invalidated_ranges
|
||||
.iter()
|
||||
@@ -651,13 +659,20 @@ impl SyntaxSnapshot {
|
||||
LogIncludedRanges(&included_ranges),
|
||||
);
|
||||
|
||||
tree = parse_text(
|
||||
let result = parse_text(
|
||||
grammar,
|
||||
text.as_rope(),
|
||||
step_start_byte,
|
||||
included_ranges,
|
||||
None,
|
||||
);
|
||||
match result {
|
||||
Ok(t) => tree = t,
|
||||
Err(e) => {
|
||||
log::error!("error parsing text: {:?}", e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
changed_ranges = vec![step_start_byte..step_end_byte];
|
||||
}
|
||||
|
||||
@@ -1161,16 +1176,12 @@ fn parse_text(
|
||||
start_byte: usize,
|
||||
ranges: Vec<tree_sitter::Range>,
|
||||
old_tree: Option<Tree>,
|
||||
) -> Tree {
|
||||
) -> anyhow::Result<Tree> {
|
||||
PARSER.with(|parser| {
|
||||
let mut parser = parser.borrow_mut();
|
||||
let mut chunks = text.chunks_in_range(start_byte..text.len());
|
||||
parser
|
||||
.set_included_ranges(&ranges)
|
||||
.expect("overlapping ranges");
|
||||
parser
|
||||
.set_language(&grammar.ts_language)
|
||||
.expect("incompatible grammar");
|
||||
parser.set_included_ranges(&ranges)?;
|
||||
parser.set_language(&grammar.ts_language)?;
|
||||
parser
|
||||
.parse_with(
|
||||
&mut move |offset, _| {
|
||||
@@ -1179,7 +1190,7 @@ fn parse_text(
|
||||
},
|
||||
old_tree.as_ref(),
|
||||
)
|
||||
.expect("invalid language")
|
||||
.ok_or_else(|| anyhow::anyhow!("failed to parse"))
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user