Compare commits

...

41 Commits

Author SHA1 Message Date
Mikayla
e19f6416d4 Merge branch 'main' into tab-bar-settings 2024-04-26 14:32:57 -07:00
Jakob Hellermann
393b16d226 Fix Wayland keyrepeat getting cancelled by unrelated keyup (#11052)
fixes #11048

## Problem
in the situation `press right`, `press left`, `release right` the
following happens right now:

- `keypressed right`, `current_keysym` is set to `right`
- `keypressed left`, `current_keysym` is set to `left`

the repeat timer runs asynchronously and emits keyrepeats since
`current_keysym.is_some()`

- `keyreleased right`, `current_keysym` is set to None

the repeat timer no longer emits keyrepeats

- `keyreleased left`, this is where `current_keysym` should actually be
set to None.

## Solution
Only reset `current_keysym` if the released key matches the last pressed
key.

Release Notes:

- N/A
2024-04-26 14:07:05 -07:00
Akilan Elango
7bd18fa653 Sync maximized state from top-level configure event for a wayland window (#11003)
* Otherwise is_maximized always returns `true`



Release Notes:

- Fixed maximized state. Tested with a dummy maximize/restore button
with the `zoom()` (not implemented yet). Without the right `maximized`,
in toggle zoom function is not possible to call `set_maximized()` or
`unset_maximized()`.

```rust
    fn zoom(&self) {
      if self.is_maximized() {
        self.borrow_mut().toplevel.unset_maximized();
      } else {
        self.borrow_mut().toplevel.set_maximized();
      }
    }
```
2024-04-26 14:03:19 -07:00
张小白
11dc3c2582 windows: Support all OpenType font features (#10756)
Release Notes:

- Added support for all `OpenType` font features to DirectWrite.



https://github.com/zed-industries/zed/assets/14981363/cb2848cd-9178-4d87-881a-54dc646b2b61

---------

Co-authored-by: Mikayla Maki <mikayla@zed.dev>
2024-04-26 13:58:12 -07:00
张小白
268cb948a7 windows: Move manifest file to gpui (#11036)
This is a follow up of #10810 , `embed-resource` crate uses a different
method to link the manifest file, so this makes moving manifest file to
`gpui` possible.

Now, examples can run as expected:
![Screenshot 2024-04-26
111559](https://github.com/zed-industries/zed/assets/14981363/bb040690-8129-490b-83b3-0a7d3cbd4953)

TODO:
- [ ] check if it builds with gnu toolchain

Release Notes:

- N/A
2024-04-26 13:56:48 -07:00
张小白
6a915e349c windows: Fix panicking on startup (#11028)
### Connection:
Closes #10954 

Release Notes:

- N/A
2024-04-26 13:55:41 -07:00
apricotbucket28
70d03e4841 x11: Fix window close (#11008)
Fixes https://github.com/zed-industries/zed/issues/10483 on X11

Also calls the `should_close` callback before closing the window (needed
for the "Do you want to save?" dialog).

Release Notes:

- N/A
2024-04-26 13:53:49 -07:00
DissolveDZ
b1eb0291dc Re-add README.md which might have been deleted by mistake (#11067)
Release Notes:

- N/A
2024-04-26 16:04:25 -04:00
Conrad Irwin
e0644de90e Fix panic in Diagnostics (#11066)
cc @maxbrunsfeld

Release Notes:

- Fixed a panic in populating diagnostics
2024-04-26 14:04:18 -06:00
Bennet Bo Fenner
9329ef1d78 markdown preview: Break up list items into individual blocks (#10852)
Fixes a panic related to rendering checkboxes, see #10824.

Currently we are rendering a list into a single block, meaning the whole
block has to be rendered when it is visible on screen. This would lead
to performance problems when a single list block contained a lot of
items (especially if it contained checkboxes). This PR splits up list
items into separate blocks, meaning only the actual visible list items
on screen get rendered, instead of the whole list.
A nice side effect of the refactoring is, that you can actually click on
individual list items now:


https://github.com/zed-industries/zed/assets/53836821/5ef4200c-bd85-4e96-a8bf-e0c8b452f762

Release Notes:

- Improved rendering performance of list elements inside the markdown
preview

---------

Co-authored-by: Remco <djsmits12@gmail.com>
2024-04-26 21:34:45 +02:00
Conrad Irwin
664f779eb4 new path picker (#11015)
Still TODO:

* Disable the new save-as for local projects
* Wire up sending the new path to the remote server

Release Notes:

- Added the ability to "Save-as" in remote projects

---------

Co-authored-by: Nathan <nathan@zed.dev>
Co-authored-by: Bennet <bennetbo@gmx.de>
2024-04-26 13:25:25 -06:00
Bennet Bo Fenner
314b723292 remote projects: Allow reusing window (#11058)
Release Notes:

- Allow reusing the window when opening a remote project from the recent
projects picker
- Fixed an issue, which would not let you rejoin a remote project after
disconnecting from it for the first time

---------

Co-authored-by: Conrad <conrad@zed.dev>
Co-authored-by: Remco <djsmits12@gmail.com>
2024-04-26 21:04:34 +02:00
Tim Masliuchenko
1af1a9e8b3 Toggle tasks modal in task::Rerun, when no tasks have been scheduled (#11059)
Currently, when no tasks have been scheduled, the `task::Rerun` action
does nothing.
This PR adds a fallback, so when no tasks have been scheduled so far the
`task::Rerun` action toggles the tasks modal

https://github.com/zed-industries/zed/assets/471335/72f7a71e-cfa8-49db-a295-fb05b2e7c905

Release Notes:

- Improved the `task::Rerun` action to toggle the tasks modal when no
tasks have been scheduled so far
2024-04-26 17:56:34 +02:00
Jakob Hellermann
8006f69513 Fix Cargo.toml typo ref -> rev (#11047)
Release Notes:

- N/A
2024-04-26 08:31:04 -04:00
Piotr Osiewicz
bacc92333a tasks: Fix divider position in modal (#11049)
The divider between templates and recent runs is constant, regardless of
the currently used filter string; this can lead to situations where an
user can remove the predefined task, which isn't good at all.

Additionally, in this PR I've made it so that recent runs always show up
before task templates in filtered list.

Release Notes:

- Fixed position of list divider in task modal.
2024-04-26 14:29:16 +02:00
Conrad Irwin
eb7bd0b98a Use fewer fancy cursors even for vim users (#11041)
Release Notes:

- N/A
2024-04-26 09:42:21 +02:00
Conrad Irwin
7f229dc202 Remove unread notes indicator for now (#11035)
I'd like to add something back here, but it's more distracting than
helpful today.

Fixes: #10887

Release Notes:

- Removed channel notes unread indicator

---------

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2024-04-25 23:50:31 -04:00
Conrad Irwin
03d0b68f0c Fix panic in rename selections (#11033)
cc @someonetoignore

Release Notes:

- Fixed a panic when renaming with a selection (preview only)
2024-04-25 21:29:56 -06:00
Hans
5c2f27a501 Fix VIM cw on last character of a word doesn't work as expected: (#10963)
At the moment, using the default expand_selection seems to do the job
well, without the need for some additional logic, which may also make
the code a little clearer, Fix #10945



Release Notes:


- N/A
2024-04-25 21:09:06 -06:00
Conrad Irwin
d9d509a2bb Send installation id with crashes (#11032)
This will let us prioritize crashes that affect many users.

Release Notes:

- N/A
2024-04-25 21:07:06 -06:00
Marshall Bowers
a4ad3bcc08 Hoist nanoid to workspace-level (#11029)
This PR hoists `nanoid` up to a workspace dependency.

Release Notes:

- N/A
2024-04-25 22:37:40 -04:00
Conrad Irwin
6d7332e80c Fix panic in vim search (#11022)
Release Notes:

- vim: Fixed a panic when searching
2024-04-25 20:32:15 -06:00
Marshall Bowers
1b614ef63b Add an Assistant example that can interact with the filesystem (#11027)
This PR adds a new Assistant example that is able to interact with the
filesystem using a tool.

Release Notes:

- N/A
2024-04-25 22:21:18 -04:00
Hendrik Sollich
604857ed2e vim: Increment search right (#10866)
Hi there, nice editor!
Here's my attempt at fixing #10865.

Thanks

Release Notes:

-vim: Fix ctrl+a when cursor is on a decimal point
([#10865](https://github.com/zed-industries/zed/issues/10865)).

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2024-04-25 19:47:52 -06:00
DissolveDZ
d9eb3c4b35 vim: Fix hollow cursor being offset when selecting text (#11000)
Fixed the cursor selection being offset, the hollow cursor was being
displayed fine when not having text selected that's why it might not
have been noticed at first.

Release Notes:
- N/A

Improved:
0d6fb08b67
2024-04-25 19:47:12 -06:00
Marshall Bowers
f8beda0704 Rename chat_with_functions to use snake_case (#11020)
This PR renames the `chat-with-functions.rs` example to use snake_case
for the filename, as is convention.

Release Notes:

- N/A
2024-04-25 21:43:02 -04:00
Max Brunsfeld
40fe5275cf Rework project diagnostics to prevent showing inconsistent state (#10922)
For a long time, we've had problems where diagnostics can end up showing
up inconsistently in different views. This PR is my attempt to prevent
that, and to simplify the system in the process. There are some UX
changes.

Diagnostic behaviors that have *not* changed:

* In-buffer diagnostics update immediately when LSPs send diagnostics
updates.
* The diagnostic counts in the status bar indicator also update
immediately.

Diagnostic behaviors that this PR changes:

* [x] The tab title for the project diagnostics view now simply shows
the same counts as the status bar indicator - the project's current
totals. Previously, this tab title showed something slightly different -
the numbers of diagnostics *currently shown* in the diagnostics view's
excerpts. But it was pretty confusing that you could sometimes see two
different diagnostic counts.
* [x] The project diagnostics view **never** updates its excerpts while
the user might be in the middle of typing it that view, unless the user
expressed an intent for the excerpts to update (by e.g. saving the
buffer). This was the behavior we originally implemented, but has
changed a few times since then, in attempts to fix other issues. I've
restored that invariant.

    Times when the excerpts will update:
     * diagnostics are updated while the diagnostics view is not focused
     * the user changes focus away from the diagnostics view
* the language server sends a `work done progress end` message for its
disk-based diagnostics token (i.e. cargo check finishes)
* the user saves a buffer associated with a language server, and then a
debounce timer expires

* [x] The project diagnostics view indicates when its diagnostics are
stale. States:
* when diagnostics have been updated while the diagnostics view was
focused:
        * the indicator shows a 'refresh' icon
        * clicking the indicator updates the excerpts
* when diagnostics have been updated, but a file has been saved, so that
the diagnostics will soon update, the indicator is disabled

With these UX changes, the only 'complex' part of the our diagnostics
presentation is the Project Diagnostics view's excerpt management,
because it needs to implement the deferred updates in order to avoid
disrupting the user while they may be typing. I want to take some steps
to reduce the potential for bugs in this view.

* [x] Reduce the amount of state that the view uses, and simplify its
implementation
* [x] Add a randomized test that checks the invariant that a mutated
diagnostics view matches a freshly computed diagnostics view


##  Release Notes

- Reworked the project diagnostics view:
- Fixed an issue where the project diagnostics view could update its
excerpts while you were typing in it.
- Fixed bugs where the project diagnostics view could show the wrong
excerpts.
- Changed the diagnostics view to always update its excerpts eagerly
when not focused.
- Added an indicator to the project diagnostics view's toolbar, showing
when diagnostics have been changed.

---------

Co-authored-by: Richard Feldman <oss@rtfeldman.com>
2024-04-25 18:12:15 -07:00
Andrew Lygin
6fb6cd3c5c Merge branch 'main' into tab-bar-settings 2024-04-21 08:15:22 +03:00
Andrew Lygin
0875257852 Merge branch 'main' into tab-bar-settings 2024-04-17 21:40:03 +03:00
Andrew Lygin
eb97c311c8 Move the placement setting to tab_bar 2024-04-13 18:23:09 +03:00
Andrew Lygin
6422fdea9b Merge branch 'main' into tab-bar-settings 2024-04-13 10:25:23 +03:00
Andrew Lygin
9d684d7856 Resolve conflicts 2024-03-23 16:30:10 +03:00
Andrew Lygin
93978f6017 Fix tests 2024-03-23 16:08:05 +03:00
Andrew Lygin
120ead9429 Fix tests 2024-03-23 16:08:05 +03:00
Andrew Lygin
99a0356a56 Fix tests 2024-03-23 16:08:05 +03:00
Andrew Lygin
94858caff5 Fix typo 2024-03-23 16:08:05 +03:00
Andrew Lygin
ca9645c2bf Move tab bar placement setting to the "tabs" section 2024-03-23 16:08:05 +03:00
Andrew Lygin
b81e8971df Fix default tab bar placement 2024-03-23 15:51:57 +03:00
Andrew Lygin
ed2651d62a Documentation for the editor tab bar settings 2024-03-23 15:51:56 +03:00
Andrew Lygin
b8a6fc316f Support tab bar rendering when it's at the bottom 2024-03-23 15:51:56 +03:00
Andrew Lygin
2fa9220f44 Settings for the editor tab bar placement 2024-03-23 15:46:35 +03:00
90 changed files with 3578 additions and 1763 deletions

61
Cargo.lock generated
View File

@@ -382,6 +382,7 @@ dependencies = [
"editor",
"env_logger",
"feature_flags",
"fs",
"futures 0.3.28",
"gpui",
"language",
@@ -3181,13 +3182,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 +3438,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 +3829,7 @@ dependencies = [
"ctor",
"editor",
"env_logger",
"futures 0.3.28",
"fuzzy",
"gpui",
"itertools 0.11.0",
@@ -4530,6 +4550,7 @@ dependencies = [
"cosmic-text",
"ctor",
"derive_more",
"embed-resource",
"env_logger",
"etagere",
"filedescriptor",
@@ -5896,6 +5917,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"async-recursion 1.0.5",
"collections",
"editor",
"gpui",
"language",
@@ -5952,9 +5974,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 +7985,7 @@ dependencies = [
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"winreg",
"winreg 0.50.0",
]
[[package]]
@@ -9492,7 +9514,6 @@ dependencies = [
"strum",
"theme",
"ui",
"winresource",
]
[[package]]
@@ -10620,7 +10641,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 +11147,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 +12240,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"

View File

@@ -283,6 +283,7 @@ 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"
ordered-float = "2.1.1"
palette = { version = "0.7.5", default-features = false, features = ["std"] }
parking_lot = "0.12.1"
@@ -341,7 +342,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" }

102
README.md
View File

@@ -1,51 +1,51 @@
# Zed
[![CI](https://github.com/zed-industries/zed/actions/workflows/ci.yml/badge.svg)](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
[![CI](https://github.com/zed-industries/zed/actions/workflows/ci.yml/badge.svg)](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).

View File

@@ -312,6 +312,16 @@
"autosave": "off",
// Settings related to the editor's tab bar.
"tab_bar": {
// Where to show the tab bar in the editor.
// This setting can take three values:
//
// 1. Show tab bar at the top of the editor (default):
// "top"
// 2. Show tab bar at the bottom of the editor:
// "bottom"
// 3. Don't show the tab bar:
// "no"
"placement": "top",
// Whether or not to show the navigation history buttons.
"show_nav_history_buttons": true
},

View File

@@ -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,

View File

@@ -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,

View File

@@ -19,15 +19,17 @@ assistant_tooling.workspace = true
client.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 +37,6 @@ theme.workspace = true
ui.workspace = true
util.workspace = true
workspace.workspace = true
nanoid = "0.4"
[dev-dependencies]
assets.workspace = true

View File

@@ -1,4 +1,5 @@
/// 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;

View File

@@ -0,0 +1,221 @@
//! This example creates a basic Chat UI for interacting with the filesystem.
use anyhow::{Context as _, Result};
use assets::Assets;
use assistant2::AssistantPanel;
use assistant_tooling::{LanguageModelTool, ToolRegistry};
use client::Client;
use fs::Fs;
use futures::StreamExt;
use gpui::{actions, App, AppContext, KeyBinding, Task, View, WindowOptions};
use language::LanguageRegistry;
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{KeymapFile, DEFAULT_KEYMAP_PATH};
use std::path::PathBuf;
use std::sync::Arc;
use theme::LoadThemes;
use ui::{div, prelude::*, Render};
use util::ResultExt as _;
actions!(example, [Quit]);
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 new_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| {
cx.bind_keys(Some(KeyBinding::new("cmd-q", Quit, None)));
cx.on_action(|_: &Quit, cx: &mut AppContext| {
cx.quit();
});
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);
cx.spawn(|cx| async move {
cx.update(|cx| {
let fs = Arc::new(fs::RealFs::new(None));
let cwd = std::env::current_dir().expect("Failed to get current working directory");
let mut tool_registry = ToolRegistry::new();
tool_registry
.register(FileBrowserTool::new(fs, cwd))
.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.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())
}
}

View File

@@ -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"

View File

@@ -136,6 +136,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 +167,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");

View File

@@ -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"
);
}

View File

@@ -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();

View File

@@ -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,
@@ -630,6 +630,7 @@ mod tests {
let http = FakeHttpClient::with_404_response();
let client = Client::new(clock, http.clone(), cx);
let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx));
workspace::init_settings(cx);
theme::init(theme::LoadThemes::JustBase, cx);
Project::init_settings(cx);
language::init(cx);

View File

@@ -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()

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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,

View File

@@ -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);
});
}
})),
)
}
}

View File

@@ -1821,6 +1821,7 @@ pub mod tests {
cx.set_global(settings);
language::init(cx);
crate::init(cx);
workspace::init_settings(cx);
Project::init_settings(cx);
theme::init(LoadThemes::JustBase, cx);
cx.update_global::<SettingsStore, _>(|store, cx| {

View File

@@ -130,12 +130,11 @@ use ui::{
Tooltip,
};
use util::{defer, maybe, post_inc, RangeExt, ResultExt, TryFutureExt};
use workspace::item::ItemHandle;
use workspace::notifications::NotificationId;
use workspace::{
searchable::SearchEvent, ItemNavHistory, SplitDirection, ViewId, Workspace, WorkspaceId,
item::ItemHandle, notifications::NotificationId, searchable::SearchEvent, ItemNavHistory,
OpenInTerminal, OpenTerminal, SplitDirection, TabBarPlacement, TabBarSettings, Toast, ViewId,
Workspace, WorkspaceId,
};
use workspace::{OpenInTerminal, OpenTerminal, Toast};
use crate::hover_links::find_url;
@@ -419,6 +418,7 @@ pub struct Editor {
hovered_cursors: HashMap<HoveredCursor, Task<()>>,
pub show_local_selections: bool,
mode: EditorMode,
tab_bar_placement: TabBarPlacement,
show_breadcrumbs: bool,
show_gutter: bool,
show_wrap_guides: Option<bool>,
@@ -1453,6 +1453,7 @@ impl Editor {
blink_manager: blink_manager.clone(),
show_local_selections: true,
mode,
tab_bar_placement: TabBarSettings::get_global(cx).placement,
show_breadcrumbs: EditorSettings::get_global(cx).toolbar.breadcrumbs,
show_gutter: mode == EditorMode::Full,
show_wrap_guides: None,
@@ -8206,9 +8207,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
});
@@ -9616,6 +9621,7 @@ impl Editor {
let editor_settings = EditorSettings::get_global(cx);
self.scroll_manager.vertical_scroll_margin = editor_settings.vertical_scroll_margin;
self.show_breadcrumbs = editor_settings.toolbar.breadcrumbs;
self.tab_bar_placement = TabBarSettings::get_global(cx).placement;
if self.mode == EditorMode::Full {
let inline_blame_enabled = ProjectSettings::get_global(cx).git.inline_blame_enabled();
@@ -10336,7 +10342,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 +10355,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 +10780,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');

View File

@@ -89,7 +89,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() {

View File

@@ -19,14 +19,17 @@ use language::{
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 workspace::{
item::{TabContentParams, TabsSettings},
TabBarPlacement,
};
use std::{
borrow::Cow,
cmp::{self, Ordering},
iter,
ops::Range,
path::{Path, PathBuf},
path::Path,
sync::Arc,
};
use text::{BufferId, Selection};
@@ -596,7 +599,7 @@ impl Item for Editor {
}
fn tab_content(&self, params: TabContentParams, cx: &WindowContext) -> AnyElement {
let label_color = if ItemSettings::get_global(cx).git_status {
let label_color = if TabsSettings::get_global(cx).git_status {
self.buffer()
.read(cx)
.as_singleton()
@@ -750,7 +753,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 +762,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<()>> {
@@ -800,6 +802,10 @@ impl Item for Editor {
self.pixel_position_of_newest_cursor
}
fn tab_bar_placement(&self) -> TabBarPlacement {
self.tab_bar_placement
}
fn breadcrumb_location(&self) -> ToolbarItemLocation {
if self.show_breadcrumbs {
ToolbarItemLocation::PrimaryLeft

View File

@@ -1032,6 +1032,7 @@ mod tests {
theme::init(theme::LoadThemes::JustBase, cx);
language::init(cx);
crate::init(cx);
workspace::init_settings(cx);
Project::init_settings(cx);
}
}

View File

@@ -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,

View File

@@ -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"

View File

@@ -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<_>>();

View 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()),
})
}
}
}
}

View File

@@ -714,6 +714,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);

View File

@@ -120,6 +120,9 @@ 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"

View File

@@ -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")]

View File

@@ -0,0 +1,2 @@
#define RT_MANIFEST 24
1 RT_MANIFEST "resources/windows/gpui.manifest.xml"

View File

@@ -693,7 +693,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,6 +703,10 @@ 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)

View File

@@ -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" {

View File

@@ -843,7 +843,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_keysym == Some(keysym) {
state.repeat.current_keysym = None;
}
drop(state);
focused_window.handle_input(input);

View File

@@ -322,7 +322,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
}

View File

@@ -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};
@@ -23,10 +23,10 @@ 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,
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 +36,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
@@ -68,6 +68,24 @@ pub struct X11ClientState {
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>>);
@@ -171,7 +189,7 @@ impl X11Client {
})))
}
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 +200,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()
}
}
}
@@ -424,6 +440,8 @@ 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,
@@ -492,7 +510,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,
};

View File

@@ -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;
@@ -77,6 +77,8 @@ pub struct Callbacks {
}
pub(crate) struct X11WindowState {
client: X11ClientStatePtr,
executor: ForegroundExecutor,
atoms: XcbAtoms,
raw: RawWindow,
bounds: Bounds<i32>,
@@ -88,7 +90,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>,
@@ -124,6 +126,8 @@ impl rwh::HasDisplayHandle for X11Window {
impl X11WindowState {
pub fn new(
client: X11ClientStatePtr,
executor: ForegroundExecutor,
params: WindowParams,
xcb_connection: &Rc<XCBConnection>,
x_main_screen_index: usize,
@@ -224,6 +228,8 @@ 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),
@@ -244,16 +250,47 @@ 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 {
pub fn new(
client: X11ClientStatePtr,
executor: ForegroundExecutor,
params: WindowParams,
xcb_connection: &Rc<XCBConnection>,
x_main_screen_index: usize,
x_window: xproto::Window,
atoms: &XcbAtoms,
) -> Self {
X11Window {
Self(X11WindowStatePtr {
state: Rc::new(RefCell::new(X11WindowState::new(
client,
executor,
params,
xcb_connection,
x_main_screen_index,
@@ -263,20 +300,27 @@ impl X11Window {
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 +389,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_mut().bounds.map(|v| v.into())
}
// todo(linux)
@@ -359,11 +403,11 @@ impl PlatformWindow for X11Window {
}
fn content_size(&self) -> Size<Pixels> {
self.state.borrow_mut().content_size()
self.0.state.borrow_mut().content_size()
}
fn scale_factor(&self) -> f32 {
self.state.borrow_mut().scale_factor
self.0.state.borrow_mut().scale_factor
}
// todo(linux)
@@ -372,13 +416,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 +440,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 +459,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,22 +471,24 @@ 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();
@@ -484,39 +532,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 +573,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()
}
}

View File

@@ -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(

View File

@@ -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 {

View File

@@ -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,17 @@ 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];
let _: () = msg_send![button, setHasDestructiveAction: YES];
if level == PromptLevel::Destructive {
let _: () = msg_send![button, setHasDestructiveAction: YES];
}
}
let (done_tx, done_rx) = oneshot::channel();

View File

@@ -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;
}
@@ -1767,6 +1769,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 +1910,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

View File

@@ -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) {

View File

@@ -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()
}
}

View File

@@ -108,6 +108,7 @@ fn init_test(cx: &mut gpui::TestAppContext) {
release_channel::init("0.0.0", cx);
language::init(cx);
client::init_settings(cx);
workspace::init_settings(cx);
Project::init_settings(cx);
editor::init_settings(cx);
});

View File

@@ -35,7 +35,7 @@ gpui = { workspace = true, optional = true }
live_kit_server = { workspace = true, optional = true }
log.workspace = true
media.workspace = true
nanoid = { version = "0.4", optional = true}
nanoid = { workspace = true, optional = true}
parking_lot.workspace = true
postage.workspace = true
@@ -47,14 +47,14 @@ async-trait = { workspace = true }
collections = { workspace = true }
gpui = { workspace = true }
live_kit_server.workspace = true
nanoid = "0.4"
nanoid.workspace = true
[dev-dependencies]
async-trait.workspace = true
collections = { workspace = true, features = ["test-support"] }
gpui = { workspace = true, features = ["test-support"] }
live_kit_server.workspace = true
nanoid = "0.4"
nanoid.workspace = true
sha2.workspace = true
simplelog = "0.9"

View File

@@ -17,6 +17,7 @@ test-support = []
[dependencies]
anyhow.workspace = true
async-recursion.workspace = true
collections.workspace = true
editor.workspace = true
gpui.workspace = true
language.workspace = true

View File

@@ -8,8 +8,7 @@ use std::{fmt::Display, ops::Range, path::PathBuf};
#[cfg_attr(test, derive(PartialEq))]
pub enum ParsedMarkdownElement {
Heading(ParsedMarkdownHeading),
/// An ordered or unordered list of items.
List(ParsedMarkdownList),
ListItem(ParsedMarkdownListItem),
Table(ParsedMarkdownTable),
BlockQuote(ParsedMarkdownBlockQuote),
CodeBlock(ParsedMarkdownCodeBlock),
@@ -22,7 +21,7 @@ impl ParsedMarkdownElement {
pub fn source_range(&self) -> Range<usize> {
match self {
Self::Heading(heading) => heading.source_range.clone(),
Self::List(list) => list.source_range.clone(),
Self::ListItem(list_item) => list_item.source_range.clone(),
Self::Table(table) => table.source_range.clone(),
Self::BlockQuote(block_quote) => block_quote.source_range.clone(),
Self::CodeBlock(code_block) => code_block.source_range.clone(),
@@ -30,6 +29,10 @@ impl ParsedMarkdownElement {
Self::HorizontalRule(range) => range.clone(),
}
}
pub fn is_list_item(&self) -> bool {
matches!(self, Self::ListItem(_))
}
}
#[derive(Debug)]
@@ -38,20 +41,14 @@ pub struct ParsedMarkdown {
pub children: Vec<ParsedMarkdownElement>,
}
#[derive(Debug)]
#[cfg_attr(test, derive(PartialEq))]
pub struct ParsedMarkdownList {
pub source_range: Range<usize>,
pub children: Vec<ParsedMarkdownListItem>,
}
#[derive(Debug)]
#[cfg_attr(test, derive(PartialEq))]
pub struct ParsedMarkdownListItem {
pub source_range: Range<usize>,
/// How many indentations deep this item is.
pub depth: u16,
pub item_type: ParsedMarkdownListItemType,
pub contents: Vec<Box<ParsedMarkdownElement>>,
pub content: Vec<ParsedMarkdownElement>,
}
#[derive(Debug)]
@@ -129,7 +126,7 @@ impl ParsedMarkdownTableRow {
#[cfg_attr(test, derive(PartialEq))]
pub struct ParsedMarkdownBlockQuote {
pub source_range: Range<usize>,
pub children: Vec<Box<ParsedMarkdownElement>>,
pub children: Vec<ParsedMarkdownElement>,
}
#[derive(Debug)]

View File

@@ -1,5 +1,6 @@
use crate::markdown_elements::*;
use async_recursion::async_recursion;
use collections::FxHashMap;
use gpui::FontWeight;
use language::LanguageRegistry;
use pulldown_cmark::{Alignment, Event, Options, Parser, Tag, TagEnd};
@@ -98,20 +99,22 @@ impl<'a> MarkdownParser<'a> {
async fn parse_document(mut self) -> Self {
while !self.eof() {
if let Some(block) = self.parse_block().await {
self.parsed.push(block);
self.parsed.extend(block);
}
}
self
}
async fn parse_block(&mut self) -> Option<ParsedMarkdownElement> {
#[async_recursion]
async fn parse_block(&mut self) -> Option<Vec<ParsedMarkdownElement>> {
let (current, source_range) = self.current().unwrap();
let source_range = source_range.clone();
match current {
Event::Start(tag) => match tag {
Tag::Paragraph => {
self.cursor += 1;
let text = self.parse_text(false);
Some(ParsedMarkdownElement::Paragraph(text))
let text = self.parse_text(false, Some(source_range));
Some(vec![ParsedMarkdownElement::Paragraph(text)])
}
Tag::Heading {
level,
@@ -122,24 +125,24 @@ impl<'a> MarkdownParser<'a> {
let level = *level;
self.cursor += 1;
let heading = self.parse_heading(level);
Some(ParsedMarkdownElement::Heading(heading))
Some(vec![ParsedMarkdownElement::Heading(heading)])
}
Tag::Table(alignment) => {
let alignment = alignment.clone();
self.cursor += 1;
let table = self.parse_table(alignment);
Some(ParsedMarkdownElement::Table(table))
Some(vec![ParsedMarkdownElement::Table(table)])
}
Tag::List(order) => {
let order = *order;
self.cursor += 1;
let list = self.parse_list(1, order).await;
Some(ParsedMarkdownElement::List(list))
let list = self.parse_list(order).await;
Some(list)
}
Tag::BlockQuote => {
self.cursor += 1;
let block_quote = self.parse_block_quote().await;
Some(ParsedMarkdownElement::BlockQuote(block_quote))
Some(vec![ParsedMarkdownElement::BlockQuote(block_quote)])
}
Tag::CodeBlock(kind) => {
let language = match kind {
@@ -156,7 +159,7 @@ impl<'a> MarkdownParser<'a> {
self.cursor += 1;
let code_block = self.parse_code_block(language).await;
Some(ParsedMarkdownElement::CodeBlock(code_block))
Some(vec![ParsedMarkdownElement::CodeBlock(code_block)])
}
_ => {
self.cursor += 1;
@@ -166,7 +169,7 @@ impl<'a> MarkdownParser<'a> {
Event::Rule => {
let source_range = source_range.clone();
self.cursor += 1;
Some(ParsedMarkdownElement::HorizontalRule(source_range))
Some(vec![ParsedMarkdownElement::HorizontalRule(source_range)])
}
_ => {
self.cursor += 1;
@@ -175,9 +178,16 @@ impl<'a> MarkdownParser<'a> {
}
}
fn parse_text(&mut self, should_complete_on_soft_break: bool) -> ParsedMarkdownText {
let (_current, source_range) = self.previous().unwrap();
let source_range = source_range.clone();
fn parse_text(
&mut self,
should_complete_on_soft_break: bool,
source_range: Option<Range<usize>>,
) -> ParsedMarkdownText {
let source_range = source_range.unwrap_or_else(|| {
self.current()
.map(|(_, range)| range.clone())
.unwrap_or_default()
});
let mut text = String::new();
let mut bold_depth = 0;
@@ -379,7 +389,7 @@ impl<'a> MarkdownParser<'a> {
fn parse_heading(&mut self, level: pulldown_cmark::HeadingLevel) -> ParsedMarkdownHeading {
let (_event, source_range) = self.previous().unwrap();
let source_range = source_range.clone();
let text = self.parse_text(true);
let text = self.parse_text(true, None);
// Advance past the heading end tag
self.cursor += 1;
@@ -415,7 +425,8 @@ impl<'a> MarkdownParser<'a> {
break;
}
let (current, _source_range) = self.current().unwrap();
let (current, source_range) = self.current().unwrap();
let source_range = source_range.clone();
match current {
Event::Start(Tag::TableHead)
| Event::Start(Tag::TableRow)
@@ -424,7 +435,7 @@ impl<'a> MarkdownParser<'a> {
}
Event::Start(Tag::TableCell) => {
self.cursor += 1;
let cell_contents = self.parse_text(false);
let cell_contents = self.parse_text(false, Some(source_range));
current_row.push(cell_contents);
}
Event::End(TagEnd::TableHead) | Event::End(TagEnd::TableRow) => {
@@ -465,35 +476,53 @@ impl<'a> MarkdownParser<'a> {
}
}
#[async_recursion]
async fn parse_list(&mut self, depth: u16, order: Option<u64>) -> ParsedMarkdownList {
let (_event, source_range) = self.previous().unwrap();
let source_range = source_range.clone();
let mut children = vec![];
let mut inside_list_item = false;
let mut order = order;
let mut task_item = None;
async fn parse_list(&mut self, order: Option<u64>) -> Vec<ParsedMarkdownElement> {
let (_, list_source_range) = self.previous().unwrap();
let mut current_list_items: Vec<Box<ParsedMarkdownElement>> = vec![];
let mut items = Vec::new();
let mut items_stack = vec![Vec::new()];
let mut depth = 1;
let mut task_item = None;
let mut order = order;
let mut order_stack = Vec::new();
let mut insertion_indices = FxHashMap::default();
let mut source_ranges = FxHashMap::default();
let mut start_item_range = list_source_range.clone();
while !self.eof() {
let (current, _source_range) = self.current().unwrap();
let (current, source_range) = self.current().unwrap();
match current {
Event::Start(Tag::List(order)) => {
let order = *order;
self.cursor += 1;
Event::Start(Tag::List(new_order)) => {
if items_stack.last().is_some() && !insertion_indices.contains_key(&depth) {
insertion_indices.insert(depth, items.len());
}
let inner_list = self.parse_list(depth + 1, order).await;
let block = ParsedMarkdownElement::List(inner_list);
current_list_items.push(Box::new(block));
// We will use the start of the nested list as the end for the current item's range,
// because we don't care about the hierarchy of list items
if !source_ranges.contains_key(&depth) {
source_ranges.insert(depth, start_item_range.start..source_range.start);
}
order_stack.push(order);
order = *new_order;
self.cursor += 1;
depth += 1;
}
Event::End(TagEnd::List(_)) => {
order = order_stack.pop().flatten();
self.cursor += 1;
break;
depth -= 1;
if depth == 0 {
break;
}
}
Event::Start(Tag::Item) => {
start_item_range = source_range.clone();
self.cursor += 1;
inside_list_item = true;
items_stack.push(Vec::new());
// Check for task list marker (`- [ ]` or `- [x]`)
if let Some(event) = self.current_event() {
@@ -508,17 +537,21 @@ impl<'a> MarkdownParser<'a> {
}
}
if let Some(event) = self.current_event() {
if let Some((event, range)) = self.current() {
// This is a plain list item.
// For example `- some text` or `1. [Docs](./docs.md)`
if MarkdownParser::is_text_like(event) {
let text = self.parse_text(false);
let text = self.parse_text(false, Some(range.clone()));
let block = ParsedMarkdownElement::Paragraph(text);
current_list_items.push(Box::new(block));
if let Some(content) = items_stack.last_mut() {
content.push(block);
}
} else {
let block = self.parse_block().await;
if let Some(block) = block {
current_list_items.push(Box::new(block));
if let Some(content) = items_stack.last_mut() {
content.extend(block);
}
}
}
}
@@ -543,34 +576,55 @@ impl<'a> MarkdownParser<'a> {
order = Some(current + 1);
}
let contents = std::mem::replace(&mut current_list_items, vec![]);
if let Some(content) = items_stack.pop() {
let source_range = source_ranges
.remove(&depth)
.unwrap_or(start_item_range.clone());
children.push(ParsedMarkdownListItem {
contents,
depth,
item_type,
});
// We need to remove the last character of the source range, because it includes the newline character
let source_range = source_range.start..source_range.end - 1;
let item = ParsedMarkdownElement::ListItem(ParsedMarkdownListItem {
source_range,
content,
depth,
item_type,
});
if let Some(index) = insertion_indices.get(&depth) {
items.insert(*index, item);
insertion_indices.remove(&depth);
} else {
items.push(item);
}
}
inside_list_item = false;
task_item = None;
}
_ => {
if !inside_list_item {
if depth == 0 {
break;
}
// This can only happen if a list item starts with more then one paragraph,
// or the list item contains blocks that should be rendered after the nested list items
let block = self.parse_block().await;
if let Some(block) = block {
current_list_items.push(Box::new(block));
if let Some(items_stack) = items_stack.last_mut() {
// If we did not insert any nested items yet (in this case insertion index is set), we can append the block to the current list item
if !insertion_indices.contains_key(&depth) {
items_stack.extend(block);
continue;
}
}
// Otherwise we need to insert the block after all the nested items
// that have been parsed so far
items.extend(block);
}
}
}
}
ParsedMarkdownList {
source_range,
children,
}
items
}
#[async_recursion]
@@ -579,13 +633,13 @@ impl<'a> MarkdownParser<'a> {
let source_range = source_range.clone();
let mut nested_depth = 1;
let mut children: Vec<Box<ParsedMarkdownElement>> = vec![];
let mut children: Vec<ParsedMarkdownElement> = vec![];
while !self.eof() {
let block = self.parse_block().await;
if let Some(block) = block {
children.push(Box::new(block));
children.extend(block);
} else {
break;
}
@@ -674,7 +728,6 @@ mod tests {
use language::{tree_sitter_rust, HighlightId, Language, LanguageConfig, LanguageMatcher};
use pretty_assertions::assert_eq;
use ParsedMarkdownElement::*;
use ParsedMarkdownListItemType::*;
async fn parse(input: &str) -> ParsedMarkdown {
@@ -688,9 +741,9 @@ mod tests {
assert_eq!(
parsed.children,
vec![
h1(text("Heading one", 0..14), 0..14),
h2(text("Heading two", 14..29), 14..29),
h3(text("Heading three", 29..46), 29..46),
h1(text("Heading one", 2..13), 0..14),
h2(text("Heading two", 17..28), 14..29),
h3(text("Heading three", 33..46), 29..46),
]
);
}
@@ -711,7 +764,7 @@ mod tests {
assert_eq!(
parsed.children,
vec![h1(text("Zed", 0..6), 0..6), p("The editor", 6..16),]
vec![h1(text("Zed", 2..5), 0..6), p("The editor", 6..16),]
);
}
@@ -881,14 +934,11 @@ Some other content
assert_eq!(
parsed.children,
vec![list(
vec![
list_item(1, Unordered, vec![p("Item 1", 0..9)]),
list_item(1, Unordered, vec![p("Item 2", 9..18)]),
list_item(1, Unordered, vec![p("Item 3", 18..27)]),
],
0..27
),]
vec![
list_item(0..8, 1, Unordered, vec![p("Item 1", 2..8)]),
list_item(9..17, 1, Unordered, vec![p("Item 2", 11..17)]),
list_item(18..26, 1, Unordered, vec![p("Item 3", 20..26)]),
],
);
}
@@ -904,13 +954,10 @@ Some other content
assert_eq!(
parsed.children,
vec![list(
vec![
list_item(1, Task(false, 2..5), vec![p("TODO", 2..5)]),
list_item(1, Task(true, 13..16), vec![p("Checked", 13..16)]),
],
0..25
),]
vec![
list_item(0..10, 1, Task(false, 2..5), vec![p("TODO", 6..10)]),
list_item(11..24, 1, Task(true, 13..16), vec![p("Checked", 17..24)]),
],
);
}
@@ -927,13 +974,10 @@ Some other content
assert_eq!(
parsed.children,
vec![list(
vec![
list_item(1, Task(false, 2..5), vec![p("Task 1", 2..5)]),
list_item(1, Task(true, 16..19), vec![p("Task 2", 16..19)]),
],
0..27
),]
vec![
list_item(0..13, 1, Task(false, 2..5), vec![p("Task 1", 6..12)]),
list_item(14..26, 1, Task(true, 16..19), vec![p("Task 2", 20..26)]),
],
);
}
@@ -965,84 +1009,21 @@ Some other content
assert_eq!(
parsed.children,
vec![
list(
vec![
list_item(1, Unordered, vec![p("Item 1", 0..9)]),
list_item(1, Unordered, vec![p("Item 2", 9..18)]),
list_item(1, Unordered, vec![p("Item 3", 18..28)]),
],
0..28
),
list(
vec![
list_item(1, Ordered(1), vec![p("Hello", 28..37)]),
list_item(
1,
Ordered(2),
vec![
p("Two", 37..56),
list(
vec![list_item(2, Ordered(1), vec![p("Three", 47..56)]),],
47..56
),
]
),
list_item(1, Ordered(3), vec![p("Four", 56..64)]),
list_item(1, Ordered(4), vec![p("Five", 64..73)]),
],
28..73
),
list(
vec![
list_item(
1,
Unordered,
vec![
p("First", 73..155),
list(
vec![
list_item(
2,
Ordered(1),
vec![
p("Hello", 83..141),
list(
vec![list_item(
3,
Ordered(1),
vec![
p("Goodbyte", 97..141),
list(
vec![
list_item(
4,
Unordered,
vec![p("Inner", 117..125)]
),
list_item(
4,
Unordered,
vec![p("Inner", 133..141)]
),
],
117..141
)
]
),],
97..141
)
]
),
list_item(2, Ordered(2), vec![p("Goodbyte", 143..155)]),
],
83..155
)
]
),
list_item(1, Unordered, vec![p("Last", 155..162)]),
],
73..162
),
list_item(0..8, 1, Unordered, vec![p("Item 1", 2..8)]),
list_item(9..17, 1, Unordered, vec![p("Item 2", 11..17)]),
list_item(18..27, 1, Unordered, vec![p("Item 3", 20..26)]),
list_item(28..36, 1, Ordered(1), vec![p("Hello", 31..36)]),
list_item(37..46, 1, Ordered(2), vec![p("Two", 40..43),]),
list_item(47..55, 2, Ordered(1), vec![p("Three", 50..55)]),
list_item(56..63, 1, Ordered(3), vec![p("Four", 59..63)]),
list_item(64..72, 1, Ordered(4), vec![p("Five", 67..71)]),
list_item(73..82, 1, Unordered, vec![p("First", 75..80)]),
list_item(83..96, 2, Ordered(1), vec![p("Hello", 86..91)]),
list_item(97..116, 3, Ordered(1), vec![p("Goodbyte", 100..108)]),
list_item(117..124, 4, Unordered, vec![p("Inner", 119..124)]),
list_item(133..140, 4, Unordered, vec![p("Inner", 135..140)]),
list_item(143..154, 2, Ordered(2), vec![p("Goodbyte", 146..154)]),
list_item(155..161, 1, Unordered, vec![p("Last", 157..161)]),
]
);
}
@@ -1053,23 +1034,49 @@ Some other content
"\
* This is a list item with two paragraphs.
This is the second paragraph in the list item.",
This is the second paragraph in the list item.
",
)
.await;
assert_eq!(
parsed.children,
vec![list(
vec![list_item(
1,
Unordered,
vec![
p("This is a list item with two paragraphs.", 4..45),
p("This is the second paragraph in the list item.", 50..96)
],
),],
vec![list_item(
0..96,
),]
1,
Unordered,
vec![
p("This is a list item with two paragraphs.", 4..44),
p("This is the second paragraph in the list item.", 50..97)
],
),],
);
}
#[gpui::test]
async fn test_nested_list_with_paragraph_inside() {
let parsed = parse(
"\
1. a
1. b
1. c
text
1. d
",
)
.await;
assert_eq!(
parsed.children,
vec![
list_item(0..7, 1, Ordered(1), vec![p("a", 3..4)],),
list_item(8..20, 2, Ordered(1), vec![p("b", 12..13),],),
list_item(21..27, 3, Ordered(1), vec![p("c", 25..26),],),
p("text", 32..37),
list_item(41..46, 2, Ordered(1), vec![p("d", 45..46),],),
],
);
}
@@ -1086,14 +1093,11 @@ Some other content
assert_eq!(
parsed.children,
vec![list(
vec![
list_item(1, Unordered, vec![p("code", 0..9)],),
list_item(1, Unordered, vec![p("bold", 9..20)]),
list_item(1, Unordered, vec![p("link", 20..50)],)
],
0..50,
),]
vec![
list_item(0..8, 1, Unordered, vec![p("code", 2..8)]),
list_item(9..19, 1, Unordered, vec![p("bold", 11..19)]),
list_item(20..49, 1, Unordered, vec![p("link", 22..49)],)
],
);
}
@@ -1127,7 +1131,7 @@ Some other content
parsed.children,
vec![block_quote(
vec![
h1(text("Heading", 2..12), 2..12),
h1(text("Heading", 4..11), 2..12),
p("More text", 14..26),
p("More text", 30..40)
],
@@ -1157,7 +1161,7 @@ More text
block_quote(
vec![
p("A", 2..4),
block_quote(vec![h1(text("B", 10..14), 10..14)], 8..14),
block_quote(vec![h1(text("B", 12..13), 10..14)], 8..14),
p("C", 18..20)
],
0..20
@@ -1279,7 +1283,7 @@ fn main() {
) -> ParsedMarkdownElement {
ParsedMarkdownElement::BlockQuote(ParsedMarkdownBlockQuote {
source_range,
children: children.into_iter().map(Box::new).collect(),
children,
})
}
@@ -1297,26 +1301,18 @@ fn main() {
})
}
fn list(
children: Vec<ParsedMarkdownListItem>,
source_range: Range<usize>,
) -> ParsedMarkdownElement {
List(ParsedMarkdownList {
source_range,
children,
})
}
fn list_item(
source_range: Range<usize>,
depth: u16,
item_type: ParsedMarkdownListItemType,
contents: Vec<ParsedMarkdownElement>,
) -> ParsedMarkdownListItem {
ParsedMarkdownListItem {
content: Vec<ParsedMarkdownElement>,
) -> ParsedMarkdownElement {
ParsedMarkdownElement::ListItem(ParsedMarkdownListItem {
source_range,
item_type,
depth,
contents: contents.into_iter().map(Box::new).collect(),
}
content,
})
}
fn table(

View File

@@ -15,6 +15,7 @@ use ui::prelude::*;
use workspace::item::{Item, ItemHandle, TabContentParams};
use workspace::{Pane, Workspace};
use crate::markdown_elements::ParsedMarkdownElement;
use crate::OpenPreviewToTheSide;
use crate::{
markdown_elements::ParsedMarkdown,
@@ -180,9 +181,14 @@ impl MarkdownPreviewView {
let block = contents.children.get(ix).unwrap();
let rendered_block = render_markdown_block(block, &mut render_cx);
let should_apply_padding = Self::should_apply_padding_between(
block,
contents.children.get(ix + 1),
);
div()
.id(ix)
.pb_3()
.when(should_apply_padding, |this| this.pb_3())
.group("markdown-block")
.on_click(cx.listener(move |this, event: &ClickEvent, cx| {
if event.down.click_count == 2 {
@@ -404,7 +410,7 @@ impl MarkdownPreviewView {
let Range { start, end } = block.source_range();
// Check if the cursor is between the last block and the current block
if last_end > cursor && cursor < start {
if last_end <= cursor && cursor < start {
block_index = Some(i.saturating_sub(1));
break;
}
@@ -423,6 +429,13 @@ impl MarkdownPreviewView {
block_index.unwrap_or_default()
}
fn should_apply_padding_between(
current_block: &ParsedMarkdownElement,
next_block: Option<&ParsedMarkdownElement>,
) -> bool {
!(current_block.is_list_item() && next_block.map(|b| b.is_list_item()).unwrap_or(false))
}
}
impl FocusableView for MarkdownPreviewView {

View File

@@ -1,7 +1,8 @@
use crate::markdown_elements::{
HeadingLevel, Link, ParsedMarkdown, ParsedMarkdownBlockQuote, ParsedMarkdownCodeBlock,
ParsedMarkdownElement, ParsedMarkdownHeading, ParsedMarkdownList, ParsedMarkdownListItemType,
ParsedMarkdownTable, ParsedMarkdownTableAlignment, ParsedMarkdownTableRow, ParsedMarkdownText,
ParsedMarkdownElement, ParsedMarkdownHeading, ParsedMarkdownListItem,
ParsedMarkdownListItemType, ParsedMarkdownTable, ParsedMarkdownTableAlignment,
ParsedMarkdownTableRow, ParsedMarkdownText,
};
use gpui::{
div, px, rems, AbsoluteLength, AnyElement, DefiniteLength, Div, Element, ElementId,
@@ -110,7 +111,7 @@ pub fn render_markdown_block(block: &ParsedMarkdownElement, cx: &mut RenderConte
match block {
Paragraph(text) => render_markdown_paragraph(text, cx),
Heading(heading) => render_markdown_heading(heading, cx),
List(list) => render_markdown_list(list, cx),
ListItem(list_item) => render_markdown_list_item(list_item, cx),
Table(table) => render_markdown_table(table, cx),
BlockQuote(block_quote) => render_markdown_block_quote(block_quote, cx),
CodeBlock(code_block) => render_markdown_code_block(code_block, cx),
@@ -146,79 +147,77 @@ fn render_markdown_heading(parsed: &ParsedMarkdownHeading, cx: &mut RenderContex
.into_any()
}
fn render_markdown_list(parsed: &ParsedMarkdownList, cx: &mut RenderContext) -> AnyElement {
fn render_markdown_list_item(
parsed: &ParsedMarkdownListItem,
cx: &mut RenderContext,
) -> AnyElement {
use ParsedMarkdownListItemType::*;
let mut items = vec![];
for item in &parsed.children {
let padding = rems((item.depth - 1) as f32 * 0.25);
let padding = rems((parsed.depth - 1) as f32);
let bullet = match &item.item_type {
Ordered(order) => format!("{}.", order).into_any_element(),
Unordered => "".into_any_element(),
Task(checked, range) => div()
.id(cx.next_id(range))
.mt(px(3.))
.child(
Checkbox::new(
"checkbox",
if *checked {
Selection::Selected
} else {
Selection::Unselected
},
)
.when_some(
cx.checkbox_clicked_callback.clone(),
|this, callback| {
this.on_click({
let range = range.clone();
move |selection, cx| {
let checked = match selection {
Selection::Selected => true,
Selection::Unselected => false,
_ => return,
};
if cx.modifiers().secondary() {
callback(checked, range.clone(), cx);
}
}
})
},
),
let bullet = match &parsed.item_type {
Ordered(order) => format!("{}.", order).into_any_element(),
Unordered => "".into_any_element(),
Task(checked, range) => div()
.id(cx.next_id(range))
.mt(px(3.))
.child(
Checkbox::new(
"checkbox",
if *checked {
Selection::Selected
} else {
Selection::Unselected
},
)
.hover(|s| s.cursor_pointer())
.tooltip(|cx| {
let secondary_modifier = Keystroke {
key: "".to_string(),
modifiers: Modifiers::secondary_key(),
ime_key: None,
};
Tooltip::text(
format!("{}-click to toggle the checkbox", secondary_modifier),
cx,
)
})
.into_any_element(),
};
let bullet = div().mr_2().child(bullet);
.when_some(
cx.checkbox_clicked_callback.clone(),
|this, callback| {
this.on_click({
let range = range.clone();
move |selection, cx| {
let checked = match selection {
Selection::Selected => true,
Selection::Unselected => false,
_ => return,
};
let contents: Vec<AnyElement> = item
.contents
.iter()
.map(|c| render_markdown_block(c.as_ref(), cx))
.collect();
if cx.modifiers().secondary() {
callback(checked, range.clone(), cx);
}
}
})
},
),
)
.hover(|s| s.cursor_pointer())
.tooltip(|cx| {
let secondary_modifier = Keystroke {
key: "".to_string(),
modifiers: Modifiers::secondary_key(),
ime_key: None,
};
Tooltip::text(
format!("{}-click to toggle the checkbox", secondary_modifier),
cx,
)
})
.into_any_element(),
};
let bullet = div().mr_2().child(bullet);
let item = h_flex()
.pl(DefiniteLength::Absolute(AbsoluteLength::Rems(padding)))
.items_start()
.children(vec![bullet, div().children(contents).pr_4().w_full()]);
let contents: Vec<AnyElement> = parsed
.content
.iter()
.map(|c| render_markdown_block(c, cx))
.collect();
items.push(item);
}
let item = h_flex()
.pl(DefiniteLength::Absolute(AbsoluteLength::Rems(padding)))
.items_start()
.children(vec![bullet, div().children(contents).pr_4().w_full()]);
cx.with_common_p(div()).children(items).into_any()
cx.with_common_p(item).into_any()
}
fn render_markdown_table(parsed: &ParsedMarkdownTable, cx: &mut RenderContext) -> AnyElement {

View File

@@ -274,7 +274,7 @@ impl PickerDelegate for OutlineViewDelegate {
let text_style = TextStyle {
color: cx.theme().colors().text,
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,

View File

@@ -79,11 +79,18 @@ pub trait PickerDelegate: Sized + 'static {
false
}
fn confirm_update_query(&mut self, _cx: &mut ViewContext<Picker<Self>>) -> Option<String> {
None
}
fn confirm(&mut self, secondary: bool, cx: &mut ViewContext<Picker<Self>>);
/// Instead of interacting with currently selected entry, treats editor input literally,
/// performing some kind of action on it.
fn confirm_input(&mut self, _secondary: bool, _: &mut ViewContext<Picker<Self>>) {}
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>);
fn should_dismiss(&self) -> bool {
true
}
fn selected_as_query(&self) -> Option<String> {
None
}
@@ -267,8 +274,10 @@ impl<D: PickerDelegate> Picker<D> {
}
pub fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
self.delegate.dismissed(cx);
cx.emit(DismissEvent);
if self.delegate.should_dismiss() {
self.delegate.dismissed(cx);
cx.emit(DismissEvent);
}
}
fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
@@ -280,7 +289,7 @@ impl<D: PickerDelegate> Picker<D> {
self.confirm_on_update = Some(false)
} else {
self.pending_update_matches.take();
self.delegate.confirm(false, cx);
self.do_confirm(false, cx);
}
}
@@ -292,7 +301,7 @@ impl<D: PickerDelegate> Picker<D> {
{
self.confirm_on_update = Some(true)
} else {
self.delegate.confirm(true, cx);
self.do_confirm(true, cx);
}
}
@@ -311,7 +320,16 @@ impl<D: PickerDelegate> Picker<D> {
cx.stop_propagation();
cx.prevent_default();
self.delegate.set_selected_index(ix, cx);
self.delegate.confirm(secondary, cx);
self.do_confirm(secondary, cx)
}
fn do_confirm(&mut self, secondary: bool, cx: &mut ViewContext<Self>) {
if let Some(update_query) = self.delegate.confirm_update_query(cx) {
self.set_query(update_query, cx);
self.delegate.set_selected_index(0, cx);
} else {
self.delegate.confirm(secondary, cx)
}
}
fn on_input_editor_event(
@@ -385,7 +403,7 @@ impl<D: PickerDelegate> Picker<D> {
self.scroll_to_item_index(index);
self.pending_update_matches = None;
if let Some(secondary) = self.confirm_on_update.take() {
self.delegate.confirm(secondary, cx);
self.do_confirm(secondary, cx);
}
cx.notify();
}

View File

@@ -32,6 +32,7 @@ use futures::{
stream::FuturesUnordered,
AsyncWriteExt, Future, FutureExt, StreamExt, TryFutureExt,
};
use fuzzy::CharBag;
use git::{blame::Blame, repository::GitRepository};
use globset::{Glob, GlobSet, GlobSetBuilder};
use gpui::{
@@ -370,6 +371,22 @@ pub struct ProjectPath {
pub path: Arc<Path>,
}
impl ProjectPath {
pub fn from_proto(p: proto::ProjectPath) -> Self {
Self {
worktree_id: WorktreeId::from_proto(p.worktree_id),
path: Arc::from(PathBuf::from(p.path)),
}
}
pub fn to_proto(&self) -> proto::ProjectPath {
proto::ProjectPath {
worktree_id: self.worktree_id.to_proto(),
path: self.path.to_string_lossy().to_string(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InlayHint {
pub position: language::Anchor,
@@ -2189,33 +2206,37 @@ impl Project {
let path = file.path.clone();
worktree.update(cx, |worktree, cx| match worktree {
Worktree::Local(worktree) => worktree.save_buffer(buffer, path, false, cx),
Worktree::Remote(worktree) => worktree.save_buffer(buffer, cx),
Worktree::Remote(worktree) => worktree.save_buffer(buffer, None, cx),
})
}
pub fn save_buffer_as(
&mut self,
buffer: Model<Buffer>,
abs_path: PathBuf,
path: ProjectPath,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
let worktree_task = self.find_or_create_local_worktree(&abs_path, true, cx);
let old_file = File::from_dyn(buffer.read(cx).file())
.filter(|f| f.is_local())
.cloned();
let Some(worktree) = self.worktree_for_id(path.worktree_id, cx) else {
return Task::ready(Err(anyhow!("worktree does not exist")));
};
cx.spawn(move |this, mut cx| async move {
if let Some(old_file) = &old_file {
this.update(&mut cx, |this, cx| {
this.unregister_buffer_from_language_servers(&buffer, old_file, cx);
})?;
}
let (worktree, path) = worktree_task.await?;
worktree
.update(&mut cx, |worktree, cx| match worktree {
Worktree::Local(worktree) => {
worktree.save_buffer(buffer.clone(), path.into(), true, cx)
worktree.save_buffer(buffer.clone(), path.path, true, cx)
}
Worktree::Remote(worktree) => {
worktree.save_buffer(buffer.clone(), Some(path.to_proto()), cx)
}
Worktree::Remote(_) => panic!("cannot remote buffers as new files"),
})?
.await?;
@@ -2699,7 +2720,6 @@ impl Project {
for (_, _, server) in self.language_servers_for_worktree(worktree_id) {
let text = include_text(server.as_ref()).then(|| buffer.read(cx).text());
server
.notify::<lsp::notification::DidSaveTextDocument>(
lsp::DidSaveTextDocumentParams {
@@ -2710,46 +2730,8 @@ impl Project {
.log_err();
}
let language_server_ids = self.language_server_ids_for_buffer(buffer.read(cx), cx);
for language_server_id in language_server_ids {
if let Some(LanguageServerState::Running {
adapter,
simulate_disk_based_diagnostics_completion,
..
}) = self.language_servers.get_mut(&language_server_id)
{
// After saving a buffer using a language server that doesn't provide
// a disk-based progress token, kick off a timer that will reset every
// time the buffer is saved. If the timer eventually fires, simulate
// disk-based diagnostics being finished so that other pieces of UI
// (e.g., project diagnostics view, diagnostic status bar) can update.
// We don't emit an event right away because the language server might take
// some time to publish diagnostics.
if adapter.disk_based_diagnostics_progress_token.is_none() {
const DISK_BASED_DIAGNOSTICS_DEBOUNCE: Duration =
Duration::from_secs(1);
let task = cx.spawn(move |this, mut cx| async move {
cx.background_executor().timer(DISK_BASED_DIAGNOSTICS_DEBOUNCE).await;
if let Some(this) = this.upgrade() {
this.update(&mut cx, |this, cx| {
this.disk_based_diagnostics_finished(
language_server_id,
cx,
);
this.enqueue_buffer_ordered_message(
BufferOrderedMessage::LanguageServerUpdate {
language_server_id,
message:proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated(Default::default())
},
)
.ok();
}).ok();
}
});
*simulate_disk_based_diagnostics_completion = Some(task);
}
}
for language_server_id in self.language_server_ids_for_buffer(buffer.read(cx), cx) {
self.simulate_disk_based_diagnostics_events_if_needed(language_server_id, cx);
}
}
BufferEvent::FileHandleChanged => {
@@ -2783,6 +2765,57 @@ impl Project {
None
}
// After saving a buffer using a language server that doesn't provide a disk-based progress token,
// kick off a timer that will reset every time the buffer is saved. If the timer eventually fires,
// simulate disk-based diagnostics being finished so that other pieces of UI (e.g., project
// diagnostics view, diagnostic status bar) can update. We don't emit an event right away because
// the language server might take some time to publish diagnostics.
fn simulate_disk_based_diagnostics_events_if_needed(
&mut self,
language_server_id: LanguageServerId,
cx: &mut ModelContext<Self>,
) {
const DISK_BASED_DIAGNOSTICS_DEBOUNCE: Duration = Duration::from_secs(1);
let Some(LanguageServerState::Running {
simulate_disk_based_diagnostics_completion,
adapter,
..
}) = self.language_servers.get_mut(&language_server_id)
else {
return;
};
if adapter.disk_based_diagnostics_progress_token.is_some() {
return;
}
let prev_task = simulate_disk_based_diagnostics_completion.replace(cx.spawn(
move |this, mut cx| async move {
cx.background_executor()
.timer(DISK_BASED_DIAGNOSTICS_DEBOUNCE)
.await;
this.update(&mut cx, |this, cx| {
this.disk_based_diagnostics_finished(language_server_id, cx);
if let Some(LanguageServerState::Running {
simulate_disk_based_diagnostics_completion,
..
}) = this.language_servers.get_mut(&language_server_id)
{
*simulate_disk_based_diagnostics_completion = None;
}
})
.ok();
},
));
if prev_task.is_none() {
self.disk_based_diagnostics_started(language_server_id, cx);
}
}
fn request_buffer_diff_recalculation(
&mut self,
buffer: &Model<Buffer>,
@@ -4041,13 +4074,7 @@ impl Project {
match progress {
lsp::WorkDoneProgress::Begin(report) => {
if is_disk_based_diagnostics_progress {
language_server_status.has_pending_diagnostic_updates = true;
self.disk_based_diagnostics_started(language_server_id, cx);
self.enqueue_buffer_ordered_message(BufferOrderedMessage::LanguageServerUpdate {
language_server_id,
message: proto::update_language_server::Variant::DiskBasedDiagnosticsUpdating(Default::default())
})
.ok();
} else {
self.on_lsp_work_start(
language_server_id,
@@ -4092,18 +4119,7 @@ impl Project {
language_server_status.progress_tokens.remove(&token);
if is_disk_based_diagnostics_progress {
language_server_status.has_pending_diagnostic_updates = false;
self.disk_based_diagnostics_finished(language_server_id, cx);
self.enqueue_buffer_ordered_message(
BufferOrderedMessage::LanguageServerUpdate {
language_server_id,
message:
proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated(
Default::default(),
),
},
)
.ok();
} else {
self.on_lsp_work_end(language_server_id, token.clone(), cx);
}
@@ -7708,13 +7724,7 @@ impl Project {
pub fn diagnostic_summary(&self, include_ignored: bool, cx: &AppContext) -> DiagnosticSummary {
let mut summary = DiagnosticSummary::default();
for (_, _, path_summary) in
self.diagnostic_summaries(include_ignored, cx)
.filter(|(path, _, _)| {
let worktree = self.entry_for_path(path, cx).map(|entry| entry.is_ignored);
include_ignored || worktree == Some(false)
})
{
for (_, _, path_summary) in self.diagnostic_summaries(include_ignored, cx) {
summary.error_count += path_summary.error_count;
summary.warning_count += path_summary.warning_count;
}
@@ -7726,20 +7736,23 @@ impl Project {
include_ignored: bool,
cx: &'a AppContext,
) -> impl Iterator<Item = (ProjectPath, LanguageServerId, DiagnosticSummary)> + 'a {
self.visible_worktrees(cx)
.flat_map(move |worktree| {
let worktree = worktree.read(cx);
let worktree_id = worktree.id();
worktree
.diagnostic_summaries()
.map(move |(path, server_id, summary)| {
(ProjectPath { worktree_id, path }, server_id, summary)
})
})
.filter(move |(path, _, _)| {
let worktree = self.entry_for_path(path, cx).map(|entry| entry.is_ignored);
include_ignored || worktree == Some(false)
})
self.visible_worktrees(cx).flat_map(move |worktree| {
let worktree = worktree.read(cx);
let worktree_id = worktree.id();
worktree
.diagnostic_summaries()
.filter_map(move |(path, server_id, summary)| {
if include_ignored
|| worktree
.entry_for_path(path.as_ref())
.map_or(false, |entry| !entry.is_ignored)
{
Some((ProjectPath { worktree_id, path }, server_id, summary))
} else {
None
}
})
})
}
pub fn disk_based_diagnostics_started(
@@ -7747,7 +7760,22 @@ impl Project {
language_server_id: LanguageServerId,
cx: &mut ModelContext<Self>,
) {
if let Some(language_server_status) =
self.language_server_statuses.get_mut(&language_server_id)
{
language_server_status.has_pending_diagnostic_updates = true;
}
cx.emit(Event::DiskBasedDiagnosticsStarted { language_server_id });
if self.is_local() {
self.enqueue_buffer_ordered_message(BufferOrderedMessage::LanguageServerUpdate {
language_server_id,
message: proto::update_language_server::Variant::DiskBasedDiagnosticsUpdating(
Default::default(),
),
})
.ok();
}
}
pub fn disk_based_diagnostics_finished(
@@ -7755,7 +7783,23 @@ impl Project {
language_server_id: LanguageServerId,
cx: &mut ModelContext<Self>,
) {
if let Some(language_server_status) =
self.language_server_statuses.get_mut(&language_server_id)
{
language_server_status.has_pending_diagnostic_updates = false;
}
cx.emit(Event::DiskBasedDiagnosticsFinished { language_server_id });
if self.is_local() {
self.enqueue_buffer_ordered_message(BufferOrderedMessage::LanguageServerUpdate {
language_server_id,
message: proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated(
Default::default(),
),
})
.ok();
}
}
pub fn active_entry(&self) -> Option<ProjectEntryId> {
@@ -8653,8 +8697,17 @@ impl Project {
.await?;
let buffer_id = buffer.update(&mut cx, |buffer, _| buffer.remote_id())?;
this.update(&mut cx, |this, cx| this.save_buffer(buffer.clone(), cx))?
if let Some(new_path) = envelope.payload.new_path {
let new_path = ProjectPath::from_proto(new_path);
this.update(&mut cx, |this, cx| {
this.save_buffer_as(buffer.clone(), new_path, cx)
})?
.await?;
} else {
this.update(&mut cx, |this, cx| this.save_buffer(buffer.clone(), cx))?
.await?;
}
buffer.update(&mut cx, |buffer, _| proto::BufferSaved {
project_id,
buffer_id: buffer_id.into(),
@@ -10391,6 +10444,7 @@ pub struct PathMatchCandidateSet {
pub snapshot: Snapshot,
pub include_ignored: bool,
pub include_root_name: bool,
pub directories_only: bool,
}
impl<'a> fuzzy::PathMatchCandidateSet<'a> for PathMatchCandidateSet {
@@ -10420,7 +10474,11 @@ impl<'a> fuzzy::PathMatchCandidateSet<'a> for PathMatchCandidateSet {
fn candidates(&'a self, start: usize) -> Self::Candidates {
PathMatchCandidateSetIter {
traversal: self.snapshot.files(self.include_ignored, start),
traversal: if self.directories_only {
self.snapshot.directories(self.include_ignored, start)
} else {
self.snapshot.files(self.include_ignored, start)
},
}
}
}
@@ -10433,15 +10491,16 @@ impl<'a> Iterator for PathMatchCandidateSetIter<'a> {
type Item = fuzzy::PathMatchCandidate<'a>;
fn next(&mut self) -> Option<Self::Item> {
self.traversal.next().map(|entry| {
if let EntryKind::File(char_bag) = entry.kind {
fuzzy::PathMatchCandidate {
path: &entry.path,
char_bag,
}
} else {
unreachable!()
}
self.traversal.next().map(|entry| match entry.kind {
EntryKind::Dir => fuzzy::PathMatchCandidate {
path: &entry.path,
char_bag: CharBag::from_iter(entry.path.to_string_lossy().to_lowercase().chars()),
},
EntryKind::File(char_bag) => fuzzy::PathMatchCandidate {
path: &entry.path,
char_bag,
},
EntryKind::UnloadedDir | EntryKind::PendingDir => unreachable!(),
})
}
}

View File

@@ -2942,7 +2942,12 @@ async fn test_save_as(cx: &mut gpui::TestAppContext) {
});
project
.update(cx, |project, cx| {
project.save_buffer_as(buffer.clone(), "/dir/file1.rs".into(), cx)
let worktree_id = project.worktrees().next().unwrap().read(cx).id();
let path = ProjectPath {
worktree_id,
path: Arc::from(Path::new("file1.rs")),
};
project.save_buffer_as(buffer.clone(), path, cx)
})
.await
.unwrap();

View File

@@ -887,7 +887,7 @@ impl ProjectPanel {
let answer = (!action.skip_prompt).then(|| {
cx.prompt(
PromptLevel::Info,
PromptLevel::Destructive,
&format!("Delete {file_name:?}?"),
None,
&["Delete", "Cancel"],

View File

@@ -310,7 +310,6 @@ impl PickerDelegate for RecentProjectsDelegate {
workspace.open_workspace_for_paths(false, paths, cx)
}
}
//TODO support opening remote projects in the same window
SerializedWorkspaceLocation::Remote(remote_project) => {
let store = ::remote_projects::Store::global(cx).read(cx);
let Some(project_id) = store
@@ -338,12 +337,38 @@ impl PickerDelegate for RecentProjectsDelegate {
})
};
if let Some(app_state) = AppState::global(cx).upgrade() {
let task =
workspace::join_remote_project(project_id, app_state, cx);
cx.spawn(|_, _| async move {
task.await?;
Ok(())
})
let handle = if replace_current_window {
cx.window_handle().downcast::<Workspace>()
} else {
None
};
if let Some(handle) = handle {
cx.spawn(move |workspace, mut cx| async move {
let continue_replacing = workspace
.update(&mut cx, |workspace, cx| {
workspace.
prepare_to_close(true, cx)
})?
.await?;
if continue_replacing {
workspace
.update(&mut cx, |_workspace, cx| {
workspace::join_remote_project(project_id, app_state, Some(handle), cx)
})?
.await?;
}
Ok(())
})
}
else {
let task =
workspace::join_remote_project(project_id, app_state, None, cx);
cx.spawn(|_, _| async move {
task.await?;
Ok(())
})
}
} else {
Task::ready(Err(anyhow::anyhow!("App state not found")))
}

View File

@@ -216,7 +216,7 @@ impl RemoteProjects {
fn delete_dev_server(&mut self, id: DevServerId, cx: &mut ViewContext<Self>) {
let answer = cx.prompt(
gpui::PromptLevel::Info,
gpui::PromptLevel::Destructive,
"Are you sure?",
Some("This will delete the dev server and all of its remote projects."),
&["Delete", "Cancel"],
@@ -386,7 +386,7 @@ impl RemoteProjects {
.on_click(cx.listener(move |_, _, cx| {
if let Some(project_id) = project_id {
if let Some(app_state) = AppState::global(cx).upgrade() {
workspace::join_remote_project(project_id, app_state, cx)
workspace::join_remote_project(project_id, app_state, None, cx)
.detach_and_prompt_err("Could not join project", cx, |_, _| None)
}
} else {

View File

@@ -769,6 +769,12 @@ message SaveBuffer {
uint64 project_id = 1;
uint64 buffer_id = 2;
repeated VectorClockEntry version = 3;
optional ProjectPath new_path = 4;
}
message ProjectPath {
uint64 worktree_id = 1;
string path = 2;
}
message BufferSaved {

View File

@@ -114,7 +114,7 @@ impl BufferSearchBar {
color
},
font_family: settings.buffer_font.family.clone(),
font_features: settings.buffer_font.features,
font_features: settings.buffer_font.features.clone(),
font_size: rems(0.875).into(),
font_weight: FontWeight::NORMAL,
font_style: FontStyle::Normal,
@@ -1106,7 +1106,7 @@ mod tests {
let store = settings::SettingsStore::test(cx);
cx.set_global(store);
editor::init(cx);
workspace::init_settings(cx);
language::init(cx);
Project::init_settings(cx);
theme::init(theme::LoadThemes::JustBase, cx);

View File

@@ -19,14 +19,14 @@ use gpui::{
WeakModel, WeakView, WhiteSpace, WindowContext,
};
use menu::Confirm;
use project::{search::SearchQuery, search_history::SearchHistoryCursor, Project};
use project::{search::SearchQuery, search_history::SearchHistoryCursor, Project, ProjectPath};
use settings::Settings;
use smol::stream::StreamExt;
use std::{
any::{Any, TypeId},
mem,
ops::{Not, Range},
path::{Path, PathBuf},
path::Path,
};
use theme::ThemeSettings;
use ui::{
@@ -439,7 +439,7 @@ impl Item for ProjectSearchView {
fn save_as(
&mut self,
_: Model<Project>,
_: PathBuf,
_: ProjectPath,
_: &mut ViewContext<Self>,
) -> Task<anyhow::Result<()>> {
unreachable!("save_as should not have been called")
@@ -1307,7 +1307,7 @@ impl ProjectSearchBar {
cx.theme().colors().text
},
font_family: settings.buffer_font.family.clone(),
font_features: settings.buffer_font.features,
font_features: settings.buffer_font.features.clone(),
font_size: rems(0.875).into(),
font_weight: FontWeight::NORMAL,
font_style: FontStyle::Normal,

View File

@@ -9,8 +9,8 @@ use fs::Fs;
use futures::stream::StreamExt;
use futures_batch::ChunksTimeoutStreamExt;
use gpui::{
AppContext, AsyncAppContext, Context, EntityId, EventEmitter, Global, Model, ModelContext,
Subscription, Task, WeakModel,
AppContext, AsyncAppContext, BorrowAppContext, Context, Entity, EntityId, EventEmitter, Global,
Model, ModelContext, Subscription, Task, WeakModel,
};
use heed::types::{SerdeBincode, Str};
use language::LanguageRegistry;
@@ -68,6 +68,18 @@ impl SemanticIndex {
project: Model<Project>,
cx: &mut AppContext,
) -> Model<ProjectIndex> {
let project_weak = project.downgrade();
project.update(cx, move |_, cx| {
cx.on_release(move |_, cx| {
if cx.has_global::<SemanticIndex>() {
cx.update_global::<SemanticIndex, _>(|this, _| {
this.project_indices.remove(&project_weak);
})
}
})
.detach();
});
self.project_indices
.entry(project.downgrade())
.or_insert_with(|| {
@@ -86,7 +98,7 @@ impl SemanticIndex {
pub struct ProjectIndex {
db_connection: heed::Env,
project: Model<Project>,
project: WeakModel<Project>,
worktree_indices: HashMap<EntityId, WorktreeIndexHandle>,
language_registry: Arc<LanguageRegistry>,
fs: Arc<dyn Fs>,
@@ -116,7 +128,7 @@ impl ProjectIndex {
let fs = project.read(cx).fs().clone();
let mut this = ProjectIndex {
db_connection,
project: project.clone(),
project: project.downgrade(),
worktree_indices: HashMap::default(),
language_registry,
fs,
@@ -143,8 +155,11 @@ impl ProjectIndex {
}
fn update_worktree_indices(&mut self, cx: &mut ModelContext<Self>) {
let worktrees = self
.project
let Some(project) = self.project.upgrade() else {
return;
};
let worktrees = project
.read(cx)
.visible_worktrees(cx)
.filter_map(|worktree| {

View File

@@ -35,8 +35,5 @@ strum = { version = "0.25.0", features = ["derive"] }
theme.workspace = true
ui = { workspace = true, features = ["stories"] }
[target.'cfg(target_os = "windows")'.build-dependencies]
winresource = "0.1"
[dev-dependencies]
gpui = { workspace = true, features = ["test-support"] }

View File

@@ -9,16 +9,5 @@ fn main() {
{
println!("cargo:rustc-link-arg=/stack:{}", 8 * 1024 * 1024);
}
let manifest = std::path::Path::new("../zed/resources/windows/manifest.xml");
println!("cargo:rerun-if-changed={}", manifest.display());
let mut res = winresource::WindowsResource::new();
res.set_manifest_file(manifest.to_str().unwrap());
if let Err(e) = res.compile() {
eprintln!("{}", e);
std::process::exit(1);
}
}
}

View File

@@ -66,6 +66,8 @@ pub fn init(cx: &mut AppContext) {
cx,
);
}
} else {
toggle_modal(workspace, cx);
};
});
},
@@ -76,17 +78,19 @@ pub fn init(cx: &mut AppContext) {
fn spawn_task_or_modal(workspace: &mut Workspace, action: &Spawn, cx: &mut ViewContext<Workspace>) {
match &action.task_name {
Some(name) => spawn_task_with_name(name.clone(), cx),
None => {
let inventory = workspace.project().read(cx).task_inventory().clone();
let workspace_handle = workspace.weak_handle();
let task_context = task_context(workspace, cx);
workspace.toggle_modal(cx, |cx| {
TasksModal::new(inventory, task_context, workspace_handle, cx)
})
}
None => toggle_modal(workspace, cx),
}
}
fn toggle_modal(workspace: &mut Workspace, cx: &mut ViewContext<'_, Workspace>) {
let inventory = workspace.project().read(cx).task_inventory().clone();
let workspace_handle = workspace.weak_handle();
let task_context = task_context(workspace, cx);
workspace.toggle_modal(cx, |cx| {
TasksModal::new(inventory, task_context, workspace_handle, cx)
})
}
fn spawn_task_with_name(name: String, cx: &mut ViewContext<Workspace>) {
cx.spawn(|workspace, mut cx| async move {
let did_spawn = workspace

View File

@@ -62,6 +62,7 @@ pub(crate) struct TasksModalDelegate {
inventory: Model<Inventory>,
candidates: Option<Vec<(TaskSourceKind, ResolvedTask)>>,
last_used_candidate_index: Option<usize>,
divider_index: Option<usize>,
matches: Vec<StringMatch>,
selected_index: usize,
workspace: WeakView<Workspace>,
@@ -82,6 +83,7 @@ impl TasksModalDelegate {
candidates: None,
matches: Vec::new(),
last_used_candidate_index: None,
divider_index: None,
selected_index: 0,
prompt: String::default(),
task_context,
@@ -255,7 +257,17 @@ impl PickerDelegate for TasksModalDelegate {
.update(&mut cx, |picker, _| {
let delegate = &mut picker.delegate;
delegate.matches = matches;
if let Some(index) = delegate.last_used_candidate_index {
delegate.matches.sort_by_key(|m| m.candidate_id > index);
}
delegate.prompt = query;
delegate.divider_index = delegate.last_used_candidate_index.and_then(|index| {
let index = delegate
.matches
.partition_point(|matching_task| matching_task.candidate_id <= index);
Some(index).and_then(|index| (index != 0).then(|| index - 1))
});
if delegate.matches.is_empty() {
delegate.selected_index = 0;
@@ -352,7 +364,7 @@ impl PickerDelegate for TasksModalDelegate {
})
.map(|item| {
let item = if matches!(source_kind, TaskSourceKind::UserInput)
|| Some(ix) <= self.last_used_candidate_index
|| Some(ix) <= self.divider_index
{
let task_index = hit.candidate_id;
let delete_button = div().child(
@@ -412,7 +424,7 @@ impl PickerDelegate for TasksModalDelegate {
}
fn separators_after_indices(&self) -> Vec<usize> {
if let Some(i) = self.last_used_candidate_index {
if let Some(i) = self.divider_index {
vec![i]
} else {
Vec::new()

View File

@@ -578,7 +578,8 @@ impl Element for TerminalElement {
let font_features = terminal_settings
.font_features
.unwrap_or(settings.buffer_font.features);
.clone()
.unwrap_or(settings.buffer_font.features.clone());
let line_height = terminal_settings.line_height.value();
let font_size = terminal_settings.font_size;

View File

@@ -325,13 +325,13 @@ impl settings::Settings for ThemeSettings {
ui_font_size: defaults.ui_font_size.unwrap().into(),
ui_font: Font {
family: defaults.ui_font_family.clone().unwrap().into(),
features: defaults.ui_font_features.unwrap(),
features: defaults.ui_font_features.clone().unwrap(),
weight: Default::default(),
style: Default::default(),
},
buffer_font: Font {
family: defaults.buffer_font_family.clone().unwrap().into(),
features: defaults.buffer_font_features.unwrap(),
features: defaults.buffer_font_features.clone().unwrap(),
weight: FontWeight::default(),
style: FontStyle::default(),
},
@@ -349,14 +349,14 @@ impl settings::Settings for ThemeSettings {
if let Some(value) = value.buffer_font_family.clone() {
this.buffer_font.family = value.into();
}
if let Some(value) = value.buffer_font_features {
if let Some(value) = value.buffer_font_features.clone() {
this.buffer_font.features = value;
}
if let Some(value) = value.ui_font_family.clone() {
this.ui_font.family = value.into();
}
if let Some(value) = value.ui_font_features {
if let Some(value) = value.ui_font_features.clone() {
this.ui_font.features = value;
}

View File

@@ -50,38 +50,49 @@ impl LabelCommon for HighlightedLabel {
}
}
pub fn highlight_ranges(
text: &str,
indices: &Vec<usize>,
style: HighlightStyle,
) -> Vec<(Range<usize>, HighlightStyle)> {
let mut highlight_indices = indices.iter().copied().peekable();
let mut highlights: Vec<(Range<usize>, HighlightStyle)> = Vec::new();
while let Some(start_ix) = highlight_indices.next() {
let mut end_ix = start_ix;
loop {
end_ix = end_ix + text[end_ix..].chars().next().unwrap().len_utf8();
if let Some(&next_ix) = highlight_indices.peek() {
if next_ix == end_ix {
end_ix = next_ix;
highlight_indices.next();
continue;
}
}
break;
}
highlights.push((start_ix..end_ix, style));
}
highlights
}
impl RenderOnce for HighlightedLabel {
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
let highlight_color = cx.theme().colors().text_accent;
let mut highlight_indices = self.highlight_indices.iter().copied().peekable();
let mut highlights: Vec<(Range<usize>, HighlightStyle)> = Vec::new();
let highlights = highlight_ranges(
&self.label,
&self.highlight_indices,
HighlightStyle {
color: Some(highlight_color),
..Default::default()
},
);
while let Some(start_ix) = highlight_indices.next() {
let mut end_ix = start_ix;
loop {
end_ix = end_ix + self.label[end_ix..].chars().next().unwrap().len_utf8();
if let Some(&next_ix) = highlight_indices.peek() {
if next_ix == end_ix {
end_ix = next_ix;
highlight_indices.next();
continue;
}
}
break;
}
highlights.push((
start_ix..end_ix,
HighlightStyle {
color: Some(highlight_color),
..Default::default()
},
));
}
let mut text_style = cx.text_style().clone();
let mut text_style = cx.text_style();
text_style.color = self.base.color.color(cx);
self.base

View File

@@ -1,10 +1,11 @@
use std::cmp::Ordering;
use crate::TabBarPlacement;
use crate::{prelude::*, BASE_REM_SIZE_IN_PX};
use gpui::{AnyElement, IntoElement, Stateful};
use smallvec::SmallVec;
use crate::{prelude::*, BASE_REM_SIZE_IN_PX};
/// The position of a [`Tab`] within a list of tabs.
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum TabPosition {
@@ -30,6 +31,7 @@ pub enum TabCloseSide {
pub struct Tab {
div: Stateful<Div>,
selected: bool,
tab_bar_placement: TabBarPlacement,
position: TabPosition,
close_side: TabCloseSide,
start_slot: Option<AnyElement>,
@@ -45,6 +47,7 @@ impl Tab {
.id(id.clone())
.debug_selector(|| format!("TAB-{}", id)),
selected: false,
tab_bar_placement: TabBarPlacement::Top,
position: TabPosition::First,
close_side: TabCloseSide::End,
start_slot: None,
@@ -57,6 +60,11 @@ impl Tab {
const CONTENT_HEIGHT_IN_REMS: f32 = 28. / BASE_REM_SIZE_IN_PX;
pub fn tab_bar_placement(mut self, tab_bar_placement: TabBarPlacement) -> Self {
self.tab_bar_placement = tab_bar_placement;
self
}
pub fn position(mut self, position: TabPosition) -> Self {
self.position = position;
self
@@ -117,6 +125,9 @@ impl RenderOnce for Tab {
),
};
let placement_top = self.tab_bar_placement == TabBarPlacement::Top;
let placement_bottom = self.tab_bar_placement == TabBarPlacement::Bottom;
self.div
.h(rems(Self::CONTAINER_HEIGHT_IN_REMS))
.bg(tab_bg)
@@ -124,21 +135,46 @@ impl RenderOnce for Tab {
.map(|this| match self.position {
TabPosition::First => {
if self.selected {
this.pl_px().border_r().pb_px()
this.pl_px()
.border_r()
.when(placement_top, Styled::pb_px)
.when(placement_bottom, Styled::pt_px)
} else {
this.pl_px().pr_px().border_b()
this.pl_px()
.pr_px()
.when(placement_top, Styled::border_b)
.when(placement_bottom, Styled::border_t)
}
}
TabPosition::Last => {
if self.selected {
this.border_l().border_r().pb_px()
this.border_l()
.border_r()
.when(placement_top, Styled::pb_px)
.when(placement_bottom, Styled::pt_px)
} else {
this.pr_px().pl_px().border_b().border_r()
this.pr_px()
.pl_px()
.border_r()
.when(placement_top, Styled::border_b)
.when(placement_bottom, Styled::border_t)
}
}
TabPosition::Middle(Ordering::Equal) => this.border_l().border_r().pb_px(),
TabPosition::Middle(Ordering::Less) => this.border_l().pr_px().border_b(),
TabPosition::Middle(Ordering::Greater) => this.border_r().pl_px().border_b(),
TabPosition::Middle(Ordering::Equal) => this
.border_l()
.border_r()
.when(placement_top, Styled::pb_px)
.when(placement_bottom, Styled::pt_px),
TabPosition::Middle(Ordering::Less) => this
.border_l()
.pr_px()
.when(placement_top, Styled::border_b)
.when(placement_bottom, Styled::border_t),
TabPosition::Middle(Ordering::Greater) => this
.border_r()
.pl_px()
.when(placement_top, Styled::border_b)
.when(placement_bottom, Styled::border_t),
})
.cursor_pointer()
.child(

View File

@@ -3,9 +3,19 @@ use smallvec::SmallVec;
use crate::prelude::*;
/// Placement of the tab bar in relation to the main content area.
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum TabBarPlacement {
/// On top.
Top,
/// At the bottom.
Bottom,
}
#[derive(IntoElement)]
pub struct TabBar {
id: ElementId,
placement: TabBarPlacement,
start_children: SmallVec<[AnyElement; 2]>,
children: SmallVec<[AnyElement; 2]>,
end_children: SmallVec<[AnyElement; 2]>,
@@ -16,6 +26,7 @@ impl TabBar {
pub fn new(id: impl Into<ElementId>) -> Self {
Self {
id: id.into(),
placement: TabBarPlacement::Top,
start_children: SmallVec::new(),
children: SmallVec::new(),
end_children: SmallVec::new(),
@@ -23,6 +34,11 @@ impl TabBar {
}
}
pub fn placement(mut self, placement: TabBarPlacement) -> Self {
self.placement = placement;
self
}
pub fn track_scroll(mut self, scroll_handle: ScrollHandle) -> Self {
self.scroll_handle = Some(scroll_handle);
self
@@ -90,6 +106,9 @@ impl ParentElement for TabBar {
impl RenderOnce for TabBar {
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
let placement_top = self.placement == TabBarPlacement::Top;
let placement_bottom = self.placement == TabBarPlacement::Bottom;
div()
.id(self.id)
.group("tab_bar")
@@ -104,7 +123,8 @@ impl RenderOnce for TabBar {
.flex_none()
.gap_1()
.px_1()
.border_b()
.when(placement_top, Styled::border_b)
.when(placement_bottom, Styled::border_t)
.border_r()
.border_color(cx.theme().colors().border)
.children(self.start_children),
@@ -122,7 +142,8 @@ impl RenderOnce for TabBar {
.top_0()
.left_0()
.size_full()
.border_b()
.when(placement_top, Styled::border_b)
.when(placement_bottom, Styled::border_t)
.border_color(cx.theme().colors().border),
)
.child(
@@ -142,7 +163,8 @@ impl RenderOnce for TabBar {
.flex_none()
.gap_1()
.px_1()
.border_b()
.when(placement_top, Styled::border_b)
.when(placement_bottom, Styled::border_t)
.border_l()
.border_color(cx.theme().colors().border)
.children(self.end_children),

View File

@@ -123,7 +123,7 @@ impl Render for TextField {
let text_style = TextStyle {
font_family: settings.buffer_font.family.clone(),
font_features: settings.buffer_font.features,
font_features: settings.buffer_font.features.clone(),
font_size: rems(0.875).into(),
font_weight: FontWeight::NORMAL,
font_style: FontStyle::Normal,

View File

@@ -55,7 +55,9 @@ fn blurred(editor: View<Editor>, cx: &mut WindowContext) {
}
}
editor.update(cx, |editor, cx| {
editor.set_cursor_shape(language::CursorShape::Hollow, cx);
if editor.use_modal_editing() {
editor.set_cursor_shape(language::CursorShape::Hollow, cx);
}
});
});
}

View File

@@ -31,48 +31,42 @@ pub fn change_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &m
editor.set_clip_at_line_ends(false, cx);
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_with(|map, selection| {
motion_succeeded |= if let Motion::NextWordStart { ignore_punctuation } = motion
{
expand_changed_word_selection(
map,
selection,
times,
ignore_punctuation,
&text_layout_details,
false,
)
} else if let Motion::NextSubwordStart { ignore_punctuation } = motion {
expand_changed_word_selection(
map,
selection,
times,
ignore_punctuation,
&text_layout_details,
true,
)
} else {
let result = motion.expand_selection(
map,
selection,
times,
false,
&text_layout_details,
);
if let Motion::CurrentLine = motion {
let mut start_offset = selection.start.to_offset(map, Bias::Left);
let scope = map
.buffer_snapshot
.language_scope_at(selection.start.to_point(&map));
for (ch, offset) in map.buffer_chars_at(start_offset) {
if ch == '\n' || char_kind(&scope, ch) != CharKind::Whitespace {
break;
}
start_offset = offset + ch.len_utf8();
}
selection.start = start_offset.to_display_point(map);
motion_succeeded |= match motion {
Motion::NextWordStart { ignore_punctuation }
| Motion::NextSubwordStart { ignore_punctuation } => {
expand_changed_word_selection(
map,
selection,
times,
ignore_punctuation,
&text_layout_details,
motion == Motion::NextSubwordStart { ignore_punctuation },
)
}
result
};
_ => {
let result = motion.expand_selection(
map,
selection,
times,
false,
&text_layout_details,
);
if let Motion::CurrentLine = motion {
let mut start_offset = selection.start.to_offset(map, Bias::Left);
let scope = map
.buffer_snapshot
.language_scope_at(selection.start.to_point(&map));
for (ch, offset) in map.buffer_chars_at(start_offset) {
if ch == '\n' || char_kind(&scope, ch) != CharKind::Whitespace {
break;
}
start_offset = offset + ch.len_utf8();
}
selection.start = start_offset.to_display_point(map);
}
result
}
}
});
});
copy_selections_content(vim, editor, motion.linewise(), cx);
@@ -116,8 +110,8 @@ pub fn change_object(vim: &mut Vim, object: Object, around: bool, cx: &mut Windo
// Special case: "cw" and "cW" are treated like "ce" and "cE" if the cursor is
// on a non-blank. This is because "cw" is interpreted as change-word, and a
// word does not include the following white space. {Vi: "cw" when on a blank
// followed by other blanks changes only the first blank; this is probably a
// bug, because "dw" deletes all the blanks}
// followed by other blanks changes only the first blank; this is probably a
// bug, because "dw" deletes all the blanks}
fn expand_changed_word_selection(
map: &DisplaySnapshot,
selection: &mut Selection<DisplayPoint>,
@@ -126,7 +120,7 @@ fn expand_changed_word_selection(
text_layout_details: &TextLayoutDetails,
use_subword: bool,
) -> bool {
if times.is_none() || times.unwrap() == 1 {
let is_in_word = || {
let scope = map
.buffer_snapshot
.language_scope_at(selection.start.to_point(map));
@@ -135,25 +129,28 @@ fn expand_changed_word_selection(
.next()
.map(|(c, _)| char_kind(&scope, c) != CharKind::Whitespace)
.unwrap_or_default();
if in_word {
if use_subword {
selection.end =
motion::next_subword_end(map, selection.end, ignore_punctuation, 1, false);
} else {
selection.end =
motion::next_word_end(map, selection.end, ignore_punctuation, 1, false);
return in_word;
};
if (times.is_none() || times.unwrap() == 1) && is_in_word() {
let next_char = map
.buffer_chars_at(
motion::next_char(map, selection.end, false).to_offset(map, Bias::Left),
)
.next();
match next_char {
Some((' ', _)) => selection.end = motion::next_char(map, selection.end, false),
_ => {
if use_subword {
selection.end =
motion::next_subword_end(map, selection.end, ignore_punctuation, 1, false);
} else {
selection.end =
motion::next_word_end(map, selection.end, ignore_punctuation, 1, false);
}
selection.end = motion::next_char(map, selection.end, false);
}
selection.end = motion::next_char(map, selection.end, false);
true
} else {
let motion = if use_subword {
Motion::NextSubwordStart { ignore_punctuation }
} else {
Motion::NextWordStart { ignore_punctuation }
};
motion.expand_selection(map, selection, None, false, &text_layout_details)
}
true
} else {
let motion = if use_subword {
Motion::NextSubwordStart { ignore_punctuation }
@@ -209,6 +206,7 @@ mod test {
cx.assert("Teˇst").await;
cx.assert("Tˇest test").await;
cx.assert("Testˇ test").await;
cx.assert("Tesˇt test").await;
cx.assert(indoc! {"
Test teˇst
test"})

View File

@@ -117,13 +117,16 @@ fn find_number(
) -> Option<(Range<Point>, String, u32)> {
let mut offset = start.to_offset(snapshot);
// go backwards to the start of any number the selection is within
for ch in snapshot.reversed_chars_at(offset) {
if ch.is_ascii_digit() || ch == '-' || ch == 'b' || ch == 'x' {
offset -= ch.len_utf8();
continue;
let ch0 = snapshot.chars_at(offset).next();
if ch0.as_ref().is_some_and(char::is_ascii_digit) || matches!(ch0, Some('-' | 'b' | 'x')) {
// go backwards to the start of any number the selection is within
for ch in snapshot.reversed_chars_at(offset) {
if ch.is_ascii_digit() || ch == '-' || ch == 'b' || ch == 'x' {
offset -= ch.len_utf8();
continue;
}
break;
}
break;
}
let mut begin = None;
@@ -217,6 +220,48 @@ mod test {
.await;
}
#[gpui::test]
async fn test_increment_with_dot(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;
cx.set_shared_state(indoc! {"
1ˇ.2
"})
.await;
cx.simulate_shared_keystrokes(["ctrl-a"]).await;
cx.assert_shared_state(indoc! {"
1.ˇ3
"})
.await;
cx.simulate_shared_keystrokes(["ctrl-x"]).await;
cx.assert_shared_state(indoc! {"
1.ˇ2
"})
.await;
}
#[gpui::test]
async fn test_increment_with_two_dots(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;
cx.set_shared_state(indoc! {"
111.ˇ.2
"})
.await;
cx.simulate_shared_keystrokes(["ctrl-a"]).await;
cx.assert_shared_state(indoc! {"
111..ˇ3
"})
.await;
cx.simulate_shared_keystrokes(["ctrl-x"]).await;
cx.assert_shared_state(indoc! {"
111..ˇ2
"})
.await;
}
#[gpui::test]
async fn test_increment_radix(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;

View File

@@ -159,11 +159,21 @@ fn search_submit(workspace: &mut Workspace, _: &SearchSubmit, cx: &mut ViewConte
search_bar.select_match(direction, count, cx);
search_bar.focus_editor(&Default::default(), cx);
let prior_selections = state.prior_selections.drain(..).collect();
let mut prior_selections: Vec<_> = state.prior_selections.drain(..).collect();
let prior_mode = state.prior_mode;
let prior_operator = state.prior_operator.take();
let new_selections = vim.editor_selections(cx);
// If the active editor has changed during a search, don't panic.
if prior_selections.iter().any(|s| {
vim.update_active_editor(cx, |_vim, editor, cx| {
!s.start.is_valid(&editor.snapshot(cx).buffer_snapshot)
})
.unwrap_or(true)
}) {
prior_selections.clear();
}
if prior_mode != vim.state().mode {
vim.switch_mode(prior_mode, true, cx);
}

View File

@@ -10,6 +10,10 @@
{"Key":"c"}
{"Key":"w"}
{"Get":{"state":"Testˇtest","mode":"Insert"}}
{"Put":{"state":"Tesˇt test"}}
{"Key":"c"}
{"Key":"w"}
{"Get":{"state":"Tesˇ test","mode":"Insert"}}
{"Put":{"state":"Test teˇst\ntest"}}
{"Key":"c"}
{"Key":"w"}

View File

@@ -0,0 +1,5 @@
{"Put":{"state":"1ˇ.2\n"}}
{"Key":"ctrl-a"}
{"Get":{"state":"1.ˇ3\n","mode":"Normal"}}
{"Key":"ctrl-x"}
{"Get":{"state":"1.ˇ2\n","mode":"Normal"}}

View File

@@ -0,0 +1,5 @@
{"Put":{"state":"111.ˇ.2\n"}}
{"Key":"ctrl-a"}
{"Get":{"state":"111..ˇ3\n","mode":"Normal"}}
{"Key":"ctrl-x"}
{"Get":{"state":"111..ˇ2\n","mode":"Normal"}}

View File

@@ -2,7 +2,7 @@ use crate::{
pane::{self, Pane},
persistence::model::ItemId,
searchable::SearchableItemHandle,
workspace_settings::{AutosaveSetting, WorkspaceSettings},
workspace_settings::{AutosaveSetting, TabBarPlacement, WorkspaceSettings},
DelayedDebouncedEditAction, FollowableItemBuilders, ItemNavHistory, ToolbarItemLocation,
ViewId, Workspace, WorkspaceId,
};
@@ -26,7 +26,6 @@ use std::{
any::{Any, TypeId},
cell::RefCell,
ops::Range,
path::PathBuf,
rc::Rc,
sync::Arc,
time::Duration,
@@ -37,7 +36,7 @@ use ui::Element as _;
pub const LEADER_UPDATE_THROTTLE: Duration = Duration::from_millis(200);
#[derive(Deserialize)]
pub struct ItemSettings {
pub struct TabsSettings {
pub git_status: bool,
pub close_position: ClosePosition,
}
@@ -66,10 +65,10 @@ impl ClosePosition {
}
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
pub struct ItemSettingsContent {
pub struct TabsSettingsContent {
/// Whether to show the Git file status on a tab item.
///
/// Default: true
/// Default: false
git_status: Option<bool>,
/// Position of the close button in a tab.
///
@@ -90,10 +89,10 @@ pub struct PreviewTabsSettingsContent {
enable_preview_from_file_finder: Option<bool>,
}
impl Settings for ItemSettings {
impl Settings for TabsSettings {
const KEY: Option<&'static str> = Some("tabs");
type FileContent = ItemSettingsContent;
type FileContent = TabsSettingsContent;
fn load(sources: SettingsSources<Self::FileContent>, _: &mut AppContext) -> Result<Self> {
sources.json_merge()
@@ -196,7 +195,7 @@ pub trait Item: FocusableView + EventEmitter<Self::Event> {
fn save_as(
&mut self,
_project: Model<Project>,
_abs_path: PathBuf,
_path: ProjectPath,
_cx: &mut ViewContext<Self>,
) -> Task<Result<()>> {
unimplemented!("save_as() must be implemented if can_save() returns true")
@@ -226,6 +225,10 @@ pub trait Item: FocusableView + EventEmitter<Self::Event> {
None
}
fn tab_bar_placement(&self) -> TabBarPlacement {
TabBarPlacement::Top
}
fn breadcrumb_location(&self) -> ToolbarItemLocation {
ToolbarItemLocation::Hidden
}
@@ -309,7 +312,7 @@ pub trait ItemHandle: 'static + Send {
fn save_as(
&self,
project: Model<Project>,
abs_path: PathBuf,
path: ProjectPath,
cx: &mut WindowContext,
) -> Task<Result<()>>;
fn reload(&self, project: Model<Project>, cx: &mut WindowContext) -> Task<Result<()>>;
@@ -321,6 +324,7 @@ pub trait ItemHandle: 'static + Send {
callback: Box<dyn FnOnce(&mut AppContext) + Send>,
) -> gpui::Subscription;
fn to_searchable_item_handle(&self, cx: &AppContext) -> Option<Box<dyn SearchableItemHandle>>;
fn tab_bar_placement(&self, cx: &AppContext) -> TabBarPlacement;
fn breadcrumb_location(&self, cx: &AppContext) -> ToolbarItemLocation;
fn breadcrumbs(&self, theme: &Theme, cx: &AppContext) -> Option<Vec<BreadcrumbText>>;
fn serialized_item_kind(&self) -> Option<&'static str>;
@@ -647,10 +651,10 @@ impl<T: Item> ItemHandle for View<T> {
fn save_as(
&self,
project: Model<Project>,
abs_path: PathBuf,
path: ProjectPath,
cx: &mut WindowContext,
) -> Task<anyhow::Result<()>> {
self.update(cx, |item, cx| item.save_as(project, abs_path, cx))
self.update(cx, |item, cx| item.save_as(project, path, cx))
}
fn reload(&self, project: Model<Project>, cx: &mut WindowContext) -> Task<Result<()>> {
@@ -679,6 +683,10 @@ impl<T: Item> ItemHandle for View<T> {
self.read(cx).as_searchable(self)
}
fn tab_bar_placement(&self, cx: &AppContext) -> TabBarPlacement {
self.read(cx).tab_bar_placement()
}
fn breadcrumb_location(&self, cx: &AppContext) -> ToolbarItemLocation {
self.read(cx).breadcrumb_location()
}
@@ -1126,7 +1134,7 @@ pub mod test {
fn save_as(
&mut self,
_: Model<Project>,
_: std::path::PathBuf,
_: ProjectPath,
_: &mut ViewContext<Self>,
) -> Task<anyhow::Result<()>> {
self.save_as_count += 1;

View File

@@ -263,7 +263,7 @@ impl Render for LanguageServerPrompt {
PromptLevel::Warning => {
Some(DiagnosticSeverity::WARNING)
}
PromptLevel::Critical => {
PromptLevel::Critical | PromptLevel::Destructive => {
Some(DiagnosticSeverity::ERROR)
}
}

View File

@@ -1,10 +1,10 @@
use crate::{
item::{
ClosePosition, Item, ItemHandle, ItemSettings, PreviewTabsSettings, TabContentParams,
ClosePosition, Item, ItemHandle, PreviewTabsSettings, TabContentParams, TabsSettings,
WeakItemHandle,
},
toolbar::Toolbar,
workspace_settings::{AutosaveSetting, TabBarSettings, WorkspaceSettings},
workspace_settings::{AutosaveSetting, TabBarPlacement, TabBarSettings, WorkspaceSettings},
NewCenterTerminal, NewFile, NewSearch, OpenInTerminal, OpenTerminal, OpenVisible,
SplitDirection, ToggleZoom, Workspace,
};
@@ -26,7 +26,7 @@ use std::{
any::Any,
cmp, fmt, mem,
ops::ControlFlow,
path::{Path, PathBuf},
path::PathBuf,
rc::Rc,
sync::{
atomic::{AtomicUsize, Ordering},
@@ -1322,14 +1322,10 @@ impl Pane {
pane.update(cx, |_, cx| item.save(should_format, project, cx))?
.await?;
} else if can_save_as {
let start_abs_path = project
.update(cx, |project, cx| {
let worktree = project.visible_worktrees(cx).next()?;
Some(worktree.read(cx).as_local()?.abs_path().to_path_buf())
})?
.unwrap_or_else(|| Path::new("").into());
let abs_path = cx.update(|cx| cx.prompt_for_new_path(&start_abs_path))?;
let abs_path = pane.update(cx, |pane, cx| {
pane.workspace
.update(cx, |workspace, cx| workspace.prompt_for_new_path(cx))
})??;
if let Some(abs_path) = abs_path.await.ok().flatten() {
pane.update(cx, |_, cx| item.save_as(project, abs_path, cx))?
.await?;
@@ -1427,6 +1423,7 @@ impl Pane {
ix: usize,
item: &Box<dyn ItemHandle>,
detail: usize,
tab_bar_placement: ui::TabBarPlacement,
cx: &mut ViewContext<'_, Pane>,
) -> impl IntoElement {
let is_active = ix == self.active_item_index;
@@ -1443,7 +1440,7 @@ impl Pane {
},
cx,
);
let close_side = &ItemSettings::get_global(cx).close_position;
let close_side = &TabsSettings::get_global(cx).close_position;
let indicator = render_item_indicator(item.boxed_clone(), cx);
let item_id = item.item_id();
let is_first_item = ix == 0;
@@ -1451,6 +1448,7 @@ impl Pane {
let position_relative_to_active_item = ix.cmp(&self.active_item_index);
let tab = Tab::new(ix)
.tab_bar_placement(tab_bar_placement)
.position(if is_first_item {
TabPosition::First
} else if is_last_item {
@@ -1663,8 +1661,20 @@ impl Pane {
})
}
fn render_tab_bar(&mut self, cx: &mut ViewContext<'_, Pane>) -> impl IntoElement {
fn need_tab_bar_at(&self, placement: TabBarPlacement, cx: &mut ViewContext<'_, Pane>) -> bool {
let Some(item) = self.active_item() else {
return false;
};
item.tab_bar_placement(cx) == placement
}
fn render_tab_bar(
&mut self,
placement: ui::TabBarPlacement,
cx: &mut ViewContext<'_, Pane>,
) -> impl IntoElement {
TabBar::new("tab_bar")
.placement(placement)
.track_scroll(self.tab_bar_scroll_handle.clone())
.when(
self.display_nav_history_buttons.unwrap_or_default(),
@@ -1708,7 +1718,7 @@ impl Pane {
.iter()
.enumerate()
.zip(tab_details(&self.items, cx))
.map(|((ix, item), detail)| self.render_tab(ix, item, detail, cx)),
.map(|((ix, item), detail)| self.render_tab(ix, item, detail, placement, cx)),
)
.child(
div()
@@ -2046,8 +2056,8 @@ impl Render for Pane {
}
}),
)
.when(self.active_item().is_some(), |pane| {
pane.child(self.render_tab_bar(cx))
.when(self.need_tab_bar_at(TabBarPlacement::Top, cx), |pane| {
pane.child(self.render_tab_bar(ui::TabBarPlacement::Top, cx))
})
.child({
let has_worktrees = self.project.read(cx).worktrees().next().is_some();
@@ -2117,6 +2127,9 @@ impl Render for Pane {
}),
)
})
.when(self.need_tab_bar_at(TabBarPlacement::Bottom, cx), |pane| {
pane.child(self.render_tab_bar(ui::TabBarPlacement::Bottom, cx))
})
.on_mouse_down(
MouseButton::Navigate(NavigationDirection::Back),
cx.listener(|pane, _, cx| {

View File

@@ -33,8 +33,8 @@ use gpui::{
Size, Subscription, Task, View, WeakView, WindowHandle, WindowOptions,
};
use item::{
FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, PreviewTabsSettings,
ProjectItem,
FollowableItem, FollowableItemHandle, Item, ItemHandle, PreviewTabsSettings, ProjectItem,
TabsSettings,
};
use itertools::Itertools;
use language::{LanguageRegistry, Rope};
@@ -85,7 +85,7 @@ use ui::{
use util::{maybe, ResultExt};
use uuid::Uuid;
pub use workspace_settings::{
AutosaveSetting, RestoreOnStartupBehaviour, TabBarSettings, WorkspaceSettings,
AutosaveSetting, RestoreOnStartupBehaviour, TabBarPlacement, TabBarSettings, WorkspaceSettings,
};
use crate::notifications::NotificationId;
@@ -265,7 +265,8 @@ impl Column for WorkspaceId {
}
pub fn init_settings(cx: &mut AppContext) {
WorkspaceSettings::register(cx);
ItemSettings::register(cx);
TabsSettings::register(cx);
TabsSettings::register(cx);
PreviewTabsSettings::register(cx);
TabBarSettings::register(cx);
}
@@ -544,6 +545,10 @@ pub enum OpenVisible {
OnlyDirectories,
}
type PromptForNewPath = Box<
dyn Fn(&mut Workspace, &mut ViewContext<Workspace>) -> oneshot::Receiver<Option<ProjectPath>>,
>;
/// Collects everything project-related for a certain window opened.
/// In some way, is a counterpart of a window, as the [`WindowHandle`] could be downcast into `Workspace`.
///
@@ -585,6 +590,7 @@ pub struct Workspace {
bounds: Bounds<Pixels>,
centered_layout: bool,
bounds_save_task_queued: Option<Task<()>>,
on_prompt_for_new_path: Option<PromptForNewPath>,
}
impl EventEmitter<Event> for Workspace {}
@@ -875,6 +881,7 @@ impl Workspace {
bounds: Default::default(),
centered_layout: false,
bounds_save_task_queued: None,
on_prompt_for_new_path: None,
}
}
@@ -1223,6 +1230,59 @@ impl Workspace {
cx.notify();
}
pub fn set_prompt_for_new_path(&mut self, prompt: PromptForNewPath) {
self.on_prompt_for_new_path = Some(prompt)
}
pub fn prompt_for_new_path(
&mut self,
cx: &mut ViewContext<Self>,
) -> oneshot::Receiver<Option<ProjectPath>> {
if let Some(prompt) = self.on_prompt_for_new_path.take() {
let rx = prompt(self, cx);
self.on_prompt_for_new_path = Some(prompt);
rx
} else {
let start_abs_path = self
.project
.update(cx, |project, cx| {
let worktree = project.visible_worktrees(cx).next()?;
Some(worktree.read(cx).as_local()?.abs_path().to_path_buf())
})
.unwrap_or_else(|| Path::new("").into());
let (tx, rx) = oneshot::channel();
let abs_path = cx.prompt_for_new_path(&start_abs_path);
cx.spawn(|this, mut cx| async move {
let abs_path = abs_path.await?;
let project_path = abs_path.and_then(|abs_path| {
this.update(&mut cx, |this, cx| {
this.project.update(cx, |project, cx| {
project.find_or_create_local_worktree(abs_path, true, cx)
})
})
.ok()
});
if let Some(project_path) = project_path {
let (worktree, path) = project_path.await?;
let worktree_id = worktree.read_with(&cx, |worktree, _| worktree.id())?;
tx.send(Some(ProjectPath {
worktree_id,
path: path.into(),
}))
.ok();
} else {
tx.send(None).ok();
}
anyhow::Ok(())
})
.detach_and_log_err(cx);
rx
}
}
pub fn titlebar_item(&self) -> Option<AnyView> {
self.titlebar_item.clone()
}
@@ -4785,6 +4845,7 @@ pub fn join_hosted_project(
pub fn join_remote_project(
project_id: ProjectId,
app_state: Arc<AppState>,
window_to_replace: Option<WindowHandle<Workspace>>,
cx: &mut AppContext,
) -> Task<Result<WindowHandle<Workspace>>> {
let windows = cx.windows();
@@ -4816,16 +4877,25 @@ pub fn join_remote_project(
)
.await?;
let window_bounds_override = window_bounds_env_override();
cx.update(|cx| {
let mut options = (app_state.build_window_options)(None, cx);
options.bounds = window_bounds_override;
cx.open_window(options, |cx| {
cx.new_view(|cx| {
if let Some(window_to_replace) = window_to_replace {
cx.update_window(window_to_replace.into(), |_, cx| {
cx.replace_root_view(|cx| {
Workspace::new(Default::default(), project, app_state.clone(), cx)
});
})?;
window_to_replace
} else {
let window_bounds_override = window_bounds_env_override();
cx.update(|cx| {
let mut options = (app_state.build_window_options)(None, cx);
options.bounds = window_bounds_override;
cx.open_window(options, |cx| {
cx.new_view(|cx| {
Workspace::new(Default::default(), project, app_state.clone(), cx)
})
})
})
})?
})?
}
};
workspace.update(&mut cx, |_, cx| {

View File

@@ -58,13 +58,30 @@ pub struct WorkspaceSettingsContent {
pub drop_target_size: Option<f32>,
}
/// The tab bar placement in a pane.
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum TabBarPlacement {
/// Don't show tab bar.
No,
/// Place tab bar on top of the pane.
Top,
/// Place tab bar at the bottom of the pane.
Bottom,
}
#[derive(Deserialize)]
pub struct TabBarSettings {
pub placement: TabBarPlacement,
pub show_nav_history_buttons: bool,
}
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
pub struct TabBarSettingsContent {
/// Where to place tab bar in the editor.
///
/// Default: top
pub placement: Option<TabBarPlacement>,
/// Whether or not to show the navigation history buttons in the tab bar.
///
/// Default: true

View File

@@ -1625,6 +1625,7 @@ impl RemoteWorktree {
pub fn save_buffer(
&self,
buffer_handle: Model<Buffer>,
new_path: Option<proto::ProjectPath>,
cx: &mut ModelContext<Worktree>,
) -> Task<Result<()>> {
let buffer = buffer_handle.read(cx);
@@ -1637,6 +1638,7 @@ impl RemoteWorktree {
.request(proto::SaveBuffer {
project_id,
buffer_id,
new_path,
version: serialize_version(&version),
})
.await?;
@@ -1911,6 +1913,7 @@ impl Snapshot {
fn traverse_from_offset(
&self,
include_files: bool,
include_dirs: bool,
include_ignored: bool,
start_offset: usize,
@@ -1919,6 +1922,7 @@ impl Snapshot {
cursor.seek(
&TraversalTarget::Count {
count: start_offset,
include_files,
include_dirs,
include_ignored,
},
@@ -1927,6 +1931,7 @@ impl Snapshot {
);
Traversal {
cursor,
include_files,
include_dirs,
include_ignored,
}
@@ -1934,6 +1939,7 @@ impl Snapshot {
fn traverse_from_path(
&self,
include_files: bool,
include_dirs: bool,
include_ignored: bool,
path: &Path,
@@ -1942,17 +1948,22 @@ impl Snapshot {
cursor.seek(&TraversalTarget::Path(path), Bias::Left, &());
Traversal {
cursor,
include_files,
include_dirs,
include_ignored,
}
}
pub fn files(&self, include_ignored: bool, start: usize) -> Traversal {
self.traverse_from_offset(false, include_ignored, start)
self.traverse_from_offset(true, false, include_ignored, start)
}
pub fn directories(&self, include_ignored: bool, start: usize) -> Traversal {
self.traverse_from_offset(false, true, include_ignored, start)
}
pub fn entries(&self, include_ignored: bool) -> Traversal {
self.traverse_from_offset(true, include_ignored, 0)
self.traverse_from_offset(true, true, include_ignored, 0)
}
pub fn repositories(&self) -> impl Iterator<Item = (&Arc<Path>, &RepositoryEntry)> {
@@ -2084,6 +2095,7 @@ impl Snapshot {
cursor.seek(&TraversalTarget::Path(parent_path), Bias::Right, &());
let traversal = Traversal {
cursor,
include_files: true,
include_dirs: true,
include_ignored: true,
};
@@ -2103,6 +2115,7 @@ impl Snapshot {
cursor.seek(&TraversalTarget::Path(parent_path), Bias::Left, &());
let mut traversal = Traversal {
cursor,
include_files: true,
include_dirs,
include_ignored,
};
@@ -2141,7 +2154,7 @@ impl Snapshot {
pub fn entry_for_path(&self, path: impl AsRef<Path>) -> Option<&Entry> {
let path = path.as_ref();
self.traverse_from_path(true, true, path)
self.traverse_from_path(true, true, true, path)
.entry()
.and_then(|entry| {
if entry.path.as_ref() == path {
@@ -4532,12 +4545,15 @@ struct TraversalProgress<'a> {
}
impl<'a> TraversalProgress<'a> {
fn count(&self, include_dirs: bool, include_ignored: bool) -> usize {
match (include_ignored, include_dirs) {
(true, true) => self.count,
(true, false) => self.file_count,
(false, true) => self.non_ignored_count,
(false, false) => self.non_ignored_file_count,
fn count(&self, include_files: bool, include_dirs: bool, include_ignored: bool) -> usize {
match (include_files, include_dirs, include_ignored) {
(true, true, true) => self.count,
(true, true, false) => self.non_ignored_count,
(true, false, true) => self.file_count,
(true, false, false) => self.non_ignored_file_count,
(false, true, true) => self.count - self.file_count,
(false, true, false) => self.non_ignored_count - self.non_ignored_file_count,
(false, false, _) => 0,
}
}
}
@@ -4600,6 +4616,7 @@ impl<'a> sum_tree::Dimension<'a, EntrySummary> for GitStatuses {
pub struct Traversal<'a> {
cursor: sum_tree::Cursor<'a, Entry, TraversalProgress<'a>>,
include_ignored: bool,
include_files: bool,
include_dirs: bool,
}
@@ -4609,6 +4626,7 @@ impl<'a> Traversal<'a> {
&TraversalTarget::Count {
count: self.end_offset() + 1,
include_dirs: self.include_dirs,
include_files: self.include_files,
include_ignored: self.include_ignored,
},
Bias::Left,
@@ -4624,7 +4642,8 @@ impl<'a> Traversal<'a> {
&(),
);
if let Some(entry) = self.cursor.item() {
if (self.include_dirs || !entry.is_dir())
if (self.include_files || !entry.is_file())
&& (self.include_dirs || !entry.is_dir())
&& (self.include_ignored || !entry.is_ignored)
{
return true;
@@ -4641,13 +4660,13 @@ impl<'a> Traversal<'a> {
pub fn start_offset(&self) -> usize {
self.cursor
.start()
.count(self.include_dirs, self.include_ignored)
.count(self.include_files, self.include_dirs, self.include_ignored)
}
pub fn end_offset(&self) -> usize {
self.cursor
.end(&())
.count(self.include_dirs, self.include_ignored)
.count(self.include_files, self.include_dirs, self.include_ignored)
}
}
@@ -4670,6 +4689,7 @@ enum TraversalTarget<'a> {
PathSuccessor(&'a Path),
Count {
count: usize,
include_files: bool,
include_ignored: bool,
include_dirs: bool,
},
@@ -4688,11 +4708,12 @@ impl<'a, 'b> SeekTarget<'a, EntrySummary, TraversalProgress<'a>> for TraversalTa
}
TraversalTarget::Count {
count,
include_files,
include_dirs,
include_ignored,
} => Ord::cmp(
count,
&cursor_location.count(*include_dirs, *include_ignored),
&cursor_location.count(*include_files, *include_dirs, *include_ignored),
),
}
}

View File

@@ -52,14 +52,11 @@ fn main() {
println!("cargo:rustc-link-arg=/stack:{}", 8 * 1024 * 1024);
}
let manifest = std::path::Path::new("resources/windows/manifest.xml");
let icon = std::path::Path::new("resources/windows/app-icon.ico");
println!("cargo:rerun-if-changed={}", manifest.display());
println!("cargo:rerun-if-changed={}", icon.display());
let mut res = winresource::WindowsResource::new();
res.set_icon(icon.to_str().unwrap());
res.set_manifest_file(manifest.to_str().unwrap());
if let Err(e) = res.compile() {
eprintln!("{}", e);

View File

@@ -145,6 +145,13 @@ fn init_headless(dev_server_token: DevServerToken) {
);
handle_settings_file_changes(user_settings_file_rx, cx);
let (installation_id, _) = cx
.background_executor()
.block(installation_id())
.ok()
.unzip();
upload_panics_and_crashes(client.http_client(), installation_id, cx);
headless::init(
client.clone(),
headless::AppState {
@@ -323,7 +330,7 @@ fn init_ui(args: Args) {
.detach();
let telemetry = client.telemetry();
telemetry.start(installation_id, session_id, cx);
telemetry.start(installation_id.clone(), session_id, cx);
telemetry.report_setting_event("theme", cx.theme().name.to_string());
telemetry.report_setting_event("keymap", BaseKeymap::get_global(cx).to_string());
telemetry.report_app_event(
@@ -378,8 +385,7 @@ fn init_ui(args: Args) {
cx.set_menus(app_menus());
initialize_workspace(app_state.clone(), cx);
// todo(linux): unblock this
upload_panics_and_crashes(client.http_client(), cx);
upload_panics_and_crashes(client.http_client(), installation_id, cx);
cx.activate(true);
@@ -824,7 +830,11 @@ fn init_panic_hook(app: &App, installation_id: Option<String>, session_id: Strin
}));
}
fn upload_panics_and_crashes(http: Arc<HttpClientWithUrl>, cx: &mut AppContext) {
fn upload_panics_and_crashes(
http: Arc<HttpClientWithUrl>,
installation_id: Option<String>,
cx: &mut AppContext,
) {
let telemetry_settings = *client::TelemetrySettings::get_global(cx);
cx.background_executor()
.spawn(async move {
@@ -832,7 +842,7 @@ fn upload_panics_and_crashes(http: Arc<HttpClientWithUrl>, cx: &mut AppContext)
.await
.log_err()
.flatten();
upload_previous_crashes(http, most_recent_panic, telemetry_settings)
upload_previous_crashes(http, most_recent_panic, installation_id, telemetry_settings)
.await
.log_err()
})
@@ -915,6 +925,7 @@ static LAST_CRASH_UPLOADED: &'static str = "LAST_CRASH_UPLOADED";
async fn upload_previous_crashes(
http: Arc<HttpClientWithUrl>,
most_recent_panic: Option<(i64, String)>,
installation_id: Option<String>,
telemetry_settings: client::TelemetrySettings,
) -> Result<()> {
if !telemetry_settings.diagnostics {
@@ -964,6 +975,9 @@ async fn upload_previous_crashes(
.header("x-zed-panicked-on", format!("{}", panicked_on))
.header("x-zed-panic", payload)
}
if let Some(installation_id) = installation_id.as_ref() {
request = request.header("x-zed-installation-id", installation_id);
}
let request = request.body(body.into())?;

View File

@@ -304,6 +304,104 @@ List of `string` values
`boolean` values
## Editor Tab Bar
- Description: Settings related to the editor's tab bar.
- Settings: `tab_bar`
- Default:
```json
"tab_bar": {
"placement": "top",
"show_nav_history_buttons": true
}
```
### Placement
- Description: Where to place the editor tab bar.
- Setting: `placement`
- Default: `top`
**Options**
1. Place the tab bar on top of the editor:
```json
{
"placement": "top"
}
```
2. Place the tab bar at the bottom of the editor:
```json
{
"placement": "bottom"
}
```
3. Hide the tab bar:
```json
{
"placement": "no"
}
```
### Navigation History Buttons
- Description: Whether to show the navigation history buttons.
- Setting: `show_nav_history_buttons`
- Default: `true`
**Options**
`boolean` values
## Editor Tabs
- Description: Configuration for the editor tabs.
- Setting: `tabs`
- Default:
```json
"tabs": {
"close_position": "right",
"git_status": false
},
```
### Close Position
- Description: Where to display close button within a tab.
- Setting: `close_position`
- Default: `right`
**Options**
1. Display the close button on the right:
```json
{
"close_position": "right"
}
```
2. Display the close button on the left:
```json
{
"close_position": "left"
}
```
### Git Status
- Description: Whether or not to show Git file status in tab.
- Setting: `git_status`
- Default: `false`
## Editor Toolbar
- Description: Whether or not to show various elements in the editor toolbar.