Compare commits

..

41 Commits

Author SHA1 Message Date
Thorsten Ball
a0c10abe80 Revert "scrolling: use display scale factor on pixel scroll"
This reverts commit 32d490b16c.
2024-02-02 09:31:12 +01:00
Thorsten Ball
32d490b16c scrolling: use display scale factor on pixel scroll
Co-Authored-By: Conrad <conrad@zed.dev>
Co-Authored-By: Bennet <bennetbo@gmx.de>
2024-02-02 09:24:34 +01:00
Thorsten Ball
f99f8d232a gpui: Schedule a refresh right after launching app 2024-02-02 09:24:34 +01:00
Thorsten Ball
2ee290727a window: redraw on resize after adding display-link 2024-02-02 09:24:34 +01:00
Thorsten Ball
12d9ebe8fd Do not block when refreshing display 2024-02-02 09:24:34 +01:00
Antonio Scandurra
ca9c247722 WIP 2024-02-02 09:24:33 +01:00
Ocean
5ed3b44686 Add rename to JetBrains keymaps (#7263)
Add rename actions to JetBrains keymaps.

Closes #7261.

Release Notes:

- Added rename keybindings to JetBrains keymap ([#7261](https://github.com/zed-industries/zed/issues/7261)).
2024-02-02 09:55:54 +02:00
Conrad Irwin
69e0ea92e4 Links to channel notes (#7262)
Release Notes:

- Added outline support for Markdown files
- Added the ability to link to channel notes:
https://zed.dev/channel/zed-283/notes#Roadmap
2024-02-01 22:22:02 -07:00
Max Brunsfeld
b35a7223b6 Add missing secret in release nightly workflow 2024-02-01 15:45:53 -08:00
Max Brunsfeld
020c38a891 Avoid excessive blocking of main thread when rendering in direct mode (#7253)
Release Notes:

- Fixed a bug that caused inconsistent frame rate when scrolling on
certain hardware.

Co-authored-by: Antonio Scandurra <me@as-cii.com>
Co-authored-by: Nathan Sobo <nathan@zed.dev>
2024-02-01 15:39:41 -08:00
Max Brunsfeld
21f4da6bf2 Correctly log LSP adapter name on LSP request error (#7232)
Previously, we were logging the language server's binary filename
instead.

Release Notes:

- N/A

Co-authored-by: Nathan <nathan@zed.dev>
Co-authored-by: Antonio <antonio@zed.dev>
2024-02-01 14:17:09 -08:00
Kirill Bulatov
da44f637ed Order history items by open recency (#7248)
Closes https://github.com/zed-industries/zed/issues/7244

Follow-up of https://github.com/zed-industries/zed/pull/7210 that
returns back ordering of history items by open recency (last opened
history item should be on top)

Release Notes:

- N/A

---------

Co-authored-by: Andrew Lygin <alygin@gmail.com>
2024-02-01 23:45:03 +02:00
Conrad Irwin
0897c8eebd just kidding (#7241)
Release Notes:

- N/A
2024-02-01 11:57:09 -07:00
Conrad Irwin
7b9d51929d Deploy collab like nightly (#7174)
After this change we'll be able to push a tag to github to deploy to
collab.

The advantages of this are that there's no longer a separate step to
first
build the image, and then deploy it.

In the future I'd like to make this happen more automatically (maybe as
part of
bump nightly).

Release Notes:

- N/A
2024-02-01 11:54:49 -07:00
Antonio Scandurra
5424c8bfd5 Introduce a fast path for drawing quads with no borders / corner radii (#7231)
This will introduce an extra conditional but saves us from doing a bunch
of math in the simple case of drawing simple rectangles that aren't
rounded or don't have borders.


![Figure_1](https://github.com/zed-industries/zed/assets/482957/cba95ce2-2d9a-46ab-a142-35368334eb75)

Release Notes:

- Improved rendering performance.
2024-02-01 09:49:27 -08:00
Conrad Irwin
3521b50405 vim: Fix , and ; in visual mode (#7230)
Release Notes:

- vim: Fixed , and ; in visual mode
([#7182](https://github.com/zed-industries/zed/issues/7182)).
2024-02-01 10:13:30 -07:00
Mikayla Maki
d4264cbe4e Fix scrolling and wrapping in the markdown preview renderer (#7234)
Release Notes:

- N/A
2024-02-01 09:07:01 -08:00
Dairon M
97be0a930c Add syntax highlighting and LSP (erlang_lsp) for Erlang (#7093)
This pull request implements support for the [Erlang
Language](https://erlang.org/).

**It adds:**

* [tree-sitter-erlang](https://github.com/WhatsApp/tree-sitter-erlang)
grammar
highlights (Licensed under Apache-2 from WhatsApp which is compatible
with Zed licensing model), folds and indents
* Erlang file icon based on the [official
one](https://www.erlang.org/doc/erlang-logo.png)
* [erlang_ls](https://github.com/erlang-ls/erlang_ls) support

Fixes https://github.com/zed-industries/zed/issues/4939, possibly a
duplicate of https://github.com/zed-industries/zed/pull/7085 with more
features. Suppose @wingyplus wants to join efforts here.

**To complete (out of scope for this PR):**

* Support for the ELP language server from WhatsApp. CC @robertoaloi
* Better indentation handling, need something like
`indentNextLinePattern` in VS Code

**Screenshots:**

![Screenshot 2024-01-30 at 11 03 51
AM](https://github.com/zed-industries/zed/assets/168440/5289c245-9edd-46b8-b443-d7b3210f6510)
![Screenshot 2024-01-30 at 11 01 19
AM](https://github.com/zed-industries/zed/assets/168440/bd22b322-5344-44e6-b5f7-6e352fb3deef)
![Screenshot 2024-01-30 at 11 01 37
AM](https://github.com/zed-industries/zed/assets/168440/f28f6a15-383e-4719-8a87-fceae5062436)
![Screenshot 2024-01-30 at 11 02 03
AM](https://github.com/zed-industries/zed/assets/168440/980d5213-0367-4a08-86eb-5743dfa628eb)
![Screenshot 2024-01-30 at 11 02 19
AM](https://github.com/zed-industries/zed/assets/168440/ea998891-604d-48d6-929f-ae4c1bb3fae1)

Outline: 
![Screenshot 2024-01-31 at 9 09 36
AM](https://github.com/zed-industries/zed/assets/168440/46d56d94-21c3-414d-84fb-9251fa2506ab)



**Release Notes:**

* Added Erlang Support
([7093](https://github.com/zed-industries/zed/pull/7093)).

---------

Signed-off-by: Thanabodee Charoenpiriyakij <wingyminus@gmail.com>
Co-authored-by: Thanabodee Charoenpiriyakij <wingyminus@gmail.com>
2024-02-01 18:54:26 +02:00
Thorsten Ball
3107ed847a lsp: if language server closes stdout/stderr, break loop (#7229)
Previously we would run these loops indefinitely when a language server
closed its stdout/stderr and the `read_until` returned `0` bytes read.

Easy to reproduce: start Zed with LSP attached, `kill -9` the LSP, see
logs accumulate.

Release Notes:

- Fix high CPU usage when a language server crashes (or closes its
stdout/stderr on purpose).

Co-authored-by: Julia <julia@zed.dev>
Co-authored-by: Mikayla <mikayla@zed.dev>
2024-02-01 17:49:53 +01:00
Kirill Bulatov
944a1f8fb0 Send lsp_types::InitializeParams with Zed version (#7216)
Based on the great work in
https://github.com/zed-industries/zed/pull/7130 , now sends this data

```
[crates/lsp/src/lsp.rs:588] ClientInfo { name: name.to_string(), version: Some(version.to_string()) } = ClientInfo {
    name: "Zed Dev",
    version: Some(
        "0.122.0",
    ),
}
```

with every LSP server initialization.

Release Notes:

- Added Zed name and version to LSP InitializeParams requests
2024-02-01 18:39:28 +02:00
Marshall Bowers
47a1ff7df9 markdown_preview: Sort dependencies in Cargo.toml (#7226)
This PR sorts the dependencies for the `markdown_preview` crate in
alphabetical order.

Release Notes:

- N/A
2024-02-01 11:26:50 -05:00
d1y
b9d5eb17a3 Fix typo (#7223)
Release Notes:

- N/A
2024-02-01 11:05:51 -05:00
Thorsten Ball
adc7cfb0d3 Fix moving focus to docks when navigating via keybinds (#7221)
This is a follow-up to #7141 and fixes the focus-switching to docks in
case they haven't been focused before.

We ran into issues when trying to focus a dock, that hasn't been focused
in the app's lifecycle: focus would only flip after the next re-render
(which could be triggered by moving the mouse, for example)

This changes the approach and uses the one we have for `toggle focus`
actions.

Release Notes:

- N/A

Co-authored-by: Piotr <piotr@zed.dev>
Co-authored-by: bennetbo <bennetbo@gmx.de>
2024-02-01 16:28:51 +01:00
thurain
a853a80634 Add YAML file type icon (#7185)
Add YAML file type icon from
[file-icons/icons](https://github.com/file-icons/icons)

https://github.com/file-icons/icons/blob/master/svg/YAML.svg
Release Notes:
- Added YAML file type icon.

---------

Co-authored-by: d1y <chenhonzhou@gmail.com>
2024-02-01 16:13:07 +02:00
Bennet Bo Fenner
2d41a119b3 markdown: Support alignment for table cells (#7201)
Just a small improvement as a follow up to @kierangilliam great work on
#6958

Rendering a table specified like this:
```markdown
| Left columns  | Center columns | Right columns |
| ------------- |:--------------:| -------------:|
| left foo      | center foo     | right foo     |
| left bar      | center bar     | right bar     |
| left baz      | center baz     | right baz     |
```
Does now look like this (notice the cell alignments):

![image](https://github.com/zed-industries/zed/assets/53836821/0f5b6a1e-a3c2-4fe9-bdcb-8654dbae7980)

Release Notes:
- N/A
2024-02-01 15:43:04 +02:00
Kirill Bulatov
0102ffbfca Refactor file_finder send element open code (#7210)
Follow-up of https://github.com/zed-industries/zed/pull/6947 (cc
@alygin) that fixes a few style nits and refactors the code around:
* use already stored `currently_opened_path` to decide what to do with
the history item sorting
* use the same method to set history items, encapsulate the bubbling up
logic there
* ensure history elements are properly sorted before populating

The main reason to change all that is the new comparator in the previous
version:
https://github.com/zed-industries/zed/pull/6947/files#diff-eac7c8c99856f77cee39117708cd1467fd5bbc8805da2564f851951638020842R234
that almost violated `util::extend_sorted` contract, requiring both
collections to be sorted the same way as the comparator would be: it did
work, because we bubbled currently open item up in the history items
list manually, and that we have only one such item.

Release Notes:
- N/A
2024-02-01 14:35:42 +02:00
Andrew Lygin
0edffd9248 Select the second item in the file finder by default (#6947)
This PR completes the first task of the Tabless editing feature (#6424).
It makes file finder select the previously opened file by default which
allows the user to quickly switch between two last opened files by
clicking `Cmd-P + Enter`.

This feature was also requested in #4663 comments.

Release Notes:
* Improved file finder selection: currently opened item is not selected now
2024-02-01 14:21:59 +02:00
Thorsten Ball
e65a76f0ec Add ability to navigate to/from docks via keybindings (#7141)
This adds the ability to navigate to/from docks (Terminal, Project,
Collaboration, Assistant) via keybindings.

When using the `ActivatePaneInDirection` keybinding from the
left/bottom/right dock, we check whether the movement is towards the
center panel. If it is, we focus the last active pane.

Fixes https://github.com/zed-industries/zed/issues/6833 and it came up
in a few other tickes/discussions.

Release Notes:

- Added ability to navigate to docks and back to the editor using the
`workspace::ActivatePaneInDirection` action (by default bound to `Ctrl-w
[hjkl]` in Vim mode).
([#6833](https://github.com/zed-industries/zed/issues/6833)).

## Drawback

There's this weird behavior: if you start Zed and no files are opened,
you focus terminal, go left (project panel), then back to right to
terminal, the terminal isn't focused. Even though we focus it in the
code.

Maybe this is a bug in the current focus handling code?

## Demo


https://github.com/zed-industries/zed/assets/1185253/5d56db40-36aa-4758-a3bc-7a0de20ce5d7

---------

Co-authored-by: Piotr <piotr@zed.dev>
2024-02-01 12:18:12 +01:00
Thorsten Ball
6c93c4bd35 assistant: render api key editor if no credentials are set (#7197)
This hopefully reduces confusion for new users. I updated the docs just
this morning, but I figured it's probably better to fix the issue
itself.

So what this does is to render the API key editor whenever the assistant
panel is opened/focused and no credentials can be found.

See: https://github.com/zed-industries/zed/discussions/6943

Release Notes:

- Fixed assistant panel not showing dialog to enter API key when opened
without saved credentials.

---------

Co-authored-by: Piotr <piotr@zed.dev>
2024-02-01 11:16:03 +01:00
Kieran Gill
8bafc61ef5 Add initial markdown preview to Zed (#6958)
Adds a "markdown: open preview" action to open a markdown preview.

https://github.com/zed-industries/zed/assets/18583882/6fd7f009-53f7-4f98-84ea-7dd3f0dd11bf


This PR extends the work done in `crates/rich_text` to render markdown
to also support:

- Variable heading sizes
- Markdown tables
- Code blocks
- Block quotes

## Release Notes

- Added `Markdown: Open preview` action to partially close
([#6789](https://github.com/zed-industries/zed/issues/6789)).

## Known issues that will not be included in this PR

- Images.
- Nested block quotes.
- Footnote Reference.
- Headers highlighting.
- Inline code highlighting (this will need to be implemented in
`rich_text`)
- Checkboxes (`- [ ]` and `- [x]`)
- Syntax highlighting in code blocks.
- Markdown table text alignment.
- Inner markdown URL clicks
2024-02-01 11:03:09 +02:00
Ares Andrew
3b882918f7 Filter LSP github releases that have no assets to properly download LSP servers (#7189)
Fixes https://github.com/zed-industries/zed/issues/7183

Release Notes:

- Filter lsp github releases that have no assets ([7189](https://github.com/zed-industries/zed/issues/7183))
2024-02-01 10:59:35 +02:00
Conrad Irwin
5e64d45194 Remove links to docs.zed.dev (#7187)
Release Notes:

- N/A
2024-01-31 22:26:15 -07:00
Conrad Irwin
3df7da236d Also add proxy to zed http client (#7184)
Follow up to #6765 because I couldn't figure out how to add to that PR.

Release Notes:

- N/A
2024-01-31 21:40:12 -07:00
lichuan6
5e81d780bd Read HTTP proxy from env (#6765)
This PR will use http proxy from env for downloading files.
2024-01-31 20:57:09 -07:00
d1y
cbc2746d70 docs: add gitcommit language and update go language (#7181)
Release Notes:

- N/A
2024-01-31 19:27:17 -07:00
Conrad Irwin
aaba98d8ec Debug build (#7176)
Release Notes:

- N/A
2024-01-31 19:22:58 -07:00
Conrad Irwin
2cc2a61c77 collab 0.44.0 2024-01-31 19:10:19 -07:00
Conrad Irwin
3025e5620d Tell the user when screen-sharing fails (#7171)
Release Notes:

- Added an alert when screen-sharing fails
2024-01-31 16:28:32 -07:00
Marshall Bowers
c4083c3cf6 Watch the themes directory for changes (#7173)
This PR makes Zed watch the themes directory for changes.

When theme files are added or modified, we reload the theme and apply
any changes to Zed.

Release Notes:

- Added live reloading for the themes directory.
2024-01-31 18:17:31 -05:00
Conrad Irwin
2187513026 app version to server (#7130)
- Send app version and release stage to collab on connect
- Read the new header on the server

Release Notes:

- Added the ability to collaborate with users on different releases of
Zed.
2024-01-31 15:46:24 -07:00
Conrad Irwin
5b7b5bfea5 Add a checksum telemetry request (#7168)
We're seeing a bit of nonsense on telemetry. Although the checksum seed
isn't secret per-se, it does make sending nonsense a little more effort.

Release Notes:

- N/A
2024-01-31 15:44:38 -07:00
104 changed files with 2483 additions and 641 deletions

View File

@@ -81,6 +81,7 @@ jobs:
MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
APPLE_NOTARIZATION_USERNAME: ${{ secrets.APPLE_NOTARIZATION_USERNAME }}
APPLE_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_NOTARIZATION_PASSWORD }}
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
steps:
- name: Install Node
uses: actions/setup-node@v3

107
.github/workflows/deploy_collab.yml vendored Normal file
View File

@@ -0,0 +1,107 @@
name: Publish Collab Server Image
on:
push:
tags:
- collab-production
- collab-staging
env:
DOCKER_BUILDKIT: 1
DIGITALOCEAN_ACCESS_TOKEN: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }}
jobs:
style:
name: Check formatting and Clippy lints
runs-on:
- self-hosted
- test
steps:
- name: Checkout repo
uses: actions/checkout@v4
with:
clean: false
submodules: "recursive"
fetch-depth: 0
- name: Run style checks
uses: ./.github/actions/check_style
tests:
name: Run tests
runs-on:
- self-hosted
- test
needs: style
steps:
- name: Checkout repo
uses: actions/checkout@v4
with:
clean: false
submodules: "recursive"
fetch-depth: 0
- name: Run tests
uses: ./.github/actions/run_tests
publish:
name: Publish collab server image
needs:
- style
- tests
runs-on:
- self-hosted
- deploy
steps:
- name: Add Rust to the PATH
run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH
- name: Sign into DigitalOcean docker registry
run: doctl registry login
- name: Prune Docker system
run: docker system prune --filter 'until=720h' -f
- name: Checkout repo
uses: actions/checkout@v4
with:
clean: false
submodules: "recursive"
- name: Build docker image
run: docker build . --build-arg GITHUB_SHA=$GITHUB_SHA --tag registry.digitalocean.com/zed/collab:$GITHUB_SHA
- name: Publish docker image
run: docker push registry.digitalocean.com/zed/collab:${GITHUB_SHA}
deploy:
name: Deploy new server image
needs:
- publish
runs-on:
- self-hosted
- deploy
steps:
- name: Sign into Kubernetes
run: doctl kubernetes cluster kubeconfig save --expiry-seconds 600 ${{ secrets.CLUSTER_NAME }}
- name: Determine namespace
run: |
set -eu
if [[ $GITHUB_REF_NAME = "collab-production" ]]; then
echo "Deploying collab:$GITHUB_SHA to production"
echo "KUBE_NAMESPACE=production" >> $GITHUB_ENV
elif [[ $GITHUB_REF_NAME = "collab-staging" ]]; then
echo "Deploying collab:$GITHUB_SHA to staging"
echo "KUBE_NAMESPACE=staging" >> $GITHUB_ENV
else
echo "cowardly refusing to deploy from an unknown branch"
exit 1
fi
- name: Start rollout
run: kubectl -n "$KUBE_NAMESPACE" set image deployment/collab collab=registry.digitalocean.com/zed/collab:${GITHUB_SHA}
- name: Wait for rollout to finish
run: kubectl -n "$KUBE_NAMESPACE" rollout status deployment/collab

View File

@@ -1,49 +0,0 @@
name: Publish Collab Server Image
on:
push:
tags:
- collab-v*
env:
DOCKER_BUILDKIT: 1
DIGITALOCEAN_ACCESS_TOKEN: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }}
jobs:
publish:
name: Publish collab server image
runs-on:
- self-hosted
- deploy
steps:
- name: Add Rust to the PATH
run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH
- name: Sign into DigitalOcean docker registry
run: doctl registry login
- name: Prune Docker system
run: docker system prune
- name: Checkout repo
uses: actions/checkout@v4
with:
clean: false
submodules: 'recursive'
- name: Determine version
run: |
set -eu
version=$(script/get-crate-version collab)
if [[ $GITHUB_REF_NAME != "collab-v${version}" ]]; then
echo "release tag ${GITHUB_REF_NAME} does not match version ${version}"
exit 1
fi
echo "Publishing collab version: ${version}"
echo "COLLAB_VERSION=${version}" >> $GITHUB_ENV
- name: Build docker image
run: docker build . --tag registry.digitalocean.com/zed/collab:v${COLLAB_VERSION}
- name: Publish docker image
run: docker push registry.digitalocean.com/zed/collab:v${COLLAB_VERSION}

View File

@@ -59,6 +59,7 @@ jobs:
APPLE_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_NOTARIZATION_PASSWORD }}
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
steps:
- name: Install Node
uses: actions/setup-node@v3

45
Cargo.lock generated
View File

@@ -1367,6 +1367,7 @@ dependencies = [
"image",
"lazy_static",
"log",
"once_cell",
"parking_lot 0.11.2",
"postage",
"rand 0.8.5",
@@ -1377,6 +1378,7 @@ dependencies = [
"serde_derive",
"serde_json",
"settings",
"sha2 0.10.7",
"smol",
"sum_tree",
"sysinfo",
@@ -1438,7 +1440,7 @@ dependencies = [
[[package]]
name = "collab"
version = "0.43.0"
version = "0.44.0"
dependencies = [
"anyhow",
"async-trait",
@@ -1484,6 +1486,7 @@ dependencies = [
"prometheus",
"prost 0.8.0",
"rand 0.8.5",
"release_channel",
"reqwest",
"rpc",
"scrypt",
@@ -2422,6 +2425,7 @@ dependencies = [
"postage",
"project",
"rand 0.8.5",
"release_channel",
"rich_text",
"rpc",
"schemars",
@@ -2658,6 +2662,7 @@ dependencies = [
"env_logger",
"fuzzy",
"gpui",
"itertools 0.11.0",
"language",
"menu",
"picker",
@@ -4017,6 +4022,7 @@ dependencies = [
"language",
"lsp",
"project",
"release_channel",
"serde",
"serde_json",
"settings",
@@ -4261,6 +4267,7 @@ dependencies = [
"lsp-types",
"parking_lot 0.11.2",
"postage",
"release_channel",
"serde",
"serde_derive",
"serde_json",
@@ -4314,6 +4321,26 @@ dependencies = [
"libc",
]
[[package]]
name = "markdown_preview"
version = "0.1.0"
dependencies = [
"anyhow",
"editor",
"gpui",
"language",
"lazy_static",
"log",
"menu",
"project",
"pulldown-cmark",
"rich_text",
"theme",
"ui",
"util",
"workspace",
]
[[package]]
name = "matchers"
version = "0.1.0"
@@ -5795,6 +5822,7 @@ dependencies = [
"pretty_assertions",
"rand 0.8.5",
"regex",
"release_channel",
"rpc",
"schemars",
"serde",
@@ -5859,6 +5887,7 @@ dependencies = [
"picker",
"postage",
"project",
"release_channel",
"serde_json",
"settings",
"smol",
@@ -8783,6 +8812,16 @@ dependencies = [
"tree-sitter",
]
[[package]]
name = "tree-sitter-erlang"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93ced5145ebb17f83243bf055b74e108da7cc129e12faab4166df03f59b287f4"
dependencies = [
"cc",
"tree-sitter",
]
[[package]]
name = "tree-sitter-gitcommit"
version = "0.3.3"
@@ -9419,6 +9458,7 @@ dependencies = [
"parking_lot 0.11.2",
"project",
"regex",
"release_channel",
"search",
"serde",
"serde_derive",
@@ -10304,6 +10344,7 @@ dependencies = [
"indexmap 1.9.3",
"install_cli",
"isahc",
"itertools 0.11.0",
"journal",
"language",
"language_selector",
@@ -10312,6 +10353,7 @@ dependencies = [
"libc",
"log",
"lsp",
"markdown_preview",
"menu",
"mimalloc",
"node_runtime",
@@ -10361,6 +10403,7 @@ dependencies = [
"tree-sitter-elixir",
"tree-sitter-elm",
"tree-sitter-embedded-template",
"tree-sitter-erlang",
"tree-sitter-gitcommit",
"tree-sitter-gleam",
"tree-sitter-glsl",

View File

@@ -42,6 +42,7 @@ members = [
"crates/live_kit_client",
"crates/live_kit_server",
"crates/lsp",
"crates/markdown_preview",
"crates/media",
"crates/menu",
"crates/multi_buffer",
@@ -111,6 +112,7 @@ parking_lot = "0.11.1"
postage = { version = "0.5", features = ["futures-traits"] }
pretty_assertions = "1.3.0"
prost = "0.8"
pulldown-cmark = { version = "0.9.2", default-features = false }
rand = "0.8.5"
refineable = { path = "./crates/refineable" }
regex = "1.5"
@@ -140,6 +142,7 @@ tree-sitter-css = { git = "https://github.com/tree-sitter/tree-sitter-css", rev
tree-sitter-elixir = { git = "https://github.com/elixir-lang/tree-sitter-elixir", rev = "a2861e88a730287a60c11ea9299c033c7d076e30" }
tree-sitter-elm = { git = "https://github.com/elm-tooling/tree-sitter-elm", rev = "692c50c0b961364c40299e73c1306aecb5d20f40" }
tree-sitter-embedded-template = "0.20.0"
tree-sitter-erlang = "0.4.0"
tree-sitter-gitcommit = { git = "https://github.com/gbprod/tree-sitter-gitcommit" }
tree-sitter-gleam = { git = "https://github.com/gleam-lang/tree-sitter-gleam", rev = "58b7cac8fc14c92b0677c542610d8738c373fa81" }
tree-sitter-glsl = { git = "https://github.com/theHamsta/tree-sitter-glsl", rev = "2a56fb7bc8bb03a1892b4741279dd0a8758b7fb3" }

View File

@@ -6,6 +6,9 @@ COPY . .
# Compile collab server
ARG CARGO_PROFILE_RELEASE_PANIC=abort
ARG GITHUB_SHA
ENV GITHUB_SHA=$GITHUB_SHA
RUN --mount=type=cache,target=./script/node_modules \
--mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=./target \

View File

@@ -0,0 +1 @@
<svg height="64" viewBox="0 0 128 128" width="64" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><linearGradient id="a" gradientUnits="userSpaceOnUse" x1="0" x2="128" y1="128" y2="0"><stop offset="0" stop-color="#333"/><stop offset="1" stop-color="#5d5d5d"/></linearGradient><path d="m12.239265 30.664279h14.960911c-5.59432 5.460938-7.654216 10.692785-10.342106 18.023379-3.200764 8.729348-.549141 29.987457 3.815534 37.55289 2.943384 5.101853 6.282685 8.994876 8.233522 11.095173h-16.667861zm89.614855 0h13.90661v66.671442h-13.55518c1.31391-1.750328 3.43934-4.534454 5.12085-6.426163 2.32782-2.618784 4.97023-6.978412 4.97023-6.978412l-16.015202-8.133112s-5.48977 11.600331-15.964999 15.964998c-10.475214 4.364666-19.784679-.838179-25.604243-7.530659-5.819578-6.692502-5.82371-22.14014-5.82371-22.14014h60.797524c1.16391-14.839892-2.63216-21.249816-4.66901-25.90547-.91799-2.098266-1.89261-3.810819-3.16287-5.522484zm-38.356164 1.757154c.35429-.01632.731685-.0092 1.104497 0 11.930114.290977 13.053143 12.802122 13.053143 12.802122h-27.311192s2.170772-12.298638 13.153552-12.802122z" fill="url(#a)"/></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -1,7 +1,9 @@
{
"suffixes": {
"Emakefile": "erlang",
"aac": "audio",
"accdb": "storage",
"app.src": "erlang",
"avif": "image",
"bak": "backup",
"bash": "terminal",
@@ -23,6 +25,8 @@
"doc": "document",
"docx": "document",
"eex": "elixir",
"erl": "erlang",
"escript": "erlang",
"eslintrc": "eslint",
"eslintrc.js": "eslint",
"eslintrc.json": "eslint",
@@ -37,17 +41,18 @@
"gif": "image",
"gitattributes": "vcs",
"gitignore": "vcs",
"gitmodules": "vcs",
"gitkeep": "vcs",
"gitmodules": "vcs",
"go": "go",
"h": "code",
"handlebars": "code",
"hbs": "template",
"heex": "elixir",
"heif": "image",
"hrl": "erlang",
"hs": "haskell",
"htm": "template",
"html": "template",
"hs": "haskell",
"ib": "storage",
"ico": "image",
"ini": "settings",
@@ -85,6 +90,7 @@
"psd": "image",
"py": "python",
"rb": "ruby",
"rebar.config": "erlang",
"rkt": "code",
"rs": "rust",
"rtf": "document",
@@ -104,13 +110,15 @@
"txt": "document",
"vue": "vue",
"wav": "audio",
"webp": "image",
"webm": "video",
"webp": "image",
"xls": "document",
"xlsx": "document",
"xml": "template",
"yaml": "settings",
"yml": "settings",
"xrl": "erlang",
"yaml": "yaml",
"yml": "yaml",
"yrl": "erlang",
"zlogin": "terminal",
"zsh": "terminal",
"zsh_aliases": "terminal",
@@ -133,7 +141,7 @@
"icon": "icons/file_icons/folder.svg"
},
"css": {
"icon": "icons/file_icons/css.svg"
"icon": "icons/file_icons/css.svg"
},
"default": {
"icon": "icons/file_icons/file.svg"
@@ -144,6 +152,9 @@
"elixir": {
"icon": "icons/file_icons/elixir.svg"
},
"erlang": {
"icon": "icons/file_icons/erlang.svg"
},
"eslint": {
"icon": "icons/file_icons/eslint.svg"
},
@@ -174,6 +185,9 @@
"php": {
"icon": "icons/file_icons/php.svg"
},
"yaml": {
"icon": "icons/file_icons/yaml.svg"
},
"prettier": {
"icon": "icons/file_icons/prettier.svg"
},

View File

@@ -0,0 +1 @@
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="457px" height="512px"><polygon points="342.0159302,0 457,0 114.9831009,512 0,512 171.0082092,256 0,0 114.9831009,0 228.4997559,169.9342041 "/></svg>

After

Width:  |  Height:  |  Size: 209 B

View File

@@ -42,6 +42,7 @@
"shift-alt-up": "editor::MoveLineUp",
"shift-alt-down": "editor::MoveLineDown",
"cmd-alt-l": "editor::Format",
"shift-f6": "editor::Rename",
"cmd-[": "pane::GoBack",
"cmd-]": "pane::GoForward",
"alt-f7": "editor::FindAllReferences",
@@ -83,7 +84,8 @@
{
"context": "ProjectPanel",
"bindings": {
"enter": "project_panel::Open"
"enter": "project_panel::Open",
"shift-f6": "project_panel::Rename"
}
}
]

View File

@@ -96,6 +96,8 @@
}
}
],
";": "vim::RepeatFind",
",": "vim::RepeatFindReversed",
"ctrl-o": "pane::GoBack",
"ctrl-i": "pane::GoForward",
"ctrl-]": "editor::GoToDefinition",
@@ -333,8 +335,6 @@
],
"*": "vim::MoveToNext",
"#": "vim::MoveToPrev",
";": "vim::RepeatFind",
",": "vim::RepeatFindReversed",
"r": ["vim::PushOperator", "Replace"],
"s": "vim::Substitute",
"shift-s": "vim::SubstituteLine",
@@ -502,5 +502,18 @@
"enter": "vim::SearchSubmit",
"escape": "buffer_search::Dismiss"
}
},
{
"context": "Dock",
"bindings": {
"ctrl-w h": ["workspace::ActivatePaneInDirection", "Left"],
"ctrl-w l": ["workspace::ActivatePaneInDirection", "Right"],
"ctrl-w k": ["workspace::ActivatePaneInDirection", "Up"],
"ctrl-w j": ["workspace::ActivatePaneInDirection", "Down"],
"ctrl-w ctrl-h": ["workspace::ActivatePaneInDirection", "Left"],
"ctrl-w ctrl-l": ["workspace::ActivatePaneInDirection", "Right"],
"ctrl-w ctrl-k": ["workspace::ActivatePaneInDirection", "Up"],
"ctrl-w ctrl-j": ["workspace::ActivatePaneInDirection", "Down"]
}
}
]

View File

@@ -1,5 +1,5 @@
// Folder-specific settings
//
// For a full list of overridable settings, and general information on folder-specific settings,
// see the documentation: https://docs.zed.dev/configuration/configuring-zed#folder-specific-settings
// see the documentation: https://zed.dev/docs/configuring-zed#folder-specific-settings
{}

View File

@@ -199,9 +199,13 @@ impl AssistantPanel {
.update(cx, |toolbar, cx| toolbar.focus_changed(true, cx));
cx.notify();
if self.focus_handle.is_focused(cx) {
if let Some(editor) = self.active_editor() {
cx.focus_view(editor);
} else if let Some(api_key_editor) = self.api_key_editor.as_ref() {
if self.has_credentials() {
if let Some(editor) = self.active_editor() {
cx.focus_view(editor);
}
}
if let Some(api_key_editor) = self.api_key_editor.as_ref() {
cx.focus_view(api_key_editor);
}
}
@@ -777,6 +781,10 @@ impl AssistantPanel {
});
}
fn build_api_key_editor(&mut self, cx: &mut WindowContext<'_>) {
self.api_key_editor = Some(build_api_key_editor(cx));
}
fn new_conversation(&mut self, cx: &mut ViewContext<Self>) -> View<ConversationEditor> {
let editor = cx.new_view(|cx| {
ConversationEditor::new(
@@ -870,7 +878,7 @@ impl AssistantPanel {
cx.update(|cx| completion_provider.delete_credentials(cx))?
.await;
this.update(&mut cx, |this, cx| {
this.api_key_editor = Some(build_api_key_editor(cx));
this.build_api_key_editor(cx);
this.focus_handle.focus(cx);
cx.notify();
})
@@ -1136,7 +1144,7 @@ impl AssistantPanel {
}
}
fn build_api_key_editor(cx: &mut ViewContext<AssistantPanel>) -> View<Editor> {
fn build_api_key_editor(cx: &mut WindowContext) -> View<Editor> {
cx.new_view(|cx| {
let mut editor = Editor::single_line(cx);
editor.set_placeholder_text("sk-000000000000000000000000000000000000000000000000", cx);
@@ -1147,9 +1155,10 @@ fn build_api_key_editor(cx: &mut ViewContext<AssistantPanel>) -> View<Editor> {
impl Render for AssistantPanel {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
if let Some(api_key_editor) = self.api_key_editor.clone() {
const INSTRUCTIONS: [&'static str; 5] = [
const INSTRUCTIONS: [&'static str; 6] = [
"To use the assistant panel or inline assistant, you need to add your OpenAI API key.",
" - You can create an API key at: platform.openai.com/api-keys",
" - Make sure your OpenAI account has credits",
" - Having a subscription for another service like GitHub Copilot won't work.",
" ",
"Paste your OpenAI API key and press Enter to use the assistant:"
@@ -1342,7 +1351,9 @@ impl Panel for AssistantPanel {
cx.spawn(|this, mut cx| async move {
load_credentials.await;
this.update(&mut cx, |this, cx| {
if this.editors.is_empty() {
if !this.has_credentials() {
this.build_api_key_editor(cx);
} else if this.editors.is_empty() {
this.new_conversation(cx);
}
})

View File

@@ -1,7 +1,7 @@
mod update_notification;
use anyhow::{anyhow, Context, Result};
use client::{Client, TelemetrySettings, ZED_APP_PATH, ZED_APP_VERSION};
use client::{Client, TelemetrySettings, ZED_APP_PATH};
use db::kvp::KEY_VALUE_STORE;
use db::RELEASE_CHANNEL;
use gpui::{
@@ -108,29 +108,28 @@ pub fn init(http_client: Arc<ZedHttpClient>, cx: &mut AppContext) {
})
.detach();
if let Some(version) = ZED_APP_VERSION.or_else(|| cx.app_metadata().app_version) {
let auto_updater = cx.new_model(|cx| {
let updater = AutoUpdater::new(version, http_client);
let version = release_channel::AppVersion::global(cx);
let auto_updater = cx.new_model(|cx| {
let updater = AutoUpdater::new(version, http_client);
let mut update_subscription = AutoUpdateSetting::get_global(cx)
.0
.then(|| updater.start_polling(cx));
let mut update_subscription = AutoUpdateSetting::get_global(cx)
.0
.then(|| updater.start_polling(cx));
cx.observe_global::<SettingsStore>(move |updater, cx| {
if AutoUpdateSetting::get_global(cx).0 {
if update_subscription.is_none() {
update_subscription = Some(updater.start_polling(cx))
}
} else {
update_subscription.take();
cx.observe_global::<SettingsStore>(move |updater, cx| {
if AutoUpdateSetting::get_global(cx).0 {
if update_subscription.is_none() {
update_subscription = Some(updater.start_polling(cx))
}
})
.detach();
} else {
update_subscription.take();
}
})
.detach();
updater
});
cx.set_global(GlobalAutoUpdate(Some(auto_updater)));
}
updater
});
cx.set_global(GlobalAutoUpdate(Some(auto_updater)));
}
pub fn check(_: &Check, cx: &mut WindowContext) {

View File

@@ -74,11 +74,19 @@ impl Channel {
pub fn link(&self) -> String {
RELEASE_CHANNEL.link_prefix().to_owned()
+ "channel/"
+ &self.slug()
+ &Self::slug(&self.name)
+ "-"
+ &self.id.to_string()
}
pub fn notes_link(&self, heading: Option<String>) -> String {
self.link()
+ "/notes"
+ &heading
.map(|h| format!("#{}", Self::slug(&h)))
.unwrap_or_default()
}
pub fn is_root_channel(&self) -> bool {
self.parent_path.is_empty()
}
@@ -90,9 +98,8 @@ impl Channel {
.unwrap_or(self.id)
}
pub fn slug(&self) -> String {
let slug: String = self
.name
pub fn slug(str: &str) -> String {
let slug: String = str
.chars()
.map(|c| if c.is_alphanumeric() { c } else { '-' })
.collect();

View File

@@ -329,6 +329,7 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
fn init_test(cx: &mut AppContext) -> Model<ChannelStore> {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
release_channel::init("0.0.0", cx);
client::init_settings(cx);
let http = FakeHttpClient::with_404_response();

View File

@@ -32,6 +32,7 @@ futures.workspace = true
image = "0.23"
lazy_static.workspace = true
log.workspace = true
once_cell = "1.19.0"
parking_lot.workspace = true
postage.workspace = true
rand.workspace = true
@@ -39,6 +40,7 @@ schemars.workspace = true
serde.workspace = true
serde_derive.workspace = true
serde_json.workspace = true
sha2 = "0.10"
smol.workspace = true
sysinfo.workspace = true
tempfile.workspace = true

View File

@@ -15,14 +15,13 @@ use futures::{
TryFutureExt as _, TryStreamExt,
};
use gpui::{
actions, AnyModel, AnyWeakModel, AppContext, AsyncAppContext, Global, Model, SemanticVersion,
Task, WeakModel,
actions, AnyModel, AnyWeakModel, AppContext, AsyncAppContext, Global, Model, Task, WeakModel,
};
use lazy_static::lazy_static;
use parking_lot::RwLock;
use postage::watch;
use rand::prelude::*;
use release_channel::ReleaseChannel;
use release_channel::{AppVersion, ReleaseChannel};
use rpc::proto::{AnyTypedEnvelope, EntityMessage, EnvelopedMessage, PeerId, RequestMessage};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
@@ -58,9 +57,6 @@ lazy_static! {
pub static ref ADMIN_API_TOKEN: Option<String> = std::env::var("ZED_ADMIN_API_TOKEN")
.ok()
.and_then(|s| if s.is_empty() { None } else { Some(s) });
pub static ref ZED_APP_VERSION: Option<SemanticVersion> = std::env::var("ZED_APP_VERSION")
.ok()
.and_then(|v| v.parse().ok());
pub static ref ZED_APP_PATH: Option<PathBuf> =
std::env::var("ZED_APP_PATH").ok().map(PathBuf::from);
pub static ref ZED_ALWAYS_ACTIVE: bool =
@@ -1011,13 +1007,22 @@ impl Client {
.update(|cx| ReleaseChannel::try_global(cx))
.ok()
.flatten();
let app_version = cx
.update(|cx| AppVersion::global(cx).to_string())
.ok()
.unwrap_or_default();
let request = Request::builder()
.header(
"Authorization",
format!("{} {}", credentials.user_id, credentials.access_token),
)
.header("x-zed-protocol-version", rpc::PROTOCOL_VERSION);
.header("x-zed-protocol-version", rpc::PROTOCOL_VERSION)
.header("x-zed-app-version", app_version)
.header(
"x-zed-release-channel",
release_channel.map(|r| r.dev_name()).unwrap_or("unknown"),
);
let http = self.http.clone();
cx.background_executor().spawn(async move {

View File

@@ -4,16 +4,19 @@ use crate::TelemetrySettings;
use chrono::{DateTime, Utc};
use futures::Future;
use gpui::{AppContext, AppMetadata, BackgroundExecutor, Task};
use once_cell::sync::Lazy;
use parking_lot::Mutex;
use release_channel::ReleaseChannel;
use serde::Serialize;
use settings::{Settings, SettingsStore};
use std::{env, io::Write, mem, path::PathBuf, sync::Arc, time::Duration};
use sha2::{Digest, Sha256};
use std::io::Write;
use std::{env, mem, path::PathBuf, sync::Arc, time::Duration};
use sysinfo::{
CpuRefreshKind, Pid, PidExt, ProcessExt, ProcessRefreshKind, RefreshKind, System, SystemExt,
};
use tempfile::NamedTempFile;
use util::http::{HttpClient, ZedHttpClient};
use util::http::{self, HttpClient, Method, ZedHttpClient};
#[cfg(not(debug_assertions))]
use util::ResultExt;
use util::TryFutureExt;
@@ -142,6 +145,13 @@ const FLUSH_INTERVAL: Duration = Duration::from_secs(1);
#[cfg(not(debug_assertions))]
const FLUSH_INTERVAL: Duration = Duration::from_secs(60 * 5);
static ZED_CLIENT_CHECKSUM_SEED: Lazy<Vec<u8>> = Lazy::new(|| {
option_env!("ZED_CLIENT_CHECKSUM_SEED")
.unwrap_or("development-checksum-seed")
.as_bytes()
.into()
});
impl Telemetry {
pub fn new(client: Arc<ZedHttpClient>, cx: &mut AppContext) -> Arc<Self> {
let release_channel =
@@ -540,9 +550,27 @@ impl Telemetry {
serde_json::to_writer(&mut json_bytes, &request_body)?;
}
this.http_client
.post_json(&this.http_client.zed_url("/api/events"), json_bytes.into())
.await?;
let mut summer = Sha256::new();
summer.update(&*ZED_CLIENT_CHECKSUM_SEED);
summer.update(&json_bytes);
summer.update(&*ZED_CLIENT_CHECKSUM_SEED);
let mut checksum = String::new();
for byte in summer.finalize().as_slice() {
use std::fmt::Write;
write!(&mut checksum, "{:02x}", byte).unwrap();
}
let request = http::Request::builder()
.method(Method::POST)
.uri(&this.http_client.zed_url("/api/events"))
.header("Content-Type", "text/plain")
.header("x-zed-checksum", checksum)
.body(json_bytes.into());
let response = this.http_client.send(request?).await?;
if response.status() != 200 {
log::error!("Failed to send events: HTTP {:?}", response.status());
}
anyhow::Ok(())
}
.log_err(),

View File

@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"]
default-run = "collab"
edition = "2021"
name = "collab"
version = "0.43.0"
version = "0.44.0"
publish = false
license = "AGPL-3.0-or-later"
@@ -61,6 +61,7 @@ util = { path = "../util" }
uuid.workspace = true
[dev-dependencies]
release_channel = { path = "../release_channel" }
async-trait.workspace = true
audio = { path = "../audio" }
call = { path = "../call", features = ["test-support"] }

View File

@@ -3,3 +3,35 @@
This crate is what we run at https://collab.zed.dev.
It contains our back-end logic for collaboration, to which we connect from the Zed client via a websocket after authenticating via https://zed.dev, which is a separate repo running on Vercel.
# Local Development
Detailed instructions on getting started are [here](https://zed.dev/docs/local-collaboration).
# Deployment
We run two instances of collab:
* Staging (https://staging-collab.zed.dev)
* Production (https://collab.zed.dev)
Both of these run on the Kubernetes cluster hosted in Digital Ocean.
Deployment is triggered by pushing to the `collab-staging` (or `collab-production`) tag in Github. The best way to do this is:
* `./script/deploy-collab staging`
* `./script/deploy-collab production`
You can tell what is currently deployed with `./script/what-is-deployed`.
# Database Migrations
To create a new migration:
```
./script/sqlx migrate add <name>
```
Migrations are run automatically on service start, so run `foreman start` again. The service will crash if the migrations fail.
When you create a new migration, you also need to update the [SQLite schema](./migrations.sqlite/20221109000000_test_schema.sql) that is used for testing.

View File

@@ -14,6 +14,7 @@ use tracing_subscriber::{filter::EnvFilter, fmt::format::JsonFields, Layer};
use util::ResultExt;
const VERSION: &'static str = env!("CARGO_PKG_VERSION");
const REVISION: Option<&'static str> = option_env!("GITHUB_SHA");
#[tokio::main]
async fn main() -> Result<()> {
@@ -26,7 +27,7 @@ async fn main() -> Result<()> {
match args().skip(1).next().as_deref() {
Some("version") => {
println!("collab v{VERSION}");
println!("collab v{} ({})", VERSION, REVISION.unwrap_or("unknown"));
}
Some("migrate") => {
run_migrations().await?;
@@ -105,7 +106,7 @@ async fn run_migrations() -> Result<()> {
}
async fn handle_root() -> String {
format!("collab v{VERSION}")
format!("collab v{} ({})", VERSION, REVISION.unwrap_or("unknown"))
}
async fn handle_liveness_probe(Extension(state): Extension<Arc<AppState>>) -> Result<String> {

View File

@@ -64,6 +64,7 @@ use time::OffsetDateTime;
use tokio::sync::{watch, Semaphore};
use tower::ServiceBuilder;
use tracing::{field, info_span, instrument, Instrument};
use util::SemanticVersion;
pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30);
pub const CLEANUP_TIMEOUT: Duration = Duration::from_secs(10);
@@ -795,6 +796,7 @@ fn broadcast<F>(
lazy_static! {
static ref ZED_PROTOCOL_VERSION: HeaderName = HeaderName::from_static("x-zed-protocol-version");
static ref ZED_APP_VERSION: HeaderName = HeaderName::from_static("x-zed-app-version");
}
pub struct ProtocolVersion(u32);
@@ -824,6 +826,32 @@ impl Header for ProtocolVersion {
}
}
pub struct AppVersionHeader(SemanticVersion);
impl Header for AppVersionHeader {
fn name() -> &'static HeaderName {
&ZED_APP_VERSION
}
fn decode<'i, I>(values: &mut I) -> Result<Self, axum::headers::Error>
where
Self: Sized,
I: Iterator<Item = &'i axum::http::HeaderValue>,
{
let version = values
.next()
.ok_or_else(axum::headers::Error::invalid)?
.to_str()
.map_err(|_| axum::headers::Error::invalid())?
.parse()
.map_err(|_| axum::headers::Error::invalid())?;
Ok(Self(version))
}
fn encode<E: Extend<axum::http::HeaderValue>>(&self, values: &mut E) {
values.extend([self.0.to_string().parse().unwrap()]);
}
}
pub fn routes(server: Arc<Server>) -> Router<Body> {
Router::new()
.route("/rpc", get(handle_websocket_request))
@@ -838,6 +866,7 @@ pub fn routes(server: Arc<Server>) -> Router<Body> {
pub async fn handle_websocket_request(
TypedHeader(ProtocolVersion(protocol_version)): TypedHeader<ProtocolVersion>,
_app_version_header: Option<TypedHeader<AppVersionHeader>>,
ConnectInfo(socket_address): ConnectInfo<SocketAddr>,
Extension(server): Extension<Arc<Server>>,
Extension(user): Extension<User>,
@@ -851,6 +880,7 @@ pub async fn handle_websocket_request(
)
.into_response();
}
let socket_address = socket_address.to_string();
ws.on_upgrade(move |socket| {
use util::ResultExt;

View File

@@ -161,15 +161,15 @@ async fn test_channel_notes_participant_indices(
// Clients A, B, and C open the channel notes
let channel_view_a = cx_a
.update(|cx| ChannelView::open(channel_id, workspace_a.clone(), cx))
.update(|cx| ChannelView::open(channel_id, None, workspace_a.clone(), cx))
.await
.unwrap();
let channel_view_b = cx_b
.update(|cx| ChannelView::open(channel_id, workspace_b.clone(), cx))
.update(|cx| ChannelView::open(channel_id, None, workspace_b.clone(), cx))
.await
.unwrap();
let channel_view_c = cx_c
.update(|cx| ChannelView::open(channel_id, workspace_c.clone(), cx))
.update(|cx| ChannelView::open(channel_id, None, workspace_c.clone(), cx))
.await
.unwrap();
@@ -644,7 +644,7 @@ async fn test_channel_buffer_changes(
let project_b = client_b.build_empty_local_project(cx_b);
let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
let channel_view_b = cx_b
.update(|cx| ChannelView::open(channel_id, workspace_b.clone(), cx))
.update(|cx| ChannelView::open(channel_id, None, workspace_b.clone(), cx))
.await
.unwrap();
deterministic.run_until_parked();

View File

@@ -1905,7 +1905,7 @@ async fn test_following_to_channel_notes_without_a_shared_project(
// Client A opens the notes for channel 1.
let channel_notes_1_a = cx_a
.update(|cx| ChannelView::open(channel_1_id, workspace_a.clone(), cx))
.update(|cx| ChannelView::open(channel_1_id, None, workspace_a.clone(), cx))
.await
.unwrap();
channel_notes_1_a.update(cx_a, |notes, cx| {
@@ -1951,7 +1951,7 @@ async fn test_following_to_channel_notes_without_a_shared_project(
// Client A opens the notes for channel 2.
let channel_notes_2_a = cx_a
.update(|cx| ChannelView::open(channel_2_id, workspace_a.clone(), cx))
.update(|cx| ChannelView::open(channel_2_id, None, workspace_a.clone(), cx))
.await
.unwrap();
channel_notes_2_a.update(cx_a, |notes, cx| {

View File

@@ -153,6 +153,7 @@ impl TestServer {
}
let settings = SettingsStore::test(cx);
cx.set_global(settings);
release_channel::init("0.0.0", cx);
client::init_settings(cx);
});

View File

@@ -6,11 +6,14 @@ use client::{
Collaborator, ParticipantIndex,
};
use collections::HashMap;
use editor::{CollaborationHub, Editor, EditorEvent};
use editor::{
display_map::ToDisplayPoint, scroll::Autoscroll, CollaborationHub, DisplayPoint, Editor,
EditorEvent,
};
use gpui::{
actions, AnyElement, AnyView, AppContext, Entity as _, EventEmitter, FocusableView,
IntoElement as _, Model, Pixels, Point, Render, Subscription, Task, View, ViewContext,
VisualContext as _, WindowContext,
actions, AnyElement, AnyView, AppContext, ClipboardItem, Entity as _, EventEmitter,
FocusableView, IntoElement as _, Model, Pixels, Point, Render, Subscription, Task, View,
ViewContext, VisualContext as _, WeakView, WindowContext,
};
use project::Project;
use std::{
@@ -23,10 +26,10 @@ use workspace::{
item::{FollowableItem, Item, ItemEvent, ItemHandle},
register_followable_item,
searchable::SearchableItemHandle,
ItemNavHistory, Pane, SaveIntent, ViewId, Workspace, WorkspaceId,
ItemNavHistory, Pane, SaveIntent, Toast, ViewId, Workspace, WorkspaceId,
};
actions!(collab, [Deploy]);
actions!(collab, [CopyLink]);
pub fn init(cx: &mut AppContext) {
register_followable_item::<ChannelView>(cx)
@@ -34,21 +37,30 @@ pub fn init(cx: &mut AppContext) {
pub struct ChannelView {
pub editor: View<Editor>,
workspace: WeakView<Workspace>,
project: Model<Project>,
channel_store: Model<ChannelStore>,
channel_buffer: Model<ChannelBuffer>,
remote_id: Option<ViewId>,
_editor_event_subscription: Subscription,
_reparse_subscription: Option<Subscription>,
}
impl ChannelView {
pub fn open(
channel_id: ChannelId,
link_position: Option<String>,
workspace: View<Workspace>,
cx: &mut WindowContext,
) -> Task<Result<View<Self>>> {
let pane = workspace.read(cx).active_pane().clone();
let channel_view = Self::open_in_pane(channel_id, pane.clone(), workspace.clone(), cx);
let channel_view = Self::open_in_pane(
channel_id,
link_position,
pane.clone(),
workspace.clone(),
cx,
);
cx.spawn(|mut cx| async move {
let channel_view = channel_view.await?;
pane.update(&mut cx, |pane, cx| {
@@ -66,10 +78,12 @@ impl ChannelView {
pub fn open_in_pane(
channel_id: ChannelId,
link_position: Option<String>,
pane: View<Pane>,
workspace: View<Workspace>,
cx: &mut WindowContext,
) -> Task<Result<View<Self>>> {
let weak_workspace = workspace.downgrade();
let workspace = workspace.read(cx);
let project = workspace.project().to_owned();
let channel_store = ChannelStore::global(cx);
@@ -82,12 +96,13 @@ impl ChannelView {
let channel_buffer = channel_buffer.await?;
let markdown = markdown.await.log_err();
channel_buffer.update(&mut cx, |buffer, cx| {
buffer.buffer().update(cx, |buffer, cx| {
channel_buffer.update(&mut cx, |channel_buffer, cx| {
channel_buffer.buffer().update(cx, |buffer, cx| {
buffer.set_language_registry(language_registry);
if let Some(markdown) = markdown {
buffer.set_language(Some(markdown), cx);
}
let Some(markdown) = markdown else {
return;
};
buffer.set_language(Some(markdown), cx);
})
})?;
@@ -101,12 +116,18 @@ impl ChannelView {
// If this channel buffer is already open in this pane, just return it.
if let Some(existing_view) = existing_view.clone() {
if existing_view.read(cx).channel_buffer == channel_buffer {
if let Some(link_position) = link_position {
existing_view.update(cx, |channel_view, cx| {
channel_view.focus_position_from_link(link_position, true, cx)
});
}
return existing_view;
}
}
let view = cx.new_view(|cx| {
let mut this = Self::new(project, channel_store, channel_buffer, cx);
let mut this =
Self::new(project, weak_workspace, channel_store, channel_buffer, cx);
this.acknowledge_buffer_version(cx);
this
});
@@ -121,6 +142,12 @@ impl ChannelView {
}
}
if let Some(link_position) = link_position {
view.update(cx, |channel_view, cx| {
channel_view.focus_position_from_link(link_position, true, cx)
});
}
view
})
})
@@ -128,16 +155,29 @@ impl ChannelView {
pub fn new(
project: Model<Project>,
workspace: WeakView<Workspace>,
channel_store: Model<ChannelStore>,
channel_buffer: Model<ChannelBuffer>,
cx: &mut ViewContext<Self>,
) -> Self {
let buffer = channel_buffer.read(cx).buffer();
let this = cx.view().downgrade();
let editor = cx.new_view(|cx| {
let mut editor = Editor::for_buffer(buffer, None, cx);
editor.set_collaboration_hub(Box::new(ChannelBufferCollaborationHub(
channel_buffer.clone(),
)));
editor.set_custom_context_menu(move |_, position, cx| {
let this = this.clone();
Some(ui::ContextMenu::build(cx, move |menu, _| {
menu.entry("Copy link to section", None, move |cx| {
this.update(cx, |this, cx| {
this.copy_link_for_position(position.clone(), cx)
})
.ok();
})
}))
});
editor
});
let _editor_event_subscription =
@@ -148,14 +188,94 @@ impl ChannelView {
Self {
editor,
workspace,
project,
channel_store,
channel_buffer,
remote_id: None,
_editor_event_subscription,
_reparse_subscription: None,
}
}
fn focus_position_from_link(
&mut self,
position: String,
first_attempt: bool,
cx: &mut ViewContext<Self>,
) {
let position = Channel::slug(&position).to_lowercase();
let snapshot = self.editor.update(cx, |editor, cx| editor.snapshot(cx));
if let Some(outline) = snapshot.buffer_snapshot.outline(None) {
if let Some(item) = outline
.items
.iter()
.find(|item| &Channel::slug(&item.text).to_lowercase() == &position)
{
self.editor.update(cx, |editor, cx| {
editor.change_selections(Some(Autoscroll::focused()), cx, |s| {
s.replace_cursors_with(|map| vec![item.range.start.to_display_point(&map)])
})
});
return;
}
}
if !first_attempt {
return;
}
self._reparse_subscription = Some(cx.subscribe(
&self.editor,
move |this, _, e: &EditorEvent, cx| {
match e {
EditorEvent::Reparsed => {
this.focus_position_from_link(position.clone(), false, cx);
this._reparse_subscription.take();
}
EditorEvent::Edited | EditorEvent::SelectionsChanged { local: true } => {
this._reparse_subscription.take();
}
_ => {}
};
},
));
}
fn copy_link(&mut self, _: &CopyLink, cx: &mut ViewContext<Self>) {
let position = self
.editor
.update(cx, |editor, cx| editor.selections.newest_display(cx).start);
self.copy_link_for_position(position, cx)
}
fn copy_link_for_position(&self, position: DisplayPoint, cx: &mut ViewContext<Self>) {
let snapshot = self.editor.update(cx, |editor, cx| editor.snapshot(cx));
let mut closest_heading = None;
if let Some(outline) = snapshot.buffer_snapshot.outline(None) {
for item in outline.items {
if item.range.start.to_display_point(&snapshot) > position {
break;
}
closest_heading = Some(item);
}
}
let Some(channel) = self.channel(cx) else {
return;
};
let link = channel.notes_link(closest_heading.map(|heading| heading.text));
cx.write_to_clipboard(ClipboardItem::new(link));
self.workspace
.update(cx, |workspace, cx| {
workspace.show_toast(Toast::new(0, "Link copied to clipboard"), cx);
})
.ok();
}
pub fn channel(&self, cx: &AppContext) -> Option<Arc<Channel>> {
self.channel_buffer.read(cx).channel(cx)
}
@@ -215,8 +335,11 @@ impl ChannelView {
impl EventEmitter<EditorEvent> for ChannelView {}
impl Render for ChannelView {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
self.editor.clone()
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
div()
.size_full()
.on_action(cx.listener(Self::copy_link))
.child(self.editor.clone())
}
}
@@ -274,6 +397,7 @@ impl Item for ChannelView {
Some(cx.new_view(|cx| {
Self::new(
self.project.clone(),
self.workspace.clone(),
self.channel_store.clone(),
self.channel_buffer.clone(),
cx,
@@ -356,7 +480,7 @@ impl FollowableItem for ChannelView {
unreachable!()
};
let open = ChannelView::open_in_pane(state.channel_id, pane, workspace, cx);
let open = ChannelView::open_in_pane(state.channel_id, None, pane, workspace, cx);
Some(cx.spawn(|mut cx| async move {
let this = open.await?;

View File

@@ -453,7 +453,7 @@ impl ChatPanel {
})
.collect::<Vec<_>>();
rich_text::render_markdown(message.body.clone(), &mentions, language_registry, None)
rich_text::render_rich_text(message.body.clone(), &mentions, language_registry, None)
}
fn send(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {

View File

@@ -1678,7 +1678,7 @@ impl CollabPanel {
fn open_channel_notes(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
if let Some(workspace) = self.workspace.upgrade() {
ChannelView::open(channel_id, workspace, cx).detach();
ChannelView::open(channel_id, None, workspace, cx).detach();
}
}

View File

@@ -14,13 +14,13 @@ pub use collab_panel::CollabPanel;
pub use collab_titlebar_item::CollabTitlebarItem;
use gpui::{
actions, point, AppContext, GlobalPixels, Pixels, PlatformDisplay, Size, Task, WindowBounds,
WindowKind, WindowOptions,
WindowContext, WindowKind, WindowOptions,
};
pub use panel_settings::{
ChatPanelSettings, CollaborationPanelSettings, NotificationPanelSettings,
};
use settings::Settings;
use workspace::AppState;
use workspace::{notifications::DetachAndPromptErr, AppState};
actions!(
collab,
@@ -41,7 +41,7 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
notifications::init(&app_state, cx);
}
pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut AppContext) {
pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut WindowContext) {
let call = ActiveCall::global(cx).read(cx);
if let Some(room) = call.room().cloned() {
let client = call.client();
@@ -64,7 +64,7 @@ pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut AppContext) {
room.share_screen(cx)
}
});
toggle_screen_sharing.detach_and_log_err(cx);
toggle_screen_sharing.detach_and_prompt_err("Sharing Screen Failed", cx, |e, _| Some(format!("{:?}\n\nPlease check that you have given Zed permissions to record your screen in Settings.", e)));
}
}

View File

@@ -445,7 +445,7 @@ impl Copilot {
)
.detach();
let server = server.initialize(Default::default()).await?;
let server = cx.update(|cx| server.initialize(None, cx))?.await?;
let status = server
.request::<request::CheckStatus>(request::CheckStatusParams {

View File

@@ -79,6 +79,7 @@ language = { path = "../language", features = ["test-support"] }
lsp = { path = "../lsp", features = ["test-support"] }
multi_buffer = { path = "../multi_buffer", features = ["test-support"] }
project = { path = "../project", features = ["test-support"] }
release_channel = { path = "../release_channel" }
rand.workspace = true
settings = { path = "../settings", features = ["test-support"] }
text = { path = "../text", features = ["test-support"] }

View File

@@ -74,7 +74,7 @@ use language::{
language_settings::{self, all_language_settings, InlayHintSettings},
markdown, point_from_lsp, AutoindentMode, BracketPair, Buffer, Capability, CodeAction,
CodeLabel, Completion, CursorShape, Diagnostic, Documentation, IndentKind, IndentSize,
Language, LanguageServerName, OffsetRangeExt, Point, Selection, SelectionGoal, TransactionId,
Language, OffsetRangeExt, Point, Selection, SelectionGoal, TransactionId,
};
use link_go_to_definition::{GoToDefinitionLink, InlayHighlight, LinkGoToDefinitionState};
@@ -413,6 +413,12 @@ pub struct Editor {
editor_actions: Vec<Box<dyn Fn(&mut ViewContext<Self>)>>,
show_copilot_suggestions: bool,
use_autoclose: bool,
custom_context_menu: Option<
Box<
dyn 'static
+ Fn(&mut Self, DisplayPoint, &mut ViewContext<Self>) -> Option<View<ui::ContextMenu>>,
>,
>,
}
pub struct EditorSnapshot {
@@ -1476,6 +1482,7 @@ impl Editor {
hovered_cursors: Default::default(),
editor_actions: Default::default(),
show_copilot_suggestions: mode == EditorMode::Full,
custom_context_menu: None,
_subscriptions: vec![
cx.observe(&buffer, Self::on_buffer_changed),
cx.subscribe(&buffer, Self::on_buffer_event),
@@ -1665,6 +1672,14 @@ impl Editor {
self.collaboration_hub = Some(hub);
}
pub fn set_custom_context_menu(
&mut self,
f: impl 'static
+ Fn(&mut Self, DisplayPoint, &mut ViewContext<Self>) -> Option<View<ui::ContextMenu>>,
) {
self.custom_context_menu = Some(Box::new(f))
}
pub fn set_completion_provider(&mut self, hub: Box<dyn CompletionProvider>) {
self.completion_provider = Some(hub);
}
@@ -7289,9 +7304,7 @@ impl Editor {
editor.buffer.read(cx).as_singleton().and_then(|buffer| {
project
.language_server_for_buffer(buffer.read(cx), server_id, cx)
.map(|(_, lsp_adapter)| {
LanguageServerName(Arc::from(lsp_adapter.name()))
})
.map(|(lsp_adapter, _)| lsp_adapter.name.clone())
});
language_server_name.map(|language_server_name| {
project.open_local_buffer_via_lsp(

View File

@@ -8392,6 +8392,7 @@ pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsC
let store = SettingsStore::test(cx);
cx.set_global(store);
theme::init(theme::LoadThemes::JustBase, cx);
release_channel::init("0.0.0", cx);
client::init_settings(cx);
language::init(cx);
Project::init_settings(cx);

View File

@@ -3216,6 +3216,7 @@ pub mod tests {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
theme::init(theme::LoadThemes::JustBase, cx);
release_channel::init("0.0.0", cx);
client::init_settings(cx);
language::init(cx);
Project::init_settings(cx);

View File

@@ -25,31 +25,40 @@ pub fn deploy_context_menu(
return;
}
// Don't show the context menu if there isn't a project associated with this editor
if editor.project.is_none() {
return;
}
let context_menu = if let Some(custom) = editor.custom_context_menu.take() {
let menu = custom(editor, point, cx);
editor.custom_context_menu = Some(custom);
if menu.is_none() {
return;
}
menu.unwrap()
} else {
// Don't show the context menu if there isn't a project associated with this editor
if editor.project.is_none() {
return;
}
// Move the cursor to the clicked location so that dispatched actions make sense
editor.change_selections(None, cx, |s| {
s.clear_disjoint();
s.set_pending_display_range(point..point, SelectMode::Character);
});
// Move the cursor to the clicked location so that dispatched actions make sense
editor.change_selections(None, cx, |s| {
s.clear_disjoint();
s.set_pending_display_range(point..point, SelectMode::Character);
});
let context_menu = ui::ContextMenu::build(cx, |menu, _cx| {
menu.action("Rename Symbol", Box::new(Rename))
.action("Go to Definition", Box::new(GoToDefinition))
.action("Go to Type Definition", Box::new(GoToTypeDefinition))
.action("Find All References", Box::new(FindAllReferences))
.action(
"Code Actions",
Box::new(ToggleCodeActions {
deployed_from_indicator: false,
}),
)
.separator()
.action("Reveal in Finder", Box::new(RevealInFinder))
});
ui::ContextMenu::build(cx, |menu, _cx| {
menu.action("Rename Symbol", Box::new(Rename))
.action("Go to Definition", Box::new(GoToDefinition))
.action("Go to Type Definition", Box::new(GoToTypeDefinition))
.action("Find All References", Box::new(FindAllReferences))
.action(
"Code Actions",
Box::new(ToggleCodeActions {
deployed_from_indicator: false,
}),
)
.separator()
.action("Reveal in Finder", Box::new(RevealInFinder))
})
};
let context_menu_focus = context_menu.focus_handle(cx);
cx.focus(&context_menu_focus);

View File

@@ -12,17 +12,26 @@ pub enum Autoscroll {
}
impl Autoscroll {
/// scrolls the minimal amount to (try) and fit all cursors onscreen
pub fn fit() -> Self {
Self::Strategy(AutoscrollStrategy::Fit)
}
/// scrolls the minimal amount to fit the newest cursor
pub fn newest() -> Self {
Self::Strategy(AutoscrollStrategy::Newest)
}
/// scrolls so the newest cursor is vertically centered
pub fn center() -> Self {
Self::Strategy(AutoscrollStrategy::Center)
}
/// scrolls so the neweset cursor is near the top
/// (offset by vertical_scroll_margin)
pub fn focused() -> Self {
Self::Strategy(AutoscrollStrategy::Focused)
}
}
#[derive(PartialEq, Eq, Default, Clone, Copy)]
@@ -31,6 +40,7 @@ pub enum AutoscrollStrategy {
Newest,
#[default]
Center,
Focused,
Top,
Bottom,
}
@@ -155,6 +165,11 @@ impl Editor {
scroll_position.y = (target_top - margin).max(0.0);
self.set_scroll_position_internal(scroll_position, local, true, cx);
}
AutoscrollStrategy::Focused => {
scroll_position.y =
(target_top - self.scroll_manager.vertical_scroll_margin).max(0.0);
self.set_scroll_position_internal(scroll_position, local, true, cx);
}
AutoscrollStrategy::Top => {
scroll_position.y = (target_top).max(0.0);
self.set_scroll_position_internal(scroll_position, local, true, cx);

View File

@@ -1,14 +1,13 @@
use client::ZED_APP_VERSION;
use gpui::AppContext;
use human_bytes::human_bytes;
use release_channel::ReleaseChannel;
use release_channel::{AppVersion, ReleaseChannel};
use serde::Serialize;
use std::{env, fmt::Display};
use sysinfo::{RefreshKind, System, SystemExt};
#[derive(Clone, Debug, Serialize)]
pub struct SystemSpecs {
app_version: Option<String>,
app_version: String,
release_channel: &'static str,
os_name: &'static str,
os_version: Option<String>,
@@ -18,9 +17,7 @@ pub struct SystemSpecs {
impl SystemSpecs {
pub fn new(cx: &AppContext) -> Self {
let app_version = ZED_APP_VERSION
.or_else(|| cx.app_metadata().app_version)
.map(|v| v.to_string());
let app_version = AppVersion::global(cx).to_string();
let release_channel = ReleaseChannel::global(cx).display_name();
let os_name = cx.app_metadata().os_name;
let system = System::new_with_specifics(RefreshKind::new().with_memory());
@@ -48,18 +45,15 @@ impl Display for SystemSpecs {
Some(os_version) => format!("OS: {} {}", self.os_name, os_version),
None => format!("OS: {}", self.os_name),
};
let app_version_information = self
.app_version
.as_ref()
.map(|app_version| format!("Zed: v{} ({})", app_version, self.release_channel));
let app_version_information =
format!("Zed: v{} ({})", self.app_version, self.release_channel);
let system_specs = [
app_version_information,
Some(os_information),
Some(format!("Memory: {}", human_bytes(self.memory as f64))),
Some(format!("Architecture: {}", self.architecture)),
os_information,
format!("Memory: {}", human_bytes(self.memory as f64)),
format!("Architecture: {}", self.architecture),
]
.into_iter()
.flatten()
.collect::<Vec<String>>()
.join("\n");

View File

@@ -15,6 +15,7 @@ collections = { path = "../collections" }
editor = { path = "../editor" }
fuzzy = { path = "../fuzzy" }
gpui = { path = "../gpui" }
itertools = "0.11"
menu = { path = "../menu" }
picker = { path = "../picker" }
postage.workspace = true

View File

@@ -1,13 +1,14 @@
#[cfg(test)]
mod file_finder_tests;
use collections::HashMap;
use collections::{HashMap, HashSet};
use editor::{scroll::Autoscroll, Bias, Editor};
use fuzzy::{CharBag, PathMatch, PathMatchCandidate};
use gpui::{
actions, rems, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Model,
ParentElement, Render, Styled, Task, View, ViewContext, VisualContext, WeakView,
};
use itertools::Itertools;
use picker::{Picker, PickerDelegate};
use project::{PathMatchCandidateSet, Project, ProjectPath, WorktreeId};
use std::{
@@ -64,33 +65,15 @@ impl FileFinder {
FoundPath::new(project_path, abs_path)
});
// if exists, bubble the currently opened path to the top
let history_items = currently_opened_path
.clone()
let history_items = workspace
.recent_navigation_history(Some(MAX_RECENT_SELECTIONS), cx)
.into_iter()
.chain(
workspace
.recent_navigation_history(Some(MAX_RECENT_SELECTIONS), cx)
.into_iter()
.filter(|(history_path, _)| {
Some(history_path)
!= currently_opened_path
.as_ref()
.map(|found_path| &found_path.project)
})
.filter(|(_, history_abs_path)| {
history_abs_path.as_ref()
!= currently_opened_path
.as_ref()
.and_then(|found_path| found_path.absolute.as_ref())
})
.filter(|(_, history_abs_path)| match history_abs_path {
Some(abs_path) => history_file_exists(abs_path),
None => true,
})
.map(|(history_path, abs_path)| FoundPath::new(history_path, abs_path)),
)
.collect();
.filter(|(_, history_abs_path)| match history_abs_path {
Some(abs_path) => history_file_exists(abs_path),
None => true,
})
.map(|(history_path, abs_path)| FoundPath::new(history_path, abs_path))
.collect::<Vec<_>>();
let project = workspace.project().clone();
let weak_workspace = cx.view().downgrade();
@@ -139,7 +122,7 @@ pub struct FileFinderDelegate {
latest_search_query: Option<PathLikeWithPosition<FileSearchQuery>>,
currently_opened_path: Option<FoundPath>,
matches: Matches,
selected_index: Option<usize>,
selected_index: usize,
cancel_flag: Arc<AtomicBool>,
history_items: Vec<FoundPath>,
}
@@ -209,31 +192,21 @@ impl Matches {
fn push_new_matches(
&mut self,
history_items: &Vec<FoundPath>,
currently_opened: Option<&FoundPath>,
query: &PathLikeWithPosition<FileSearchQuery>,
new_search_matches: impl Iterator<Item = ProjectPanelOrdMatch>,
extend_old_matches: bool,
) {
let matching_history_paths = matching_history_item_paths(history_items, query);
let matching_history_paths =
matching_history_item_paths(history_items, currently_opened, query);
let new_search_matches = new_search_matches
.filter(|path_match| !matching_history_paths.contains_key(&path_match.0.path));
let history_items_to_show = history_items.iter().filter_map(|history_item| {
Some((
history_item.clone(),
Some(
matching_history_paths
.get(&history_item.project.path)?
.clone(),
),
))
});
self.history.clear();
util::extend_sorted(
&mut self.history,
history_items_to_show,
100,
|(_, a), (_, b)| b.cmp(a),
);
self.set_new_history(
currently_opened,
Some(&matching_history_paths),
history_items,
);
if extend_old_matches {
self.search
.retain(|path_match| !matching_history_paths.contains_key(&path_match.0.path));
@@ -242,14 +215,52 @@ impl Matches {
}
util::extend_sorted(&mut self.search, new_search_matches, 100, |a, b| b.cmp(a));
}
fn set_new_history<'a>(
&mut self,
currently_opened: Option<&'a FoundPath>,
query_matches: Option<&'a HashMap<Arc<Path>, ProjectPanelOrdMatch>>,
history_items: impl IntoIterator<Item = &'a FoundPath> + 'a,
) {
let mut processed_paths = HashSet::default();
self.history = history_items
.into_iter()
.chain(currently_opened)
.filter(|&path| processed_paths.insert(path))
.filter_map(|history_item| match &query_matches {
Some(query_matches) => Some((
history_item.clone(),
Some(query_matches.get(&history_item.project.path)?.clone()),
)),
None => Some((history_item.clone(), None)),
})
.enumerate()
.sorted_by(
|(index_a, (path_a, match_a)), (index_b, (path_b, match_b))| match (
Some(path_a) == currently_opened,
Some(path_b) == currently_opened,
) {
// bubble currently opened files to the top
(true, false) => cmp::Ordering::Less,
(false, true) => cmp::Ordering::Greater,
// arrange the files by their score (best score on top) and by their occurrence in the history
// (history items visited later are on the top)
_ => match_b.cmp(match_a).then(index_a.cmp(index_b)),
},
)
.map(|(_, paths)| paths)
.collect();
}
}
fn matching_history_item_paths(
history_items: &Vec<FoundPath>,
currently_opened: Option<&FoundPath>,
query: &PathLikeWithPosition<FileSearchQuery>,
) -> HashMap<Arc<Path>, ProjectPanelOrdMatch> {
let history_items_by_worktrees = history_items
.iter()
.chain(currently_opened)
.filter_map(|found_path| {
let candidate = PathMatchCandidate {
path: &found_path.project.path,
@@ -301,7 +312,7 @@ fn matching_history_item_paths(
matching_history_paths
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
struct FoundPath {
project: ProjectPath,
absolute: Option<PathBuf>,
@@ -372,7 +383,7 @@ impl FileFinderDelegate {
latest_search_query: None,
currently_opened_path,
matches: Matches::default(),
selected_index: None,
selected_index: 0,
cancel_flag: Arc::new(AtomicBool::new(false)),
history_items,
}
@@ -427,7 +438,6 @@ impl FileFinderDelegate {
let did_cancel = cancel_flag.load(atomic::Ordering::Relaxed);
picker
.update(&mut cx, |picker, cx| {
picker.delegate.selected_index.take();
picker
.delegate
.set_search_matches(search_id, did_cancel, query, matches, cx)
@@ -454,12 +464,14 @@ impl FileFinderDelegate {
.map(|query| query.path_like.path_query());
self.matches.push_new_matches(
&self.history_items,
self.currently_opened_path.as_ref(),
&query,
matches.into_iter(),
extend_old_matches,
);
self.latest_search_query = Some(query);
self.latest_search_did_cancel = did_cancel;
self.selected_index = self.calculate_selected_index();
cx.notify();
}
}
@@ -630,6 +642,19 @@ impl FileFinderDelegate {
.log_err();
})
}
/// Skips first history match (that is displayed topmost) if it's currently opened.
fn calculate_selected_index(&self) -> usize {
if let Some(Match::History(path, _)) = self.matches.get(0) {
if Some(path) == self.currently_opened_path.as_ref() {
let elements_after_first = self.matches.len() - 1;
if elements_after_first > 0 {
return 1;
}
}
}
0
}
}
impl PickerDelegate for FileFinderDelegate {
@@ -644,11 +669,11 @@ impl PickerDelegate for FileFinderDelegate {
}
fn selected_index(&self) -> usize {
self.selected_index.unwrap_or(0)
self.selected_index
}
fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>) {
self.selected_index = Some(ix);
self.selected_index = ix;
cx.notify();
}
@@ -671,22 +696,22 @@ impl PickerDelegate for FileFinderDelegate {
if raw_query.is_empty() {
let project = self.project.read(cx);
self.latest_search_id = post_inc(&mut self.search_count);
self.selected_index.take();
self.matches = Matches {
history: self
.history_items
.iter()
.filter(|history_item| {
project
.worktree_for_id(history_item.project.worktree_id, cx)
.is_some()
|| (project.is_local() && history_item.absolute.is_some())
})
.cloned()
.map(|p| (p, None))
.collect(),
history: Vec::new(),
search: Vec::new(),
};
self.matches.set_new_history(
self.currently_opened_path.as_ref(),
None,
self.history_items.iter().filter(|history_item| {
project
.worktree_for_id(history_item.project.worktree_id, cx)
.is_some()
|| (project.is_local() && history_item.absolute.is_some())
}),
);
self.selected_index = self.calculate_selected_index();
cx.notify();
Task::ready(())
} else {

View File

@@ -1062,6 +1062,177 @@ async fn test_search_sorts_history_items(cx: &mut gpui::TestAppContext) {
});
}
#[gpui::test]
async fn test_select_current_open_file_when_no_history(cx: &mut gpui::TestAppContext) {
let app_state = init_test(cx);
app_state
.fs
.as_fake()
.insert_tree(
"/root",
json!({
"test": {
"1_qw": "",
}
}),
)
.await;
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
// Open new buffer
open_queried_buffer("1", 1, "1_qw", &workspace, cx).await;
let picker = open_file_picker(&workspace, cx);
picker.update(cx, |finder, _| {
assert_match_selection(&finder, 0, "1_qw");
});
}
#[gpui::test]
async fn test_keep_opened_file_on_top_of_search_results_and_select_next_one(
cx: &mut TestAppContext,
) {
let app_state = init_test(cx);
app_state
.fs
.as_fake()
.insert_tree(
"/src",
json!({
"test": {
"bar.rs": "// Bar file",
"lib.rs": "// Lib file",
"maaa.rs": "// Maaaaaaa",
"main.rs": "// Main file",
"moo.rs": "// Moooooo",
}
}),
)
.await;
let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
open_close_queried_buffer("bar", 1, "bar.rs", &workspace, cx).await;
open_close_queried_buffer("lib", 1, "lib.rs", &workspace, cx).await;
open_queried_buffer("main", 1, "main.rs", &workspace, cx).await;
// main.rs is on top, previously used is selected
let picker = open_file_picker(&workspace, cx);
picker.update(cx, |finder, _| {
assert_eq!(finder.delegate.matches.len(), 3);
assert_match_at_position(finder, 0, "main.rs");
assert_match_selection(finder, 1, "lib.rs");
assert_match_at_position(finder, 2, "bar.rs");
});
// all files match, main.rs is still on top
picker
.update(cx, |finder, cx| {
finder.delegate.update_matches(".rs".to_string(), cx)
})
.await;
picker.update(cx, |finder, _| {
assert_eq!(finder.delegate.matches.len(), 5);
assert_match_at_position(finder, 0, "main.rs");
assert_match_selection(finder, 1, "bar.rs");
});
// main.rs is not among matches, select top item
picker
.update(cx, |finder, cx| {
finder.delegate.update_matches("b".to_string(), cx)
})
.await;
picker.update(cx, |finder, _| {
assert_eq!(finder.delegate.matches.len(), 2);
assert_match_at_position(finder, 0, "bar.rs");
});
// main.rs is back, put it on top and select next item
picker
.update(cx, |finder, cx| {
finder.delegate.update_matches("m".to_string(), cx)
})
.await;
picker.update(cx, |finder, _| {
assert_eq!(finder.delegate.matches.len(), 3);
assert_match_at_position(finder, 0, "main.rs");
assert_match_selection(finder, 1, "moo.rs");
});
// get back to the initial state
picker
.update(cx, |finder, cx| {
finder.delegate.update_matches("".to_string(), cx)
})
.await;
picker.update(cx, |finder, _| {
assert_eq!(finder.delegate.matches.len(), 3);
assert_match_at_position(finder, 0, "main.rs");
assert_match_selection(finder, 1, "lib.rs");
});
}
#[gpui::test]
async fn test_history_items_shown_in_order_of_open(cx: &mut TestAppContext) {
let app_state = init_test(cx);
app_state
.fs
.as_fake()
.insert_tree(
"/test",
json!({
"test": {
"1.txt": "// One",
"2.txt": "// Two",
"3.txt": "// Three",
}
}),
)
.await;
let project = Project::test(app_state.fs.clone(), ["/test".as_ref()], cx).await;
let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
open_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
open_queried_buffer("2", 1, "2.txt", &workspace, cx).await;
open_queried_buffer("3", 1, "3.txt", &workspace, cx).await;
let picker = open_file_picker(&workspace, cx);
picker.update(cx, |finder, _| {
assert_eq!(finder.delegate.matches.len(), 3);
assert_match_at_position(finder, 0, "3.txt");
assert_match_selection(finder, 1, "2.txt");
assert_match_at_position(finder, 2, "1.txt");
});
cx.dispatch_action(Confirm); // Open 2.txt
let picker = open_file_picker(&workspace, cx);
picker.update(cx, |finder, _| {
assert_eq!(finder.delegate.matches.len(), 3);
assert_match_at_position(finder, 0, "2.txt");
assert_match_selection(finder, 1, "3.txt");
assert_match_at_position(finder, 2, "1.txt");
});
cx.dispatch_action(SelectNext);
cx.dispatch_action(Confirm); // Open 1.txt
let picker = open_file_picker(&workspace, cx);
picker.update(cx, |finder, _| {
assert_eq!(finder.delegate.matches.len(), 3);
assert_match_at_position(finder, 0, "1.txt");
assert_match_selection(finder, 1, "2.txt");
assert_match_at_position(finder, 2, "3.txt");
});
}
#[gpui::test]
async fn test_history_items_vs_very_good_external_match(cx: &mut gpui::TestAppContext) {
let app_state = init_test(cx);
@@ -1172,6 +1343,27 @@ async fn open_close_queried_buffer(
expected_editor_title: &str,
workspace: &View<Workspace>,
cx: &mut gpui::VisualTestContext,
) -> Vec<FoundPath> {
let history_items = open_queried_buffer(
input,
expected_matches,
expected_editor_title,
workspace,
cx,
)
.await;
cx.dispatch_action(workspace::CloseActiveItem { save_intent: None });
history_items
}
async fn open_queried_buffer(
input: &str,
expected_matches: usize,
expected_editor_title: &str,
workspace: &View<Workspace>,
cx: &mut gpui::VisualTestContext,
) -> Vec<FoundPath> {
let picker = open_file_picker(&workspace, cx);
cx.simulate_input(input);
@@ -1186,7 +1378,6 @@ async fn open_close_queried_buffer(
finder.delegate.history_items.clone()
});
cx.dispatch_action(SelectNext);
cx.dispatch_action(Confirm);
cx.read(|cx| {
@@ -1198,8 +1389,6 @@ async fn open_close_queried_buffer(
);
});
cx.dispatch_action(workspace::CloseActiveItem { save_intent: None });
history_items
}
@@ -1313,3 +1502,37 @@ fn collect_search_matches(picker: &Picker<FileFinderDelegate>) -> SearchEntries
.collect(),
}
}
#[track_caller]
fn assert_match_selection(
finder: &Picker<FileFinderDelegate>,
expected_selection_index: usize,
expected_file_name: &str,
) {
assert_eq!(
finder.delegate.selected_index(),
expected_selection_index,
"Match is not selected"
);
assert_match_at_position(finder, expected_selection_index, expected_file_name);
}
#[track_caller]
fn assert_match_at_position(
finder: &Picker<FileFinderDelegate>,
match_index: usize,
expected_file_name: &str,
) {
let match_item = finder
.delegate
.matches
.get(match_index)
.unwrap_or_else(|| panic!("Finder has no match for index {match_index}"));
let match_file_name = match match_item {
Match::History(found_path, _) => found_path.absolute.as_deref().unwrap().file_name(),
Match::Search(path_match) => path_match.0.path.file_name(),
}
.unwrap()
.to_string_lossy();
assert_eq!(match_file_name, expected_file_name);
}

View File

@@ -9,6 +9,7 @@ use derive_more::{Deref, DerefMut};
pub use entity_map::*;
pub use model_context::*;
use refineable::Refineable;
use smallvec::SmallVec;
use smol::future::FutureExt;
#[cfg(any(test, feature = "test-support"))]
pub use test_context::*;
@@ -18,13 +19,18 @@ use crate::{
current_platform, image_cache::ImageCache, init_app_menus, Action, ActionRegistry, Any,
AnyView, AnyWindowHandle, AppMetadata, AssetSource, BackgroundExecutor, ClipboardItem, Context,
DispatchPhase, DisplayId, Entity, EventEmitter, ForegroundExecutor, Global, KeyBinding, Keymap,
Keystroke, LayoutId, Menu, PathPromptOptions, Pixels, Platform, PlatformDisplay, Point, Render,
SharedString, SubscriberSet, Subscription, SvgRenderer, Task, TextStyle, TextStyleRefinement,
TextSystem, View, ViewContext, Window, WindowContext, WindowHandle, WindowId,
Keystroke, LayoutId, Menu, PathPromptOptions, Pixels, Platform, PlatformDisplay,
PlatformDisplayLink, Point, Render, SharedString, SubscriberSet, Subscription, SvgRenderer,
Task, TextStyle, TextStyleRefinement, TextSystem, View, ViewContext, Window, WindowContext,
WindowHandle, WindowId,
};
use anyhow::{anyhow, Result};
use collections::{FxHashMap, FxHashSet, VecDeque};
use futures::{channel::oneshot, future::LocalBoxFuture, Future};
use collections::{hash_map, FxHashMap, FxHashSet, VecDeque};
use futures::{
channel::{mpsc, oneshot},
future::LocalBoxFuture,
Future, StreamExt,
};
use slotmap::SlotMap;
use std::{
@@ -39,7 +45,7 @@ use std::{
};
use util::{
http::{self, HttpClient},
ResultExt,
measure, ResultExt,
};
/// The duration for which futures returned from [AppContext::on_app_context] or [ModelContext::on_app_quit] can run before the application fully quits.
@@ -213,7 +219,8 @@ pub struct AppContext {
pub(crate) actions: Rc<ActionRegistry>,
pub(crate) active_drag: Option<AnyDrag>,
pub(crate) next_frame_callbacks: FxHashMap<DisplayId, Vec<FrameCallback>>,
pub(crate) frame_consumers: FxHashMap<DisplayId, Task<()>>,
display_links: FxHashMap<DisplayId, Box<dyn PlatformDisplayLink>>,
display_updates: mpsc::UnboundedSender<DisplayId>,
pub(crate) background_executor: BackgroundExecutor,
pub(crate) foreground_executor: ForegroundExecutor,
pub(crate) svg_renderer: SvgRenderer,
@@ -264,6 +271,7 @@ impl AppContext {
app_version: platform.app_version().ok(),
};
let (display_updates_tx, mut display_updates_rx) = mpsc::unbounded();
let app = Rc::new_cyclic(|this| AppCell {
app: RefCell::new(AppContext {
this: this.clone(),
@@ -275,9 +283,10 @@ impl AppContext {
pending_updates: 0,
active_drag: None,
next_frame_callbacks: FxHashMap::default(),
frame_consumers: FxHashMap::default(),
display_links: FxHashMap::default(),
display_updates: display_updates_tx,
background_executor: executor,
foreground_executor,
foreground_executor: foreground_executor.clone(),
svg_renderer: SvgRenderer::new(asset_source.clone()),
asset_source,
image_cache: ImageCache::new(http_client),
@@ -311,6 +320,23 @@ impl AppContext {
}
}));
foreground_executor
.spawn({
let cx = app.borrow().to_async();
async move {
while let Some(display_id) = display_updates_rx.next().await {
if cx
.update(|cx| cx.refresh_display(display_id))
.log_err()
.is_none()
{
break;
}
}
}
})
.detach();
app
}
@@ -483,6 +509,8 @@ impl AppContext {
let root_view = build_root_view(&mut WindowContext::new(cx, &mut window));
window.root_view.replace(root_view.into());
cx.windows.get_mut(id).unwrap().replace(window);
// Schedule a draw right after launching the window.
cx.refresh();
handle
})
}
@@ -652,10 +680,29 @@ impl AppContext {
}
}
} else {
let mut active_display_ids = FxHashSet::default();
for window in self.windows.values() {
if let Some(window) = window.as_ref() {
if window.dirty {
window.platform_window.invalidate();
active_display_ids.insert(window.display_id);
}
}
self.display_links
.retain(|display_id, _| active_display_ids.contains(display_id));
for display_id in active_display_ids {
if let hash_map::Entry::Vacant(entry) = self.display_links.entry(display_id) {
let tx = self.display_updates.clone();
if let Some(display_link) = self
.platform
.start_display_link(
display_id,
Box::new(move || {
tx.unbounded_send(display_id).log_err();
}),
)
.log_err()
{
entry.insert(display_link);
}
}
}
@@ -765,6 +812,32 @@ impl AppContext {
callback(self);
}
fn refresh_display(&mut self, display_id: DisplayId) {
// TODO: run this as part of an effect cycle, so that if there are any side
// effects, they all get executed so that we can finally draw the windows at
// the end.
if let Some(callbacks) = self.next_frame_callbacks.remove(&display_id) {
for callback in callbacks {
callback(self);
}
}
let mut dirty_handles = SmallVec::<[AnyWindowHandle; 4]>::new();
for window in self.windows.values() {
if let Some(window) = window {
if window.dirty && window.display_id == display_id {
dirty_handles.push(window.handle);
}
}
}
for dirty_handle in dirty_handles {
dirty_handle
.update(self, |_, cx| measure("frame duration", || cx.draw()))
.log_err();
}
}
/// Creates an `AsyncAppContext`, which can be cloned and has a static lifetime
/// so it can be held across `await` points.
pub fn to_async(&self) -> AsyncAppContext {

View File

@@ -11,7 +11,7 @@ use crate::{
Pixels, PlatformInput, Point, RenderGlyphParams, RenderImageParams, RenderSvgParams, Scene,
SharedString, Size, Task, TaskLabel, WindowContext,
};
use anyhow::{anyhow, Result};
use anyhow::Result;
use async_task::Runnable;
use futures::channel::oneshot;
use parking::Unparker;
@@ -23,11 +23,10 @@ use std::hash::{Hash, Hasher};
use std::time::Duration;
use std::{
any::Any,
fmt::{self, Debug, Display},
fmt::{self, Debug},
ops::Range,
path::{Path, PathBuf},
rc::Rc,
str::FromStr,
sync::Arc,
};
use uuid::Uuid;
@@ -39,6 +38,7 @@ pub(crate) use mac::*;
#[cfg(any(test, feature = "test-support"))]
pub(crate) use test::*;
use time::UtcOffset;
pub use util::SemanticVersion;
#[cfg(target_os = "macos")]
pub(crate) fn current_platform() -> Rc<dyn Platform> {
@@ -67,13 +67,11 @@ pub(crate) trait Platform: 'static {
options: WindowOptions,
) -> Box<dyn PlatformWindow>;
fn set_display_link_output_callback(
fn start_display_link(
&self,
display_id: DisplayId,
callback: Box<dyn FnMut() + Send>,
);
fn start_display_link(&self, display_id: DisplayId);
fn stop_display_link(&self, display_id: DisplayId);
) -> Result<Box<dyn PlatformDisplayLink>>;
fn open_url(&self, url: &str);
fn on_open_urls(&self, callback: Box<dyn FnMut(Vec<String>)>);
@@ -186,6 +184,8 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
}
}
pub(crate) trait PlatformDisplayLink {}
/// This type is public so that our test macro can generate and use it, but it should not
/// be considered part of our public API.
#[doc(hidden)]
@@ -697,45 +697,6 @@ impl Default for CursorStyle {
}
}
/// A datastructure representing a semantic version number
#[derive(Clone, Copy, Debug, Default, Eq, Ord, PartialEq, PartialOrd, Serialize)]
pub struct SemanticVersion {
major: usize,
minor: usize,
patch: usize,
}
impl FromStr for SemanticVersion {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self> {
let mut components = s.trim().split('.');
let major = components
.next()
.ok_or_else(|| anyhow!("missing major version number"))?
.parse()?;
let minor = components
.next()
.ok_or_else(|| anyhow!("missing minor version number"))?
.parse()?;
let patch = components
.next()
.ok_or_else(|| anyhow!("missing patch version number"))?
.parse()?;
Ok(Self {
major,
minor,
patch,
})
}
}
impl Display for SemanticVersion {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
}
}
/// A clipboard item that should be copied to the clipboard
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ClipboardItem {

View File

@@ -1,76 +1,50 @@
use crate::{DisplayId, PlatformDisplayLink};
use anyhow::{anyhow, Result};
use parking_lot::Mutex;
use std::{
ffi::c_void,
mem,
sync::{Arc, Weak},
};
use crate::DisplayId;
use collections::HashMap;
use parking_lot::Mutex;
pub(crate) struct MacDisplayLinker {
links: HashMap<DisplayId, MacDisplayLink>,
}
struct MacDisplayLink {
system_link: sys::DisplayLink,
_output_callback: Arc<OutputCallback>,
}
impl MacDisplayLinker {
pub fn new() -> Self {
MacDisplayLinker {
links: Default::default(),
}
}
}
type OutputCallback = Mutex<Box<dyn FnMut() + Send>>;
impl MacDisplayLinker {
pub fn set_output_callback(
&mut self,
display_id: DisplayId,
output_callback: Box<dyn FnMut() + Send>,
) {
if let Some(mut system_link) = unsafe { sys::DisplayLink::on_display(display_id.0) } {
let callback = Arc::new(Mutex::new(output_callback));
let weak_callback_ptr: *const OutputCallback = Arc::downgrade(&callback).into_raw();
unsafe { system_link.set_output_callback(trampoline, weak_callback_ptr as *mut c_void) }
pub(crate) struct MacDisplayLink {
system_link: sys::DisplayLink,
_callback: Arc<OutputCallback>,
}
self.links.insert(
display_id,
MacDisplayLink {
_output_callback: callback,
system_link,
},
);
} else {
log::warn!("DisplayLink could not be obtained for {:?}", display_id);
impl MacDisplayLink {
pub fn new(display_id: DisplayId, callback: Box<dyn FnMut() + Send>) -> Result<Self> {
let mut system_link = unsafe {
sys::DisplayLink::on_display(display_id.0)
.ok_or_else(|| anyhow!("could not create DisplayLink"))
}?;
let callback = Arc::new(Mutex::new(callback));
let weak_callback_ptr: *const OutputCallback = Arc::downgrade(&callback).into_raw();
unsafe {
system_link.set_output_callback(trampoline, weak_callback_ptr as *mut c_void);
system_link.start();
}
}
pub fn start(&mut self, display_id: DisplayId) {
if let Some(link) = self.links.get_mut(&display_id) {
unsafe {
link.system_link.start();
}
} else {
log::warn!("No DisplayLink callback registered for {:?}", display_id)
}
Ok(Self {
system_link,
_callback: callback,
})
}
}
pub fn stop(&mut self, display_id: DisplayId) {
if let Some(link) = self.links.get_mut(&display_id) {
unsafe {
link.system_link.stop();
}
} else {
log::warn!("No DisplayLink callback registered for {:?}", display_id)
impl Drop for MacDisplayLink {
fn drop(&mut self) {
unsafe {
self.system_link.stop();
}
}
}
impl PlatformDisplayLink for MacDisplayLink {}
unsafe extern "C" fn trampoline(
_display_link_out: *mut sys::CVDisplayLink,
current_time: *const sys::CVTimeStamp,

View File

@@ -314,7 +314,7 @@ impl MetalRenderer {
command_buffer.commit();
self.sprite_atlas.clear_textures(AtlasTextureKind::Path);
command_buffer.wait_until_completed();
command_buffer.wait_until_scheduled();
drawable.present();
}

View File

@@ -1,9 +1,10 @@
use super::{events::key_to_native, BoolExt};
use crate::{
Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId,
ForegroundExecutor, Keymap, MacDispatcher, MacDisplay, MacDisplayLinker, MacTextSystem,
MacWindow, Menu, MenuItem, PathPromptOptions, Platform, PlatformDisplay, PlatformInput,
PlatformTextSystem, PlatformWindow, Result, SemanticVersion, Task, WindowOptions,
ForegroundExecutor, Keymap, MacDispatcher, MacDisplay, MacDisplayLink, MacTextSystem,
MacWindow, Menu, MenuItem, PathPromptOptions, Platform, PlatformDisplay, PlatformDisplayLink,
PlatformInput, PlatformTextSystem, PlatformWindow, Result, SemanticVersion, Task,
WindowOptions,
};
use anyhow::anyhow;
use block::ConcreteBlock;
@@ -145,7 +146,6 @@ pub(crate) struct MacPlatformState {
background_executor: BackgroundExecutor,
foreground_executor: ForegroundExecutor,
text_system: Arc<MacTextSystem>,
display_linker: MacDisplayLinker,
pasteboard: id,
text_hash_pasteboard_type: id,
metadata_pasteboard_type: id,
@@ -175,7 +175,6 @@ impl MacPlatform {
background_executor: BackgroundExecutor::new(dispatcher.clone()),
foreground_executor: ForegroundExecutor::new(dispatcher),
text_system: Arc::new(MacTextSystem::new()),
display_linker: MacDisplayLinker::new(),
pasteboard: unsafe { NSPasteboard::generalPasteboard(nil) },
text_hash_pasteboard_type: unsafe { ns_string("zed-text-hash") },
metadata_pasteboard_type: unsafe { ns_string("zed-metadata") },
@@ -497,23 +496,12 @@ impl Platform for MacPlatform {
Box::new(MacWindow::open(handle, options, self.foreground_executor()))
}
fn set_display_link_output_callback(
fn start_display_link(
&self,
display_id: DisplayId,
callback: Box<dyn FnMut() + Send>,
) {
self.0
.lock()
.display_linker
.set_output_callback(display_id, callback);
}
fn start_display_link(&self, display_id: DisplayId) {
self.0.lock().display_linker.start(display_id);
}
fn stop_display_link(&self, display_id: DisplayId) {
self.0.lock().display_linker.stop(display_id);
) -> Result<Box<dyn PlatformDisplayLink>> {
Ok(Box::new(MacDisplayLink::new(display_id, callback)?))
}
fn open_url(&self, url: &str) {

View File

@@ -61,6 +61,16 @@ fragment float4 quad_fragment(QuadFragmentInput input [[stage_in]],
constant Quad *quads
[[buffer(QuadInputIndex_Quads)]]) {
Quad quad = quads[input.quad_id];
// Fast path when the quad is not rounded and doesn't have any border.
if (quad.corner_radii.top_left == 0. && quad.corner_radii.bottom_left == 0. &&
quad.corner_radii.top_right == 0. &&
quad.corner_radii.bottom_right == 0. && quad.border_widths.top == 0. &&
quad.border_widths.left == 0. && quad.border_widths.right == 0. &&
quad.border_widths.bottom == 0.) {
return input.background_color;
}
float2 half_size =
float2(quad.bounds.size.width, quad.bounds.size.height) / 2.;
float2 center =

View File

@@ -1,7 +1,7 @@
use crate::{
AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId, ForegroundExecutor,
Keymap, Platform, PlatformDisplay, PlatformTextSystem, Task, TestDisplay, TestWindow,
WindowOptions,
Keymap, Platform, PlatformDisplay, PlatformDisplayLink, PlatformTextSystem, Task, TestDisplay,
TestWindow, WindowOptions,
};
use anyhow::{anyhow, Result};
use collections::VecDeque;
@@ -176,18 +176,14 @@ impl Platform for TestPlatform {
Box::new(window)
}
fn set_display_link_output_callback(
fn start_display_link(
&self,
_display_id: DisplayId,
mut callback: Box<dyn FnMut() + Send>,
) {
callback()
_callback: Box<dyn FnMut() + Send>,
) -> Result<Box<dyn PlatformDisplayLink>> {
todo!()
}
fn start_display_link(&self, _display_id: DisplayId) {}
fn stop_display_link(&self, _display_id: DisplayId) {}
fn open_url(&self, _url: &str) {
unimplemented!()
}

View File

@@ -12,10 +12,7 @@ use crate::{
use anyhow::{anyhow, Context as _, Result};
use collections::FxHashSet;
use derive_more::{Deref, DerefMut};
use futures::{
channel::{mpsc, oneshot},
StreamExt,
};
use futures::channel::oneshot;
use parking_lot::RwLock;
use slotmap::SlotMap;
use smallvec::SmallVec;
@@ -23,7 +20,6 @@ use std::{
any::{Any, TypeId},
borrow::{Borrow, BorrowMut},
cell::RefCell,
collections::hash_map::Entry,
fmt::{Debug, Display},
future::Future,
hash::{Hash, Hasher},
@@ -36,7 +32,7 @@ use std::{
},
time::Duration,
};
use util::{measure, ResultExt};
use util::ResultExt;
mod element_cx;
pub use element_cx::*;
@@ -249,7 +245,7 @@ pub struct Window {
pub(crate) handle: AnyWindowHandle,
pub(crate) removed: bool,
pub(crate) platform_window: Box<dyn PlatformWindow>,
display_id: DisplayId,
pub(crate) display_id: DisplayId,
sprite_atlas: Arc<dyn PlatformAtlas>,
pub(crate) rem_size: Pixels,
pub(crate) viewport_size: Size<Pixels>,
@@ -338,19 +334,14 @@ impl Window {
let scale_factor = platform_window.scale_factor();
let bounds = platform_window.bounds();
platform_window.on_request_frame(Box::new({
let mut cx = cx.to_async();
move || {
measure("frame duration", || {
handle.update(&mut cx, |_, cx| cx.draw()).log_err();
})
}
}));
platform_window.on_resize(Box::new({
let mut cx = cx.to_async();
move |_, _| {
handle
.update(&mut cx, |_, cx| cx.window_bounds_changed())
.update(&mut cx, |_, cx| {
cx.window_bounds_changed();
cx.draw()
})
.log_err();
}
}));
@@ -642,48 +633,6 @@ impl<'a> WindowContext<'a> {
let handle = self.window.handle;
let display_id = self.window.display_id;
let mut frame_consumers = std::mem::take(&mut self.app.frame_consumers);
if let Entry::Vacant(e) = frame_consumers.entry(display_id) {
let (tx, mut rx) = mpsc::unbounded::<()>();
self.platform.set_display_link_output_callback(
display_id,
Box::new(move || _ = tx.unbounded_send(())),
);
let consumer_task = self.app.spawn(|cx| async move {
while rx.next().await.is_some() {
cx.update(|cx| {
for callback in cx
.next_frame_callbacks
.get_mut(&display_id)
.unwrap()
.drain(..)
.collect::<SmallVec<[_; 32]>>()
{
callback(cx);
}
})
.ok();
// Flush effects, then stop the display link if no new next_frame_callbacks have been added.
cx.update(|cx| {
if cx.next_frame_callbacks.is_empty() {
cx.platform.stop_display_link(display_id);
}
})
.ok();
}
});
e.insert(consumer_task);
}
debug_assert!(self.app.frame_consumers.is_empty());
self.app.frame_consumers = frame_consumers;
if self.next_frame_callbacks.is_empty() {
self.platform.start_display_link(display_id);
}
self.next_frame_callbacks
.entry(display_id)
.or_default()

View File

@@ -29,7 +29,7 @@ async-trait.workspace = true
clock = { path = "../clock" }
collections = { path = "../collections" }
futures.workspace = true
fuzzy = { path = "../fuzzy" }
fuzzy = { path = "../fuzzy" }
git = { path = "../git" }
globset.workspace = true
gpui = { path = "../gpui" }
@@ -38,7 +38,6 @@ log.workspace = true
lsp = { path = "../lsp" }
parking_lot.workspace = true
postage.workspace = true
pulldown-cmark = { version = "0.9.2", default-features = false }
rand = { workspace = true, optional = true }
regex.workspace = true
rpc = { path = "../rpc" }
@@ -55,6 +54,7 @@ text = { path = "../text" }
theme = { path = "../theme" }
tree-sitter-rust = { workspace = true, optional = true }
tree-sitter-typescript = { workspace = true, optional = true }
pulldown-cmark.workspace = true
tree-sitter.workspace = true
unicase = "2.6"
util = { path = "../util" }

View File

@@ -30,6 +30,7 @@ workspace = { path = "../workspace" }
[dev-dependencies]
client = { path = "../client", features = ["test-support"] }
editor = { path = "../editor", features = ["test-support"] }
release_channel = { path = "../release_channel" }
env_logger.workspace = true
gpui = { path = "../gpui", features = ["test-support"] }
unindent.workspace = true

View File

@@ -100,6 +100,7 @@ fn init_test(cx: &mut gpui::TestAppContext) {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
theme::init(theme::LoadThemes::JustBase, cx);
release_channel::init("0.0.0", cx);
language::init(cx);
client::init_settings(cx);
Project::init_settings(cx);

View File

@@ -27,6 +27,7 @@ serde_derive.workspace = true
serde_json.workspace = true
smol.workspace = true
util = { path = "../util" }
release_channel = { path = "../release_channel" }
[dev-dependencies]
async-pipe = { git = "https://github.com/zed-industries/async-pipe-rs", rev = "82d00a04211cf4e1236029aa03e6b6ce2a74c553" }

View File

@@ -5,7 +5,7 @@ pub use lsp_types::*;
use anyhow::{anyhow, Context, Result};
use collections::HashMap;
use futures::{channel::oneshot, io::BufWriter, AsyncRead, AsyncWrite, FutureExt};
use gpui::{AsyncAppContext, BackgroundExecutor, Task};
use gpui::{AppContext, AsyncAppContext, BackgroundExecutor, Task};
use parking_lot::Mutex;
use postage::{barrier, prelude::Stream};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
@@ -322,8 +322,15 @@ impl LanguageServer {
let mut buffer = Vec::new();
loop {
buffer.clear();
stdout.read_until(b'\n', &mut buffer).await?;
stdout.read_until(b'\n', &mut buffer).await?;
if stdout.read_until(b'\n', &mut buffer).await? == 0 {
break;
};
if stdout.read_until(b'\n', &mut buffer).await? == 0 {
break;
};
let header = std::str::from_utf8(&buffer)?;
let message_len: usize = header
.strip_prefix(CONTENT_LEN_HEADER)
@@ -378,6 +385,8 @@ impl LanguageServer {
// Don't starve the main thread when receiving lots of messages at once.
smol::future::yield_now().await;
}
Ok(())
}
async fn handle_stderr<Stderr>(
@@ -393,7 +402,12 @@ impl LanguageServer {
loop {
buffer.clear();
stderr.read_until(b'\n', &mut buffer).await?;
let bytes_read = stderr.read_until(b'\n', &mut buffer).await?;
if bytes_read == 0 {
return Ok(());
}
if let Ok(message) = str::from_utf8(&buffer) {
log::trace!("incoming stderr message:{message}");
for handler in io_handlers.lock().values_mut() {
@@ -450,7 +464,11 @@ impl LanguageServer {
/// Note that `options` is used directly to construct [`InitializeParams`], which is why it is owned.
///
/// [LSP Specification](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initialize)
pub async fn initialize(mut self, options: Option<Value>) -> Result<Arc<Self>> {
pub fn initialize(
mut self,
options: Option<Value>,
cx: &AppContext,
) -> Task<Result<Arc<Self>>> {
let root_uri = Url::from_file_path(&self.root_path).unwrap();
#[allow(deprecated)]
let params = InitializeParams {
@@ -579,18 +597,25 @@ impl LanguageServer {
uri: root_uri,
name: Default::default(),
}]),
client_info: None,
client_info: Some(ClientInfo {
name: release_channel::ReleaseChannel::global(cx)
.display_name()
.to_string(),
version: Some(release_channel::AppVersion::global(cx).to_string()),
}),
locale: None,
};
let response = self.request::<request::Initialize>(params).await?;
if let Some(info) = response.server_info {
self.name = info.name;
}
self.capabilities = response.capabilities;
cx.spawn(|_| async move {
let response = self.request::<request::Initialize>(params).await?;
if let Some(info) = response.server_info {
self.name = info.name;
}
self.capabilities = response.capabilities;
self.notify::<notification::Initialized>(InitializedParams {})?;
Ok(Arc::new(self))
self.notify::<notification::Initialized>(InitializedParams {})?;
Ok(Arc::new(self))
})
}
/// Sends a shutdown request to the language server process and prepares the [`LanguageServer`] to be dropped.
@@ -1213,6 +1238,9 @@ mod tests {
#[gpui::test]
async fn test_fake(cx: &mut TestAppContext) {
cx.update(|cx| {
release_channel::init("0.0.0", cx);
});
let (server, mut fake) =
FakeLanguageServer::new("the-lsp".to_string(), Default::default(), cx.to_async());
@@ -1229,7 +1257,7 @@ mod tests {
})
.detach();
let server = server.initialize(None).await.unwrap();
let server = cx.update(|cx| server.initialize(None, cx)).await.unwrap();
server
.notify::<notification::DidOpenTextDocument>(DidOpenTextDocumentParams {
text_document: TextDocumentItem::new(

View File

@@ -0,0 +1,31 @@
[package]
name = "markdown_preview"
version = "0.1.0"
edition = "2021"
publish = false
license = "GPL-3.0-or-later"
[lib]
path = "src/markdown_preview.rs"
[features]
test-support = []
[dependencies]
anyhow.workspace = true
editor = { path = "../editor" }
gpui = { path = "../gpui" }
language = { path = "../language" }
lazy_static.workspace = true
log.workspace = true
menu = { path = "../menu" }
project = { path = "../project" }
pulldown-cmark.workspace = true
rich_text = { path = "../rich_text" }
theme = { path = "../theme" }
ui = { path = "../ui" }
util = { path = "../util" }
workspace = { path = "../workspace" }
[dev-dependencies]
editor = { path = "../editor", features = ["test-support"] }

View File

@@ -0,0 +1 @@
../../LICENSE-GPL

View File

@@ -0,0 +1,14 @@
use gpui::{actions, AppContext};
use workspace::Workspace;
pub mod markdown_preview_view;
pub mod markdown_renderer;
actions!(markdown, [OpenPreview]);
pub fn init(cx: &mut AppContext) {
cx.observe_new_views(|workspace: &mut Workspace, cx| {
markdown_preview_view::MarkdownPreviewView::register(workspace, cx);
})
.detach();
}

View File

@@ -0,0 +1,137 @@
use editor::{Editor, EditorEvent};
use gpui::{
canvas, AnyElement, AppContext, AvailableSpace, EventEmitter, FocusHandle, FocusableView,
InteractiveElement, IntoElement, ParentElement, Render, Styled, View, ViewContext,
};
use language::LanguageRegistry;
use std::sync::Arc;
use ui::prelude::*;
use workspace::item::Item;
use workspace::Workspace;
use crate::{markdown_renderer::render_markdown, OpenPreview};
pub struct MarkdownPreviewView {
focus_handle: FocusHandle,
languages: Arc<LanguageRegistry>,
contents: String,
}
impl MarkdownPreviewView {
pub fn register(workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>) {
let languages = workspace.app_state().languages.clone();
workspace.register_action(move |workspace, _: &OpenPreview, cx| {
if workspace.has_active_modal(cx) {
cx.propagate();
return;
}
let languages = languages.clone();
if let Some(editor) = workspace.active_item_as::<Editor>(cx) {
let view: View<MarkdownPreviewView> =
cx.new_view(|cx| MarkdownPreviewView::new(editor, languages, cx));
workspace.split_item(workspace::SplitDirection::Right, Box::new(view.clone()), cx);
cx.notify();
}
});
}
pub fn new(
active_editor: View<Editor>,
languages: Arc<LanguageRegistry>,
cx: &mut ViewContext<Self>,
) -> Self {
let focus_handle = cx.focus_handle();
cx.subscribe(&active_editor, |this, editor, event: &EditorEvent, cx| {
if *event == EditorEvent::Edited {
let editor = editor.read(cx);
let contents = editor.buffer().read(cx).snapshot(cx).text();
this.contents = contents;
cx.notify();
}
})
.detach();
let editor = active_editor.read(cx);
let contents = editor.buffer().read(cx).snapshot(cx).text();
Self {
focus_handle,
languages,
contents,
}
}
}
impl FocusableView for MarkdownPreviewView {
fn focus_handle(&self, _: &AppContext) -> gpui::FocusHandle {
self.focus_handle.clone()
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum PreviewEvent {}
impl EventEmitter<PreviewEvent> for MarkdownPreviewView {}
impl Item for MarkdownPreviewView {
type Event = PreviewEvent;
fn tab_content(
&self,
_detail: Option<usize>,
selected: bool,
_cx: &WindowContext,
) -> AnyElement {
h_flex()
.gap_2()
.child(Icon::new(IconName::FileDoc).color(if selected {
Color::Default
} else {
Color::Muted
}))
.child(Label::new("Markdown preview").color(if selected {
Color::Default
} else {
Color::Muted
}))
.into_any()
}
fn telemetry_event_text(&self) -> Option<&'static str> {
Some("markdown preview")
}
fn to_item_events(_event: &Self::Event, _f: impl FnMut(workspace::item::ItemEvent)) {}
}
impl Render for MarkdownPreviewView {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let rendered_markdown = v_flex()
.items_start()
.justify_start()
.key_context("MarkdownPreview")
.track_focus(&self.focus_handle)
.id("MarkdownPreview")
.overflow_y_scroll()
.overflow_x_hidden()
.size_full()
.bg(cx.theme().colors().editor_background)
.p_4()
.children(render_markdown(&self.contents, &self.languages, cx));
div().flex_1().child(
// FIXME: This shouldn't be necessary
// but the overflow_scroll above doesn't seem to work without it
canvas(move |bounds, cx| {
rendered_markdown.into_any().draw(
bounds.origin,
bounds.size.map(AvailableSpace::Definite),
cx,
)
})
.size_full(),
)
}
}

View File

@@ -0,0 +1,346 @@
use std::{ops::Range, sync::Arc};
use gpui::{
div, px, rems, AnyElement, DefiniteLength, Div, ElementId, Hsla, ParentElement, SharedString,
Styled, StyledText, WindowContext,
};
use language::LanguageRegistry;
use pulldown_cmark::{Alignment, CodeBlockKind, Event, HeadingLevel, Options, Parser, Tag};
use rich_text::render_rich_text;
use theme::{ActiveTheme, Theme};
use ui::{h_flex, v_flex};
enum TableState {
Header,
Body,
}
struct MarkdownTable {
column_alignments: Vec<Alignment>,
header: Vec<Div>,
body: Vec<Vec<Div>>,
current_row: Vec<Div>,
state: TableState,
border_color: Hsla,
}
impl MarkdownTable {
fn new(border_color: Hsla, column_alignments: Vec<Alignment>) -> Self {
Self {
column_alignments,
header: Vec::new(),
body: Vec::new(),
current_row: Vec::new(),
state: TableState::Header,
border_color,
}
}
fn finish_row(&mut self) {
match self.state {
TableState::Header => {
self.header.extend(self.current_row.drain(..));
self.state = TableState::Body;
}
TableState::Body => {
self.body.push(self.current_row.drain(..).collect());
}
}
}
fn add_cell(&mut self, contents: AnyElement) {
let container = match self.alignment_for_next_cell() {
Alignment::Left | Alignment::None => div(),
Alignment::Center => v_flex().items_center(),
Alignment::Right => v_flex().items_end(),
};
let cell = container
.w_full()
.child(contents)
.px_2()
.py_1()
.border_color(self.border_color);
let cell = match self.state {
TableState::Header => cell.border_2(),
TableState::Body => cell.border_1(),
};
self.current_row.push(cell);
}
fn finish(self) -> Div {
let mut table = v_flex().w_full();
let mut header = h_flex();
for cell in self.header {
header = header.child(cell);
}
table = table.child(header);
for row in self.body {
let mut row_div = h_flex();
for cell in row {
row_div = row_div.child(cell);
}
table = table.child(row_div);
}
table
}
fn alignment_for_next_cell(&self) -> Alignment {
self.column_alignments
.get(self.current_row.len())
.copied()
.unwrap_or(Alignment::None)
}
}
struct Renderer<I> {
source_contents: String,
iter: I,
theme: Arc<Theme>,
finished: Vec<Div>,
language_registry: Arc<LanguageRegistry>,
table: Option<MarkdownTable>,
list_depth: usize,
block_quote_depth: usize,
}
impl<'a, I> Renderer<I>
where
I: Iterator<Item = (Event<'a>, Range<usize>)>,
{
fn new(
iter: I,
source_contents: String,
language_registry: &Arc<LanguageRegistry>,
theme: Arc<Theme>,
) -> Self {
Self {
iter,
source_contents,
theme,
table: None,
finished: vec![],
language_registry: language_registry.clone(),
list_depth: 0,
block_quote_depth: 0,
}
}
fn run(mut self, cx: &WindowContext) -> Self {
while let Some((event, source_range)) = self.iter.next() {
match event {
Event::Start(tag) => {
self.start_tag(tag);
}
Event::End(tag) => {
self.end_tag(tag, source_range, cx);
}
Event::Rule => {
let rule = div().w_full().h(px(2.)).bg(self.theme.colors().border);
self.finished.push(div().mb_4().child(rule));
}
_ => {}
}
}
self
}
fn start_tag(&mut self, tag: Tag<'a>) {
match tag {
Tag::List(_) => {
self.list_depth += 1;
}
Tag::BlockQuote => {
self.block_quote_depth += 1;
}
Tag::Table(column_alignments) => {
self.table = Some(MarkdownTable::new(
self.theme.colors().border,
column_alignments,
));
}
_ => {}
}
}
fn end_tag(&mut self, tag: Tag, source_range: Range<usize>, cx: &WindowContext) {
match tag {
Tag::Paragraph => {
if self.list_depth > 0 || self.block_quote_depth > 0 {
return;
}
let element = self.render_md_from_range(source_range.clone(), cx);
let paragraph = div().mb_3().child(element);
self.finished.push(paragraph);
}
Tag::Heading(level, _, _) => {
let mut headline = self.headline(level);
if source_range.start > 0 {
headline = headline.mt_4();
}
let element = self.render_md_from_range(source_range.clone(), cx);
let headline = headline.child(element);
self.finished.push(headline);
}
Tag::List(_) => {
if self.list_depth == 1 {
let element = self.render_md_from_range(source_range.clone(), cx);
let list = div().mb_3().child(element);
self.finished.push(list);
}
self.list_depth -= 1;
}
Tag::BlockQuote => {
let element = self.render_md_from_range(source_range.clone(), cx);
let block_quote = h_flex()
.mb_3()
.child(
div()
.w(px(4.))
.bg(self.theme.colors().border)
.h_full()
.mr_2()
.mt_1(),
)
.text_color(self.theme.colors().text_muted)
.child(element);
self.finished.push(block_quote);
self.block_quote_depth -= 1;
}
Tag::CodeBlock(kind) => {
let contents = self.source_contents[source_range.clone()].trim();
let contents = contents.trim_start_matches("```");
let contents = contents.trim_end_matches("```");
let contents = match kind {
CodeBlockKind::Fenced(language) => {
contents.trim_start_matches(&language.to_string())
}
CodeBlockKind::Indented => contents,
};
let contents: String = contents.into();
let contents = SharedString::from(contents);
let code_block = div()
.mb_3()
.px_4()
.py_0()
.bg(self.theme.colors().surface_background)
.child(StyledText::new(contents));
self.finished.push(code_block);
}
Tag::Table(_alignment) => {
if self.table.is_none() {
log::error!("Table end without table ({:?})", source_range);
return;
}
let table = self.table.take().unwrap();
let table = table.finish().mb_4();
self.finished.push(table);
}
Tag::TableHead => {
if self.table.is_none() {
log::error!("Table head without table ({:?})", source_range);
return;
}
self.table.as_mut().unwrap().finish_row();
}
Tag::TableRow => {
if self.table.is_none() {
log::error!("Table row without table ({:?})", source_range);
return;
}
self.table.as_mut().unwrap().finish_row();
}
Tag::TableCell => {
if self.table.is_none() {
log::error!("Table cell without table ({:?})", source_range);
return;
}
let contents = self.render_md_from_range(source_range.clone(), cx);
self.table.as_mut().unwrap().add_cell(contents);
}
_ => {}
}
}
fn render_md_from_range(
&self,
source_range: Range<usize>,
cx: &WindowContext,
) -> gpui::AnyElement {
let mentions = &[];
let language = None;
let paragraph = &self.source_contents[source_range.clone()];
let rich_text = render_rich_text(
paragraph.into(),
mentions,
&self.language_registry,
language,
);
let id: ElementId = source_range.start.into();
rich_text.element(id, cx)
}
fn headline(&self, level: HeadingLevel) -> Div {
let size = match level {
HeadingLevel::H1 => rems(2.),
HeadingLevel::H2 => rems(1.5),
HeadingLevel::H3 => rems(1.25),
HeadingLevel::H4 => rems(1.),
HeadingLevel::H5 => rems(0.875),
HeadingLevel::H6 => rems(0.85),
};
let color = match level {
HeadingLevel::H6 => self.theme.colors().text_muted,
_ => self.theme.colors().text,
};
let line_height = DefiniteLength::from(rems(1.25));
let headline = h_flex()
.w_full()
.line_height(line_height)
.text_size(size)
.text_color(color)
.mb_4()
.pb(rems(0.15));
headline
}
}
pub fn render_markdown(
markdown_input: &str,
language_registry: &Arc<LanguageRegistry>,
cx: &WindowContext,
) -> Vec<Div> {
let theme = cx.theme().clone();
let options = Options::all();
let parser = Parser::new_ext(markdown_input, options);
let renderer = Renderer::new(
parser.into_offset_iter(),
markdown_input.to_owned(),
language_registry,
theme,
);
let renderer = renderer.run(cx);
return renderer.finished;
}

View File

@@ -39,7 +39,7 @@ lsp = { path = "../lsp" }
ordered-float.workspace = true
parking_lot.workspace = true
postage.workspace = true
pulldown-cmark = { version = "0.9.2", default-features = false }
pulldown-cmark.workspace = true
rand.workspace = true
rich_text = { path = "../rich_text" }
schemars.workspace = true

View File

@@ -195,11 +195,11 @@ impl Prettier {
},
Path::new("/"),
None,
cx,
cx.clone(),
)
.context("prettier server creation")?;
let server = executor
.spawn(server.initialize(None))
let server = cx
.update(|cx| executor.spawn(server.initialize(None, cx)))?
.await
.context("prettier server initialization")?;
Ok(Self::Real(RealPrettier {

View File

@@ -75,6 +75,7 @@ fs = { path = "../fs", features = ["test-support"] }
git2.workspace = true
gpui = { path = "../gpui", features = ["test-support"] }
language = { path = "../language", features = ["test-support"] }
release_channel = { path = "../release_channel" }
lsp = { path = "../lsp", features = ["test-support"] }
prettier = { path = "../prettier", features = ["test-support"] }
pretty_assertions.workspace = true

View File

@@ -3130,7 +3130,9 @@ impl Project {
(None, override_options) => initialization_options = override_options,
_ => {}
}
let language_server = language_server.initialize(initialization_options).await?;
let language_server = cx
.update(|cx| language_server.initialize(initialization_options, cx))?
.await?;
language_server
.notify::<lsp::notification::DidChangeConfiguration>(

View File

@@ -4380,6 +4380,7 @@ fn init_test(cx: &mut gpui::TestAppContext) {
cx.update(|cx| {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
release_channel::init("0.0.0", cx);
language::init(cx);
Project::init_settings(cx);
});

View File

@@ -33,6 +33,7 @@ gpui = { path = "../gpui", features = ["test-support"] }
language = { path = "../language", features = ["test-support"] }
lsp = { path = "../lsp", features = ["test-support"] }
project = { path = "../project", features = ["test-support"] }
release_channel = { path = "../release_channel" }
settings = { path = "../settings", features = ["test-support"] }
theme = { path = "../theme", features = ["test-support"] }
workspace = { path = "../workspace", features = ["test-support"] }

View File

@@ -392,6 +392,7 @@ mod tests {
let store = SettingsStore::test(cx);
cx.set_global(store);
theme::init(theme::LoadThemes::JustBase, cx);
release_channel::init("0.0.0", cx);
language::init(cx);
Project::init_settings(cx);
workspace::init_settings(cx);

View File

@@ -1,9 +1,9 @@
use gpui::{AppContext, Global};
use gpui::{AppContext, Global, SemanticVersion};
use once_cell::sync::Lazy;
use std::env;
#[doc(hidden)]
pub static RELEASE_CHANNEL_NAME: Lazy<String> = if cfg!(debug_assertions) {
static RELEASE_CHANNEL_NAME: Lazy<String> = if cfg!(debug_assertions) {
Lazy::new(|| {
env::var("ZED_RELEASE_CHANNEL")
.unwrap_or_else(|_| include_str!("../../zed/RELEASE_CHANNEL").trim().to_string())
@@ -11,6 +11,7 @@ pub static RELEASE_CHANNEL_NAME: Lazy<String> = if cfg!(debug_assertions) {
} else {
Lazy::new(|| include_str!("../../zed/RELEASE_CHANNEL").trim().to_string())
};
#[doc(hidden)]
pub static RELEASE_CHANNEL: Lazy<ReleaseChannel> =
Lazy::new(|| match RELEASE_CHANNEL_NAME.as_str() {
@@ -39,6 +40,29 @@ impl AppCommitSha {
}
}
struct GlobalAppVersion(SemanticVersion);
impl Global for GlobalAppVersion {}
pub struct AppVersion;
impl AppVersion {
pub fn init(pkg_version: &str, cx: &mut AppContext) {
let version = if let Some(from_env) = env::var("ZED_APP_VERSION").ok() {
from_env.parse().expect("invalid ZED_APP_VERSION")
} else {
cx.app_metadata()
.app_version
.unwrap_or_else(|| pkg_version.parse().expect("invalid version in Cargo.toml"))
};
cx.set_global(GlobalAppVersion(version))
}
pub fn global(cx: &AppContext) -> SemanticVersion {
cx.global::<GlobalAppVersion>().0
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)]
pub enum ReleaseChannel {
#[default]
@@ -52,11 +76,12 @@ struct GlobalReleaseChannel(ReleaseChannel);
impl Global for GlobalReleaseChannel {}
impl ReleaseChannel {
pub fn init(cx: &mut AppContext) {
cx.set_global(GlobalReleaseChannel(*RELEASE_CHANNEL))
}
pub fn init(pkg_version: &str, cx: &mut AppContext) {
AppVersion::init(pkg_version, cx);
cx.set_global(GlobalReleaseChannel(*RELEASE_CHANNEL))
}
impl ReleaseChannel {
pub fn global(cx: &AppContext) -> Self {
cx.global::<GlobalReleaseChannel>().0
}

View File

@@ -22,7 +22,7 @@ futures.workspace = true
gpui = { path = "../gpui" }
language = { path = "../language" }
lazy_static.workspace = true
pulldown-cmark = { version = "0.9.2", default-features = false }
pulldown-cmark.workspace = true
smallvec.workspace = true
smol.workspace = true
sum_tree = { path = "../sum_tree" }

View File

@@ -47,7 +47,7 @@ pub struct Mention {
}
impl RichText {
pub fn element(&self, id: ElementId, cx: &mut WindowContext) -> AnyElement {
pub fn element(&self, id: ElementId, cx: &WindowContext) -> AnyElement {
let theme = cx.theme();
let code_background = theme.colors().surface_background;
@@ -83,7 +83,12 @@ impl RichText {
)
.on_click(self.link_ranges.clone(), {
let link_urls = self.link_urls.clone();
move |ix, cx| cx.open_url(&link_urls[ix])
move |ix, cx| {
let url = &link_urls[ix];
if url.starts_with("http") {
cx.open_url(url);
}
}
})
.tooltip({
let link_ranges = self.link_ranges.clone();
@@ -256,7 +261,7 @@ pub fn render_markdown_mut(
}
}
pub fn render_markdown(
pub fn render_rich_text(
block: String,
mentions: &[Mention],
language_registry: &Arc<LanguageRegistry>,

View File

@@ -210,7 +210,7 @@ impl SettingsStore {
if let Some(release_settings) = &self
.raw_user_settings
.get(&*release_channel::RELEASE_CHANNEL_NAME)
.get(&*release_channel::RELEASE_CHANNEL.dev_name())
{
if let Some(release_settings) = setting_value
.deserialize_setting(&release_settings)
@@ -543,7 +543,7 @@ impl SettingsStore {
if let Some(release_settings) = &self
.raw_user_settings
.get(&*release_channel::RELEASE_CHANNEL_NAME)
.get(&*release_channel::RELEASE_CHANNEL.dev_name())
{
if let Some(release_settings) = setting_value
.deserialize_setting(&release_settings)

View File

@@ -255,19 +255,23 @@ impl ThemeRegistry {
continue;
};
let Some(reader) = fs.open_sync(&theme_path).await.log_err() else {
continue;
};
let Some(theme) = serde_json_lenient::from_reader(reader).log_err() else {
continue;
};
self.insert_user_theme_families([theme]);
self.load_user_theme(&theme_path, fs.clone())
.await
.log_err();
}
Ok(())
}
/// Loads the user theme from the specified path and adds it to the registry.
pub async fn load_user_theme(&self, theme_path: &Path, fs: Arc<dyn Fs>) -> Result<()> {
let reader = fs.open_sync(&theme_path).await?;
let theme = serde_json_lenient::from_reader(reader)?;
self.insert_user_theme_families([theme]);
Ok(())
}
}
impl Default for ThemeRegistry {

View File

@@ -68,6 +68,6 @@ pub async fn latest_github_release(
releases
.into_iter()
.find(|release| release.pre_release == pre_release)
.find(|release| !release.assets.is_empty() && release.pre_release == pre_release)
.ok_or(anyhow!("Failed to find a release"))
}

View File

@@ -1,3 +1,4 @@
use crate::http_proxy_from_env;
pub use anyhow::{anyhow, Result};
use futures::future::BoxFuture;
use isahc::config::{Configurable, RedirectPolicy};
@@ -43,6 +44,7 @@ pub fn zed_client(zed_host: &str) -> Arc<ZedHttpClient> {
isahc::HttpClient::builder()
.connect_timeout(Duration::from_secs(5))
.low_speed_timeout(100, Duration::from_secs(5))
.proxy(http_proxy_from_env())
.build()
.unwrap(),
),
@@ -95,6 +97,7 @@ pub fn client() -> Arc<dyn HttpClient> {
isahc::HttpClient::builder()
.connect_timeout(Duration::from_secs(5))
.low_speed_timeout(100, Duration::from_secs(5))
.proxy(http_proxy_from_env())
.build()
.unwrap(),
)

View File

@@ -0,0 +1,46 @@
use std::{
fmt::{self, Display},
str::FromStr,
};
use anyhow::{anyhow, Result};
use serde::Serialize;
/// A datastructure representing a semantic version number
#[derive(Clone, Copy, Debug, Default, Eq, Ord, PartialEq, PartialOrd, Serialize)]
pub struct SemanticVersion {
pub major: usize,
pub minor: usize,
pub patch: usize,
}
impl FromStr for SemanticVersion {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self> {
let mut components = s.trim().split('.');
let major = components
.next()
.ok_or_else(|| anyhow!("missing major version number"))?
.parse()?;
let minor = components
.next()
.ok_or_else(|| anyhow!("missing minor version number"))?
.parse()?;
let patch = components
.next()
.ok_or_else(|| anyhow!("missing patch version number"))?
.parse()?;
Ok(Self {
major,
minor,
patch,
})
}
}
impl Display for SemanticVersion {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
}
}

View File

@@ -3,6 +3,7 @@ pub mod fs;
pub mod github;
pub mod http;
pub mod paths;
mod semantic_version;
#[cfg(any(test, feature = "test-support"))]
pub mod test;
@@ -10,6 +11,7 @@ pub use backtrace::Backtrace;
use futures::Future;
use lazy_static::lazy_static;
use rand::{seq::SliceRandom, Rng};
pub use semantic_version::SemanticVersion;
use std::{
borrow::Cow,
cmp::{self, Ordering},
@@ -42,6 +44,28 @@ pub fn truncate(s: &str, max_chars: usize) -> &str {
}
}
pub fn http_proxy_from_env() -> Option<isahc::http::Uri> {
macro_rules! try_env {
($($env:literal),+) => {
$(
if let Ok(env) = std::env::var($env) {
return env.parse::<isahc::http::Uri>().ok();
}
)+
};
}
try_env!(
"ALL_PROXY",
"all_proxy",
"HTTPS_PROXY",
"https_proxy",
"HTTP_PROXY",
"http_proxy"
);
None
}
/// Removes characters from the end of the string if its length is greater than `max_chars` and
/// appends "..." to the string. Returns string unchanged if its length is smaller than max_chars.
pub fn truncate_and_trailoff(s: &str, max_chars: usize) -> String {

View File

@@ -43,6 +43,7 @@ zed_actions = { path = "../zed_actions" }
editor = { path = "../editor", features = ["test-support"] }
futures.workspace = true
gpui = { path = "../gpui", features = ["test-support"] }
release_channel = { path = "../release_channel" }
indoc.workspace = true
language = { path = "../language", features = ["test-support"] }
lsp = { path = "../lsp", features = ["test-support"] }

View File

@@ -23,6 +23,7 @@ impl VimTestContext {
search::init(cx);
let settings = SettingsStore::test(cx);
cx.set_global(settings);
release_channel::init("0.0.0", cx);
command_palette::init(cx);
crate::init(cx);
});

View File

@@ -204,8 +204,7 @@ impl Vim {
let editor = editor.read(cx);
if editor.leader_peer_id().is_none() {
let newest = editor.selections.newest::<usize>(cx);
let is_multicursor = editor.selections.count() > 1;
local_selections_changed(newest, is_multicursor, cx);
local_selections_changed(newest, cx);
}
}
EditorEvent::InputIgnored { text } => {
@@ -627,24 +626,13 @@ impl Settings for VimModeSetting {
}
}
fn local_selections_changed(
newest: Selection<usize>,
is_multicursor: bool,
cx: &mut WindowContext,
) {
fn local_selections_changed(newest: Selection<usize>, cx: &mut WindowContext) {
Vim::update(cx, |vim, cx| {
if vim.enabled {
if vim.state().mode == Mode::Normal && !newest.is_empty() {
if matches!(newest.goal, SelectionGoal::HorizontalRange { .. }) {
vim.switch_mode(Mode::VisualBlock, false, cx);
} else {
vim.switch_mode(Mode::Visual, false, cx)
}
} else if newest.is_empty()
&& !is_multicursor
&& [Mode::Visual, Mode::VisualLine, Mode::VisualBlock].contains(&vim.state().mode)
{
vim.switch_mode(Mode::Normal, true, cx)
if vim.enabled && vim.state().mode == Mode::Normal && !newest.is_empty() {
if matches!(newest.goal, SelectionGoal::HorizontalRange { .. }) {
vim.switch_mode(Mode::VisualBlock, false, cx);
} else {
vim.switch_mode(Mode::Visual, false, cx)
}
}
})

View File

@@ -3,8 +3,9 @@ use crate::DraggedDock;
use crate::{status_bar::StatusItemView, Workspace};
use gpui::{
div, px, Action, AnchorCorner, AnyView, AppContext, Axis, ClickEvent, Entity, EntityId,
EventEmitter, FocusHandle, FocusableView, IntoElement, MouseButton, ParentElement, Render,
SharedString, Styled, Subscription, View, ViewContext, VisualContext, WeakView, WindowContext,
EventEmitter, FocusHandle, FocusableView, IntoElement, KeyContext, MouseButton, ParentElement,
Render, SharedString, Styled, Subscription, View, ViewContext, VisualContext, WeakView,
WindowContext,
};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
@@ -534,10 +535,18 @@ impl Dock {
DockPosition::Right => crate::ToggleRightDock.boxed_clone(),
}
}
fn dispatch_context() -> KeyContext {
let mut dispatch_context = KeyContext::default();
dispatch_context.add("Dock");
dispatch_context
}
}
impl Render for Dock {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let dispatch_context = Self::dispatch_context();
if let Some(entry) = self.visible_entry() {
let size = entry.panel.size(cx);
@@ -588,6 +597,7 @@ impl Render for Dock {
}
div()
.key_context(dispatch_context)
.track_focus(&self.focus_handle)
.flex()
.bg(cx.theme().colors().panel_background)
@@ -612,7 +622,9 @@ impl Render for Dock {
)
.child(handle)
} else {
div().track_focus(&self.focus_handle)
div()
.key_context(dispatch_context)
.track_focus(&self.focus_handle)
}
}
}

View File

@@ -522,6 +522,17 @@ pub enum SplitDirection {
Right,
}
impl std::fmt::Display for SplitDirection {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SplitDirection::Up => write!(f, "up"),
SplitDirection::Down => write!(f, "down"),
SplitDirection::Left => write!(f, "left"),
SplitDirection::Right => write!(f, "right"),
}
}
}
impl SplitDirection {
pub fn all() -> [Self; 4] {
[Self::Up, Self::Down, Self::Left, Self::Right]

View File

@@ -2075,30 +2075,99 @@ impl Workspace {
direction: SplitDirection,
cx: &mut WindowContext,
) {
if let Some(pane) = self.find_pane_in_direction(direction, cx) {
cx.focus_view(pane);
use ActivateInDirectionTarget as Target;
enum Origin {
LeftDock,
RightDock,
BottomDock,
Center,
}
}
pub fn swap_pane_in_direction(
&mut self,
direction: SplitDirection,
cx: &mut ViewContext<Self>,
) {
if let Some(to) = self
.find_pane_in_direction(direction, cx)
.map(|pane| pane.clone())
{
self.center.swap(&self.active_pane.clone(), &to);
cx.notify();
let origin: Origin = [
(&self.left_dock, Origin::LeftDock),
(&self.right_dock, Origin::RightDock),
(&self.bottom_dock, Origin::BottomDock),
]
.into_iter()
.find_map(|(dock, origin)| {
if dock.focus_handle(cx).contains_focused(cx) && dock.read(cx).is_open() {
Some(origin)
} else {
None
}
})
.unwrap_or(Origin::Center);
let get_last_active_pane = || {
self.last_active_center_pane.as_ref().and_then(|p| {
let p = p.upgrade()?;
(p.read(cx).items_len() != 0).then_some(p)
})
};
let try_dock =
|dock: &View<Dock>| dock.read(cx).is_open().then(|| Target::Dock(dock.clone()));
let target = match (origin, direction) {
// We're in the center, so we first try to go to a different pane,
// otherwise try to go to a dock.
(Origin::Center, direction) => {
if let Some(pane) = self.find_pane_in_direction(direction, cx) {
Some(Target::Pane(pane))
} else {
match direction {
SplitDirection::Up => None,
SplitDirection::Down => try_dock(&self.bottom_dock),
SplitDirection::Left => try_dock(&self.left_dock),
SplitDirection::Right => try_dock(&self.right_dock),
}
}
}
(Origin::LeftDock, SplitDirection::Right) => {
if let Some(last_active_pane) = get_last_active_pane() {
Some(Target::Pane(last_active_pane))
} else {
try_dock(&self.bottom_dock).or_else(|| try_dock(&self.right_dock))
}
}
(Origin::LeftDock, SplitDirection::Down)
| (Origin::RightDock, SplitDirection::Down) => try_dock(&self.bottom_dock),
(Origin::BottomDock, SplitDirection::Up) => get_last_active_pane().map(Target::Pane),
(Origin::BottomDock, SplitDirection::Left) => try_dock(&self.left_dock),
(Origin::BottomDock, SplitDirection::Right) => try_dock(&self.right_dock),
(Origin::RightDock, SplitDirection::Left) => {
if let Some(last_active_pane) = get_last_active_pane() {
Some(Target::Pane(last_active_pane))
} else {
try_dock(&self.bottom_dock).or_else(|| try_dock(&self.left_dock))
}
}
_ => None,
};
match target {
Some(ActivateInDirectionTarget::Pane(pane)) => cx.focus_view(&pane),
Some(ActivateInDirectionTarget::Dock(dock)) => {
if let Some(panel) = dock.read(cx).active_panel() {
panel.focus_handle(cx).focus(cx);
} else {
log::error!("Could not find a focus target when in switching focus in {direction} direction for a {:?} dock", dock.read(cx).position());
}
}
None => {}
}
}
fn find_pane_in_direction(
&mut self,
direction: SplitDirection,
cx: &AppContext,
) -> Option<&View<Pane>> {
cx: &WindowContext,
) -> Option<View<Pane>> {
let Some(bounding_box) = self.center.bounding_box_for_pane(&self.active_pane) else {
return None;
};
@@ -2124,7 +2193,21 @@ impl Workspace {
Point::new(center.x, bounding_box.bottom() + distance_to_next.into())
}
};
self.center.pane_at_pixel_position(target)
self.center.pane_at_pixel_position(target).cloned()
}
pub fn swap_pane_in_direction(
&mut self,
direction: SplitDirection,
cx: &mut ViewContext<Self>,
) {
if let Some(to) = self
.find_pane_in_direction(direction, cx)
.map(|pane| pane.clone())
{
self.center.swap(&self.active_pane.clone(), &to);
cx.notify();
}
}
fn handle_pane_focused(&mut self, pane: View<Pane>, cx: &mut ViewContext<Self>) {
@@ -3488,6 +3571,11 @@ fn open_items(
})
}
enum ActivateInDirectionTarget {
Pane(View<Pane>),
Dock(View<Dock>),
}
fn notify_if_database_failed(workspace: WindowHandle<Workspace>, cx: &mut AsyncAppContext) {
const REPORT_ISSUE_URL: &str ="https://github.com/zed-industries/zed/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml";

View File

@@ -57,6 +57,7 @@ image = "0.23"
indexmap = "1.6.2"
install_cli = { path = "../install_cli" }
isahc.workspace = true
itertools = "0.11"
journal = { path = "../journal" }
language = { path = "../language" }
language_selector = { path = "../language_selector" }
@@ -65,6 +66,7 @@ lazy_static.workspace = true
libc = "0.2"
log.workspace = true
lsp = { path = "../lsp" }
markdown_preview = { path = "../markdown_preview" }
menu = { path = "../menu" }
mimalloc = "0.1"
node_runtime = { path = "../node_runtime" }
@@ -113,6 +115,7 @@ tree-sitter-css.workspace = true
tree-sitter-elixir.workspace = true
tree-sitter-elm.workspace = true
tree-sitter-embedded-template.workspace = true
tree-sitter-erlang.workspace = true
tree-sitter-gitcommit.workspace = true
tree-sitter-gleam.workspace = true
tree-sitter-glsl.workspace = true

View File

@@ -15,6 +15,7 @@ mod css;
mod deno;
mod elixir;
mod elm;
mod erlang;
mod gleam;
mod go;
mod haskell;
@@ -113,6 +114,12 @@ pub fn init(
),
}
language("gitcommit", tree_sitter_gitcommit::language(), vec![]);
language(
"erlang",
tree_sitter_erlang::language(),
vec![Arc::new(erlang::ErlangLspAdapter)],
);
language(
"gleam",
tree_sitter_gleam::language(),

View File

@@ -0,0 +1,58 @@
use anyhow::{anyhow, Result};
use async_trait::async_trait;
use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
use lsp::LanguageServerBinary;
use std::{any::Any, path::PathBuf};
pub struct ErlangLspAdapter;
#[async_trait]
impl LspAdapter for ErlangLspAdapter {
fn name(&self) -> LanguageServerName {
LanguageServerName("erlang_ls".into())
}
fn short_name(&self) -> &'static str {
"erlang_ls"
}
async fn fetch_latest_server_version(
&self,
_: &dyn LspAdapterDelegate,
) -> Result<Box<dyn 'static + Send + Any>> {
Ok(Box::new(()) as Box<_>)
}
async fn fetch_server_binary(
&self,
_version: Box<dyn 'static + Send + Any>,
_container_dir: PathBuf,
_: &dyn LspAdapterDelegate,
) -> Result<LanguageServerBinary> {
Err(anyhow!(
"erlang_ls must be installed and available in your $PATH"
))
}
async fn cached_server_binary(
&self,
_: PathBuf,
_: &dyn LspAdapterDelegate,
) -> Option<LanguageServerBinary> {
Some(LanguageServerBinary {
path: "erlang_ls".into(),
arguments: vec![],
})
}
fn can_be_reinstalled(&self) -> bool {
false
}
async fn installation_test_binary(&self, _: PathBuf) -> Option<LanguageServerBinary> {
Some(LanguageServerBinary {
path: "erlang_ls".into(),
arguments: vec!["--version".into()],
})
}
}

View File

@@ -0,0 +1,3 @@
("(" @open ")" @close)
("[" @open "]" @close)
("{" @open "}" @close)

View File

@@ -0,0 +1,23 @@
name = "Erlang"
# TODO: support parsing rebar.config files
# # https://github.com/WhatsApp/tree-sitter-erlang/issues/3
path_suffixes = ["erl", "hrl", "app.src", "escript", "xrl", "yrl", "Emakefile", "rebar.config"]
line_comments = ["% ", "%% ", "%%% "]
autoclose_before = ";:.,=}])>"
brackets = [
{ start = "{", end = "}", close = true, newline = true },
{ start = "[", end = "]", close = true, newline = true },
{ start = "(", end = ")", close = true, newline = true },
{ start = "<<", end = ">>", close = true, newline = false, not_in = ["string"] },
{ start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] },
{ start = "'", end = "'", close = true, newline = false, not_in = ["string", "comment"] },
]
# Indent if a line ends brackets, "->" or most keywords. Also if prefixed
# with "||". This should work with most formatting models.
# The ([^%]).* is to ensure this doesn't match inside comments.
increase_indent_pattern = "^([^%]).*([{(\\[]]|\\->|after|begin|case|catch|fun|if|of|try|when|maybe|else|(\\|\\|.*))\\s*$"
# Dedent after brackets, end or lone "->". The latter happens in a spec
# with indented types, typically after "when". Only do this if it's _only_
# preceded by whitespace.
decrease_indent_pattern = "^\\s*([)}\\]]|end|else|\\->\\s*$)"

View File

@@ -0,0 +1,9 @@
[
(fun_decl)
(anonymous_fun)
(case_expr)
(maybe_expr)
(map_expr)
(export_attribute)
(export_type_attribute)
] @fold

View File

@@ -0,0 +1,231 @@
;; Copyright (c) Facebook, Inc. and its affiliates.
;;
;; Licensed under the Apache License, Version 2.0 (the "License");
;; you may not use this file except in compliance with the License.
;; You may obtain a copy of the License at
;;
;; http://www.apache.org/licenses/LICENSE-2.0
;;
;; Unless required by applicable law or agreed to in writing, software
;; distributed under the License is distributed on an "AS IS" BASIS,
;; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
;; See the License for the specific language governing permissions and
;; limitations under the License.
;; ---------------------------------------------------------------------
;; Based initially on the contents of https://github.com/WhatsApp/tree-sitter-erlang/issues/2 by @Wilfred
;; and https://github.com/the-mikedavis/tree-sitter-erlang/blob/main/queries/highlights.scm
;;
;; The tests are also based on those in
;; https://github.com/the-mikedavis/tree-sitter-erlang/tree/main/test/highlight
;;
;; First match wins in this file
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Attributes
;; module attribute
(module_attribute
name: (atom) @module)
;; behaviour
(behaviour_attribute name: (atom) @module)
;; export
;; Import attribute
(import_attribute
module: (atom) @module)
;; export_type
;; optional_callbacks
;; compile
(compile_options_attribute
options: (tuple
expr: (atom)
expr: (list
exprs: (binary_op_expr
lhs: (atom)
rhs: (integer)))))
;; file attribute
;; record
(record_decl name: (atom) @type)
(record_decl name: (macro_call_expr name: (var) @constant))
(record_field name: (atom) @property)
;; type alias
;; opaque
;; Spec attribute
(spec fun: (atom) @function)
(spec
module: (module name: (atom) @module)
fun: (atom) @function)
;; callback
(callback fun: (atom) @function)
;; fun decl
;; include/include_lib
;; ifdef/ifndef
(pp_ifdef name: (_) @keyword.directive)
(pp_ifndef name: (_) @keyword.directive)
;; define
(pp_define
lhs: (macro_lhs
name: (_) @keyword.directive
args: (var_args args: (var))))
(pp_define
lhs: (macro_lhs
name: (var) @constant))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Functions
(fa fun: (atom) @function)
(type_name name: (atom) @function)
(call expr: (atom) @function)
(function_clause name: (atom) @function)
(internal_fun fun: (atom) @function)
;; This is a fudge, we should check that the operator is '/'
;; But our grammar does not (currently) provide it
(binary_op_expr lhs: (atom) @function rhs: (integer))
;; Others
(remote_module module: (atom) @module)
(remote fun: (atom) @function)
(macro_call_expr name: (var) @keyword.directive args: (_) )
(macro_call_expr name: (var) @constant)
(macro_call_expr name: (atom) @keyword.directive)
(record_field_name name: (atom) @property)
(record_name name: (atom) @type)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Reserved words
[ "after"
"and"
"band"
"begin"
"behavior"
"behaviour"
"bnot"
"bor"
"bsl"
"bsr"
"bxor"
"callback"
"case"
"catch"
"compile"
"define"
"deprecated"
"div"
"elif"
"else"
"end"
"endif"
"export"
"export_type"
"file"
"fun"
"if"
"ifdef"
"ifndef"
"import"
"include"
"include_lib"
"maybe"
"module"
"of"
"opaque"
"optional_callbacks"
"or"
"receive"
"record"
"spec"
"try"
"type"
"undef"
"unit"
"when"
"xor"] @keyword
["andalso" "orelse"] @keyword.operator
;; Punctuation
["," "." ";"] @punctuation.delimiter
["(" ")" "{" "}" "[" "]" "<<" ">>"] @punctuation.bracket
;; Operators
["!"
"->"
"<-"
"#"
"::"
"|"
":"
"="
"||"
"+"
"-"
"bnot"
"not"
"/"
"*"
"div"
"rem"
"band"
"and"
"+"
"-"
"bor"
"bxor"
"bsl"
"bsr"
"or"
"xor"
"++"
"--"
"=="
"/="
"=<"
"<"
">="
">"
"=:="
"=/="
] @operator
;;; Comments
((var) @comment.discard
(#match? @comment.discard "^_"))
(dotdotdot) @comment.discard
(comment) @comment
;; Primitive types
(string) @string
(char) @constant
(integer) @number
(var) @variable
(atom) @string.special.symbol
;; wild attribute (Should take precedence over atoms, otherwise they are highlighted as atoms)
(wild_attribute name: (attr_name name: (_) @keyword))

View File

@@ -0,0 +1,3 @@
(_ "[" "]" @end) @indent
(_ "{" "}" @end) @indent
(_ "(" ")" @end) @indent

View File

@@ -0,0 +1,31 @@
(module_attribute
"module" @context
name: (_) @name) @item
(behaviour_attribute
"behaviour" @context
(atom) @name) @item
(type_alias
"type" @context
name: (_) @name) @item
(opaque
"opaque" @context
name: (_) @name) @item
(pp_define
"define" @context
lhs: (_) @name) @item
(record_decl
"record" @context
name: (_) @name) @item
(callback
"callback" @context
fun: (_) @function ( (_) @name)) @item
(fun_decl (function_clause
name: (_) @name
args: (_) @context)) @item

View File

@@ -1,4 +1,4 @@
name = "Git commit"
name = "Git Commit"
path_suffixes = [
# Refer to https://github.com/neovim/neovim/blob/master/runtime/lua/vim/filetype.lua#L1286-L1290
"TAG_EDITMSG",

View File

@@ -0,0 +1,5 @@
(atx_heading
.
(_) @context
.
(_) @name ) @item

View File

@@ -11,6 +11,7 @@ use db::kvp::KEY_VALUE_STORE;
use editor::Editor;
use env_logger::Builder;
use fs::RealFs;
use fsevent::StreamFlags;
use futures::StreamExt;
use gpui::{App, AppContext, AsyncAppContext, Context, SemanticVersion, Task};
use isahc::{prelude::Configurable, Request};
@@ -120,7 +121,7 @@ fn main() {
});
app.run(move |cx| {
ReleaseChannel::init(cx);
release_channel::init(env!("CARGO_PKG_VERSION"), cx);
if let Some(build_sha) = option_env!("ZED_COMMIT_SHA") {
AppCommitSha::set_global(AppCommitSha(build_sha.into()), cx);
}
@@ -171,35 +172,8 @@ fn main() {
);
assistant::init(cx);
// TODO: Should we be loading the themes in a different spot?
cx.spawn({
let fs = fs.clone();
|cx| async move {
if let Some(theme_registry) =
cx.update(|cx| ThemeRegistry::global(cx).clone()).log_err()
{
if let Some(()) = theme_registry
.load_user_themes(&paths::THEMES_DIR.clone(), fs)
.await
.log_err()
{
cx.update(|cx| {
let mut theme_settings = ThemeSettings::get_global(cx).clone();
if let Some(requested_theme) = theme_settings.requested_theme.clone() {
if let Some(_theme) =
theme_settings.switch_theme(&requested_theme, cx)
{
ThemeSettings::override_global(theme_settings, cx);
}
}
})
.log_err();
}
}
}
})
.detach();
load_user_themes_in_background(fs.clone(), cx);
watch_themes(fs.clone(), cx);
cx.spawn(|_| watch_languages(fs.clone(), languages.clone()))
.detach();
@@ -274,6 +248,7 @@ fn main() {
notifications::init(app_state.client.clone(), app_state.user_store.clone(), cx);
collab_ui::init(&app_state, cx);
feedback::init(cx);
markdown_preview::init(cx);
welcome::init(cx);
cx.set_menus(app_menus());
@@ -339,7 +314,10 @@ fn main() {
})
.detach_and_log_err(cx);
}
Ok(Some(OpenRequest::OpenChannelNotes { channel_id })) => {
Ok(Some(OpenRequest::OpenChannelNotes {
channel_id,
heading,
})) => {
triggered_authentication = true;
let app_state = app_state.clone();
let client = client.clone();
@@ -348,11 +326,11 @@ fn main() {
let _ = authenticate(client, &cx).await;
let workspace_window =
workspace::get_any_active_workspace(app_state, cx.clone()).await?;
let _ = workspace_window
.update(&mut cx, |_, cx| {
ChannelView::open(channel_id, cx.view().clone(), cx)
})?
.await?;
let workspace = workspace_window.root_view(&cx)?;
cx.update_window(workspace_window.into(), |_, cx| {
ChannelView::open(channel_id, heading, workspace, cx)
})?
.await?;
anyhow::Ok(())
})
.detach_and_log_err(cx);
@@ -394,16 +372,19 @@ fn main() {
})
.log_err();
}
OpenRequest::OpenChannelNotes { channel_id } => {
OpenRequest::OpenChannelNotes {
channel_id,
heading,
} => {
let app_state = app_state.clone();
let open_notes_task = cx.spawn(|mut cx| async move {
let workspace_window =
workspace::get_any_active_workspace(app_state, cx.clone()).await?;
let _ = workspace_window
.update(&mut cx, |_, cx| {
ChannelView::open(channel_id, cx.view().clone(), cx)
})?
.await?;
let workspace = workspace_window.root_view(&cx)?;
cx.update_window(workspace_window.into(), |_, cx| {
ChannelView::open(channel_id, heading, workspace, cx)
})?
.await?;
anyhow::Ok(())
});
cx.update(|cx| open_notes_task.detach_and_log_err(cx))
@@ -608,9 +589,13 @@ fn init_panic_hook(app: &App, installation_id: Option<String>, session_id: Strin
std::process::exit(-1);
}
let app_version = client::ZED_APP_VERSION
.or(app_metadata.app_version)
.map_or("dev".to_string(), |v| v.to_string());
let app_version = if let Some(version) = app_metadata.app_version {
version.to_string()
} else {
option_env!("CARGO_PKG_VERSION")
.unwrap_or("dev")
.to_string()
};
let backtrace = Backtrace::new();
let mut backtrace = backtrace
@@ -639,7 +624,7 @@ fn init_panic_hook(app: &App, installation_id: Option<String>, session_id: Strin
file: location.file().into(),
line: location.line(),
}),
app_version: app_version.clone(),
app_version: app_version.to_string(),
release_channel: RELEASE_CHANNEL.display_name().into(),
os_name: app_metadata.os_name.into(),
os_version: app_metadata
@@ -899,6 +884,81 @@ fn load_embedded_fonts(cx: &AppContext) {
.unwrap();
}
/// Spawns a background task to load the user themes from the themes directory.
fn load_user_themes_in_background(fs: Arc<dyn fs::Fs>, cx: &mut AppContext) {
cx.spawn({
let fs = fs.clone();
|cx| async move {
if let Some(theme_registry) =
cx.update(|cx| ThemeRegistry::global(cx).clone()).log_err()
{
if let Some(()) = theme_registry
.load_user_themes(&paths::THEMES_DIR.clone(), fs)
.await
.log_err()
{
cx.update(|cx| {
let mut theme_settings = ThemeSettings::get_global(cx).clone();
if let Some(requested_theme) = theme_settings.requested_theme.clone() {
if let Some(_theme) = theme_settings.switch_theme(&requested_theme, cx)
{
ThemeSettings::override_global(theme_settings, cx);
}
}
})
.log_err();
}
}
}
})
.detach();
}
/// Spawns a background task to watch the themes directory for changes.
fn watch_themes(fs: Arc<dyn fs::Fs>, cx: &mut AppContext) {
cx.spawn(|cx| async move {
let mut events = fs
.watch(&paths::THEMES_DIR.clone(), Duration::from_millis(100))
.await;
while let Some(events) = events.next().await {
for event in events {
if event.flags.contains(StreamFlags::ITEM_REMOVED) {
// Theme was removed, don't need to reload.
// We may want to remove the theme from the registry, in this case.
} else {
if let Some(theme_registry) =
cx.update(|cx| ThemeRegistry::global(cx).clone()).log_err()
{
if let Some(()) = theme_registry
.load_user_theme(&event.path, fs.clone())
.await
.log_err()
{
cx.update(|cx| {
let mut theme_settings = ThemeSettings::get_global(cx).clone();
if let Some(requested_theme) =
theme_settings.requested_theme.clone()
{
if let Some(_theme) =
theme_settings.switch_theme(&requested_theme, cx)
{
ThemeSettings::override_global(theme_settings, cx);
}
}
})
.log_err();
}
}
}
}
}
})
.detach()
}
async fn watch_languages(fs: Arc<dyn fs::Fs>, languages: Arc<LanguageRegistry>) {
let reload_debounce = Duration::from_millis(250);

View File

@@ -7,6 +7,7 @@ use futures::channel::mpsc::{UnboundedReceiver, UnboundedSender};
use futures::channel::{mpsc, oneshot};
use futures::{FutureExt, SinkExt, StreamExt};
use gpui::{AppContext, AsyncAppContext, Global};
use itertools::Itertools;
use language::{Bias, Point};
use release_channel::parse_zed_link;
use std::collections::HashMap;
@@ -34,6 +35,7 @@ pub enum OpenRequest {
},
OpenChannelNotes {
channel_id: u64,
heading: Option<String>,
},
}
@@ -100,10 +102,20 @@ impl OpenListener {
if let Some(slug) = parts.next() {
if let Some(id_str) = slug.split("-").last() {
if let Ok(channel_id) = id_str.parse::<u64>() {
if Some("notes") == parts.next() {
return Some(OpenRequest::OpenChannelNotes { channel_id });
} else {
let Some(next) = parts.next() else {
return Some(OpenRequest::JoinChannel { channel_id });
};
if let Some(heading) = next.strip_prefix("notes#") {
return Some(OpenRequest::OpenChannelNotes {
channel_id,
heading: Some([heading].into_iter().chain(parts).join("/")),
});
} else if next == "notes" {
return Some(OpenRequest::OpenChannelNotes {
channel_id,
heading: None,
});
}
}
}

View File

@@ -50,7 +50,7 @@ Finally, Vim mode's search and replace functionality is backed by Zed's. This me
## Custom key bindings
Zed does not yet have an equivalent to vims `map` command to convert one set of keystrokes into another, however you can bind any sequence of keys to fire any Action documented in the [Key bindings documentation](https://docs.zed.dev/configuration/key-bindings).
Zed does not yet have an equivalent to vims `map` command to convert one set of keystrokes into another, however you can bind any sequence of keys to fire any Action documented in the [Key bindings documentation](https://zed.dev/docs/key-bindings).
You can edit your personal key bindings with `:keymap`.
For vim-specific shortcuts, you may find the following template a good place to start:

View File

@@ -0,0 +1,4 @@
# Erlang
- Tree Sitter: [tree-sitter-erlang](https://github.com/WhatsApp/tree-sitter-erlang)
- Language Server: [erlang_ls](https://github.com/erlang-ls/erlang_ls)

View File

@@ -0,0 +1,4 @@
# Git Commit
- Tree Sitter: [tree-sitter-gitcommit](https://github.com/gbprod/tree-sitter-gitcommit)
- Language Server: N/A

View File

@@ -2,3 +2,18 @@
- Tree Sitter: [tree-sitter-go](https://github.com/tree-sitter/tree-sitter-go)
- Language Server: [gopls](https://github.com/golang/tools/tree/master/gopls)
# Go Mod
- Tree Sitter: [tree-sitter-gomod](https://github.com/camdencheek/tree-sitter-go-mod)
- Language Server: N/A
# Go Sum
TODO: https://github.com/zed-industries/zed/pull/7139
# Go Work
- Tree Sitter:
[tree-sitter-go-work](https://github.com/d1y/tree-sitter-go-work)
- Language Server: N/A

View File

@@ -1,8 +0,0 @@
#!/bin/bash
if [[ $# < 1 ]]; then
echo "Missing version increment (major, minor, or patch)" >&2
exit 1
fi
exec script/lib/bump-version.sh collab collab-v '' $1

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