Compare commits
69 Commits
quick-comm
...
assistant-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
967dd02414 | ||
|
|
a32dd07e27 | ||
|
|
57333fdcf0 | ||
|
|
dd5b4f23e6 | ||
|
|
397c19c103 | ||
|
|
5f515089ad | ||
|
|
f8fe881631 | ||
|
|
10eed50765 | ||
|
|
6b44b11fc4 | ||
|
|
2bb7567cbb | ||
|
|
0d8b8492f0 | ||
|
|
4bf61f892f | ||
|
|
fc7874e64e | ||
|
|
dcb0da0a7d | ||
|
|
a9f48bd9d1 | ||
|
|
33197608ed | ||
|
|
f951410ef0 | ||
|
|
47ade2f9f9 | ||
|
|
263e143d1b | ||
|
|
21a44d74bd | ||
|
|
efd485cbb8 | ||
|
|
7a6550c1d1 | ||
|
|
23ad470daf | ||
|
|
6dcec47235 | ||
|
|
e93d62680d | ||
|
|
5dbf68ddc4 | ||
|
|
d8d84bf5d4 | ||
|
|
6f6893a93a | ||
|
|
7ae25d10c8 | ||
|
|
d80683f2bf | ||
|
|
ab98d4889b | ||
|
|
ce11ca9d49 | ||
|
|
edda149d75 | ||
|
|
fc524ad02a | ||
|
|
291ca2c32c | ||
|
|
3ba2af289b | ||
|
|
d8d8c908ed | ||
|
|
680b3dd80b | ||
|
|
607fb34124 | ||
|
|
270e13bb9a | ||
|
|
b3aa7055e4 | ||
|
|
f16461d7d0 | ||
|
|
970f8db5c4 | ||
|
|
bc9086c9af | ||
|
|
a931c7ac06 | ||
|
|
9322069dce | ||
|
|
a367c6de6e | ||
|
|
27d1a566d0 | ||
|
|
52732e75a0 | ||
|
|
4f52077d97 | ||
|
|
192aa78f94 | ||
|
|
c50b572faf | ||
|
|
2e54737a23 | ||
|
|
6e485453d0 | ||
|
|
9bae93cd39 | ||
|
|
1a4b253ee5 | ||
|
|
89f6b65ee6 | ||
|
|
a2c6b4ad2f | ||
|
|
6b7d85b769 | ||
|
|
cb3eb75712 | ||
|
|
bae85d858e | ||
|
|
2e0ae8f1e1 | ||
|
|
f92c892b49 | ||
|
|
1504f9d661 | ||
|
|
755fd695f5 | ||
|
|
74e25c11f1 | ||
|
|
a5e6b222dd | ||
|
|
23e3539f54 | ||
|
|
3303be678e |
1
.github/workflows/ci.yml
vendored
1
.github/workflows/ci.yml
vendored
@@ -386,7 +386,6 @@ jobs:
|
||||
|
||||
- name: Upload app bundle to release
|
||||
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
|
||||
if: ${{ env.RELEASE_CHANNEL == 'preview' || env.RELEASE_CHANNEL == 'stable' }}
|
||||
with:
|
||||
draft: true
|
||||
prerelease: ${{ env.RELEASE_CHANNEL == 'preview' }}
|
||||
|
||||
@@ -14,10 +14,10 @@ jobs:
|
||||
stale-issue-message: >
|
||||
Hi there! 👋
|
||||
|
||||
We're working to clean up our issue tracker by closing older issues that might not be relevant anymore. Are you able to reproduce this issue in the latest version of Zed? If so, please let us know by commenting on this issue and we will keep it open; otherwise, we'll close it in 7 days. Feel free to open a new issue if you're seeing this message after the issue has been closed.
|
||||
We're working to clean up our issue tracker by closing older issues that might not be relevant anymore. If you are able to reproduce this issue in the latest version of Zed, please let us know by commenting on this issue, and we will keep it open. If you can't reproduce it, feel free to close the issue yourself. Otherwise, we'll close it in 7 days.
|
||||
|
||||
Thanks for your help!
|
||||
close-issue-message: "This issue was closed due to inactivity. If you're still experiencing this problem, please open a new issue with a link to this issue."
|
||||
close-issue-message: "This issue was closed due to inactivity. If you're still experiencing this problem, please open a new issue with a link to this issue."
|
||||
# We will increase `days-before-stale` to 365 on or after Jan 24th,
|
||||
# 2024. This date marks one year since migrating issues from
|
||||
# 'community' to 'zed' repository. The migration added activity to all
|
||||
|
||||
35
Cargo.lock
generated
35
Cargo.lock
generated
@@ -1009,6 +1009,7 @@ dependencies = [
|
||||
"smol",
|
||||
"tempfile",
|
||||
"util",
|
||||
"which 6.0.3",
|
||||
"workspace",
|
||||
]
|
||||
|
||||
@@ -1577,7 +1578,7 @@ dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"cexpr",
|
||||
"clang-sys",
|
||||
"itertools 0.12.1",
|
||||
"itertools 0.10.5",
|
||||
"lazy_static",
|
||||
"lazycell",
|
||||
"proc-macro2",
|
||||
@@ -3722,6 +3723,7 @@ dependencies = [
|
||||
"tree-sitter-rust",
|
||||
"tree-sitter-typescript",
|
||||
"ui",
|
||||
"unicode-segmentation",
|
||||
"unindent",
|
||||
"url",
|
||||
"util",
|
||||
@@ -5587,7 +5589,7 @@ dependencies = [
|
||||
"httpdate",
|
||||
"itoa",
|
||||
"pin-project-lite",
|
||||
"socket2 0.5.7",
|
||||
"socket2 0.4.10",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
@@ -6230,6 +6232,7 @@ dependencies = [
|
||||
"lsp",
|
||||
"parking_lot",
|
||||
"postage",
|
||||
"pretty_assertions",
|
||||
"pulldown-cmark 0.12.1",
|
||||
"rand 0.8.5",
|
||||
"regex",
|
||||
@@ -6471,7 +6474,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"windows-targets 0.52.6",
|
||||
"windows-targets 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8985,6 +8988,7 @@ dependencies = [
|
||||
"itertools 0.13.0",
|
||||
"language",
|
||||
"log",
|
||||
"markdown",
|
||||
"menu",
|
||||
"ordered-float 2.10.1",
|
||||
"paths",
|
||||
@@ -9000,6 +9004,7 @@ dependencies = [
|
||||
"smol",
|
||||
"task",
|
||||
"terminal_view",
|
||||
"theme",
|
||||
"ui",
|
||||
"util",
|
||||
"workspace",
|
||||
@@ -9156,6 +9161,7 @@ dependencies = [
|
||||
"client",
|
||||
"clock",
|
||||
"env_logger",
|
||||
"fork",
|
||||
"fs",
|
||||
"futures 0.3.30",
|
||||
"git",
|
||||
@@ -9164,6 +9170,7 @@ dependencies = [
|
||||
"http_client",
|
||||
"language",
|
||||
"languages",
|
||||
"libc",
|
||||
"log",
|
||||
"lsp",
|
||||
"node_runtime",
|
||||
@@ -12436,7 +12443,7 @@ checksum = "2545046bd1473dac6c626659cc2567c6c0ff302fc8b84a56c4243378276f7f57"
|
||||
[[package]]
|
||||
name = "tree-sitter-md"
|
||||
version = "0.3.2"
|
||||
source = "git+https://github.com/zed-industries/tree-sitter-markdown?rev=4cfa6aad6b75052a5077c80fd934757d9267d81b#4cfa6aad6b75052a5077c80fd934757d9267d81b"
|
||||
source = "git+https://github.com/tree-sitter-grammars/tree-sitter-markdown?rev=9a23c1a96c0513d8fc6520972beedd419a973539#9a23c1a96c0513d8fc6520972beedd419a973539"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"tree-sitter-language",
|
||||
@@ -12604,11 +12611,9 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"editor",
|
||||
"gpui",
|
||||
"menu",
|
||||
"settings",
|
||||
"theme",
|
||||
"ui",
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -13422,9 +13427,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasmtime-wasi"
|
||||
version = "24.0.0"
|
||||
version = "24.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "545ae8298ffce025604f7480f9c7d6948c985bef7ce9aee249ef79307813e83c"
|
||||
checksum = "fda03f5bfd5c4cc09f75c7e44846663f25f2c48a2d688fbfb5c7a33af6cf34f5"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -13677,9 +13682,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wiggle"
|
||||
version = "24.0.0"
|
||||
version = "24.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc850ca3c02c5835934d23f28cec4c5a3fb66fe0b4ecd968bbb35609dda5ddc0"
|
||||
checksum = "2d3b31bd2b4d2d82a4b747b8dbc45f566214214a4ffdc5690429a73bc221dc8a"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -13692,9 +13697,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wiggle-generate"
|
||||
version = "24.0.0"
|
||||
version = "24.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "634b8804a67200bcb43ea8af5f7c53e862439a086b68b16fd333454bc74d5aab"
|
||||
checksum = "e2c6136b195fc12067aa9d4e7a5baf118729394df7bc7cbf8c63119bc9f2a7cd"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"heck 0.4.1",
|
||||
@@ -13707,9 +13712,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wiggle-macro"
|
||||
version = "24.0.0"
|
||||
version = "24.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "474b7cbdb942c74031e619d66c600bba7f73867c5800fc2c2306cf307649be2f"
|
||||
checksum = "8a41eaceee468da976ac43b85c4eb82e482f828d5e8e56f49f90dfac2d9bc3b4"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -13739,7 +13744,7 @@ version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
|
||||
dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -459,7 +459,7 @@ tree-sitter-diff = "0.1.0"
|
||||
tree-sitter-html = "0.20"
|
||||
tree-sitter-jsdoc = "0.23"
|
||||
tree-sitter-json = "0.23"
|
||||
tree-sitter-md = { git = "https://github.com/zed-industries/tree-sitter-markdown", rev = "4cfa6aad6b75052a5077c80fd934757d9267d81b" }
|
||||
tree-sitter-md = { git = "https://github.com/tree-sitter-grammars/tree-sitter-markdown", rev = "9a23c1a96c0513d8fc6520972beedd419a973539" }
|
||||
tree-sitter-python = "0.23"
|
||||
tree-sitter-regex = "0.23"
|
||||
tree-sitter-ruby = "0.23"
|
||||
@@ -468,7 +468,7 @@ tree-sitter-typescript = "0.23"
|
||||
tree-sitter-yaml = { git = "https://github.com/zed-industries/tree-sitter-yaml", rev = "baff0b51c64ef6a1fb1f8390f3ad6015b83ec13a" }
|
||||
unicase = "2.6"
|
||||
unindent = "0.1.7"
|
||||
unicode-segmentation = "1.10"
|
||||
unicode-segmentation = "1.11"
|
||||
url = "2.2"
|
||||
uuid = { version = "1.1.2", features = ["v4", "v5", "serde"] }
|
||||
wasmparser = "0.215"
|
||||
|
||||
@@ -65,6 +65,7 @@
|
||||
"h": "c",
|
||||
"handlebars": "code",
|
||||
"hbs": "template",
|
||||
"hcl": "hcl",
|
||||
"heex": "elixir",
|
||||
"heic": "image",
|
||||
"heif": "image",
|
||||
@@ -89,6 +90,7 @@
|
||||
"json": "storage",
|
||||
"jsonc": "storage",
|
||||
"jsx": "react",
|
||||
"julia": "julia",
|
||||
"jxl": "image",
|
||||
"kt": "kotlin",
|
||||
"ldf": "storage",
|
||||
@@ -116,6 +118,7 @@
|
||||
"myd": "storage",
|
||||
"myi": "storage",
|
||||
"nim": "nim",
|
||||
"nix": "nix",
|
||||
"nu": "terminal",
|
||||
"odp": "document",
|
||||
"ods": "document",
|
||||
@@ -143,12 +146,15 @@
|
||||
"rb": "ruby",
|
||||
"rebar.config": "erlang",
|
||||
"rkt": "code",
|
||||
"roc": "roc",
|
||||
"rs": "rust",
|
||||
"rtf": "document",
|
||||
"sass": "sass",
|
||||
"sav": "storage",
|
||||
"sc": "scala",
|
||||
"scala": "scala",
|
||||
"scm": "code",
|
||||
"scss": "sass",
|
||||
"sdf": "storage",
|
||||
"sh": "terminal",
|
||||
"sql": "storage",
|
||||
@@ -182,6 +188,7 @@
|
||||
"yaml": "settings",
|
||||
"yml": "settings",
|
||||
"yrl": "erlang",
|
||||
"zig": "zig",
|
||||
"zlogin": "terminal",
|
||||
"zsh": "terminal",
|
||||
"zsh_aliases": "terminal",
|
||||
@@ -266,6 +273,9 @@
|
||||
"haskell": {
|
||||
"icon": "icons/file_icons/haskell.svg"
|
||||
},
|
||||
"hcl": {
|
||||
"icon": "icons/file_icons/hcl.svg"
|
||||
},
|
||||
"heroku": {
|
||||
"icon": "icons/file_icons/heroku.svg"
|
||||
},
|
||||
@@ -278,6 +288,9 @@
|
||||
"javascript": {
|
||||
"icon": "icons/file_icons/javascript.svg"
|
||||
},
|
||||
"julia": {
|
||||
"icon": "icons/file_icons/julia.svg"
|
||||
},
|
||||
"kotlin": {
|
||||
"icon": "icons/file_icons/kotlin.svg"
|
||||
},
|
||||
@@ -293,6 +306,9 @@
|
||||
"nim": {
|
||||
"icon": "icons/file_icons/nim.svg"
|
||||
},
|
||||
"nix": {
|
||||
"icon": "icons/file_icons/nix.svg"
|
||||
},
|
||||
"ocaml": {
|
||||
"icon": "icons/file_icons/ocaml.svg"
|
||||
},
|
||||
@@ -317,12 +333,18 @@
|
||||
"react": {
|
||||
"icon": "icons/file_icons/react.svg"
|
||||
},
|
||||
"roc": {
|
||||
"icon": "icons/file_icons/roc.svg"
|
||||
},
|
||||
"ruby": {
|
||||
"icon": "icons/file_icons/ruby.svg"
|
||||
},
|
||||
"rust": {
|
||||
"icon": "icons/file_icons/rust.svg"
|
||||
},
|
||||
"sass": {
|
||||
"icon": "icons/file_icons/sass.svg"
|
||||
},
|
||||
"scala": {
|
||||
"icon": "icons/file_icons/scala.svg"
|
||||
},
|
||||
@@ -361,6 +383,9 @@
|
||||
},
|
||||
"vue": {
|
||||
"icon": "icons/file_icons/vue.svg"
|
||||
},
|
||||
"zig": {
|
||||
"icon": "icons/file_icons/zig.svg"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
3
assets/icons/file_icons/hcl.svg
Normal file
3
assets/icons/file_icons/hcl.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.11466 3.11809C7.21859 3.37393 7.09545 3.66558 6.83961 3.76952L4.31181 4.79643C4.1233 4.87302 4 5.05619 4 5.25967V11.5C4 11.7761 3.77614 12 3.5 12H2.5C2.22386 12 2 11.7761 2 11.5V4.41827C2 3.90959 2.30825 3.45164 2.77953 3.26018L6.08686 1.91658C6.34269 1.81265 6.63434 1.93579 6.73828 2.19163L7.11466 3.11809ZM10.5 1.99999C10.7761 1.99999 11 2.22384 11 2.49999V10.5C11 10.7761 10.7761 11 10.5 11H9.5C9.22386 11 9 10.7761 9 10.5V9.49999C9 9.22384 8.77614 8.99999 8.5 8.99999H7.5C7.22386 8.99999 7 9.22384 7 9.49999V13.5C7 13.7761 6.77614 14 6.5 14H5.5C5.22386 14 5 13.7761 5 13.5V5.53124C5 5.25509 5.22386 5.03124 5.5 5.03124H6.5C6.77614 5.03124 7 5.25509 7 5.53124V6.49999C7 6.77613 7.22386 6.99999 7.5 6.99999H8.5C8.77614 6.99999 9 6.77613 9 6.49999V2.49999C9 2.22384 9.22386 1.99999 9.5 1.99999H10.5ZM13.5 4.03124C13.7761 4.03124 14 4.2551 14 4.53124L14 11.5847C14 12.0859 13.7006 12.5386 13.2394 12.7349L9.99399 14.1159C9.7399 14.224 9.44626 14.1057 9.33813 13.8516L8.94658 12.9315C8.83845 12.6774 8.95678 12.3837 9.21087 12.2756L11.6958 11.2182C11.8802 11.1397 12 10.9586 12 10.7581L12 4.53124C12 4.2551 12.2238 4.03124 12.5 4.03124L13.5 4.03124Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
5
assets/icons/file_icons/julia.svg
Normal file
5
assets/icons/file_icons/julia.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="8" cy="5" r="2.75" fill="black"/>
|
||||
<circle cx="4.75" cy="11" r="2.75" fill="black" fill-opacity="0.5"/>
|
||||
<circle cx="11.25" cy="11" r="2.75" fill="black" fill-opacity="0.75"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 289 B |
8
assets/icons/file_icons/nix.svg
Normal file
8
assets/icons/file_icons/nix.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6.00005 4.76556L4.76569 2.74996M6.00005 4.76556L3.75 4.76563M6.00005 4.76556L7.25006 4.7656" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<path d="M10.0232 11.2311L11.2675 13.2406M10.0232 11.2311L12.2732 11.2199M10.0232 11.2311L8.7732 11.2373" stroke="black" stroke-opacity="0.5" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<path d="M9.99025 4.91551L10.9985 2.77781M9.99025 4.91551L8.75599 3.03419M9.99025 4.91551L10.6759 5.9607" stroke="black" stroke-opacity="0.5" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<path d="M6.0323 11.1009L5.03465 13.2436M6.0323 11.1009L7.27585 12.9761M6.0323 11.1009L5.34151 10.0592" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<path d="M11.883 8.19023L14.2466 8.19287M11.883 8.19023L13.0602 6.27268M11.883 8.19023L11.229 9.25547" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<path d="M4.12354 7.8356L1.76002 7.84465M4.12354 7.8356L2.95585 9.75894M4.12354 7.8356L4.7723 6.76713" stroke="black" stroke-opacity="0.5" stroke-width="1.5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
7
assets/icons/file_icons/roc.svg
Normal file
7
assets/icons/file_icons/roc.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5.51497 2.02702L1.92042 1.95067C1.69543 1.94589 1.57917 2.21756 1.73796 2.37702L6.24865 6.9068C6.42388 7.08277 6.72071 6.92326 6.67067 6.68002L5.75454 2.22659C5.73103 2.11231 5.63161 2.02949 5.51497 2.02702Z" fill="black" fill-opacity="0.5"/>
|
||||
<path d="M8.05816 7.38492L12.1366 8.02844C12.3704 8.06532 12.5198 7.78697 12.3599 7.61255L7.30439 2.09814C7.13336 1.91159 6.82522 2.06811 6.87499 2.31624L7.852 7.18714C7.87257 7.28971 7.95483 7.36862 8.05816 7.38492Z" fill="black"/>
|
||||
<path d="M9.0952 10.9797L11.3824 9.35081C11.564 9.22151 11.4983 8.93722 11.2785 8.90058L8.496 8.43683C8.31974 8.40746 8.17047 8.56712 8.21162 8.74101L8.70689 10.8337C8.74777 11.0064 8.95062 11.0827 9.0952 10.9797Z" fill="black" fill-opacity="0.5"/>
|
||||
<path d="M5.10282 13.9632L7.59108 12.4532C7.68331 12.3972 7.72923 12.2884 7.70498 12.1832L6.75736 8.07484C6.699 7.8218 6.34133 7.81448 6.27266 8.06491L4.73201 13.6834C4.67223 13.9014 4.90954 14.0805 5.10282 13.9632Z" fill="black"/>
|
||||
<path d="M11.3183 4.89351L13.1588 7.03149L15.535 6.14302C15.7099 6.07761 15.754 5.85043 15.6161 5.72438L13.7222 3.99219L11.4546 4.48614C11.2695 4.52645 11.1947 4.74995 11.3183 4.89351Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
3
assets/icons/file_icons/sass.svg
Normal file
3
assets/icons/file_icons/sass.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.92096 7.00668C7.87408 7.83549 10.0987 7.48203 10.9376 7.06254C12.8751 6.09381 13.9407 4.39379 12.6407 2.90629C11.0157 1.04692 6.24221 2.49998 4.89844 3.40625C3.55467 4.31252 2.67972 5.53126 2.89071 7.1719C3.1017 8.81254 4.68758 9.7422 6.03128 10.3203C5.38786 10.5616 3.8517 11.0388 3.3125 11.7188C2.71341 12.4742 3.04343 14 4.51577 14C7.15639 14 7.59539 11.1486 7.14847 10.4375C7.88773 10.1295 8.49597 9.96169 9.40138 9.77081C9.63831 9.72087 9.65457 9.46395 9.41295 9.44827C8.80252 9.40864 7.30567 9.8489 6.92096 9.97657C5.78909 9.35157 4.51016 7.93818 4.59378 6.87501C4.68676 5.6928 5.27676 5.07603 6.84508 4.21876C8.01705 3.57813 10.258 3.10695 11.25 3.62501C12.6563 4.35936 10.7875 5.75599 9.92969 6.32031C9.28179 6.74656 8.21971 6.77513 7.22979 6.61435C6.99371 6.576 6.74048 6.84974 6.92096 7.00668ZM5.6719 12.4643C6.35508 11.9894 6.45471 11.1076 6.29955 10.8844C5.76663 11.0874 4.36593 11.9102 4.75111 12.4643C4.90628 12.6875 5.31358 12.7134 5.6719 12.4643Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
5
assets/icons/file_icons/zig.svg
Normal file
5
assets/icons/file_icons/zig.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14.25 12H11C10.794 12 10.6764 11.7648 10.8 11.6L11.925 10.1C11.9722 10.037 12.0463 10 12.125 10H12.75C12.8881 10 13 9.88807 13 9.75V6.25C13 6.11193 12.8881 6 12.75 6H12.4045C12.2187 6 12.0978 5.80442 12.1809 5.6382L12.9309 4.1382C12.9732 4.0535 13.0598 4 13.1545 4H14.25C14.3881 4 14.5 4.11193 14.5 4.25V11.75C14.5 11.8881 14.3881 12 14.25 12Z" fill="black"/>
|
||||
<path d="M1.75 4H5C5.20601 4 5.32361 4.23519 5.2 4.4L4.075 5.9C4.02779 5.96295 3.95369 6 3.875 6H3.25C3.11193 6 3 6.11193 3 6.25V9.75C3 9.88807 3.11193 10 3.25 10H3.59549C3.78134 10 3.90221 10.1956 3.8191 10.3618L3.0691 11.8618C3.02675 11.9465 2.94018 12 2.84549 12H1.75C1.61193 12 1.5 11.8881 1.5 11.75V4.25C1.5 4.11193 1.61193 4 1.75 4Z" fill="black"/>
|
||||
<path d="M7.55748 6H5.95006C5.74177 6 5.62482 5.76022 5.75306 5.59609L6.92493 4.09609C6.97231 4.03544 7.04498 4 7.12194 4H9.93075C9.97607 4 10.0205 3.98769 10.0594 3.96437L11.6408 3.0155C11.8641 2.88154 12.1179 3.13555 11.9837 3.3587L8.22612 9.6083C8.12629 9.77433 8.24508 9.98591 8.43881 9.98712L10.0039 9.9969C10.2092 9.99818 10.3255 10.2327 10.2023 10.3969L9.075 11.9C9.02779 11.963 8.95369 12 8.875 12H6.55383C6.51835 12 6.48328 12.0076 6.45094 12.0222L4.32473 12.9824C4.10122 13.0833 3.88113 12.8356 4.00771 12.6255L7.77161 6.37903C7.87201 6.2124 7.75202 6 7.55748 6Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -26,8 +26,8 @@ use collections::{BTreeSet, HashMap, HashSet};
|
||||
use editor::{
|
||||
actions::{FoldAt, MoveToEndOfLine, Newline, ShowCompletions, UnfoldAt},
|
||||
display_map::{
|
||||
BlockContext, BlockDisposition, BlockId, BlockProperties, BlockStyle, Crease,
|
||||
CreaseMetadata, CustomBlockId, FoldId, RenderBlock, ToDisplayPoint,
|
||||
BlockContext, BlockId, BlockPlacement, BlockProperties, BlockStyle, Crease, CreaseMetadata,
|
||||
CustomBlockId, FoldId, RenderBlock, ToDisplayPoint,
|
||||
},
|
||||
scroll::{Autoscroll, AutoscrollStrategy},
|
||||
Anchor, Editor, EditorEvent, ProposedChangeLocation, ProposedChangesEditor, RowExt,
|
||||
@@ -356,8 +356,10 @@ impl AssistantPanel {
|
||||
let project = workspace.project().clone();
|
||||
pane.set_custom_drop_handle(cx, move |_, dropped_item, cx| {
|
||||
let action = maybe!({
|
||||
if let Some(paths) = dropped_item.downcast_ref::<ExternalPaths>() {
|
||||
return Some(InsertDraggedFiles::ExternalFiles(paths.paths().to_vec()));
|
||||
if project.read(cx).is_local() {
|
||||
if let Some(paths) = dropped_item.downcast_ref::<ExternalPaths>() {
|
||||
return Some(InsertDraggedFiles::ExternalFiles(paths.paths().to_vec()));
|
||||
}
|
||||
}
|
||||
|
||||
let project_paths = if let Some(tab) = dropped_item.downcast_ref::<DraggedTab>()
|
||||
@@ -1444,8 +1446,8 @@ struct ScrollPosition {
|
||||
}
|
||||
|
||||
struct PatchViewState {
|
||||
footer_block_id: CustomBlockId,
|
||||
crease_id: CreaseId,
|
||||
block_id: CustomBlockId,
|
||||
// crease_id: CreaseId,
|
||||
editor: Option<PatchEditorState>,
|
||||
update_task: Option<Task<()>>,
|
||||
}
|
||||
@@ -2007,13 +2009,12 @@ impl ContextEditor {
|
||||
})
|
||||
.map(|(command, error_message)| BlockProperties {
|
||||
style: BlockStyle::Fixed,
|
||||
position: Anchor {
|
||||
height: 1,
|
||||
placement: BlockPlacement::Below(Anchor {
|
||||
buffer_id: Some(buffer_id),
|
||||
excerpt_id,
|
||||
text_anchor: command.source_range.start,
|
||||
},
|
||||
height: 1,
|
||||
disposition: BlockDisposition::Below,
|
||||
}),
|
||||
render: slash_command_error_block_renderer(error_message),
|
||||
priority: 0,
|
||||
}),
|
||||
@@ -2157,8 +2158,8 @@ impl ContextEditor {
|
||||
for range in removed {
|
||||
if let Some(state) = self.patches.remove(range) {
|
||||
editors_to_close.extend(state.editor.and_then(|state| state.editor.upgrade()));
|
||||
removed_block_ids.insert(state.footer_block_id);
|
||||
removed_crease_ids.push(state.crease_id);
|
||||
removed_block_ids.insert(state.block_id);
|
||||
// removed_crease_ids.push(state.crease_id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2188,7 +2189,7 @@ impl ContextEditor {
|
||||
let gutter_width = cx.gutter_dimensions.full_width();
|
||||
let block_id = cx.block_id;
|
||||
this.update(&mut **cx, |this, cx| {
|
||||
this.render_patch_footer(
|
||||
this.render_patch(
|
||||
patch_range.clone(),
|
||||
max_width,
|
||||
gutter_width,
|
||||
@@ -2202,26 +2203,9 @@ impl ContextEditor {
|
||||
}
|
||||
});
|
||||
|
||||
let header_placeholder = FoldPlaceholder {
|
||||
render: {
|
||||
let this = this.clone();
|
||||
let patch_range = range.clone();
|
||||
Arc::new(move |fold_id, _range, cx| {
|
||||
this.update(cx, |this, cx| {
|
||||
this.render_patch_header(patch_range.clone(), fold_id, cx)
|
||||
})
|
||||
.ok()
|
||||
.flatten()
|
||||
.unwrap_or_else(|| Empty.into_any())
|
||||
})
|
||||
},
|
||||
constrain_width: false,
|
||||
merge_adjacent: false,
|
||||
};
|
||||
|
||||
let should_refold;
|
||||
if let Some(state) = self.patches.get_mut(&range) {
|
||||
replaced_blocks.insert(state.footer_block_id, render_block);
|
||||
replaced_blocks.insert(state.block_id, render_block);
|
||||
if let Some(editor_state) = &state.editor {
|
||||
if editor_state.opened_patch != patch {
|
||||
state.update_task = Some({
|
||||
@@ -2240,32 +2224,31 @@ impl ContextEditor {
|
||||
} else {
|
||||
let block_ids = editor.insert_blocks(
|
||||
[BlockProperties {
|
||||
position: patch_start,
|
||||
height: path_count as u32 + 1,
|
||||
style: BlockStyle::Flex,
|
||||
style: BlockStyle::Fixed,
|
||||
render: render_block,
|
||||
disposition: BlockDisposition::Below,
|
||||
placement: BlockPlacement::Replace(patch_start..patch_end),
|
||||
priority: 0,
|
||||
}],
|
||||
None,
|
||||
cx,
|
||||
);
|
||||
|
||||
let new_crease_ids = editor.insert_creases(
|
||||
[Crease::new(
|
||||
patch_start..patch_end,
|
||||
header_placeholder.clone(),
|
||||
fold_toggle("patch-header"),
|
||||
|_, _, _| Empty.into_any_element(),
|
||||
)],
|
||||
cx,
|
||||
);
|
||||
// let new_crease_ids = editor.insert_creases(
|
||||
// [Crease::new(
|
||||
// patch_start..patch_end,
|
||||
// header_placeholder.clone(),
|
||||
// fold_toggle("patch-header"),
|
||||
// |_, _, _| Empty.into_any_element(),
|
||||
// )],
|
||||
// cx,
|
||||
// );
|
||||
|
||||
self.patches.insert(
|
||||
range.clone(),
|
||||
PatchViewState {
|
||||
footer_block_id: block_ids[0],
|
||||
crease_id: new_crease_ids[0],
|
||||
block_id: block_ids[0],
|
||||
// crease_id: new_crease_ids[0],
|
||||
editor: None,
|
||||
update_task: None,
|
||||
},
|
||||
@@ -2275,8 +2258,8 @@ impl ContextEditor {
|
||||
}
|
||||
|
||||
if should_refold {
|
||||
editor.unfold_ranges([patch_start..patch_end], true, false, cx);
|
||||
editor.fold_ranges([(patch_start..patch_end, header_placeholder)], false, cx);
|
||||
// editor.unfold_ranges([patch_start..patch_end], true, false, cx);
|
||||
// editor.fold_ranges([(patch_start..patch_end, header_placeholder)], false, cx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2729,12 +2712,13 @@ impl ContextEditor {
|
||||
})
|
||||
};
|
||||
let create_block_properties = |message: &Message| BlockProperties {
|
||||
position: buffer
|
||||
.anchor_in_excerpt(excerpt_id, message.anchor_range.start)
|
||||
.unwrap(),
|
||||
height: 2,
|
||||
style: BlockStyle::Sticky,
|
||||
disposition: BlockDisposition::Above,
|
||||
placement: BlockPlacement::Above(
|
||||
buffer
|
||||
.anchor_in_excerpt(excerpt_id, message.anchor_range.start)
|
||||
.unwrap(),
|
||||
),
|
||||
priority: usize::MAX,
|
||||
render: render_block(MessageMetadata::from(message)),
|
||||
};
|
||||
@@ -3370,7 +3354,7 @@ impl ContextEditor {
|
||||
let anchor = buffer.anchor_in_excerpt(excerpt_id, anchor).unwrap();
|
||||
let image = render_image.clone();
|
||||
anchor.is_valid(&buffer).then(|| BlockProperties {
|
||||
position: anchor,
|
||||
placement: BlockPlacement::Above(anchor),
|
||||
height: MAX_HEIGHT_IN_LINES,
|
||||
style: BlockStyle::Sticky,
|
||||
render: Box::new(move |cx| {
|
||||
@@ -3391,8 +3375,6 @@ impl ContextEditor {
|
||||
)
|
||||
.into_any_element()
|
||||
}),
|
||||
|
||||
disposition: BlockDisposition::Above,
|
||||
priority: 0,
|
||||
})
|
||||
})
|
||||
@@ -3432,28 +3414,7 @@ impl ContextEditor {
|
||||
.unwrap_or_else(|| Cow::Borrowed(DEFAULT_TAB_TITLE))
|
||||
}
|
||||
|
||||
fn render_patch_header(
|
||||
&self,
|
||||
range: Range<text::Anchor>,
|
||||
_id: FoldId,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<AnyElement> {
|
||||
let patch = self.context.read(cx).patch_for_range(&range, cx)?;
|
||||
let theme = cx.theme().clone();
|
||||
Some(
|
||||
h_flex()
|
||||
.px_1()
|
||||
.py_0p5()
|
||||
.border_b_1()
|
||||
.border_color(theme.status().info_border)
|
||||
.gap_1()
|
||||
.child(Icon::new(IconName::Diff).size(IconSize::Small))
|
||||
.child(Label::new(patch.title.clone()).size(LabelSize::Small))
|
||||
.into_any(),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_patch_footer(
|
||||
fn render_patch(
|
||||
&mut self,
|
||||
range: Range<text::Anchor>,
|
||||
max_width: Pixels,
|
||||
@@ -3469,10 +3430,6 @@ impl ContextEditor {
|
||||
.anchor_in_excerpt(excerpt_id, range.start)
|
||||
.unwrap();
|
||||
|
||||
if !snapshot.intersects_fold(anchor) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let patch = self.context.read(cx).patch_for_range(&range, cx)?;
|
||||
let paths = patch
|
||||
.paths()
|
||||
@@ -3481,10 +3438,13 @@ impl ContextEditor {
|
||||
|
||||
Some(
|
||||
v_flex()
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.id(id)
|
||||
.pl(gutter_width)
|
||||
.w(max_width)
|
||||
.py_2()
|
||||
.ml(gutter_width)
|
||||
.p_2()
|
||||
.rounded_md()
|
||||
.min_h(cx.line_height() * 3.)
|
||||
.cursor(CursorStyle::PointingHand)
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
this.editor.update(cx, |editor, cx| {
|
||||
@@ -3494,6 +3454,7 @@ impl ContextEditor {
|
||||
});
|
||||
this.focus_active_patch(cx);
|
||||
}))
|
||||
.child(Label::new(patch.title.clone()))
|
||||
.children(paths.into_iter().map(|path| {
|
||||
h_flex()
|
||||
.pl_1()
|
||||
|
||||
@@ -2487,7 +2487,8 @@ impl Context {
|
||||
request.messages.push(LanguageModelRequestMessage {
|
||||
role: Role::User,
|
||||
content: vec![
|
||||
"Summarize the context into a short title without punctuation.".into(),
|
||||
"Generate a concise 3-7 word title for this conversation, omitting punctuation"
|
||||
.into(),
|
||||
],
|
||||
cache: false,
|
||||
});
|
||||
|
||||
@@ -9,7 +9,7 @@ use collections::{hash_map, HashMap, HashSet, VecDeque};
|
||||
use editor::{
|
||||
actions::{MoveDown, MoveUp, SelectAll},
|
||||
display_map::{
|
||||
BlockContext, BlockDisposition, BlockProperties, BlockStyle, CustomBlockId, RenderBlock,
|
||||
BlockContext, BlockPlacement, BlockProperties, BlockStyle, CustomBlockId, RenderBlock,
|
||||
ToDisplayPoint,
|
||||
},
|
||||
Anchor, AnchorRangeExt, CodeActionProvider, Editor, EditorElement, EditorEvent, EditorMode,
|
||||
@@ -446,15 +446,14 @@ impl InlineAssistant {
|
||||
let assist_blocks = vec![
|
||||
BlockProperties {
|
||||
style: BlockStyle::Sticky,
|
||||
position: range.start,
|
||||
placement: BlockPlacement::Above(range.start),
|
||||
height: prompt_editor_height,
|
||||
render: build_assist_editor_renderer(prompt_editor),
|
||||
disposition: BlockDisposition::Above,
|
||||
priority: 0,
|
||||
},
|
||||
BlockProperties {
|
||||
style: BlockStyle::Sticky,
|
||||
position: range.end,
|
||||
placement: BlockPlacement::Below(range.end),
|
||||
height: 0,
|
||||
render: Box::new(|cx| {
|
||||
v_flex()
|
||||
@@ -464,7 +463,6 @@ impl InlineAssistant {
|
||||
.border_color(cx.theme().status().info_border)
|
||||
.into_any_element()
|
||||
}),
|
||||
disposition: BlockDisposition::Below,
|
||||
priority: 0,
|
||||
},
|
||||
];
|
||||
@@ -1179,7 +1177,7 @@ impl InlineAssistant {
|
||||
let height =
|
||||
deleted_lines_editor.update(cx, |editor, cx| editor.max_point(cx).row().0 + 1);
|
||||
new_blocks.push(BlockProperties {
|
||||
position: new_row,
|
||||
placement: BlockPlacement::Above(new_row),
|
||||
height,
|
||||
style: BlockStyle::Flex,
|
||||
render: Box::new(move |cx| {
|
||||
@@ -1191,7 +1189,6 @@ impl InlineAssistant {
|
||||
.child(deleted_lines_editor.clone())
|
||||
.into_any_element()
|
||||
}),
|
||||
disposition: BlockDisposition::Above,
|
||||
priority: 0,
|
||||
});
|
||||
}
|
||||
@@ -2256,6 +2253,7 @@ pub enum CodegenEvent {
|
||||
pub struct Codegen {
|
||||
alternatives: Vec<Model<CodegenAlternative>>,
|
||||
active_alternative: usize,
|
||||
seen_alternatives: HashSet<usize>,
|
||||
subscriptions: Vec<Subscription>,
|
||||
buffer: Model<MultiBuffer>,
|
||||
range: Range<Anchor>,
|
||||
@@ -2286,6 +2284,7 @@ impl Codegen {
|
||||
let mut this = Self {
|
||||
alternatives: vec![codegen],
|
||||
active_alternative: 0,
|
||||
seen_alternatives: HashSet::default(),
|
||||
subscriptions: Vec::new(),
|
||||
buffer,
|
||||
range,
|
||||
@@ -2338,6 +2337,7 @@ impl Codegen {
|
||||
fn activate(&mut self, index: usize, cx: &mut ModelContext<Self>) {
|
||||
self.active_alternative()
|
||||
.update(cx, |codegen, cx| codegen.set_active(false, cx));
|
||||
self.seen_alternatives.insert(index);
|
||||
self.active_alternative = index;
|
||||
self.active_alternative()
|
||||
.update(cx, |codegen, cx| codegen.set_active(true, cx));
|
||||
@@ -2467,6 +2467,8 @@ pub struct CodegenAlternative {
|
||||
active: bool,
|
||||
edits: Vec<(Range<Anchor>, String)>,
|
||||
line_operations: Vec<LineOperation>,
|
||||
request: Option<LanguageModelRequest>,
|
||||
elapsed_time: Option<f64>,
|
||||
}
|
||||
|
||||
enum CodegenStatus {
|
||||
@@ -2538,6 +2540,8 @@ impl CodegenAlternative {
|
||||
edits: Vec::new(),
|
||||
line_operations: Vec::new(),
|
||||
range,
|
||||
request: None,
|
||||
elapsed_time: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2634,6 +2638,7 @@ impl CodegenAlternative {
|
||||
async { Ok(stream::empty().boxed()) }.boxed_local()
|
||||
} else {
|
||||
let request = self.build_request(user_prompt, assistant_panel_context, cx)?;
|
||||
self.request = Some(request.clone());
|
||||
|
||||
let chunks = cx
|
||||
.spawn(|_, cx| async move { model.stream_completion_text(request, &cx).await });
|
||||
@@ -2707,6 +2712,7 @@ impl CodegenAlternative {
|
||||
stream: impl 'static + Future<Output = Result<BoxStream<'static, Result<String>>>>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
let start_time = Instant::now();
|
||||
let snapshot = self.snapshot.clone();
|
||||
let selected_text = snapshot
|
||||
.text_for_range(self.range.start..self.range.end)
|
||||
@@ -2923,6 +2929,8 @@ impl CodegenAlternative {
|
||||
};
|
||||
|
||||
let result = generate.await;
|
||||
let elapsed_time = start_time.elapsed().as_secs_f64();
|
||||
|
||||
codegen
|
||||
.update(&mut cx, |this, cx| {
|
||||
this.last_equal_ranges.clear();
|
||||
@@ -2931,6 +2939,7 @@ impl CodegenAlternative {
|
||||
} else {
|
||||
this.status = CodegenStatus::Done;
|
||||
}
|
||||
this.elapsed_time = Some(elapsed_time);
|
||||
cx.emit(CodegenEvent::Finished);
|
||||
cx.notify();
|
||||
})
|
||||
@@ -3277,6 +3286,10 @@ impl CodeActionProvider for AssistantCodeActionProvider {
|
||||
range: Range<text::Anchor>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<Result<Vec<CodeAction>>> {
|
||||
if !AssistantSettings::get_global(cx).enabled {
|
||||
return Task::ready(Ok(Vec::new()));
|
||||
}
|
||||
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
let mut range = range.to_point(&snapshot);
|
||||
|
||||
|
||||
@@ -717,7 +717,6 @@ mod tests {
|
||||
);
|
||||
|
||||
// Ensure InsertBefore merges correctly with Update of the same text
|
||||
|
||||
assert_edits(
|
||||
"
|
||||
fn foo() {
|
||||
@@ -782,6 +781,90 @@ mod tests {
|
||||
.unindent(),
|
||||
cx,
|
||||
);
|
||||
|
||||
// Correctly indent new text when replacing multiple adjacent indented blocks.
|
||||
assert_edits(
|
||||
"
|
||||
impl Numbers {
|
||||
fn one() {
|
||||
1
|
||||
}
|
||||
|
||||
fn two() {
|
||||
2
|
||||
}
|
||||
|
||||
fn three() {
|
||||
3
|
||||
}
|
||||
}
|
||||
"
|
||||
.unindent(),
|
||||
vec![
|
||||
AssistantEditKind::Update {
|
||||
old_text: "
|
||||
fn one() {
|
||||
1
|
||||
}
|
||||
"
|
||||
.unindent(),
|
||||
new_text: "
|
||||
fn one() {
|
||||
101
|
||||
}
|
||||
"
|
||||
.unindent(),
|
||||
description: "pick better number".into(),
|
||||
},
|
||||
AssistantEditKind::Update {
|
||||
old_text: "
|
||||
fn two() {
|
||||
2
|
||||
}
|
||||
"
|
||||
.unindent(),
|
||||
new_text: "
|
||||
fn two() {
|
||||
102
|
||||
}
|
||||
"
|
||||
.unindent(),
|
||||
description: "pick better number".into(),
|
||||
},
|
||||
AssistantEditKind::Update {
|
||||
old_text: "
|
||||
fn three() {
|
||||
3
|
||||
}
|
||||
"
|
||||
.unindent(),
|
||||
new_text: "
|
||||
fn three() {
|
||||
103
|
||||
}
|
||||
"
|
||||
.unindent(),
|
||||
description: "pick better number".into(),
|
||||
},
|
||||
],
|
||||
"
|
||||
impl Numbers {
|
||||
fn one() {
|
||||
101
|
||||
}
|
||||
|
||||
fn two() {
|
||||
102
|
||||
}
|
||||
|
||||
fn three() {
|
||||
103
|
||||
}
|
||||
}
|
||||
"
|
||||
.unindent(),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
|
||||
@@ -145,7 +145,28 @@ impl SlashCommand for ContextServerSlashCommand {
|
||||
return Err(anyhow!("Context server not initialized"));
|
||||
};
|
||||
let result = protocol.run_prompt(&prompt_name, prompt_args).await?;
|
||||
let mut prompt = result.prompt;
|
||||
|
||||
// Check that there are only user roles
|
||||
if result
|
||||
.messages
|
||||
.iter()
|
||||
.any(|msg| !matches!(msg.role, context_servers::types::SamplingRole::User))
|
||||
{
|
||||
return Err(anyhow!(
|
||||
"Prompt contains non-user roles, which is not supported"
|
||||
));
|
||||
}
|
||||
|
||||
// Extract text from user messages into a single prompt string
|
||||
let mut prompt = result
|
||||
.messages
|
||||
.into_iter()
|
||||
.filter_map(|msg| match msg.content {
|
||||
context_servers::types::SamplingContent::Text { text } => Some(text),
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n\n");
|
||||
|
||||
// We must normalize the line endings here, since servers might return CR characters.
|
||||
LineEnding::normalize(&mut prompt);
|
||||
|
||||
@@ -32,4 +32,5 @@ settings.workspace = true
|
||||
smol.workspace = true
|
||||
tempfile.workspace = true
|
||||
util.workspace = true
|
||||
which.workspace = true
|
||||
workspace.workspace = true
|
||||
|
||||
@@ -33,6 +33,7 @@ use std::{
|
||||
};
|
||||
use update_notification::UpdateNotification;
|
||||
use util::ResultExt;
|
||||
use which::which;
|
||||
use workspace::notifications::NotificationId;
|
||||
use workspace::Workspace;
|
||||
|
||||
@@ -560,6 +561,12 @@ impl AutoUpdater {
|
||||
"linux" => Ok("zed.tar.gz"),
|
||||
_ => Err(anyhow!("not supported: {:?}", OS)),
|
||||
}?;
|
||||
|
||||
anyhow::ensure!(
|
||||
which("rsync").is_ok(),
|
||||
"Aborting. Could not find rsync which is required for auto-updates."
|
||||
);
|
||||
|
||||
let downloaded_asset = temp_dir.path().join(filename);
|
||||
download_release(&downloaded_asset, release, client, &cx).await?;
|
||||
|
||||
|
||||
@@ -11,7 +11,8 @@ CREATE TABLE "users" (
|
||||
"metrics_id" TEXT,
|
||||
"github_user_id" INTEGER NOT NULL,
|
||||
"accepted_tos_at" TIMESTAMP WITHOUT TIME ZONE,
|
||||
"github_user_created_at" TIMESTAMP WITHOUT TIME ZONE
|
||||
"github_user_created_at" TIMESTAMP WITHOUT TIME ZONE,
|
||||
"custom_llm_monthly_allowance_in_cents" INTEGER
|
||||
);
|
||||
CREATE UNIQUE INDEX "index_users_github_login" ON "users" ("github_login");
|
||||
CREATE UNIQUE INDEX "index_invite_code_users" ON "users" ("invite_code");
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
alter table users add column custom_llm_monthly_allowance_in_cents integer;
|
||||
@@ -34,7 +34,7 @@ use crate::{
|
||||
db::{billing_subscription::StripeSubscriptionStatus, UserId},
|
||||
llm::db::LlmDatabase,
|
||||
};
|
||||
use crate::{AppState, Error, Result};
|
||||
use crate::{AppState, Cents, Error, Result};
|
||||
|
||||
pub fn router() -> Router {
|
||||
Router::new()
|
||||
@@ -226,6 +226,13 @@ async fn create_billing_subscription(
|
||||
))?
|
||||
};
|
||||
|
||||
if app.db.has_active_billing_subscription(user.id).await? {
|
||||
return Err(Error::http(
|
||||
StatusCode::CONFLICT,
|
||||
"user already has an active subscription".into(),
|
||||
));
|
||||
}
|
||||
|
||||
let customer_id =
|
||||
if let Some(existing_customer) = app.db.get_billing_customer_by_user_id(user.id).await? {
|
||||
CustomerId::from_str(&existing_customer.stripe_customer_id)
|
||||
@@ -655,6 +662,33 @@ async fn handle_customer_subscription_event(
|
||||
)
|
||||
.await?;
|
||||
} else {
|
||||
// If the user already has an active billing subscription, ignore the
|
||||
// event and return an `Ok` to signal that it was processed
|
||||
// successfully.
|
||||
//
|
||||
// There is the possibility that this could cause us to not create a
|
||||
// subscription in the following scenario:
|
||||
//
|
||||
// 1. User has an active subscription A
|
||||
// 2. User cancels subscription A
|
||||
// 3. User creates a new subscription B
|
||||
// 4. We process the new subscription B before the cancellation of subscription A
|
||||
// 5. User ends up with no subscriptions
|
||||
//
|
||||
// In theory this situation shouldn't arise as we try to process the events in the order they occur.
|
||||
if app
|
||||
.db
|
||||
.has_active_billing_subscription(billing_customer.user_id)
|
||||
.await?
|
||||
{
|
||||
log::info!(
|
||||
"user {user_id} already has an active subscription, skipping creation of subscription {subscription_id}",
|
||||
user_id = billing_customer.user_id,
|
||||
subscription_id = subscription.id
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
app.db
|
||||
.create_billing_subscription(&CreateBillingSubscriptionParams {
|
||||
billing_customer_id: billing_customer.id,
|
||||
@@ -680,7 +714,9 @@ struct GetMonthlySpendParams {
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct GetMonthlySpendResponse {
|
||||
monthly_spend_in_cents: i32,
|
||||
monthly_free_tier_spend_in_cents: u32,
|
||||
monthly_free_tier_allowance_in_cents: u32,
|
||||
monthly_spend_in_cents: u32,
|
||||
}
|
||||
|
||||
async fn get_monthly_spend(
|
||||
@@ -700,13 +736,22 @@ async fn get_monthly_spend(
|
||||
));
|
||||
};
|
||||
|
||||
let monthly_spend = llm_db
|
||||
let free_tier = user
|
||||
.custom_llm_monthly_allowance_in_cents
|
||||
.map(|allowance| Cents(allowance as u32))
|
||||
.unwrap_or(FREE_TIER_MONTHLY_SPENDING_LIMIT);
|
||||
|
||||
let spending_for_month = llm_db
|
||||
.get_user_spending_for_month(user.id, Utc::now())
|
||||
.await?
|
||||
.saturating_sub(FREE_TIER_MONTHLY_SPENDING_LIMIT);
|
||||
.await?;
|
||||
|
||||
let free_tier_spend = Cents::min(spending_for_month, free_tier);
|
||||
let monthly_spend = spending_for_month.saturating_sub(free_tier);
|
||||
|
||||
Ok(Json(GetMonthlySpendResponse {
|
||||
monthly_spend_in_cents: monthly_spend.0 as i32,
|
||||
monthly_free_tier_spend_in_cents: free_tier_spend.0,
|
||||
monthly_free_tier_allowance_in_cents: free_tier.0,
|
||||
monthly_spend_in_cents: monthly_spend.0,
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ pub struct Model {
|
||||
pub metrics_id: Uuid,
|
||||
pub created_at: NaiveDateTime,
|
||||
pub accepted_tos_at: Option<NaiveDateTime>,
|
||||
pub custom_llm_monthly_allowance_in_cents: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
|
||||
@@ -459,8 +459,9 @@ async fn check_usage_limit(
|
||||
Utc::now(),
|
||||
)
|
||||
.await?;
|
||||
let free_tier = claims.free_tier_monthly_spending_limit();
|
||||
|
||||
if usage.spending_this_month >= FREE_TIER_MONTHLY_SPENDING_LIMIT {
|
||||
if usage.spending_this_month >= free_tier {
|
||||
if !claims.has_llm_subscription {
|
||||
return Err(Error::http(
|
||||
StatusCode::PAYMENT_REQUIRED,
|
||||
@@ -468,9 +469,7 @@ async fn check_usage_limit(
|
||||
));
|
||||
}
|
||||
|
||||
if (usage.spending_this_month - FREE_TIER_MONTHLY_SPENDING_LIMIT)
|
||||
>= Cents(claims.max_monthly_spend_in_cents)
|
||||
{
|
||||
if (usage.spending_this_month - free_tier) >= Cents(claims.max_monthly_spend_in_cents) {
|
||||
return Err(Error::Http(
|
||||
StatusCode::FORBIDDEN,
|
||||
"Maximum spending limit reached for this month.".to_string(),
|
||||
@@ -640,6 +639,7 @@ impl<S> Drop for TokenCountingStream<S> {
|
||||
tokens,
|
||||
claims.has_llm_subscription,
|
||||
Cents(claims.max_monthly_spend_in_cents),
|
||||
claims.free_tier_monthly_spending_limit(),
|
||||
Utc::now(),
|
||||
)
|
||||
.await
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::db::UserId;
|
||||
use crate::llm::Cents;
|
||||
use crate::{db::UserId, llm::FREE_TIER_MONTHLY_SPENDING_LIMIT};
|
||||
use chrono::{Datelike, Duration};
|
||||
use futures::StreamExt as _;
|
||||
use rpc::LanguageModelProvider;
|
||||
@@ -299,6 +299,7 @@ impl LlmDatabase {
|
||||
tokens: TokenUsage,
|
||||
has_llm_subscription: bool,
|
||||
max_monthly_spend: Cents,
|
||||
free_tier_monthly_spending_limit: Cents,
|
||||
now: DateTimeUtc,
|
||||
) -> Result<Usage> {
|
||||
self.transaction(|tx| async move {
|
||||
@@ -410,9 +411,9 @@ impl LlmDatabase {
|
||||
);
|
||||
|
||||
if !is_staff
|
||||
&& spending_this_month > FREE_TIER_MONTHLY_SPENDING_LIMIT
|
||||
&& spending_this_month > free_tier_monthly_spending_limit
|
||||
&& has_llm_subscription
|
||||
&& (spending_this_month - FREE_TIER_MONTHLY_SPENDING_LIMIT) <= max_monthly_spend
|
||||
&& (spending_this_month - free_tier_monthly_spending_limit) <= max_monthly_spend
|
||||
{
|
||||
billing_event::ActiveModel {
|
||||
id: ActiveValue::not_set(),
|
||||
|
||||
@@ -66,6 +66,7 @@ async fn test_billing_limit_exceeded(db: &mut LlmDatabase) {
|
||||
usage,
|
||||
true,
|
||||
max_monthly_spend,
|
||||
FREE_TIER_MONTHLY_SPENDING_LIMIT,
|
||||
now,
|
||||
)
|
||||
.await
|
||||
@@ -103,6 +104,7 @@ async fn test_billing_limit_exceeded(db: &mut LlmDatabase) {
|
||||
usage_2,
|
||||
true,
|
||||
max_monthly_spend,
|
||||
FREE_TIER_MONTHLY_SPENDING_LIMIT,
|
||||
now,
|
||||
)
|
||||
.await
|
||||
@@ -132,6 +134,7 @@ async fn test_billing_limit_exceeded(db: &mut LlmDatabase) {
|
||||
model,
|
||||
usage_exceeding,
|
||||
true,
|
||||
FREE_TIER_MONTHLY_SPENDING_LIMIT,
|
||||
max_monthly_spend,
|
||||
now,
|
||||
)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::llm::FREE_TIER_MONTHLY_SPENDING_LIMIT;
|
||||
use crate::{
|
||||
db::UserId,
|
||||
llm::db::{
|
||||
@@ -49,6 +50,7 @@ async fn test_tracking_usage(db: &mut LlmDatabase) {
|
||||
},
|
||||
false,
|
||||
Cents::ZERO,
|
||||
FREE_TIER_MONTHLY_SPENDING_LIMIT,
|
||||
now,
|
||||
)
|
||||
.await
|
||||
@@ -68,6 +70,7 @@ async fn test_tracking_usage(db: &mut LlmDatabase) {
|
||||
},
|
||||
false,
|
||||
Cents::ZERO,
|
||||
FREE_TIER_MONTHLY_SPENDING_LIMIT,
|
||||
now,
|
||||
)
|
||||
.await
|
||||
@@ -124,6 +127,7 @@ async fn test_tracking_usage(db: &mut LlmDatabase) {
|
||||
},
|
||||
false,
|
||||
Cents::ZERO,
|
||||
FREE_TIER_MONTHLY_SPENDING_LIMIT,
|
||||
now,
|
||||
)
|
||||
.await
|
||||
@@ -180,6 +184,7 @@ async fn test_tracking_usage(db: &mut LlmDatabase) {
|
||||
},
|
||||
false,
|
||||
Cents::ZERO,
|
||||
FREE_TIER_MONTHLY_SPENDING_LIMIT,
|
||||
now,
|
||||
)
|
||||
.await
|
||||
@@ -222,6 +227,7 @@ async fn test_tracking_usage(db: &mut LlmDatabase) {
|
||||
},
|
||||
false,
|
||||
Cents::ZERO,
|
||||
FREE_TIER_MONTHLY_SPENDING_LIMIT,
|
||||
now,
|
||||
)
|
||||
.await
|
||||
@@ -259,6 +265,7 @@ async fn test_tracking_usage(db: &mut LlmDatabase) {
|
||||
},
|
||||
false,
|
||||
Cents::ZERO,
|
||||
FREE_TIER_MONTHLY_SPENDING_LIMIT,
|
||||
now,
|
||||
)
|
||||
.await
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
use crate::llm::DEFAULT_MAX_MONTHLY_SPEND;
|
||||
use crate::{
|
||||
db::{billing_preference, UserId},
|
||||
Config,
|
||||
};
|
||||
use crate::db::user;
|
||||
use crate::llm::{DEFAULT_MAX_MONTHLY_SPEND, FREE_TIER_MONTHLY_SPENDING_LIMIT};
|
||||
use crate::Cents;
|
||||
use crate::{db::billing_preference, Config};
|
||||
use anyhow::{anyhow, Result};
|
||||
use chrono::Utc;
|
||||
use jsonwebtoken::{DecodingKey, EncodingKey, Header, Validation};
|
||||
@@ -22,6 +21,7 @@ pub struct LlmTokenClaims {
|
||||
pub has_llm_closed_beta_feature_flag: bool,
|
||||
pub has_llm_subscription: bool,
|
||||
pub max_monthly_spend_in_cents: u32,
|
||||
pub custom_llm_monthly_allowance_in_cents: Option<u32>,
|
||||
pub plan: rpc::proto::Plan,
|
||||
}
|
||||
|
||||
@@ -30,8 +30,7 @@ const LLM_TOKEN_LIFETIME: Duration = Duration::from_secs(60 * 60);
|
||||
impl LlmTokenClaims {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn create(
|
||||
user_id: UserId,
|
||||
github_user_login: String,
|
||||
user: &user::Model,
|
||||
is_staff: bool,
|
||||
billing_preferences: Option<billing_preference::Model>,
|
||||
has_llm_closed_beta_feature_flag: bool,
|
||||
@@ -49,8 +48,8 @@ impl LlmTokenClaims {
|
||||
iat: now.timestamp() as u64,
|
||||
exp: (now + LLM_TOKEN_LIFETIME).timestamp() as u64,
|
||||
jti: uuid::Uuid::new_v4().to_string(),
|
||||
user_id: user_id.to_proto(),
|
||||
github_user_login,
|
||||
user_id: user.id.to_proto(),
|
||||
github_user_login: user.github_login.clone(),
|
||||
is_staff,
|
||||
has_llm_closed_beta_feature_flag,
|
||||
has_llm_subscription,
|
||||
@@ -58,6 +57,9 @@ impl LlmTokenClaims {
|
||||
.map_or(DEFAULT_MAX_MONTHLY_SPEND.0, |preferences| {
|
||||
preferences.max_monthly_llm_usage_spending_in_cents as u32
|
||||
}),
|
||||
custom_llm_monthly_allowance_in_cents: user
|
||||
.custom_llm_monthly_allowance_in_cents
|
||||
.map(|allowance| allowance as u32),
|
||||
plan,
|
||||
};
|
||||
|
||||
@@ -89,6 +91,12 @@ impl LlmTokenClaims {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn free_tier_monthly_spending_limit(&self) -> Cents {
|
||||
self.custom_llm_monthly_allowance_in_cents
|
||||
.map(Cents)
|
||||
.unwrap_or(FREE_TIER_MONTHLY_SPENDING_LIMIT)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
|
||||
@@ -4930,8 +4930,7 @@ async fn get_llm_api_token(
|
||||
let billing_preferences = db.get_billing_preferences(user.id).await?;
|
||||
|
||||
let token = LlmTokenClaims::create(
|
||||
user.id,
|
||||
user.github_login.clone(),
|
||||
&user,
|
||||
session.is_staff(),
|
||||
billing_preferences,
|
||||
has_llm_closed_beta_feature_flag,
|
||||
|
||||
@@ -11,7 +11,7 @@ use collections::HashMap;
|
||||
use crate::client::Client;
|
||||
use crate::types;
|
||||
|
||||
const PROTOCOL_VERSION: u32 = 1;
|
||||
const PROTOCOL_VERSION: &str = "2024-10-07";
|
||||
|
||||
pub struct ModelContextProtocol {
|
||||
inner: Client,
|
||||
@@ -22,12 +22,19 @@ impl ModelContextProtocol {
|
||||
Self { inner }
|
||||
}
|
||||
|
||||
fn supported_protocols() -> Vec<types::ProtocolVersion> {
|
||||
vec![
|
||||
types::ProtocolVersion::VersionString(PROTOCOL_VERSION.to_string()),
|
||||
types::ProtocolVersion::VersionNumber(1),
|
||||
]
|
||||
}
|
||||
|
||||
pub async fn initialize(
|
||||
self,
|
||||
client_info: types::Implementation,
|
||||
) -> Result<InitializedContextServerProtocol> {
|
||||
let params = types::InitializeParams {
|
||||
protocol_version: PROTOCOL_VERSION,
|
||||
protocol_version: types::ProtocolVersion::VersionString(PROTOCOL_VERSION.to_string()),
|
||||
capabilities: types::ClientCapabilities {
|
||||
experimental: None,
|
||||
sampling: None,
|
||||
@@ -40,6 +47,13 @@ impl ModelContextProtocol {
|
||||
.request(types::RequestType::Initialize.as_str(), params)
|
||||
.await?;
|
||||
|
||||
if !Self::supported_protocols().contains(&response.protocol_version) {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Unsupported protocol version: {:?}",
|
||||
response.protocol_version
|
||||
));
|
||||
}
|
||||
|
||||
log::trace!("mcp server info {:?}", response.server_info);
|
||||
|
||||
self.inner.notify(
|
||||
|
||||
@@ -36,10 +36,17 @@ impl RequestType {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum ProtocolVersion {
|
||||
VersionString(String),
|
||||
VersionNumber(u32),
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct InitializeParams {
|
||||
pub protocol_version: u32,
|
||||
pub protocol_version: ProtocolVersion,
|
||||
pub capabilities: ClientCapabilities,
|
||||
pub client_info: Implementation,
|
||||
}
|
||||
@@ -131,7 +138,7 @@ pub struct CompletionArgument {
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct InitializeResponse {
|
||||
pub protocol_version: u32,
|
||||
pub protocol_version: ProtocolVersion,
|
||||
pub capabilities: ServerCapabilities,
|
||||
pub server_info: Implementation,
|
||||
}
|
||||
@@ -145,10 +152,9 @@ pub struct ResourcesReadResponse {
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ResourcesListResponse {
|
||||
pub resources: Vec<Resource>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub resource_templates: Option<Vec<ResourceTemplate>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub resources: Option<Vec<Resource>>,
|
||||
pub next_cursor: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
@@ -179,13 +185,15 @@ pub enum SamplingContent {
|
||||
pub struct PromptsGetResponse {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub description: Option<String>,
|
||||
pub prompt: String,
|
||||
pub messages: Vec<SamplingMessage>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PromptsListResponse {
|
||||
pub prompts: Vec<Prompt>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub next_cursor: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
|
||||
@@ -9,7 +9,7 @@ use anyhow::Result;
|
||||
use collections::{BTreeSet, HashSet};
|
||||
use editor::{
|
||||
diagnostic_block_renderer,
|
||||
display_map::{BlockDisposition, BlockProperties, BlockStyle, CustomBlockId, RenderBlock},
|
||||
display_map::{BlockPlacement, BlockProperties, BlockStyle, CustomBlockId, RenderBlock},
|
||||
highlight_diagnostic_message,
|
||||
scroll::Autoscroll,
|
||||
Editor, EditorEvent, ExcerptId, ExcerptRange, MultiBuffer, ToOffset,
|
||||
@@ -439,11 +439,10 @@ impl ProjectDiagnosticsEditor {
|
||||
primary.message.split('\n').next().unwrap().to_string();
|
||||
group_state.block_count += 1;
|
||||
blocks_to_add.push(BlockProperties {
|
||||
position: header_position,
|
||||
placement: BlockPlacement::Above(header_position),
|
||||
height: 2,
|
||||
style: BlockStyle::Sticky,
|
||||
render: diagnostic_header_renderer(primary),
|
||||
disposition: BlockDisposition::Above,
|
||||
priority: 0,
|
||||
});
|
||||
}
|
||||
@@ -459,13 +458,15 @@ impl ProjectDiagnosticsEditor {
|
||||
if !diagnostic.message.is_empty() {
|
||||
group_state.block_count += 1;
|
||||
blocks_to_add.push(BlockProperties {
|
||||
position: (excerpt_id, entry.range.start),
|
||||
placement: BlockPlacement::Below((
|
||||
excerpt_id,
|
||||
entry.range.start,
|
||||
)),
|
||||
height: diagnostic.message.matches('\n').count() as u32 + 1,
|
||||
style: BlockStyle::Fixed,
|
||||
render: diagnostic_block_renderer(
|
||||
diagnostic, None, true, true,
|
||||
),
|
||||
disposition: BlockDisposition::Below,
|
||||
priority: 0,
|
||||
});
|
||||
}
|
||||
@@ -498,13 +499,24 @@ impl ProjectDiagnosticsEditor {
|
||||
editor.remove_blocks(blocks_to_remove, None, cx);
|
||||
let block_ids = editor.insert_blocks(
|
||||
blocks_to_add.into_iter().flat_map(|block| {
|
||||
let (excerpt_id, text_anchor) = block.position;
|
||||
let placement = match block.placement {
|
||||
BlockPlacement::Above((excerpt_id, text_anchor)) => BlockPlacement::Above(
|
||||
excerpts_snapshot.anchor_in_excerpt(excerpt_id, text_anchor)?,
|
||||
),
|
||||
BlockPlacement::Below((excerpt_id, text_anchor)) => BlockPlacement::Below(
|
||||
excerpts_snapshot.anchor_in_excerpt(excerpt_id, text_anchor)?,
|
||||
),
|
||||
BlockPlacement::Replace(_) => {
|
||||
unreachable!(
|
||||
"no Replace block should have been pushed to blocks_to_add"
|
||||
)
|
||||
}
|
||||
};
|
||||
Some(BlockProperties {
|
||||
position: excerpts_snapshot.anchor_in_excerpt(excerpt_id, text_anchor)?,
|
||||
placement,
|
||||
height: block.height,
|
||||
style: block.style,
|
||||
render: block.render,
|
||||
disposition: block.disposition,
|
||||
priority: 0,
|
||||
})
|
||||
}),
|
||||
|
||||
@@ -81,6 +81,7 @@ ui.workspace = true
|
||||
url.workspace = true
|
||||
util.workspace = true
|
||||
workspace.workspace = true
|
||||
unicode-segmentation.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
ctor.workspace = true
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
//! of several smaller structures that form a hierarchy (starting at the bottom):
|
||||
//! - [`InlayMap`] that decides where the [`Inlay`]s should be displayed.
|
||||
//! - [`FoldMap`] that decides where the fold indicators should be; it also tracks parts of a source file that are currently folded.
|
||||
//! - [`TabMap`] that keeps track of hard tabs in a buffer.
|
||||
//! - [`CharMap`] that replaces tabs and non-printable characters
|
||||
//! - [`WrapMap`] that handles soft wrapping.
|
||||
//! - [`BlockMap`] that tracks custom blocks such as diagnostics that should be displayed within buffer.
|
||||
//! - [`DisplayMap`] that adds background highlights to the regions of text.
|
||||
@@ -18,20 +18,22 @@
|
||||
//! [EditorElement]: crate::element::EditorElement
|
||||
|
||||
mod block_map;
|
||||
mod char_map;
|
||||
mod crease_map;
|
||||
mod fold_map;
|
||||
mod inlay_map;
|
||||
mod tab_map;
|
||||
mod invisibles;
|
||||
mod wrap_map;
|
||||
|
||||
use crate::{
|
||||
hover_links::InlayHighlight, movement::TextLayoutDetails, EditorStyle, InlayId, RowExt,
|
||||
};
|
||||
pub use block_map::{
|
||||
Block, BlockBufferRows, BlockChunks as DisplayChunks, BlockContext, BlockDisposition, BlockId,
|
||||
BlockMap, BlockPoint, BlockProperties, BlockStyle, CustomBlockId, RenderBlock,
|
||||
Block, BlockBufferRows, BlockChunks as DisplayChunks, BlockContext, BlockId, BlockMap,
|
||||
BlockPlacement, BlockPoint, BlockProperties, BlockStyle, CustomBlockId, RenderBlock,
|
||||
};
|
||||
use block_map::{BlockRow, BlockSnapshot};
|
||||
use char_map::{CharMap, CharSnapshot};
|
||||
use collections::{HashMap, HashSet};
|
||||
pub use crease_map::*;
|
||||
pub use fold_map::{Fold, FoldId, FoldPlaceholder, FoldPoint};
|
||||
@@ -42,6 +44,7 @@ use gpui::{
|
||||
pub(crate) use inlay_map::Inlay;
|
||||
use inlay_map::{InlayMap, InlaySnapshot};
|
||||
pub use inlay_map::{InlayOffset, InlayPoint};
|
||||
pub use invisibles::is_invisible;
|
||||
use language::{
|
||||
language_settings::language_settings, ChunkRenderer, OffsetUtf16, Point,
|
||||
Subscription as BufferSubscription,
|
||||
@@ -61,9 +64,9 @@ use std::{
|
||||
sync::Arc,
|
||||
};
|
||||
use sum_tree::{Bias, TreeMap};
|
||||
use tab_map::{TabMap, TabSnapshot};
|
||||
use text::LineIndent;
|
||||
use ui::WindowContext;
|
||||
use ui::{px, WindowContext};
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
use wrap_map::{WrapMap, WrapSnapshot};
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
@@ -94,7 +97,7 @@ pub struct DisplayMap {
|
||||
/// Decides where the fold indicators should be and tracks parts of a source file that are currently folded.
|
||||
fold_map: FoldMap,
|
||||
/// Keeps track of hard tabs in a buffer.
|
||||
tab_map: TabMap,
|
||||
char_map: CharMap,
|
||||
/// Handles soft wrapping.
|
||||
wrap_map: Model<WrapMap>,
|
||||
/// Tracks custom blocks such as diagnostics that should be displayed within buffer.
|
||||
@@ -131,7 +134,7 @@ impl DisplayMap {
|
||||
let crease_map = CreaseMap::new(&buffer_snapshot);
|
||||
let (inlay_map, snapshot) = InlayMap::new(buffer_snapshot);
|
||||
let (fold_map, snapshot) = FoldMap::new(snapshot);
|
||||
let (tab_map, snapshot) = TabMap::new(snapshot, tab_size);
|
||||
let (char_map, snapshot) = CharMap::new(snapshot, tab_size);
|
||||
let (wrap_map, snapshot) = WrapMap::new(snapshot, font, font_size, wrap_width, cx);
|
||||
let block_map = BlockMap::new(
|
||||
snapshot,
|
||||
@@ -148,7 +151,7 @@ impl DisplayMap {
|
||||
buffer_subscription,
|
||||
fold_map,
|
||||
inlay_map,
|
||||
tab_map,
|
||||
char_map,
|
||||
wrap_map,
|
||||
block_map,
|
||||
crease_map,
|
||||
@@ -166,17 +169,17 @@ impl DisplayMap {
|
||||
let (inlay_snapshot, edits) = self.inlay_map.sync(buffer_snapshot, edits);
|
||||
let (fold_snapshot, edits) = self.fold_map.read(inlay_snapshot.clone(), edits);
|
||||
let tab_size = Self::tab_size(&self.buffer, cx);
|
||||
let (tab_snapshot, edits) = self.tab_map.sync(fold_snapshot.clone(), edits, tab_size);
|
||||
let (char_snapshot, edits) = self.char_map.sync(fold_snapshot.clone(), edits, tab_size);
|
||||
let (wrap_snapshot, edits) = self
|
||||
.wrap_map
|
||||
.update(cx, |map, cx| map.sync(tab_snapshot.clone(), edits, cx));
|
||||
.update(cx, |map, cx| map.sync(char_snapshot.clone(), edits, cx));
|
||||
let block_snapshot = self.block_map.read(wrap_snapshot.clone(), edits).snapshot;
|
||||
|
||||
DisplaySnapshot {
|
||||
buffer_snapshot: self.buffer.read(cx).snapshot(cx),
|
||||
fold_snapshot,
|
||||
inlay_snapshot,
|
||||
tab_snapshot,
|
||||
char_snapshot,
|
||||
wrap_snapshot,
|
||||
block_snapshot,
|
||||
crease_snapshot: self.crease_map.snapshot(),
|
||||
@@ -212,13 +215,13 @@ impl DisplayMap {
|
||||
let tab_size = Self::tab_size(&self.buffer, cx);
|
||||
let (snapshot, edits) = self.inlay_map.sync(snapshot, edits);
|
||||
let (mut fold_map, snapshot, edits) = self.fold_map.write(snapshot, edits);
|
||||
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
|
||||
let (snapshot, edits) = self.char_map.sync(snapshot, edits, tab_size);
|
||||
let (snapshot, edits) = self
|
||||
.wrap_map
|
||||
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
|
||||
self.block_map.read(snapshot, edits);
|
||||
let (snapshot, edits) = fold_map.fold(ranges);
|
||||
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
|
||||
let (snapshot, edits) = self.char_map.sync(snapshot, edits, tab_size);
|
||||
let (snapshot, edits) = self
|
||||
.wrap_map
|
||||
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
|
||||
@@ -236,13 +239,13 @@ impl DisplayMap {
|
||||
let tab_size = Self::tab_size(&self.buffer, cx);
|
||||
let (snapshot, edits) = self.inlay_map.sync(snapshot, edits);
|
||||
let (mut fold_map, snapshot, edits) = self.fold_map.write(snapshot, edits);
|
||||
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
|
||||
let (snapshot, edits) = self.char_map.sync(snapshot, edits, tab_size);
|
||||
let (snapshot, edits) = self
|
||||
.wrap_map
|
||||
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
|
||||
self.block_map.read(snapshot, edits);
|
||||
let (snapshot, edits) = fold_map.unfold(ranges, inclusive);
|
||||
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
|
||||
let (snapshot, edits) = self.char_map.sync(snapshot, edits, tab_size);
|
||||
let (snapshot, edits) = self
|
||||
.wrap_map
|
||||
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
|
||||
@@ -277,7 +280,7 @@ impl DisplayMap {
|
||||
let tab_size = Self::tab_size(&self.buffer, cx);
|
||||
let (snapshot, edits) = self.inlay_map.sync(snapshot, edits);
|
||||
let (snapshot, edits) = self.fold_map.read(snapshot, edits);
|
||||
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
|
||||
let (snapshot, edits) = self.char_map.sync(snapshot, edits, tab_size);
|
||||
let (snapshot, edits) = self
|
||||
.wrap_map
|
||||
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
|
||||
@@ -295,7 +298,7 @@ impl DisplayMap {
|
||||
let tab_size = Self::tab_size(&self.buffer, cx);
|
||||
let (snapshot, edits) = self.inlay_map.sync(snapshot, edits);
|
||||
let (snapshot, edits) = self.fold_map.read(snapshot, edits);
|
||||
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
|
||||
let (snapshot, edits) = self.char_map.sync(snapshot, edits, tab_size);
|
||||
let (snapshot, edits) = self
|
||||
.wrap_map
|
||||
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
|
||||
@@ -313,7 +316,7 @@ impl DisplayMap {
|
||||
let tab_size = Self::tab_size(&self.buffer, cx);
|
||||
let (snapshot, edits) = self.inlay_map.sync(snapshot, edits);
|
||||
let (snapshot, edits) = self.fold_map.read(snapshot, edits);
|
||||
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
|
||||
let (snapshot, edits) = self.char_map.sync(snapshot, edits, tab_size);
|
||||
let (snapshot, edits) = self
|
||||
.wrap_map
|
||||
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
|
||||
@@ -331,7 +334,7 @@ impl DisplayMap {
|
||||
let tab_size = Self::tab_size(&self.buffer, cx);
|
||||
let (snapshot, edits) = self.inlay_map.sync(snapshot, edits);
|
||||
let (snapshot, edits) = self.fold_map.read(snapshot, edits);
|
||||
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
|
||||
let (snapshot, edits) = self.char_map.sync(snapshot, edits, tab_size);
|
||||
let (snapshot, edits) = self
|
||||
.wrap_map
|
||||
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
|
||||
@@ -407,7 +410,7 @@ impl DisplayMap {
|
||||
let (snapshot, edits) = self.inlay_map.sync(buffer_snapshot, edits);
|
||||
let (snapshot, edits) = self.fold_map.read(snapshot, edits);
|
||||
let tab_size = Self::tab_size(&self.buffer, cx);
|
||||
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
|
||||
let (snapshot, edits) = self.char_map.sync(snapshot, edits, tab_size);
|
||||
let (snapshot, edits) = self
|
||||
.wrap_map
|
||||
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
|
||||
@@ -415,7 +418,7 @@ impl DisplayMap {
|
||||
|
||||
let (snapshot, edits) = self.inlay_map.splice(to_remove, to_insert);
|
||||
let (snapshot, edits) = self.fold_map.read(snapshot, edits);
|
||||
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
|
||||
let (snapshot, edits) = self.char_map.sync(snapshot, edits, tab_size);
|
||||
let (snapshot, edits) = self
|
||||
.wrap_map
|
||||
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
|
||||
@@ -467,7 +470,7 @@ pub struct DisplaySnapshot {
|
||||
pub fold_snapshot: FoldSnapshot,
|
||||
pub crease_snapshot: CreaseSnapshot,
|
||||
inlay_snapshot: InlaySnapshot,
|
||||
tab_snapshot: TabSnapshot,
|
||||
char_snapshot: CharSnapshot,
|
||||
wrap_snapshot: WrapSnapshot,
|
||||
block_snapshot: BlockSnapshot,
|
||||
text_highlights: TextHighlights,
|
||||
@@ -567,8 +570,8 @@ impl DisplaySnapshot {
|
||||
fn point_to_display_point(&self, point: MultiBufferPoint, bias: Bias) -> DisplayPoint {
|
||||
let inlay_point = self.inlay_snapshot.to_inlay_point(point);
|
||||
let fold_point = self.fold_snapshot.to_fold_point(inlay_point, bias);
|
||||
let tab_point = self.tab_snapshot.to_tab_point(fold_point);
|
||||
let wrap_point = self.wrap_snapshot.tab_point_to_wrap_point(tab_point);
|
||||
let char_point = self.char_snapshot.to_char_point(fold_point);
|
||||
let wrap_point = self.wrap_snapshot.char_point_to_wrap_point(char_point);
|
||||
let block_point = self.block_snapshot.to_block_point(wrap_point);
|
||||
DisplayPoint(block_point)
|
||||
}
|
||||
@@ -596,21 +599,21 @@ impl DisplaySnapshot {
|
||||
fn display_point_to_inlay_point(&self, point: DisplayPoint, bias: Bias) -> InlayPoint {
|
||||
let block_point = point.0;
|
||||
let wrap_point = self.block_snapshot.to_wrap_point(block_point);
|
||||
let tab_point = self.wrap_snapshot.to_tab_point(wrap_point);
|
||||
let fold_point = self.tab_snapshot.to_fold_point(tab_point, bias).0;
|
||||
let char_point = self.wrap_snapshot.to_char_point(wrap_point);
|
||||
let fold_point = self.char_snapshot.to_fold_point(char_point, bias).0;
|
||||
fold_point.to_inlay_point(&self.fold_snapshot)
|
||||
}
|
||||
|
||||
pub fn display_point_to_fold_point(&self, point: DisplayPoint, bias: Bias) -> FoldPoint {
|
||||
let block_point = point.0;
|
||||
let wrap_point = self.block_snapshot.to_wrap_point(block_point);
|
||||
let tab_point = self.wrap_snapshot.to_tab_point(wrap_point);
|
||||
self.tab_snapshot.to_fold_point(tab_point, bias).0
|
||||
let char_point = self.wrap_snapshot.to_char_point(wrap_point);
|
||||
self.char_snapshot.to_fold_point(char_point, bias).0
|
||||
}
|
||||
|
||||
pub fn fold_point_to_display_point(&self, fold_point: FoldPoint) -> DisplayPoint {
|
||||
let tab_point = self.tab_snapshot.to_tab_point(fold_point);
|
||||
let wrap_point = self.wrap_snapshot.tab_point_to_wrap_point(tab_point);
|
||||
let char_point = self.char_snapshot.to_char_point(fold_point);
|
||||
let wrap_point = self.wrap_snapshot.char_point_to_wrap_point(char_point);
|
||||
let block_point = self.block_snapshot.to_block_point(wrap_point);
|
||||
DisplayPoint(block_point)
|
||||
}
|
||||
@@ -688,6 +691,23 @@ impl DisplaySnapshot {
|
||||
}
|
||||
}
|
||||
|
||||
if chunk.is_invisible {
|
||||
let invisible_highlight = HighlightStyle {
|
||||
background_color: Some(editor_style.status.hint_background),
|
||||
underline: Some(UnderlineStyle {
|
||||
color: Some(editor_style.status.hint),
|
||||
thickness: px(1.),
|
||||
wavy: false,
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
if let Some(highlight_style) = highlight_style.as_mut() {
|
||||
highlight_style.highlight(invisible_highlight);
|
||||
} else {
|
||||
highlight_style = Some(invisible_highlight);
|
||||
}
|
||||
}
|
||||
|
||||
let mut diagnostic_highlight = HighlightStyle::default();
|
||||
|
||||
if chunk.is_unnecessary {
|
||||
@@ -784,12 +804,11 @@ impl DisplaySnapshot {
|
||||
layout_line.closest_index_for_x(x) as u32
|
||||
}
|
||||
|
||||
pub fn display_chars_at(
|
||||
&self,
|
||||
mut point: DisplayPoint,
|
||||
) -> impl Iterator<Item = (char, DisplayPoint)> + '_ {
|
||||
pub fn grapheme_at(&self, mut point: DisplayPoint) -> Option<String> {
|
||||
point = DisplayPoint(self.block_snapshot.clip_point(point.0, Bias::Left));
|
||||
self.text_chunks(point.row())
|
||||
|
||||
let chars = self
|
||||
.text_chunks(point.row())
|
||||
.flat_map(str::chars)
|
||||
.skip_while({
|
||||
let mut column = 0;
|
||||
@@ -799,16 +818,21 @@ impl DisplaySnapshot {
|
||||
!at_point
|
||||
}
|
||||
})
|
||||
.map(move |ch| {
|
||||
let result = (ch, point);
|
||||
if ch == '\n' {
|
||||
*point.row_mut() += 1;
|
||||
*point.column_mut() = 0;
|
||||
} else {
|
||||
*point.column_mut() += ch.len_utf8() as u32;
|
||||
.take_while({
|
||||
let mut prev = false;
|
||||
move |char| {
|
||||
let now = char.is_ascii();
|
||||
let end = char.is_ascii() && (char.is_ascii_whitespace() || prev);
|
||||
prev = now;
|
||||
!end
|
||||
}
|
||||
result
|
||||
})
|
||||
});
|
||||
|
||||
chars
|
||||
.collect::<String>()
|
||||
.graphemes(true)
|
||||
.next()
|
||||
.map(|s| s.to_owned())
|
||||
}
|
||||
|
||||
pub fn buffer_chars_at(&self, mut offset: usize) -> impl Iterator<Item = (char, usize)> + '_ {
|
||||
@@ -1120,8 +1144,8 @@ impl DisplayPoint {
|
||||
|
||||
pub fn to_offset(self, map: &DisplaySnapshot, bias: Bias) -> usize {
|
||||
let wrap_point = map.block_snapshot.to_wrap_point(self.0);
|
||||
let tab_point = map.wrap_snapshot.to_tab_point(wrap_point);
|
||||
let fold_point = map.tab_snapshot.to_fold_point(tab_point, bias).0;
|
||||
let char_point = map.wrap_snapshot.to_char_point(wrap_point);
|
||||
let fold_point = map.char_snapshot.to_fold_point(char_point, bias).0;
|
||||
let inlay_point = fold_point.to_inlay_point(&map.fold_snapshot);
|
||||
map.inlay_snapshot
|
||||
.to_buffer_offset(map.inlay_snapshot.to_offset(inlay_point))
|
||||
@@ -1156,6 +1180,7 @@ impl ToDisplayPoint for Anchor {
|
||||
pub mod tests {
|
||||
use super::*;
|
||||
use crate::{movement, test::marked_display_snapshot};
|
||||
use block_map::BlockPlacement;
|
||||
use gpui::{div, font, observe, px, AppContext, BorrowAppContext, Context, Element, Hsla};
|
||||
use language::{
|
||||
language_settings::{AllLanguageSettings, AllLanguageSettingsContent},
|
||||
@@ -1228,7 +1253,7 @@ pub mod tests {
|
||||
let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
|
||||
log::info!("buffer text: {:?}", snapshot.buffer_snapshot.text());
|
||||
log::info!("fold text: {:?}", snapshot.fold_snapshot.text());
|
||||
log::info!("tab text: {:?}", snapshot.tab_snapshot.text());
|
||||
log::info!("char text: {:?}", snapshot.char_snapshot.text());
|
||||
log::info!("wrap text: {:?}", snapshot.wrap_snapshot.text());
|
||||
log::info!("block text: {:?}", snapshot.block_snapshot.text());
|
||||
log::info!("display text: {:?}", snapshot.text());
|
||||
@@ -1269,24 +1294,22 @@ pub mod tests {
|
||||
Bias::Left,
|
||||
));
|
||||
|
||||
let disposition = if rng.gen() {
|
||||
BlockDisposition::Above
|
||||
let placement = if rng.gen() {
|
||||
BlockPlacement::Above(position)
|
||||
} else {
|
||||
BlockDisposition::Below
|
||||
BlockPlacement::Below(position)
|
||||
};
|
||||
let height = rng.gen_range(1..5);
|
||||
log::info!(
|
||||
"inserting block {:?} {:?} with height {}",
|
||||
disposition,
|
||||
position.to_point(&buffer),
|
||||
"inserting block {:?} with height {}",
|
||||
placement.as_ref().map(|p| p.to_point(&buffer)),
|
||||
height
|
||||
);
|
||||
let priority = rng.gen_range(1..100);
|
||||
BlockProperties {
|
||||
placement,
|
||||
style: BlockStyle::Fixed,
|
||||
position,
|
||||
height,
|
||||
disposition,
|
||||
render: Box::new(|_| div().into_any()),
|
||||
priority,
|
||||
}
|
||||
@@ -1345,7 +1368,7 @@ pub mod tests {
|
||||
fold_count = snapshot.fold_count();
|
||||
log::info!("buffer text: {:?}", snapshot.buffer_snapshot.text());
|
||||
log::info!("fold text: {:?}", snapshot.fold_snapshot.text());
|
||||
log::info!("tab text: {:?}", snapshot.tab_snapshot.text());
|
||||
log::info!("char text: {:?}", snapshot.char_snapshot.text());
|
||||
log::info!("wrap text: {:?}", snapshot.wrap_snapshot.text());
|
||||
log::info!("block text: {:?}", snapshot.block_snapshot.text());
|
||||
log::info!("display text: {:?}", snapshot.text());
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,6 @@
|
||||
use super::{
|
||||
fold_map::{self, FoldChunks, FoldEdit, FoldPoint, FoldSnapshot},
|
||||
invisibles::{is_invisible, replacement},
|
||||
Highlights,
|
||||
};
|
||||
use language::{Chunk, Point};
|
||||
@@ -9,14 +10,14 @@ use sum_tree::Bias;
|
||||
|
||||
const MAX_EXPANSION_COLUMN: u32 = 256;
|
||||
|
||||
/// Keeps track of hard tabs in a text buffer.
|
||||
/// Keeps track of hard tabs and non-printable characters in a text buffer.
|
||||
///
|
||||
/// See the [`display_map` module documentation](crate::display_map) for more information.
|
||||
pub struct TabMap(TabSnapshot);
|
||||
pub struct CharMap(CharSnapshot);
|
||||
|
||||
impl TabMap {
|
||||
pub fn new(fold_snapshot: FoldSnapshot, tab_size: NonZeroU32) -> (Self, TabSnapshot) {
|
||||
let snapshot = TabSnapshot {
|
||||
impl CharMap {
|
||||
pub fn new(fold_snapshot: FoldSnapshot, tab_size: NonZeroU32) -> (Self, CharSnapshot) {
|
||||
let snapshot = CharSnapshot {
|
||||
fold_snapshot,
|
||||
tab_size,
|
||||
max_expansion_column: MAX_EXPANSION_COLUMN,
|
||||
@@ -26,7 +27,7 @@ impl TabMap {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn set_max_expansion_column(&mut self, column: u32) -> TabSnapshot {
|
||||
pub fn set_max_expansion_column(&mut self, column: u32) -> CharSnapshot {
|
||||
self.0.max_expansion_column = column;
|
||||
self.0.clone()
|
||||
}
|
||||
@@ -36,9 +37,9 @@ impl TabMap {
|
||||
fold_snapshot: FoldSnapshot,
|
||||
mut fold_edits: Vec<FoldEdit>,
|
||||
tab_size: NonZeroU32,
|
||||
) -> (TabSnapshot, Vec<TabEdit>) {
|
||||
) -> (CharSnapshot, Vec<TabEdit>) {
|
||||
let old_snapshot = &mut self.0;
|
||||
let mut new_snapshot = TabSnapshot {
|
||||
let mut new_snapshot = CharSnapshot {
|
||||
fold_snapshot,
|
||||
tab_size,
|
||||
max_expansion_column: old_snapshot.max_expansion_column,
|
||||
@@ -137,15 +138,15 @@ impl TabMap {
|
||||
let new_start = fold_edit.new.start.to_point(&new_snapshot.fold_snapshot);
|
||||
let new_end = fold_edit.new.end.to_point(&new_snapshot.fold_snapshot);
|
||||
tab_edits.push(TabEdit {
|
||||
old: old_snapshot.to_tab_point(old_start)..old_snapshot.to_tab_point(old_end),
|
||||
new: new_snapshot.to_tab_point(new_start)..new_snapshot.to_tab_point(new_end),
|
||||
old: old_snapshot.to_char_point(old_start)..old_snapshot.to_char_point(old_end),
|
||||
new: new_snapshot.to_char_point(new_start)..new_snapshot.to_char_point(new_end),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
new_snapshot.version += 1;
|
||||
tab_edits.push(TabEdit {
|
||||
old: TabPoint::zero()..old_snapshot.max_point(),
|
||||
new: TabPoint::zero()..new_snapshot.max_point(),
|
||||
old: CharPoint::zero()..old_snapshot.max_point(),
|
||||
new: CharPoint::zero()..new_snapshot.max_point(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -155,14 +156,14 @@ impl TabMap {
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct TabSnapshot {
|
||||
pub struct CharSnapshot {
|
||||
pub fold_snapshot: FoldSnapshot,
|
||||
pub tab_size: NonZeroU32,
|
||||
pub max_expansion_column: u32,
|
||||
pub version: usize,
|
||||
}
|
||||
|
||||
impl TabSnapshot {
|
||||
impl CharSnapshot {
|
||||
pub fn buffer_snapshot(&self) -> &MultiBufferSnapshot {
|
||||
&self.fold_snapshot.inlay_snapshot.buffer
|
||||
}
|
||||
@@ -170,7 +171,7 @@ impl TabSnapshot {
|
||||
pub fn line_len(&self, row: u32) -> u32 {
|
||||
let max_point = self.max_point();
|
||||
if row < max_point.row() {
|
||||
self.to_tab_point(FoldPoint::new(row, self.fold_snapshot.line_len(row)))
|
||||
self.to_char_point(FoldPoint::new(row, self.fold_snapshot.line_len(row)))
|
||||
.0
|
||||
.column
|
||||
} else {
|
||||
@@ -179,10 +180,10 @@ impl TabSnapshot {
|
||||
}
|
||||
|
||||
pub fn text_summary(&self) -> TextSummary {
|
||||
self.text_summary_for_range(TabPoint::zero()..self.max_point())
|
||||
self.text_summary_for_range(CharPoint::zero()..self.max_point())
|
||||
}
|
||||
|
||||
pub fn text_summary_for_range(&self, range: Range<TabPoint>) -> TextSummary {
|
||||
pub fn text_summary_for_range(&self, range: Range<CharPoint>) -> TextSummary {
|
||||
let input_start = self.to_fold_point(range.start, Bias::Left).0;
|
||||
let input_end = self.to_fold_point(range.end, Bias::Right).0;
|
||||
let input_summary = self
|
||||
@@ -211,7 +212,7 @@ impl TabSnapshot {
|
||||
} else {
|
||||
for _ in self
|
||||
.chunks(
|
||||
TabPoint::new(range.end.row(), 0)..range.end,
|
||||
CharPoint::new(range.end.row(), 0)..range.end,
|
||||
false,
|
||||
Highlights::default(),
|
||||
)
|
||||
@@ -232,7 +233,7 @@ impl TabSnapshot {
|
||||
|
||||
pub fn chunks<'a>(
|
||||
&'a self,
|
||||
range: Range<TabPoint>,
|
||||
range: Range<CharPoint>,
|
||||
language_aware: bool,
|
||||
highlights: Highlights<'a>,
|
||||
) -> TabChunks<'a> {
|
||||
@@ -251,6 +252,7 @@ impl TabSnapshot {
|
||||
};
|
||||
|
||||
TabChunks {
|
||||
snapshot: self,
|
||||
fold_chunks: self.fold_snapshot.chunks(
|
||||
input_start..input_end,
|
||||
language_aware,
|
||||
@@ -278,7 +280,7 @@ impl TabSnapshot {
|
||||
#[cfg(test)]
|
||||
pub fn text(&self) -> String {
|
||||
self.chunks(
|
||||
TabPoint::zero()..self.max_point(),
|
||||
CharPoint::zero()..self.max_point(),
|
||||
false,
|
||||
Highlights::default(),
|
||||
)
|
||||
@@ -286,24 +288,24 @@ impl TabSnapshot {
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn max_point(&self) -> TabPoint {
|
||||
self.to_tab_point(self.fold_snapshot.max_point())
|
||||
pub fn max_point(&self) -> CharPoint {
|
||||
self.to_char_point(self.fold_snapshot.max_point())
|
||||
}
|
||||
|
||||
pub fn clip_point(&self, point: TabPoint, bias: Bias) -> TabPoint {
|
||||
self.to_tab_point(
|
||||
pub fn clip_point(&self, point: CharPoint, bias: Bias) -> CharPoint {
|
||||
self.to_char_point(
|
||||
self.fold_snapshot
|
||||
.clip_point(self.to_fold_point(point, bias).0, bias),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn to_tab_point(&self, input: FoldPoint) -> TabPoint {
|
||||
pub fn to_char_point(&self, input: FoldPoint) -> CharPoint {
|
||||
let chars = self.fold_snapshot.chars_at(FoldPoint::new(input.row(), 0));
|
||||
let expanded = self.expand_tabs(chars, input.column());
|
||||
TabPoint::new(input.row(), expanded)
|
||||
CharPoint::new(input.row(), expanded)
|
||||
}
|
||||
|
||||
pub fn to_fold_point(&self, output: TabPoint, bias: Bias) -> (FoldPoint, u32, u32) {
|
||||
pub fn to_fold_point(&self, output: CharPoint, bias: Bias) -> (FoldPoint, u32, u32) {
|
||||
let chars = self.fold_snapshot.chars_at(FoldPoint::new(output.row(), 0));
|
||||
let expanded = output.column();
|
||||
let (collapsed, expanded_char_column, to_next_stop) =
|
||||
@@ -315,13 +317,13 @@ impl TabSnapshot {
|
||||
)
|
||||
}
|
||||
|
||||
pub fn make_tab_point(&self, point: Point, bias: Bias) -> TabPoint {
|
||||
pub fn make_char_point(&self, point: Point, bias: Bias) -> CharPoint {
|
||||
let inlay_point = self.fold_snapshot.inlay_snapshot.to_inlay_point(point);
|
||||
let fold_point = self.fold_snapshot.to_fold_point(inlay_point, bias);
|
||||
self.to_tab_point(fold_point)
|
||||
self.to_char_point(fold_point)
|
||||
}
|
||||
|
||||
pub fn to_point(&self, point: TabPoint, bias: Bias) -> Point {
|
||||
pub fn to_point(&self, point: CharPoint, bias: Bias) -> Point {
|
||||
let fold_point = self.to_fold_point(point, bias).0;
|
||||
let inlay_point = fold_point.to_inlay_point(&self.fold_snapshot);
|
||||
self.fold_snapshot
|
||||
@@ -344,6 +346,9 @@ impl TabSnapshot {
|
||||
let tab_len = tab_size - expanded_chars % tab_size;
|
||||
expanded_bytes += tab_len;
|
||||
expanded_chars += tab_len;
|
||||
} else if let Some(replacement) = replacement(c) {
|
||||
expanded_chars += replacement.chars().count() as u32;
|
||||
expanded_bytes += replacement.len() as u32;
|
||||
} else {
|
||||
expanded_bytes += c.len_utf8() as u32;
|
||||
expanded_chars += 1;
|
||||
@@ -383,6 +388,9 @@ impl TabSnapshot {
|
||||
Bias::Right => (collapsed_bytes + 1, expanded_chars, 0),
|
||||
};
|
||||
}
|
||||
} else if let Some(replacement) = replacement(c) {
|
||||
expanded_chars += replacement.chars().count() as u32;
|
||||
expanded_bytes += replacement.len() as u32;
|
||||
} else {
|
||||
expanded_chars += 1;
|
||||
expanded_bytes += c.len_utf8() as u32;
|
||||
@@ -404,9 +412,9 @@ impl TabSnapshot {
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
|
||||
pub struct TabPoint(pub Point);
|
||||
pub struct CharPoint(pub Point);
|
||||
|
||||
impl TabPoint {
|
||||
impl CharPoint {
|
||||
pub fn new(row: u32, column: u32) -> Self {
|
||||
Self(Point::new(row, column))
|
||||
}
|
||||
@@ -424,13 +432,13 @@ impl TabPoint {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Point> for TabPoint {
|
||||
impl From<Point> for CharPoint {
|
||||
fn from(point: Point) -> Self {
|
||||
Self(point)
|
||||
}
|
||||
}
|
||||
|
||||
pub type TabEdit = text::Edit<TabPoint>;
|
||||
pub type TabEdit = text::Edit<CharPoint>;
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq)]
|
||||
pub struct TextSummary {
|
||||
@@ -485,6 +493,7 @@ impl<'a> std::ops::AddAssign<&'a Self> for TextSummary {
|
||||
const SPACES: &str = " ";
|
||||
|
||||
pub struct TabChunks<'a> {
|
||||
snapshot: &'a CharSnapshot,
|
||||
fold_chunks: FoldChunks<'a>,
|
||||
chunk: Chunk<'a>,
|
||||
column: u32,
|
||||
@@ -496,6 +505,37 @@ pub struct TabChunks<'a> {
|
||||
inside_leading_tab: bool,
|
||||
}
|
||||
|
||||
impl<'a> TabChunks<'a> {
|
||||
pub(crate) fn seek(&mut self, range: Range<CharPoint>) {
|
||||
let (input_start, expanded_char_column, to_next_stop) =
|
||||
self.snapshot.to_fold_point(range.start, Bias::Left);
|
||||
let input_column = input_start.column();
|
||||
let input_start = input_start.to_offset(&self.snapshot.fold_snapshot);
|
||||
let input_end = self
|
||||
.snapshot
|
||||
.to_fold_point(range.end, Bias::Right)
|
||||
.0
|
||||
.to_offset(&self.snapshot.fold_snapshot);
|
||||
let to_next_stop = if range.start.0 + Point::new(0, to_next_stop) > range.end.0 {
|
||||
range.end.column() - range.start.column()
|
||||
} else {
|
||||
to_next_stop
|
||||
};
|
||||
|
||||
self.fold_chunks.seek(input_start..input_end);
|
||||
self.input_column = input_column;
|
||||
self.column = expanded_char_column;
|
||||
self.output_position = range.start.0;
|
||||
self.max_output_position = range.end.0;
|
||||
self.chunk = Chunk {
|
||||
text: &SPACES[0..(to_next_stop as usize)],
|
||||
is_tab: true,
|
||||
..Default::default()
|
||||
};
|
||||
self.inside_leading_tab = to_next_stop > 0;
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for TabChunks<'a> {
|
||||
type Item = Chunk<'a>;
|
||||
|
||||
@@ -551,6 +591,37 @@ impl<'a> Iterator for TabChunks<'a> {
|
||||
self.input_column = 0;
|
||||
self.output_position += Point::new(1, 0);
|
||||
}
|
||||
_ if is_invisible(c) => {
|
||||
if ix > 0 {
|
||||
let (prefix, suffix) = self.chunk.text.split_at(ix);
|
||||
self.chunk.text = suffix;
|
||||
return Some(Chunk {
|
||||
text: prefix,
|
||||
is_invisible: false,
|
||||
..self.chunk.clone()
|
||||
});
|
||||
}
|
||||
let c_len = c.len_utf8();
|
||||
let replacement = replacement(c).unwrap_or(&self.chunk.text[..c_len]);
|
||||
if self.chunk.text.len() >= c_len {
|
||||
self.chunk.text = &self.chunk.text[c_len..];
|
||||
} else {
|
||||
self.chunk.text = "";
|
||||
}
|
||||
let len = replacement.chars().count() as u32;
|
||||
let next_output_position = cmp::min(
|
||||
self.output_position + Point::new(0, len),
|
||||
self.max_output_position,
|
||||
);
|
||||
self.column += len;
|
||||
self.input_column += 1;
|
||||
self.output_position = next_output_position;
|
||||
return Some(Chunk {
|
||||
text: replacement,
|
||||
is_invisible: true,
|
||||
..self.chunk.clone()
|
||||
});
|
||||
}
|
||||
_ => {
|
||||
self.column += 1;
|
||||
if !self.inside_leading_tab {
|
||||
@@ -580,11 +651,11 @@ mod tests {
|
||||
let buffer_snapshot = buffer.read(cx).snapshot(cx);
|
||||
let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
|
||||
let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
|
||||
let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
|
||||
let (_, char_snapshot) = CharMap::new(fold_snapshot, 4.try_into().unwrap());
|
||||
|
||||
assert_eq!(tab_snapshot.expand_tabs("\t".chars(), 0), 0);
|
||||
assert_eq!(tab_snapshot.expand_tabs("\t".chars(), 1), 4);
|
||||
assert_eq!(tab_snapshot.expand_tabs("\ta".chars(), 2), 5);
|
||||
assert_eq!(char_snapshot.expand_tabs("\t".chars(), 0), 0);
|
||||
assert_eq!(char_snapshot.expand_tabs("\t".chars(), 1), 4);
|
||||
assert_eq!(char_snapshot.expand_tabs("\ta".chars(), 2), 5);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
@@ -597,16 +668,16 @@ mod tests {
|
||||
let buffer_snapshot = buffer.read(cx).snapshot(cx);
|
||||
let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
|
||||
let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
|
||||
let (_, mut tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
|
||||
let (_, mut char_snapshot) = CharMap::new(fold_snapshot, 4.try_into().unwrap());
|
||||
|
||||
tab_snapshot.max_expansion_column = max_expansion_column;
|
||||
assert_eq!(tab_snapshot.text(), output);
|
||||
char_snapshot.max_expansion_column = max_expansion_column;
|
||||
assert_eq!(char_snapshot.text(), output);
|
||||
|
||||
for (ix, c) in input.char_indices() {
|
||||
assert_eq!(
|
||||
tab_snapshot
|
||||
char_snapshot
|
||||
.chunks(
|
||||
TabPoint::new(0, ix as u32)..tab_snapshot.max_point(),
|
||||
CharPoint::new(0, ix as u32)..char_snapshot.max_point(),
|
||||
false,
|
||||
Highlights::default(),
|
||||
)
|
||||
@@ -620,13 +691,13 @@ mod tests {
|
||||
let input_point = Point::new(0, ix as u32);
|
||||
let output_point = Point::new(0, output.find(c).unwrap() as u32);
|
||||
assert_eq!(
|
||||
tab_snapshot.to_tab_point(FoldPoint(input_point)),
|
||||
TabPoint(output_point),
|
||||
"to_tab_point({input_point:?})"
|
||||
char_snapshot.to_char_point(FoldPoint(input_point)),
|
||||
CharPoint(output_point),
|
||||
"to_char_point({input_point:?})"
|
||||
);
|
||||
assert_eq!(
|
||||
tab_snapshot
|
||||
.to_fold_point(TabPoint(output_point), Bias::Left)
|
||||
char_snapshot
|
||||
.to_fold_point(CharPoint(output_point), Bias::Left)
|
||||
.0,
|
||||
FoldPoint(input_point),
|
||||
"to_fold_point({output_point:?})"
|
||||
@@ -644,10 +715,10 @@ mod tests {
|
||||
let buffer_snapshot = buffer.read(cx).snapshot(cx);
|
||||
let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
|
||||
let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
|
||||
let (_, mut tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
|
||||
let (_, mut char_snapshot) = CharMap::new(fold_snapshot, 4.try_into().unwrap());
|
||||
|
||||
tab_snapshot.max_expansion_column = max_expansion_column;
|
||||
assert_eq!(tab_snapshot.text(), input);
|
||||
char_snapshot.max_expansion_column = max_expansion_column;
|
||||
assert_eq!(char_snapshot.text(), input);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
@@ -658,10 +729,10 @@ mod tests {
|
||||
let buffer_snapshot = buffer.read(cx).snapshot(cx);
|
||||
let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
|
||||
let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
|
||||
let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
|
||||
let (_, char_snapshot) = CharMap::new(fold_snapshot, 4.try_into().unwrap());
|
||||
|
||||
assert_eq!(
|
||||
chunks(&tab_snapshot, TabPoint::zero()),
|
||||
chunks(&char_snapshot, CharPoint::zero()),
|
||||
vec![
|
||||
(" ".to_string(), true),
|
||||
(" ".to_string(), false),
|
||||
@@ -670,7 +741,7 @@ mod tests {
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
chunks(&tab_snapshot, TabPoint::new(0, 2)),
|
||||
chunks(&char_snapshot, CharPoint::new(0, 2)),
|
||||
vec![
|
||||
(" ".to_string(), true),
|
||||
(" ".to_string(), false),
|
||||
@@ -679,7 +750,7 @@ mod tests {
|
||||
]
|
||||
);
|
||||
|
||||
fn chunks(snapshot: &TabSnapshot, start: TabPoint) -> Vec<(String, bool)> {
|
||||
fn chunks(snapshot: &CharSnapshot, start: CharPoint) -> Vec<(String, bool)> {
|
||||
let mut chunks = Vec::new();
|
||||
let mut was_tab = false;
|
||||
let mut text = String::new();
|
||||
@@ -725,12 +796,12 @@ mod tests {
|
||||
let (inlay_snapshot, _) = inlay_map.randomly_mutate(&mut 0, &mut rng);
|
||||
log::info!("InlayMap text: {:?}", inlay_snapshot.text());
|
||||
|
||||
let (mut tab_map, _) = TabMap::new(fold_snapshot.clone(), tab_size);
|
||||
let tabs_snapshot = tab_map.set_max_expansion_column(32);
|
||||
let (mut char_map, _) = CharMap::new(fold_snapshot.clone(), tab_size);
|
||||
let tabs_snapshot = char_map.set_max_expansion_column(32);
|
||||
|
||||
let text = text::Rope::from(tabs_snapshot.text().as_str());
|
||||
log::info!(
|
||||
"TabMap text (tab size: {}): {:?}",
|
||||
"CharMap text (tab size: {}): {:?}",
|
||||
tab_size,
|
||||
tabs_snapshot.text(),
|
||||
);
|
||||
@@ -738,11 +809,11 @@ mod tests {
|
||||
for _ in 0..5 {
|
||||
let end_row = rng.gen_range(0..=text.max_point().row);
|
||||
let end_column = rng.gen_range(0..=text.line_len(end_row));
|
||||
let mut end = TabPoint(text.clip_point(Point::new(end_row, end_column), Bias::Right));
|
||||
let mut end = CharPoint(text.clip_point(Point::new(end_row, end_column), Bias::Right));
|
||||
let start_row = rng.gen_range(0..=text.max_point().row);
|
||||
let start_column = rng.gen_range(0..=text.line_len(start_row));
|
||||
let mut start =
|
||||
TabPoint(text.clip_point(Point::new(start_row, start_column), Bias::Left));
|
||||
CharPoint(text.clip_point(Point::new(start_row, start_column), Bias::Left));
|
||||
if start > end {
|
||||
mem::swap(&mut start, &mut end);
|
||||
}
|
||||
@@ -1100,6 +1100,17 @@ pub struct FoldBufferRows<'a> {
|
||||
fold_point: FoldPoint,
|
||||
}
|
||||
|
||||
impl<'a> FoldBufferRows<'a> {
|
||||
pub(crate) fn seek(&mut self, row: u32) {
|
||||
let fold_point = FoldPoint::new(row, 0);
|
||||
self.cursor.seek(&fold_point, Bias::Left, &());
|
||||
let overshoot = fold_point.0 - self.cursor.start().0 .0;
|
||||
let inlay_point = InlayPoint(self.cursor.start().1 .0 + overshoot);
|
||||
self.input_buffer_rows.seek(inlay_point.row());
|
||||
self.fold_point = fold_point;
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for FoldBufferRows<'a> {
|
||||
type Item = Option<u32>;
|
||||
|
||||
@@ -1135,6 +1146,38 @@ pub struct FoldChunks<'a> {
|
||||
max_output_offset: FoldOffset,
|
||||
}
|
||||
|
||||
impl<'a> FoldChunks<'a> {
|
||||
pub(crate) fn seek(&mut self, range: Range<FoldOffset>) {
|
||||
self.transform_cursor.seek(&range.start, Bias::Right, &());
|
||||
|
||||
let inlay_start = {
|
||||
let overshoot = range.start.0 - self.transform_cursor.start().0 .0;
|
||||
self.transform_cursor.start().1 + InlayOffset(overshoot)
|
||||
};
|
||||
|
||||
let transform_end = self.transform_cursor.end(&());
|
||||
|
||||
let inlay_end = if self
|
||||
.transform_cursor
|
||||
.item()
|
||||
.map_or(true, |transform| transform.is_fold())
|
||||
{
|
||||
inlay_start
|
||||
} else if range.end < transform_end.0 {
|
||||
let overshoot = range.end.0 - self.transform_cursor.start().0 .0;
|
||||
self.transform_cursor.start().1 + InlayOffset(overshoot)
|
||||
} else {
|
||||
transform_end.1
|
||||
};
|
||||
|
||||
self.inlay_chunks.seek(inlay_start..inlay_end);
|
||||
self.inlay_chunk = None;
|
||||
self.inlay_offset = inlay_start;
|
||||
self.output_offset = range.start;
|
||||
self.max_output_offset = range.end;
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for FoldChunks<'a> {
|
||||
type Item = Chunk<'a>;
|
||||
|
||||
|
||||
157
crates/editor/src/display_map/invisibles.rs
Normal file
157
crates/editor/src/display_map/invisibles.rs
Normal file
@@ -0,0 +1,157 @@
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use collections::HashMap;
|
||||
|
||||
// Invisibility in a Unicode context is not well defined, so we have to guess.
|
||||
//
|
||||
// We highlight all ASCII control codes, and unicode whitespace because they are likely
|
||||
// confused with a normal space (U+0020).
|
||||
//
|
||||
// We also highlight the handful of blank non-space characters:
|
||||
// U+2800 BRAILLE PATTERN BLANK - Category: So
|
||||
// U+115F HANGUL CHOSEONG FILLER - Category: Lo
|
||||
// U+1160 HANGUL CHOSEONG FILLER - Category: Lo
|
||||
// U+3164 HANGUL FILLER - Category: Lo
|
||||
// U+FFA0 HALFWIDTH HANGUL FILLER - Category: Lo
|
||||
// U+FFFC OBJECT REPLACEMENT CHARACTER - Category: So
|
||||
//
|
||||
// For the rest of Unicode, invisibility happens for two reasons:
|
||||
// * A Format character (like a byte order mark or right-to-left override)
|
||||
// * An invisible Nonspacing Mark character (like U+034F, or variation selectors)
|
||||
//
|
||||
// We don't consider unassigned codepoints invisible as the font renderer already shows
|
||||
// a replacement character in that case (and there are a *lot* of them)
|
||||
//
|
||||
// Control characters are mostly fine to highlight; except:
|
||||
// * U+E0020..=U+E007F are used in emoji flags. We don't highlight them right now, but we could if we tightened our heuristics.
|
||||
// * U+200D is used to join characters. We highlight this but don't replace it. As our font system ignores mid-glyph highlights this mostly works to highlight unexpected uses.
|
||||
//
|
||||
// Nonspacing marks are handled like U+200D. This means that mid-glyph we ignore them, but
|
||||
// probably causes issues with end-of-glyph usage.
|
||||
//
|
||||
// ref: https://invisible-characters.com
|
||||
// ref: https://www.compart.com/en/unicode/category/Cf
|
||||
// ref: https://gist.github.com/ConradIrwin/f759e1fc29267143c4c7895aa495dca5?h=1
|
||||
// ref: https://unicode.org/Public/emoji/13.0/emoji-test.txt
|
||||
// https://github.com/bits/UTF-8-Unicode-Test-Documents/blob/master/UTF-8_sequence_separated/utf8_sequence_0-0x10ffff_assigned_including-unprintable-asis.txt
|
||||
pub fn is_invisible(c: char) -> bool {
|
||||
if c <= '\u{1f}' {
|
||||
c != '\t' && c != '\n' && c != '\r'
|
||||
} else if c >= '\u{7f}' {
|
||||
c <= '\u{9f}' || c.is_whitespace() || contains(c, &FORMAT) || contains(c, &OTHER)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn replacement(c: char) -> Option<&'static str> {
|
||||
if !is_invisible(c) {
|
||||
return None;
|
||||
}
|
||||
if c <= '\x7f' {
|
||||
REPLACEMENTS.get(&c).copied()
|
||||
} else if contains(c, &PRESERVE) {
|
||||
None
|
||||
} else {
|
||||
Some(" ")
|
||||
}
|
||||
}
|
||||
|
||||
const REPLACEMENTS: LazyLock<HashMap<char, &'static str>> = LazyLock::new(|| {
|
||||
[
|
||||
('\x00', "␀"),
|
||||
('\x01', "␁"),
|
||||
('\x02', "␂"),
|
||||
('\x03', "␃"),
|
||||
('\x04', "␄"),
|
||||
('\x05', "␅"),
|
||||
('\x06', "␆"),
|
||||
('\x07', "␇"),
|
||||
('\x08', "␈"),
|
||||
('\x0B', "␋"),
|
||||
('\x0C', "␌"),
|
||||
('\x0D', "␍"),
|
||||
('\x0E', "␎"),
|
||||
('\x0F', "␏"),
|
||||
('\x10', "␐"),
|
||||
('\x11', "␑"),
|
||||
('\x12', "␒"),
|
||||
('\x13', "␓"),
|
||||
('\x14', "␔"),
|
||||
('\x15', "␕"),
|
||||
('\x16', "␖"),
|
||||
('\x17', "␗"),
|
||||
('\x18', "␘"),
|
||||
('\x19', "␙"),
|
||||
('\x1A', "␚"),
|
||||
('\x1B', "␛"),
|
||||
('\x1C', "␜"),
|
||||
('\x1D', "␝"),
|
||||
('\x1E', "␞"),
|
||||
('\x1F', "␟"),
|
||||
('\u{007F}', "␡"),
|
||||
]
|
||||
.into_iter()
|
||||
.collect()
|
||||
});
|
||||
|
||||
// generated using ucd-generate: ucd-generate general-category --include Format --chars ucd-16.0.0
|
||||
pub const FORMAT: &'static [(char, char)] = &[
|
||||
('\u{ad}', '\u{ad}'),
|
||||
('\u{600}', '\u{605}'),
|
||||
('\u{61c}', '\u{61c}'),
|
||||
('\u{6dd}', '\u{6dd}'),
|
||||
('\u{70f}', '\u{70f}'),
|
||||
('\u{890}', '\u{891}'),
|
||||
('\u{8e2}', '\u{8e2}'),
|
||||
('\u{180e}', '\u{180e}'),
|
||||
('\u{200b}', '\u{200f}'),
|
||||
('\u{202a}', '\u{202e}'),
|
||||
('\u{2060}', '\u{2064}'),
|
||||
('\u{2066}', '\u{206f}'),
|
||||
('\u{feff}', '\u{feff}'),
|
||||
('\u{fff9}', '\u{fffb}'),
|
||||
('\u{110bd}', '\u{110bd}'),
|
||||
('\u{110cd}', '\u{110cd}'),
|
||||
('\u{13430}', '\u{1343f}'),
|
||||
('\u{1bca0}', '\u{1bca3}'),
|
||||
('\u{1d173}', '\u{1d17a}'),
|
||||
('\u{e0001}', '\u{e0001}'),
|
||||
('\u{e0020}', '\u{e007f}'),
|
||||
];
|
||||
|
||||
// hand-made base on https://invisible-characters.com (Excluding Cf)
|
||||
pub const OTHER: &'static [(char, char)] = &[
|
||||
('\u{034f}', '\u{034f}'),
|
||||
('\u{115F}', '\u{1160}'),
|
||||
('\u{17b4}', '\u{17b5}'),
|
||||
('\u{180b}', '\u{180d}'),
|
||||
('\u{2800}', '\u{2800}'),
|
||||
('\u{3164}', '\u{3164}'),
|
||||
('\u{fe00}', '\u{fe0d}'),
|
||||
('\u{ffa0}', '\u{ffa0}'),
|
||||
('\u{fffc}', '\u{fffc}'),
|
||||
('\u{e0100}', '\u{e01ef}'),
|
||||
];
|
||||
|
||||
// a subset of FORMAT/OTHER that may appear within glyphs
|
||||
const PRESERVE: &'static [(char, char)] = &[
|
||||
('\u{034f}', '\u{034f}'),
|
||||
('\u{200d}', '\u{200d}'),
|
||||
('\u{17b4}', '\u{17b5}'),
|
||||
('\u{180b}', '\u{180d}'),
|
||||
('\u{e0061}', '\u{e007a}'),
|
||||
('\u{e007f}', '\u{e007f}'),
|
||||
];
|
||||
|
||||
fn contains(c: char, list: &[(char, char)]) -> bool {
|
||||
for (start, end) in list {
|
||||
if c < *start {
|
||||
return false;
|
||||
}
|
||||
if c <= *end {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
use super::{
|
||||
char_map::{self, CharPoint, CharSnapshot, TabEdit},
|
||||
fold_map::FoldBufferRows,
|
||||
tab_map::{self, TabEdit, TabPoint, TabSnapshot},
|
||||
Highlights,
|
||||
};
|
||||
use gpui::{AppContext, Context, Font, LineWrapper, Model, ModelContext, Pixels, Task};
|
||||
@@ -12,7 +12,7 @@ use std::{cmp, collections::VecDeque, mem, ops::Range, time::Duration};
|
||||
use sum_tree::{Bias, Cursor, SumTree};
|
||||
use text::Patch;
|
||||
|
||||
pub use super::tab_map::TextSummary;
|
||||
pub use super::char_map::TextSummary;
|
||||
pub type WrapEdit = text::Edit<u32>;
|
||||
|
||||
/// Handles soft wrapping of text.
|
||||
@@ -20,7 +20,7 @@ pub type WrapEdit = text::Edit<u32>;
|
||||
/// See the [`display_map` module documentation](crate::display_map) for more information.
|
||||
pub struct WrapMap {
|
||||
snapshot: WrapSnapshot,
|
||||
pending_edits: VecDeque<(TabSnapshot, Vec<TabEdit>)>,
|
||||
pending_edits: VecDeque<(CharSnapshot, Vec<TabEdit>)>,
|
||||
interpolated_edits: Patch<u32>,
|
||||
edits_since_sync: Patch<u32>,
|
||||
wrap_width: Option<Pixels>,
|
||||
@@ -30,7 +30,7 @@ pub struct WrapMap {
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct WrapSnapshot {
|
||||
tab_snapshot: TabSnapshot,
|
||||
char_snapshot: CharSnapshot,
|
||||
transforms: SumTree<Transform>,
|
||||
interpolated: bool,
|
||||
}
|
||||
@@ -51,11 +51,12 @@ struct TransformSummary {
|
||||
pub struct WrapPoint(pub Point);
|
||||
|
||||
pub struct WrapChunks<'a> {
|
||||
input_chunks: tab_map::TabChunks<'a>,
|
||||
input_chunks: char_map::TabChunks<'a>,
|
||||
input_chunk: Chunk<'a>,
|
||||
output_position: WrapPoint,
|
||||
max_output_row: u32,
|
||||
transforms: Cursor<'a, Transform, (WrapPoint, TabPoint)>,
|
||||
transforms: Cursor<'a, Transform, (WrapPoint, CharPoint)>,
|
||||
snapshot: &'a WrapSnapshot,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -65,12 +66,27 @@ pub struct WrapBufferRows<'a> {
|
||||
output_row: u32,
|
||||
soft_wrapped: bool,
|
||||
max_output_row: u32,
|
||||
transforms: Cursor<'a, Transform, (WrapPoint, TabPoint)>,
|
||||
transforms: Cursor<'a, Transform, (WrapPoint, CharPoint)>,
|
||||
}
|
||||
|
||||
impl<'a> WrapBufferRows<'a> {
|
||||
pub(crate) fn seek(&mut self, start_row: u32) {
|
||||
self.transforms
|
||||
.seek(&WrapPoint::new(start_row, 0), Bias::Left, &());
|
||||
let mut input_row = self.transforms.start().1.row();
|
||||
if self.transforms.item().map_or(false, |t| t.is_isomorphic()) {
|
||||
input_row += start_row - self.transforms.start().0.row();
|
||||
}
|
||||
self.soft_wrapped = self.transforms.item().map_or(false, |t| !t.is_isomorphic());
|
||||
self.input_buffer_rows.seek(input_row);
|
||||
self.input_buffer_row = self.input_buffer_rows.next().unwrap();
|
||||
self.output_row = start_row;
|
||||
}
|
||||
}
|
||||
|
||||
impl WrapMap {
|
||||
pub fn new(
|
||||
tab_snapshot: TabSnapshot,
|
||||
char_snapshot: CharSnapshot,
|
||||
font: Font,
|
||||
font_size: Pixels,
|
||||
wrap_width: Option<Pixels>,
|
||||
@@ -83,7 +99,7 @@ impl WrapMap {
|
||||
pending_edits: Default::default(),
|
||||
interpolated_edits: Default::default(),
|
||||
edits_since_sync: Default::default(),
|
||||
snapshot: WrapSnapshot::new(tab_snapshot),
|
||||
snapshot: WrapSnapshot::new(char_snapshot),
|
||||
background_task: None,
|
||||
};
|
||||
this.set_wrap_width(wrap_width, cx);
|
||||
@@ -101,17 +117,17 @@ impl WrapMap {
|
||||
|
||||
pub fn sync(
|
||||
&mut self,
|
||||
tab_snapshot: TabSnapshot,
|
||||
char_snapshot: CharSnapshot,
|
||||
edits: Vec<TabEdit>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> (WrapSnapshot, Patch<u32>) {
|
||||
if self.wrap_width.is_some() {
|
||||
self.pending_edits.push_back((tab_snapshot, edits));
|
||||
self.pending_edits.push_back((char_snapshot, edits));
|
||||
self.flush_edits(cx);
|
||||
} else {
|
||||
self.edits_since_sync = self
|
||||
.edits_since_sync
|
||||
.compose(self.snapshot.interpolate(tab_snapshot, &edits));
|
||||
.compose(self.snapshot.interpolate(char_snapshot, &edits));
|
||||
self.snapshot.interpolated = false;
|
||||
}
|
||||
|
||||
@@ -161,11 +177,11 @@ impl WrapMap {
|
||||
let (font, font_size) = self.font_with_size.clone();
|
||||
let task = cx.background_executor().spawn(async move {
|
||||
let mut line_wrapper = text_system.line_wrapper(font, font_size);
|
||||
let tab_snapshot = new_snapshot.tab_snapshot.clone();
|
||||
let range = TabPoint::zero()..tab_snapshot.max_point();
|
||||
let char_snapshot = new_snapshot.char_snapshot.clone();
|
||||
let range = CharPoint::zero()..char_snapshot.max_point();
|
||||
let edits = new_snapshot
|
||||
.update(
|
||||
tab_snapshot,
|
||||
char_snapshot,
|
||||
&[TabEdit {
|
||||
old: range.clone(),
|
||||
new: range.clone(),
|
||||
@@ -205,7 +221,7 @@ impl WrapMap {
|
||||
} else {
|
||||
let old_rows = self.snapshot.transforms.summary().output.lines.row + 1;
|
||||
self.snapshot.transforms = SumTree::default();
|
||||
let summary = self.snapshot.tab_snapshot.text_summary();
|
||||
let summary = self.snapshot.char_snapshot.text_summary();
|
||||
if !summary.lines.is_zero() {
|
||||
self.snapshot
|
||||
.transforms
|
||||
@@ -223,8 +239,8 @@ impl WrapMap {
|
||||
fn flush_edits(&mut self, cx: &mut ModelContext<Self>) {
|
||||
if !self.snapshot.interpolated {
|
||||
let mut to_remove_len = 0;
|
||||
for (tab_snapshot, _) in &self.pending_edits {
|
||||
if tab_snapshot.version <= self.snapshot.tab_snapshot.version {
|
||||
for (char_snapshot, _) in &self.pending_edits {
|
||||
if char_snapshot.version <= self.snapshot.char_snapshot.version {
|
||||
to_remove_len += 1;
|
||||
} else {
|
||||
break;
|
||||
@@ -246,9 +262,9 @@ impl WrapMap {
|
||||
let update_task = cx.background_executor().spawn(async move {
|
||||
let mut edits = Patch::default();
|
||||
let mut line_wrapper = text_system.line_wrapper(font, font_size);
|
||||
for (tab_snapshot, tab_edits) in pending_edits {
|
||||
for (char_snapshot, tab_edits) in pending_edits {
|
||||
let wrap_edits = snapshot
|
||||
.update(tab_snapshot, &tab_edits, wrap_width, &mut line_wrapper)
|
||||
.update(char_snapshot, &tab_edits, wrap_width, &mut line_wrapper)
|
||||
.await;
|
||||
edits = edits.compose(&wrap_edits);
|
||||
}
|
||||
@@ -285,11 +301,11 @@ impl WrapMap {
|
||||
|
||||
let was_interpolated = self.snapshot.interpolated;
|
||||
let mut to_remove_len = 0;
|
||||
for (tab_snapshot, edits) in &self.pending_edits {
|
||||
if tab_snapshot.version <= self.snapshot.tab_snapshot.version {
|
||||
for (char_snapshot, edits) in &self.pending_edits {
|
||||
if char_snapshot.version <= self.snapshot.char_snapshot.version {
|
||||
to_remove_len += 1;
|
||||
} else {
|
||||
let interpolated_edits = self.snapshot.interpolate(tab_snapshot.clone(), edits);
|
||||
let interpolated_edits = self.snapshot.interpolate(char_snapshot.clone(), edits);
|
||||
self.edits_since_sync = self.edits_since_sync.compose(&interpolated_edits);
|
||||
self.interpolated_edits = self.interpolated_edits.compose(&interpolated_edits);
|
||||
}
|
||||
@@ -302,45 +318,49 @@ impl WrapMap {
|
||||
}
|
||||
|
||||
impl WrapSnapshot {
|
||||
fn new(tab_snapshot: TabSnapshot) -> Self {
|
||||
fn new(char_snapshot: CharSnapshot) -> Self {
|
||||
let mut transforms = SumTree::default();
|
||||
let extent = tab_snapshot.text_summary();
|
||||
let extent = char_snapshot.text_summary();
|
||||
if !extent.lines.is_zero() {
|
||||
transforms.push(Transform::isomorphic(extent), &());
|
||||
}
|
||||
Self {
|
||||
transforms,
|
||||
tab_snapshot,
|
||||
char_snapshot,
|
||||
interpolated: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn buffer_snapshot(&self) -> &MultiBufferSnapshot {
|
||||
self.tab_snapshot.buffer_snapshot()
|
||||
self.char_snapshot.buffer_snapshot()
|
||||
}
|
||||
|
||||
fn interpolate(&mut self, new_tab_snapshot: TabSnapshot, tab_edits: &[TabEdit]) -> Patch<u32> {
|
||||
fn interpolate(
|
||||
&mut self,
|
||||
new_char_snapshot: CharSnapshot,
|
||||
tab_edits: &[TabEdit],
|
||||
) -> Patch<u32> {
|
||||
let mut new_transforms;
|
||||
if tab_edits.is_empty() {
|
||||
new_transforms = self.transforms.clone();
|
||||
} else {
|
||||
let mut old_cursor = self.transforms.cursor::<TabPoint>(&());
|
||||
let mut old_cursor = self.transforms.cursor::<CharPoint>(&());
|
||||
|
||||
let mut tab_edits_iter = tab_edits.iter().peekable();
|
||||
new_transforms =
|
||||
old_cursor.slice(&tab_edits_iter.peek().unwrap().old.start, Bias::Right, &());
|
||||
|
||||
while let Some(edit) = tab_edits_iter.next() {
|
||||
if edit.new.start > TabPoint::from(new_transforms.summary().input.lines) {
|
||||
let summary = new_tab_snapshot.text_summary_for_range(
|
||||
TabPoint::from(new_transforms.summary().input.lines)..edit.new.start,
|
||||
if edit.new.start > CharPoint::from(new_transforms.summary().input.lines) {
|
||||
let summary = new_char_snapshot.text_summary_for_range(
|
||||
CharPoint::from(new_transforms.summary().input.lines)..edit.new.start,
|
||||
);
|
||||
new_transforms.push_or_extend(Transform::isomorphic(summary));
|
||||
}
|
||||
|
||||
if !edit.new.is_empty() {
|
||||
new_transforms.push_or_extend(Transform::isomorphic(
|
||||
new_tab_snapshot.text_summary_for_range(edit.new.clone()),
|
||||
new_char_snapshot.text_summary_for_range(edit.new.clone()),
|
||||
));
|
||||
}
|
||||
|
||||
@@ -349,7 +369,7 @@ impl WrapSnapshot {
|
||||
if next_edit.old.start > old_cursor.end(&()) {
|
||||
if old_cursor.end(&()) > edit.old.end {
|
||||
let summary = self
|
||||
.tab_snapshot
|
||||
.char_snapshot
|
||||
.text_summary_for_range(edit.old.end..old_cursor.end(&()));
|
||||
new_transforms.push_or_extend(Transform::isomorphic(summary));
|
||||
}
|
||||
@@ -363,7 +383,7 @@ impl WrapSnapshot {
|
||||
} else {
|
||||
if old_cursor.end(&()) > edit.old.end {
|
||||
let summary = self
|
||||
.tab_snapshot
|
||||
.char_snapshot
|
||||
.text_summary_for_range(edit.old.end..old_cursor.end(&()));
|
||||
new_transforms.push_or_extend(Transform::isomorphic(summary));
|
||||
}
|
||||
@@ -376,7 +396,7 @@ impl WrapSnapshot {
|
||||
let old_snapshot = mem::replace(
|
||||
self,
|
||||
WrapSnapshot {
|
||||
tab_snapshot: new_tab_snapshot,
|
||||
char_snapshot: new_char_snapshot,
|
||||
transforms: new_transforms,
|
||||
interpolated: true,
|
||||
},
|
||||
@@ -387,7 +407,7 @@ impl WrapSnapshot {
|
||||
|
||||
async fn update(
|
||||
&mut self,
|
||||
new_tab_snapshot: TabSnapshot,
|
||||
new_char_snapshot: CharSnapshot,
|
||||
tab_edits: &[TabEdit],
|
||||
wrap_width: Pixels,
|
||||
line_wrapper: &mut LineWrapper,
|
||||
@@ -424,27 +444,27 @@ impl WrapSnapshot {
|
||||
new_transforms = self.transforms.clone();
|
||||
} else {
|
||||
let mut row_edits = row_edits.into_iter().peekable();
|
||||
let mut old_cursor = self.transforms.cursor::<TabPoint>(&());
|
||||
let mut old_cursor = self.transforms.cursor::<CharPoint>(&());
|
||||
|
||||
new_transforms = old_cursor.slice(
|
||||
&TabPoint::new(row_edits.peek().unwrap().old_rows.start, 0),
|
||||
&CharPoint::new(row_edits.peek().unwrap().old_rows.start, 0),
|
||||
Bias::Right,
|
||||
&(),
|
||||
);
|
||||
|
||||
while let Some(edit) = row_edits.next() {
|
||||
if edit.new_rows.start > new_transforms.summary().input.lines.row {
|
||||
let summary = new_tab_snapshot.text_summary_for_range(
|
||||
TabPoint(new_transforms.summary().input.lines)
|
||||
..TabPoint::new(edit.new_rows.start, 0),
|
||||
let summary = new_char_snapshot.text_summary_for_range(
|
||||
CharPoint(new_transforms.summary().input.lines)
|
||||
..CharPoint::new(edit.new_rows.start, 0),
|
||||
);
|
||||
new_transforms.push_or_extend(Transform::isomorphic(summary));
|
||||
}
|
||||
|
||||
let mut line = String::new();
|
||||
let mut remaining = None;
|
||||
let mut chunks = new_tab_snapshot.chunks(
|
||||
TabPoint::new(edit.new_rows.start, 0)..new_tab_snapshot.max_point(),
|
||||
let mut chunks = new_char_snapshot.chunks(
|
||||
CharPoint::new(edit.new_rows.start, 0)..new_char_snapshot.max_point(),
|
||||
false,
|
||||
Highlights::default(),
|
||||
);
|
||||
@@ -491,19 +511,19 @@ impl WrapSnapshot {
|
||||
}
|
||||
new_transforms.extend(edit_transforms, &());
|
||||
|
||||
old_cursor.seek_forward(&TabPoint::new(edit.old_rows.end, 0), Bias::Right, &());
|
||||
old_cursor.seek_forward(&CharPoint::new(edit.old_rows.end, 0), Bias::Right, &());
|
||||
if let Some(next_edit) = row_edits.peek() {
|
||||
if next_edit.old_rows.start > old_cursor.end(&()).row() {
|
||||
if old_cursor.end(&()) > TabPoint::new(edit.old_rows.end, 0) {
|
||||
let summary = self.tab_snapshot.text_summary_for_range(
|
||||
TabPoint::new(edit.old_rows.end, 0)..old_cursor.end(&()),
|
||||
if old_cursor.end(&()) > CharPoint::new(edit.old_rows.end, 0) {
|
||||
let summary = self.char_snapshot.text_summary_for_range(
|
||||
CharPoint::new(edit.old_rows.end, 0)..old_cursor.end(&()),
|
||||
);
|
||||
new_transforms.push_or_extend(Transform::isomorphic(summary));
|
||||
}
|
||||
old_cursor.next(&());
|
||||
new_transforms.append(
|
||||
old_cursor.slice(
|
||||
&TabPoint::new(next_edit.old_rows.start, 0),
|
||||
&CharPoint::new(next_edit.old_rows.start, 0),
|
||||
Bias::Right,
|
||||
&(),
|
||||
),
|
||||
@@ -511,9 +531,9 @@ impl WrapSnapshot {
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if old_cursor.end(&()) > TabPoint::new(edit.old_rows.end, 0) {
|
||||
let summary = self.tab_snapshot.text_summary_for_range(
|
||||
TabPoint::new(edit.old_rows.end, 0)..old_cursor.end(&()),
|
||||
if old_cursor.end(&()) > CharPoint::new(edit.old_rows.end, 0) {
|
||||
let summary = self.char_snapshot.text_summary_for_range(
|
||||
CharPoint::new(edit.old_rows.end, 0)..old_cursor.end(&()),
|
||||
);
|
||||
new_transforms.push_or_extend(Transform::isomorphic(summary));
|
||||
}
|
||||
@@ -526,7 +546,7 @@ impl WrapSnapshot {
|
||||
let old_snapshot = mem::replace(
|
||||
self,
|
||||
WrapSnapshot {
|
||||
tab_snapshot: new_tab_snapshot,
|
||||
char_snapshot: new_char_snapshot,
|
||||
transforms: new_transforms,
|
||||
interpolated: false,
|
||||
},
|
||||
@@ -579,17 +599,17 @@ impl WrapSnapshot {
|
||||
) -> WrapChunks<'a> {
|
||||
let output_start = WrapPoint::new(rows.start, 0);
|
||||
let output_end = WrapPoint::new(rows.end, 0);
|
||||
let mut transforms = self.transforms.cursor::<(WrapPoint, TabPoint)>(&());
|
||||
let mut transforms = self.transforms.cursor::<(WrapPoint, CharPoint)>(&());
|
||||
transforms.seek(&output_start, Bias::Right, &());
|
||||
let mut input_start = TabPoint(transforms.start().1 .0);
|
||||
let mut input_start = CharPoint(transforms.start().1 .0);
|
||||
if transforms.item().map_or(false, |t| t.is_isomorphic()) {
|
||||
input_start.0 += output_start.0 - transforms.start().0 .0;
|
||||
}
|
||||
let input_end = self
|
||||
.to_tab_point(output_end)
|
||||
.min(self.tab_snapshot.max_point());
|
||||
.to_char_point(output_end)
|
||||
.min(self.char_snapshot.max_point());
|
||||
WrapChunks {
|
||||
input_chunks: self.tab_snapshot.chunks(
|
||||
input_chunks: self.char_snapshot.chunks(
|
||||
input_start..input_end,
|
||||
language_aware,
|
||||
highlights,
|
||||
@@ -598,6 +618,7 @@ impl WrapSnapshot {
|
||||
output_position: output_start,
|
||||
max_output_row: rows.end,
|
||||
transforms,
|
||||
snapshot: self,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -606,7 +627,7 @@ impl WrapSnapshot {
|
||||
}
|
||||
|
||||
pub fn line_len(&self, row: u32) -> u32 {
|
||||
let mut cursor = self.transforms.cursor::<(WrapPoint, TabPoint)>(&());
|
||||
let mut cursor = self.transforms.cursor::<(WrapPoint, CharPoint)>(&());
|
||||
cursor.seek(&WrapPoint::new(row + 1, 0), Bias::Left, &());
|
||||
if cursor
|
||||
.item()
|
||||
@@ -614,7 +635,7 @@ impl WrapSnapshot {
|
||||
{
|
||||
let overshoot = row - cursor.start().0.row();
|
||||
let tab_row = cursor.start().1.row() + overshoot;
|
||||
let tab_line_len = self.tab_snapshot.line_len(tab_row);
|
||||
let tab_line_len = self.char_snapshot.line_len(tab_row);
|
||||
if overshoot == 0 {
|
||||
cursor.start().0.column() + (tab_line_len - cursor.start().1.column())
|
||||
} else {
|
||||
@@ -625,6 +646,67 @@ impl WrapSnapshot {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn text_summary_for_range(&self, rows: Range<u32>) -> TextSummary {
|
||||
let mut summary = TextSummary::default();
|
||||
|
||||
let start = WrapPoint::new(rows.start, 0);
|
||||
let end = WrapPoint::new(rows.end, 0);
|
||||
|
||||
let mut cursor = self.transforms.cursor::<(WrapPoint, CharPoint)>(&());
|
||||
cursor.seek(&start, Bias::Right, &());
|
||||
if let Some(transform) = cursor.item() {
|
||||
let start_in_transform = start.0 - cursor.start().0 .0;
|
||||
let end_in_transform = cmp::min(end, cursor.end(&()).0).0 - cursor.start().0 .0;
|
||||
if transform.is_isomorphic() {
|
||||
let char_start = CharPoint(cursor.start().1 .0 + start_in_transform);
|
||||
let char_end = CharPoint(cursor.start().1 .0 + end_in_transform);
|
||||
summary += &self
|
||||
.char_snapshot
|
||||
.text_summary_for_range(char_start..char_end);
|
||||
} else {
|
||||
debug_assert_eq!(start_in_transform.row, end_in_transform.row);
|
||||
let indent_len = end_in_transform.column - start_in_transform.column;
|
||||
summary += &TextSummary {
|
||||
lines: Point::new(0, indent_len),
|
||||
first_line_chars: indent_len,
|
||||
last_line_chars: indent_len,
|
||||
longest_row: 0,
|
||||
longest_row_chars: indent_len,
|
||||
};
|
||||
}
|
||||
|
||||
cursor.next(&());
|
||||
}
|
||||
|
||||
if rows.end > cursor.start().0.row() {
|
||||
summary += &cursor
|
||||
.summary::<_, TransformSummary>(&WrapPoint::new(rows.end, 0), Bias::Right, &())
|
||||
.output;
|
||||
|
||||
if let Some(transform) = cursor.item() {
|
||||
let end_in_transform = end.0 - cursor.start().0 .0;
|
||||
if transform.is_isomorphic() {
|
||||
let char_start = cursor.start().1;
|
||||
let char_end = CharPoint(char_start.0 + end_in_transform);
|
||||
summary += &self
|
||||
.char_snapshot
|
||||
.text_summary_for_range(char_start..char_end);
|
||||
} else {
|
||||
debug_assert_eq!(end_in_transform, Point::new(1, 0));
|
||||
summary += &TextSummary {
|
||||
lines: Point::new(1, 0),
|
||||
first_line_chars: 0,
|
||||
last_line_chars: 0,
|
||||
longest_row: 0,
|
||||
longest_row_chars: 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
summary
|
||||
}
|
||||
|
||||
pub fn soft_wrap_indent(&self, row: u32) -> Option<u32> {
|
||||
let mut cursor = self.transforms.cursor::<WrapPoint>(&());
|
||||
cursor.seek(&WrapPoint::new(row + 1, 0), Bias::Right, &());
|
||||
@@ -642,14 +724,14 @@ impl WrapSnapshot {
|
||||
}
|
||||
|
||||
pub fn buffer_rows(&self, start_row: u32) -> WrapBufferRows {
|
||||
let mut transforms = self.transforms.cursor::<(WrapPoint, TabPoint)>(&());
|
||||
let mut transforms = self.transforms.cursor::<(WrapPoint, CharPoint)>(&());
|
||||
transforms.seek(&WrapPoint::new(start_row, 0), Bias::Left, &());
|
||||
let mut input_row = transforms.start().1.row();
|
||||
if transforms.item().map_or(false, |t| t.is_isomorphic()) {
|
||||
input_row += start_row - transforms.start().0.row();
|
||||
}
|
||||
let soft_wrapped = transforms.item().map_or(false, |t| !t.is_isomorphic());
|
||||
let mut input_buffer_rows = self.tab_snapshot.buffer_rows(input_row);
|
||||
let mut input_buffer_rows = self.char_snapshot.buffer_rows(input_row);
|
||||
let input_buffer_row = input_buffer_rows.next().unwrap();
|
||||
WrapBufferRows {
|
||||
transforms,
|
||||
@@ -661,26 +743,26 @@ impl WrapSnapshot {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_tab_point(&self, point: WrapPoint) -> TabPoint {
|
||||
let mut cursor = self.transforms.cursor::<(WrapPoint, TabPoint)>(&());
|
||||
pub fn to_char_point(&self, point: WrapPoint) -> CharPoint {
|
||||
let mut cursor = self.transforms.cursor::<(WrapPoint, CharPoint)>(&());
|
||||
cursor.seek(&point, Bias::Right, &());
|
||||
let mut tab_point = cursor.start().1 .0;
|
||||
let mut char_point = cursor.start().1 .0;
|
||||
if cursor.item().map_or(false, |t| t.is_isomorphic()) {
|
||||
tab_point += point.0 - cursor.start().0 .0;
|
||||
char_point += point.0 - cursor.start().0 .0;
|
||||
}
|
||||
TabPoint(tab_point)
|
||||
CharPoint(char_point)
|
||||
}
|
||||
|
||||
pub fn to_point(&self, point: WrapPoint, bias: Bias) -> Point {
|
||||
self.tab_snapshot.to_point(self.to_tab_point(point), bias)
|
||||
self.char_snapshot.to_point(self.to_char_point(point), bias)
|
||||
}
|
||||
|
||||
pub fn make_wrap_point(&self, point: Point, bias: Bias) -> WrapPoint {
|
||||
self.tab_point_to_wrap_point(self.tab_snapshot.make_tab_point(point, bias))
|
||||
self.char_point_to_wrap_point(self.char_snapshot.make_char_point(point, bias))
|
||||
}
|
||||
|
||||
pub fn tab_point_to_wrap_point(&self, point: TabPoint) -> WrapPoint {
|
||||
let mut cursor = self.transforms.cursor::<(TabPoint, WrapPoint)>(&());
|
||||
pub fn char_point_to_wrap_point(&self, point: CharPoint) -> WrapPoint {
|
||||
let mut cursor = self.transforms.cursor::<(CharPoint, WrapPoint)>(&());
|
||||
cursor.seek(&point, Bias::Right, &());
|
||||
WrapPoint(cursor.start().1 .0 + (point.0 - cursor.start().0 .0))
|
||||
}
|
||||
@@ -695,7 +777,10 @@ impl WrapSnapshot {
|
||||
}
|
||||
}
|
||||
|
||||
self.tab_point_to_wrap_point(self.tab_snapshot.clip_point(self.to_tab_point(point), bias))
|
||||
self.char_point_to_wrap_point(
|
||||
self.char_snapshot
|
||||
.clip_point(self.to_char_point(point), bias),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn prev_row_boundary(&self, mut point: WrapPoint) -> u32 {
|
||||
@@ -705,7 +790,7 @@ impl WrapSnapshot {
|
||||
|
||||
*point.column_mut() = 0;
|
||||
|
||||
let mut cursor = self.transforms.cursor::<(WrapPoint, TabPoint)>(&());
|
||||
let mut cursor = self.transforms.cursor::<(WrapPoint, CharPoint)>(&());
|
||||
cursor.seek(&point, Bias::Right, &());
|
||||
if cursor.item().is_none() {
|
||||
cursor.prev(&());
|
||||
@@ -725,7 +810,7 @@ impl WrapSnapshot {
|
||||
pub fn next_row_boundary(&self, mut point: WrapPoint) -> Option<u32> {
|
||||
point.0 += Point::new(1, 0);
|
||||
|
||||
let mut cursor = self.transforms.cursor::<(WrapPoint, TabPoint)>(&());
|
||||
let mut cursor = self.transforms.cursor::<(WrapPoint, CharPoint)>(&());
|
||||
cursor.seek(&point, Bias::Right, &());
|
||||
while let Some(transform) = cursor.item() {
|
||||
if transform.is_isomorphic() && cursor.start().1.column() == 0 {
|
||||
@@ -738,12 +823,27 @@ impl WrapSnapshot {
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn text(&self) -> String {
|
||||
self.text_chunks(0).collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn text_chunks(&self, wrap_row: u32) -> impl Iterator<Item = &str> {
|
||||
self.chunks(
|
||||
wrap_row..self.max_point().row() + 1,
|
||||
false,
|
||||
Highlights::default(),
|
||||
)
|
||||
.map(|h| h.text)
|
||||
}
|
||||
|
||||
fn check_invariants(&self) {
|
||||
#[cfg(test)]
|
||||
{
|
||||
assert_eq!(
|
||||
TabPoint::from(self.transforms.summary().input.lines),
|
||||
self.tab_snapshot.max_point()
|
||||
CharPoint::from(self.transforms.summary().input.lines),
|
||||
self.char_snapshot.max_point()
|
||||
);
|
||||
|
||||
{
|
||||
@@ -756,18 +856,18 @@ impl WrapSnapshot {
|
||||
}
|
||||
|
||||
let text = language::Rope::from(self.text().as_str());
|
||||
let mut input_buffer_rows = self.tab_snapshot.buffer_rows(0);
|
||||
let mut input_buffer_rows = self.char_snapshot.buffer_rows(0);
|
||||
let mut expected_buffer_rows = Vec::new();
|
||||
let mut prev_tab_row = 0;
|
||||
for display_row in 0..=self.max_point().row() {
|
||||
let tab_point = self.to_tab_point(WrapPoint::new(display_row, 0));
|
||||
if tab_point.row() == prev_tab_row && display_row != 0 {
|
||||
let char_point = self.to_char_point(WrapPoint::new(display_row, 0));
|
||||
if char_point.row() == prev_tab_row && display_row != 0 {
|
||||
expected_buffer_rows.push(None);
|
||||
} else {
|
||||
expected_buffer_rows.push(input_buffer_rows.next().unwrap());
|
||||
}
|
||||
|
||||
prev_tab_row = tab_point.row();
|
||||
prev_tab_row = char_point.row();
|
||||
assert_eq!(self.line_len(display_row), text.line_len(display_row));
|
||||
}
|
||||
|
||||
@@ -784,6 +884,26 @@ impl WrapSnapshot {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> WrapChunks<'a> {
|
||||
pub(crate) fn seek(&mut self, rows: Range<u32>) {
|
||||
let output_start = WrapPoint::new(rows.start, 0);
|
||||
let output_end = WrapPoint::new(rows.end, 0);
|
||||
self.transforms.seek(&output_start, Bias::Right, &());
|
||||
let mut input_start = CharPoint(self.transforms.start().1 .0);
|
||||
if self.transforms.item().map_or(false, |t| t.is_isomorphic()) {
|
||||
input_start.0 += output_start.0 - self.transforms.start().0 .0;
|
||||
}
|
||||
let input_end = self
|
||||
.snapshot
|
||||
.to_char_point(output_end)
|
||||
.min(self.snapshot.char_snapshot.max_point());
|
||||
self.input_chunks.seek(input_start..input_end);
|
||||
self.input_chunk = Chunk::default();
|
||||
self.output_position = output_start;
|
||||
self.max_output_row = rows.end;
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for WrapChunks<'a> {
|
||||
type Item = Chunk<'a>;
|
||||
|
||||
@@ -831,13 +951,11 @@ impl<'a> Iterator for WrapChunks<'a> {
|
||||
} else {
|
||||
*self.output_position.column_mut() += char_len as u32;
|
||||
}
|
||||
|
||||
if self.output_position >= transform_end {
|
||||
self.transforms.next(&());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let (prefix, suffix) = self.input_chunk.text.split_at(input_len);
|
||||
self.input_chunk.text = suffix;
|
||||
Some(Chunk {
|
||||
@@ -992,7 +1110,7 @@ impl sum_tree::Summary for TransformSummary {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> sum_tree::Dimension<'a, TransformSummary> for TabPoint {
|
||||
impl<'a> sum_tree::Dimension<'a, TransformSummary> for CharPoint {
|
||||
fn zero(_cx: &()) -> Self {
|
||||
Default::default()
|
||||
}
|
||||
@@ -1002,7 +1120,7 @@ impl<'a> sum_tree::Dimension<'a, TransformSummary> for TabPoint {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> sum_tree::SeekTarget<'a, TransformSummary, TransformSummary> for TabPoint {
|
||||
impl<'a> sum_tree::SeekTarget<'a, TransformSummary, TransformSummary> for CharPoint {
|
||||
fn cmp(&self, cursor_location: &TransformSummary, _: &()) -> std::cmp::Ordering {
|
||||
Ord::cmp(&self.0, &cursor_location.input.lines)
|
||||
}
|
||||
@@ -1050,7 +1168,7 @@ fn consolidate_wrap_edits(edits: Vec<WrapEdit>) -> Vec<WrapEdit> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{
|
||||
display_map::{fold_map::FoldMap, inlay_map::InlayMap, tab_map::TabMap},
|
||||
display_map::{char_map::CharMap, fold_map::FoldMap, inlay_map::InlayMap},
|
||||
MultiBuffer,
|
||||
};
|
||||
use gpui::{font, px, test::observe};
|
||||
@@ -1102,9 +1220,9 @@ mod tests {
|
||||
log::info!("InlayMap text: {:?}", inlay_snapshot.text());
|
||||
let (mut fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot.clone());
|
||||
log::info!("FoldMap text: {:?}", fold_snapshot.text());
|
||||
let (mut tab_map, _) = TabMap::new(fold_snapshot.clone(), tab_size);
|
||||
let tabs_snapshot = tab_map.set_max_expansion_column(32);
|
||||
log::info!("TabMap text: {:?}", tabs_snapshot.text());
|
||||
let (mut char_map, _) = CharMap::new(fold_snapshot.clone(), tab_size);
|
||||
let tabs_snapshot = char_map.set_max_expansion_column(32);
|
||||
log::info!("CharMap text: {:?}", tabs_snapshot.text());
|
||||
|
||||
let mut line_wrapper = text_system.line_wrapper(font.clone(), font_size);
|
||||
let unwrapped_text = tabs_snapshot.text();
|
||||
@@ -1150,7 +1268,7 @@ mod tests {
|
||||
20..=39 => {
|
||||
for (fold_snapshot, fold_edits) in fold_map.randomly_mutate(&mut rng) {
|
||||
let (tabs_snapshot, tab_edits) =
|
||||
tab_map.sync(fold_snapshot, fold_edits, tab_size);
|
||||
char_map.sync(fold_snapshot, fold_edits, tab_size);
|
||||
let (mut snapshot, wrap_edits) =
|
||||
wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, tab_edits, cx));
|
||||
snapshot.check_invariants();
|
||||
@@ -1163,7 +1281,7 @@ mod tests {
|
||||
inlay_map.randomly_mutate(&mut next_inlay_id, &mut rng);
|
||||
let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
|
||||
let (tabs_snapshot, tab_edits) =
|
||||
tab_map.sync(fold_snapshot, fold_edits, tab_size);
|
||||
char_map.sync(fold_snapshot, fold_edits, tab_size);
|
||||
let (mut snapshot, wrap_edits) =
|
||||
wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, tab_edits, cx));
|
||||
snapshot.check_invariants();
|
||||
@@ -1187,8 +1305,8 @@ mod tests {
|
||||
log::info!("InlayMap text: {:?}", inlay_snapshot.text());
|
||||
let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
|
||||
log::info!("FoldMap text: {:?}", fold_snapshot.text());
|
||||
let (tabs_snapshot, tab_edits) = tab_map.sync(fold_snapshot, fold_edits, tab_size);
|
||||
log::info!("TabMap text: {:?}", tabs_snapshot.text());
|
||||
let (tabs_snapshot, tab_edits) = char_map.sync(fold_snapshot, fold_edits, tab_size);
|
||||
log::info!("CharMap text: {:?}", tabs_snapshot.text());
|
||||
|
||||
let unwrapped_text = tabs_snapshot.text();
|
||||
let expected_text = wrap_text(&unwrapped_text, wrap_width, &mut line_wrapper);
|
||||
@@ -1234,7 +1352,7 @@ mod tests {
|
||||
|
||||
if tab_size.get() == 1
|
||||
|| !wrapped_snapshot
|
||||
.tab_snapshot
|
||||
.char_snapshot
|
||||
.fold_snapshot
|
||||
.text()
|
||||
.contains('\t')
|
||||
@@ -1331,19 +1449,6 @@ mod tests {
|
||||
}
|
||||
|
||||
impl WrapSnapshot {
|
||||
pub fn text(&self) -> String {
|
||||
self.text_chunks(0).collect()
|
||||
}
|
||||
|
||||
pub fn text_chunks(&self, wrap_row: u32) -> impl Iterator<Item = &str> {
|
||||
self.chunks(
|
||||
wrap_row..self.max_point().row() + 1,
|
||||
false,
|
||||
Highlights::default(),
|
||||
)
|
||||
.map(|h| h.text)
|
||||
}
|
||||
|
||||
fn verify_chunks(&mut self, rng: &mut impl Rng) {
|
||||
for _ in 0..5 {
|
||||
let mut end_row = rng.gen_range(0..=self.max_point().row());
|
||||
|
||||
@@ -76,9 +76,9 @@ use gpui::{
|
||||
ClipboardItem, Context, DispatchPhase, ElementId, EventEmitter, FocusHandle, FocusOutEvent,
|
||||
FocusableView, FontId, FontWeight, HighlightStyle, Hsla, InteractiveText, KeyContext,
|
||||
ListSizingBehavior, Model, MouseButton, PaintQuad, ParentElement, Pixels, Render, SharedString,
|
||||
Size, StrikethroughStyle, Styled, StyledText, Subscription, Task, TextStyle, UTF16Selection,
|
||||
UnderlineStyle, UniformListScrollHandle, View, ViewContext, ViewInputHandler, VisualContext,
|
||||
WeakFocusHandle, WeakView, WindowContext,
|
||||
Size, StrikethroughStyle, Styled, StyledText, Subscription, Task, TextStyle,
|
||||
TextStyleRefinement, UTF16Selection, UnderlineStyle, UniformListScrollHandle, View,
|
||||
ViewContext, ViewInputHandler, VisualContext, WeakFocusHandle, WeakView, WindowContext,
|
||||
};
|
||||
use highlight_matching_bracket::refresh_matching_bracket_highlights;
|
||||
use hover_popover::{hide_hover, HoverState};
|
||||
@@ -546,6 +546,7 @@ pub struct Editor {
|
||||
ime_transaction: Option<TransactionId>,
|
||||
active_diagnostics: Option<ActiveDiagnosticGroup>,
|
||||
soft_wrap_mode_override: Option<language_settings::SoftWrap>,
|
||||
|
||||
project: Option<Model<Project>>,
|
||||
semantics_provider: Option<Rc<dyn SemanticsProvider>>,
|
||||
completion_provider: Option<Box<dyn CompletionProvider>>,
|
||||
@@ -615,6 +616,7 @@ pub struct Editor {
|
||||
pixel_position_of_newest_cursor: Option<gpui::Point<Pixels>>,
|
||||
gutter_dimensions: GutterDimensions,
|
||||
style: Option<EditorStyle>,
|
||||
text_style_refinement: Option<TextStyleRefinement>,
|
||||
next_editor_action_id: EditorActionId,
|
||||
editor_actions: Rc<RefCell<BTreeMap<EditorActionId, Box<dyn Fn(&mut ViewContext<Self>)>>>>,
|
||||
use_autoclose: bool,
|
||||
@@ -2062,6 +2064,7 @@ impl Editor {
|
||||
next_scroll_position: NextScrollCursorCenterTopBottom::default(),
|
||||
addons: HashMap::default(),
|
||||
_scroll_cursor_center_top_bottom_task: Task::ready(()),
|
||||
text_style_refinement: None,
|
||||
};
|
||||
this.tasks_update_task = Some(this.refresh_runnables(cx));
|
||||
this._subscriptions.extend(project_subscriptions);
|
||||
@@ -6257,28 +6260,6 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_selected_diff_hunks(&mut self, _: &ApplyDiffHunk, cx: &mut ViewContext<Self>) {
|
||||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
||||
let hunks = hunks_for_selections(&snapshot, &self.selections.disjoint_anchors());
|
||||
let mut ranges_by_buffer = HashMap::default();
|
||||
self.transact(cx, |editor, cx| {
|
||||
for hunk in hunks {
|
||||
if let Some(buffer) = editor.buffer.read(cx).buffer(hunk.buffer_id) {
|
||||
ranges_by_buffer
|
||||
.entry(buffer.clone())
|
||||
.or_insert_with(Vec::new)
|
||||
.push(hunk.buffer_range.to_offset(buffer.read(cx)));
|
||||
}
|
||||
}
|
||||
|
||||
for (buffer, ranges) in ranges_by_buffer {
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.merge_into_base(ranges, cx);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn open_active_item_in_terminal(&mut self, _: &OpenInTerminal, cx: &mut ViewContext<Self>) {
|
||||
if let Some(working_directory) = self.active_excerpt(cx).and_then(|(_, buffer, _)| {
|
||||
let project_path = buffer.read(cx).project_path(cx)?;
|
||||
@@ -10229,7 +10210,7 @@ impl Editor {
|
||||
let block_id = this.insert_blocks(
|
||||
[BlockProperties {
|
||||
style: BlockStyle::Flex,
|
||||
position: range.start,
|
||||
placement: BlockPlacement::Below(range.start),
|
||||
height: 1,
|
||||
render: Box::new({
|
||||
let rename_editor = rename_editor.clone();
|
||||
@@ -10265,7 +10246,6 @@ impl Editor {
|
||||
.into_any_element()
|
||||
}
|
||||
}),
|
||||
disposition: BlockDisposition::Below,
|
||||
priority: 0,
|
||||
}],
|
||||
Some(Autoscroll::fit()),
|
||||
@@ -10550,10 +10530,11 @@ impl Editor {
|
||||
let message_height = diagnostic.message.matches('\n').count() as u32 + 1;
|
||||
BlockProperties {
|
||||
style: BlockStyle::Fixed,
|
||||
position: buffer.anchor_after(entry.range.start),
|
||||
placement: BlockPlacement::Below(
|
||||
buffer.anchor_after(entry.range.start),
|
||||
),
|
||||
height: message_height,
|
||||
render: diagnostic_block_renderer(diagnostic, None, true, true),
|
||||
disposition: BlockDisposition::Below,
|
||||
priority: 0,
|
||||
}
|
||||
}),
|
||||
@@ -11180,7 +11161,12 @@ impl Editor {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn set_style(&mut self, style: EditorStyle, cx: &mut ViewContext<Self>) {
|
||||
pub fn set_text_style_refinement(&mut self, style: TextStyleRefinement) {
|
||||
self.text_style_refinement = Some(style);
|
||||
}
|
||||
|
||||
/// called by the Element so we know what style we were most recently rendered with.
|
||||
pub(crate) fn set_style(&mut self, style: EditorStyle, cx: &mut ViewContext<Self>) {
|
||||
let rem_size = cx.rem_size();
|
||||
self.display_map.update(cx, |map, cx| {
|
||||
map.set_font(
|
||||
@@ -13676,7 +13662,7 @@ impl Render for Editor {
|
||||
fn render<'a>(&mut self, cx: &mut ViewContext<'a, Self>) -> impl IntoElement {
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
|
||||
let text_style = match self.mode {
|
||||
let mut text_style = match self.mode {
|
||||
EditorMode::SingleLine { .. } | EditorMode::AutoHeight { .. } => TextStyle {
|
||||
color: cx.theme().colors().editor_foreground,
|
||||
font_family: settings.ui_font.family.clone(),
|
||||
@@ -13698,6 +13684,9 @@ impl Render for Editor {
|
||||
..Default::default()
|
||||
},
|
||||
};
|
||||
if let Some(text_style_refinement) = &self.text_style_refinement {
|
||||
text_style.refine(text_style_refinement)
|
||||
}
|
||||
|
||||
let background = match self.mode {
|
||||
EditorMode::SingleLine { .. } => cx.theme().system().transparent,
|
||||
|
||||
@@ -3868,8 +3868,7 @@ fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
|
||||
editor.insert_blocks(
|
||||
[BlockProperties {
|
||||
style: BlockStyle::Fixed,
|
||||
position: snapshot.anchor_after(Point::new(2, 0)),
|
||||
disposition: BlockDisposition::Below,
|
||||
placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
|
||||
height: 1,
|
||||
render: Box::new(|_| div().into_any()),
|
||||
priority: 0,
|
||||
|
||||
@@ -68,6 +68,7 @@ use sum_tree::Bias;
|
||||
use theme::{ActiveTheme, Appearance, PlayerColor};
|
||||
use ui::prelude::*;
|
||||
use ui::{h_flex, ButtonLike, ButtonStyle, ContextMenu, Tooltip};
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
use util::RangeExt;
|
||||
use util::ResultExt;
|
||||
use workspace::{item::Item, Workspace};
|
||||
@@ -1025,23 +1026,21 @@ impl EditorElement {
|
||||
}
|
||||
let block_text = if let CursorShape::Block = selection.cursor_shape {
|
||||
snapshot
|
||||
.display_chars_at(cursor_position)
|
||||
.next()
|
||||
.grapheme_at(cursor_position)
|
||||
.or_else(|| {
|
||||
if cursor_column == 0 {
|
||||
snapshot
|
||||
.placeholder_text()
|
||||
.and_then(|s| s.chars().next())
|
||||
.map(|c| (c, cursor_position))
|
||||
snapshot.placeholder_text().and_then(|s| {
|
||||
s.graphemes(true).next().map(|s| s.to_owned())
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.and_then(|(character, _)| {
|
||||
let text = if character == '\n' {
|
||||
.and_then(|grapheme| {
|
||||
let text = if grapheme == "\n" {
|
||||
SharedString::from(" ")
|
||||
} else {
|
||||
SharedString::from(character.to_string())
|
||||
SharedString::from(grapheme)
|
||||
};
|
||||
let len = text.len();
|
||||
|
||||
@@ -2072,7 +2071,7 @@ impl EditorElement {
|
||||
let mut element = match block {
|
||||
Block::Custom(block) => {
|
||||
let align_to = block
|
||||
.position()
|
||||
.start()
|
||||
.to_point(&snapshot.buffer_snapshot)
|
||||
.to_display_point(snapshot);
|
||||
let anchor_x = text_x
|
||||
@@ -6295,7 +6294,7 @@ fn compute_auto_height_layout(
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{
|
||||
display_map::{BlockDisposition, BlockProperties},
|
||||
display_map::{BlockPlacement, BlockProperties},
|
||||
editor_tests::{init_test, update_test_language_settings},
|
||||
Editor, MultiBuffer,
|
||||
};
|
||||
@@ -6551,9 +6550,8 @@ mod tests {
|
||||
editor.insert_blocks(
|
||||
[BlockProperties {
|
||||
style: BlockStyle::Fixed,
|
||||
disposition: BlockDisposition::Above,
|
||||
placement: BlockPlacement::Above(Anchor::min()),
|
||||
height: 3,
|
||||
position: Anchor::min(),
|
||||
render: Box::new(|cx| div().h(3. * cx.line_height()).into_any()),
|
||||
priority: 0,
|
||||
}],
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::{
|
||||
display_map::{InlayOffset, ToDisplayPoint},
|
||||
hover_links::{InlayHighlight, RangeInEditor},
|
||||
is_invisible,
|
||||
scroll::ScrollAmount,
|
||||
Anchor, AnchorRangeExt, DisplayPoint, DisplayRow, Editor, EditorSettings, EditorSnapshot,
|
||||
Hover, RangeToAnchorExt,
|
||||
@@ -11,7 +12,7 @@ use gpui::{
|
||||
StyleRefinement, Styled, Task, TextStyleRefinement, View, ViewContext,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use language::{DiagnosticEntry, Language, LanguageRegistry};
|
||||
use language::{Diagnostic, DiagnosticEntry, Language, LanguageRegistry};
|
||||
use lsp::DiagnosticSeverity;
|
||||
use markdown::{Markdown, MarkdownStyle};
|
||||
use multi_buffer::ToOffset;
|
||||
@@ -199,7 +200,6 @@ fn show_hover(
|
||||
if editor.pending_rename.is_some() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let snapshot = editor.snapshot(cx);
|
||||
|
||||
let (buffer, buffer_position) = editor
|
||||
@@ -259,7 +259,7 @@ fn show_hover(
|
||||
}
|
||||
|
||||
// If there's a diagnostic, assign it on the hover state and notify
|
||||
let local_diagnostic = snapshot
|
||||
let mut local_diagnostic = snapshot
|
||||
.buffer_snapshot
|
||||
.diagnostics_in_range::<_, usize>(anchor..anchor, false)
|
||||
// Find the entry with the most specific range
|
||||
@@ -281,6 +281,42 @@ fn show_hover(
|
||||
})
|
||||
});
|
||||
|
||||
if let Some(invisible) = snapshot
|
||||
.buffer_snapshot
|
||||
.chars_at(anchor)
|
||||
.next()
|
||||
.filter(|&c| is_invisible(c))
|
||||
{
|
||||
let after = snapshot.buffer_snapshot.anchor_after(
|
||||
anchor.to_offset(&snapshot.buffer_snapshot) + invisible.len_utf8(),
|
||||
);
|
||||
local_diagnostic = Some(DiagnosticEntry {
|
||||
diagnostic: Diagnostic {
|
||||
severity: DiagnosticSeverity::HINT,
|
||||
message: format!("Unicode character U+{:02X}", invisible as u32),
|
||||
..Default::default()
|
||||
},
|
||||
range: anchor..after,
|
||||
})
|
||||
} else if let Some(invisible) = snapshot
|
||||
.buffer_snapshot
|
||||
.reversed_chars_at(anchor)
|
||||
.next()
|
||||
.filter(|&c| is_invisible(c))
|
||||
{
|
||||
let before = snapshot.buffer_snapshot.anchor_before(
|
||||
anchor.to_offset(&snapshot.buffer_snapshot) - invisible.len_utf8(),
|
||||
);
|
||||
local_diagnostic = Some(DiagnosticEntry {
|
||||
diagnostic: Diagnostic {
|
||||
severity: DiagnosticSeverity::HINT,
|
||||
message: format!("Unicode character U+{:02X}", invisible as u32),
|
||||
..Default::default()
|
||||
},
|
||||
range: before..anchor,
|
||||
})
|
||||
}
|
||||
|
||||
let diagnostic_popover = if let Some(local_diagnostic) = local_diagnostic {
|
||||
let text = match local_diagnostic.diagnostic.source {
|
||||
Some(ref source) => {
|
||||
@@ -288,7 +324,6 @@ fn show_hover(
|
||||
}
|
||||
None => local_diagnostic.diagnostic.message.clone(),
|
||||
};
|
||||
|
||||
let mut border_color: Option<Hsla> = None;
|
||||
let mut background_color: Option<Hsla> = None;
|
||||
|
||||
@@ -344,7 +379,6 @@ fn show_hover(
|
||||
Markdown::new_text(text, markdown_style.clone(), None, cx, None)
|
||||
})
|
||||
.ok();
|
||||
|
||||
Some(DiagnosticPopover {
|
||||
local_diagnostic,
|
||||
primary_diagnostic,
|
||||
@@ -432,7 +466,6 @@ fn show_hover(
|
||||
cx.notify();
|
||||
cx.refresh();
|
||||
})?;
|
||||
|
||||
anyhow::Ok(())
|
||||
}
|
||||
.log_err()
|
||||
|
||||
@@ -7,15 +7,17 @@ use multi_buffer::{
|
||||
MultiBufferSnapshot, ToPoint,
|
||||
};
|
||||
use std::{ops::Range, sync::Arc};
|
||||
use text::OffsetRangeExt;
|
||||
use ui::{
|
||||
prelude::*, ActiveTheme, ContextMenu, IconButtonShape, InteractiveElement, IntoElement,
|
||||
ParentElement, PopoverMenu, Styled, Tooltip, ViewContext, VisualContext,
|
||||
};
|
||||
use util::RangeExt;
|
||||
use workspace::Item;
|
||||
|
||||
use crate::{
|
||||
editor_settings::CurrentLineHighlight, hunk_status, hunks_for_selections, ApplyDiffHunk,
|
||||
BlockDisposition, BlockProperties, BlockStyle, CustomBlockId, DiffRowHighlight, DisplayRow,
|
||||
BlockPlacement, BlockProperties, BlockStyle, CustomBlockId, DiffRowHighlight, DisplayRow,
|
||||
DisplaySnapshot, Editor, EditorElement, ExpandAllHunkDiffs, GoToHunk, GoToPrevHunk, RevertFile,
|
||||
RevertSelectedHunks, ToDisplayPoint, ToggleHunkDiff,
|
||||
};
|
||||
@@ -327,7 +329,7 @@ impl Editor {
|
||||
Some(())
|
||||
}
|
||||
|
||||
fn apply_changes_in_range(
|
||||
fn apply_diff_hunks_in_range(
|
||||
&mut self,
|
||||
range: Range<Anchor>,
|
||||
cx: &mut ViewContext<'_, Editor>,
|
||||
@@ -343,16 +345,54 @@ impl Editor {
|
||||
branch_buffer.merge_into_base(vec![range], cx);
|
||||
});
|
||||
|
||||
if let Some(project) = self.project.clone() {
|
||||
self.save(true, project, cx).detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub(crate) fn apply_all_changes(&self, cx: &mut ViewContext<Self>) {
|
||||
pub(crate) fn apply_all_diff_hunks(&mut self, cx: &mut ViewContext<Self>) {
|
||||
let buffers = self.buffer.read(cx).all_buffers();
|
||||
for branch_buffer in buffers {
|
||||
branch_buffer.update(cx, |branch_buffer, cx| {
|
||||
branch_buffer.merge_into_base(Vec::new(), cx);
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(project) = self.project.clone() {
|
||||
self.save(true, project, cx).detach_and_log_err(cx);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn apply_selected_diff_hunks(
|
||||
&mut self,
|
||||
_: &ApplyDiffHunk,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
||||
let hunks = hunks_for_selections(&snapshot, &self.selections.disjoint_anchors());
|
||||
let mut ranges_by_buffer = HashMap::default();
|
||||
self.transact(cx, |editor, cx| {
|
||||
for hunk in hunks {
|
||||
if let Some(buffer) = editor.buffer.read(cx).buffer(hunk.buffer_id) {
|
||||
ranges_by_buffer
|
||||
.entry(buffer.clone())
|
||||
.or_insert_with(Vec::new)
|
||||
.push(hunk.buffer_range.to_offset(buffer.read(cx)));
|
||||
}
|
||||
}
|
||||
|
||||
for (buffer, ranges) in ranges_by_buffer {
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.merge_into_base(ranges, cx);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(project) = self.project.clone() {
|
||||
self.save(true, project, cx).detach_and_log_err(cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn hunk_header_block(
|
||||
@@ -377,10 +417,9 @@ impl Editor {
|
||||
};
|
||||
|
||||
BlockProperties {
|
||||
position: hunk.multi_buffer_range.start,
|
||||
placement: BlockPlacement::Above(hunk.multi_buffer_range.start),
|
||||
height: 1,
|
||||
style: BlockStyle::Sticky,
|
||||
disposition: BlockDisposition::Above,
|
||||
priority: 0,
|
||||
render: Box::new({
|
||||
let editor = cx.view().clone();
|
||||
@@ -418,7 +457,7 @@ impl Editor {
|
||||
h_flex()
|
||||
.px_6()
|
||||
.size_full()
|
||||
.justify_between()
|
||||
.justify_end()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
@@ -548,11 +587,12 @@ impl Editor {
|
||||
let hunk = hunk.clone();
|
||||
move |_event, cx| {
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.apply_changes_in_range(
|
||||
hunk.multi_buffer_range
|
||||
.clone(),
|
||||
cx,
|
||||
);
|
||||
editor
|
||||
.apply_diff_hunks_in_range(
|
||||
hunk.multi_buffer_range
|
||||
.clone(),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
}),
|
||||
@@ -659,10 +699,9 @@ impl Editor {
|
||||
let hunk = hunk.clone();
|
||||
let height = editor_height.max(deleted_text_height);
|
||||
BlockProperties {
|
||||
position: hunk.multi_buffer_range.start,
|
||||
placement: BlockPlacement::Above(hunk.multi_buffer_range.start),
|
||||
height,
|
||||
style: BlockStyle::Flex,
|
||||
disposition: BlockDisposition::Above,
|
||||
priority: 0,
|
||||
render: Box::new(move |cx| {
|
||||
let width = EditorElement::diff_hunk_strip_width(cx.line_height());
|
||||
|
||||
@@ -720,6 +720,10 @@ impl Item for Editor {
|
||||
) -> Task<Result<()>> {
|
||||
self.report_editor_event("save", None, cx);
|
||||
let buffers = self.buffer().clone().read(cx).all_buffers();
|
||||
let buffers = buffers
|
||||
.into_iter()
|
||||
.map(|handle| handle.read(cx).diff_base_buffer().unwrap_or(handle.clone()))
|
||||
.collect::<HashSet<_>>();
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
if format {
|
||||
this.update(&mut cx, |editor, cx| {
|
||||
|
||||
@@ -298,6 +298,20 @@ impl Item for ProposedChangesEditor {
|
||||
Item::set_nav_history(editor, nav_history, cx)
|
||||
});
|
||||
}
|
||||
|
||||
fn can_save(&self, cx: &AppContext) -> bool {
|
||||
self.editor.read(cx).can_save(cx)
|
||||
}
|
||||
|
||||
fn save(
|
||||
&mut self,
|
||||
format: bool,
|
||||
project: Model<Project>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Task<gpui::Result<()>> {
|
||||
self.editor
|
||||
.update(cx, |editor, cx| Item::save(editor, format, project, cx))
|
||||
}
|
||||
}
|
||||
|
||||
impl ProposedChangesEditorToolbar {
|
||||
@@ -323,7 +337,7 @@ impl Render for ProposedChangesEditorToolbar {
|
||||
if let Some(editor) = &editor {
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.editor.update(cx, |editor, cx| {
|
||||
editor.apply_all_changes(cx);
|
||||
editor.apply_all_diff_hunks(cx);
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
@@ -146,7 +146,7 @@ impl Keystroke {
|
||||
"space" => Some(" ".into()),
|
||||
"tab" => Some("\t".into()),
|
||||
"enter" => Some("\n".into()),
|
||||
key if !is_printable_key(key) => None,
|
||||
key if !is_printable_key(key) || key.is_empty() => None,
|
||||
key => {
|
||||
if self.modifiers.shift {
|
||||
Some(key.to_uppercase())
|
||||
|
||||
@@ -381,6 +381,11 @@ impl MacPlatform {
|
||||
}
|
||||
item.setSubmenu_(submenu);
|
||||
item.setTitle_(ns_string(&name));
|
||||
if name == "Services" {
|
||||
let app: id = msg_send![APP_CLASS, sharedApplication];
|
||||
app.setServicesMenu_(item);
|
||||
}
|
||||
|
||||
item
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::{
|
||||
black, fill, point, px, size, Bounds, Hsla, LineLayout, Pixels, Point, Result, SharedString,
|
||||
StrikethroughStyle, UnderlineStyle, WindowContext, WrapBoundary, WrappedLineLayout,
|
||||
black, fill, point, px, size, Bounds, Half, Hsla, LineLayout, Pixels, Point, Result,
|
||||
SharedString, StrikethroughStyle, UnderlineStyle, WindowContext, WrapBoundary,
|
||||
WrappedLineLayout,
|
||||
};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use smallvec::SmallVec;
|
||||
@@ -129,8 +130,9 @@ fn paint_line(
|
||||
let text_system = cx.text_system().clone();
|
||||
let mut glyph_origin = origin;
|
||||
let mut prev_glyph_position = Point::default();
|
||||
let mut max_glyph_size = size(px(0.), px(0.));
|
||||
for (run_ix, run) in layout.runs.iter().enumerate() {
|
||||
let max_glyph_size = text_system.bounding_box(run.font_id, layout.font_size).size;
|
||||
max_glyph_size = text_system.bounding_box(run.font_id, layout.font_size).size;
|
||||
|
||||
for (glyph_ix, glyph) in run.glyphs.iter().enumerate() {
|
||||
glyph_origin.x += glyph.position.x - prev_glyph_position.x;
|
||||
@@ -139,6 +141,9 @@ fn paint_line(
|
||||
wraps.next();
|
||||
if let Some((background_origin, background_color)) = current_background.as_mut()
|
||||
{
|
||||
if glyph_origin.x == background_origin.x {
|
||||
background_origin.x -= max_glyph_size.width.half()
|
||||
}
|
||||
cx.paint_quad(fill(
|
||||
Bounds {
|
||||
origin: *background_origin,
|
||||
@@ -150,6 +155,9 @@ fn paint_line(
|
||||
background_origin.y += line_height;
|
||||
}
|
||||
if let Some((underline_origin, underline_style)) = current_underline.as_mut() {
|
||||
if glyph_origin.x == underline_origin.x {
|
||||
underline_origin.x -= max_glyph_size.width.half();
|
||||
};
|
||||
cx.paint_underline(
|
||||
*underline_origin,
|
||||
glyph_origin.x - underline_origin.x,
|
||||
@@ -161,6 +169,9 @@ fn paint_line(
|
||||
if let Some((strikethrough_origin, strikethrough_style)) =
|
||||
current_strikethrough.as_mut()
|
||||
{
|
||||
if glyph_origin.x == strikethrough_origin.x {
|
||||
strikethrough_origin.x -= max_glyph_size.width.half();
|
||||
};
|
||||
cx.paint_strikethrough(
|
||||
*strikethrough_origin,
|
||||
glyph_origin.x - strikethrough_origin.x,
|
||||
@@ -179,7 +190,18 @@ fn paint_line(
|
||||
let mut finished_underline: Option<(Point<Pixels>, UnderlineStyle)> = None;
|
||||
let mut finished_strikethrough: Option<(Point<Pixels>, StrikethroughStyle)> = None;
|
||||
if glyph.index >= run_end {
|
||||
if let Some(style_run) = decoration_runs.next() {
|
||||
let mut style_run = decoration_runs.next();
|
||||
|
||||
// ignore style runs that apply to a partial glyph
|
||||
while let Some(run) = style_run {
|
||||
if glyph.index < run_end + (run.len as usize) {
|
||||
break;
|
||||
}
|
||||
run_end += run.len as usize;
|
||||
style_run = decoration_runs.next();
|
||||
}
|
||||
|
||||
if let Some(style_run) = style_run {
|
||||
if let Some((_, background_color)) = &mut current_background {
|
||||
if style_run.background_color.as_ref() != Some(background_color) {
|
||||
finished_background = current_background.take();
|
||||
@@ -240,10 +262,14 @@ fn paint_line(
|
||||
}
|
||||
|
||||
if let Some((background_origin, background_color)) = finished_background {
|
||||
let mut width = glyph_origin.x - background_origin.x;
|
||||
if width == px(0.) {
|
||||
width = px(5.)
|
||||
};
|
||||
cx.paint_quad(fill(
|
||||
Bounds {
|
||||
origin: background_origin,
|
||||
size: size(glyph_origin.x - background_origin.x, line_height),
|
||||
size: size(width, line_height),
|
||||
},
|
||||
background_color,
|
||||
));
|
||||
@@ -299,7 +325,10 @@ fn paint_line(
|
||||
last_line_end_x -= glyph.position.x;
|
||||
}
|
||||
|
||||
if let Some((background_origin, background_color)) = current_background.take() {
|
||||
if let Some((mut background_origin, background_color)) = current_background.take() {
|
||||
if last_line_end_x == background_origin.x {
|
||||
background_origin.x -= max_glyph_size.width.half()
|
||||
};
|
||||
cx.paint_quad(fill(
|
||||
Bounds {
|
||||
origin: background_origin,
|
||||
@@ -309,7 +338,10 @@ fn paint_line(
|
||||
));
|
||||
}
|
||||
|
||||
if let Some((underline_start, underline_style)) = current_underline.take() {
|
||||
if let Some((mut underline_start, underline_style)) = current_underline.take() {
|
||||
if last_line_end_x == underline_start.x {
|
||||
underline_start.x -= max_glyph_size.width.half()
|
||||
};
|
||||
cx.paint_underline(
|
||||
underline_start,
|
||||
last_line_end_x - underline_start.x,
|
||||
@@ -317,7 +349,10 @@ fn paint_line(
|
||||
);
|
||||
}
|
||||
|
||||
if let Some((strikethrough_start, strikethrough_style)) = current_strikethrough.take() {
|
||||
if let Some((mut strikethrough_start, strikethrough_style)) = current_strikethrough.take() {
|
||||
if last_line_end_x == strikethrough_start.x {
|
||||
strikethrough_start.x -= max_glyph_size.width.half()
|
||||
};
|
||||
cx.paint_strikethrough(
|
||||
strikethrough_start,
|
||||
last_line_end_x - strikethrough_start.x,
|
||||
|
||||
@@ -71,6 +71,7 @@ env_logger.workspace = true
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
indoc.workspace = true
|
||||
lsp = { workspace = true, features = ["test-support"] }
|
||||
pretty_assertions.workspace = true
|
||||
rand.workspace = true
|
||||
settings = { workspace = true, features = ["test-support"] }
|
||||
text = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -336,6 +336,8 @@ pub enum BufferEvent {
|
||||
FileHandleChanged,
|
||||
/// The buffer was reloaded.
|
||||
Reloaded,
|
||||
/// The buffer is in need of a reload
|
||||
ReloadNeeded,
|
||||
/// The buffer's diff_base changed.
|
||||
DiffBaseChanged,
|
||||
/// Buffer's excerpts for a certain diff base were recalculated.
|
||||
@@ -440,7 +442,7 @@ struct AutoindentRequest {
|
||||
is_block_mode: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Debug, Clone)]
|
||||
struct AutoindentRequestEntry {
|
||||
/// A range of the buffer whose indentation should be adjusted.
|
||||
range: Range<Anchor>,
|
||||
@@ -499,6 +501,8 @@ pub struct Chunk<'a> {
|
||||
pub is_unnecessary: bool,
|
||||
/// Whether this chunk of text was originally a tab character.
|
||||
pub is_tab: bool,
|
||||
/// Whether this chunk of text is an invisible character.
|
||||
pub is_invisible: bool,
|
||||
/// An optional recipe for how the chunk should be presented.
|
||||
pub renderer: Option<ChunkRenderer>,
|
||||
}
|
||||
@@ -1077,7 +1081,7 @@ impl Buffer {
|
||||
file_changed = true;
|
||||
|
||||
if !self.is_dirty() {
|
||||
self.reload(cx).close();
|
||||
cx.emit(BufferEvent::ReloadNeeded);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1418,24 +1422,17 @@ impl Buffer {
|
||||
yield_now().await;
|
||||
}
|
||||
|
||||
// In block mode, only compute indentation suggestions for the first line
|
||||
// of each insertion. Otherwise, compute suggestions for every inserted line.
|
||||
let new_edited_row_ranges = contiguous_ranges(
|
||||
row_ranges.iter().flat_map(|(range, _)| {
|
||||
if request.is_block_mode {
|
||||
range.start..range.start + 1
|
||||
} else {
|
||||
range.clone()
|
||||
}
|
||||
}),
|
||||
max_rows_between_yields,
|
||||
);
|
||||
|
||||
// Compute new suggestions for each line, but only include them in the result
|
||||
// if they differ from the old suggestion for that line.
|
||||
let mut language_indent_sizes = language_indent_sizes_by_new_row.iter().peekable();
|
||||
let mut language_indent_size = IndentSize::default();
|
||||
for new_edited_row_range in new_edited_row_ranges {
|
||||
for (row_range, original_indent_column) in row_ranges {
|
||||
let new_edited_row_range = if request.is_block_mode {
|
||||
row_range.start..row_range.start + 1
|
||||
} else {
|
||||
row_range.clone()
|
||||
};
|
||||
|
||||
let suggestions = snapshot
|
||||
.suggest_autoindents(new_edited_row_range.clone())
|
||||
.into_iter()
|
||||
@@ -1469,22 +1466,9 @@ impl Buffer {
|
||||
}
|
||||
}
|
||||
}
|
||||
yield_now().await;
|
||||
}
|
||||
|
||||
// For each block of inserted text, adjust the indentation of the remaining
|
||||
// lines of the block by the same amount as the first line was adjusted.
|
||||
if request.is_block_mode {
|
||||
for (row_range, original_indent_column) in
|
||||
row_ranges
|
||||
.into_iter()
|
||||
.filter_map(|(range, original_indent_column)| {
|
||||
if range.len() > 1 {
|
||||
Some((range, original_indent_column?))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
if let (true, Some(original_indent_column)) =
|
||||
(request.is_block_mode, original_indent_column)
|
||||
{
|
||||
let new_indent = indent_sizes
|
||||
.get(&row_range.start)
|
||||
@@ -1509,6 +1493,8 @@ impl Buffer {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
yield_now().await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4227,7 +4213,6 @@ impl<'a> Iterator for BufferChunks<'a> {
|
||||
if self.range.start == self.chunks.offset() + chunk.len() {
|
||||
self.chunks.next().unwrap();
|
||||
}
|
||||
|
||||
Some(Chunk {
|
||||
text: slice,
|
||||
syntax_highlight_id: highlight_id,
|
||||
|
||||
@@ -1658,6 +1658,69 @@ fn test_autoindent_block_mode_without_original_indent_columns(cx: &mut AppContex
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_autoindent_block_mode_multiple_adjacent_ranges(cx: &mut AppContext) {
|
||||
init_settings(cx, |_| {});
|
||||
|
||||
cx.new_model(|cx| {
|
||||
let (text, ranges_to_replace) = marked_text_ranges(
|
||||
&"
|
||||
mod numbers {
|
||||
«fn one() {
|
||||
1
|
||||
}
|
||||
»
|
||||
«fn two() {
|
||||
2
|
||||
}
|
||||
»
|
||||
«fn three() {
|
||||
3
|
||||
}
|
||||
»}
|
||||
"
|
||||
.unindent(),
|
||||
false,
|
||||
);
|
||||
|
||||
let mut buffer = Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx);
|
||||
|
||||
buffer.edit(
|
||||
[
|
||||
(ranges_to_replace[0].clone(), "fn one() {\n 101\n}\n"),
|
||||
(ranges_to_replace[1].clone(), "fn two() {\n 102\n}\n"),
|
||||
(ranges_to_replace[2].clone(), "fn three() {\n 103\n}\n"),
|
||||
],
|
||||
Some(AutoindentMode::Block {
|
||||
original_indent_columns: vec![0, 0, 0],
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
|
||||
pretty_assertions::assert_eq!(
|
||||
buffer.text(),
|
||||
"
|
||||
mod numbers {
|
||||
fn one() {
|
||||
101
|
||||
}
|
||||
|
||||
fn two() {
|
||||
102
|
||||
}
|
||||
|
||||
fn three() {
|
||||
103
|
||||
}
|
||||
}
|
||||
"
|
||||
.unindent()
|
||||
);
|
||||
|
||||
buffer
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_autoindent_language_without_indents_query(cx: &mut AppContext) {
|
||||
init_settings(cx, |_| {});
|
||||
|
||||
@@ -831,7 +831,7 @@ impl AllLanguageSettings {
|
||||
|
||||
let editorconfig_properties = location.and_then(|location| {
|
||||
cx.global::<SettingsStore>()
|
||||
.editorconfg_properties(location.worktree_id, location.path)
|
||||
.editorconfig_properties(location.worktree_id, location.path)
|
||||
});
|
||||
if let Some(editorconfig_properties) = editorconfig_properties {
|
||||
let mut settings = settings.clone();
|
||||
|
||||
@@ -119,6 +119,10 @@ impl Markdown {
|
||||
this
|
||||
}
|
||||
|
||||
pub fn source(&self) -> &str {
|
||||
&self.source
|
||||
}
|
||||
|
||||
pub fn append(&mut self, text: &str, cx: &ViewContext<Self>) {
|
||||
self.source.push_str(text);
|
||||
self.parse(cx);
|
||||
@@ -137,10 +141,6 @@ impl Markdown {
|
||||
self.parse(cx);
|
||||
}
|
||||
|
||||
pub fn source(&self) -> &str {
|
||||
&self.source
|
||||
}
|
||||
|
||||
pub fn parsed_markdown(&self) -> &ParsedMarkdown {
|
||||
&self.parsed_markdown
|
||||
}
|
||||
|
||||
@@ -36,6 +36,20 @@ struct MarkdownParser<'a> {
|
||||
language_registry: Option<Arc<LanguageRegistry>>,
|
||||
}
|
||||
|
||||
struct MarkdownListItem {
|
||||
content: Vec<ParsedMarkdownElement>,
|
||||
item_type: ParsedMarkdownListItemType,
|
||||
}
|
||||
|
||||
impl Default for MarkdownListItem {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
content: Vec::new(),
|
||||
item_type: ParsedMarkdownListItemType::Unordered,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> MarkdownParser<'a> {
|
||||
fn new(
|
||||
tokens: Vec<(Event<'a>, Range<usize>)>,
|
||||
@@ -475,9 +489,8 @@ impl<'a> MarkdownParser<'a> {
|
||||
let (_, list_source_range) = self.previous().unwrap();
|
||||
|
||||
let mut items = Vec::new();
|
||||
let mut items_stack = vec![Vec::new()];
|
||||
let mut items_stack = vec![MarkdownListItem::default()];
|
||||
let mut depth = 1;
|
||||
let mut task_item = None;
|
||||
let mut order = order;
|
||||
let mut order_stack = Vec::new();
|
||||
|
||||
@@ -517,8 +530,9 @@ impl<'a> MarkdownParser<'a> {
|
||||
start_item_range = source_range.clone();
|
||||
|
||||
self.cursor += 1;
|
||||
items_stack.push(Vec::new());
|
||||
items_stack.push(MarkdownListItem::default());
|
||||
|
||||
let mut task_list = None;
|
||||
// Check for task list marker (`- [ ]` or `- [x]`)
|
||||
if let Some(event) = self.current_event() {
|
||||
// If there is a linebreak in between two list items the task list marker will actually be the first element of the paragraph
|
||||
@@ -527,7 +541,7 @@ impl<'a> MarkdownParser<'a> {
|
||||
}
|
||||
|
||||
if let Some((Event::TaskListMarker(checked), range)) = self.current() {
|
||||
task_item = Some((*checked, range.clone()));
|
||||
task_list = Some((*checked, range.clone()));
|
||||
self.cursor += 1;
|
||||
}
|
||||
}
|
||||
@@ -539,13 +553,21 @@ impl<'a> MarkdownParser<'a> {
|
||||
let text = self.parse_text(false, Some(range.clone()));
|
||||
let block = ParsedMarkdownElement::Paragraph(text);
|
||||
if let Some(content) = items_stack.last_mut() {
|
||||
content.push(block);
|
||||
let item_type = if let Some((checked, range)) = task_list {
|
||||
ParsedMarkdownListItemType::Task(checked, range)
|
||||
} else if let Some(order) = order {
|
||||
ParsedMarkdownListItemType::Ordered(order)
|
||||
} else {
|
||||
ParsedMarkdownListItemType::Unordered
|
||||
};
|
||||
content.item_type = item_type;
|
||||
content.content.push(block);
|
||||
}
|
||||
} else {
|
||||
let block = self.parse_block().await;
|
||||
if let Some(block) = block {
|
||||
if let Some(content) = items_stack.last_mut() {
|
||||
content.extend(block);
|
||||
if let Some(list_item) = items_stack.last_mut() {
|
||||
list_item.content.extend(block);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -559,19 +581,11 @@ impl<'a> MarkdownParser<'a> {
|
||||
Event::End(TagEnd::Item) => {
|
||||
self.cursor += 1;
|
||||
|
||||
let item_type = if let Some((checked, range)) = task_item {
|
||||
ParsedMarkdownListItemType::Task(checked, range)
|
||||
} else if let Some(order) = order {
|
||||
ParsedMarkdownListItemType::Ordered(order)
|
||||
} else {
|
||||
ParsedMarkdownListItemType::Unordered
|
||||
};
|
||||
|
||||
if let Some(current) = order {
|
||||
order = Some(current + 1);
|
||||
}
|
||||
|
||||
if let Some(content) = items_stack.pop() {
|
||||
if let Some(list_item) = items_stack.pop() {
|
||||
let source_range = source_ranges
|
||||
.remove(&depth)
|
||||
.unwrap_or(start_item_range.clone());
|
||||
@@ -580,9 +594,9 @@ impl<'a> MarkdownParser<'a> {
|
||||
let source_range = source_range.start..source_range.end - 1;
|
||||
let item = ParsedMarkdownElement::ListItem(ParsedMarkdownListItem {
|
||||
source_range,
|
||||
content,
|
||||
content: list_item.content,
|
||||
depth,
|
||||
item_type,
|
||||
item_type: list_item.item_type,
|
||||
});
|
||||
|
||||
if let Some(index) = insertion_indices.get(&depth) {
|
||||
@@ -592,8 +606,6 @@ impl<'a> MarkdownParser<'a> {
|
||||
items.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
task_item = None;
|
||||
}
|
||||
_ => {
|
||||
if depth == 0 {
|
||||
@@ -603,10 +615,10 @@ impl<'a> MarkdownParser<'a> {
|
||||
// or the list item contains blocks that should be rendered after the nested list items
|
||||
let block = self.parse_block().await;
|
||||
if let Some(block) = block {
|
||||
if let Some(items_stack) = items_stack.last_mut() {
|
||||
if let Some(list_item) = items_stack.last_mut() {
|
||||
// If we did not insert any nested items yet (in this case insertion index is set), we can append the block to the current list item
|
||||
if !insertion_indices.contains_key(&depth) {
|
||||
items_stack.extend(block);
|
||||
list_item.content.extend(block);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -722,7 +734,6 @@ mod tests {
|
||||
use gpui::BackgroundExecutor;
|
||||
use language::{tree_sitter_rust, HighlightId, Language, LanguageConfig, LanguageMatcher};
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use ParsedMarkdownListItemType::*;
|
||||
|
||||
async fn parse(input: &str) -> ParsedMarkdown {
|
||||
@@ -956,6 +967,33 @@ Some other content
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_list_with_indented_task() {
|
||||
let parsed = parse(
|
||||
"\
|
||||
- [ ] TODO
|
||||
- [x] Checked
|
||||
- Unordered
|
||||
1. Number 1
|
||||
1. Number 2
|
||||
1. Number A
|
||||
",
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
parsed.children,
|
||||
vec![
|
||||
list_item(0..12, 1, Task(false, 2..5), vec![p("TODO", 6..10)]),
|
||||
list_item(13..26, 2, Task(true, 15..18), vec![p("Checked", 19..26)]),
|
||||
list_item(29..40, 2, Unordered, vec![p("Unordered", 31..40)]),
|
||||
list_item(43..54, 2, Ordered(1), vec![p("Number 1", 46..54)]),
|
||||
list_item(57..68, 2, Ordered(2), vec![p("Number 2", 60..68)]),
|
||||
list_item(69..80, 1, Ordered(1), vec![p("Number A", 72..80)]),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_list_with_linebreak_is_handled_correctly() {
|
||||
let parsed = parse(
|
||||
|
||||
@@ -94,6 +94,7 @@ pub enum Event {
|
||||
transaction_id: TransactionId,
|
||||
},
|
||||
Reloaded,
|
||||
ReloadNeeded,
|
||||
DiffBaseChanged,
|
||||
DiffUpdated {
|
||||
buffer: Model<Buffer>,
|
||||
@@ -1735,6 +1736,7 @@ impl MultiBuffer {
|
||||
language::BufferEvent::Saved => Event::Saved,
|
||||
language::BufferEvent::FileHandleChanged => Event::FileHandleChanged,
|
||||
language::BufferEvent::Reloaded => Event::Reloaded,
|
||||
language::BufferEvent::ReloadNeeded => Event::ReloadNeeded,
|
||||
language::BufferEvent::DiffBaseChanged => Event::DiffBaseChanged,
|
||||
language::BufferEvent::DiffUpdated => Event::DiffUpdated { buffer },
|
||||
language::BufferEvent::LanguageChanged => {
|
||||
@@ -1748,7 +1750,6 @@ impl MultiBuffer {
|
||||
self.capability = buffer.read(cx).capability();
|
||||
Event::CapabilityChanged
|
||||
}
|
||||
|
||||
//
|
||||
language::BufferEvent::Operation { .. } => return,
|
||||
});
|
||||
|
||||
@@ -54,7 +54,7 @@ trait BufferStoreImpl {
|
||||
|
||||
fn reload_buffers(
|
||||
&self,
|
||||
buffers: Vec<Model<Buffer>>,
|
||||
buffers: HashSet<Model<Buffer>>,
|
||||
push_to_history: bool,
|
||||
cx: &mut ModelContext<BufferStore>,
|
||||
) -> Task<Result<ProjectTransaction>>;
|
||||
@@ -392,7 +392,7 @@ impl BufferStoreImpl for Model<RemoteBufferStore> {
|
||||
|
||||
fn reload_buffers(
|
||||
&self,
|
||||
buffers: Vec<Model<Buffer>>,
|
||||
buffers: HashSet<Model<Buffer>>,
|
||||
push_to_history: bool,
|
||||
cx: &mut ModelContext<BufferStore>,
|
||||
) -> Task<Result<ProjectTransaction>> {
|
||||
@@ -938,7 +938,7 @@ impl BufferStoreImpl for Model<LocalBufferStore> {
|
||||
|
||||
fn reload_buffers(
|
||||
&self,
|
||||
buffers: Vec<Model<Buffer>>,
|
||||
buffers: HashSet<Model<Buffer>>,
|
||||
push_to_history: bool,
|
||||
cx: &mut ModelContext<BufferStore>,
|
||||
) -> Task<Result<ProjectTransaction>> {
|
||||
@@ -1894,13 +1894,10 @@ impl BufferStore {
|
||||
push_to_history: bool,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<ProjectTransaction>> {
|
||||
let buffers: Vec<Model<Buffer>> = buffers
|
||||
.into_iter()
|
||||
.filter(|buffer| buffer.read(cx).is_dirty())
|
||||
.collect();
|
||||
if buffers.is_empty() {
|
||||
return Task::ready(Ok(ProjectTransaction::default()));
|
||||
}
|
||||
|
||||
self.state.reload_buffers(buffers, push_to_history, cx)
|
||||
}
|
||||
|
||||
|
||||
@@ -1029,6 +1029,7 @@ impl LspStore {
|
||||
})
|
||||
.detach()
|
||||
}
|
||||
WorktreeStoreEvent::WorktreeReleased(..) => {}
|
||||
WorktreeStoreEvent::WorktreeRemoved(_, id) => self.remove_worktree(*id, cx),
|
||||
WorktreeStoreEvent::WorktreeOrderChanged => {}
|
||||
WorktreeStoreEvent::WorktreeUpdateSent(worktree) => {
|
||||
|
||||
@@ -2223,9 +2223,11 @@ impl Project {
|
||||
cx.emit(Event::WorktreeAdded);
|
||||
}
|
||||
WorktreeStoreEvent::WorktreeRemoved(_, id) => {
|
||||
self.on_worktree_removed(*id, cx);
|
||||
cx.emit(Event::WorktreeRemoved(*id));
|
||||
}
|
||||
WorktreeStoreEvent::WorktreeReleased(_, id) => {
|
||||
self.on_worktree_released(*id, cx);
|
||||
}
|
||||
WorktreeStoreEvent::WorktreeOrderChanged => cx.emit(Event::WorktreeOrderChanged),
|
||||
WorktreeStoreEvent::WorktreeUpdateSent(_) => {}
|
||||
}
|
||||
@@ -2261,7 +2263,7 @@ impl Project {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn on_worktree_removed(&mut self, id_to_remove: WorktreeId, cx: &mut ModelContext<Self>) {
|
||||
fn on_worktree_released(&mut self, id_to_remove: WorktreeId, cx: &mut ModelContext<Self>) {
|
||||
if let Some(dev_server_project_id) = self.dev_server_project_id {
|
||||
let paths: Vec<String> = self
|
||||
.visible_worktrees(cx)
|
||||
@@ -2312,6 +2314,12 @@ impl Project {
|
||||
|
||||
let buffer_id = buffer.read(cx).remote_id();
|
||||
match event {
|
||||
BufferEvent::ReloadNeeded => {
|
||||
if !self.is_via_collab() {
|
||||
self.reload_buffers([buffer.clone()].into_iter().collect(), false, cx)
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
}
|
||||
BufferEvent::Operation {
|
||||
operation,
|
||||
is_local: true,
|
||||
|
||||
@@ -62,6 +62,7 @@ pub struct WorktreeStore {
|
||||
pub enum WorktreeStoreEvent {
|
||||
WorktreeAdded(Model<Worktree>),
|
||||
WorktreeRemoved(EntityId, WorktreeId),
|
||||
WorktreeReleased(EntityId, WorktreeId),
|
||||
WorktreeOrderChanged,
|
||||
WorktreeUpdateSent(Model<Worktree>),
|
||||
}
|
||||
@@ -394,6 +395,10 @@ impl WorktreeStore {
|
||||
|
||||
let handle_id = worktree.entity_id();
|
||||
cx.observe_release(worktree, move |this, worktree, cx| {
|
||||
cx.emit(WorktreeStoreEvent::WorktreeReleased(
|
||||
handle_id,
|
||||
worktree.id(),
|
||||
));
|
||||
cx.emit(WorktreeStoreEvent::WorktreeRemoved(
|
||||
handle_id,
|
||||
worktree.id(),
|
||||
|
||||
@@ -2327,6 +2327,7 @@ impl ProjectPanel {
|
||||
let depth = details.depth;
|
||||
let worktree_id = details.worktree_id;
|
||||
let selections = Arc::new(self.marked_entries.clone());
|
||||
let is_local = self.project.read(cx).is_local();
|
||||
|
||||
let dragged_selection = DraggedSelection {
|
||||
active_selection: selection,
|
||||
@@ -2334,57 +2335,59 @@ impl ProjectPanel {
|
||||
};
|
||||
div()
|
||||
.id(entry_id.to_proto() as usize)
|
||||
.on_drag_move::<ExternalPaths>(cx.listener(
|
||||
move |this, event: &DragMoveEvent<ExternalPaths>, cx| {
|
||||
if event.bounds.contains(&event.event.position) {
|
||||
if this.last_external_paths_drag_over_entry == Some(entry_id) {
|
||||
return;
|
||||
}
|
||||
this.last_external_paths_drag_over_entry = Some(entry_id);
|
||||
this.marked_entries.clear();
|
||||
.when(is_local, |div| {
|
||||
div.on_drag_move::<ExternalPaths>(cx.listener(
|
||||
move |this, event: &DragMoveEvent<ExternalPaths>, cx| {
|
||||
if event.bounds.contains(&event.event.position) {
|
||||
if this.last_external_paths_drag_over_entry == Some(entry_id) {
|
||||
return;
|
||||
}
|
||||
this.last_external_paths_drag_over_entry = Some(entry_id);
|
||||
this.marked_entries.clear();
|
||||
|
||||
let Some((worktree, path, entry)) = maybe!({
|
||||
let worktree = this
|
||||
.project
|
||||
.read(cx)
|
||||
.worktree_for_id(selection.worktree_id, cx)?;
|
||||
let worktree = worktree.read(cx);
|
||||
let abs_path = worktree.absolutize(&path).log_err()?;
|
||||
let path = if abs_path.is_dir() {
|
||||
path.as_ref()
|
||||
} else {
|
||||
path.parent()?
|
||||
let Some((worktree, path, entry)) = maybe!({
|
||||
let worktree = this
|
||||
.project
|
||||
.read(cx)
|
||||
.worktree_for_id(selection.worktree_id, cx)?;
|
||||
let worktree = worktree.read(cx);
|
||||
let abs_path = worktree.absolutize(&path).log_err()?;
|
||||
let path = if abs_path.is_dir() {
|
||||
path.as_ref()
|
||||
} else {
|
||||
path.parent()?
|
||||
};
|
||||
let entry = worktree.entry_for_path(path)?;
|
||||
Some((worktree, path, entry))
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
let entry = worktree.entry_for_path(path)?;
|
||||
Some((worktree, path, entry))
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
|
||||
this.marked_entries.insert(SelectedEntry {
|
||||
entry_id: entry.id,
|
||||
worktree_id: worktree.id(),
|
||||
});
|
||||
|
||||
for entry in worktree.child_entries(path) {
|
||||
this.marked_entries.insert(SelectedEntry {
|
||||
entry_id: entry.id,
|
||||
worktree_id: worktree.id(),
|
||||
});
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
},
|
||||
))
|
||||
.on_drop(
|
||||
cx.listener(move |this, external_paths: &ExternalPaths, cx| {
|
||||
this.last_external_paths_drag_over_entry = None;
|
||||
this.marked_entries.clear();
|
||||
this.drop_external_files(external_paths.paths(), entry_id, cx);
|
||||
cx.stop_propagation();
|
||||
}),
|
||||
)
|
||||
for entry in worktree.child_entries(path) {
|
||||
this.marked_entries.insert(SelectedEntry {
|
||||
entry_id: entry.id,
|
||||
worktree_id: worktree.id(),
|
||||
});
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
},
|
||||
))
|
||||
.on_drop(cx.listener(
|
||||
move |this, external_paths: &ExternalPaths, cx| {
|
||||
this.last_external_paths_drag_over_entry = None;
|
||||
this.marked_entries.clear();
|
||||
this.drop_external_files(external_paths.paths(), entry_id, cx);
|
||||
cx.stop_propagation();
|
||||
},
|
||||
))
|
||||
})
|
||||
.on_drag(dragged_selection, move |selection, cx| {
|
||||
cx.new_view(|_| DraggedProjectEntryView {
|
||||
details: details.clone(),
|
||||
@@ -2802,6 +2805,7 @@ impl Render for ProjectPanel {
|
||||
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> impl IntoElement {
|
||||
let has_worktree = !self.visible_entries.is_empty();
|
||||
let project = self.project.read(cx);
|
||||
let is_local = project.is_local();
|
||||
|
||||
if has_worktree {
|
||||
let item_count = self
|
||||
@@ -2939,29 +2943,31 @@ impl Render for ProjectPanel {
|
||||
.log_err();
|
||||
})),
|
||||
)
|
||||
.drag_over::<ExternalPaths>(|style, _, cx| {
|
||||
style.bg(cx.theme().colors().drop_target_background)
|
||||
.when(is_local, |div| {
|
||||
div.drag_over::<ExternalPaths>(|style, _, cx| {
|
||||
style.bg(cx.theme().colors().drop_target_background)
|
||||
})
|
||||
.on_drop(cx.listener(
|
||||
move |this, external_paths: &ExternalPaths, cx| {
|
||||
this.last_external_paths_drag_over_entry = None;
|
||||
this.marked_entries.clear();
|
||||
if let Some(task) = this
|
||||
.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.open_workspace_for_paths(
|
||||
true,
|
||||
external_paths.paths().to_owned(),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.log_err()
|
||||
{
|
||||
task.detach_and_log_err(cx);
|
||||
}
|
||||
cx.stop_propagation();
|
||||
},
|
||||
))
|
||||
})
|
||||
.on_drop(
|
||||
cx.listener(move |this, external_paths: &ExternalPaths, cx| {
|
||||
this.last_external_paths_drag_over_entry = None;
|
||||
this.marked_entries.clear();
|
||||
if let Some(task) = this
|
||||
.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.open_workspace_for_paths(
|
||||
true,
|
||||
external_paths.paths().to_owned(),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.log_err()
|
||||
{
|
||||
task.detach_and_log_err(cx);
|
||||
}
|
||||
cx.stop_propagation();
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,8 @@ fuzzy.workspace = true
|
||||
gpui.workspace = true
|
||||
itertools.workspace = true
|
||||
log.workspace = true
|
||||
language.workspace = true
|
||||
markdown.workspace = true
|
||||
menu.workspace = true
|
||||
ordered-float.workspace = true
|
||||
picker.workspace = true
|
||||
@@ -37,6 +39,7 @@ settings.workspace = true
|
||||
smol.workspace = true
|
||||
task.workspace = true
|
||||
terminal_view.workspace = true
|
||||
theme.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
workspace.workspace = true
|
||||
|
||||
@@ -2,7 +2,9 @@ use std::path::PathBuf;
|
||||
|
||||
use dev_server_projects::DevServer;
|
||||
use gpui::{ClickEvent, DismissEvent, EventEmitter, FocusHandle, FocusableView, Render, WeakView};
|
||||
use project::project_settings::ProjectSettings;
|
||||
use remote::SshConnectionOptions;
|
||||
use settings::Settings;
|
||||
use ui::{
|
||||
div, h_flex, rems, Button, ButtonCommon, ButtonStyle, Clickable, ElevationIndex, FluentBuilder,
|
||||
Headline, HeadlineSize, IconName, IconPosition, InteractiveElement, IntoElement, Label, Modal,
|
||||
@@ -11,8 +13,8 @@ use ui::{
|
||||
use workspace::{notifications::DetachAndPromptErr, ModalView, OpenOptions, Workspace};
|
||||
|
||||
use crate::{
|
||||
dev_servers::reconnect_to_dev_server_project, open_dev_server_project, open_ssh_project,
|
||||
DevServerProjects,
|
||||
open_dev_server_project, open_ssh_project, remote_servers::reconnect_to_dev_server_project,
|
||||
RemoteServerProjects, SshSettings,
|
||||
};
|
||||
|
||||
enum Host {
|
||||
@@ -25,6 +27,7 @@ pub struct DisconnectedOverlay {
|
||||
workspace: WeakView<Workspace>,
|
||||
host: Host,
|
||||
focus_handle: FocusHandle,
|
||||
finished: bool,
|
||||
}
|
||||
|
||||
impl EventEmitter<DismissEvent> for DisconnectedOverlay {}
|
||||
@@ -34,6 +37,9 @@ impl FocusableView for DisconnectedOverlay {
|
||||
}
|
||||
}
|
||||
impl ModalView for DisconnectedOverlay {
|
||||
fn on_before_dismiss(&mut self, _: &mut ViewContext<Self>) -> workspace::DismissDecision {
|
||||
return workspace::DismissDecision::Dismiss(self.finished);
|
||||
}
|
||||
fn fade_out_background(&self) -> bool {
|
||||
true
|
||||
}
|
||||
@@ -69,6 +75,7 @@ impl DisconnectedOverlay {
|
||||
};
|
||||
|
||||
workspace.toggle_modal(cx, |cx| DisconnectedOverlay {
|
||||
finished: false,
|
||||
workspace: handle,
|
||||
host,
|
||||
focus_handle: cx.focus_handle(),
|
||||
@@ -78,6 +85,7 @@ impl DisconnectedOverlay {
|
||||
}
|
||||
|
||||
fn handle_reconnect(&mut self, _: &ClickEvent, cx: &mut ViewContext<Self>) {
|
||||
self.finished = true;
|
||||
cx.emit(DismissEvent);
|
||||
|
||||
match &self.host {
|
||||
@@ -130,7 +138,7 @@ impl DisconnectedOverlay {
|
||||
} else {
|
||||
return workspace.update(cx, |workspace, cx| {
|
||||
let handle = cx.view().downgrade();
|
||||
workspace.toggle_modal(cx, |cx| DevServerProjects::new(cx, handle))
|
||||
workspace.toggle_modal(cx, |cx| RemoteServerProjects::new(cx, handle))
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -157,6 +165,16 @@ impl DisconnectedOverlay {
|
||||
let paths = ssh_project.paths.iter().map(PathBuf::from).collect();
|
||||
|
||||
cx.spawn(move |_, mut cx| async move {
|
||||
let nickname = cx
|
||||
.update(|cx| {
|
||||
SshSettings::get_global(cx).nickname_for(
|
||||
&connection_options.host,
|
||||
connection_options.port,
|
||||
&connection_options.username,
|
||||
)
|
||||
})
|
||||
.ok()
|
||||
.flatten();
|
||||
open_ssh_project(
|
||||
connection_options,
|
||||
paths,
|
||||
@@ -165,6 +183,7 @@ impl DisconnectedOverlay {
|
||||
replace_window: Some(window),
|
||||
..Default::default()
|
||||
},
|
||||
nickname,
|
||||
&mut cx,
|
||||
)
|
||||
.await?;
|
||||
@@ -174,6 +193,7 @@ impl DisconnectedOverlay {
|
||||
}
|
||||
|
||||
fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
|
||||
self.finished = true;
|
||||
cx.emit(DismissEvent)
|
||||
}
|
||||
}
|
||||
@@ -190,9 +210,17 @@ impl Render for DisconnectedOverlay {
|
||||
"Your connection to the remote project has been lost.".to_string()
|
||||
}
|
||||
Host::SshRemoteProject(options) => {
|
||||
let autosave = if ProjectSettings::get_global(cx)
|
||||
.session
|
||||
.restore_unsaved_buffers
|
||||
{
|
||||
"\nUnsaved changes are stored locally."
|
||||
} else {
|
||||
""
|
||||
};
|
||||
format!(
|
||||
"Your connection to {} has been lost",
|
||||
options.connection_string()
|
||||
"Your connection to {} has been lost.{}",
|
||||
options.host, autosave
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
mod dev_servers;
|
||||
pub mod disconnected_overlay;
|
||||
mod remote_servers;
|
||||
mod ssh_connections;
|
||||
use remote::SshConnectionOptions;
|
||||
pub use ssh_connections::open_ssh_project;
|
||||
|
||||
use client::{DevServerProjectId, ProjectId};
|
||||
use dev_servers::reconnect_to_dev_server_project;
|
||||
pub use dev_servers::DevServerProjects;
|
||||
use disconnected_overlay::DisconnectedOverlay;
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use gpui::{
|
||||
@@ -19,6 +17,8 @@ use picker::{
|
||||
highlighted_match_with_paths::{HighlightedMatchWithPaths, HighlightedText},
|
||||
Picker, PickerDelegate,
|
||||
};
|
||||
use remote_servers::reconnect_to_dev_server_project;
|
||||
pub use remote_servers::RemoteServerProjects;
|
||||
use rpc::proto::DevServerStatus;
|
||||
use serde::Deserialize;
|
||||
use settings::Settings;
|
||||
@@ -53,7 +53,8 @@ gpui::actions!(projects, [OpenRemote]);
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
SshSettings::register(cx);
|
||||
cx.observe_new_views(RecentProjects::register).detach();
|
||||
cx.observe_new_views(DevServerProjects::register).detach();
|
||||
cx.observe_new_views(RemoteServerProjects::register)
|
||||
.detach();
|
||||
cx.observe_new_views(DisconnectedOverlay::register).detach();
|
||||
}
|
||||
|
||||
@@ -359,7 +360,7 @@ impl PickerDelegate for RecentProjectsDelegate {
|
||||
if response == 1 {
|
||||
workspace.update(&mut cx, |workspace, cx| {
|
||||
let handle = cx.view().downgrade();
|
||||
workspace.toggle_modal(cx, |cx| DevServerProjects::new(cx, handle))
|
||||
workspace.toggle_modal(cx, |cx| RemoteServerProjects::new(cx, handle))
|
||||
})?;
|
||||
} else {
|
||||
workspace.update(&mut cx, |workspace, cx| {
|
||||
@@ -387,6 +388,7 @@ impl PickerDelegate for RecentProjectsDelegate {
|
||||
};
|
||||
|
||||
let args = SshSettings::get_global(cx).args_for(&ssh_project.host, ssh_project.port, &ssh_project.user);
|
||||
let nickname = SshSettings::get_global(cx).nickname_for(&ssh_project.host, ssh_project.port, &ssh_project.user);
|
||||
let connection_options = SshConnectionOptions {
|
||||
host: ssh_project.host.clone(),
|
||||
username: ssh_project.user.clone(),
|
||||
@@ -398,7 +400,7 @@ impl PickerDelegate for RecentProjectsDelegate {
|
||||
let paths = ssh_project.paths.iter().map(PathBuf::from).collect();
|
||||
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
open_ssh_project(connection_options, paths, app_state, open_options, &mut cx).await
|
||||
open_ssh_project(connection_options, paths, app_state, open_options, nickname, &mut cx).await
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,8 +18,8 @@ use gpui::ClipboardItem;
|
||||
use gpui::Task;
|
||||
use gpui::WeakView;
|
||||
use gpui::{
|
||||
AnyElement, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, FontWeight,
|
||||
Model, PromptLevel, ScrollHandle, View, ViewContext,
|
||||
AnyElement, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Model,
|
||||
PromptLevel, ScrollHandle, View, ViewContext,
|
||||
};
|
||||
use picker::Picker;
|
||||
use project::terminals::wrap_for_ssh;
|
||||
@@ -33,10 +33,10 @@ use task::HideStrategy;
|
||||
use task::RevealStrategy;
|
||||
use task::SpawnInTerminal;
|
||||
use terminal_view::terminal_panel::TerminalPanel;
|
||||
use ui::Scrollbar;
|
||||
use ui::ScrollbarState;
|
||||
use ui::Section;
|
||||
use ui::{prelude::*, IconButtonShape, List, ListItem, ListSeparator, Modal, ModalHeader, Tooltip};
|
||||
use ui::{
|
||||
prelude::*, IconButtonShape, List, ListItem, ListSeparator, Modal, ModalHeader, Scrollbar,
|
||||
ScrollbarState, Section, Tooltip,
|
||||
};
|
||||
use util::ResultExt;
|
||||
use workspace::notifications::NotificationId;
|
||||
use workspace::OpenOptions;
|
||||
@@ -55,7 +55,7 @@ use crate::ssh_connections::SshPrompt;
|
||||
use crate::ssh_connections::SshSettings;
|
||||
use crate::OpenRemote;
|
||||
|
||||
pub struct DevServerProjects {
|
||||
pub struct RemoteServerProjects {
|
||||
mode: Mode,
|
||||
focus_handle: FocusHandle,
|
||||
scroll_handle: ScrollHandle,
|
||||
@@ -63,14 +63,14 @@ pub struct DevServerProjects {
|
||||
selectable_items: SelectableItemList,
|
||||
}
|
||||
|
||||
struct CreateDevServer {
|
||||
struct CreateRemoteServer {
|
||||
address_editor: View<Editor>,
|
||||
address_error: Option<SharedString>,
|
||||
ssh_prompt: Option<View<SshPrompt>>,
|
||||
_creating: Option<Task<Option<()>>>,
|
||||
}
|
||||
|
||||
impl CreateDevServer {
|
||||
impl CreateRemoteServer {
|
||||
fn new(cx: &mut WindowContext<'_>) -> Self {
|
||||
let address_editor = cx.new_view(Editor::single_line);
|
||||
address_editor.update(cx, |this, cx| {
|
||||
@@ -87,12 +87,13 @@ impl CreateDevServer {
|
||||
|
||||
struct ProjectPicker {
|
||||
connection_string: SharedString,
|
||||
nickname: Option<SharedString>,
|
||||
picker: View<Picker<OpenPathDelegate>>,
|
||||
_path_task: Shared<Task<Option<()>>>,
|
||||
}
|
||||
|
||||
type SelectedItemCallback =
|
||||
Box<dyn Fn(&mut DevServerProjects, &mut ViewContext<DevServerProjects>) + 'static>;
|
||||
Box<dyn Fn(&mut RemoteServerProjects, &mut ViewContext<RemoteServerProjects>) + 'static>;
|
||||
|
||||
/// Used to implement keyboard navigation for SSH modal.
|
||||
#[derive(Default)]
|
||||
@@ -171,20 +172,30 @@ impl SelectableItemList {
|
||||
self.active_item == self.items.len().checked_sub(1)
|
||||
}
|
||||
|
||||
fn confirm(&self, dev_modal: &mut DevServerProjects, cx: &mut ViewContext<DevServerProjects>) {
|
||||
fn confirm(
|
||||
&self,
|
||||
remote_modal: &mut RemoteServerProjects,
|
||||
cx: &mut ViewContext<RemoteServerProjects>,
|
||||
) {
|
||||
if let Some(active_item) = self.active_item.and_then(|ix| self.items.get(ix)) {
|
||||
active_item(dev_modal, cx);
|
||||
active_item(remote_modal, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FocusableView for ProjectPicker {
|
||||
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
|
||||
self.picker.focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl ProjectPicker {
|
||||
fn new(
|
||||
ix: usize,
|
||||
connection_string: SharedString,
|
||||
connection: SshConnectionOptions,
|
||||
project: Model<Project>,
|
||||
workspace: WeakView<Workspace>,
|
||||
cx: &mut ViewContext<DevServerProjects>,
|
||||
cx: &mut ViewContext<RemoteServerProjects>,
|
||||
) -> View<Self> {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
let lister = project::DirectoryLister::Project(project.clone());
|
||||
@@ -198,6 +209,12 @@ impl ProjectPicker {
|
||||
picker.set_query(query, cx);
|
||||
picker
|
||||
});
|
||||
let connection_string = connection.connection_string().into();
|
||||
let nickname = SshSettings::get_global(cx).nickname_for(
|
||||
&connection.host,
|
||||
connection.port,
|
||||
&connection.username,
|
||||
);
|
||||
cx.new_view(|cx| {
|
||||
let _path_task = cx
|
||||
.spawn({
|
||||
@@ -208,7 +225,7 @@ impl ProjectPicker {
|
||||
.update(&mut cx, |workspace, cx| {
|
||||
let weak = cx.view().downgrade();
|
||||
workspace
|
||||
.toggle_modal(cx, |cx| DevServerProjects::new(cx, weak));
|
||||
.toggle_modal(cx, |cx| RemoteServerProjects::new(cx, weak));
|
||||
})
|
||||
.log_err()?;
|
||||
return None;
|
||||
@@ -283,6 +300,7 @@ impl ProjectPicker {
|
||||
_path_task,
|
||||
picker,
|
||||
connection_string,
|
||||
nickname,
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -294,11 +312,16 @@ impl gpui::Render for ProjectPicker {
|
||||
.child(
|
||||
SshConnectionHeader {
|
||||
connection_string: self.connection_string.clone(),
|
||||
nickname: None,
|
||||
nickname: self.nickname.clone(),
|
||||
}
|
||||
.render(cx),
|
||||
)
|
||||
.child(self.picker.clone())
|
||||
.child(
|
||||
div()
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.child(self.picker.clone()),
|
||||
)
|
||||
}
|
||||
}
|
||||
enum Mode {
|
||||
@@ -306,7 +329,7 @@ enum Mode {
|
||||
ViewServerOptions(usize, SshConnection),
|
||||
EditNickname(EditNicknameState),
|
||||
ProjectPicker(View<ProjectPicker>),
|
||||
CreateDevServer(CreateDevServer),
|
||||
CreateRemoteServer(CreateRemoteServer),
|
||||
}
|
||||
|
||||
impl Mode {
|
||||
@@ -315,7 +338,7 @@ impl Mode {
|
||||
Self::Default(ScrollbarState::new(handle))
|
||||
}
|
||||
}
|
||||
impl DevServerProjects {
|
||||
impl RemoteServerProjects {
|
||||
pub fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
|
||||
workspace.register_action(|workspace, _: &OpenRemote, cx| {
|
||||
let handle = cx.view().downgrade();
|
||||
@@ -348,18 +371,34 @@ impl DevServerProjects {
|
||||
}
|
||||
}
|
||||
|
||||
fn scroll_to_selected(&self, _: &mut ViewContext<Self>) {
|
||||
if let Mode::Default(scroll_state) = &self.mode {
|
||||
if let ui::ScrollableHandle::NonUniform(scroll_handle) = scroll_state.scroll_handle() {
|
||||
if let Some(active_item) = self.selectable_items.active_item {
|
||||
scroll_handle.scroll_to_item(active_item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn next_item(&mut self, _: &menu::SelectNext, cx: &mut ViewContext<Self>) {
|
||||
if !matches!(self.mode, Mode::Default(_) | Mode::ViewServerOptions(_, _)) {
|
||||
return;
|
||||
}
|
||||
self.selectable_items.next(cx);
|
||||
cx.notify();
|
||||
self.scroll_to_selected(cx);
|
||||
}
|
||||
|
||||
fn prev_item(&mut self, _: &menu::SelectPrev, cx: &mut ViewContext<Self>) {
|
||||
if !matches!(self.mode, Mode::Default(_) | Mode::ViewServerOptions(_, _)) {
|
||||
return;
|
||||
}
|
||||
self.selectable_items.prev(cx);
|
||||
cx.notify();
|
||||
self.scroll_to_selected(cx);
|
||||
}
|
||||
|
||||
pub fn project_picker(
|
||||
ix: usize,
|
||||
connection_options: remote::SshConnectionOptions,
|
||||
@@ -370,11 +409,12 @@ impl DevServerProjects {
|
||||
let mut this = Self::new(cx, workspace.clone());
|
||||
this.mode = Mode::ProjectPicker(ProjectPicker::new(
|
||||
ix,
|
||||
connection_options.connection_string().into(),
|
||||
connection_options,
|
||||
project,
|
||||
workspace,
|
||||
cx,
|
||||
));
|
||||
cx.notify();
|
||||
|
||||
this
|
||||
}
|
||||
@@ -388,7 +428,7 @@ impl DevServerProjects {
|
||||
let connection_options = match SshConnectionOptions::parse_command_line(&input) {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
self.mode = Mode::CreateDevServer(CreateDevServer {
|
||||
self.mode = Mode::CreateRemoteServer(CreateRemoteServer {
|
||||
address_editor: editor,
|
||||
address_error: Some(format!("could not parse: {:?}", e).into()),
|
||||
ssh_prompt: None,
|
||||
@@ -397,10 +437,10 @@ impl DevServerProjects {
|
||||
return;
|
||||
}
|
||||
};
|
||||
let ssh_prompt = cx.new_view(|cx| SshPrompt::new(&connection_options, cx));
|
||||
let ssh_prompt = cx.new_view(|cx| SshPrompt::new(&connection_options, None, cx));
|
||||
|
||||
let connection = connect_over_ssh(
|
||||
connection_options.dev_server_identifier(),
|
||||
connection_options.remote_server_identifier(),
|
||||
connection_options.clone(),
|
||||
ssh_prompt.clone(),
|
||||
cx,
|
||||
@@ -430,7 +470,7 @@ impl DevServerProjects {
|
||||
address_editor.update(cx, |this, _| {
|
||||
this.set_read_only(false);
|
||||
});
|
||||
this.mode = Mode::CreateDevServer(CreateDevServer {
|
||||
this.mode = Mode::CreateRemoteServer(CreateRemoteServer {
|
||||
address_editor,
|
||||
address_error: None,
|
||||
ssh_prompt: None,
|
||||
@@ -446,7 +486,7 @@ impl DevServerProjects {
|
||||
editor.update(cx, |this, _| {
|
||||
this.set_read_only(true);
|
||||
});
|
||||
self.mode = Mode::CreateDevServer(CreateDevServer {
|
||||
self.mode = Mode::CreateRemoteServer(CreateRemoteServer {
|
||||
address_editor: editor,
|
||||
address_error: None,
|
||||
ssh_prompt: Some(ssh_prompt.clone()),
|
||||
@@ -474,11 +514,12 @@ impl DevServerProjects {
|
||||
return;
|
||||
};
|
||||
|
||||
let nickname = ssh_connection.nickname.clone();
|
||||
let connection_options = ssh_connection.into();
|
||||
workspace.update(cx, |_, cx| {
|
||||
cx.defer(move |workspace, cx| {
|
||||
workspace.toggle_modal(cx, |cx| {
|
||||
SshConnectionModal::new(&connection_options, false, cx)
|
||||
SshConnectionModal::new(&connection_options, nickname, cx)
|
||||
});
|
||||
let prompt = workspace
|
||||
.active_modal::<SshConnectionModal>(cx)
|
||||
@@ -488,18 +529,30 @@ impl DevServerProjects {
|
||||
.clone();
|
||||
|
||||
let connect = connect_over_ssh(
|
||||
connection_options.dev_server_identifier(),
|
||||
connection_options.remote_server_identifier(),
|
||||
connection_options.clone(),
|
||||
prompt,
|
||||
cx,
|
||||
)
|
||||
.prompt_err("Failed to connect", cx, |_, _| None);
|
||||
|
||||
cx.spawn(move |workspace, mut cx| async move {
|
||||
let Some(session) = connect.await else {
|
||||
let session = connect.await;
|
||||
|
||||
workspace
|
||||
.update(&mut cx, |workspace, cx| {
|
||||
if let Some(prompt) = workspace.active_modal::<SshConnectionModal>(cx) {
|
||||
prompt.update(cx, |prompt, cx| prompt.finished(cx))
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
|
||||
let Some(Some(session)) = session else {
|
||||
workspace
|
||||
.update(&mut cx, |workspace, cx| {
|
||||
let weak = cx.view().downgrade();
|
||||
workspace.toggle_modal(cx, |cx| DevServerProjects::new(cx, weak));
|
||||
workspace
|
||||
.toggle_modal(cx, |cx| RemoteServerProjects::new(cx, weak));
|
||||
})
|
||||
.log_err();
|
||||
return;
|
||||
@@ -519,7 +572,7 @@ impl DevServerProjects {
|
||||
cx,
|
||||
);
|
||||
workspace.toggle_modal(cx, |cx| {
|
||||
DevServerProjects::project_picker(
|
||||
RemoteServerProjects::project_picker(
|
||||
ix,
|
||||
connection_options,
|
||||
project,
|
||||
@@ -543,7 +596,7 @@ impl DevServerProjects {
|
||||
self.selectable_items = items;
|
||||
}
|
||||
Mode::ProjectPicker(_) => {}
|
||||
Mode::CreateDevServer(state) => {
|
||||
Mode::CreateRemoteServer(state) => {
|
||||
if let Some(prompt) = state.ssh_prompt.as_ref() {
|
||||
prompt.update(cx, |prompt, cx| {
|
||||
prompt.confirm(cx);
|
||||
@@ -575,8 +628,14 @@ impl DevServerProjects {
|
||||
fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
|
||||
match &self.mode {
|
||||
Mode::Default(_) => cx.emit(DismissEvent),
|
||||
Mode::CreateDevServer(state) if state.ssh_prompt.is_some() => {
|
||||
self.mode = Mode::CreateDevServer(CreateDevServer::new(cx));
|
||||
Mode::CreateRemoteServer(state) if state.ssh_prompt.is_some() => {
|
||||
let new_state = CreateRemoteServer::new(cx);
|
||||
let old_prompt = state.address_editor.read(cx).text(cx);
|
||||
new_state.address_editor.update(cx, |this, cx| {
|
||||
this.set_text(old_prompt, cx);
|
||||
});
|
||||
|
||||
self.mode = Mode::CreateRemoteServer(new_state);
|
||||
self.selectable_items.reset_selection();
|
||||
cx.notify();
|
||||
}
|
||||
@@ -616,7 +675,6 @@ impl DevServerProjects {
|
||||
.child(
|
||||
Label::new(main_label)
|
||||
.size(LabelSize::Small)
|
||||
.weight(FontWeight::SEMIBOLD)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.children(
|
||||
@@ -710,11 +768,13 @@ impl DevServerProjects {
|
||||
let project = project.clone();
|
||||
let server = server.clone();
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
let nickname = server.nickname.clone();
|
||||
let result = open_ssh_project(
|
||||
server.into(),
|
||||
project.paths.into_iter().map(PathBuf::from).collect(),
|
||||
app_state,
|
||||
OpenOptions::default(),
|
||||
nickname,
|
||||
&mut cx,
|
||||
)
|
||||
.await;
|
||||
@@ -818,9 +878,9 @@ impl DevServerProjects {
|
||||
});
|
||||
}
|
||||
|
||||
fn render_create_dev_server(
|
||||
fn render_create_remote_server(
|
||||
&self,
|
||||
state: &CreateDevServer,
|
||||
state: &CreateRemoteServer,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> impl IntoElement {
|
||||
let ssh_prompt = state.ssh_prompt.clone();
|
||||
@@ -834,7 +894,7 @@ impl DevServerProjects {
|
||||
let theme = cx.theme();
|
||||
|
||||
v_flex()
|
||||
.id("create-dev-server")
|
||||
.id("create-remote-server")
|
||||
.overflow_hidden()
|
||||
.size_full()
|
||||
.flex_1()
|
||||
@@ -911,7 +971,8 @@ impl DevServerProjects {
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.py_1()
|
||||
.pb_1()
|
||||
.child(ListSeparator)
|
||||
.child({
|
||||
self.selectable_items.add_item(Box::new({
|
||||
move |this, cx| {
|
||||
@@ -995,7 +1056,7 @@ impl DevServerProjects {
|
||||
})
|
||||
.child({
|
||||
fn remove_ssh_server(
|
||||
dev_servers: View<DevServerProjects>,
|
||||
remote_servers: View<RemoteServerProjects>,
|
||||
index: usize,
|
||||
connection_string: SharedString,
|
||||
cx: &mut WindowContext<'_>,
|
||||
@@ -1010,8 +1071,8 @@ impl DevServerProjects {
|
||||
);
|
||||
|
||||
cx.spawn(|mut cx| async move {
|
||||
if confirmation.await.ok() == Some(0) {
|
||||
dev_servers
|
||||
if confirmation.await.ok() == Some(1) {
|
||||
remote_servers
|
||||
.update(&mut cx, |this, cx| {
|
||||
this.delete_ssh_server(index, cx);
|
||||
this.mode = Mode::default_mode();
|
||||
@@ -1095,7 +1156,13 @@ impl DevServerProjects {
|
||||
}
|
||||
.render(cx),
|
||||
)
|
||||
.child(h_flex().p_2().child(state.editor.clone()))
|
||||
.child(
|
||||
h_flex()
|
||||
.p_2()
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.child(state.editor.clone()),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_default(
|
||||
@@ -1108,21 +1175,21 @@ impl DevServerProjects {
|
||||
.ssh_connections()
|
||||
.collect::<Vec<_>>();
|
||||
self.selectable_items.add_item(Box::new(|this, cx| {
|
||||
this.mode = Mode::CreateDevServer(CreateDevServer::new(cx));
|
||||
this.mode = Mode::CreateRemoteServer(CreateRemoteServer::new(cx));
|
||||
cx.notify();
|
||||
}));
|
||||
|
||||
let is_selected = self.selectable_items.is_selected();
|
||||
|
||||
let connect_button = ListItem::new("register-dev-server-button")
|
||||
let connect_button = ListItem::new("register-remove-server-button")
|
||||
.selected(is_selected)
|
||||
.inset(true)
|
||||
.spacing(ui::ListItemSpacing::Sparse)
|
||||
.start_slot(Icon::new(IconName::Plus).color(Color::Muted))
|
||||
.child(Label::new("Connect New Server"))
|
||||
.on_click(cx.listener(|this, _, cx| {
|
||||
let state = CreateDevServer::new(cx);
|
||||
this.mode = Mode::CreateDevServer(state);
|
||||
let state = CreateRemoteServer::new(cx);
|
||||
this.mode = Mode::CreateRemoteServer(state);
|
||||
|
||||
cx.notify();
|
||||
}));
|
||||
@@ -1138,26 +1205,21 @@ impl DevServerProjects {
|
||||
.size_full()
|
||||
.child(connect_button)
|
||||
.child(
|
||||
h_flex().child(
|
||||
List::new()
|
||||
.empty_message(
|
||||
v_flex()
|
||||
.child(ListSeparator)
|
||||
.child(
|
||||
div().px_3().child(
|
||||
Label::new("No dev servers registered yet.")
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
.into_any_element(),
|
||||
)
|
||||
.children(ssh_connections.iter().cloned().enumerate().map(
|
||||
|(ix, connection)| {
|
||||
self.render_ssh_connection(ix, connection, cx)
|
||||
.into_any_element()
|
||||
},
|
||||
)),
|
||||
),
|
||||
List::new()
|
||||
.empty_message(
|
||||
v_flex()
|
||||
// .child(ListSeparator)
|
||||
.child(div().px_3().child(
|
||||
Label::new("No remote servers registered yet.").color(Color::Muted),
|
||||
))
|
||||
.into_any_element(),
|
||||
)
|
||||
.children(ssh_connections.iter().cloned().enumerate().map(
|
||||
|(ix, connection)| {
|
||||
self.render_ssh_connection(ix, connection, cx)
|
||||
.into_any_element()
|
||||
},
|
||||
)),
|
||||
)
|
||||
.into_any_element();
|
||||
|
||||
@@ -1168,36 +1230,38 @@ impl DevServerProjects {
|
||||
)
|
||||
.section(
|
||||
Section::new().padded(false).child(
|
||||
h_flex()
|
||||
v_flex()
|
||||
.min_h(rems(20.))
|
||||
.group("remote-projects-section")
|
||||
.size_full()
|
||||
.relative()
|
||||
.child(ListSeparator)
|
||||
.child(
|
||||
v_flex().size_full().child(ListSeparator).child(
|
||||
canvas(
|
||||
|bounds, cx| {
|
||||
modal_section.prepaint_as_root(
|
||||
bounds.origin,
|
||||
bounds.size.into(),
|
||||
cx,
|
||||
);
|
||||
modal_section
|
||||
},
|
||||
|_, mut modal_section, cx| {
|
||||
modal_section.paint(cx);
|
||||
},
|
||||
)
|
||||
.size_full(),
|
||||
),
|
||||
canvas(
|
||||
|bounds, cx| {
|
||||
modal_section.prepaint_as_root(
|
||||
bounds.origin,
|
||||
bounds.size.into(),
|
||||
cx,
|
||||
);
|
||||
modal_section
|
||||
},
|
||||
|_, mut modal_section, cx| {
|
||||
modal_section.paint(cx);
|
||||
},
|
||||
)
|
||||
.size_full(),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.visible_on_hover("remote-projects-section")
|
||||
.occlude()
|
||||
.h_full()
|
||||
.absolute()
|
||||
.right_1()
|
||||
.top_1()
|
||||
.bottom_1()
|
||||
.w(px(12.))
|
||||
.right_1()
|
||||
.w(px(8.))
|
||||
.children(Scrollbar::vertical(scroll_state)),
|
||||
),
|
||||
),
|
||||
@@ -1209,23 +1273,27 @@ fn get_text(element: &View<Editor>, cx: &mut WindowContext) -> String {
|
||||
element.read(cx).text(cx).trim().to_string()
|
||||
}
|
||||
|
||||
impl ModalView for DevServerProjects {}
|
||||
impl ModalView for RemoteServerProjects {}
|
||||
|
||||
impl FocusableView for DevServerProjects {
|
||||
fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
impl FocusableView for RemoteServerProjects {
|
||||
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
|
||||
match &self.mode {
|
||||
Mode::ProjectPicker(picker) => picker.focus_handle(cx),
|
||||
_ => self.focus_handle.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<DismissEvent> for DevServerProjects {}
|
||||
impl EventEmitter<DismissEvent> for RemoteServerProjects {}
|
||||
|
||||
impl Render for DevServerProjects {
|
||||
impl Render for RemoteServerProjects {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
self.selectable_items.reset();
|
||||
div()
|
||||
.track_focus(&self.focus_handle)
|
||||
.elevation_3(cx)
|
||||
.key_context("DevServerModal")
|
||||
.w(rems(34.))
|
||||
.key_context("RemoteServerModal")
|
||||
.on_action(cx.listener(Self::cancel))
|
||||
.on_action(cx.listener(Self::confirm))
|
||||
.on_action(cx.listener(Self::prev_item))
|
||||
@@ -1238,16 +1306,15 @@ impl Render for DevServerProjects {
|
||||
cx.emit(DismissEvent)
|
||||
}
|
||||
}))
|
||||
.w(rems(34.))
|
||||
.child(match &self.mode {
|
||||
Mode::Default(state) => self.render_default(state.clone(), cx).into_any_element(),
|
||||
Mode::ViewServerOptions(index, connection) => self
|
||||
.render_view_options(*index, connection.clone(), cx)
|
||||
.into_any_element(),
|
||||
Mode::ProjectPicker(element) => element.clone().into_any_element(),
|
||||
Mode::CreateDevServer(state) => {
|
||||
self.render_create_dev_server(state, cx).into_any_element()
|
||||
}
|
||||
Mode::CreateRemoteServer(state) => self
|
||||
.render_create_remote_server(state, cx)
|
||||
.into_any_element(),
|
||||
Mode::EditNickname(state) => {
|
||||
self.render_edit_nickname(state, cx).into_any_element()
|
||||
}
|
||||
@@ -5,21 +5,23 @@ use auto_update::AutoUpdater;
|
||||
use editor::Editor;
|
||||
use futures::channel::oneshot;
|
||||
use gpui::{
|
||||
percentage, px, Animation, AnimationExt, AnyWindowHandle, AsyncAppContext, DismissEvent,
|
||||
EventEmitter, FocusableView, ParentElement as _, Render, SemanticVersion, SharedString, Task,
|
||||
Transformation, View,
|
||||
percentage, Animation, AnimationExt, AnyWindowHandle, AsyncAppContext, DismissEvent,
|
||||
EventEmitter, FocusableView, ParentElement as _, PromptLevel, Render, SemanticVersion,
|
||||
SharedString, Task, TextStyleRefinement, Transformation, View,
|
||||
};
|
||||
use gpui::{AppContext, Model};
|
||||
|
||||
use language::CursorShape;
|
||||
use markdown::{Markdown, MarkdownStyle};
|
||||
use release_channel::{AppVersion, ReleaseChannel};
|
||||
use remote::{SshConnectionOptions, SshPlatform, SshRemoteClient};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsSources};
|
||||
use theme::ThemeSettings;
|
||||
use ui::{
|
||||
div, h_flex, prelude::*, v_flex, ActiveTheme, Color, Icon, IconName, IconSize,
|
||||
InteractiveElement, IntoElement, Label, LabelCommon, Styled, ViewContext, VisualContext,
|
||||
WindowContext,
|
||||
prelude::*, ActiveTheme, Color, Icon, IconName, IconSize, InteractiveElement, IntoElement,
|
||||
Label, LabelCommon, Styled, ViewContext, VisualContext, WindowContext,
|
||||
};
|
||||
use workspace::{AppState, ModalView, Workspace};
|
||||
|
||||
@@ -49,6 +51,24 @@ impl SshSettings {
|
||||
})
|
||||
.next()
|
||||
}
|
||||
|
||||
pub fn nickname_for(
|
||||
&self,
|
||||
host: &str,
|
||||
port: Option<u16>,
|
||||
user: &Option<String>,
|
||||
) -> Option<SharedString> {
|
||||
self.ssh_connections()
|
||||
.filter_map(|conn| {
|
||||
if conn.host == host && &conn.username == user && conn.port == port {
|
||||
Some(conn.nickname)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.next()
|
||||
.flatten()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
|
||||
@@ -66,6 +86,7 @@ pub struct SshConnection {
|
||||
#[serde(default)]
|
||||
pub args: Vec<String>,
|
||||
}
|
||||
|
||||
impl From<SshConnection> for SshConnectionOptions {
|
||||
fn from(val: SshConnection) -> Self {
|
||||
SshConnectionOptions {
|
||||
@@ -100,46 +121,73 @@ impl Settings for SshSettings {
|
||||
|
||||
pub struct SshPrompt {
|
||||
connection_string: SharedString,
|
||||
nickname: Option<SharedString>,
|
||||
status_message: Option<SharedString>,
|
||||
error_message: Option<SharedString>,
|
||||
prompt: Option<(SharedString, oneshot::Sender<Result<String>>)>,
|
||||
prompt: Option<(View<Markdown>, oneshot::Sender<Result<String>>)>,
|
||||
cancellation: Option<oneshot::Sender<()>>,
|
||||
editor: View<Editor>,
|
||||
}
|
||||
|
||||
pub struct SshConnectionModal {
|
||||
pub(crate) prompt: View<SshPrompt>,
|
||||
is_separate_window: bool,
|
||||
finished: bool,
|
||||
}
|
||||
|
||||
impl SshPrompt {
|
||||
pub(crate) fn new(
|
||||
connection_options: &SshConnectionOptions,
|
||||
nickname: Option<SharedString>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let connection_string = connection_options.connection_string().into();
|
||||
Self {
|
||||
connection_string,
|
||||
status_message: None,
|
||||
error_message: None,
|
||||
prompt: None,
|
||||
nickname,
|
||||
editor: cx.new_view(Editor::single_line),
|
||||
status_message: None,
|
||||
cancellation: None,
|
||||
prompt: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_cancellation_tx(&mut self, tx: oneshot::Sender<()>) {
|
||||
self.cancellation = Some(tx);
|
||||
}
|
||||
|
||||
pub fn set_prompt(
|
||||
&mut self,
|
||||
prompt: String,
|
||||
tx: oneshot::Sender<Result<String>>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let theme = ThemeSettings::get_global(cx);
|
||||
|
||||
let mut text_style = cx.text_style();
|
||||
let refinement = TextStyleRefinement {
|
||||
font_family: Some(theme.buffer_font.family.clone()),
|
||||
font_size: Some(theme.buffer_font_size.into()),
|
||||
color: Some(cx.theme().colors().editor_foreground),
|
||||
background_color: Some(gpui::transparent_black()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
text_style.refine(&refinement);
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
if prompt.contains("yes/no") {
|
||||
editor.set_masked(false, cx);
|
||||
} else {
|
||||
editor.set_masked(true, cx);
|
||||
}
|
||||
editor.set_text_style_refinement(refinement);
|
||||
editor.set_cursor_shape(CursorShape::Block, cx);
|
||||
});
|
||||
self.prompt = Some((prompt.into(), tx));
|
||||
let markdown_style = MarkdownStyle {
|
||||
base_text_style: text_style,
|
||||
selection_background_color: cx.theme().players().local().selection,
|
||||
..Default::default()
|
||||
};
|
||||
let markdown = cx.new_view(|cx| Markdown::new_text(prompt, markdown_style, None, cx, None));
|
||||
self.prompt = Some((markdown, tx));
|
||||
self.status_message.take();
|
||||
cx.focus_view(&self.editor);
|
||||
cx.notify();
|
||||
@@ -150,13 +198,9 @@ impl SshPrompt {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn set_error(&mut self, error_message: String, cx: &mut ViewContext<Self>) {
|
||||
self.error_message = Some(error_message.into());
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn confirm(&mut self, cx: &mut ViewContext<Self>) {
|
||||
if let Some((_, tx)) = self.prompt.take() {
|
||||
self.status_message = Some("Connecting".into());
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
tx.send(Ok(editor.text(cx))).ok();
|
||||
editor.clear(cx);
|
||||
@@ -168,76 +212,57 @@ impl SshPrompt {
|
||||
impl Render for SshPrompt {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let cx = cx.window_context();
|
||||
let theme = cx.theme();
|
||||
|
||||
v_flex()
|
||||
.key_context("PasswordPrompt")
|
||||
.py_2()
|
||||
.px_3()
|
||||
.size_full()
|
||||
.child(
|
||||
h_flex()
|
||||
.p_2()
|
||||
.flex()
|
||||
.child(if self.error_message.is_some() {
|
||||
Icon::new(IconName::XCircle)
|
||||
.size(IconSize::Medium)
|
||||
.color(Color::Error)
|
||||
.into_any_element()
|
||||
} else {
|
||||
Icon::new(IconName::ArrowCircle)
|
||||
.size(IconSize::Medium)
|
||||
.with_animation(
|
||||
"arrow-circle",
|
||||
Animation::new(Duration::from_secs(2)).repeat(),
|
||||
|icon, delta| {
|
||||
icon.transform(Transformation::rotate(percentage(delta)))
|
||||
},
|
||||
)
|
||||
.into_any_element()
|
||||
})
|
||||
.child(
|
||||
div()
|
||||
.ml_1()
|
||||
.text_ellipsis()
|
||||
.overflow_x_hidden()
|
||||
.when_some(self.error_message.as_ref(), |el, error| {
|
||||
el.child(Label::new(format!("{}", error)).size(LabelSize::Small))
|
||||
})
|
||||
.when(
|
||||
self.error_message.is_none() && self.status_message.is_some(),
|
||||
|el| {
|
||||
el.child(
|
||||
Label::new(format!(
|
||||
"{}…",
|
||||
self.status_message.clone().unwrap()
|
||||
))
|
||||
.size(LabelSize::Small),
|
||||
)
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(div().when_some(self.prompt.as_ref(), |el, prompt| {
|
||||
.text_buffer(cx)
|
||||
.when_some(self.status_message.clone(), |el, status_message| {
|
||||
el.child(
|
||||
h_flex()
|
||||
.p_4()
|
||||
.border_t_1()
|
||||
.border_color(theme.colors().border_variant)
|
||||
.font_buffer(cx)
|
||||
.child(Label::new(prompt.0.clone()))
|
||||
.gap_1()
|
||||
.child(
|
||||
Icon::new(IconName::ArrowCircle)
|
||||
.size(IconSize::Medium)
|
||||
.with_animation(
|
||||
"arrow-circle",
|
||||
Animation::new(Duration::from_secs(2)).repeat(),
|
||||
|icon, delta| {
|
||||
icon.transform(Transformation::rotate(percentage(delta)))
|
||||
},
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_ellipsis()
|
||||
.overflow_x_hidden()
|
||||
.child(format!("{}…", status_message)),
|
||||
),
|
||||
)
|
||||
})
|
||||
.when_some(self.prompt.as_ref(), |el, prompt| {
|
||||
el.child(
|
||||
div()
|
||||
.size_full()
|
||||
.overflow_hidden()
|
||||
.child(prompt.0.clone())
|
||||
.child(self.editor.clone()),
|
||||
)
|
||||
}))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl SshConnectionModal {
|
||||
pub fn new(
|
||||
pub(crate) fn new(
|
||||
connection_options: &SshConnectionOptions,
|
||||
is_separate_window: bool,
|
||||
nickname: Option<SharedString>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
Self {
|
||||
prompt: cx.new_view(|cx| SshPrompt::new(connection_options, cx)),
|
||||
is_separate_window,
|
||||
prompt: cx.new_view(|cx| SshPrompt::new(connection_options, nickname, cx)),
|
||||
finished: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -245,11 +270,19 @@ impl SshConnectionModal {
|
||||
self.prompt.update(cx, |prompt, cx| prompt.confirm(cx))
|
||||
}
|
||||
|
||||
fn dismiss(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
|
||||
pub fn finished(&mut self, cx: &mut ViewContext<Self>) {
|
||||
self.finished = true;
|
||||
cx.emit(DismissEvent);
|
||||
if self.is_separate_window {
|
||||
cx.remove_window();
|
||||
}
|
||||
|
||||
fn dismiss(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
|
||||
if let Some(tx) = self
|
||||
.prompt
|
||||
.update(cx, |prompt, _cx| prompt.cancellation.take())
|
||||
{
|
||||
tx.send(()).ok();
|
||||
}
|
||||
self.finished(cx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -272,60 +305,53 @@ impl RenderOnce for SshConnectionHeader {
|
||||
};
|
||||
|
||||
h_flex()
|
||||
.p_1()
|
||||
.px(Spacing::XLarge.rems(cx))
|
||||
.pt(Spacing::Large.rems(cx))
|
||||
.pb(Spacing::Small.rems(cx))
|
||||
.rounded_t_md()
|
||||
.w_full()
|
||||
.gap_2()
|
||||
.justify_center()
|
||||
.border_b_1()
|
||||
.border_color(theme.colors().border_variant)
|
||||
.bg(header_color)
|
||||
.gap_1p5()
|
||||
.child(Icon::new(IconName::Server).size(IconSize::XSmall))
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
Label::new(main_label)
|
||||
.size(ui::LabelSize::Small)
|
||||
.single_line(),
|
||||
)
|
||||
.children(meta_label.map(|label| {
|
||||
Label::new(label)
|
||||
.size(ui::LabelSize::Small)
|
||||
.single_line()
|
||||
.color(Color::Muted)
|
||||
})),
|
||||
.child(Headline::new(main_label).size(HeadlineSize::XSmall))
|
||||
.children(meta_label.map(|label| Label::new(label).color(Color::Muted))),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for SshConnectionModal {
|
||||
fn render(&mut self, cx: &mut ui::ViewContext<Self>) -> impl ui::IntoElement {
|
||||
let nickname = self.prompt.read(cx).nickname.clone();
|
||||
let connection_string = self.prompt.read(cx).connection_string.clone();
|
||||
let theme = cx.theme();
|
||||
|
||||
let theme = cx.theme().clone();
|
||||
let body_color = theme.colors().editor_background;
|
||||
|
||||
v_flex()
|
||||
.elevation_3(cx)
|
||||
.w(rems(34.))
|
||||
.border_1()
|
||||
.border_color(theme.colors().border)
|
||||
.key_context("SshConnectionModal")
|
||||
.track_focus(&self.focus_handle(cx))
|
||||
.on_action(cx.listener(Self::dismiss))
|
||||
.on_action(cx.listener(Self::confirm))
|
||||
.w(px(500.))
|
||||
.border_1()
|
||||
.border_color(theme.colors().border)
|
||||
.child(
|
||||
SshConnectionHeader {
|
||||
connection_string,
|
||||
nickname: None,
|
||||
nickname,
|
||||
}
|
||||
.render(cx),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.rounded_b_md()
|
||||
.bg(body_color)
|
||||
div()
|
||||
.w_full()
|
||||
.rounded_b_lg()
|
||||
.bg(body_color)
|
||||
.border_t_1()
|
||||
.border_color(theme.colors().border_variant)
|
||||
.child(self.prompt.clone()),
|
||||
)
|
||||
}
|
||||
@@ -339,7 +365,15 @@ impl FocusableView for SshConnectionModal {
|
||||
|
||||
impl EventEmitter<DismissEvent> for SshConnectionModal {}
|
||||
|
||||
impl ModalView for SshConnectionModal {}
|
||||
impl ModalView for SshConnectionModal {
|
||||
fn on_before_dismiss(&mut self, _: &mut ViewContext<Self>) -> workspace::DismissDecision {
|
||||
return workspace::DismissDecision::Dismiss(self.finished);
|
||||
}
|
||||
|
||||
fn fade_out_background(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SshClientDelegate {
|
||||
@@ -374,10 +408,6 @@ impl remote::SshClientDelegate for SshClientDelegate {
|
||||
self.update_status(status, cx)
|
||||
}
|
||||
|
||||
fn set_error(&self, error: String, cx: &mut AsyncAppContext) {
|
||||
self.update_error(error, cx)
|
||||
}
|
||||
|
||||
fn get_server_binary(
|
||||
&self,
|
||||
platform: SshPlatform,
|
||||
@@ -419,16 +449,6 @@ impl SshClientDelegate {
|
||||
.ok();
|
||||
}
|
||||
|
||||
fn update_error(&self, error: String, cx: &mut AsyncAppContext) {
|
||||
self.window
|
||||
.update(cx, |_, cx| {
|
||||
self.ui.update(cx, |modal, cx| {
|
||||
modal.set_error(error, cx);
|
||||
})
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
async fn get_server_binary_impl(
|
||||
&self,
|
||||
platform: SshPlatform,
|
||||
@@ -479,7 +499,11 @@ impl SshClientDelegate {
|
||||
use smol::process::{Command, Stdio};
|
||||
|
||||
async fn run_cmd(command: &mut Command) -> Result<()> {
|
||||
let output = command.stderr(Stdio::inherit()).output().await?;
|
||||
let output = command
|
||||
.kill_on_drop(true)
|
||||
.stderr(Stdio::inherit())
|
||||
.output()
|
||||
.await?;
|
||||
if !output.status.success() {
|
||||
Err(anyhow::anyhow!("failed to run command: {:?}", command))?;
|
||||
}
|
||||
@@ -578,13 +602,16 @@ pub fn connect_over_ssh(
|
||||
connection_options: SshConnectionOptions,
|
||||
ui: View<SshPrompt>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<Result<Model<SshRemoteClient>>> {
|
||||
) -> Task<Result<Option<Model<SshRemoteClient>>>> {
|
||||
let window = cx.window_handle();
|
||||
let known_password = connection_options.password.clone();
|
||||
let (tx, rx) = oneshot::channel();
|
||||
ui.update(cx, |ui, _cx| ui.set_cancellation_tx(tx));
|
||||
|
||||
remote::SshRemoteClient::new(
|
||||
unique_identifier,
|
||||
connection_options,
|
||||
rx,
|
||||
Arc::new(SshClientDelegate {
|
||||
window,
|
||||
ui,
|
||||
@@ -599,6 +626,7 @@ pub async fn open_ssh_project(
|
||||
paths: Vec<PathBuf>,
|
||||
app_state: Arc<AppState>,
|
||||
open_options: workspace::OpenOptions,
|
||||
nickname: Option<SharedString>,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
let window = if let Some(window) = open_options.replace_window {
|
||||
@@ -619,45 +647,80 @@ pub async fn open_ssh_project(
|
||||
})?
|
||||
};
|
||||
|
||||
let delegate = window.update(cx, |workspace, cx| {
|
||||
cx.activate_window();
|
||||
workspace.toggle_modal(cx, |cx| {
|
||||
SshConnectionModal::new(&connection_options, true, cx)
|
||||
});
|
||||
let ui = workspace
|
||||
.active_modal::<SshConnectionModal>(cx)
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.prompt
|
||||
.clone();
|
||||
loop {
|
||||
let (cancel_tx, cancel_rx) = oneshot::channel();
|
||||
let delegate = window.update(cx, {
|
||||
let connection_options = connection_options.clone();
|
||||
let nickname = nickname.clone();
|
||||
move |workspace, cx| {
|
||||
cx.activate_window();
|
||||
workspace.toggle_modal(cx, |cx| {
|
||||
SshConnectionModal::new(&connection_options, nickname.clone(), cx)
|
||||
});
|
||||
|
||||
Arc::new(SshClientDelegate {
|
||||
window: cx.window_handle(),
|
||||
ui,
|
||||
known_password: connection_options.password.clone(),
|
||||
})
|
||||
})?;
|
||||
let ui = workspace
|
||||
.active_modal::<SshConnectionModal>(cx)?
|
||||
.read(cx)
|
||||
.prompt
|
||||
.clone();
|
||||
|
||||
let did_open_ssh_project = cx
|
||||
.update(|cx| {
|
||||
workspace::open_ssh_project(
|
||||
window,
|
||||
connection_options,
|
||||
delegate.clone(),
|
||||
app_state,
|
||||
paths,
|
||||
cx,
|
||||
)
|
||||
})?
|
||||
.await;
|
||||
ui.update(cx, |ui, _cx| {
|
||||
ui.set_cancellation_tx(cancel_tx);
|
||||
});
|
||||
|
||||
let did_open_ssh_project = match did_open_ssh_project {
|
||||
Ok(ok) => Ok(ok),
|
||||
Err(e) => {
|
||||
delegate.update_error(e.to_string(), cx);
|
||||
Err(e)
|
||||
Some(Arc::new(SshClientDelegate {
|
||||
window: cx.window_handle(),
|
||||
ui,
|
||||
known_password: connection_options.password.clone(),
|
||||
}))
|
||||
}
|
||||
})?;
|
||||
|
||||
let Some(delegate) = delegate else { break };
|
||||
|
||||
let did_open_ssh_project = cx
|
||||
.update(|cx| {
|
||||
workspace::open_ssh_project(
|
||||
window,
|
||||
connection_options.clone(),
|
||||
cancel_rx,
|
||||
delegate.clone(),
|
||||
app_state.clone(),
|
||||
paths.clone(),
|
||||
cx,
|
||||
)
|
||||
})?
|
||||
.await;
|
||||
|
||||
window
|
||||
.update(cx, |workspace, cx| {
|
||||
if let Some(ui) = workspace.active_modal::<SshConnectionModal>(cx) {
|
||||
ui.update(cx, |modal, cx| modal.finished(cx))
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
|
||||
if let Err(e) = did_open_ssh_project {
|
||||
log::error!("Failed to open project: {:?}", e);
|
||||
let response = window
|
||||
.update(cx, |_, cx| {
|
||||
cx.prompt(
|
||||
PromptLevel::Critical,
|
||||
"Failed to connect over SSH",
|
||||
Some(&e.to_string()),
|
||||
&["Retry", "Ok"],
|
||||
)
|
||||
})?
|
||||
.await;
|
||||
|
||||
if response == Ok(0) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
did_open_ssh_project
|
||||
break;
|
||||
}
|
||||
|
||||
// Already showed the error to the user
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ use futures::{
|
||||
oneshot,
|
||||
},
|
||||
future::BoxFuture,
|
||||
select_biased, AsyncReadExt as _, Future, FutureExt as _, StreamExt as _,
|
||||
select, select_biased, AsyncReadExt as _, Future, FutureExt as _, StreamExt as _,
|
||||
};
|
||||
use gpui::{
|
||||
AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, SemanticVersion, Task,
|
||||
@@ -186,7 +186,7 @@ impl SshConnectionOptions {
|
||||
|
||||
// Uniquely identifies dev server projects on a remote host. Needs to be
|
||||
// stable for the same dev server project.
|
||||
pub fn dev_server_identifier(&self) -> String {
|
||||
pub fn remote_server_identifier(&self) -> String {
|
||||
let mut identifier = format!("dev-server-{:?}", self.host);
|
||||
if let Some(username) = self.username.as_ref() {
|
||||
identifier.push('-');
|
||||
@@ -233,7 +233,6 @@ pub trait SshClientDelegate: Send + Sync {
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> oneshot::Receiver<Result<(PathBuf, SemanticVersion)>>;
|
||||
fn set_status(&self, status: Option<&str>, cx: &mut AsyncAppContext);
|
||||
fn set_error(&self, error_message: String, cx: &mut AsyncAppContext);
|
||||
}
|
||||
|
||||
impl SshSocket {
|
||||
@@ -357,6 +356,10 @@ impl State {
|
||||
matches!(self, Self::ReconnectExhausted { .. })
|
||||
}
|
||||
|
||||
fn is_server_not_running(&self) -> bool {
|
||||
matches!(self, Self::ServerNotRunning)
|
||||
}
|
||||
|
||||
fn is_reconnecting(&self) -> bool {
|
||||
matches!(self, Self::Reconnecting { .. })
|
||||
}
|
||||
@@ -452,55 +455,65 @@ impl SshRemoteClient {
|
||||
pub fn new(
|
||||
unique_identifier: String,
|
||||
connection_options: SshConnectionOptions,
|
||||
cancellation: oneshot::Receiver<()>,
|
||||
delegate: Arc<dyn SshClientDelegate>,
|
||||
cx: &AppContext,
|
||||
) -> Task<Result<Model<Self>>> {
|
||||
) -> Task<Result<Option<Model<Self>>>> {
|
||||
cx.spawn(|mut cx| async move {
|
||||
let (outgoing_tx, outgoing_rx) = mpsc::unbounded::<Envelope>();
|
||||
let (incoming_tx, incoming_rx) = mpsc::unbounded::<Envelope>();
|
||||
let (connection_activity_tx, connection_activity_rx) = mpsc::channel::<()>(1);
|
||||
let success = Box::pin(async move {
|
||||
let (outgoing_tx, outgoing_rx) = mpsc::unbounded::<Envelope>();
|
||||
let (incoming_tx, incoming_rx) = mpsc::unbounded::<Envelope>();
|
||||
let (connection_activity_tx, connection_activity_rx) = mpsc::channel::<()>(1);
|
||||
|
||||
let client =
|
||||
cx.update(|cx| ChannelClient::new(incoming_rx, outgoing_tx, cx, "client"))?;
|
||||
let this = cx.new_model(|_| Self {
|
||||
client: client.clone(),
|
||||
unique_identifier: unique_identifier.clone(),
|
||||
connection_options: connection_options.clone(),
|
||||
state: Arc::new(Mutex::new(Some(State::Connecting))),
|
||||
})?;
|
||||
let client =
|
||||
cx.update(|cx| ChannelClient::new(incoming_rx, outgoing_tx, cx, "client"))?;
|
||||
let this = cx.new_model(|_| Self {
|
||||
client: client.clone(),
|
||||
unique_identifier: unique_identifier.clone(),
|
||||
connection_options: connection_options.clone(),
|
||||
state: Arc::new(Mutex::new(Some(State::Connecting))),
|
||||
})?;
|
||||
|
||||
let (ssh_connection, io_task) = Self::establish_connection(
|
||||
unique_identifier,
|
||||
false,
|
||||
connection_options,
|
||||
incoming_tx,
|
||||
outgoing_rx,
|
||||
connection_activity_tx,
|
||||
delegate.clone(),
|
||||
&mut cx,
|
||||
)
|
||||
.await?;
|
||||
let (ssh_connection, io_task) = Self::establish_connection(
|
||||
unique_identifier,
|
||||
false,
|
||||
connection_options,
|
||||
incoming_tx,
|
||||
outgoing_rx,
|
||||
connection_activity_tx,
|
||||
delegate.clone(),
|
||||
&mut cx,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let multiplex_task = Self::monitor(this.downgrade(), io_task, &cx);
|
||||
let multiplex_task = Self::monitor(this.downgrade(), io_task, &cx);
|
||||
|
||||
if let Err(error) = client.ping(HEARTBEAT_TIMEOUT).await {
|
||||
log::error!("failed to establish connection: {}", error);
|
||||
delegate.set_error(error.to_string(), &mut cx);
|
||||
return Err(error);
|
||||
if let Err(error) = client.ping(HEARTBEAT_TIMEOUT).await {
|
||||
log::error!("failed to establish connection: {}", error);
|
||||
return Err(error);
|
||||
}
|
||||
|
||||
let heartbeat_task =
|
||||
Self::heartbeat(this.downgrade(), connection_activity_rx, &mut cx);
|
||||
|
||||
this.update(&mut cx, |this, _| {
|
||||
*this.state.lock() = Some(State::Connected {
|
||||
ssh_connection,
|
||||
delegate,
|
||||
multiplex_task,
|
||||
heartbeat_task,
|
||||
});
|
||||
})?;
|
||||
|
||||
Ok(Some(this))
|
||||
});
|
||||
|
||||
select! {
|
||||
_ = cancellation.fuse() => {
|
||||
Ok(None)
|
||||
}
|
||||
result = success.fuse() => result
|
||||
}
|
||||
|
||||
let heartbeat_task = Self::heartbeat(this.downgrade(), connection_activity_rx, &mut cx);
|
||||
|
||||
this.update(&mut cx, |this, _| {
|
||||
*this.state.lock() = Some(State::Connected {
|
||||
ssh_connection,
|
||||
delegate,
|
||||
multiplex_task,
|
||||
heartbeat_task,
|
||||
});
|
||||
})?;
|
||||
|
||||
Ok(this)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -702,7 +715,6 @@ impl SshRemoteClient {
|
||||
if this.state_is(State::is_reconnect_failed) {
|
||||
this.reconnect(cx)
|
||||
} else if this.state_is(State::is_reconnect_exhausted) {
|
||||
cx.emit(SshRemoteEvent::Disconnected);
|
||||
Ok(())
|
||||
} else {
|
||||
log::debug!("State has transition from Reconnecting into new state while attempting reconnect.");
|
||||
@@ -740,8 +752,6 @@ impl SshRemoteClient {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
keepalive_timer.set(cx.background_executor().timer(HEARTBEAT_INTERVAL).fuse());
|
||||
|
||||
if missed_heartbeats != 0 {
|
||||
missed_heartbeats = 0;
|
||||
this.update(&mut cx, |this, mut cx| {
|
||||
@@ -783,6 +793,8 @@ impl SshRemoteClient {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
keepalive_timer.set(cx.background_executor().timer(HEARTBEAT_INTERVAL).fuse());
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -872,6 +884,9 @@ impl SshRemoteClient {
|
||||
let len = child_stderr
|
||||
.read(&mut stderr_buffer[stderr_offset..])
|
||||
.await?;
|
||||
if len == 0 {
|
||||
return anyhow::Ok(());
|
||||
}
|
||||
|
||||
stderr_offset += len;
|
||||
let mut start_ix = 0;
|
||||
@@ -908,8 +923,9 @@ impl SshRemoteClient {
|
||||
}
|
||||
};
|
||||
|
||||
let status = ssh_proxy_process.status().await?.code().unwrap_or(1);
|
||||
match result {
|
||||
Ok(_) => Ok(ssh_proxy_process.status().await?.code().unwrap_or(1)),
|
||||
Ok(_) => Ok(status),
|
||||
Err(error) => Err(error),
|
||||
}
|
||||
})
|
||||
@@ -931,7 +947,6 @@ impl SshRemoteClient {
|
||||
log::error!("failed to reconnect because server is not running");
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.set_state(State::ServerNotRunning, cx);
|
||||
cx.emit(SshRemoteEvent::Disconnected);
|
||||
})?;
|
||||
}
|
||||
}
|
||||
@@ -974,7 +989,14 @@ impl SshRemoteClient {
|
||||
|
||||
fn set_state(&self, state: State, cx: &mut ModelContext<Self>) {
|
||||
log::info!("setting state to '{}'", &state);
|
||||
|
||||
let is_reconnect_exhausted = state.is_reconnect_exhausted();
|
||||
let is_server_not_running = state.is_server_not_running();
|
||||
self.state.lock().replace(state);
|
||||
|
||||
if is_reconnect_exhausted || is_server_not_running {
|
||||
cx.emit(SshRemoteEvent::Disconnected);
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -1117,6 +1139,7 @@ impl SshRemoteClient {
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub async fn fake_client(port: u16, client_cx: &mut gpui::TestAppContext) -> Model<Self> {
|
||||
let (_tx, rx) = oneshot::channel();
|
||||
client_cx
|
||||
.update(|cx| {
|
||||
Self::new(
|
||||
@@ -1126,12 +1149,14 @@ impl SshRemoteClient {
|
||||
port: Some(port),
|
||||
..Default::default()
|
||||
},
|
||||
rx,
|
||||
Arc::new(fake::Delegate),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1202,7 +1227,7 @@ impl SshRemoteConnection {
|
||||
use smol::{fs::unix::PermissionsExt as _, net::unix::UnixListener};
|
||||
use util::ResultExt as _;
|
||||
|
||||
delegate.set_status(Some("connecting"), cx);
|
||||
delegate.set_status(Some("Connecting"), cx);
|
||||
|
||||
let url = connection_options.ssh_url();
|
||||
let temp_dir = tempfile::Builder::new()
|
||||
@@ -1301,9 +1326,7 @@ impl SshRemoteConnection {
|
||||
};
|
||||
|
||||
if let Err(e) = result {
|
||||
let error_message = format!("Failed to connect to host: {}.", e);
|
||||
delegate.set_error(error_message, cx);
|
||||
return Err(e);
|
||||
return Err(e.context("Failed to connect to host"));
|
||||
}
|
||||
|
||||
drop(askpass_task);
|
||||
@@ -1317,7 +1340,6 @@ impl SshRemoteConnection {
|
||||
"failed to connect: {}",
|
||||
String::from_utf8_lossy(&output).trim()
|
||||
);
|
||||
delegate.set_error(error_message.clone(), cx);
|
||||
Err(anyhow!(error_message))?;
|
||||
}
|
||||
|
||||
@@ -1606,9 +1628,18 @@ impl ChannelClient {
|
||||
pub fn request<T: RequestMessage>(
|
||||
&self,
|
||||
payload: T,
|
||||
) -> impl 'static + Future<Output = Result<T::Response>> {
|
||||
self.request_internal(payload, true)
|
||||
}
|
||||
|
||||
fn request_internal<T: RequestMessage>(
|
||||
&self,
|
||||
payload: T,
|
||||
use_buffer: bool,
|
||||
) -> impl 'static + Future<Output = Result<T::Response>> {
|
||||
log::debug!("ssh request start. name:{}", T::NAME);
|
||||
let response = self.request_dynamic(payload.into_envelope(0, None, None), T::NAME);
|
||||
let response =
|
||||
self.request_dynamic(payload.into_envelope(0, None, None), T::NAME, use_buffer);
|
||||
async move {
|
||||
let response = response.await?;
|
||||
log::debug!("ssh request finish. name:{}", T::NAME);
|
||||
@@ -1620,7 +1651,9 @@ impl ChannelClient {
|
||||
pub async fn resync(&self, timeout: Duration) -> Result<()> {
|
||||
smol::future::or(
|
||||
async {
|
||||
self.request(proto::FlushBufferedMessages {}).await?;
|
||||
self.request_internal(proto::FlushBufferedMessages {}, false)
|
||||
.await?;
|
||||
|
||||
for envelope in self.buffer.lock().iter() {
|
||||
self.outgoing_tx
|
||||
.lock()
|
||||
@@ -1656,10 +1689,11 @@ impl ChannelClient {
|
||||
self.send_dynamic(payload.into_envelope(0, None, None))
|
||||
}
|
||||
|
||||
pub fn request_dynamic(
|
||||
fn request_dynamic(
|
||||
&self,
|
||||
mut envelope: proto::Envelope,
|
||||
type_name: &'static str,
|
||||
use_buffer: bool,
|
||||
) -> impl 'static + Future<Output = Result<proto::Envelope>> {
|
||||
envelope.id = self.next_message_id.fetch_add(1, SeqCst);
|
||||
let (tx, rx) = oneshot::channel();
|
||||
@@ -1667,7 +1701,11 @@ impl ChannelClient {
|
||||
response_channels_lock.insert(MessageId(envelope.id), tx);
|
||||
drop(response_channels_lock);
|
||||
|
||||
let result = self.send_buffered(envelope);
|
||||
let result = if use_buffer {
|
||||
self.send_buffered(envelope)
|
||||
} else {
|
||||
self.send_unbuffered(envelope)
|
||||
};
|
||||
async move {
|
||||
if let Err(error) = &result {
|
||||
log::error!("failed to send message: {}", error);
|
||||
@@ -1687,7 +1725,7 @@ impl ChannelClient {
|
||||
self.send_buffered(envelope)
|
||||
}
|
||||
|
||||
pub fn send_buffered(&self, mut envelope: proto::Envelope) -> Result<()> {
|
||||
fn send_buffered(&self, mut envelope: proto::Envelope) -> Result<()> {
|
||||
envelope.ack_id = Some(self.max_received.load(SeqCst));
|
||||
self.buffer.lock().push_back(envelope.clone());
|
||||
// ignore errors on send (happen while we're reconnecting)
|
||||
@@ -1695,6 +1733,12 @@ impl ChannelClient {
|
||||
self.outgoing_tx.lock().unbounded_send(envelope).ok();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn send_unbuffered(&self, mut envelope: proto::Envelope) -> Result<()> {
|
||||
envelope.ack_id = Some(self.max_received.load(SeqCst));
|
||||
self.outgoing_tx.lock().unbounded_send(envelope).ok();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl ProtoClient for ChannelClient {
|
||||
@@ -1703,7 +1747,7 @@ impl ProtoClient for ChannelClient {
|
||||
envelope: proto::Envelope,
|
||||
request_type: &'static str,
|
||||
) -> BoxFuture<'static, Result<proto::Envelope>> {
|
||||
self.request_dynamic(envelope, request_type).boxed()
|
||||
self.request_dynamic(envelope, request_type, true).boxed()
|
||||
}
|
||||
|
||||
fn send(&self, envelope: proto::Envelope, _message_type: &'static str) -> Result<()> {
|
||||
@@ -1862,8 +1906,5 @@ mod fake {
|
||||
fn set_status(&self, _: Option<&str>, _: &mut AsyncAppContext) {
|
||||
unreachable!()
|
||||
}
|
||||
fn set_error(&self, _: String, _: &mut AsyncAppContext) {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,10 @@ smol.workspace = true
|
||||
util.workspace = true
|
||||
worktree.workspace = true
|
||||
|
||||
[target.'cfg(not(windows))'.dependencies]
|
||||
fork.workspace = true
|
||||
libc.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
client = { workspace = true, features = ["test-support"] }
|
||||
clock = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -479,7 +479,19 @@ async fn test_remote_reload(cx: &mut TestAppContext, server_cx: &mut TestAppCont
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
fs.save(
|
||||
&PathBuf::from("/code/project1/src/lib.rs"),
|
||||
&("bangles".to_string().into()),
|
||||
LineEnding::Unix,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
assert_eq!(buffer.text(), "bangles");
|
||||
buffer.edit([(0..0, "a")], None, cx);
|
||||
});
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ use smol::io::AsyncReadExt;
|
||||
|
||||
use smol::Async;
|
||||
use smol::{net::unix::UnixListener, stream::StreamExt as _};
|
||||
use std::ops::ControlFlow;
|
||||
use std::{
|
||||
io::Write,
|
||||
mem,
|
||||
@@ -305,10 +306,15 @@ pub fn execute_run(
|
||||
stdout_socket: PathBuf,
|
||||
stderr_socket: PathBuf,
|
||||
) -> Result<()> {
|
||||
let log_rx = init_logging_server(log_file)?;
|
||||
init_panic_hook();
|
||||
init_paths()?;
|
||||
|
||||
match daemonize()? {
|
||||
ControlFlow::Break(_) => return Ok(()),
|
||||
ControlFlow::Continue(_) => {}
|
||||
}
|
||||
|
||||
init_panic_hook();
|
||||
let log_rx = init_logging_server(log_file)?;
|
||||
log::info!(
|
||||
"starting up. pid_file: {:?}, stdin_socket: {:?}, stdout_socket: {:?}, stderr_socket: {:?}",
|
||||
pid_file,
|
||||
@@ -322,8 +328,6 @@ pub fn execute_run(
|
||||
|
||||
let listeners = ServerListeners::new(stdin_socket, stdout_socket, stderr_socket)?;
|
||||
|
||||
log::info!("starting headless gpui app");
|
||||
|
||||
let git_hosting_provider_registry = Arc::new(GitHostingProviderRegistry::new());
|
||||
gpui::App::headless().run(move |cx| {
|
||||
settings::init(cx);
|
||||
@@ -471,19 +475,13 @@ pub fn execute_proxy(identifier: String, is_reconnecting: bool) -> Result<()> {
|
||||
|
||||
if let Err(forwarding_result) = smol::block_on(async move {
|
||||
futures::select! {
|
||||
result = stdin_task.fuse() => result,
|
||||
result = stdout_task.fuse() => result,
|
||||
result = stderr_task.fuse() => result,
|
||||
result = stdin_task.fuse() => result.context("stdin_task failed"),
|
||||
result = stdout_task.fuse() => result.context("stdout_task failed"),
|
||||
result = stderr_task.fuse() => result.context("stderr_task failed"),
|
||||
}
|
||||
}) {
|
||||
if let Some(error) = forwarding_result.downcast_ref::<std::io::Error>() {
|
||||
if error.kind() == std::io::ErrorKind::UnexpectedEof {
|
||||
log::error!("connection to server closed due to unexpected EOF");
|
||||
return Err(anyhow!("connection to server closed"));
|
||||
}
|
||||
}
|
||||
log::error!(
|
||||
"failed to forward messages: {:?}, terminating...",
|
||||
"encountered error while forwarding messages: {:?}, terminating...",
|
||||
forwarding_result
|
||||
);
|
||||
return Err(forwarding_result);
|
||||
@@ -523,7 +521,8 @@ fn spawn_server(paths: &ServerPaths) -> Result<()> {
|
||||
}
|
||||
|
||||
let binary_name = std::env::current_exe()?;
|
||||
let server_process = smol::process::Command::new(binary_name)
|
||||
let mut server_process = std::process::Command::new(binary_name);
|
||||
server_process
|
||||
.arg("run")
|
||||
.arg("--log-file")
|
||||
.arg(&paths.log_file)
|
||||
@@ -534,12 +533,14 @@ fn spawn_server(paths: &ServerPaths) -> Result<()> {
|
||||
.arg("--stdout-socket")
|
||||
.arg(&paths.stdout_socket)
|
||||
.arg("--stderr-socket")
|
||||
.arg(&paths.stderr_socket)
|
||||
.spawn()?;
|
||||
.arg(&paths.stderr_socket);
|
||||
|
||||
log::info!(
|
||||
"proxy spawned server process. PID: {:?}",
|
||||
server_process.id()
|
||||
let status = server_process
|
||||
.status()
|
||||
.context("failed to launch server process")?;
|
||||
anyhow::ensure!(
|
||||
status.success(),
|
||||
"failed to launch and detach server process"
|
||||
);
|
||||
|
||||
let mut total_time_waited = std::time::Duration::from_secs(0);
|
||||
@@ -745,3 +746,43 @@ fn read_proxy_settings(cx: &mut ModelContext<'_, HeadlessProject>) -> Option<Uri
|
||||
.or_else(read_proxy_from_env);
|
||||
proxy_url
|
||||
}
|
||||
|
||||
fn daemonize() -> Result<ControlFlow<()>> {
|
||||
match fork::fork().map_err(|e| anyhow::anyhow!("failed to call fork with error code {}", e))? {
|
||||
fork::Fork::Parent(_) => {
|
||||
return Ok(ControlFlow::Break(()));
|
||||
}
|
||||
fork::Fork::Child => {}
|
||||
}
|
||||
|
||||
// Once we've detached from the parent, we want to close stdout/stderr/stdin
|
||||
// so that the outer SSH process is not attached to us in any way anymore.
|
||||
unsafe { redirect_standard_streams() }?;
|
||||
|
||||
Ok(ControlFlow::Continue(()))
|
||||
}
|
||||
|
||||
unsafe fn redirect_standard_streams() -> Result<()> {
|
||||
let devnull_fd = libc::open(b"/dev/null\0" as *const [u8; 10] as _, libc::O_RDWR);
|
||||
anyhow::ensure!(devnull_fd != -1, "failed to open /dev/null");
|
||||
|
||||
let process_stdio = |name, fd| {
|
||||
let reopened_fd = libc::dup2(devnull_fd, fd);
|
||||
anyhow::ensure!(
|
||||
reopened_fd != -1,
|
||||
format!("failed to redirect {} to /dev/null", name)
|
||||
);
|
||||
Ok(())
|
||||
};
|
||||
|
||||
process_stdio("stdin", libc::STDIN_FILENO)?;
|
||||
process_stdio("stdout", libc::STDOUT_FILENO)?;
|
||||
process_stdio("stderr", libc::STDERR_FILENO)?;
|
||||
|
||||
anyhow::ensure!(
|
||||
libc::close(devnull_fd) != -1,
|
||||
"failed to close /dev/null fd after redirecting"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ use client::telemetry::Telemetry;
|
||||
use collections::{HashMap, HashSet};
|
||||
use editor::{
|
||||
display_map::{
|
||||
BlockContext, BlockDisposition, BlockId, BlockProperties, BlockStyle, CustomBlockId,
|
||||
BlockContext, BlockId, BlockPlacement, BlockProperties, BlockStyle, CustomBlockId,
|
||||
RenderBlock,
|
||||
},
|
||||
scroll::Autoscroll,
|
||||
@@ -90,12 +90,11 @@ impl EditorBlock {
|
||||
|
||||
let invalidation_anchor = buffer.read(cx).read(cx).anchor_before(next_row_start);
|
||||
let block = BlockProperties {
|
||||
position: code_range.end,
|
||||
placement: BlockPlacement::Below(code_range.end),
|
||||
// Take up at least one height for status, allow the editor to determine the real height based on the content from render
|
||||
height: 1,
|
||||
style: BlockStyle::Sticky,
|
||||
render: Self::create_output_area_renderer(execution_view.clone(), on_close.clone()),
|
||||
disposition: BlockDisposition::Below,
|
||||
priority: 0,
|
||||
};
|
||||
|
||||
|
||||
@@ -968,7 +968,7 @@ impl SettingsStore {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn editorconfg_properties(
|
||||
pub fn editorconfig_properties(
|
||||
&self,
|
||||
for_worktree: WorktreeId,
|
||||
for_path: &Path,
|
||||
|
||||
@@ -89,6 +89,7 @@ impl TerminalPanel {
|
||||
pane.display_nav_history_buttons(None);
|
||||
pane.set_should_display_tab_bar(|_| true);
|
||||
|
||||
let is_local = workspace.project().read(cx).is_local();
|
||||
let workspace = workspace.weak_handle();
|
||||
pane.set_custom_drop_handle(cx, move |pane, dropped_item, cx| {
|
||||
if let Some(tab) = dropped_item.downcast_ref::<DraggedTab>() {
|
||||
@@ -128,8 +129,10 @@ impl TerminalPanel {
|
||||
{
|
||||
add_paths_to_terminal(pane, &[entry_path], cx);
|
||||
}
|
||||
} else if let Some(paths) = dropped_item.downcast_ref::<ExternalPaths>() {
|
||||
add_paths_to_terminal(pane, paths.paths(), cx);
|
||||
} else if is_local {
|
||||
if let Some(paths) = dropped_item.downcast_ref::<ExternalPaths>() {
|
||||
add_paths_to_terminal(pane, paths.paths(), cx);
|
||||
}
|
||||
}
|
||||
|
||||
ControlFlow::Break(())
|
||||
|
||||
@@ -44,6 +44,7 @@ recent_projects.workspace = true
|
||||
remote.workspace = true
|
||||
rpc.workspace = true
|
||||
serde.workspace = true
|
||||
settings.workspace = true
|
||||
smallvec.workspace = true
|
||||
story = { workspace = true, optional = true }
|
||||
theme.workspace = true
|
||||
|
||||
@@ -18,8 +18,10 @@ use gpui::{
|
||||
StatefulInteractiveElement, Styled, Subscription, View, ViewContext, VisualContext, WeakView,
|
||||
};
|
||||
use project::{Project, RepositoryEntry};
|
||||
use recent_projects::{OpenRemote, RecentProjects};
|
||||
use recent_projects::{OpenRemote, RecentProjects, SshSettings};
|
||||
use remote::SshConnectionOptions;
|
||||
use rpc::proto::{self, DevServerStatus};
|
||||
use settings::Settings;
|
||||
use smallvec::SmallVec;
|
||||
use std::sync::Arc;
|
||||
use theme::ActiveTheme;
|
||||
@@ -27,7 +29,7 @@ use ui::{
|
||||
h_flex, prelude::*, Avatar, Button, ButtonLike, ButtonStyle, ContextMenu, Icon, IconName,
|
||||
IconSize, IconWithIndicator, Indicator, PopoverMenu, Tooltip,
|
||||
};
|
||||
use util::ResultExt;
|
||||
use util::{maybe, ResultExt};
|
||||
use vcs_menu::{BranchList, OpenRecent as ToggleVcsMenu};
|
||||
use workspace::{notifications::NotifyResultExt, Workspace};
|
||||
|
||||
@@ -263,7 +265,18 @@ impl TitleBar {
|
||||
}
|
||||
|
||||
fn render_ssh_project_host(&self, cx: &mut ViewContext<Self>) -> Option<AnyElement> {
|
||||
let host = self.project.read(cx).ssh_connection_string(cx)?;
|
||||
let options = self.project.read(cx).ssh_connection_options(cx)?;
|
||||
let host: SharedString = options.connection_string().into();
|
||||
|
||||
let nickname = maybe!({
|
||||
SshSettings::get_global(cx)
|
||||
.ssh_connections
|
||||
.as_ref()?
|
||||
.into_iter()
|
||||
.find(|connection| SshConnectionOptions::from((*connection).clone()) == options)
|
||||
.and_then(|connection| connection.nickname.clone())
|
||||
})
|
||||
.unwrap_or_else(|| host.clone());
|
||||
|
||||
let (indicator_color, meta) = match self.project.read(cx).ssh_connection_state(cx)? {
|
||||
remote::ConnectionState::Connecting => (Color::Info, format!("Connecting to: {host}")),
|
||||
@@ -295,12 +308,22 @@ impl TitleBar {
|
||||
ButtonLike::new("ssh-server-icon")
|
||||
.child(
|
||||
IconWithIndicator::new(
|
||||
Icon::new(IconName::Server).color(icon_color),
|
||||
Icon::new(IconName::Server)
|
||||
.size(IconSize::XSmall)
|
||||
.color(icon_color),
|
||||
Some(Indicator::dot().color(indicator_color)),
|
||||
)
|
||||
.indicator_border_color(Some(cx.theme().colors().title_bar_background))
|
||||
.into_any_element(),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.max_w_32()
|
||||
.overflow_hidden()
|
||||
.truncate()
|
||||
.text_ellipsis()
|
||||
.child(Label::new(nickname.clone()).size(LabelSize::Small)),
|
||||
)
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::with_meta("Remote Project", Some(&OpenRemote), meta.clone(), cx)
|
||||
})
|
||||
@@ -339,7 +362,7 @@ impl TitleBar {
|
||||
.tooltip(move |cx| Tooltip::text("Project is hosted on a dev server", cx))
|
||||
.on_click(cx.listener(|this, _, cx| {
|
||||
if let Some(workspace) = this.workspace.upgrade() {
|
||||
recent_projects::DevServerProjects::open(workspace, cx)
|
||||
recent_projects::RemoteServerProjects::open(workspace, cx)
|
||||
}
|
||||
}))
|
||||
.into_any_element(),
|
||||
|
||||
@@ -79,7 +79,7 @@ pub trait StyledTypography: Styled + Sized {
|
||||
///
|
||||
/// This should only be used for text that is displayed in a buffer,
|
||||
/// or other places that text needs to match the user's buffer font size.
|
||||
fn text_buffer(self, cx: &mut WindowContext) -> Self {
|
||||
fn text_buffer(self, cx: &WindowContext) -> Self {
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
self.text_size(settings.buffer_font_size(cx))
|
||||
}
|
||||
|
||||
@@ -17,8 +17,6 @@ gpui.workspace = true
|
||||
settings.workspace = true
|
||||
theme.workspace = true
|
||||
ui.workspace = true
|
||||
workspace.workspace = true
|
||||
menu.workspace = true
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#![allow(unused, dead_code)]
|
||||
|
||||
//! # UI – Text Field
|
||||
//!
|
||||
//! This crate provides a text field component that can be used to create text fields like search inputs, form fields, etc.
|
||||
@@ -7,14 +5,11 @@
|
||||
//! It can't be located in the `ui` crate because it depends on `editor`.
|
||||
//!
|
||||
|
||||
use std::default;
|
||||
|
||||
use editor::*;
|
||||
use gpui::*;
|
||||
use settings::Settings;
|
||||
use theme::ThemeSettings;
|
||||
use ui::{List, *};
|
||||
use workspace::{ModalView, Workspace};
|
||||
use ui::*;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum FieldLabelLayout {
|
||||
@@ -192,243 +187,3 @@ impl Render for TextField {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
|
||||
actions!(quick_commit, [ToggleStageAll]);
|
||||
|
||||
pub const MODAL_WIDTH: f32 = 700.0;
|
||||
pub const MODAL_HEIGHT: f32 = 300.0;
|
||||
|
||||
fn test_files() -> Vec<ChangedFile> {
|
||||
vec![
|
||||
ChangedFile {
|
||||
id: 0,
|
||||
state: FileVCSState::Modified,
|
||||
file_name: "file1.txt".into(),
|
||||
file_path: "/path/to/file1.txt".into(),
|
||||
},
|
||||
ChangedFile {
|
||||
id: 1,
|
||||
state: FileVCSState::Deleted,
|
||||
file_name: "file2.txt".into(),
|
||||
file_path: "/path/to/file2.txt".into(),
|
||||
},
|
||||
ChangedFile {
|
||||
id: 2,
|
||||
state: FileVCSState::Created,
|
||||
file_name: "file3.txt".into(),
|
||||
file_path: "/path/to/file3.txt".into(),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
|
||||
enum FileVCSState {
|
||||
Deleted,
|
||||
Modified,
|
||||
Created,
|
||||
}
|
||||
|
||||
struct ChangedFileId(usize);
|
||||
|
||||
impl ChangedFileId {
|
||||
fn new(id: usize) -> Self {
|
||||
Self(id)
|
||||
}
|
||||
}
|
||||
|
||||
// placeholder for ui
|
||||
#[derive(Debug, Clone)]
|
||||
struct ChangedFile {
|
||||
id: usize,
|
||||
state: FileVCSState,
|
||||
file_name: SharedString,
|
||||
file_path: SharedString,
|
||||
}
|
||||
|
||||
struct QuickCommitState {
|
||||
placeholder_text: SharedString,
|
||||
tracked_files: Vec<ChangedFile>,
|
||||
staged_files: Vec<usize>,
|
||||
active_participant_handles: Vec<SharedString>,
|
||||
editor: View<Editor>,
|
||||
workspace: WeakView<Workspace>,
|
||||
}
|
||||
|
||||
impl QuickCommitState {
|
||||
fn init(
|
||||
editor: View<Editor>,
|
||||
workspace: WeakView<Workspace>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Self {
|
||||
let workspace = workspace.clone();
|
||||
|
||||
Self {
|
||||
placeholder_text: "Add a message".into(),
|
||||
tracked_files: Default::default(),
|
||||
staged_files: Default::default(),
|
||||
active_participant_handles: Default::default(),
|
||||
editor,
|
||||
workspace,
|
||||
}
|
||||
}
|
||||
|
||||
fn stage_state(&self) -> Selection {
|
||||
let staged_files = self.staged_files.clone();
|
||||
let tracked_files = self.tracked_files.clone();
|
||||
|
||||
if staged_files.len() == tracked_files.len() {
|
||||
Selection::Selected
|
||||
} else if staged_files.is_empty() {
|
||||
Selection::Unselected
|
||||
} else {
|
||||
Selection::Indeterminate
|
||||
}
|
||||
}
|
||||
|
||||
fn stage_all(&mut self) -> &mut Self {
|
||||
let tracked_files = self.tracked_files.clone();
|
||||
|
||||
self.staged_files = tracked_files.iter().map(|file| file.id).collect();
|
||||
self
|
||||
}
|
||||
|
||||
fn toggle_stage_all(&mut self) {
|
||||
let stage_state = self.stage_state();
|
||||
|
||||
let staged_files = self.staged_files.clone();
|
||||
let tracked_files = self.tracked_files.clone();
|
||||
|
||||
match stage_state {
|
||||
Selection::Selected => {
|
||||
self.staged_files.clear();
|
||||
}
|
||||
Selection::Unselected | Selection::Indeterminate => {
|
||||
self.stage_all();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle_file_staged(&mut self, file_id: usize) {
|
||||
if let Some(pos) = self.staged_files.iter().position() {
|
||||
self.staged_files.swap_remove(pos);
|
||||
} else {
|
||||
self.staged_files.push(file_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct QuickCommit {
|
||||
state: Model<QuickCommitState>,
|
||||
}
|
||||
|
||||
impl QuickCommit {
|
||||
pub fn init(workspace: WeakView<Workspace>, cx: &mut WindowContext) -> View<Self> {
|
||||
let editor = cx.new_view(|cx| {
|
||||
let mut editor = Editor::multi_line(cx);
|
||||
editor.set_show_gutter(false, cx);
|
||||
editor
|
||||
});
|
||||
|
||||
cx.new_view(|cx| {
|
||||
let state = cx
|
||||
.new_model(move |cx| QuickCommitState::init(editor.clone(), workspace.clone(), cx));
|
||||
|
||||
Self { state }
|
||||
})
|
||||
}
|
||||
|
||||
fn stage_state(&self, cx: &ViewContext<Self>) -> Selection {
|
||||
self.state.read(cx).stage_state()
|
||||
}
|
||||
fn toggle_stage_all(&mut self, _: &ToggleStageAll, cx: &mut ViewContext<Self>) {
|
||||
self.state.update(cx, |state, _| state.toggle_stage_all());
|
||||
}
|
||||
|
||||
fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
|
||||
cx.emit(DismissEvent)
|
||||
}
|
||||
|
||||
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
|
||||
self.state.read(cx).editor.focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl QuickCommit {
|
||||
fn render_file_list(&mut self, cx: &mut ViewContext<Self>) -> List {
|
||||
List::new().empty_message("No changes")
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for QuickCommit {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let staged_files = self.state.read(cx).staged_files.clone();
|
||||
let total_tracked_files = self.state.read(cx).tracked_files.clone();
|
||||
let staged_state = self.stage_state(cx);
|
||||
|
||||
h_flex()
|
||||
.id("quick_commit_modal")
|
||||
.key_context("quick_commit")
|
||||
.track_focus(&self.focus_handle(cx))
|
||||
.on_action(cx.listener(Self::cancel))
|
||||
.occlude()
|
||||
.h(px(MODAL_HEIGHT))
|
||||
.w(px(MODAL_WIDTH))
|
||||
.child(
|
||||
// commit editor
|
||||
div()
|
||||
.h_full()
|
||||
.flex_1()
|
||||
// .child(self.editor.clone())
|
||||
.child(
|
||||
div()
|
||||
.absolute()
|
||||
.bottom_2()
|
||||
.right_2()
|
||||
.child(Button::new("submit_commit", "Commit")),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
// file list
|
||||
div()
|
||||
.w(relative(0.42))
|
||||
.h_full()
|
||||
.border_l_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
// sticky header
|
||||
.child(
|
||||
h_flex()
|
||||
.h_10()
|
||||
.w_full()
|
||||
.child(Label::new(format!(
|
||||
"Staged Files: {}/{}",
|
||||
staged_files.len(),
|
||||
total_tracked_files.len()
|
||||
)))
|
||||
.child(Checkbox::new("toggle-stage-all", staged_state).on_click(
|
||||
|_, cx| {
|
||||
cx.dispatch_action(ToggleStageAll.boxed_clone());
|
||||
},
|
||||
)),
|
||||
)
|
||||
// file list
|
||||
.child(self.render_file_list(cx)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<DismissEvent> for QuickCommit {}
|
||||
|
||||
impl FocusableView for QuickCommit {
|
||||
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
|
||||
// TODO: Not sure this is right
|
||||
self.focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl ModalView for QuickCommit {
|
||||
fn fade_out_background(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,6 +145,7 @@ mod test {
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
async fn test_replace_mode(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
|
||||
@@ -2570,6 +2570,7 @@ impl Render for Pane {
|
||||
|
||||
let should_display_tab_bar = self.should_display_tab_bar.clone();
|
||||
let display_tab_bar = should_display_tab_bar(cx);
|
||||
let is_local = self.project.read(cx).is_local();
|
||||
|
||||
v_flex()
|
||||
.key_context(key_context)
|
||||
@@ -2697,7 +2698,9 @@ impl Render for Pane {
|
||||
.group("")
|
||||
.on_drag_move::<DraggedTab>(cx.listener(Self::handle_drag_move))
|
||||
.on_drag_move::<DraggedSelection>(cx.listener(Self::handle_drag_move))
|
||||
.on_drag_move::<ExternalPaths>(cx.listener(Self::handle_drag_move))
|
||||
.when(is_local, |div| {
|
||||
div.on_drag_move::<ExternalPaths>(cx.listener(Self::handle_drag_move))
|
||||
})
|
||||
.map(|div| {
|
||||
if let Some(item) = self.active_item() {
|
||||
div.v_flex()
|
||||
@@ -2723,7 +2726,9 @@ impl Render for Pane {
|
||||
.bg(cx.theme().colors().drop_target_background)
|
||||
.group_drag_over::<DraggedTab>("", |style| style.visible())
|
||||
.group_drag_over::<DraggedSelection>("", |style| style.visible())
|
||||
.group_drag_over::<ExternalPaths>("", |style| style.visible())
|
||||
.when(is_local, |div| {
|
||||
div.group_drag_over::<ExternalPaths>("", |style| style.visible())
|
||||
})
|
||||
.when_some(self.can_drop_predicate.clone(), |this, p| {
|
||||
this.can_drop(move |a, cx| p(a, cx))
|
||||
})
|
||||
|
||||
@@ -31,7 +31,7 @@ use futures::{
|
||||
};
|
||||
use gpui::{
|
||||
action_as, actions, canvas, impl_action_as, impl_actions, point, relative, size,
|
||||
transparent_black, Action, AnyElement, AnyView, AnyWeakView, AppContext, AsyncAppContext,
|
||||
transparent_black, Action, AnyView, AnyWeakView, AppContext, AsyncAppContext,
|
||||
AsyncWindowContext, Bounds, CursorStyle, Decorations, DragMoveEvent, Entity as _, EntityId,
|
||||
EventEmitter, Flatten, FocusHandle, FocusableView, Global, Hsla, KeyContext, Keystroke,
|
||||
ManagedView, Model, ModelContext, MouseButton, PathPromptOptions, Point, PromptLevel, Render,
|
||||
@@ -762,8 +762,6 @@ pub struct Workspace {
|
||||
bounds_save_task_queued: Option<Task<()>>,
|
||||
on_prompt_for_new_path: Option<PromptForNewPath>,
|
||||
on_prompt_for_open_path: Option<PromptForOpenPath>,
|
||||
render_disconnected_overlay:
|
||||
Option<Box<dyn Fn(&mut Self, &mut ViewContext<Self>) -> AnyElement>>,
|
||||
serializable_items_tx: UnboundedSender<Box<dyn SerializableItemHandle>>,
|
||||
serialized_ssh_project: Option<SerializedSshProject>,
|
||||
_items_serializer: Task<Result<()>>,
|
||||
@@ -1067,7 +1065,6 @@ impl Workspace {
|
||||
bounds_save_task_queued: None,
|
||||
on_prompt_for_new_path: None,
|
||||
on_prompt_for_open_path: None,
|
||||
render_disconnected_overlay: None,
|
||||
serializable_items_tx,
|
||||
_items_serializer,
|
||||
session_id: Some(session_id),
|
||||
@@ -1472,13 +1469,6 @@ impl Workspace {
|
||||
self.serialized_ssh_project = Some(serialized_ssh_project);
|
||||
}
|
||||
|
||||
pub fn set_render_disconnected_overlay(
|
||||
&mut self,
|
||||
render: impl Fn(&mut Self, &mut ViewContext<Self>) -> AnyElement + 'static,
|
||||
) {
|
||||
self.render_disconnected_overlay = Some(Box::new(render))
|
||||
}
|
||||
|
||||
pub fn prompt_for_open_path(
|
||||
&mut self,
|
||||
path_prompt_options: PathPromptOptions,
|
||||
@@ -1528,7 +1518,7 @@ impl Workspace {
|
||||
&mut self,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> oneshot::Receiver<Option<ProjectPath>> {
|
||||
if self.project.read(cx).is_via_collab()
|
||||
if (self.project.read(cx).is_via_collab() || self.project.read(cx).is_via_ssh())
|
||||
|| !WorkspaceSettings::get_global(cx).use_system_path_prompts
|
||||
{
|
||||
let prompt = self.on_prompt_for_new_path.take().unwrap();
|
||||
@@ -4746,130 +4736,158 @@ impl Render for Workspace {
|
||||
.children(self.titlebar_item.clone())
|
||||
.child(
|
||||
div()
|
||||
.id("workspace")
|
||||
.bg(colors.background)
|
||||
.size_full()
|
||||
.relative()
|
||||
.flex_1()
|
||||
.w_full()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.overflow_hidden()
|
||||
.border_t_1()
|
||||
.border_b_1()
|
||||
.border_color(colors.border)
|
||||
.child({
|
||||
let this = cx.view().clone();
|
||||
canvas(
|
||||
move |bounds, cx| this.update(cx, |this, _cx| this.bounds = bounds),
|
||||
|_, _, _| {},
|
||||
)
|
||||
.absolute()
|
||||
.size_full()
|
||||
})
|
||||
.when(self.zoomed.is_none(), |this| {
|
||||
this.on_drag_move(cx.listener(
|
||||
|workspace, e: &DragMoveEvent<DraggedDock>, cx| match e.drag(cx).0 {
|
||||
DockPosition::Left => {
|
||||
let size = e.event.position.x - workspace.bounds.left();
|
||||
workspace.left_dock.update(cx, |left_dock, cx| {
|
||||
left_dock.resize_active_panel(Some(size), cx);
|
||||
});
|
||||
}
|
||||
DockPosition::Right => {
|
||||
let size = workspace.bounds.right() - e.event.position.x;
|
||||
workspace.right_dock.update(cx, |right_dock, cx| {
|
||||
right_dock.resize_active_panel(Some(size), cx);
|
||||
});
|
||||
}
|
||||
DockPosition::Bottom => {
|
||||
let size = workspace.bounds.bottom() - e.event.position.y;
|
||||
workspace.bottom_dock.update(cx, |bottom_dock, cx| {
|
||||
bottom_dock.resize_active_panel(Some(size), cx);
|
||||
});
|
||||
}
|
||||
},
|
||||
))
|
||||
})
|
||||
.child(
|
||||
div()
|
||||
.id("workspace")
|
||||
.bg(colors.background)
|
||||
.relative()
|
||||
.flex_1()
|
||||
.w_full()
|
||||
.flex()
|
||||
.flex_row()
|
||||
.h_full()
|
||||
// Left Dock
|
||||
.children(self.render_dock(DockPosition::Left, &self.left_dock, cx))
|
||||
// Panes
|
||||
.flex_col()
|
||||
.overflow_hidden()
|
||||
.border_t_1()
|
||||
.border_b_1()
|
||||
.border_color(colors.border)
|
||||
.child({
|
||||
let this = cx.view().clone();
|
||||
canvas(
|
||||
move |bounds, cx| {
|
||||
this.update(cx, |this, _cx| this.bounds = bounds)
|
||||
},
|
||||
|_, _, _| {},
|
||||
)
|
||||
.absolute()
|
||||
.size_full()
|
||||
})
|
||||
.when(self.zoomed.is_none(), |this| {
|
||||
this.on_drag_move(cx.listener(
|
||||
|workspace, e: &DragMoveEvent<DraggedDock>, cx| {
|
||||
match e.drag(cx).0 {
|
||||
DockPosition::Left => {
|
||||
let size = e.event.position.x
|
||||
- workspace.bounds.left();
|
||||
workspace.left_dock.update(
|
||||
cx,
|
||||
|left_dock, cx| {
|
||||
left_dock.resize_active_panel(
|
||||
Some(size),
|
||||
cx,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
DockPosition::Right => {
|
||||
let size = workspace.bounds.right()
|
||||
- e.event.position.x;
|
||||
workspace.right_dock.update(
|
||||
cx,
|
||||
|right_dock, cx| {
|
||||
right_dock.resize_active_panel(
|
||||
Some(size),
|
||||
cx,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
DockPosition::Bottom => {
|
||||
let size = workspace.bounds.bottom()
|
||||
- e.event.position.y;
|
||||
workspace.bottom_dock.update(
|
||||
cx,
|
||||
|bottom_dock, cx| {
|
||||
bottom_dock.resize_active_panel(
|
||||
Some(size),
|
||||
cx,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
))
|
||||
})
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.flex_1()
|
||||
.overflow_hidden()
|
||||
.child(
|
||||
h_flex()
|
||||
.flex_1()
|
||||
.when_some(paddings.0, |this, p| {
|
||||
this.child(p.border_r_1())
|
||||
})
|
||||
.child(self.center.render(
|
||||
&self.project,
|
||||
&self.follower_states,
|
||||
self.active_call(),
|
||||
&self.active_pane,
|
||||
self.zoomed.as_ref(),
|
||||
&self.app_state,
|
||||
cx,
|
||||
))
|
||||
.when_some(paddings.1, |this, p| {
|
||||
this.child(p.border_l_1())
|
||||
}),
|
||||
)
|
||||
.flex_row()
|
||||
.h_full()
|
||||
// Left Dock
|
||||
.children(self.render_dock(
|
||||
DockPosition::Bottom,
|
||||
&self.bottom_dock,
|
||||
DockPosition::Left,
|
||||
&self.left_dock,
|
||||
cx,
|
||||
))
|
||||
// Panes
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.flex_1()
|
||||
.overflow_hidden()
|
||||
.child(
|
||||
h_flex()
|
||||
.flex_1()
|
||||
.when_some(paddings.0, |this, p| {
|
||||
this.child(p.border_r_1())
|
||||
})
|
||||
.child(self.center.render(
|
||||
&self.project,
|
||||
&self.follower_states,
|
||||
self.active_call(),
|
||||
&self.active_pane,
|
||||
self.zoomed.as_ref(),
|
||||
&self.app_state,
|
||||
cx,
|
||||
))
|
||||
.when_some(paddings.1, |this, p| {
|
||||
this.child(p.border_l_1())
|
||||
}),
|
||||
)
|
||||
.children(self.render_dock(
|
||||
DockPosition::Bottom,
|
||||
&self.bottom_dock,
|
||||
cx,
|
||||
)),
|
||||
)
|
||||
// Right Dock
|
||||
.children(self.render_dock(
|
||||
DockPosition::Right,
|
||||
&self.right_dock,
|
||||
cx,
|
||||
)),
|
||||
)
|
||||
// Right Dock
|
||||
.children(self.render_dock(
|
||||
DockPosition::Right,
|
||||
&self.right_dock,
|
||||
cx,
|
||||
)),
|
||||
)
|
||||
.children(self.zoomed.as_ref().and_then(|view| {
|
||||
let zoomed_view = view.upgrade()?;
|
||||
let div = div()
|
||||
.occlude()
|
||||
.absolute()
|
||||
.overflow_hidden()
|
||||
.border_color(colors.border)
|
||||
.bg(colors.background)
|
||||
.child(zoomed_view)
|
||||
.inset_0()
|
||||
.shadow_lg();
|
||||
.children(self.zoomed.as_ref().and_then(|view| {
|
||||
let zoomed_view = view.upgrade()?;
|
||||
let div = div()
|
||||
.occlude()
|
||||
.absolute()
|
||||
.overflow_hidden()
|
||||
.border_color(colors.border)
|
||||
.bg(colors.background)
|
||||
.child(zoomed_view)
|
||||
.inset_0()
|
||||
.shadow_lg();
|
||||
|
||||
Some(match self.zoomed_position {
|
||||
Some(DockPosition::Left) => div.right_2().border_r_1(),
|
||||
Some(DockPosition::Right) => div.left_2().border_l_1(),
|
||||
Some(DockPosition::Bottom) => div.top_2().border_t_1(),
|
||||
None => div.top_2().bottom_2().left_2().right_2().border_1(),
|
||||
})
|
||||
}))
|
||||
.child(self.modal_layer.clone())
|
||||
.children(self.render_notifications(cx)),
|
||||
)
|
||||
.child(self.status_bar.clone())
|
||||
.children(if self.project.read(cx).is_disconnected(cx) {
|
||||
if let Some(render) = self.render_disconnected_overlay.take() {
|
||||
let result = render(self, cx);
|
||||
self.render_disconnected_overlay = Some(render);
|
||||
Some(result)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}),
|
||||
Some(match self.zoomed_position {
|
||||
Some(DockPosition::Left) => div.right_2().border_r_1(),
|
||||
Some(DockPosition::Right) => div.left_2().border_l_1(),
|
||||
Some(DockPosition::Bottom) => div.top_2().border_t_1(),
|
||||
None => {
|
||||
div.top_2().bottom_2().left_2().right_2().border_1()
|
||||
}
|
||||
})
|
||||
}))
|
||||
.children(self.render_notifications(cx)),
|
||||
)
|
||||
.child(self.status_bar.clone())
|
||||
.child(self.modal_layer.clone()),
|
||||
),
|
||||
cx,
|
||||
)
|
||||
}
|
||||
@@ -5516,6 +5534,7 @@ pub fn join_hosted_project(
|
||||
pub fn open_ssh_project(
|
||||
window: WindowHandle<Workspace>,
|
||||
connection_options: SshConnectionOptions,
|
||||
cancel_rx: oneshot::Receiver<()>,
|
||||
delegate: Arc<dyn SshClientDelegate>,
|
||||
app_state: Arc<AppState>,
|
||||
paths: Vec<PathBuf>,
|
||||
@@ -5537,11 +5556,21 @@ pub fn open_ssh_project(
|
||||
workspace_id.0
|
||||
);
|
||||
|
||||
let session = cx
|
||||
let session = match cx
|
||||
.update(|cx| {
|
||||
remote::SshRemoteClient::new(unique_identifier, connection_options, delegate, cx)
|
||||
remote::SshRemoteClient::new(
|
||||
unique_identifier,
|
||||
connection_options,
|
||||
cancel_rx,
|
||||
delegate,
|
||||
cx,
|
||||
)
|
||||
})?
|
||||
.await?;
|
||||
.await?
|
||||
{
|
||||
Some(result) => result,
|
||||
None => return Ok(()),
|
||||
};
|
||||
|
||||
let project = cx.update(|cx| {
|
||||
project::Project::ssh(
|
||||
|
||||
@@ -713,6 +713,16 @@ fn handle_open_request(
|
||||
|
||||
if let Some(connection_info) = request.ssh_connection {
|
||||
cx.spawn(|mut cx| async move {
|
||||
let nickname = cx
|
||||
.update(|cx| {
|
||||
SshSettings::get_global(cx).nickname_for(
|
||||
&connection_info.host,
|
||||
connection_info.port,
|
||||
&connection_info.username,
|
||||
)
|
||||
})
|
||||
.ok()
|
||||
.flatten();
|
||||
let paths_with_position =
|
||||
derive_paths_with_position(app_state.fs.as_ref(), request.open_paths).await;
|
||||
open_ssh_project(
|
||||
@@ -720,6 +730,7 @@ fn handle_open_request(
|
||||
paths_with_position.into_iter().map(|p| p.path).collect(),
|
||||
app_state,
|
||||
workspace::OpenOptions::default(),
|
||||
nickname,
|
||||
&mut cx,
|
||||
)
|
||||
.await
|
||||
@@ -888,6 +899,12 @@ async fn restore_or_create_workspace(
|
||||
})
|
||||
.ok()
|
||||
.flatten();
|
||||
let nickname = cx
|
||||
.update(|cx| {
|
||||
SshSettings::get_global(cx).nickname_for(&ssh.host, ssh.port, &ssh.user)
|
||||
})
|
||||
.ok()
|
||||
.flatten();
|
||||
let connection_options = SshConnectionOptions {
|
||||
args,
|
||||
host: ssh.host.clone(),
|
||||
@@ -902,6 +919,7 @@ async fn restore_or_create_workspace(
|
||||
ssh.paths.into_iter().map(PathBuf::from).collect(),
|
||||
app_state,
|
||||
workspace::OpenOptions::default(),
|
||||
nickname,
|
||||
&mut cx,
|
||||
)
|
||||
.await
|
||||
|
||||
@@ -274,7 +274,6 @@ pub fn initialize_workspace(
|
||||
workspace.add_panel(channels_panel, cx);
|
||||
workspace.add_panel(chat_panel, cx);
|
||||
workspace.add_panel(notification_panel, cx);
|
||||
cx.focus_self();
|
||||
})
|
||||
})
|
||||
.detach();
|
||||
|
||||
@@ -13,7 +13,7 @@ pub fn app_menus() -> Vec<Menu> {
|
||||
MenuItem::action("Check for Updates", auto_update::Check),
|
||||
MenuItem::separator(),
|
||||
MenuItem::submenu(Menu {
|
||||
name: "Preferences".into(),
|
||||
name: "Settings".into(),
|
||||
items: vec![
|
||||
MenuItem::action("Open Settings", super::OpenSettings),
|
||||
MenuItem::action("Open Key Bindings", zed_actions::OpenKeymap),
|
||||
@@ -23,6 +23,12 @@ pub fn app_menus() -> Vec<Menu> {
|
||||
MenuItem::action("Select Theme...", theme_selector::Toggle::default()),
|
||||
],
|
||||
}),
|
||||
MenuItem::separator(),
|
||||
MenuItem::submenu(Menu {
|
||||
name: "Services".into(),
|
||||
items: vec![],
|
||||
}),
|
||||
MenuItem::separator(),
|
||||
MenuItem::action("Extensions", extensions_ui::Extensions),
|
||||
MenuItem::action("Install CLI", install_cli::Install),
|
||||
MenuItem::separator(),
|
||||
|
||||
@@ -437,12 +437,19 @@ async fn open_workspaces(
|
||||
port: ssh.port,
|
||||
password: None,
|
||||
};
|
||||
let nickname = cx
|
||||
.update(|cx| {
|
||||
SshSettings::get_global(cx).nickname_for(&ssh.host, ssh.port, &ssh.user)
|
||||
})
|
||||
.ok()
|
||||
.flatten();
|
||||
cx.spawn(|mut cx| async move {
|
||||
open_ssh_project(
|
||||
connection_options,
|
||||
ssh.paths.into_iter().map(PathBuf::from).collect(),
|
||||
app_state,
|
||||
OpenOptions::default(),
|
||||
nickname,
|
||||
&mut cx,
|
||||
)
|
||||
.await
|
||||
|
||||
@@ -85,6 +85,7 @@
|
||||
- [JavaScript](./languages/javascript.md)
|
||||
- [Julia](./languages/julia.md)
|
||||
- [JSON](./languages/json.md)
|
||||
- [Jsonnet](./languages/jsonnet.md)
|
||||
- [Kotlin](./languages/kotlin.md)
|
||||
- [Lua](./languages/lua.md)
|
||||
- [Luau](./languages/luau.md)
|
||||
|
||||
@@ -10,6 +10,26 @@ JSON support is available natively in Zed.
|
||||
Zed also supports a super-set of JSON called JSONC, which allows single line comments (`//`) in JSON files.
|
||||
While editing these files you can use `cmd-/` (macOS) or `ctrl-/` (Linux) to toggle comments on the current line or selection.
|
||||
|
||||
## JSONC Prettier Formatting
|
||||
|
||||
If you use files with the `*.jsonc` extension when using `Format Document` or have `format_on_save` enabled, Zed invokes Prettier as the formatter. Prettier has an [outstanding issue](https://github.com/prettier/prettier/issues/15956) where it will add trailing commas to files with a `jsonc` extension. JSONC files which have a `.json` extension are unaffected.
|
||||
|
||||
To workaround this behavior you can add the following to your `.prettierrc`
|
||||
|
||||
```json
|
||||
{
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.jsonc"],
|
||||
"options": {
|
||||
"parser": "json",
|
||||
"trailingComma": "none"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
<!--
|
||||
TBD: JSONC Example for how to use `file_types`
|
||||
TBD: Add formatter (prettier) settings (autoformat, tab_size, etc)
|
||||
|
||||
@@ -10,4 +10,4 @@ Zed Employees are not currently working on the Windows build.
|
||||
However, we welcome contributions from the community to improve Windows support.
|
||||
|
||||
- [GitHub Issues with 'Windows' label](https://github.com/zed-industries/zed/issues?q=is%3Aissue+is%3Aopen+label%3Awindows)
|
||||
- [#windows-port on Zed Community Discord](https://discord.com/channels/869392257814519848/1208481909676576818)
|
||||
- [Zed Community Discord](https://discord.gg/zed-community) -> `#windows-port`
|
||||
|
||||
@@ -37,26 +37,28 @@ commit=$(git rev-parse HEAD | cut -c 1-7)
|
||||
version_info=$(rustc --version --verbose)
|
||||
host_line=$(echo "$version_info" | grep host)
|
||||
target_triple=${host_line#*: }
|
||||
musl_triple=${target_triple%-gnu}-musl
|
||||
|
||||
# Generate the licenses first, so they can be baked into the binaries
|
||||
script/generate-licenses
|
||||
rustup target add "$musl_triple"
|
||||
|
||||
# Build binary in release mode
|
||||
export RUSTFLAGS="${RUSTFLAGS:-} -C link-args=-Wl,--disable-new-dtags,-rpath,\$ORIGIN/../lib"
|
||||
cargo build --release --target "${target_triple}" --package zed --package cli
|
||||
# Build remote_server in separate invocation to prevent feature unification from other crates
|
||||
# from influencing dynamic libraries required by it.
|
||||
cargo build --release --target "${target_triple}" --package remote_server
|
||||
cargo build --release --target "${musl_triple}" --package remote_server
|
||||
|
||||
# Strip the binary of all debug symbols
|
||||
# Later, we probably want to do something like this: https://github.com/GabrielMajeri/separate-symbols
|
||||
strip --strip-debug "${target_dir}/${target_triple}/release/zed"
|
||||
strip --strip-debug "${target_dir}/${target_triple}/release/cli"
|
||||
strip --strip-debug "${target_dir}/${target_triple}/release/remote_server"
|
||||
strip --strip-debug "${target_dir}/${musl_triple}/release/remote_server"
|
||||
|
||||
|
||||
# Ensure that remote_server does not depend on libssl nor libcrypto, as we got rid of these deps.
|
||||
! ldd "${target_dir}/${target_triple}/release/remote_server" | grep -q 'libcrypto\|libssl'
|
||||
! ldd "${target_dir}/${musl_triple}/release/remote_server" | grep -q 'libcrypto\|libssl'
|
||||
|
||||
suffix=""
|
||||
if [ "$channel" != "stable" ]; then
|
||||
@@ -125,4 +127,4 @@ remove_match="zed(-[a-zA-Z0-9]+)?-linux-$(uname -m)\.tar\.gz"
|
||||
ls "${target_dir}/release" | grep -E ${remove_match} | xargs -d "\n" -I {} rm -f "${target_dir}/release/{}" || true
|
||||
tar -czvf "${target_dir}/release/$archive" -C ${temp_dir} "zed$suffix.app"
|
||||
|
||||
gzip --stdout --best "${target_dir}/${target_triple}/release/remote_server" > "${target_dir}/zed-remote-server-linux-${arch}.gz"
|
||||
gzip --stdout --best "${target_dir}/${musl_triple}/release/remote_server" > "${target_dir}/zed-remote-server-linux-${arch}.gz"
|
||||
|
||||
@@ -42,6 +42,8 @@ if [[ -n $apt ]]; then
|
||||
gettext-base
|
||||
elfutils
|
||||
libsqlite3-dev
|
||||
musl-tools
|
||||
musl-dev
|
||||
)
|
||||
if (grep -qP 'PRETTY_NAME="(Linux Mint 22|.+24\.04)' /etc/os-release); then
|
||||
deps+=( mold libstdc++-14-dev )
|
||||
|
||||
Reference in New Issue
Block a user