Compare commits
32 Commits
vim_click
...
views-post
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
809776f6bc | ||
|
|
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 }}
|
MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
|
||||||
APPLE_NOTARIZATION_USERNAME: ${{ secrets.APPLE_NOTARIZATION_USERNAME }}
|
APPLE_NOTARIZATION_USERNAME: ${{ secrets.APPLE_NOTARIZATION_USERNAME }}
|
||||||
APPLE_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_NOTARIZATION_PASSWORD }}
|
APPLE_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_NOTARIZATION_PASSWORD }}
|
||||||
|
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
|
||||||
steps:
|
steps:
|
||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v3
|
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}
|
|
||||||
44
Cargo.lock
generated
44
Cargo.lock
generated
@@ -1367,6 +1367,7 @@ dependencies = [
|
|||||||
"image",
|
"image",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"log",
|
"log",
|
||||||
|
"once_cell",
|
||||||
"parking_lot 0.11.2",
|
"parking_lot 0.11.2",
|
||||||
"postage",
|
"postage",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
@@ -1377,6 +1378,7 @@ dependencies = [
|
|||||||
"serde_derive",
|
"serde_derive",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"settings",
|
"settings",
|
||||||
|
"sha2 0.10.7",
|
||||||
"smol",
|
"smol",
|
||||||
"sum_tree",
|
"sum_tree",
|
||||||
"sysinfo",
|
"sysinfo",
|
||||||
@@ -1438,7 +1440,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "collab"
|
name = "collab"
|
||||||
version = "0.43.0"
|
version = "0.44.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
@@ -1484,6 +1486,7 @@ dependencies = [
|
|||||||
"prometheus",
|
"prometheus",
|
||||||
"prost 0.8.0",
|
"prost 0.8.0",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
|
"release_channel",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"rpc",
|
"rpc",
|
||||||
"scrypt",
|
"scrypt",
|
||||||
@@ -2422,6 +2425,7 @@ dependencies = [
|
|||||||
"postage",
|
"postage",
|
||||||
"project",
|
"project",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
|
"release_channel",
|
||||||
"rich_text",
|
"rich_text",
|
||||||
"rpc",
|
"rpc",
|
||||||
"schemars",
|
"schemars",
|
||||||
@@ -2658,6 +2662,7 @@ dependencies = [
|
|||||||
"env_logger",
|
"env_logger",
|
||||||
"fuzzy",
|
"fuzzy",
|
||||||
"gpui",
|
"gpui",
|
||||||
|
"itertools 0.11.0",
|
||||||
"language",
|
"language",
|
||||||
"menu",
|
"menu",
|
||||||
"picker",
|
"picker",
|
||||||
@@ -4017,6 +4022,7 @@ dependencies = [
|
|||||||
"language",
|
"language",
|
||||||
"lsp",
|
"lsp",
|
||||||
"project",
|
"project",
|
||||||
|
"release_channel",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"settings",
|
"settings",
|
||||||
@@ -4261,6 +4267,7 @@ dependencies = [
|
|||||||
"lsp-types",
|
"lsp-types",
|
||||||
"parking_lot 0.11.2",
|
"parking_lot 0.11.2",
|
||||||
"postage",
|
"postage",
|
||||||
|
"release_channel",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@@ -4314,6 +4321,26 @@ dependencies = [
|
|||||||
"libc",
|
"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]]
|
[[package]]
|
||||||
name = "matchers"
|
name = "matchers"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -5795,6 +5822,7 @@ dependencies = [
|
|||||||
"pretty_assertions",
|
"pretty_assertions",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
"regex",
|
"regex",
|
||||||
|
"release_channel",
|
||||||
"rpc",
|
"rpc",
|
||||||
"schemars",
|
"schemars",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -5859,6 +5887,7 @@ dependencies = [
|
|||||||
"picker",
|
"picker",
|
||||||
"postage",
|
"postage",
|
||||||
"project",
|
"project",
|
||||||
|
"release_channel",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"settings",
|
"settings",
|
||||||
"smol",
|
"smol",
|
||||||
@@ -8783,6 +8812,16 @@ dependencies = [
|
|||||||
"tree-sitter",
|
"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]]
|
[[package]]
|
||||||
name = "tree-sitter-gitcommit"
|
name = "tree-sitter-gitcommit"
|
||||||
version = "0.3.3"
|
version = "0.3.3"
|
||||||
@@ -9419,6 +9458,7 @@ dependencies = [
|
|||||||
"parking_lot 0.11.2",
|
"parking_lot 0.11.2",
|
||||||
"project",
|
"project",
|
||||||
"regex",
|
"regex",
|
||||||
|
"release_channel",
|
||||||
"search",
|
"search",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
@@ -10312,6 +10352,7 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
"log",
|
"log",
|
||||||
"lsp",
|
"lsp",
|
||||||
|
"markdown_preview",
|
||||||
"menu",
|
"menu",
|
||||||
"mimalloc",
|
"mimalloc",
|
||||||
"node_runtime",
|
"node_runtime",
|
||||||
@@ -10361,6 +10402,7 @@ dependencies = [
|
|||||||
"tree-sitter-elixir",
|
"tree-sitter-elixir",
|
||||||
"tree-sitter-elm",
|
"tree-sitter-elm",
|
||||||
"tree-sitter-embedded-template",
|
"tree-sitter-embedded-template",
|
||||||
|
"tree-sitter-erlang",
|
||||||
"tree-sitter-gitcommit",
|
"tree-sitter-gitcommit",
|
||||||
"tree-sitter-gleam",
|
"tree-sitter-gleam",
|
||||||
"tree-sitter-glsl",
|
"tree-sitter-glsl",
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ members = [
|
|||||||
"crates/live_kit_client",
|
"crates/live_kit_client",
|
||||||
"crates/live_kit_server",
|
"crates/live_kit_server",
|
||||||
"crates/lsp",
|
"crates/lsp",
|
||||||
|
"crates/markdown_preview",
|
||||||
"crates/media",
|
"crates/media",
|
||||||
"crates/menu",
|
"crates/menu",
|
||||||
"crates/multi_buffer",
|
"crates/multi_buffer",
|
||||||
@@ -111,6 +112,7 @@ parking_lot = "0.11.1"
|
|||||||
postage = { version = "0.5", features = ["futures-traits"] }
|
postage = { version = "0.5", features = ["futures-traits"] }
|
||||||
pretty_assertions = "1.3.0"
|
pretty_assertions = "1.3.0"
|
||||||
prost = "0.8"
|
prost = "0.8"
|
||||||
|
pulldown-cmark = { version = "0.9.2", default-features = false }
|
||||||
rand = "0.8.5"
|
rand = "0.8.5"
|
||||||
refineable = { path = "./crates/refineable" }
|
refineable = { path = "./crates/refineable" }
|
||||||
regex = "1.5"
|
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-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-elm = { git = "https://github.com/elm-tooling/tree-sitter-elm", rev = "692c50c0b961364c40299e73c1306aecb5d20f40" }
|
||||||
tree-sitter-embedded-template = "0.20.0"
|
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-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-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" }
|
tree-sitter-glsl = { git = "https://github.com/theHamsta/tree-sitter-glsl", rev = "2a56fb7bc8bb03a1892b4741279dd0a8758b7fb3" }
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ COPY . .
|
|||||||
|
|
||||||
# Compile collab server
|
# Compile collab server
|
||||||
ARG CARGO_PROFILE_RELEASE_PANIC=abort
|
ARG CARGO_PROFILE_RELEASE_PANIC=abort
|
||||||
|
ARG GITHUB_SHA
|
||||||
|
|
||||||
|
ENV GITHUB_SHA=$GITHUB_SHA
|
||||||
RUN --mount=type=cache,target=./script/node_modules \
|
RUN --mount=type=cache,target=./script/node_modules \
|
||||||
--mount=type=cache,target=/usr/local/cargo/registry \
|
--mount=type=cache,target=/usr/local/cargo/registry \
|
||||||
--mount=type=cache,target=./target \
|
--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": {
|
"suffixes": {
|
||||||
|
"Emakefile": "erlang",
|
||||||
"aac": "audio",
|
"aac": "audio",
|
||||||
"accdb": "storage",
|
"accdb": "storage",
|
||||||
|
"app.src": "erlang",
|
||||||
"avif": "image",
|
"avif": "image",
|
||||||
"bak": "backup",
|
"bak": "backup",
|
||||||
"bash": "terminal",
|
"bash": "terminal",
|
||||||
@@ -23,6 +25,8 @@
|
|||||||
"doc": "document",
|
"doc": "document",
|
||||||
"docx": "document",
|
"docx": "document",
|
||||||
"eex": "elixir",
|
"eex": "elixir",
|
||||||
|
"erl": "erlang",
|
||||||
|
"escript": "erlang",
|
||||||
"eslintrc": "eslint",
|
"eslintrc": "eslint",
|
||||||
"eslintrc.js": "eslint",
|
"eslintrc.js": "eslint",
|
||||||
"eslintrc.json": "eslint",
|
"eslintrc.json": "eslint",
|
||||||
@@ -37,17 +41,18 @@
|
|||||||
"gif": "image",
|
"gif": "image",
|
||||||
"gitattributes": "vcs",
|
"gitattributes": "vcs",
|
||||||
"gitignore": "vcs",
|
"gitignore": "vcs",
|
||||||
"gitmodules": "vcs",
|
|
||||||
"gitkeep": "vcs",
|
"gitkeep": "vcs",
|
||||||
|
"gitmodules": "vcs",
|
||||||
"go": "go",
|
"go": "go",
|
||||||
"h": "code",
|
"h": "code",
|
||||||
"handlebars": "code",
|
"handlebars": "code",
|
||||||
"hbs": "template",
|
"hbs": "template",
|
||||||
"heex": "elixir",
|
"heex": "elixir",
|
||||||
"heif": "image",
|
"heif": "image",
|
||||||
|
"hrl": "erlang",
|
||||||
|
"hs": "haskell",
|
||||||
"htm": "template",
|
"htm": "template",
|
||||||
"html": "template",
|
"html": "template",
|
||||||
"hs": "haskell",
|
|
||||||
"ib": "storage",
|
"ib": "storage",
|
||||||
"ico": "image",
|
"ico": "image",
|
||||||
"ini": "settings",
|
"ini": "settings",
|
||||||
@@ -85,6 +90,7 @@
|
|||||||
"psd": "image",
|
"psd": "image",
|
||||||
"py": "python",
|
"py": "python",
|
||||||
"rb": "ruby",
|
"rb": "ruby",
|
||||||
|
"rebar.config": "erlang",
|
||||||
"rkt": "code",
|
"rkt": "code",
|
||||||
"rs": "rust",
|
"rs": "rust",
|
||||||
"rtf": "document",
|
"rtf": "document",
|
||||||
@@ -104,13 +110,15 @@
|
|||||||
"txt": "document",
|
"txt": "document",
|
||||||
"vue": "vue",
|
"vue": "vue",
|
||||||
"wav": "audio",
|
"wav": "audio",
|
||||||
"webp": "image",
|
|
||||||
"webm": "video",
|
"webm": "video",
|
||||||
|
"webp": "image",
|
||||||
"xls": "document",
|
"xls": "document",
|
||||||
"xlsx": "document",
|
"xlsx": "document",
|
||||||
"xml": "template",
|
"xml": "template",
|
||||||
"yaml": "settings",
|
"xrl": "erlang",
|
||||||
"yml": "settings",
|
"yaml": "yaml",
|
||||||
|
"yml": "yaml",
|
||||||
|
"yrl": "erlang",
|
||||||
"zlogin": "terminal",
|
"zlogin": "terminal",
|
||||||
"zsh": "terminal",
|
"zsh": "terminal",
|
||||||
"zsh_aliases": "terminal",
|
"zsh_aliases": "terminal",
|
||||||
@@ -133,7 +141,7 @@
|
|||||||
"icon": "icons/file_icons/folder.svg"
|
"icon": "icons/file_icons/folder.svg"
|
||||||
},
|
},
|
||||||
"css": {
|
"css": {
|
||||||
"icon": "icons/file_icons/css.svg"
|
"icon": "icons/file_icons/css.svg"
|
||||||
},
|
},
|
||||||
"default": {
|
"default": {
|
||||||
"icon": "icons/file_icons/file.svg"
|
"icon": "icons/file_icons/file.svg"
|
||||||
@@ -144,6 +152,9 @@
|
|||||||
"elixir": {
|
"elixir": {
|
||||||
"icon": "icons/file_icons/elixir.svg"
|
"icon": "icons/file_icons/elixir.svg"
|
||||||
},
|
},
|
||||||
|
"erlang": {
|
||||||
|
"icon": "icons/file_icons/erlang.svg"
|
||||||
|
},
|
||||||
"eslint": {
|
"eslint": {
|
||||||
"icon": "icons/file_icons/eslint.svg"
|
"icon": "icons/file_icons/eslint.svg"
|
||||||
},
|
},
|
||||||
@@ -174,6 +185,9 @@
|
|||||||
"php": {
|
"php": {
|
||||||
"icon": "icons/file_icons/php.svg"
|
"icon": "icons/file_icons/php.svg"
|
||||||
},
|
},
|
||||||
|
"yaml": {
|
||||||
|
"icon": "icons/file_icons/yaml.svg"
|
||||||
|
},
|
||||||
"prettier": {
|
"prettier": {
|
||||||
"icon": "icons/file_icons/prettier.svg"
|
"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 |
@@ -96,6 +96,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
";": "vim::RepeatFind",
|
||||||
|
",": "vim::RepeatFindReversed",
|
||||||
"ctrl-o": "pane::GoBack",
|
"ctrl-o": "pane::GoBack",
|
||||||
"ctrl-i": "pane::GoForward",
|
"ctrl-i": "pane::GoForward",
|
||||||
"ctrl-]": "editor::GoToDefinition",
|
"ctrl-]": "editor::GoToDefinition",
|
||||||
@@ -333,8 +335,6 @@
|
|||||||
],
|
],
|
||||||
"*": "vim::MoveToNext",
|
"*": "vim::MoveToNext",
|
||||||
"#": "vim::MoveToPrev",
|
"#": "vim::MoveToPrev",
|
||||||
";": "vim::RepeatFind",
|
|
||||||
",": "vim::RepeatFindReversed",
|
|
||||||
"r": ["vim::PushOperator", "Replace"],
|
"r": ["vim::PushOperator", "Replace"],
|
||||||
"s": "vim::Substitute",
|
"s": "vim::Substitute",
|
||||||
"shift-s": "vim::SubstituteLine",
|
"shift-s": "vim::SubstituteLine",
|
||||||
@@ -502,5 +502,18 @@
|
|||||||
"enter": "vim::SearchSubmit",
|
"enter": "vim::SearchSubmit",
|
||||||
"escape": "buffer_search::Dismiss"
|
"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
|
// Folder-specific settings
|
||||||
//
|
//
|
||||||
// For a full list of overridable settings, and general information on 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));
|
.update(cx, |toolbar, cx| toolbar.focus_changed(true, cx));
|
||||||
cx.notify();
|
cx.notify();
|
||||||
if self.focus_handle.is_focused(cx) {
|
if self.focus_handle.is_focused(cx) {
|
||||||
if let Some(editor) = self.active_editor() {
|
if self.has_credentials() {
|
||||||
cx.focus_view(editor);
|
if let Some(editor) = self.active_editor() {
|
||||||
} else if let Some(api_key_editor) = self.api_key_editor.as_ref() {
|
cx.focus_view(editor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(api_key_editor) = self.api_key_editor.as_ref() {
|
||||||
cx.focus_view(api_key_editor);
|
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> {
|
fn new_conversation(&mut self, cx: &mut ViewContext<Self>) -> View<ConversationEditor> {
|
||||||
let editor = cx.new_view(|cx| {
|
let editor = cx.new_view(|cx| {
|
||||||
ConversationEditor::new(
|
ConversationEditor::new(
|
||||||
@@ -870,7 +878,7 @@ impl AssistantPanel {
|
|||||||
cx.update(|cx| completion_provider.delete_credentials(cx))?
|
cx.update(|cx| completion_provider.delete_credentials(cx))?
|
||||||
.await;
|
.await;
|
||||||
this.update(&mut cx, |this, cx| {
|
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);
|
this.focus_handle.focus(cx);
|
||||||
cx.notify();
|
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| {
|
cx.new_view(|cx| {
|
||||||
let mut editor = Editor::single_line(cx);
|
let mut editor = Editor::single_line(cx);
|
||||||
editor.set_placeholder_text("sk-000000000000000000000000000000000000000000000000", 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 {
|
impl Render for AssistantPanel {
|
||||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||||
if let Some(api_key_editor) = self.api_key_editor.clone() {
|
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.",
|
"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",
|
" - 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.",
|
" - Having a subscription for another service like GitHub Copilot won't work.",
|
||||||
" ",
|
" ",
|
||||||
"Paste your OpenAI API key and press Enter to use the assistant:"
|
"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 {
|
cx.spawn(|this, mut cx| async move {
|
||||||
load_credentials.await;
|
load_credentials.await;
|
||||||
this.update(&mut cx, |this, cx| {
|
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);
|
this.new_conversation(cx);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
mod update_notification;
|
mod update_notification;
|
||||||
|
|
||||||
use anyhow::{anyhow, Context, Result};
|
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::kvp::KEY_VALUE_STORE;
|
||||||
use db::RELEASE_CHANNEL;
|
use db::RELEASE_CHANNEL;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
@@ -108,29 +108,28 @@ pub fn init(http_client: Arc<ZedHttpClient>, cx: &mut AppContext) {
|
|||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
|
|
||||||
if let Some(version) = ZED_APP_VERSION.or_else(|| cx.app_metadata().app_version) {
|
let version = release_channel::AppVersion::global(cx);
|
||||||
let auto_updater = cx.new_model(|cx| {
|
let auto_updater = cx.new_model(|cx| {
|
||||||
let updater = AutoUpdater::new(version, http_client);
|
let updater = AutoUpdater::new(version, http_client);
|
||||||
|
|
||||||
let mut update_subscription = AutoUpdateSetting::get_global(cx)
|
let mut update_subscription = AutoUpdateSetting::get_global(cx)
|
||||||
.0
|
.0
|
||||||
.then(|| updater.start_polling(cx));
|
.then(|| updater.start_polling(cx));
|
||||||
|
|
||||||
cx.observe_global::<SettingsStore>(move |updater, cx| {
|
cx.observe_global::<SettingsStore>(move |updater, cx| {
|
||||||
if AutoUpdateSetting::get_global(cx).0 {
|
if AutoUpdateSetting::get_global(cx).0 {
|
||||||
if update_subscription.is_none() {
|
if update_subscription.is_none() {
|
||||||
update_subscription = Some(updater.start_polling(cx))
|
update_subscription = Some(updater.start_polling(cx))
|
||||||
}
|
|
||||||
} else {
|
|
||||||
update_subscription.take();
|
|
||||||
}
|
}
|
||||||
})
|
} else {
|
||||||
.detach();
|
update_subscription.take();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
updater
|
updater
|
||||||
});
|
});
|
||||||
cx.set_global(GlobalAutoUpdate(Some(auto_updater)));
|
cx.set_global(GlobalAutoUpdate(Some(auto_updater)));
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn check(_: &Check, cx: &mut WindowContext) {
|
pub fn check(_: &Check, cx: &mut WindowContext) {
|
||||||
|
|||||||
@@ -329,6 +329,7 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
|
|||||||
fn init_test(cx: &mut AppContext) -> Model<ChannelStore> {
|
fn init_test(cx: &mut AppContext) -> Model<ChannelStore> {
|
||||||
let settings_store = SettingsStore::test(cx);
|
let settings_store = SettingsStore::test(cx);
|
||||||
cx.set_global(settings_store);
|
cx.set_global(settings_store);
|
||||||
|
release_channel::init("0.0.0", cx);
|
||||||
client::init_settings(cx);
|
client::init_settings(cx);
|
||||||
|
|
||||||
let http = FakeHttpClient::with_404_response();
|
let http = FakeHttpClient::with_404_response();
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ futures.workspace = true
|
|||||||
image = "0.23"
|
image = "0.23"
|
||||||
lazy_static.workspace = true
|
lazy_static.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
|
once_cell = "1.19.0"
|
||||||
parking_lot.workspace = true
|
parking_lot.workspace = true
|
||||||
postage.workspace = true
|
postage.workspace = true
|
||||||
rand.workspace = true
|
rand.workspace = true
|
||||||
@@ -39,6 +40,7 @@ schemars.workspace = true
|
|||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_derive.workspace = true
|
serde_derive.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
|
sha2 = "0.10"
|
||||||
smol.workspace = true
|
smol.workspace = true
|
||||||
sysinfo.workspace = true
|
sysinfo.workspace = true
|
||||||
tempfile.workspace = true
|
tempfile.workspace = true
|
||||||
|
|||||||
@@ -15,14 +15,13 @@ use futures::{
|
|||||||
TryFutureExt as _, TryStreamExt,
|
TryFutureExt as _, TryStreamExt,
|
||||||
};
|
};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions, AnyModel, AnyWeakModel, AppContext, AsyncAppContext, Global, Model, SemanticVersion,
|
actions, AnyModel, AnyWeakModel, AppContext, AsyncAppContext, Global, Model, Task, WeakModel,
|
||||||
Task, WeakModel,
|
|
||||||
};
|
};
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use parking_lot::RwLock;
|
use parking_lot::RwLock;
|
||||||
use postage::watch;
|
use postage::watch;
|
||||||
use rand::prelude::*;
|
use rand::prelude::*;
|
||||||
use release_channel::ReleaseChannel;
|
use release_channel::{AppVersion, ReleaseChannel};
|
||||||
use rpc::proto::{AnyTypedEnvelope, EntityMessage, EnvelopedMessage, PeerId, RequestMessage};
|
use rpc::proto::{AnyTypedEnvelope, EntityMessage, EnvelopedMessage, PeerId, RequestMessage};
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
use serde::{Deserialize, Serialize};
|
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")
|
pub static ref ADMIN_API_TOKEN: Option<String> = std::env::var("ZED_ADMIN_API_TOKEN")
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|s| if s.is_empty() { None } else { Some(s) });
|
.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> =
|
pub static ref ZED_APP_PATH: Option<PathBuf> =
|
||||||
std::env::var("ZED_APP_PATH").ok().map(PathBuf::from);
|
std::env::var("ZED_APP_PATH").ok().map(PathBuf::from);
|
||||||
pub static ref ZED_ALWAYS_ACTIVE: bool =
|
pub static ref ZED_ALWAYS_ACTIVE: bool =
|
||||||
@@ -1011,13 +1007,22 @@ impl Client {
|
|||||||
.update(|cx| ReleaseChannel::try_global(cx))
|
.update(|cx| ReleaseChannel::try_global(cx))
|
||||||
.ok()
|
.ok()
|
||||||
.flatten();
|
.flatten();
|
||||||
|
let app_version = cx
|
||||||
|
.update(|cx| AppVersion::global(cx).to_string())
|
||||||
|
.ok()
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
let request = Request::builder()
|
let request = Request::builder()
|
||||||
.header(
|
.header(
|
||||||
"Authorization",
|
"Authorization",
|
||||||
format!("{} {}", credentials.user_id, credentials.access_token),
|
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();
|
let http = self.http.clone();
|
||||||
cx.background_executor().spawn(async move {
|
cx.background_executor().spawn(async move {
|
||||||
|
|||||||
@@ -4,16 +4,19 @@ use crate::TelemetrySettings;
|
|||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use futures::Future;
|
use futures::Future;
|
||||||
use gpui::{AppContext, AppMetadata, BackgroundExecutor, Task};
|
use gpui::{AppContext, AppMetadata, BackgroundExecutor, Task};
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use release_channel::ReleaseChannel;
|
use release_channel::ReleaseChannel;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use settings::{Settings, SettingsStore};
|
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::{
|
use sysinfo::{
|
||||||
CpuRefreshKind, Pid, PidExt, ProcessExt, ProcessRefreshKind, RefreshKind, System, SystemExt,
|
CpuRefreshKind, Pid, PidExt, ProcessExt, ProcessRefreshKind, RefreshKind, System, SystemExt,
|
||||||
};
|
};
|
||||||
use tempfile::NamedTempFile;
|
use tempfile::NamedTempFile;
|
||||||
use util::http::{HttpClient, ZedHttpClient};
|
use util::http::{self, HttpClient, Method, ZedHttpClient};
|
||||||
#[cfg(not(debug_assertions))]
|
#[cfg(not(debug_assertions))]
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
use util::TryFutureExt;
|
use util::TryFutureExt;
|
||||||
@@ -142,6 +145,13 @@ const FLUSH_INTERVAL: Duration = Duration::from_secs(1);
|
|||||||
#[cfg(not(debug_assertions))]
|
#[cfg(not(debug_assertions))]
|
||||||
const FLUSH_INTERVAL: Duration = Duration::from_secs(60 * 5);
|
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 {
|
impl Telemetry {
|
||||||
pub fn new(client: Arc<ZedHttpClient>, cx: &mut AppContext) -> Arc<Self> {
|
pub fn new(client: Arc<ZedHttpClient>, cx: &mut AppContext) -> Arc<Self> {
|
||||||
let release_channel =
|
let release_channel =
|
||||||
@@ -540,9 +550,27 @@ impl Telemetry {
|
|||||||
serde_json::to_writer(&mut json_bytes, &request_body)?;
|
serde_json::to_writer(&mut json_bytes, &request_body)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.http_client
|
let mut summer = Sha256::new();
|
||||||
.post_json(&this.http_client.zed_url("/api/events"), json_bytes.into())
|
summer.update(&*ZED_CLIENT_CHECKSUM_SEED);
|
||||||
.await?;
|
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(())
|
anyhow::Ok(())
|
||||||
}
|
}
|
||||||
.log_err(),
|
.log_err(),
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"]
|
|||||||
default-run = "collab"
|
default-run = "collab"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
name = "collab"
|
name = "collab"
|
||||||
version = "0.43.0"
|
version = "0.44.0"
|
||||||
publish = false
|
publish = false
|
||||||
license = "AGPL-3.0-or-later"
|
license = "AGPL-3.0-or-later"
|
||||||
|
|
||||||
@@ -61,6 +61,7 @@ util = { path = "../util" }
|
|||||||
uuid.workspace = true
|
uuid.workspace = true
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
release_channel = { path = "../release_channel" }
|
||||||
async-trait.workspace = true
|
async-trait.workspace = true
|
||||||
audio = { path = "../audio" }
|
audio = { path = "../audio" }
|
||||||
call = { path = "../call", features = ["test-support"] }
|
call = { path = "../call", features = ["test-support"] }
|
||||||
|
|||||||
@@ -3,3 +3,35 @@
|
|||||||
This crate is what we run at https://collab.zed.dev.
|
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.
|
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;
|
use util::ResultExt;
|
||||||
|
|
||||||
const VERSION: &'static str = env!("CARGO_PKG_VERSION");
|
const VERSION: &'static str = env!("CARGO_PKG_VERSION");
|
||||||
|
const REVISION: Option<&'static str> = option_env!("GITHUB_SHA");
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<()> {
|
async fn main() -> Result<()> {
|
||||||
@@ -26,7 +27,7 @@ async fn main() -> Result<()> {
|
|||||||
|
|
||||||
match args().skip(1).next().as_deref() {
|
match args().skip(1).next().as_deref() {
|
||||||
Some("version") => {
|
Some("version") => {
|
||||||
println!("collab v{VERSION}");
|
println!("collab v{} ({})", VERSION, REVISION.unwrap_or("unknown"));
|
||||||
}
|
}
|
||||||
Some("migrate") => {
|
Some("migrate") => {
|
||||||
run_migrations().await?;
|
run_migrations().await?;
|
||||||
@@ -105,7 +106,7 @@ async fn run_migrations() -> Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_root() -> String {
|
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> {
|
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 tokio::sync::{watch, Semaphore};
|
||||||
use tower::ServiceBuilder;
|
use tower::ServiceBuilder;
|
||||||
use tracing::{field, info_span, instrument, Instrument};
|
use tracing::{field, info_span, instrument, Instrument};
|
||||||
|
use util::SemanticVersion;
|
||||||
|
|
||||||
pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30);
|
pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30);
|
||||||
pub const CLEANUP_TIMEOUT: Duration = Duration::from_secs(10);
|
pub const CLEANUP_TIMEOUT: Duration = Duration::from_secs(10);
|
||||||
@@ -795,6 +796,7 @@ fn broadcast<F>(
|
|||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
static ref ZED_PROTOCOL_VERSION: HeaderName = HeaderName::from_static("x-zed-protocol-version");
|
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);
|
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> {
|
pub fn routes(server: Arc<Server>) -> Router<Body> {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/rpc", get(handle_websocket_request))
|
.route("/rpc", get(handle_websocket_request))
|
||||||
@@ -838,6 +866,7 @@ pub fn routes(server: Arc<Server>) -> Router<Body> {
|
|||||||
|
|
||||||
pub async fn handle_websocket_request(
|
pub async fn handle_websocket_request(
|
||||||
TypedHeader(ProtocolVersion(protocol_version)): TypedHeader<ProtocolVersion>,
|
TypedHeader(ProtocolVersion(protocol_version)): TypedHeader<ProtocolVersion>,
|
||||||
|
_app_version_header: Option<TypedHeader<AppVersionHeader>>,
|
||||||
ConnectInfo(socket_address): ConnectInfo<SocketAddr>,
|
ConnectInfo(socket_address): ConnectInfo<SocketAddr>,
|
||||||
Extension(server): Extension<Arc<Server>>,
|
Extension(server): Extension<Arc<Server>>,
|
||||||
Extension(user): Extension<User>,
|
Extension(user): Extension<User>,
|
||||||
@@ -851,6 +880,7 @@ pub async fn handle_websocket_request(
|
|||||||
)
|
)
|
||||||
.into_response();
|
.into_response();
|
||||||
}
|
}
|
||||||
|
|
||||||
let socket_address = socket_address.to_string();
|
let socket_address = socket_address.to_string();
|
||||||
ws.on_upgrade(move |socket| {
|
ws.on_upgrade(move |socket| {
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
|
|||||||
@@ -153,6 +153,7 @@ impl TestServer {
|
|||||||
}
|
}
|
||||||
let settings = SettingsStore::test(cx);
|
let settings = SettingsStore::test(cx);
|
||||||
cx.set_global(settings);
|
cx.set_global(settings);
|
||||||
|
release_channel::init("0.0.0", cx);
|
||||||
client::init_settings(cx);
|
client::init_settings(cx);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -453,7 +453,7 @@ impl ChatPanel {
|
|||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.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>) {
|
fn send(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
|
||||||
|
|||||||
@@ -14,13 +14,13 @@ pub use collab_panel::CollabPanel;
|
|||||||
pub use collab_titlebar_item::CollabTitlebarItem;
|
pub use collab_titlebar_item::CollabTitlebarItem;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions, point, AppContext, GlobalPixels, Pixels, PlatformDisplay, Size, Task, WindowBounds,
|
actions, point, AppContext, GlobalPixels, Pixels, PlatformDisplay, Size, Task, WindowBounds,
|
||||||
WindowKind, WindowOptions,
|
WindowContext, WindowKind, WindowOptions,
|
||||||
};
|
};
|
||||||
pub use panel_settings::{
|
pub use panel_settings::{
|
||||||
ChatPanelSettings, CollaborationPanelSettings, NotificationPanelSettings,
|
ChatPanelSettings, CollaborationPanelSettings, NotificationPanelSettings,
|
||||||
};
|
};
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
use workspace::AppState;
|
use workspace::{notifications::DetachAndPromptErr, AppState};
|
||||||
|
|
||||||
actions!(
|
actions!(
|
||||||
collab,
|
collab,
|
||||||
@@ -41,7 +41,7 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
|||||||
notifications::init(&app_state, cx);
|
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);
|
let call = ActiveCall::global(cx).read(cx);
|
||||||
if let Some(room) = call.room().cloned() {
|
if let Some(room) = call.room().cloned() {
|
||||||
let client = call.client();
|
let client = call.client();
|
||||||
@@ -64,7 +64,7 @@ pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut AppContext) {
|
|||||||
room.share_screen(cx)
|
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();
|
.detach();
|
||||||
|
|
||||||
let server = server.initialize(Default::default()).await?;
|
let server = cx.update(|cx| server.initialize(None, cx))?.await?;
|
||||||
|
|
||||||
let status = server
|
let status = server
|
||||||
.request::<request::CheckStatus>(request::CheckStatusParams {
|
.request::<request::CheckStatus>(request::CheckStatusParams {
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ language = { path = "../language", features = ["test-support"] }
|
|||||||
lsp = { path = "../lsp", features = ["test-support"] }
|
lsp = { path = "../lsp", features = ["test-support"] }
|
||||||
multi_buffer = { path = "../multi_buffer", features = ["test-support"] }
|
multi_buffer = { path = "../multi_buffer", features = ["test-support"] }
|
||||||
project = { path = "../project", features = ["test-support"] }
|
project = { path = "../project", features = ["test-support"] }
|
||||||
|
release_channel = { path = "../release_channel" }
|
||||||
rand.workspace = true
|
rand.workspace = true
|
||||||
settings = { path = "../settings", features = ["test-support"] }
|
settings = { path = "../settings", features = ["test-support"] }
|
||||||
text = { path = "../text", features = ["test-support"] }
|
text = { path = "../text", features = ["test-support"] }
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ use language::{
|
|||||||
language_settings::{self, all_language_settings, InlayHintSettings},
|
language_settings::{self, all_language_settings, InlayHintSettings},
|
||||||
markdown, point_from_lsp, AutoindentMode, BracketPair, Buffer, Capability, CodeAction,
|
markdown, point_from_lsp, AutoindentMode, BracketPair, Buffer, Capability, CodeAction,
|
||||||
CodeLabel, Completion, CursorShape, Diagnostic, Documentation, IndentKind, IndentSize,
|
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};
|
use link_go_to_definition::{GoToDefinitionLink, InlayHighlight, LinkGoToDefinitionState};
|
||||||
@@ -7289,9 +7289,7 @@ impl Editor {
|
|||||||
editor.buffer.read(cx).as_singleton().and_then(|buffer| {
|
editor.buffer.read(cx).as_singleton().and_then(|buffer| {
|
||||||
project
|
project
|
||||||
.language_server_for_buffer(buffer.read(cx), server_id, cx)
|
.language_server_for_buffer(buffer.read(cx), server_id, cx)
|
||||||
.map(|(_, lsp_adapter)| {
|
.map(|(lsp_adapter, _)| lsp_adapter.name.clone())
|
||||||
LanguageServerName(Arc::from(lsp_adapter.name()))
|
|
||||||
})
|
|
||||||
});
|
});
|
||||||
language_server_name.map(|language_server_name| {
|
language_server_name.map(|language_server_name| {
|
||||||
project.open_local_buffer_via_lsp(
|
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);
|
let store = SettingsStore::test(cx);
|
||||||
cx.set_global(store);
|
cx.set_global(store);
|
||||||
theme::init(theme::LoadThemes::JustBase, cx);
|
theme::init(theme::LoadThemes::JustBase, cx);
|
||||||
|
release_channel::init("0.0.0", cx);
|
||||||
client::init_settings(cx);
|
client::init_settings(cx);
|
||||||
language::init(cx);
|
language::init(cx);
|
||||||
Project::init_settings(cx);
|
Project::init_settings(cx);
|
||||||
|
|||||||
@@ -3216,6 +3216,7 @@ pub mod tests {
|
|||||||
let settings_store = SettingsStore::test(cx);
|
let settings_store = SettingsStore::test(cx);
|
||||||
cx.set_global(settings_store);
|
cx.set_global(settings_store);
|
||||||
theme::init(theme::LoadThemes::JustBase, cx);
|
theme::init(theme::LoadThemes::JustBase, cx);
|
||||||
|
release_channel::init("0.0.0", cx);
|
||||||
client::init_settings(cx);
|
client::init_settings(cx);
|
||||||
language::init(cx);
|
language::init(cx);
|
||||||
Project::init_settings(cx);
|
Project::init_settings(cx);
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
use client::ZED_APP_VERSION;
|
|
||||||
use gpui::AppContext;
|
use gpui::AppContext;
|
||||||
use human_bytes::human_bytes;
|
use human_bytes::human_bytes;
|
||||||
use release_channel::ReleaseChannel;
|
use release_channel::{AppVersion, ReleaseChannel};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use std::{env, fmt::Display};
|
use std::{env, fmt::Display};
|
||||||
use sysinfo::{RefreshKind, System, SystemExt};
|
use sysinfo::{RefreshKind, System, SystemExt};
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize)]
|
#[derive(Clone, Debug, Serialize)]
|
||||||
pub struct SystemSpecs {
|
pub struct SystemSpecs {
|
||||||
app_version: Option<String>,
|
app_version: String,
|
||||||
release_channel: &'static str,
|
release_channel: &'static str,
|
||||||
os_name: &'static str,
|
os_name: &'static str,
|
||||||
os_version: Option<String>,
|
os_version: Option<String>,
|
||||||
@@ -18,9 +17,7 @@ pub struct SystemSpecs {
|
|||||||
|
|
||||||
impl SystemSpecs {
|
impl SystemSpecs {
|
||||||
pub fn new(cx: &AppContext) -> Self {
|
pub fn new(cx: &AppContext) -> Self {
|
||||||
let app_version = ZED_APP_VERSION
|
let app_version = AppVersion::global(cx).to_string();
|
||||||
.or_else(|| cx.app_metadata().app_version)
|
|
||||||
.map(|v| v.to_string());
|
|
||||||
let release_channel = ReleaseChannel::global(cx).display_name();
|
let release_channel = ReleaseChannel::global(cx).display_name();
|
||||||
let os_name = cx.app_metadata().os_name;
|
let os_name = cx.app_metadata().os_name;
|
||||||
let system = System::new_with_specifics(RefreshKind::new().with_memory());
|
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),
|
Some(os_version) => format!("OS: {} {}", self.os_name, os_version),
|
||||||
None => format!("OS: {}", self.os_name),
|
None => format!("OS: {}", self.os_name),
|
||||||
};
|
};
|
||||||
let app_version_information = self
|
let app_version_information =
|
||||||
.app_version
|
format!("Zed: v{} ({})", self.app_version, self.release_channel);
|
||||||
.as_ref()
|
|
||||||
.map(|app_version| format!("Zed: v{} ({})", app_version, self.release_channel));
|
|
||||||
let system_specs = [
|
let system_specs = [
|
||||||
app_version_information,
|
app_version_information,
|
||||||
Some(os_information),
|
os_information,
|
||||||
Some(format!("Memory: {}", human_bytes(self.memory as f64))),
|
format!("Memory: {}", human_bytes(self.memory as f64)),
|
||||||
Some(format!("Architecture: {}", self.architecture)),
|
format!("Architecture: {}", self.architecture),
|
||||||
]
|
]
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.flatten()
|
|
||||||
.collect::<Vec<String>>()
|
.collect::<Vec<String>>()
|
||||||
.join("\n");
|
.join("\n");
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ collections = { path = "../collections" }
|
|||||||
editor = { path = "../editor" }
|
editor = { path = "../editor" }
|
||||||
fuzzy = { path = "../fuzzy" }
|
fuzzy = { path = "../fuzzy" }
|
||||||
gpui = { path = "../gpui" }
|
gpui = { path = "../gpui" }
|
||||||
|
itertools = "0.11"
|
||||||
menu = { path = "../menu" }
|
menu = { path = "../menu" }
|
||||||
picker = { path = "../picker" }
|
picker = { path = "../picker" }
|
||||||
postage.workspace = true
|
postage.workspace = true
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod file_finder_tests;
|
mod file_finder_tests;
|
||||||
|
|
||||||
use collections::HashMap;
|
use collections::{HashMap, HashSet};
|
||||||
use editor::{scroll::Autoscroll, Bias, Editor};
|
use editor::{scroll::Autoscroll, Bias, Editor};
|
||||||
use fuzzy::{CharBag, PathMatch, PathMatchCandidate};
|
use fuzzy::{CharBag, PathMatch, PathMatchCandidate};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions, rems, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Model,
|
actions, rems, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Model,
|
||||||
ParentElement, Render, Styled, Task, View, ViewContext, VisualContext, WeakView,
|
ParentElement, Render, Styled, Task, View, ViewContext, VisualContext, WeakView,
|
||||||
};
|
};
|
||||||
|
use itertools::Itertools;
|
||||||
use picker::{Picker, PickerDelegate};
|
use picker::{Picker, PickerDelegate};
|
||||||
use project::{PathMatchCandidateSet, Project, ProjectPath, WorktreeId};
|
use project::{PathMatchCandidateSet, Project, ProjectPath, WorktreeId};
|
||||||
use std::{
|
use std::{
|
||||||
@@ -64,33 +65,15 @@ impl FileFinder {
|
|||||||
FoundPath::new(project_path, abs_path)
|
FoundPath::new(project_path, abs_path)
|
||||||
});
|
});
|
||||||
|
|
||||||
// if exists, bubble the currently opened path to the top
|
let history_items = workspace
|
||||||
let history_items = currently_opened_path
|
.recent_navigation_history(Some(MAX_RECENT_SELECTIONS), cx)
|
||||||
.clone()
|
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.chain(
|
.filter(|(_, history_abs_path)| match history_abs_path {
|
||||||
workspace
|
Some(abs_path) => history_file_exists(abs_path),
|
||||||
.recent_navigation_history(Some(MAX_RECENT_SELECTIONS), cx)
|
None => true,
|
||||||
.into_iter()
|
})
|
||||||
.filter(|(history_path, _)| {
|
.map(|(history_path, abs_path)| FoundPath::new(history_path, abs_path))
|
||||||
Some(history_path)
|
.collect::<Vec<_>>();
|
||||||
!= 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();
|
|
||||||
|
|
||||||
let project = workspace.project().clone();
|
let project = workspace.project().clone();
|
||||||
let weak_workspace = cx.view().downgrade();
|
let weak_workspace = cx.view().downgrade();
|
||||||
@@ -139,7 +122,7 @@ pub struct FileFinderDelegate {
|
|||||||
latest_search_query: Option<PathLikeWithPosition<FileSearchQuery>>,
|
latest_search_query: Option<PathLikeWithPosition<FileSearchQuery>>,
|
||||||
currently_opened_path: Option<FoundPath>,
|
currently_opened_path: Option<FoundPath>,
|
||||||
matches: Matches,
|
matches: Matches,
|
||||||
selected_index: Option<usize>,
|
selected_index: usize,
|
||||||
cancel_flag: Arc<AtomicBool>,
|
cancel_flag: Arc<AtomicBool>,
|
||||||
history_items: Vec<FoundPath>,
|
history_items: Vec<FoundPath>,
|
||||||
}
|
}
|
||||||
@@ -209,31 +192,21 @@ impl Matches {
|
|||||||
fn push_new_matches(
|
fn push_new_matches(
|
||||||
&mut self,
|
&mut self,
|
||||||
history_items: &Vec<FoundPath>,
|
history_items: &Vec<FoundPath>,
|
||||||
|
currently_opened: Option<&FoundPath>,
|
||||||
query: &PathLikeWithPosition<FileSearchQuery>,
|
query: &PathLikeWithPosition<FileSearchQuery>,
|
||||||
new_search_matches: impl Iterator<Item = ProjectPanelOrdMatch>,
|
new_search_matches: impl Iterator<Item = ProjectPanelOrdMatch>,
|
||||||
extend_old_matches: bool,
|
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
|
let new_search_matches = new_search_matches
|
||||||
.filter(|path_match| !matching_history_paths.contains_key(&path_match.0.path));
|
.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 {
|
if extend_old_matches {
|
||||||
self.search
|
self.search
|
||||||
.retain(|path_match| !matching_history_paths.contains_key(&path_match.0.path));
|
.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));
|
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(
|
fn matching_history_item_paths(
|
||||||
history_items: &Vec<FoundPath>,
|
history_items: &Vec<FoundPath>,
|
||||||
|
currently_opened: Option<&FoundPath>,
|
||||||
query: &PathLikeWithPosition<FileSearchQuery>,
|
query: &PathLikeWithPosition<FileSearchQuery>,
|
||||||
) -> HashMap<Arc<Path>, ProjectPanelOrdMatch> {
|
) -> HashMap<Arc<Path>, ProjectPanelOrdMatch> {
|
||||||
let history_items_by_worktrees = history_items
|
let history_items_by_worktrees = history_items
|
||||||
.iter()
|
.iter()
|
||||||
|
.chain(currently_opened)
|
||||||
.filter_map(|found_path| {
|
.filter_map(|found_path| {
|
||||||
let candidate = PathMatchCandidate {
|
let candidate = PathMatchCandidate {
|
||||||
path: &found_path.project.path,
|
path: &found_path.project.path,
|
||||||
@@ -301,7 +312,7 @@ fn matching_history_item_paths(
|
|||||||
matching_history_paths
|
matching_history_paths
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
struct FoundPath {
|
struct FoundPath {
|
||||||
project: ProjectPath,
|
project: ProjectPath,
|
||||||
absolute: Option<PathBuf>,
|
absolute: Option<PathBuf>,
|
||||||
@@ -372,7 +383,7 @@ impl FileFinderDelegate {
|
|||||||
latest_search_query: None,
|
latest_search_query: None,
|
||||||
currently_opened_path,
|
currently_opened_path,
|
||||||
matches: Matches::default(),
|
matches: Matches::default(),
|
||||||
selected_index: None,
|
selected_index: 0,
|
||||||
cancel_flag: Arc::new(AtomicBool::new(false)),
|
cancel_flag: Arc::new(AtomicBool::new(false)),
|
||||||
history_items,
|
history_items,
|
||||||
}
|
}
|
||||||
@@ -427,7 +438,6 @@ impl FileFinderDelegate {
|
|||||||
let did_cancel = cancel_flag.load(atomic::Ordering::Relaxed);
|
let did_cancel = cancel_flag.load(atomic::Ordering::Relaxed);
|
||||||
picker
|
picker
|
||||||
.update(&mut cx, |picker, cx| {
|
.update(&mut cx, |picker, cx| {
|
||||||
picker.delegate.selected_index.take();
|
|
||||||
picker
|
picker
|
||||||
.delegate
|
.delegate
|
||||||
.set_search_matches(search_id, did_cancel, query, matches, cx)
|
.set_search_matches(search_id, did_cancel, query, matches, cx)
|
||||||
@@ -454,12 +464,14 @@ impl FileFinderDelegate {
|
|||||||
.map(|query| query.path_like.path_query());
|
.map(|query| query.path_like.path_query());
|
||||||
self.matches.push_new_matches(
|
self.matches.push_new_matches(
|
||||||
&self.history_items,
|
&self.history_items,
|
||||||
|
self.currently_opened_path.as_ref(),
|
||||||
&query,
|
&query,
|
||||||
matches.into_iter(),
|
matches.into_iter(),
|
||||||
extend_old_matches,
|
extend_old_matches,
|
||||||
);
|
);
|
||||||
self.latest_search_query = Some(query);
|
self.latest_search_query = Some(query);
|
||||||
self.latest_search_did_cancel = did_cancel;
|
self.latest_search_did_cancel = did_cancel;
|
||||||
|
self.selected_index = self.calculate_selected_index();
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -630,6 +642,19 @@ impl FileFinderDelegate {
|
|||||||
.log_err();
|
.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 {
|
impl PickerDelegate for FileFinderDelegate {
|
||||||
@@ -644,11 +669,11 @@ impl PickerDelegate for FileFinderDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn selected_index(&self) -> usize {
|
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>>) {
|
fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>) {
|
||||||
self.selected_index = Some(ix);
|
self.selected_index = ix;
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -671,22 +696,22 @@ impl PickerDelegate for FileFinderDelegate {
|
|||||||
if raw_query.is_empty() {
|
if raw_query.is_empty() {
|
||||||
let project = self.project.read(cx);
|
let project = self.project.read(cx);
|
||||||
self.latest_search_id = post_inc(&mut self.search_count);
|
self.latest_search_id = post_inc(&mut self.search_count);
|
||||||
self.selected_index.take();
|
|
||||||
self.matches = Matches {
|
self.matches = Matches {
|
||||||
history: self
|
history: Vec::new(),
|
||||||
.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(),
|
|
||||||
search: 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();
|
cx.notify();
|
||||||
Task::ready(())
|
Task::ready(())
|
||||||
} else {
|
} 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]
|
#[gpui::test]
|
||||||
async fn test_history_items_vs_very_good_external_match(cx: &mut gpui::TestAppContext) {
|
async fn test_history_items_vs_very_good_external_match(cx: &mut gpui::TestAppContext) {
|
||||||
let app_state = init_test(cx);
|
let app_state = init_test(cx);
|
||||||
@@ -1172,6 +1343,27 @@ async fn open_close_queried_buffer(
|
|||||||
expected_editor_title: &str,
|
expected_editor_title: &str,
|
||||||
workspace: &View<Workspace>,
|
workspace: &View<Workspace>,
|
||||||
cx: &mut gpui::VisualTestContext,
|
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> {
|
) -> Vec<FoundPath> {
|
||||||
let picker = open_file_picker(&workspace, cx);
|
let picker = open_file_picker(&workspace, cx);
|
||||||
cx.simulate_input(input);
|
cx.simulate_input(input);
|
||||||
@@ -1186,7 +1378,6 @@ async fn open_close_queried_buffer(
|
|||||||
finder.delegate.history_items.clone()
|
finder.delegate.history_items.clone()
|
||||||
});
|
});
|
||||||
|
|
||||||
cx.dispatch_action(SelectNext);
|
|
||||||
cx.dispatch_action(Confirm);
|
cx.dispatch_action(Confirm);
|
||||||
|
|
||||||
cx.read(|cx| {
|
cx.read(|cx| {
|
||||||
@@ -1198,8 +1389,6 @@ async fn open_close_queried_buffer(
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
cx.dispatch_action(workspace::CloseActiveItem { save_intent: None });
|
|
||||||
|
|
||||||
history_items
|
history_items
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1313,3 +1502,37 @@ fn collect_search_matches(picker: &Picker<FileFinderDelegate>) -> SearchEntries
|
|||||||
.collect(),
|
.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);
|
||||||
|
}
|
||||||
|
|||||||
32
crates/gpui/examples/views_post.rs
Normal file
32
crates/gpui/examples/views_post.rs
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
use gpui::*;
|
||||||
|
|
||||||
|
struct Counter {
|
||||||
|
count: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Counter {
|
||||||
|
fn new(cx: &mut WindowContext) -> View<Self> {
|
||||||
|
cx.new_view(|_cx| Self { count: 0 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for Counter {
|
||||||
|
fn render(&mut self, _cx: &mut gpui::ViewContext<Self>) -> impl IntoElement {
|
||||||
|
div()
|
||||||
|
.size_full()
|
||||||
|
.flex()
|
||||||
|
.justify_center()
|
||||||
|
.items_center()
|
||||||
|
.text_xl()
|
||||||
|
.bg(rgb(0x2d004b))
|
||||||
|
.text_color(rgb(0xffffff))
|
||||||
|
.child(self.count.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
App::new().run(|cx: &mut AppContext| {
|
||||||
|
cx.open_window(WindowOptions::default(), Counter::new);
|
||||||
|
cx.activate(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -11,7 +11,7 @@ use crate::{
|
|||||||
Pixels, PlatformInput, Point, RenderGlyphParams, RenderImageParams, RenderSvgParams, Scene,
|
Pixels, PlatformInput, Point, RenderGlyphParams, RenderImageParams, RenderSvgParams, Scene,
|
||||||
SharedString, Size, Task, TaskLabel, WindowContext,
|
SharedString, Size, Task, TaskLabel, WindowContext,
|
||||||
};
|
};
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::Result;
|
||||||
use async_task::Runnable;
|
use async_task::Runnable;
|
||||||
use futures::channel::oneshot;
|
use futures::channel::oneshot;
|
||||||
use parking::Unparker;
|
use parking::Unparker;
|
||||||
@@ -23,11 +23,10 @@ use std::hash::{Hash, Hasher};
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use std::{
|
use std::{
|
||||||
any::Any,
|
any::Any,
|
||||||
fmt::{self, Debug, Display},
|
fmt::{self, Debug},
|
||||||
ops::Range,
|
ops::Range,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
rc::Rc,
|
rc::Rc,
|
||||||
str::FromStr,
|
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
};
|
};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
@@ -39,6 +38,7 @@ pub(crate) use mac::*;
|
|||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
pub(crate) use test::*;
|
pub(crate) use test::*;
|
||||||
use time::UtcOffset;
|
use time::UtcOffset;
|
||||||
|
pub use util::SemanticVersion;
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
pub(crate) fn current_platform() -> Rc<dyn Platform> {
|
pub(crate) fn current_platform() -> Rc<dyn Platform> {
|
||||||
@@ -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
|
/// A clipboard item that should be copied to the clipboard
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
pub struct ClipboardItem {
|
pub struct ClipboardItem {
|
||||||
|
|||||||
@@ -61,6 +61,16 @@ fragment float4 quad_fragment(QuadFragmentInput input [[stage_in]],
|
|||||||
constant Quad *quads
|
constant Quad *quads
|
||||||
[[buffer(QuadInputIndex_Quads)]]) {
|
[[buffer(QuadInputIndex_Quads)]]) {
|
||||||
Quad quad = quads[input.quad_id];
|
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 half_size =
|
||||||
float2(quad.bounds.size.width, quad.bounds.size.height) / 2.;
|
float2(quad.bounds.size.width, quad.bounds.size.height) / 2.;
|
||||||
float2 center =
|
float2 center =
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ async-trait.workspace = true
|
|||||||
clock = { path = "../clock" }
|
clock = { path = "../clock" }
|
||||||
collections = { path = "../collections" }
|
collections = { path = "../collections" }
|
||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
fuzzy = { path = "../fuzzy" }
|
fuzzy = { path = "../fuzzy" }
|
||||||
git = { path = "../git" }
|
git = { path = "../git" }
|
||||||
globset.workspace = true
|
globset.workspace = true
|
||||||
gpui = { path = "../gpui" }
|
gpui = { path = "../gpui" }
|
||||||
@@ -38,7 +38,6 @@ log.workspace = true
|
|||||||
lsp = { path = "../lsp" }
|
lsp = { path = "../lsp" }
|
||||||
parking_lot.workspace = true
|
parking_lot.workspace = true
|
||||||
postage.workspace = true
|
postage.workspace = true
|
||||||
pulldown-cmark = { version = "0.9.2", default-features = false }
|
|
||||||
rand = { workspace = true, optional = true }
|
rand = { workspace = true, optional = true }
|
||||||
regex.workspace = true
|
regex.workspace = true
|
||||||
rpc = { path = "../rpc" }
|
rpc = { path = "../rpc" }
|
||||||
@@ -55,6 +54,7 @@ text = { path = "../text" }
|
|||||||
theme = { path = "../theme" }
|
theme = { path = "../theme" }
|
||||||
tree-sitter-rust = { workspace = true, optional = true }
|
tree-sitter-rust = { workspace = true, optional = true }
|
||||||
tree-sitter-typescript = { workspace = true, optional = true }
|
tree-sitter-typescript = { workspace = true, optional = true }
|
||||||
|
pulldown-cmark.workspace = true
|
||||||
tree-sitter.workspace = true
|
tree-sitter.workspace = true
|
||||||
unicase = "2.6"
|
unicase = "2.6"
|
||||||
util = { path = "../util" }
|
util = { path = "../util" }
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ workspace = { path = "../workspace" }
|
|||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
client = { path = "../client", features = ["test-support"] }
|
client = { path = "../client", features = ["test-support"] }
|
||||||
editor = { path = "../editor", features = ["test-support"] }
|
editor = { path = "../editor", features = ["test-support"] }
|
||||||
|
release_channel = { path = "../release_channel" }
|
||||||
env_logger.workspace = true
|
env_logger.workspace = true
|
||||||
gpui = { path = "../gpui", features = ["test-support"] }
|
gpui = { path = "../gpui", features = ["test-support"] }
|
||||||
unindent.workspace = true
|
unindent.workspace = true
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ fn init_test(cx: &mut gpui::TestAppContext) {
|
|||||||
let settings_store = SettingsStore::test(cx);
|
let settings_store = SettingsStore::test(cx);
|
||||||
cx.set_global(settings_store);
|
cx.set_global(settings_store);
|
||||||
theme::init(theme::LoadThemes::JustBase, cx);
|
theme::init(theme::LoadThemes::JustBase, cx);
|
||||||
|
release_channel::init("0.0.0", cx);
|
||||||
language::init(cx);
|
language::init(cx);
|
||||||
client::init_settings(cx);
|
client::init_settings(cx);
|
||||||
Project::init_settings(cx);
|
Project::init_settings(cx);
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ serde_derive.workspace = true
|
|||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
smol.workspace = true
|
smol.workspace = true
|
||||||
util = { path = "../util" }
|
util = { path = "../util" }
|
||||||
|
release_channel = { path = "../release_channel" }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
async-pipe = { git = "https://github.com/zed-industries/async-pipe-rs", rev = "82d00a04211cf4e1236029aa03e6b6ce2a74c553" }
|
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 anyhow::{anyhow, Context, Result};
|
||||||
use collections::HashMap;
|
use collections::HashMap;
|
||||||
use futures::{channel::oneshot, io::BufWriter, AsyncRead, AsyncWrite, FutureExt};
|
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 parking_lot::Mutex;
|
||||||
use postage::{barrier, prelude::Stream};
|
use postage::{barrier, prelude::Stream};
|
||||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||||
@@ -322,8 +322,15 @@ impl LanguageServer {
|
|||||||
let mut buffer = Vec::new();
|
let mut buffer = Vec::new();
|
||||||
loop {
|
loop {
|
||||||
buffer.clear();
|
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 header = std::str::from_utf8(&buffer)?;
|
||||||
let message_len: usize = header
|
let message_len: usize = header
|
||||||
.strip_prefix(CONTENT_LEN_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.
|
// Don't starve the main thread when receiving lots of messages at once.
|
||||||
smol::future::yield_now().await;
|
smol::future::yield_now().await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_stderr<Stderr>(
|
async fn handle_stderr<Stderr>(
|
||||||
@@ -393,7 +402,12 @@ impl LanguageServer {
|
|||||||
|
|
||||||
loop {
|
loop {
|
||||||
buffer.clear();
|
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) {
|
if let Ok(message) = str::from_utf8(&buffer) {
|
||||||
log::trace!("incoming stderr message:{message}");
|
log::trace!("incoming stderr message:{message}");
|
||||||
for handler in io_handlers.lock().values_mut() {
|
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.
|
/// 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)
|
/// [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();
|
let root_uri = Url::from_file_path(&self.root_path).unwrap();
|
||||||
#[allow(deprecated)]
|
#[allow(deprecated)]
|
||||||
let params = InitializeParams {
|
let params = InitializeParams {
|
||||||
@@ -579,18 +597,25 @@ impl LanguageServer {
|
|||||||
uri: root_uri,
|
uri: root_uri,
|
||||||
name: Default::default(),
|
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,
|
locale: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let response = self.request::<request::Initialize>(params).await?;
|
cx.spawn(|_| async move {
|
||||||
if let Some(info) = response.server_info {
|
let response = self.request::<request::Initialize>(params).await?;
|
||||||
self.name = info.name;
|
if let Some(info) = response.server_info {
|
||||||
}
|
self.name = info.name;
|
||||||
self.capabilities = response.capabilities;
|
}
|
||||||
|
self.capabilities = response.capabilities;
|
||||||
|
|
||||||
self.notify::<notification::Initialized>(InitializedParams {})?;
|
self.notify::<notification::Initialized>(InitializedParams {})?;
|
||||||
Ok(Arc::new(self))
|
Ok(Arc::new(self))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sends a shutdown request to the language server process and prepares the [`LanguageServer`] to be dropped.
|
/// Sends a shutdown request to the language server process and prepares the [`LanguageServer`] to be dropped.
|
||||||
@@ -1213,6 +1238,9 @@ mod tests {
|
|||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_fake(cx: &mut TestAppContext) {
|
async fn test_fake(cx: &mut TestAppContext) {
|
||||||
|
cx.update(|cx| {
|
||||||
|
release_channel::init("0.0.0", cx);
|
||||||
|
});
|
||||||
let (server, mut fake) =
|
let (server, mut fake) =
|
||||||
FakeLanguageServer::new("the-lsp".to_string(), Default::default(), cx.to_async());
|
FakeLanguageServer::new("the-lsp".to_string(), Default::default(), cx.to_async());
|
||||||
|
|
||||||
@@ -1229,7 +1257,7 @@ mod tests {
|
|||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
|
|
||||||
let server = server.initialize(None).await.unwrap();
|
let server = cx.update(|cx| server.initialize(None, cx)).await.unwrap();
|
||||||
server
|
server
|
||||||
.notify::<notification::DidOpenTextDocument>(DidOpenTextDocumentParams {
|
.notify::<notification::DidOpenTextDocument>(DidOpenTextDocumentParams {
|
||||||
text_document: TextDocumentItem::new(
|
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
|
ordered-float.workspace = true
|
||||||
parking_lot.workspace = true
|
parking_lot.workspace = true
|
||||||
postage.workspace = true
|
postage.workspace = true
|
||||||
pulldown-cmark = { version = "0.9.2", default-features = false }
|
pulldown-cmark.workspace = true
|
||||||
rand.workspace = true
|
rand.workspace = true
|
||||||
rich_text = { path = "../rich_text" }
|
rich_text = { path = "../rich_text" }
|
||||||
schemars.workspace = true
|
schemars.workspace = true
|
||||||
|
|||||||
@@ -195,11 +195,11 @@ impl Prettier {
|
|||||||
},
|
},
|
||||||
Path::new("/"),
|
Path::new("/"),
|
||||||
None,
|
None,
|
||||||
cx,
|
cx.clone(),
|
||||||
)
|
)
|
||||||
.context("prettier server creation")?;
|
.context("prettier server creation")?;
|
||||||
let server = executor
|
let server = cx
|
||||||
.spawn(server.initialize(None))
|
.update(|cx| executor.spawn(server.initialize(None, cx)))?
|
||||||
.await
|
.await
|
||||||
.context("prettier server initialization")?;
|
.context("prettier server initialization")?;
|
||||||
Ok(Self::Real(RealPrettier {
|
Ok(Self::Real(RealPrettier {
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ fs = { path = "../fs", features = ["test-support"] }
|
|||||||
git2.workspace = true
|
git2.workspace = true
|
||||||
gpui = { path = "../gpui", features = ["test-support"] }
|
gpui = { path = "../gpui", features = ["test-support"] }
|
||||||
language = { path = "../language", features = ["test-support"] }
|
language = { path = "../language", features = ["test-support"] }
|
||||||
|
release_channel = { path = "../release_channel" }
|
||||||
lsp = { path = "../lsp", features = ["test-support"] }
|
lsp = { path = "../lsp", features = ["test-support"] }
|
||||||
prettier = { path = "../prettier", features = ["test-support"] }
|
prettier = { path = "../prettier", features = ["test-support"] }
|
||||||
pretty_assertions.workspace = true
|
pretty_assertions.workspace = true
|
||||||
|
|||||||
@@ -3130,7 +3130,9 @@ impl Project {
|
|||||||
(None, override_options) => initialization_options = override_options,
|
(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
|
language_server
|
||||||
.notify::<lsp::notification::DidChangeConfiguration>(
|
.notify::<lsp::notification::DidChangeConfiguration>(
|
||||||
|
|||||||
@@ -4380,6 +4380,7 @@ fn init_test(cx: &mut gpui::TestAppContext) {
|
|||||||
cx.update(|cx| {
|
cx.update(|cx| {
|
||||||
let settings_store = SettingsStore::test(cx);
|
let settings_store = SettingsStore::test(cx);
|
||||||
cx.set_global(settings_store);
|
cx.set_global(settings_store);
|
||||||
|
release_channel::init("0.0.0", cx);
|
||||||
language::init(cx);
|
language::init(cx);
|
||||||
Project::init_settings(cx);
|
Project::init_settings(cx);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ gpui = { path = "../gpui", features = ["test-support"] }
|
|||||||
language = { path = "../language", features = ["test-support"] }
|
language = { path = "../language", features = ["test-support"] }
|
||||||
lsp = { path = "../lsp", features = ["test-support"] }
|
lsp = { path = "../lsp", features = ["test-support"] }
|
||||||
project = { path = "../project", features = ["test-support"] }
|
project = { path = "../project", features = ["test-support"] }
|
||||||
|
release_channel = { path = "../release_channel" }
|
||||||
settings = { path = "../settings", features = ["test-support"] }
|
settings = { path = "../settings", features = ["test-support"] }
|
||||||
theme = { path = "../theme", features = ["test-support"] }
|
theme = { path = "../theme", features = ["test-support"] }
|
||||||
workspace = { path = "../workspace", features = ["test-support"] }
|
workspace = { path = "../workspace", features = ["test-support"] }
|
||||||
|
|||||||
@@ -392,6 +392,7 @@ mod tests {
|
|||||||
let store = SettingsStore::test(cx);
|
let store = SettingsStore::test(cx);
|
||||||
cx.set_global(store);
|
cx.set_global(store);
|
||||||
theme::init(theme::LoadThemes::JustBase, cx);
|
theme::init(theme::LoadThemes::JustBase, cx);
|
||||||
|
release_channel::init("0.0.0", cx);
|
||||||
language::init(cx);
|
language::init(cx);
|
||||||
Project::init_settings(cx);
|
Project::init_settings(cx);
|
||||||
workspace::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 once_cell::sync::Lazy;
|
||||||
use std::env;
|
use std::env;
|
||||||
|
|
||||||
#[doc(hidden)]
|
#[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(|| {
|
Lazy::new(|| {
|
||||||
env::var("ZED_RELEASE_CHANNEL")
|
env::var("ZED_RELEASE_CHANNEL")
|
||||||
.unwrap_or_else(|_| include_str!("../../zed/RELEASE_CHANNEL").trim().to_string())
|
.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 {
|
} else {
|
||||||
Lazy::new(|| include_str!("../../zed/RELEASE_CHANNEL").trim().to_string())
|
Lazy::new(|| include_str!("../../zed/RELEASE_CHANNEL").trim().to_string())
|
||||||
};
|
};
|
||||||
|
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
pub static RELEASE_CHANNEL: Lazy<ReleaseChannel> =
|
pub static RELEASE_CHANNEL: Lazy<ReleaseChannel> =
|
||||||
Lazy::new(|| match RELEASE_CHANNEL_NAME.as_str() {
|
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)]
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)]
|
||||||
pub enum ReleaseChannel {
|
pub enum ReleaseChannel {
|
||||||
#[default]
|
#[default]
|
||||||
@@ -52,11 +76,12 @@ struct GlobalReleaseChannel(ReleaseChannel);
|
|||||||
|
|
||||||
impl Global for GlobalReleaseChannel {}
|
impl Global for GlobalReleaseChannel {}
|
||||||
|
|
||||||
impl ReleaseChannel {
|
pub fn init(pkg_version: &str, cx: &mut AppContext) {
|
||||||
pub fn init(cx: &mut AppContext) {
|
AppVersion::init(pkg_version, cx);
|
||||||
cx.set_global(GlobalReleaseChannel(*RELEASE_CHANNEL))
|
cx.set_global(GlobalReleaseChannel(*RELEASE_CHANNEL))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ReleaseChannel {
|
||||||
pub fn global(cx: &AppContext) -> Self {
|
pub fn global(cx: &AppContext) -> Self {
|
||||||
cx.global::<GlobalReleaseChannel>().0
|
cx.global::<GlobalReleaseChannel>().0
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ futures.workspace = true
|
|||||||
gpui = { path = "../gpui" }
|
gpui = { path = "../gpui" }
|
||||||
language = { path = "../language" }
|
language = { path = "../language" }
|
||||||
lazy_static.workspace = true
|
lazy_static.workspace = true
|
||||||
pulldown-cmark = { version = "0.9.2", default-features = false }
|
pulldown-cmark.workspace = true
|
||||||
smallvec.workspace = true
|
smallvec.workspace = true
|
||||||
smol.workspace = true
|
smol.workspace = true
|
||||||
sum_tree = { path = "../sum_tree" }
|
sum_tree = { path = "../sum_tree" }
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ pub struct Mention {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl RichText {
|
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 theme = cx.theme();
|
||||||
let code_background = theme.colors().surface_background;
|
let code_background = theme.colors().surface_background;
|
||||||
|
|
||||||
@@ -83,7 +83,12 @@ impl RichText {
|
|||||||
)
|
)
|
||||||
.on_click(self.link_ranges.clone(), {
|
.on_click(self.link_ranges.clone(), {
|
||||||
let link_urls = self.link_urls.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({
|
.tooltip({
|
||||||
let link_ranges = self.link_ranges.clone();
|
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,
|
block: String,
|
||||||
mentions: &[Mention],
|
mentions: &[Mention],
|
||||||
language_registry: &Arc<LanguageRegistry>,
|
language_registry: &Arc<LanguageRegistry>,
|
||||||
|
|||||||
@@ -210,7 +210,7 @@ impl SettingsStore {
|
|||||||
|
|
||||||
if let Some(release_settings) = &self
|
if let Some(release_settings) = &self
|
||||||
.raw_user_settings
|
.raw_user_settings
|
||||||
.get(&*release_channel::RELEASE_CHANNEL_NAME)
|
.get(&*release_channel::RELEASE_CHANNEL.dev_name())
|
||||||
{
|
{
|
||||||
if let Some(release_settings) = setting_value
|
if let Some(release_settings) = setting_value
|
||||||
.deserialize_setting(&release_settings)
|
.deserialize_setting(&release_settings)
|
||||||
@@ -543,7 +543,7 @@ impl SettingsStore {
|
|||||||
|
|
||||||
if let Some(release_settings) = &self
|
if let Some(release_settings) = &self
|
||||||
.raw_user_settings
|
.raw_user_settings
|
||||||
.get(&*release_channel::RELEASE_CHANNEL_NAME)
|
.get(&*release_channel::RELEASE_CHANNEL.dev_name())
|
||||||
{
|
{
|
||||||
if let Some(release_settings) = setting_value
|
if let Some(release_settings) = setting_value
|
||||||
.deserialize_setting(&release_settings)
|
.deserialize_setting(&release_settings)
|
||||||
|
|||||||
@@ -255,19 +255,23 @@ impl ThemeRegistry {
|
|||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
let Some(reader) = fs.open_sync(&theme_path).await.log_err() else {
|
self.load_user_theme(&theme_path, fs.clone())
|
||||||
continue;
|
.await
|
||||||
};
|
.log_err();
|
||||||
|
|
||||||
let Some(theme) = serde_json_lenient::from_reader(reader).log_err() else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
self.insert_user_theme_families([theme]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
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 {
|
impl Default for ThemeRegistry {
|
||||||
|
|||||||
@@ -68,6 +68,6 @@ pub async fn latest_github_release(
|
|||||||
|
|
||||||
releases
|
releases
|
||||||
.into_iter()
|
.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"))
|
.ok_or(anyhow!("Failed to find a release"))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use crate::http_proxy_from_env;
|
||||||
pub use anyhow::{anyhow, Result};
|
pub use anyhow::{anyhow, Result};
|
||||||
use futures::future::BoxFuture;
|
use futures::future::BoxFuture;
|
||||||
use isahc::config::{Configurable, RedirectPolicy};
|
use isahc::config::{Configurable, RedirectPolicy};
|
||||||
@@ -43,6 +44,7 @@ pub fn zed_client(zed_host: &str) -> Arc<ZedHttpClient> {
|
|||||||
isahc::HttpClient::builder()
|
isahc::HttpClient::builder()
|
||||||
.connect_timeout(Duration::from_secs(5))
|
.connect_timeout(Duration::from_secs(5))
|
||||||
.low_speed_timeout(100, Duration::from_secs(5))
|
.low_speed_timeout(100, Duration::from_secs(5))
|
||||||
|
.proxy(http_proxy_from_env())
|
||||||
.build()
|
.build()
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
),
|
),
|
||||||
@@ -95,6 +97,7 @@ pub fn client() -> Arc<dyn HttpClient> {
|
|||||||
isahc::HttpClient::builder()
|
isahc::HttpClient::builder()
|
||||||
.connect_timeout(Duration::from_secs(5))
|
.connect_timeout(Duration::from_secs(5))
|
||||||
.low_speed_timeout(100, Duration::from_secs(5))
|
.low_speed_timeout(100, Duration::from_secs(5))
|
||||||
|
.proxy(http_proxy_from_env())
|
||||||
.build()
|
.build()
|
||||||
.unwrap(),
|
.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 github;
|
||||||
pub mod http;
|
pub mod http;
|
||||||
pub mod paths;
|
pub mod paths;
|
||||||
|
mod semantic_version;
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
pub mod test;
|
pub mod test;
|
||||||
|
|
||||||
@@ -10,6 +11,7 @@ pub use backtrace::Backtrace;
|
|||||||
use futures::Future;
|
use futures::Future;
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use rand::{seq::SliceRandom, Rng};
|
use rand::{seq::SliceRandom, Rng};
|
||||||
|
pub use semantic_version::SemanticVersion;
|
||||||
use std::{
|
use std::{
|
||||||
borrow::Cow,
|
borrow::Cow,
|
||||||
cmp::{self, Ordering},
|
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
|
/// 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.
|
/// 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 {
|
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"] }
|
editor = { path = "../editor", features = ["test-support"] }
|
||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
gpui = { path = "../gpui", features = ["test-support"] }
|
gpui = { path = "../gpui", features = ["test-support"] }
|
||||||
|
release_channel = { path = "../release_channel" }
|
||||||
indoc.workspace = true
|
indoc.workspace = true
|
||||||
language = { path = "../language", features = ["test-support"] }
|
language = { path = "../language", features = ["test-support"] }
|
||||||
lsp = { path = "../lsp", features = ["test-support"] }
|
lsp = { path = "../lsp", features = ["test-support"] }
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ impl VimTestContext {
|
|||||||
search::init(cx);
|
search::init(cx);
|
||||||
let settings = SettingsStore::test(cx);
|
let settings = SettingsStore::test(cx);
|
||||||
cx.set_global(settings);
|
cx.set_global(settings);
|
||||||
|
release_channel::init("0.0.0", cx);
|
||||||
command_palette::init(cx);
|
command_palette::init(cx);
|
||||||
crate::init(cx);
|
crate::init(cx);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,8 +3,9 @@ use crate::DraggedDock;
|
|||||||
use crate::{status_bar::StatusItemView, Workspace};
|
use crate::{status_bar::StatusItemView, Workspace};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, px, Action, AnchorCorner, AnyView, AppContext, Axis, ClickEvent, Entity, EntityId,
|
div, px, Action, AnchorCorner, AnyView, AppContext, Axis, ClickEvent, Entity, EntityId,
|
||||||
EventEmitter, FocusHandle, FocusableView, IntoElement, MouseButton, ParentElement, Render,
|
EventEmitter, FocusHandle, FocusableView, IntoElement, KeyContext, MouseButton, ParentElement,
|
||||||
SharedString, Styled, Subscription, View, ViewContext, VisualContext, WeakView, WindowContext,
|
Render, SharedString, Styled, Subscription, View, ViewContext, VisualContext, WeakView,
|
||||||
|
WindowContext,
|
||||||
};
|
};
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
@@ -534,10 +535,18 @@ impl Dock {
|
|||||||
DockPosition::Right => crate::ToggleRightDock.boxed_clone(),
|
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 {
|
impl Render for Dock {
|
||||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||||
|
let dispatch_context = Self::dispatch_context();
|
||||||
if let Some(entry) = self.visible_entry() {
|
if let Some(entry) = self.visible_entry() {
|
||||||
let size = entry.panel.size(cx);
|
let size = entry.panel.size(cx);
|
||||||
|
|
||||||
@@ -588,6 +597,7 @@ impl Render for Dock {
|
|||||||
}
|
}
|
||||||
|
|
||||||
div()
|
div()
|
||||||
|
.key_context(dispatch_context)
|
||||||
.track_focus(&self.focus_handle)
|
.track_focus(&self.focus_handle)
|
||||||
.flex()
|
.flex()
|
||||||
.bg(cx.theme().colors().panel_background)
|
.bg(cx.theme().colors().panel_background)
|
||||||
@@ -612,7 +622,9 @@ impl Render for Dock {
|
|||||||
)
|
)
|
||||||
.child(handle)
|
.child(handle)
|
||||||
} else {
|
} 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,
|
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 {
|
impl SplitDirection {
|
||||||
pub fn all() -> [Self; 4] {
|
pub fn all() -> [Self; 4] {
|
||||||
[Self::Up, Self::Down, Self::Left, Self::Right]
|
[Self::Up, Self::Down, Self::Left, Self::Right]
|
||||||
|
|||||||
@@ -2075,30 +2075,99 @@ impl Workspace {
|
|||||||
direction: SplitDirection,
|
direction: SplitDirection,
|
||||||
cx: &mut WindowContext,
|
cx: &mut WindowContext,
|
||||||
) {
|
) {
|
||||||
if let Some(pane) = self.find_pane_in_direction(direction, cx) {
|
use ActivateInDirectionTarget as Target;
|
||||||
cx.focus_view(pane);
|
enum Origin {
|
||||||
|
LeftDock,
|
||||||
|
RightDock,
|
||||||
|
BottomDock,
|
||||||
|
Center,
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
pub fn swap_pane_in_direction(
|
let origin: Origin = [
|
||||||
&mut self,
|
(&self.left_dock, Origin::LeftDock),
|
||||||
direction: SplitDirection,
|
(&self.right_dock, Origin::RightDock),
|
||||||
cx: &mut ViewContext<Self>,
|
(&self.bottom_dock, Origin::BottomDock),
|
||||||
) {
|
]
|
||||||
if let Some(to) = self
|
.into_iter()
|
||||||
.find_pane_in_direction(direction, cx)
|
.find_map(|(dock, origin)| {
|
||||||
.map(|pane| pane.clone())
|
if dock.focus_handle(cx).contains_focused(cx) && dock.read(cx).is_open() {
|
||||||
{
|
Some(origin)
|
||||||
self.center.swap(&self.active_pane.clone(), &to);
|
} else {
|
||||||
cx.notify();
|
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(
|
fn find_pane_in_direction(
|
||||||
&mut self,
|
&mut self,
|
||||||
direction: SplitDirection,
|
direction: SplitDirection,
|
||||||
cx: &AppContext,
|
cx: &WindowContext,
|
||||||
) -> Option<&View<Pane>> {
|
) -> Option<View<Pane>> {
|
||||||
let Some(bounding_box) = self.center.bounding_box_for_pane(&self.active_pane) else {
|
let Some(bounding_box) = self.center.bounding_box_for_pane(&self.active_pane) else {
|
||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
@@ -2124,7 +2193,21 @@ impl Workspace {
|
|||||||
Point::new(center.x, bounding_box.bottom() + distance_to_next.into())
|
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>) {
|
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) {
|
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";
|
const REPORT_ISSUE_URL: &str ="https://github.com/zed-industries/zed/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml";
|
||||||
|
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ lazy_static.workspace = true
|
|||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
lsp = { path = "../lsp" }
|
lsp = { path = "../lsp" }
|
||||||
|
markdown_preview = { path = "../markdown_preview" }
|
||||||
menu = { path = "../menu" }
|
menu = { path = "../menu" }
|
||||||
mimalloc = "0.1"
|
mimalloc = "0.1"
|
||||||
node_runtime = { path = "../node_runtime" }
|
node_runtime = { path = "../node_runtime" }
|
||||||
@@ -113,6 +114,7 @@ tree-sitter-css.workspace = true
|
|||||||
tree-sitter-elixir.workspace = true
|
tree-sitter-elixir.workspace = true
|
||||||
tree-sitter-elm.workspace = true
|
tree-sitter-elm.workspace = true
|
||||||
tree-sitter-embedded-template.workspace = true
|
tree-sitter-embedded-template.workspace = true
|
||||||
|
tree-sitter-erlang.workspace = true
|
||||||
tree-sitter-gitcommit.workspace = true
|
tree-sitter-gitcommit.workspace = true
|
||||||
tree-sitter-gleam.workspace = true
|
tree-sitter-gleam.workspace = true
|
||||||
tree-sitter-glsl.workspace = true
|
tree-sitter-glsl.workspace = true
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ mod css;
|
|||||||
mod deno;
|
mod deno;
|
||||||
mod elixir;
|
mod elixir;
|
||||||
mod elm;
|
mod elm;
|
||||||
|
mod erlang;
|
||||||
mod gleam;
|
mod gleam;
|
||||||
mod go;
|
mod go;
|
||||||
mod haskell;
|
mod haskell;
|
||||||
@@ -113,6 +114,12 @@ pub fn init(
|
|||||||
),
|
),
|
||||||
}
|
}
|
||||||
language("gitcommit", tree_sitter_gitcommit::language(), vec![]);
|
language("gitcommit", tree_sitter_gitcommit::language(), vec![]);
|
||||||
|
language(
|
||||||
|
"erlang",
|
||||||
|
tree_sitter_erlang::language(),
|
||||||
|
vec![Arc::new(erlang::ErlangLspAdapter)],
|
||||||
|
);
|
||||||
|
|
||||||
language(
|
language(
|
||||||
"gleam",
|
"gleam",
|
||||||
tree_sitter_gleam::language(),
|
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 = [
|
path_suffixes = [
|
||||||
# Refer to https://github.com/neovim/neovim/blob/master/runtime/lua/vim/filetype.lua#L1286-L1290
|
# Refer to https://github.com/neovim/neovim/blob/master/runtime/lua/vim/filetype.lua#L1286-L1290
|
||||||
"TAG_EDITMSG",
|
"TAG_EDITMSG",
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ use db::kvp::KEY_VALUE_STORE;
|
|||||||
use editor::Editor;
|
use editor::Editor;
|
||||||
use env_logger::Builder;
|
use env_logger::Builder;
|
||||||
use fs::RealFs;
|
use fs::RealFs;
|
||||||
|
use fsevent::StreamFlags;
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use gpui::{App, AppContext, AsyncAppContext, Context, SemanticVersion, Task};
|
use gpui::{App, AppContext, AsyncAppContext, Context, SemanticVersion, Task};
|
||||||
use isahc::{prelude::Configurable, Request};
|
use isahc::{prelude::Configurable, Request};
|
||||||
@@ -120,7 +121,7 @@ fn main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
app.run(move |cx| {
|
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") {
|
if let Some(build_sha) = option_env!("ZED_COMMIT_SHA") {
|
||||||
AppCommitSha::set_global(AppCommitSha(build_sha.into()), cx);
|
AppCommitSha::set_global(AppCommitSha(build_sha.into()), cx);
|
||||||
}
|
}
|
||||||
@@ -171,35 +172,8 @@ fn main() {
|
|||||||
);
|
);
|
||||||
assistant::init(cx);
|
assistant::init(cx);
|
||||||
|
|
||||||
// TODO: Should we be loading the themes in a different spot?
|
load_user_themes_in_background(fs.clone(), cx);
|
||||||
cx.spawn({
|
watch_themes(fs.clone(), cx);
|
||||||
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();
|
|
||||||
|
|
||||||
cx.spawn(|_| watch_languages(fs.clone(), languages.clone()))
|
cx.spawn(|_| watch_languages(fs.clone(), languages.clone()))
|
||||||
.detach();
|
.detach();
|
||||||
@@ -274,6 +248,7 @@ fn main() {
|
|||||||
notifications::init(app_state.client.clone(), app_state.user_store.clone(), cx);
|
notifications::init(app_state.client.clone(), app_state.user_store.clone(), cx);
|
||||||
collab_ui::init(&app_state, cx);
|
collab_ui::init(&app_state, cx);
|
||||||
feedback::init(cx);
|
feedback::init(cx);
|
||||||
|
markdown_preview::init(cx);
|
||||||
welcome::init(cx);
|
welcome::init(cx);
|
||||||
|
|
||||||
cx.set_menus(app_menus());
|
cx.set_menus(app_menus());
|
||||||
@@ -608,9 +583,13 @@ fn init_panic_hook(app: &App, installation_id: Option<String>, session_id: Strin
|
|||||||
std::process::exit(-1);
|
std::process::exit(-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
let app_version = client::ZED_APP_VERSION
|
let app_version = if let Some(version) = app_metadata.app_version {
|
||||||
.or(app_metadata.app_version)
|
version.to_string()
|
||||||
.map_or("dev".to_string(), |v| v.to_string());
|
} else {
|
||||||
|
option_env!("CARGO_PKG_VERSION")
|
||||||
|
.unwrap_or("dev")
|
||||||
|
.to_string()
|
||||||
|
};
|
||||||
|
|
||||||
let backtrace = Backtrace::new();
|
let backtrace = Backtrace::new();
|
||||||
let mut backtrace = backtrace
|
let mut backtrace = backtrace
|
||||||
@@ -639,7 +618,7 @@ fn init_panic_hook(app: &App, installation_id: Option<String>, session_id: Strin
|
|||||||
file: location.file().into(),
|
file: location.file().into(),
|
||||||
line: location.line(),
|
line: location.line(),
|
||||||
}),
|
}),
|
||||||
app_version: app_version.clone(),
|
app_version: app_version.to_string(),
|
||||||
release_channel: RELEASE_CHANNEL.display_name().into(),
|
release_channel: RELEASE_CHANNEL.display_name().into(),
|
||||||
os_name: app_metadata.os_name.into(),
|
os_name: app_metadata.os_name.into(),
|
||||||
os_version: app_metadata
|
os_version: app_metadata
|
||||||
@@ -899,6 +878,81 @@ fn load_embedded_fonts(cx: &AppContext) {
|
|||||||
.unwrap();
|
.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>) {
|
async fn watch_languages(fs: Arc<dyn fs::Fs>, languages: Arc<LanguageRegistry>) {
|
||||||
let reload_debounce = Duration::from_millis(250);
|
let reload_debounce = Duration::from_millis(250);
|
||||||
|
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ Finally, Vim mode's search and replace functionality is backed by Zed's. This me
|
|||||||
|
|
||||||
## Custom key bindings
|
## 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`.
|
You can edit your personal key bindings with `:keymap`.
|
||||||
For vim-specific shortcuts, you may find the following template a good place to start:
|
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)
|
- Tree Sitter: [tree-sitter-go](https://github.com/tree-sitter/tree-sitter-go)
|
||||||
- Language Server: [gopls](https://github.com/golang/tools/tree/master/gopls)
|
- 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
|
|
||||||
@@ -79,6 +79,11 @@ version_info=$(rustc --version --verbose)
|
|||||||
host_line=$(echo "$version_info" | grep host)
|
host_line=$(echo "$version_info" | grep host)
|
||||||
local_target_triple=${host_line#*: }
|
local_target_triple=${host_line#*: }
|
||||||
|
|
||||||
|
if [ -z "$ZED_CLIENT_CHECKSUM_SEED" ]; then
|
||||||
|
echo "Missing ZED_CLIENT_CHECKSUM_SEED environment variable"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
if [ "$local_arch" = true ]; then
|
if [ "$local_arch" = true ]; then
|
||||||
echo "Building for local target only."
|
echo "Building for local target only."
|
||||||
cargo build ${build_flag} --package ${zed_crate}
|
cargo build ${build_flag} --package ${zed_crate}
|
||||||
|
|||||||
@@ -3,22 +3,19 @@
|
|||||||
set -eu
|
set -eu
|
||||||
source script/lib/deploy-helpers.sh
|
source script/lib/deploy-helpers.sh
|
||||||
|
|
||||||
if [[ $# < 2 ]]; then
|
if [[ $# != 1 ]]; then
|
||||||
echo "Usage: $0 <production|staging> <tag-name>"
|
echo "Usage: $0 <production|staging>"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
environment=$1
|
environment=$1
|
||||||
version=$2
|
tag="$(tag_for_environment $environment)"
|
||||||
|
|
||||||
export_vars_for_environment ${environment}
|
branch=$(git rev-parse --abbrev-ref HEAD)
|
||||||
image_id=$(image_id_for_version ${version})
|
if [ "$branch" != "main" ]; then
|
||||||
|
echo "You must be on main to run this script"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
export ZED_DO_CERTIFICATE_ID=$(doctl compute certificate list --format ID --no-header)
|
git pull --ff-only origin main
|
||||||
export ZED_KUBE_NAMESPACE=${environment}
|
git tag -f $tag
|
||||||
export ZED_IMAGE_ID=${image_id}
|
git push -f origin $tag
|
||||||
|
|
||||||
target_zed_kube_cluster
|
|
||||||
envsubst < crates/collab/k8s/collab.template.yml | kubectl apply -f -
|
|
||||||
kubectl -n "$environment" rollout status deployment/collab --watch
|
|
||||||
|
|
||||||
echo "deployed collab v${version} to ${environment}"
|
|
||||||
|
|||||||
@@ -8,33 +8,30 @@ function export_vars_for_environment {
|
|||||||
export $(cat $env_file)
|
export $(cat $env_file)
|
||||||
}
|
}
|
||||||
|
|
||||||
function image_id_for_version {
|
|
||||||
local version=$1
|
|
||||||
|
|
||||||
# Check that version is valid
|
|
||||||
if [[ ! ${version} =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
|
||||||
echo "Invalid version number '${version}'" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check that image exists for version
|
|
||||||
tag_names=$(doctl registry repository list-tags collab --no-header --format Tag)
|
|
||||||
if ! $(echo "${tag_names}" | grep -Fqx v${version}); then
|
|
||||||
echo "No docker image tagged for version '${version}'" >&2
|
|
||||||
echo "Found images with these tags:" ${tag_names} >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "registry.digitalocean.com/zed/collab:v${version}"
|
|
||||||
}
|
|
||||||
|
|
||||||
function version_for_image_id {
|
|
||||||
local image_id=$1
|
|
||||||
echo $image_id | cut -d: -f2
|
|
||||||
}
|
|
||||||
|
|
||||||
function target_zed_kube_cluster {
|
function target_zed_kube_cluster {
|
||||||
if [[ $(kubectl config current-context 2> /dev/null) != do-nyc1-zed-1 ]]; then
|
if [[ $(kubectl config current-context 2> /dev/null) != do-nyc1-zed-1 ]]; then
|
||||||
doctl kubernetes cluster kubeconfig save zed-1
|
doctl kubernetes cluster kubeconfig save zed-1
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function tag_for_environment {
|
||||||
|
if [[ "$1" == "production" ]]; then
|
||||||
|
echo "collab-production"
|
||||||
|
elif [[ "$1" == "staging" ]]; then
|
||||||
|
echo "collab-staging"
|
||||||
|
else
|
||||||
|
echo "Invalid environment name '${environment}'" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
function url_for_environment {
|
||||||
|
if [[ "$1" == "production" ]]; then
|
||||||
|
echo "https://collab.zed.dev"
|
||||||
|
elif [[ "$1" == "staging" ]]; then
|
||||||
|
echo "https://collab-staging.zed.dev"
|
||||||
|
else
|
||||||
|
echo "Invalid environment name '${environment}'" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,13 +3,15 @@
|
|||||||
set -eu
|
set -eu
|
||||||
source script/lib/deploy-helpers.sh
|
source script/lib/deploy-helpers.sh
|
||||||
|
|
||||||
if [[ $# < 1 ]]; then
|
if [[ $# != 1 ]]; then
|
||||||
echo "Usage: $0 <production|staging>"
|
echo "Usage: $0 <production|staging>"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
environment=$1
|
|
||||||
|
|
||||||
export_vars_for_environment ${environment}
|
environment=$1
|
||||||
|
url="$(url_for_environment $environment)"
|
||||||
|
tag="$(tag_for_environment $environment)"
|
||||||
|
|
||||||
target_zed_kube_cluster
|
target_zed_kube_cluster
|
||||||
|
|
||||||
deployed_image_id=$(
|
deployed_image_id=$(
|
||||||
@@ -20,18 +22,9 @@ deployed_image_id=$(
|
|||||||
| cut -d: -f2
|
| cut -d: -f2
|
||||||
)
|
)
|
||||||
|
|
||||||
job_image_ids=$(
|
echo "Deployed image version: $deployed_image_id"
|
||||||
kubectl \
|
|
||||||
--namespace=${environment} \
|
|
||||||
get jobs \
|
|
||||||
-o 'jsonpath={range .items[0:5]}{.spec.template.spec.containers[0].image}{"\n"}{end}' \
|
|
||||||
2> /dev/null \
|
|
||||||
|| true
|
|
||||||
)
|
|
||||||
|
|
||||||
echo "Deployed image version:"
|
git fetch >/dev/null
|
||||||
echo "$deployed_image_id"
|
if [[ "$(git rev-parse tags/$tag)" != $deployed_image_id ]]; then
|
||||||
echo
|
echo "NOTE: tags/$tag $(git rev-parse tags/$tag) is not yet deployed"
|
||||||
echo "Migration job image versions:"
|
fi;
|
||||||
echo "$job_image_ids"
|
|
||||||
echo
|
|
||||||
|
|||||||
Reference in New Issue
Block a user