Compare commits
41 Commits
vim_click
...
display-li
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a0c10abe80 | ||
|
|
32d490b16c | ||
|
|
f99f8d232a | ||
|
|
2ee290727a | ||
|
|
12d9ebe8fd | ||
|
|
ca9c247722 | ||
|
|
5ed3b44686 | ||
|
|
69e0ea92e4 | ||
|
|
b35a7223b6 | ||
|
|
020c38a891 | ||
|
|
21f4da6bf2 | ||
|
|
da44f637ed | ||
|
|
0897c8eebd | ||
|
|
7b9d51929d | ||
|
|
5424c8bfd5 | ||
|
|
3521b50405 | ||
|
|
d4264cbe4e | ||
|
|
97be0a930c | ||
|
|
3107ed847a | ||
|
|
944a1f8fb0 | ||
|
|
47a1ff7df9 | ||
|
|
b9d5eb17a3 | ||
|
|
adc7cfb0d3 | ||
|
|
a853a80634 | ||
|
|
2d41a119b3 | ||
|
|
0102ffbfca | ||
|
|
0edffd9248 | ||
|
|
e65a76f0ec | ||
|
|
6c93c4bd35 | ||
|
|
8bafc61ef5 | ||
|
|
3b882918f7 | ||
|
|
5e64d45194 | ||
|
|
3df7da236d | ||
|
|
5e81d780bd | ||
|
|
cbc2746d70 | ||
|
|
aaba98d8ec | ||
|
|
2cc2a61c77 | ||
|
|
3025e5620d | ||
|
|
c4083c3cf6 | ||
|
|
2187513026 | ||
|
|
5b7b5bfea5 |
1
.github/workflows/ci.yml
vendored
1
.github/workflows/ci.yml
vendored
@@ -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
107
.github/workflows/deploy_collab.yml
vendored
Normal 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
|
||||
49
.github/workflows/publish_collab_image.yml
vendored
49
.github/workflows/publish_collab_image.yml
vendored
@@ -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}
|
||||
1
.github/workflows/release_nightly.yml
vendored
1
.github/workflows/release_nightly.yml
vendored
@@ -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
45
Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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 \
|
||||
|
||||
1
assets/icons/file_icons/erlang.svg
Normal file
1
assets/icons/file_icons/erlang.svg
Normal 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 |
@@ -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"
|
||||
},
|
||||
|
||||
1
assets/icons/file_icons/yaml.svg
Normal file
1
assets/icons/file_icons/yaml.svg
Normal 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 |
@@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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
|
||||
{}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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| {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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?;
|
||||
|
||||
@@ -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>) {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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!()
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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(
|
||||
|
||||
31
crates/markdown_preview/Cargo.toml
Normal file
31
crates/markdown_preview/Cargo.toml
Normal 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"] }
|
||||
1
crates/markdown_preview/LICENSE-GPL
Symbolic link
1
crates/markdown_preview/LICENSE-GPL
Symbolic link
@@ -0,0 +1 @@
|
||||
../../LICENSE-GPL
|
||||
14
crates/markdown_preview/src/markdown_preview.rs
Normal file
14
crates/markdown_preview/src/markdown_preview.rs
Normal 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();
|
||||
}
|
||||
137
crates/markdown_preview/src/markdown_preview_view.rs
Normal file
137
crates/markdown_preview/src/markdown_preview_view.rs
Normal 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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
346
crates/markdown_preview/src/markdown_renderer.rs
Normal file
346
crates/markdown_preview/src/markdown_renderer.rs
Normal 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;
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>(
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"))
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
)
|
||||
|
||||
46
crates/util/src/semantic_version.rs
Normal file
46
crates/util/src/semantic_version.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(),
|
||||
|
||||
58
crates/zed/src/languages/erlang.rs
Normal file
58
crates/zed/src/languages/erlang.rs
Normal 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()],
|
||||
})
|
||||
}
|
||||
}
|
||||
3
crates/zed/src/languages/erlang/brackets.scm
Normal file
3
crates/zed/src/languages/erlang/brackets.scm
Normal file
@@ -0,0 +1,3 @@
|
||||
("(" @open ")" @close)
|
||||
("[" @open "]" @close)
|
||||
("{" @open "}" @close)
|
||||
23
crates/zed/src/languages/erlang/config.toml
Normal file
23
crates/zed/src/languages/erlang/config.toml
Normal 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*$)"
|
||||
9
crates/zed/src/languages/erlang/folds.scm
Normal file
9
crates/zed/src/languages/erlang/folds.scm
Normal file
@@ -0,0 +1,9 @@
|
||||
[
|
||||
(fun_decl)
|
||||
(anonymous_fun)
|
||||
(case_expr)
|
||||
(maybe_expr)
|
||||
(map_expr)
|
||||
(export_attribute)
|
||||
(export_type_attribute)
|
||||
] @fold
|
||||
231
crates/zed/src/languages/erlang/highlights.scm
Normal file
231
crates/zed/src/languages/erlang/highlights.scm
Normal 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))
|
||||
3
crates/zed/src/languages/erlang/indents.scm
Normal file
3
crates/zed/src/languages/erlang/indents.scm
Normal file
@@ -0,0 +1,3 @@
|
||||
(_ "[" "]" @end) @indent
|
||||
(_ "{" "}" @end) @indent
|
||||
(_ "(" ")" @end) @indent
|
||||
31
crates/zed/src/languages/erlang/outline.scm
Normal file
31
crates/zed/src/languages/erlang/outline.scm
Normal 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
|
||||
@@ -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",
|
||||
|
||||
5
crates/zed/src/languages/markdown/outline.scm
Normal file
5
crates/zed/src/languages/markdown/outline.scm
Normal file
@@ -0,0 +1,5 @@
|
||||
(atx_heading
|
||||
.
|
||||
(_) @context
|
||||
.
|
||||
(_) @name ) @item
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 vim’s `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 vim’s `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:
|
||||
|
||||
4
docs/src/languages/erlang.md
Normal file
4
docs/src/languages/erlang.md
Normal 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)
|
||||
4
docs/src/languages/gitcommit.md
Normal file
4
docs/src/languages/gitcommit.md
Normal file
@@ -0,0 +1,4 @@
|
||||
# Git Commit
|
||||
|
||||
- Tree Sitter: [tree-sitter-gitcommit](https://github.com/gbprod/tree-sitter-gitcommit)
|
||||
- Language Server: N/A
|
||||
@@ -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
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user