Compare commits
117 Commits
gamma-corr
...
pass-remot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
983e878cff | ||
|
|
1cfcdfa7ac | ||
|
|
c9f2c2792c | ||
|
|
8240a52a39 | ||
|
|
c28f5b11f8 | ||
|
|
96854c68ea | ||
|
|
becc36380f | ||
|
|
1a0a8a9559 | ||
|
|
2fd210bc9a | ||
|
|
23321be2ce | ||
|
|
659b1c9dcf | ||
|
|
cb8028c092 | ||
|
|
ca76948044 | ||
|
|
852fb51528 | ||
|
|
d489f96aef | ||
|
|
b4659bb44e | ||
|
|
d5f2bca382 | ||
|
|
114c462143 | ||
|
|
933c11a9b2 | ||
|
|
14ea4621ab | ||
|
|
477c6e6833 | ||
|
|
6c470748ac | ||
|
|
e0245b3f30 | ||
|
|
9211e699ee | ||
|
|
0663bf2a53 | ||
|
|
9d95da56c3 | ||
|
|
5ee5a1a51e | ||
|
|
72613b7668 | ||
|
|
f74f670865 | ||
|
|
af34953bc3 | ||
|
|
b102a40e04 | ||
|
|
790fdcf737 | ||
|
|
2868b67286 | ||
|
|
614b3b979b | ||
|
|
4c7b48b35d | ||
|
|
6b2f1cc543 | ||
|
|
f62ccf9c8a | ||
|
|
841d3221b3 | ||
|
|
02447a8552 | ||
|
|
c16dfc1a39 | ||
|
|
268ac4c047 | ||
|
|
571c7d4f66 | ||
|
|
5ff49db92f | ||
|
|
395e25be25 | ||
|
|
74223c1b00 | ||
|
|
0b373d43dc | ||
|
|
75c545aa1e | ||
|
|
6ab4b46984 | ||
|
|
ebaa270baf | ||
|
|
7285cdb955 | ||
|
|
e062f30d9e | ||
|
|
37a59d6b2e | ||
|
|
a03770837e | ||
|
|
0e62b6dddd | ||
|
|
95ace03706 | ||
|
|
49ed932c1f | ||
|
|
33e84da657 | ||
|
|
536d7e5355 | ||
|
|
cbba44900d | ||
|
|
335b112abd | ||
|
|
33bed8d680 | ||
|
|
6d4a5f9ad2 | ||
|
|
427c2017c3 | ||
|
|
ebca6a8f3d | ||
|
|
29c9f0f6a1 | ||
|
|
e076f55d78 | ||
|
|
e31f44450e | ||
|
|
e0761db62d | ||
|
|
8c342ef706 | ||
|
|
7e67753d51 | ||
|
|
1475a7000f | ||
|
|
41fd9189e3 | ||
|
|
973498e075 | ||
|
|
b63394f4bd | ||
|
|
743165fa6c | ||
|
|
e03968f538 | ||
|
|
3c57a4071c | ||
|
|
ad6a07e574 | ||
|
|
c2668bc953 | ||
|
|
705a06c3dd | ||
|
|
f77b6ab79c | ||
|
|
ea5131ce0a | ||
|
|
1c2b3ad782 | ||
|
|
496dae968b | ||
|
|
5c6565a9e0 | ||
|
|
7853e32f80 | ||
|
|
f5cbfa718e | ||
|
|
6a2c712990 | ||
|
|
9454f0f1c7 | ||
|
|
5b0c15d8c4 | ||
|
|
aae39071ef | ||
|
|
a35b73e63e | ||
|
|
c0d11be75f | ||
|
|
0e26d22fea | ||
|
|
bd0f197415 | ||
|
|
343c88574a | ||
|
|
e7a0890086 | ||
|
|
d4c5c0f05e | ||
|
|
f0c7e62adc | ||
|
|
80d50f56f3 | ||
|
|
fb6c987e3e | ||
|
|
b4c2f29c8b | ||
|
|
8666ec95ba | ||
|
|
889aac9c03 | ||
|
|
5b9916e34b | ||
|
|
5b317f60df | ||
|
|
e2552b9add | ||
|
|
37899187c6 | ||
|
|
d265e44209 | ||
|
|
f12981db32 | ||
|
|
d99f5fe83e | ||
|
|
df1d0dec0a | ||
|
|
ad94ad511a | ||
|
|
0e7770a9a2 | ||
|
|
3f905d57e5 | ||
|
|
f01a86c644 | ||
|
|
5fd7afb9da |
29
.github/workflows/ci.yml
vendored
29
.github/workflows/ci.yml
vendored
@@ -245,6 +245,7 @@ jobs:
|
||||
# 25 was chosen arbitrarily.
|
||||
fetch-depth: 25
|
||||
clean: false
|
||||
ref: ${{ github.ref }}
|
||||
|
||||
- name: Limit target directory size
|
||||
run: script/clear-target-dir-if-larger-than 100
|
||||
@@ -261,6 +262,9 @@ jobs:
|
||||
mkdir -p target/
|
||||
# Ignore any errors that occur while drafting release notes to not fail the build.
|
||||
script/draft-release-notes "$RELEASE_VERSION" "$RELEASE_CHANNEL" > target/release-notes.md || true
|
||||
script/create-draft-release target/release-notes.md
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Generate license file
|
||||
run: script/generate-licenses
|
||||
@@ -268,18 +272,12 @@ jobs:
|
||||
- name: Create macOS app bundle
|
||||
run: script/bundle-mac
|
||||
|
||||
- name: Rename single-architecture binaries
|
||||
- name: Rename binaries
|
||||
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
|
||||
run: |
|
||||
mv target/aarch64-apple-darwin/release/Zed.dmg target/aarch64-apple-darwin/release/Zed-aarch64.dmg
|
||||
mv target/x86_64-apple-darwin/release/Zed.dmg target/x86_64-apple-darwin/release/Zed-x86_64.dmg
|
||||
|
||||
- name: Upload app bundle (universal) to workflow run if main branch or specific label
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4
|
||||
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
|
||||
with:
|
||||
name: Zed_${{ github.event.pull_request.head.sha || github.sha }}.dmg
|
||||
path: target/release/Zed.dmg
|
||||
- name: Upload app bundle (aarch64) to workflow run if main branch or specific label
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4
|
||||
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
|
||||
@@ -305,8 +303,6 @@ jobs:
|
||||
target/zed-remote-server-macos-aarch64.gz
|
||||
target/aarch64-apple-darwin/release/Zed-aarch64.dmg
|
||||
target/x86_64-apple-darwin/release/Zed-x86_64.dmg
|
||||
target/release/Zed.dmg
|
||||
body_path: target/release-notes.md
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -353,7 +349,6 @@ jobs:
|
||||
files: |
|
||||
target/zed-remote-server-linux-x86_64.gz
|
||||
target/release/zed-linux-x86_64.tar.gz
|
||||
body: ""
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -400,6 +395,18 @@ jobs:
|
||||
files: |
|
||||
target/zed-remote-server-linux-aarch64.gz
|
||||
target/release/zed-linux-aarch64.tar.gz
|
||||
body: ""
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
auto-release-preview:
|
||||
name: Auto release preview
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/v') && endsWith(github.ref, '-pre') && !endsWith(github.ref, '.0-pre') }}
|
||||
needs: [bundle-mac, bundle-linux, bundle-linux-aarch64]
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- bundle
|
||||
steps:
|
||||
- name: gh release
|
||||
run: gh release edit $GITHUB_REF_NAME --draft=false
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
21
.github/workflows/script_checks.yml
vendored
Normal file
21
.github/workflows/script_checks.yml
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
name: Script
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "script/**"
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
shellcheck:
|
||||
name: "ShellCheck Scripts"
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
- name: Shellcheck ./scripts
|
||||
run: |
|
||||
./script/shellcheck-scripts error
|
||||
2220
Cargo.lock
generated
2220
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
25
Cargo.toml
25
Cargo.toml
@@ -9,6 +9,7 @@ members = [
|
||||
"crates/assistant_tool",
|
||||
"crates/audio",
|
||||
"crates/auto_update",
|
||||
"crates/auto_update_ui",
|
||||
"crates/breadcrumbs",
|
||||
"crates/call",
|
||||
"crates/channel",
|
||||
@@ -49,11 +50,14 @@ members = [
|
||||
"crates/http_client",
|
||||
"crates/image_viewer",
|
||||
"crates/indexed_docs",
|
||||
"crates/inline_completion",
|
||||
"crates/inline_completion_button",
|
||||
"crates/install_cli",
|
||||
"crates/journal",
|
||||
"crates/language",
|
||||
"crates/language_extension",
|
||||
"crates/language_model",
|
||||
"crates/language_models",
|
||||
"crates/language_selector",
|
||||
"crates/language_tools",
|
||||
"crates/languages",
|
||||
@@ -78,7 +82,6 @@ members = [
|
||||
"crates/project_panel",
|
||||
"crates/project_symbols",
|
||||
"crates/proto",
|
||||
"crates/quick_action_bar",
|
||||
"crates/recent_projects",
|
||||
"crates/refineable",
|
||||
"crates/refineable/derive_refineable",
|
||||
@@ -114,6 +117,7 @@ members = [
|
||||
"crates/terminal_view",
|
||||
"crates/text",
|
||||
"crates/theme",
|
||||
"crates/theme_extension",
|
||||
"crates/theme_importer",
|
||||
"crates/theme_selector",
|
||||
"crates/time_format",
|
||||
@@ -126,6 +130,7 @@ members = [
|
||||
"crates/util",
|
||||
"crates/vcs_menu",
|
||||
"crates/vim",
|
||||
"crates/vim_mode_setting",
|
||||
"crates/welcome",
|
||||
"crates/workspace",
|
||||
"crates/worktree",
|
||||
@@ -148,7 +153,6 @@ members = [
|
||||
"extensions/haskell",
|
||||
"extensions/html",
|
||||
"extensions/lua",
|
||||
"extensions/ocaml",
|
||||
"extensions/php",
|
||||
"extensions/perplexity",
|
||||
"extensions/prisma",
|
||||
@@ -186,6 +190,7 @@ assistant_slash_command = { path = "crates/assistant_slash_command" }
|
||||
assistant_tool = { path = "crates/assistant_tool" }
|
||||
audio = { path = "crates/audio" }
|
||||
auto_update = { path = "crates/auto_update" }
|
||||
auto_update_ui = { path = "crates/auto_update_ui" }
|
||||
breadcrumbs = { path = "crates/breadcrumbs" }
|
||||
call = { path = "crates/call" }
|
||||
channel = { path = "crates/channel" }
|
||||
@@ -222,11 +227,14 @@ html_to_markdown = { path = "crates/html_to_markdown" }
|
||||
http_client = { path = "crates/http_client" }
|
||||
image_viewer = { path = "crates/image_viewer" }
|
||||
indexed_docs = { path = "crates/indexed_docs" }
|
||||
inline_completion = { path = "crates/inline_completion" }
|
||||
inline_completion_button = { path = "crates/inline_completion_button" }
|
||||
install_cli = { path = "crates/install_cli" }
|
||||
journal = { path = "crates/journal" }
|
||||
language = { path = "crates/language" }
|
||||
language_extension = { path = "crates/language_extension" }
|
||||
language_model = { path = "crates/language_model" }
|
||||
language_models = { path = "crates/language_models" }
|
||||
language_selector = { path = "crates/language_selector" }
|
||||
language_tools = { path = "crates/language_tools" }
|
||||
languages = { path = "crates/languages" }
|
||||
@@ -253,7 +261,6 @@ project = { path = "crates/project" }
|
||||
project_panel = { path = "crates/project_panel" }
|
||||
project_symbols = { path = "crates/project_symbols" }
|
||||
proto = { path = "crates/proto" }
|
||||
quick_action_bar = { path = "crates/quick_action_bar" }
|
||||
recent_projects = { path = "crates/recent_projects" }
|
||||
refineable = { path = "crates/refineable" }
|
||||
release_channel = { path = "crates/release_channel" }
|
||||
@@ -288,6 +295,7 @@ terminal = { path = "crates/terminal" }
|
||||
terminal_view = { path = "crates/terminal_view" }
|
||||
text = { path = "crates/text" }
|
||||
theme = { path = "crates/theme" }
|
||||
theme_extension = { path = "crates/theme_extension" }
|
||||
theme_importer = { path = "crates/theme_importer" }
|
||||
theme_selector = { path = "crates/theme_selector" }
|
||||
time_format = { path = "crates/time_format" }
|
||||
@@ -299,6 +307,7 @@ ui_macros = { path = "crates/ui_macros" }
|
||||
util = { path = "crates/util" }
|
||||
vcs_menu = { path = "crates/vcs_menu" }
|
||||
vim = { path = "crates/vim" }
|
||||
vim_mode_setting = { path = "crates/vim_mode_setting" }
|
||||
welcome = { path = "crates/welcome" }
|
||||
workspace = { path = "crates/workspace" }
|
||||
worktree = { path = "crates/worktree" }
|
||||
@@ -333,7 +342,7 @@ blade-macros = { git = "https://github.com/kvark/blade", rev = "e142a3a5e678eb6a
|
||||
blade-util = { git = "https://github.com/kvark/blade", rev = "e142a3a5e678eb6a13e642ad8401b1f3aa38e969" }
|
||||
blake3 = "1.5.3"
|
||||
bytes = "1.0"
|
||||
cargo_metadata = "0.18"
|
||||
cargo_metadata = "0.19"
|
||||
cargo_toml = "0.20"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
clap = { version = "4.4", features = ["derive"] }
|
||||
@@ -369,12 +378,14 @@ indexmap = { version = "1.6.2", features = ["serde"] }
|
||||
indoc = "2"
|
||||
itertools = "0.13.0"
|
||||
jsonwebtoken = "9.3"
|
||||
jupyter-protocol = { version = "0.2.0" }
|
||||
jupyter-websocket-client = { version = "0.4.1" }
|
||||
libc = "0.2"
|
||||
linkify = "0.10.0"
|
||||
log = { version = "0.4.16", features = ["kv_unstable_serde", "serde"] }
|
||||
markup5ever_rcdom = "0.3.0"
|
||||
nanoid = "0.4"
|
||||
nbformat = "0.5.0"
|
||||
nbformat = "0.6.0"
|
||||
nix = "0.29"
|
||||
num-format = "0.4.4"
|
||||
once_cell = "1.19.0"
|
||||
@@ -389,7 +400,7 @@ pet-core = { git = "https://github.com/microsoft/python-environment-tools.git",
|
||||
pet-poetry = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
|
||||
pet-reporter = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
|
||||
postage = { version = "0.5", features = ["futures-traits"] }
|
||||
pretty_assertions = "1.3.0"
|
||||
pretty_assertions = { version = "1.3.0", features = ["unstable"] }
|
||||
profiling = "1"
|
||||
prost = "0.9"
|
||||
prost-build = "0.9"
|
||||
@@ -408,7 +419,7 @@ reqwest = { git = "https://github.com/zed-industries/reqwest.git", rev = "fd110f
|
||||
"stream",
|
||||
] }
|
||||
rsa = "0.9.6"
|
||||
runtimelib = { version = "0.19.0", default-features = false, features = [
|
||||
runtimelib = { version = "0.21.0", default-features = false, features = [
|
||||
"async-dispatcher-runtime",
|
||||
] }
|
||||
rustc-demangle = "0.1.23"
|
||||
|
||||
@@ -251,6 +251,8 @@
|
||||
"ctrl-pagedown": "pane::ActivateNextItem",
|
||||
"ctrl-shift-pageup": "pane::SwapItemLeft",
|
||||
"ctrl-shift-pagedown": "pane::SwapItemRight",
|
||||
"back": "pane::GoBack",
|
||||
"forward": "pane::GoForward",
|
||||
"ctrl-w": "pane::CloseActiveItem",
|
||||
"ctrl-f4": "pane::CloseActiveItem",
|
||||
"alt-ctrl-t": ["pane::CloseInactiveItems", { "close_pinned": false }],
|
||||
|
||||
@@ -49,8 +49,9 @@
|
||||
"ctrl-d": "editor::Delete",
|
||||
"tab": "editor::Tab",
|
||||
"shift-tab": "editor::TabPrev",
|
||||
"ctrl-k": "editor::CutToEndOfLine",
|
||||
"ctrl-t": "editor::Transpose",
|
||||
"ctrl-k": "editor::KillRingCut",
|
||||
"ctrl-y": "editor::KillRingYank",
|
||||
"cmd-k q": "editor::Rewrap",
|
||||
"cmd-k cmd-q": "editor::Rewrap",
|
||||
"cmd-backspace": "editor::DeleteToBeginningOfLine",
|
||||
@@ -92,6 +93,8 @@
|
||||
"ctrl-e": "editor::MoveToEndOfLine",
|
||||
"cmd-up": "editor::MoveToBeginning",
|
||||
"cmd-down": "editor::MoveToEnd",
|
||||
"ctrl-home": "editor::MoveToBeginning",
|
||||
"ctrl-end": "editor::MoveToEnd",
|
||||
"shift-up": "editor::SelectUp",
|
||||
"ctrl-shift-p": "editor::SelectUp",
|
||||
"shift-down": "editor::SelectDown",
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
"ctrl-shift-l": "editor::SplitSelectionIntoLines",
|
||||
"ctrl-shift-a": "editor::SelectLargerSyntaxNode",
|
||||
"ctrl-shift-d": "editor::DuplicateLineDown",
|
||||
"alt-f3": "editor::SelectAllMatches", // find_all_under
|
||||
"f12": "editor::GoToDefinition",
|
||||
"ctrl-f12": "editor::GoToDefinitionSplit",
|
||||
"shift-f12": "editor::FindAllReferences",
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
"cmd-shift-l": "editor::SplitSelectionIntoLines",
|
||||
"cmd-shift-a": "editor::SelectLargerSyntaxNode",
|
||||
"cmd-shift-d": "editor::DuplicateLineDown",
|
||||
"ctrl-cmd-g": "editor::SelectAllMatches", // find_all_under
|
||||
"shift-f12": "editor::FindAllReferences",
|
||||
"alt-cmd-down": "editor::GoToDefinition",
|
||||
"ctrl-alt-cmd-down": "editor::GoToDefinitionSplit",
|
||||
|
||||
@@ -381,8 +381,7 @@
|
||||
"shift-b": "vim::CurlyBrackets",
|
||||
"<": "vim::AngleBrackets",
|
||||
">": "vim::AngleBrackets",
|
||||
"a": "vim::AngleBrackets",
|
||||
"g": "vim::Argument"
|
||||
"a": "vim::Argument"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -578,7 +577,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "EmptyPane || SharedScreen || MarkdownPreview || KeyContextView",
|
||||
"context": "EmptyPane || SharedScreen || MarkdownPreview || KeyContextView || Welcome",
|
||||
"use_layout_keys": true,
|
||||
"bindings": {
|
||||
":": "command_palette::Toggle",
|
||||
|
||||
@@ -490,6 +490,9 @@
|
||||
"version": "2",
|
||||
// Whether the assistant is enabled.
|
||||
"enabled": true,
|
||||
// Whether to show inline hints showing the keybindings to use the inline assistant and the
|
||||
// assistant panel.
|
||||
"show_hints": true,
|
||||
// Whether to show the assistant panel button in the status bar.
|
||||
"button": true,
|
||||
// Where to dock the assistant panel. Can be 'left', 'right' or 'bottom'.
|
||||
@@ -581,20 +584,22 @@
|
||||
"file_finder": {
|
||||
// Whether to show file icons in the file finder.
|
||||
"file_icons": true,
|
||||
// Width of the file finder modal. This setting can
|
||||
// take four values.
|
||||
// Determines how much space the file finder can take up in relation to the available window width.
|
||||
// There are 5 possible width values:
|
||||
//
|
||||
// 1. Small width:
|
||||
// "modal_width": "small",
|
||||
// 2. Medium width (default):
|
||||
// "modal_width": "medium",
|
||||
// 3. Large width:
|
||||
// "modal_width": "large",
|
||||
// 4. Extra Large width:
|
||||
// "modal_width": "xlarge"
|
||||
// 5. Fullscreen width:
|
||||
// "modal_width": "full"
|
||||
"modal_width": "medium"
|
||||
// 1. Small: This value is essentially a fixed width.
|
||||
// "modal_width": "small"
|
||||
// 2. Medium:
|
||||
// "modal_width": "medium"
|
||||
// 3. Large:
|
||||
// "modal_width": "large"
|
||||
// 4. Extra Large:
|
||||
// "modal_width": "xlarge"
|
||||
// 5. Fullscreen: This value removes any horizontal padding, as it consumes the whole viewport width.
|
||||
// "modal_width": "full"
|
||||
//
|
||||
// Default: small
|
||||
"modal_max_width": "small"
|
||||
},
|
||||
// Whether or not to remove any trailing whitespace from lines of a buffer
|
||||
// before saving it.
|
||||
@@ -663,7 +668,7 @@
|
||||
},
|
||||
// Add files or globs of files that will be excluded by Zed entirely:
|
||||
// they will be skipped during FS scan(s), file tree and file search
|
||||
// will lack the corresponding file entries.
|
||||
// will lack the corresponding file entries. Overrides `file_scan_inclusions`.
|
||||
"file_scan_exclusions": [
|
||||
"**/.git",
|
||||
"**/.svn",
|
||||
@@ -674,6 +679,11 @@
|
||||
"**/.classpath",
|
||||
"**/.settings"
|
||||
],
|
||||
// Add files or globs of files that will be included by Zed, even when
|
||||
// ignored by git. This is useful for files that are not tracked by git,
|
||||
// but are still important to your project. Note that globs that are
|
||||
// overly broad can slow down Zed's file scanning. Overridden by `file_scan_exclusions`.
|
||||
"file_scan_inclusions": [".env*"],
|
||||
// Git gutter behavior configuration.
|
||||
"git": {
|
||||
// Control whether the git gutter is shown. May take 2 values:
|
||||
@@ -834,8 +844,12 @@
|
||||
}
|
||||
},
|
||||
"toolbar": {
|
||||
// Whether to display the terminal title in its toolbar.
|
||||
"title": true
|
||||
// Whether to display the terminal title in its toolbar's breadcrumbs.
|
||||
// Only shown if the terminal title is not empty.
|
||||
//
|
||||
// The shell running in the terminal needs to be configured to emit the title.
|
||||
// Example: `echo -e "\e]2;New Title\007";`
|
||||
"breadcrumbs": true
|
||||
}
|
||||
// Set the terminal's font size. If this option is not included,
|
||||
// the terminal will default to matching the buffer's font size.
|
||||
@@ -871,15 +885,8 @@
|
||||
//
|
||||
"file_types": {
|
||||
"Plain Text": ["txt"],
|
||||
"JSON": ["flake.lock"],
|
||||
"JSONC": [
|
||||
"**/.zed/**/*.json",
|
||||
"**/zed/**/*.json",
|
||||
"**/Zed/**/*.json",
|
||||
"tsconfig.json",
|
||||
"pyrightconfig.json"
|
||||
],
|
||||
"TOML": ["uv.lock"]
|
||||
"JSONC": ["**/.zed/**/*.json", "**/zed/**/*.json", "**/Zed/**/*.json"],
|
||||
"Shell Script": [".env.*"]
|
||||
},
|
||||
/// By default use a recent system version of node, or install our own.
|
||||
/// You can override this to use a version of node that is not in $PATH with:
|
||||
|
||||
@@ -50,6 +50,7 @@ indexed_docs.workspace = true
|
||||
indoc.workspace = true
|
||||
language.workspace = true
|
||||
language_model.workspace = true
|
||||
language_models.workspace = true
|
||||
log.workspace = true
|
||||
lsp.workspace = true
|
||||
markdown.workspace = true
|
||||
|
||||
@@ -33,7 +33,6 @@ use feature_flags::FeatureFlagAppExt;
|
||||
use fs::Fs;
|
||||
use gpui::impl_actions;
|
||||
use gpui::{actions, AppContext, Global, SharedString, UpdateGlobal};
|
||||
use indexed_docs::IndexedDocsRegistry;
|
||||
pub(crate) use inline_assistant::*;
|
||||
use language_model::{
|
||||
LanguageModelId, LanguageModelProviderId, LanguageModelRegistry, LanguageModelResponseMessage,
|
||||
@@ -275,7 +274,7 @@ pub fn init(
|
||||
client.telemetry().clone(),
|
||||
cx,
|
||||
);
|
||||
IndexedDocsRegistry::init_global(cx);
|
||||
indexed_docs::init(cx);
|
||||
|
||||
CommandPaletteFilter::update_global(cx, |filter, _cx| {
|
||||
filter.hide_namespace(Assistant::NAMESPACE);
|
||||
|
||||
@@ -50,11 +50,11 @@ use indexed_docs::IndexedDocsStore;
|
||||
use language::{
|
||||
language_settings::SoftWrap, BufferSnapshot, LanguageRegistry, LspAdapterDelegate, ToOffset,
|
||||
};
|
||||
use language_model::{
|
||||
provider::cloud::PROVIDER_ID, LanguageModelProvider, LanguageModelProviderId,
|
||||
LanguageModelRegistry, Role,
|
||||
};
|
||||
use language_model::{LanguageModelImage, LanguageModelToolUse};
|
||||
use language_model::{
|
||||
LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry, Role,
|
||||
ZED_CLOUD_PROVIDER_ID,
|
||||
};
|
||||
use multi_buffer::MultiBufferRow;
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use project::lsp_store::LocalLspAdapterDelegate;
|
||||
@@ -664,7 +664,7 @@ impl AssistantPanel {
|
||||
// If we're signed out and don't have a provider configured, or we're signed-out AND Zed.dev is
|
||||
// the provider, we want to show a nudge to sign in.
|
||||
let show_zed_ai_notice = client_status.is_signed_out()
|
||||
&& active_provider.map_or(true, |provider| provider.id().0 == PROVIDER_ID);
|
||||
&& active_provider.map_or(true, |provider| provider.id().0 == ZED_CLOUD_PROVIDER_ID);
|
||||
|
||||
self.show_zed_ai_notice = show_zed_ai_notice;
|
||||
cx.notify();
|
||||
@@ -2050,30 +2050,6 @@ impl ContextEditor {
|
||||
ContextEvent::SlashCommandOutputSectionAdded { section } => {
|
||||
self.insert_slash_command_output_sections([section.clone()], false, cx);
|
||||
}
|
||||
ContextEvent::SlashCommandFinished {
|
||||
output_range: _output_range,
|
||||
run_commands_in_ranges,
|
||||
} => {
|
||||
for range in run_commands_in_ranges {
|
||||
let commands = self.context.update(cx, |context, cx| {
|
||||
context.reparse(cx);
|
||||
context
|
||||
.pending_commands_for_range(range.clone(), cx)
|
||||
.to_vec()
|
||||
});
|
||||
|
||||
for command in commands {
|
||||
self.run_command(
|
||||
command.source_range,
|
||||
&command.name,
|
||||
&command.arguments,
|
||||
false,
|
||||
self.workspace.clone(),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
ContextEvent::UsePendingTools => {
|
||||
let pending_tool_uses = self
|
||||
.context
|
||||
@@ -2152,6 +2128,37 @@ impl ContextEditor {
|
||||
command_id: InvokedSlashCommandId,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
if let Some(invoked_slash_command) =
|
||||
self.context.read(cx).invoked_slash_command(&command_id)
|
||||
{
|
||||
if let InvokedSlashCommandStatus::Finished = invoked_slash_command.status {
|
||||
let run_commands_in_ranges = invoked_slash_command
|
||||
.run_commands_in_ranges
|
||||
.iter()
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
for range in run_commands_in_ranges {
|
||||
let commands = self.context.update(cx, |context, cx| {
|
||||
context.reparse(cx);
|
||||
context
|
||||
.pending_commands_for_range(range.clone(), cx)
|
||||
.to_vec()
|
||||
});
|
||||
|
||||
for command in commands {
|
||||
self.run_command(
|
||||
command.source_range,
|
||||
&command.name,
|
||||
&command.arguments,
|
||||
false,
|
||||
self.workspace.clone(),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
if let Some(invoked_slash_command) =
|
||||
self.context.read(cx).invoked_slash_command(&command_id)
|
||||
|
||||
@@ -5,13 +5,12 @@ use anthropic::Model as AnthropicModel;
|
||||
use feature_flags::FeatureFlagAppExt;
|
||||
use fs::Fs;
|
||||
use gpui::{AppContext, Pixels};
|
||||
use language_model::provider::open_ai;
|
||||
use language_model::settings::{
|
||||
AnthropicSettingsContent, AnthropicSettingsContentV1, OllamaSettingsContent,
|
||||
OpenAiSettingsContent, OpenAiSettingsContentV1, VersionedAnthropicSettingsContent,
|
||||
VersionedOpenAiSettingsContent,
|
||||
use language_model::{CloudModel, LanguageModel};
|
||||
use language_models::{
|
||||
provider::open_ai, AllLanguageModelSettings, AnthropicSettingsContent,
|
||||
AnthropicSettingsContentV1, OllamaSettingsContent, OpenAiSettingsContent,
|
||||
OpenAiSettingsContentV1, VersionedAnthropicSettingsContent, VersionedOpenAiSettingsContent,
|
||||
};
|
||||
use language_model::{settings::AllLanguageModelSettings, CloudModel, LanguageModel};
|
||||
use ollama::Model as OllamaModel;
|
||||
use schemars::{schema::Schema, JsonSchema};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -60,6 +59,7 @@ pub struct AssistantSettings {
|
||||
pub inline_alternatives: Vec<LanguageModelSelection>,
|
||||
pub using_outdated_settings_version: bool,
|
||||
pub enable_experimental_live_diffs: bool,
|
||||
pub show_hints: bool,
|
||||
}
|
||||
|
||||
impl AssistantSettings {
|
||||
@@ -202,6 +202,7 @@ impl AssistantSettingsContent {
|
||||
AssistantSettingsContent::Versioned(settings) => match settings {
|
||||
VersionedAssistantSettingsContent::V1(settings) => AssistantSettingsContentV2 {
|
||||
enabled: settings.enabled,
|
||||
show_hints: None,
|
||||
button: settings.button,
|
||||
dock: settings.dock,
|
||||
default_width: settings.default_width,
|
||||
@@ -242,6 +243,7 @@ impl AssistantSettingsContent {
|
||||
},
|
||||
AssistantSettingsContent::Legacy(settings) => AssistantSettingsContentV2 {
|
||||
enabled: None,
|
||||
show_hints: None,
|
||||
button: settings.button,
|
||||
dock: settings.dock,
|
||||
default_width: settings.default_width,
|
||||
@@ -354,6 +356,7 @@ impl Default for VersionedAssistantSettingsContent {
|
||||
fn default() -> Self {
|
||||
Self::V2(AssistantSettingsContentV2 {
|
||||
enabled: None,
|
||||
show_hints: None,
|
||||
button: None,
|
||||
dock: None,
|
||||
default_width: None,
|
||||
@@ -371,6 +374,11 @@ pub struct AssistantSettingsContentV2 {
|
||||
///
|
||||
/// Default: true
|
||||
enabled: Option<bool>,
|
||||
/// Whether to show inline hints that show keybindings for inline assistant
|
||||
/// and assistant panel.
|
||||
///
|
||||
/// Default: true
|
||||
show_hints: Option<bool>,
|
||||
/// Whether to show the assistant panel button in the status bar.
|
||||
///
|
||||
/// Default: true
|
||||
@@ -505,6 +513,7 @@ impl Settings for AssistantSettings {
|
||||
|
||||
let value = value.upgrade();
|
||||
merge(&mut settings.enabled, value.enabled);
|
||||
merge(&mut settings.show_hints, value.show_hints);
|
||||
merge(&mut settings.button, value.button);
|
||||
merge(&mut settings.dock, value.dock);
|
||||
merge(
|
||||
@@ -575,6 +584,7 @@ mod tests {
|
||||
}),
|
||||
inline_alternatives: None,
|
||||
enabled: None,
|
||||
show_hints: None,
|
||||
button: None,
|
||||
dock: None,
|
||||
default_width: None,
|
||||
|
||||
@@ -25,13 +25,15 @@ use gpui::{
|
||||
|
||||
use language::{AnchorRangeExt, Bias, Buffer, LanguageRegistry, OffsetRangeExt, Point, ToOffset};
|
||||
use language_model::{
|
||||
logging::report_assistant_event,
|
||||
provider::cloud::{MaxMonthlySpendReachedError, PaymentRequiredError},
|
||||
LanguageModel, LanguageModelCacheConfiguration, LanguageModelCompletionEvent,
|
||||
LanguageModelImage, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage,
|
||||
LanguageModelRequestTool, LanguageModelToolResult, LanguageModelToolUse, MessageContent, Role,
|
||||
StopReason,
|
||||
};
|
||||
use language_models::{
|
||||
provider::cloud::{MaxMonthlySpendReachedError, PaymentRequiredError},
|
||||
report_assistant_event,
|
||||
};
|
||||
use open_ai::Model as OpenAiModel;
|
||||
use paths::contexts_dir;
|
||||
use project::Project;
|
||||
@@ -381,10 +383,6 @@ pub enum ContextEvent {
|
||||
SlashCommandOutputSectionAdded {
|
||||
section: SlashCommandOutputSection<language::Anchor>,
|
||||
},
|
||||
SlashCommandFinished {
|
||||
output_range: Range<language::Anchor>,
|
||||
run_commands_in_ranges: Vec<Range<language::Anchor>>,
|
||||
},
|
||||
UsePendingTools,
|
||||
ToolFinished {
|
||||
tool_use_id: Arc<str>,
|
||||
@@ -916,6 +914,7 @@ impl Context {
|
||||
InvokedSlashCommand {
|
||||
name: name.into(),
|
||||
range: output_range,
|
||||
run_commands_in_ranges: Vec::new(),
|
||||
status: InvokedSlashCommandStatus::Running(Task::ready(())),
|
||||
transaction: None,
|
||||
timestamp: id.0,
|
||||
@@ -1914,7 +1913,6 @@ impl Context {
|
||||
}
|
||||
|
||||
let mut pending_section_stack: Vec<PendingSection> = Vec::new();
|
||||
let mut run_commands_in_ranges: Vec<Range<language::Anchor>> = Vec::new();
|
||||
let mut last_role: Option<Role> = None;
|
||||
let mut last_section_range = None;
|
||||
|
||||
@@ -1980,7 +1978,13 @@ impl Context {
|
||||
|
||||
let end = this.buffer.read(cx).anchor_before(insert_position);
|
||||
if run_commands_in_text {
|
||||
run_commands_in_ranges.push(start..end);
|
||||
if let Some(invoked_slash_command) =
|
||||
this.invoked_slash_commands.get_mut(&command_id)
|
||||
{
|
||||
invoked_slash_command
|
||||
.run_commands_in_ranges
|
||||
.push(start..end);
|
||||
}
|
||||
}
|
||||
}
|
||||
SlashCommandEvent::EndSection => {
|
||||
@@ -2100,6 +2104,7 @@ impl Context {
|
||||
InvokedSlashCommand {
|
||||
name: name.to_string().into(),
|
||||
range: command_range.clone(),
|
||||
run_commands_in_ranges: Vec::new(),
|
||||
status: InvokedSlashCommandStatus::Running(insert_output_task),
|
||||
transaction: Some(first_transaction),
|
||||
timestamp: command_id.0,
|
||||
@@ -2891,7 +2896,7 @@ impl Context {
|
||||
request.messages.push(LanguageModelRequestMessage {
|
||||
role: Role::User,
|
||||
content: vec![
|
||||
"Generate a concise 3-7 word title for this conversation, omitting punctuation"
|
||||
"Generate a concise 3-7 word title for this conversation, omitting punctuation. Go straight to the title, without any preamble and prefix like `Here's a concise suggestion:...` or `Title:`"
|
||||
.into(),
|
||||
],
|
||||
cache: false,
|
||||
@@ -3176,6 +3181,7 @@ pub struct ParsedSlashCommand {
|
||||
pub struct InvokedSlashCommand {
|
||||
pub name: SharedString,
|
||||
pub range: Range<language::Anchor>,
|
||||
pub run_commands_in_ranges: Vec<Range<language::Anchor>>,
|
||||
pub status: InvokedSlashCommandStatus,
|
||||
pub transaction: Option<language::TransactionId>,
|
||||
timestamp: clock::Lamport,
|
||||
|
||||
@@ -770,7 +770,7 @@ impl ContextStore {
|
||||
contexts.push(SavedContextMetadata {
|
||||
title: title.to_string(),
|
||||
path,
|
||||
mtime: metadata.mtime.into(),
|
||||
mtime: metadata.mtime.timestamp_for_user().into(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,9 +30,10 @@ use gpui::{
|
||||
};
|
||||
use language::{Buffer, IndentKind, Point, Selection, TransactionId};
|
||||
use language_model::{
|
||||
logging::report_assistant_event, LanguageModel, LanguageModelRegistry, LanguageModelRequest,
|
||||
LanguageModelRequestMessage, LanguageModelTextStream, Role,
|
||||
LanguageModel, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage,
|
||||
LanguageModelTextStream, Role,
|
||||
};
|
||||
use language_models::report_assistant_event;
|
||||
use multi_buffer::MultiBufferRow;
|
||||
use parking_lot::Mutex;
|
||||
use project::{CodeAction, ProjectTransaction};
|
||||
|
||||
@@ -69,6 +69,10 @@ impl SlashCommand for DefaultSlashCommand {
|
||||
text.push('\n');
|
||||
}
|
||||
|
||||
if !text.ends_with('\n') {
|
||||
text.push('\n');
|
||||
}
|
||||
|
||||
Ok(SlashCommandOutput {
|
||||
sections: vec![SlashCommandOutputSection {
|
||||
range: 0..text.len(),
|
||||
|
||||
@@ -17,9 +17,9 @@ use gpui::{
|
||||
};
|
||||
use language::Buffer;
|
||||
use language_model::{
|
||||
logging::report_assistant_event, LanguageModelRegistry, LanguageModelRequest,
|
||||
LanguageModelRequestMessage, Role,
|
||||
LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, Role,
|
||||
};
|
||||
use language_models::report_assistant_event;
|
||||
use settings::Settings;
|
||||
use std::{
|
||||
cmp,
|
||||
|
||||
@@ -18,6 +18,7 @@ use workspace::{ui::IconName, Workspace};
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
SlashCommandRegistry::default_global(cx);
|
||||
extension_slash_command::init(cx);
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
|
||||
@@ -3,17 +3,39 @@ use std::sync::{atomic::AtomicBool, Arc};
|
||||
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use extension::{Extension, WorktreeDelegate};
|
||||
use gpui::{Task, WeakView, WindowContext};
|
||||
use extension::{Extension, ExtensionHostProxy, ExtensionSlashCommandProxy, WorktreeDelegate};
|
||||
use gpui::{AppContext, Task, WeakView, WindowContext};
|
||||
use language::{BufferSnapshot, LspAdapterDelegate};
|
||||
use ui::prelude::*;
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::{
|
||||
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
|
||||
SlashCommandResult,
|
||||
SlashCommandRegistry, SlashCommandResult,
|
||||
};
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
let proxy = ExtensionHostProxy::default_global(cx);
|
||||
proxy.register_slash_command_proxy(SlashCommandRegistryProxy {
|
||||
slash_command_registry: SlashCommandRegistry::global(cx),
|
||||
});
|
||||
}
|
||||
|
||||
struct SlashCommandRegistryProxy {
|
||||
slash_command_registry: Arc<SlashCommandRegistry>,
|
||||
}
|
||||
|
||||
impl ExtensionSlashCommandProxy for SlashCommandRegistryProxy {
|
||||
fn register_slash_command(
|
||||
&self,
|
||||
extension: Arc<dyn Extension>,
|
||||
command: extension::SlashCommand,
|
||||
) {
|
||||
self.slash_command_registry
|
||||
.register_command(ExtensionSlashCommand::new(extension, command), false)
|
||||
}
|
||||
}
|
||||
|
||||
/// An adapter that allows an [`LspAdapterDelegate`] to be used as a [`WorktreeDelegate`].
|
||||
struct WorktreeDelegateAdapter(Arc<dyn LspAdapterDelegate>);
|
||||
|
||||
|
||||
@@ -16,21 +16,16 @@ doctest = false
|
||||
anyhow.workspace = true
|
||||
client.workspace = true
|
||||
db.workspace = true
|
||||
editor.workspace = true
|
||||
gpui.workspace = true
|
||||
http_client.workspace = true
|
||||
log.workspace = true
|
||||
markdown_preview.workspace = true
|
||||
menu.workspace = true
|
||||
paths.workspace = true
|
||||
release_channel.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
serde_derive.workspace = true
|
||||
serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
smol.workspace = true
|
||||
tempfile.workspace = true
|
||||
util.workspace = true
|
||||
which.workspace = true
|
||||
workspace.workspace = true
|
||||
|
||||
@@ -1,27 +1,19 @@
|
||||
mod update_notification;
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use client::{Client, TelemetrySettings};
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
use db::RELEASE_CHANNEL;
|
||||
use editor::{Editor, MultiBuffer};
|
||||
use gpui::{
|
||||
actions, AppContext, AsyncAppContext, Context as _, Global, Model, ModelContext,
|
||||
SemanticVersion, SharedString, Task, View, ViewContext, VisualContext, WindowContext,
|
||||
SemanticVersion, Task, WindowContext,
|
||||
};
|
||||
|
||||
use markdown_preview::markdown_preview_view::{MarkdownPreviewMode, MarkdownPreviewView};
|
||||
use paths::remote_servers_dir;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde_derive::Serialize;
|
||||
use smol::{fs, io::AsyncReadExt};
|
||||
|
||||
use settings::{Settings, SettingsSources, SettingsStore};
|
||||
use smol::{fs::File, process::Command};
|
||||
|
||||
use http_client::{AsyncBody, HttpClient, HttpClientWithUrl};
|
||||
use release_channel::{AppCommitSha, AppVersion, ReleaseChannel};
|
||||
use paths::remote_servers_dir;
|
||||
use release_channel::{AppCommitSha, ReleaseChannel};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsSources, SettingsStore};
|
||||
use smol::{fs, io::AsyncReadExt};
|
||||
use smol::{fs::File, process::Command};
|
||||
use std::{
|
||||
env::{
|
||||
self,
|
||||
@@ -32,24 +24,13 @@ use std::{
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
use update_notification::UpdateNotification;
|
||||
use util::ResultExt;
|
||||
use which::which;
|
||||
use workspace::notifications::NotificationId;
|
||||
use workspace::Workspace;
|
||||
|
||||
const SHOULD_SHOW_UPDATE_NOTIFICATION_KEY: &str = "auto-updater-should-show-updated-notification";
|
||||
const POLL_INTERVAL: Duration = Duration::from_secs(60 * 60);
|
||||
|
||||
actions!(
|
||||
auto_update,
|
||||
[
|
||||
Check,
|
||||
DismissErrorMessage,
|
||||
ViewReleaseNotes,
|
||||
ViewReleaseNotesLocally
|
||||
]
|
||||
);
|
||||
actions!(auto_update, [Check, DismissErrorMessage, ViewReleaseNotes,]);
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct UpdateRequestBody {
|
||||
@@ -146,12 +127,6 @@ struct GlobalAutoUpdate(Option<Model<AutoUpdater>>);
|
||||
|
||||
impl Global for GlobalAutoUpdate {}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ReleaseNotesBody {
|
||||
title: String,
|
||||
release_notes: String,
|
||||
}
|
||||
|
||||
pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut AppContext) {
|
||||
AutoUpdateSetting::register(cx);
|
||||
|
||||
@@ -161,10 +136,6 @@ pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut AppContext) {
|
||||
workspace.register_action(|_, action, cx| {
|
||||
view_release_notes(action, cx);
|
||||
});
|
||||
|
||||
workspace.register_action(|workspace, _: &ViewReleaseNotesLocally, cx| {
|
||||
view_release_notes_locally(workspace, cx);
|
||||
});
|
||||
})
|
||||
.detach();
|
||||
|
||||
@@ -264,121 +235,6 @@ pub fn view_release_notes(_: &ViewReleaseNotes, cx: &mut AppContext) -> Option<(
|
||||
None
|
||||
}
|
||||
|
||||
fn view_release_notes_locally(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
|
||||
let release_channel = ReleaseChannel::global(cx);
|
||||
|
||||
let url = match release_channel {
|
||||
ReleaseChannel::Nightly => Some("https://github.com/zed-industries/zed/commits/nightly/"),
|
||||
ReleaseChannel::Dev => Some("https://github.com/zed-industries/zed/commits/main/"),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if let Some(url) = url {
|
||||
cx.open_url(url);
|
||||
return;
|
||||
}
|
||||
|
||||
let version = AppVersion::global(cx).to_string();
|
||||
|
||||
let client = client::Client::global(cx).http_client();
|
||||
let url = client.build_url(&format!(
|
||||
"/api/release_notes/v2/{}/{}",
|
||||
release_channel.dev_name(),
|
||||
version
|
||||
));
|
||||
|
||||
let markdown = workspace
|
||||
.app_state()
|
||||
.languages
|
||||
.language_for_name("Markdown");
|
||||
|
||||
workspace
|
||||
.with_local_workspace(cx, move |_, cx| {
|
||||
cx.spawn(|workspace, mut cx| async move {
|
||||
let markdown = markdown.await.log_err();
|
||||
let response = client.get(&url, Default::default(), true).await;
|
||||
let Some(mut response) = response.log_err() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut body = Vec::new();
|
||||
response.body_mut().read_to_end(&mut body).await.ok();
|
||||
|
||||
let body: serde_json::Result<ReleaseNotesBody> =
|
||||
serde_json::from_slice(body.as_slice());
|
||||
|
||||
if let Ok(body) = body {
|
||||
workspace
|
||||
.update(&mut cx, |workspace, cx| {
|
||||
let project = workspace.project().clone();
|
||||
let buffer = project.update(cx, |project, cx| {
|
||||
project.create_local_buffer("", markdown, cx)
|
||||
});
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit([(0..0, body.release_notes)], None, cx)
|
||||
});
|
||||
let language_registry = project.read(cx).languages().clone();
|
||||
|
||||
let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
|
||||
let tab_description = SharedString::from(body.title.to_string());
|
||||
let editor = cx.new_view(|cx| {
|
||||
Editor::for_multibuffer(buffer, Some(project), true, cx)
|
||||
});
|
||||
let workspace_handle = workspace.weak_handle();
|
||||
let view: View<MarkdownPreviewView> = MarkdownPreviewView::new(
|
||||
MarkdownPreviewMode::Default,
|
||||
editor,
|
||||
workspace_handle,
|
||||
language_registry,
|
||||
Some(tab_description),
|
||||
cx,
|
||||
);
|
||||
workspace.add_item_to_active_pane(
|
||||
Box::new(view.clone()),
|
||||
None,
|
||||
true,
|
||||
cx,
|
||||
);
|
||||
cx.notify();
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub fn notify_of_any_new_update(cx: &mut ViewContext<Workspace>) -> Option<()> {
|
||||
let updater = AutoUpdater::get(cx)?;
|
||||
let version = updater.read(cx).current_version;
|
||||
let should_show_notification = updater.read(cx).should_show_update_notification(cx);
|
||||
|
||||
cx.spawn(|workspace, mut cx| async move {
|
||||
let should_show_notification = should_show_notification.await?;
|
||||
if should_show_notification {
|
||||
workspace.update(&mut cx, |workspace, cx| {
|
||||
let workspace_handle = workspace.weak_handle();
|
||||
workspace.show_notification(
|
||||
NotificationId::unique::<UpdateNotification>(),
|
||||
cx,
|
||||
|cx| cx.new_view(|_| UpdateNotification::new(version, workspace_handle)),
|
||||
);
|
||||
updater.update(cx, |updater, cx| {
|
||||
updater
|
||||
.set_should_show_update_notification(false, cx)
|
||||
.detach_and_log_err(cx);
|
||||
});
|
||||
})?;
|
||||
}
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach();
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
impl AutoUpdater {
|
||||
pub fn get(cx: &mut AppContext) -> Option<Model<Self>> {
|
||||
cx.default_global::<GlobalAutoUpdate>().0.clone()
|
||||
@@ -423,6 +279,10 @@ impl AutoUpdater {
|
||||
}));
|
||||
}
|
||||
|
||||
pub fn current_version(&self) -> SemanticVersion {
|
||||
self.current_version
|
||||
}
|
||||
|
||||
pub fn status(&self) -> AutoUpdateStatus {
|
||||
self.status.clone()
|
||||
}
|
||||
@@ -646,7 +506,7 @@ impl AutoUpdater {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_should_show_update_notification(
|
||||
pub fn set_should_show_update_notification(
|
||||
&self,
|
||||
should_show: bool,
|
||||
cx: &AppContext,
|
||||
@@ -668,7 +528,7 @@ impl AutoUpdater {
|
||||
})
|
||||
}
|
||||
|
||||
fn should_show_update_notification(&self, cx: &AppContext) -> Task<Result<bool>> {
|
||||
pub fn should_show_update_notification(&self, cx: &AppContext) -> Task<Result<bool>> {
|
||||
cx.background_executor().spawn(async move {
|
||||
Ok(KEY_VALUE_STORE
|
||||
.read_kvp(SHOULD_SHOW_UPDATE_NOTIFICATION_KEY)?
|
||||
|
||||
28
crates/auto_update_ui/Cargo.toml
Normal file
28
crates/auto_update_ui/Cargo.toml
Normal file
@@ -0,0 +1,28 @@
|
||||
[package]
|
||||
name = "auto_update_ui"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/auto_update_ui.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
auto_update.workspace = true
|
||||
client.workspace = true
|
||||
editor.workspace = true
|
||||
gpui.workspace = true
|
||||
http_client.workspace = true
|
||||
markdown_preview.workspace = true
|
||||
menu.workspace = true
|
||||
release_channel.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
smol.workspace = true
|
||||
util.workspace = true
|
||||
workspace.workspace = true
|
||||
147
crates/auto_update_ui/src/auto_update_ui.rs
Normal file
147
crates/auto_update_ui/src/auto_update_ui.rs
Normal file
@@ -0,0 +1,147 @@
|
||||
mod update_notification;
|
||||
|
||||
use auto_update::AutoUpdater;
|
||||
use editor::{Editor, MultiBuffer};
|
||||
use gpui::{actions, prelude::*, AppContext, SharedString, View, ViewContext};
|
||||
use http_client::HttpClient;
|
||||
use markdown_preview::markdown_preview_view::{MarkdownPreviewMode, MarkdownPreviewView};
|
||||
use release_channel::{AppVersion, ReleaseChannel};
|
||||
use serde::Deserialize;
|
||||
use smol::io::AsyncReadExt;
|
||||
use util::ResultExt as _;
|
||||
use workspace::notifications::NotificationId;
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::update_notification::UpdateNotification;
|
||||
|
||||
actions!(auto_update, [ViewReleaseNotesLocally]);
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.observe_new_views(|workspace: &mut Workspace, _cx| {
|
||||
workspace.register_action(|workspace, _: &ViewReleaseNotesLocally, cx| {
|
||||
view_release_notes_locally(workspace, cx);
|
||||
});
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ReleaseNotesBody {
|
||||
title: String,
|
||||
release_notes: String,
|
||||
}
|
||||
|
||||
fn view_release_notes_locally(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
|
||||
let release_channel = ReleaseChannel::global(cx);
|
||||
|
||||
let url = match release_channel {
|
||||
ReleaseChannel::Nightly => Some("https://github.com/zed-industries/zed/commits/nightly/"),
|
||||
ReleaseChannel::Dev => Some("https://github.com/zed-industries/zed/commits/main/"),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if let Some(url) = url {
|
||||
cx.open_url(url);
|
||||
return;
|
||||
}
|
||||
|
||||
let version = AppVersion::global(cx).to_string();
|
||||
|
||||
let client = client::Client::global(cx).http_client();
|
||||
let url = client.build_url(&format!(
|
||||
"/api/release_notes/v2/{}/{}",
|
||||
release_channel.dev_name(),
|
||||
version
|
||||
));
|
||||
|
||||
let markdown = workspace
|
||||
.app_state()
|
||||
.languages
|
||||
.language_for_name("Markdown");
|
||||
|
||||
workspace
|
||||
.with_local_workspace(cx, move |_, cx| {
|
||||
cx.spawn(|workspace, mut cx| async move {
|
||||
let markdown = markdown.await.log_err();
|
||||
let response = client.get(&url, Default::default(), true).await;
|
||||
let Some(mut response) = response.log_err() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut body = Vec::new();
|
||||
response.body_mut().read_to_end(&mut body).await.ok();
|
||||
|
||||
let body: serde_json::Result<ReleaseNotesBody> =
|
||||
serde_json::from_slice(body.as_slice());
|
||||
|
||||
if let Ok(body) = body {
|
||||
workspace
|
||||
.update(&mut cx, |workspace, cx| {
|
||||
let project = workspace.project().clone();
|
||||
let buffer = project.update(cx, |project, cx| {
|
||||
project.create_local_buffer("", markdown, cx)
|
||||
});
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit([(0..0, body.release_notes)], None, cx)
|
||||
});
|
||||
let language_registry = project.read(cx).languages().clone();
|
||||
|
||||
let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
|
||||
let tab_description = SharedString::from(body.title.to_string());
|
||||
let editor = cx.new_view(|cx| {
|
||||
Editor::for_multibuffer(buffer, Some(project), true, cx)
|
||||
});
|
||||
let workspace_handle = workspace.weak_handle();
|
||||
let view: View<MarkdownPreviewView> = MarkdownPreviewView::new(
|
||||
MarkdownPreviewMode::Default,
|
||||
editor,
|
||||
workspace_handle,
|
||||
language_registry,
|
||||
Some(tab_description),
|
||||
cx,
|
||||
);
|
||||
workspace.add_item_to_active_pane(
|
||||
Box::new(view.clone()),
|
||||
None,
|
||||
true,
|
||||
cx,
|
||||
);
|
||||
cx.notify();
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub fn notify_of_any_new_update(cx: &mut ViewContext<Workspace>) -> Option<()> {
|
||||
let updater = AutoUpdater::get(cx)?;
|
||||
let version = updater.read(cx).current_version();
|
||||
let should_show_notification = updater.read(cx).should_show_update_notification(cx);
|
||||
|
||||
cx.spawn(|workspace, mut cx| async move {
|
||||
let should_show_notification = should_show_notification.await?;
|
||||
if should_show_notification {
|
||||
workspace.update(&mut cx, |workspace, cx| {
|
||||
let workspace_handle = workspace.weak_handle();
|
||||
workspace.show_notification(
|
||||
NotificationId::unique::<UpdateNotification>(),
|
||||
cx,
|
||||
|cx| cx.new_view(|_| UpdateNotification::new(version, workspace_handle)),
|
||||
);
|
||||
updater.update(cx, |updater, cx| {
|
||||
updater
|
||||
.set_should_show_update_notification(false, cx)
|
||||
.detach_and_log_err(cx);
|
||||
});
|
||||
})?;
|
||||
}
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach();
|
||||
|
||||
None
|
||||
}
|
||||
@@ -343,7 +343,7 @@ fn init_test(cx: &mut AppContext) -> Model<ChannelStore> {
|
||||
release_channel::init(SemanticVersion::default(), cx);
|
||||
client::init_settings(cx);
|
||||
|
||||
let clock = Arc::new(FakeSystemClock::default());
|
||||
let clock = Arc::new(FakeSystemClock::new());
|
||||
let http = FakeHttpClient::with_404_response();
|
||||
let client = Client::new(clock, http.clone(), cx);
|
||||
let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx));
|
||||
|
||||
@@ -42,7 +42,6 @@ serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
sha2.workspace = true
|
||||
smol.workspace = true
|
||||
sysinfo.workspace = true
|
||||
telemetry_events.workspace = true
|
||||
text.workspace = true
|
||||
thiserror.workspace = true
|
||||
|
||||
@@ -1780,7 +1780,7 @@ mod tests {
|
||||
let user_id = 5;
|
||||
let client = cx.update(|cx| {
|
||||
Client::new(
|
||||
Arc::new(FakeSystemClock::default()),
|
||||
Arc::new(FakeSystemClock::new()),
|
||||
FakeHttpClient::with_404_response(),
|
||||
cx,
|
||||
)
|
||||
@@ -1821,7 +1821,7 @@ mod tests {
|
||||
let user_id = 5;
|
||||
let client = cx.update(|cx| {
|
||||
Client::new(
|
||||
Arc::new(FakeSystemClock::default()),
|
||||
Arc::new(FakeSystemClock::new()),
|
||||
FakeHttpClient::with_404_response(),
|
||||
cx,
|
||||
)
|
||||
@@ -1900,7 +1900,7 @@ mod tests {
|
||||
let dropped_auth_count = Arc::new(Mutex::new(0));
|
||||
let client = cx.update(|cx| {
|
||||
Client::new(
|
||||
Arc::new(FakeSystemClock::default()),
|
||||
Arc::new(FakeSystemClock::new()),
|
||||
FakeHttpClient::with_404_response(),
|
||||
cx,
|
||||
)
|
||||
@@ -1943,7 +1943,7 @@ mod tests {
|
||||
let user_id = 5;
|
||||
let client = cx.update(|cx| {
|
||||
Client::new(
|
||||
Arc::new(FakeSystemClock::default()),
|
||||
Arc::new(FakeSystemClock::new()),
|
||||
FakeHttpClient::with_404_response(),
|
||||
cx,
|
||||
)
|
||||
@@ -2003,7 +2003,7 @@ mod tests {
|
||||
let user_id = 5;
|
||||
let client = cx.update(|cx| {
|
||||
Client::new(
|
||||
Arc::new(FakeSystemClock::default()),
|
||||
Arc::new(FakeSystemClock::new()),
|
||||
FakeHttpClient::with_404_response(),
|
||||
cx,
|
||||
)
|
||||
@@ -2038,7 +2038,7 @@ mod tests {
|
||||
let user_id = 5;
|
||||
let client = cx.update(|cx| {
|
||||
Client::new(
|
||||
Arc::new(FakeSystemClock::default()),
|
||||
Arc::new(FakeSystemClock::new()),
|
||||
FakeHttpClient::with_404_response(),
|
||||
cx,
|
||||
)
|
||||
|
||||
@@ -2,7 +2,6 @@ mod event_coalescer;
|
||||
|
||||
use crate::{ChannelId, TelemetrySettings};
|
||||
use anyhow::Result;
|
||||
use chrono::{DateTime, Utc};
|
||||
use clock::SystemClock;
|
||||
use collections::{HashMap, HashSet};
|
||||
use futures::Future;
|
||||
@@ -15,12 +14,11 @@ use settings::{Settings, SettingsStore};
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::time::Instant;
|
||||
use std::{env, mem, path::PathBuf, sync::Arc, time::Duration};
|
||||
use sysinfo::{CpuRefreshKind, Pid, ProcessRefreshKind, RefreshKind, System};
|
||||
use telemetry_events::{
|
||||
ActionEvent, AppEvent, AssistantEvent, CallEvent, CpuEvent, EditEvent, EditorEvent, Event,
|
||||
EventRequestBody, EventWrapper, ExtensionEvent, InlineCompletionEvent, MemoryEvent, ReplEvent,
|
||||
SettingEvent,
|
||||
ActionEvent, AppEvent, AssistantEvent, CallEvent, EditEvent, EditorEvent, Event,
|
||||
EventRequestBody, EventWrapper, ExtensionEvent, InlineCompletionEvent, ReplEvent, SettingEvent,
|
||||
};
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
use worktree::{UpdatedEntriesSet, WorktreeId};
|
||||
@@ -46,7 +44,7 @@ struct TelemetryState {
|
||||
flush_events_task: Option<Task<()>>,
|
||||
log_file: Option<File>,
|
||||
is_staff: Option<bool>,
|
||||
first_event_date_time: Option<DateTime<Utc>>,
|
||||
first_event_date_time: Option<Instant>,
|
||||
event_coalescer: EventCoalescer,
|
||||
max_queue_size: usize,
|
||||
worktree_id_map: WorktreeIdMap,
|
||||
@@ -226,6 +224,8 @@ impl Telemetry {
|
||||
cx.background_executor()
|
||||
.spawn({
|
||||
let state = state.clone();
|
||||
let os_version = os_version();
|
||||
state.lock().os_version = Some(os_version.clone());
|
||||
async move {
|
||||
if let Some(tempfile) = File::create(Self::log_file_path()).log_err() {
|
||||
state.lock().log_file = Some(tempfile);
|
||||
@@ -293,48 +293,6 @@ impl Telemetry {
|
||||
state.session_id = Some(session_id);
|
||||
state.app_version = release_channel::AppVersion::global(cx).to_string();
|
||||
state.os_name = os_name();
|
||||
|
||||
drop(state);
|
||||
|
||||
let this = self.clone();
|
||||
cx.background_executor()
|
||||
.spawn(async move {
|
||||
let mut system = System::new_with_specifics(
|
||||
RefreshKind::new().with_cpu(CpuRefreshKind::everything()),
|
||||
);
|
||||
|
||||
let refresh_kind = ProcessRefreshKind::new().with_cpu().with_memory();
|
||||
let current_process = Pid::from_u32(std::process::id());
|
||||
system.refresh_processes_specifics(
|
||||
sysinfo::ProcessesToUpdate::Some(&[current_process]),
|
||||
refresh_kind,
|
||||
);
|
||||
|
||||
// Waiting some amount of time before the first query is important to get a reasonable value
|
||||
// https://docs.rs/sysinfo/0.29.10/sysinfo/trait.ProcessExt.html#tymethod.cpu_usage
|
||||
const DURATION_BETWEEN_SYSTEM_EVENTS: Duration = Duration::from_secs(4 * 60);
|
||||
|
||||
loop {
|
||||
smol::Timer::after(DURATION_BETWEEN_SYSTEM_EVENTS).await;
|
||||
|
||||
let current_process = Pid::from_u32(std::process::id());
|
||||
system.refresh_processes_specifics(
|
||||
sysinfo::ProcessesToUpdate::Some(&[current_process]),
|
||||
refresh_kind,
|
||||
);
|
||||
let Some(process) = system.process(current_process) else {
|
||||
log::error!(
|
||||
"Failed to find own process {current_process:?} in system process table"
|
||||
);
|
||||
// TODO: Fire an error telemetry event
|
||||
return;
|
||||
};
|
||||
|
||||
this.report_memory_event(process.memory(), process.virtual_memory());
|
||||
this.report_cpu_event(process.cpu_usage(), system.cpus().len() as u32);
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub fn metrics_enabled(self: &Arc<Self>) -> bool {
|
||||
@@ -416,28 +374,6 @@ impl Telemetry {
|
||||
self.report_event(event)
|
||||
}
|
||||
|
||||
pub fn report_cpu_event(self: &Arc<Self>, usage_as_percentage: f32, core_count: u32) {
|
||||
let event = Event::Cpu(CpuEvent {
|
||||
usage_as_percentage,
|
||||
core_count,
|
||||
});
|
||||
|
||||
self.report_event(event)
|
||||
}
|
||||
|
||||
pub fn report_memory_event(
|
||||
self: &Arc<Self>,
|
||||
memory_in_bytes: u64,
|
||||
virtual_memory_in_bytes: u64,
|
||||
) {
|
||||
let event = Event::Memory(MemoryEvent {
|
||||
memory_in_bytes,
|
||||
virtual_memory_in_bytes,
|
||||
});
|
||||
|
||||
self.report_event(event)
|
||||
}
|
||||
|
||||
pub fn report_app_event(self: &Arc<Self>, operation: String) -> Event {
|
||||
let event = Event::App(AppEvent { operation });
|
||||
|
||||
@@ -469,7 +405,10 @@ impl Telemetry {
|
||||
|
||||
if let Some((start, end, environment)) = period_data {
|
||||
let event = Event::Edit(EditEvent {
|
||||
duration: end.timestamp_millis() - start.timestamp_millis(),
|
||||
duration: end
|
||||
.saturating_duration_since(start)
|
||||
.min(Duration::from_secs(60 * 60 * 24))
|
||||
.as_millis() as i64,
|
||||
environment: environment.to_string(),
|
||||
is_via_ssh,
|
||||
});
|
||||
@@ -567,9 +506,10 @@ impl Telemetry {
|
||||
let date_time = self.clock.utc_now();
|
||||
|
||||
let milliseconds_since_first_event = match state.first_event_date_time {
|
||||
Some(first_event_date_time) => {
|
||||
date_time.timestamp_millis() - first_event_date_time.timestamp_millis()
|
||||
}
|
||||
Some(first_event_date_time) => date_time
|
||||
.saturating_duration_since(first_event_date_time)
|
||||
.min(Duration::from_secs(60 * 60 * 24))
|
||||
.as_millis() as i64,
|
||||
None => {
|
||||
state.first_event_date_time = Some(date_time);
|
||||
0
|
||||
@@ -702,7 +642,6 @@ pub fn calculate_json_checksum(json: &impl AsRef<[u8]>) -> Option<String> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use chrono::TimeZone;
|
||||
use clock::FakeSystemClock;
|
||||
use gpui::TestAppContext;
|
||||
use http_client::FakeHttpClient;
|
||||
@@ -710,9 +649,7 @@ mod tests {
|
||||
#[gpui::test]
|
||||
fn test_telemetry_flush_on_max_queue_size(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let clock = Arc::new(FakeSystemClock::new(
|
||||
Utc.with_ymd_and_hms(1990, 4, 12, 12, 0, 0).unwrap(),
|
||||
));
|
||||
let clock = Arc::new(FakeSystemClock::new());
|
||||
let http = FakeHttpClient::with_200_response();
|
||||
let system_id = Some("system_id".to_string());
|
||||
let installation_id = Some("installation_id".to_string());
|
||||
@@ -743,7 +680,7 @@ mod tests {
|
||||
Some(first_date_time)
|
||||
);
|
||||
|
||||
clock.advance(chrono::Duration::milliseconds(100));
|
||||
clock.advance(Duration::from_millis(100));
|
||||
|
||||
let event = telemetry.report_app_event(operation.clone());
|
||||
assert_eq!(
|
||||
@@ -759,7 +696,7 @@ mod tests {
|
||||
Some(first_date_time)
|
||||
);
|
||||
|
||||
clock.advance(chrono::Duration::milliseconds(100));
|
||||
clock.advance(Duration::from_millis(100));
|
||||
|
||||
let event = telemetry.report_app_event(operation.clone());
|
||||
assert_eq!(
|
||||
@@ -775,7 +712,7 @@ mod tests {
|
||||
Some(first_date_time)
|
||||
);
|
||||
|
||||
clock.advance(chrono::Duration::milliseconds(100));
|
||||
clock.advance(Duration::from_millis(100));
|
||||
|
||||
// Adding a 4th event should cause a flush
|
||||
let event = telemetry.report_app_event(operation.clone());
|
||||
@@ -796,9 +733,7 @@ mod tests {
|
||||
cx: &mut TestAppContext,
|
||||
) {
|
||||
init_test(cx);
|
||||
let clock = Arc::new(FakeSystemClock::new(
|
||||
Utc.with_ymd_and_hms(1990, 4, 12, 12, 0, 0).unwrap(),
|
||||
));
|
||||
let clock = Arc::new(FakeSystemClock::new());
|
||||
let http = FakeHttpClient::with_200_response();
|
||||
let system_id = Some("system_id".to_string());
|
||||
let installation_id = Some("installation_id".to_string());
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use std::sync::Arc;
|
||||
use std::time;
|
||||
use std::{sync::Arc, time::Instant};
|
||||
|
||||
use chrono::{DateTime, Duration, Utc};
|
||||
use clock::SystemClock;
|
||||
|
||||
const COALESCE_TIMEOUT: time::Duration = time::Duration::from_secs(20);
|
||||
@@ -10,8 +9,8 @@ const SIMULATED_DURATION_FOR_SINGLE_EVENT: time::Duration = time::Duration::from
|
||||
#[derive(Debug, PartialEq)]
|
||||
struct PeriodData {
|
||||
environment: &'static str,
|
||||
start: DateTime<Utc>,
|
||||
end: Option<DateTime<Utc>>,
|
||||
start: Instant,
|
||||
end: Option<Instant>,
|
||||
}
|
||||
|
||||
pub struct EventCoalescer {
|
||||
@@ -27,9 +26,8 @@ impl EventCoalescer {
|
||||
pub fn log_event(
|
||||
&mut self,
|
||||
environment: &'static str,
|
||||
) -> Option<(DateTime<Utc>, DateTime<Utc>, &'static str)> {
|
||||
) -> Option<(Instant, Instant, &'static str)> {
|
||||
let log_time = self.clock.utc_now();
|
||||
let coalesce_timeout = Duration::from_std(COALESCE_TIMEOUT).unwrap();
|
||||
|
||||
let Some(state) = &mut self.state else {
|
||||
self.state = Some(PeriodData {
|
||||
@@ -43,7 +41,7 @@ impl EventCoalescer {
|
||||
let period_end = state
|
||||
.end
|
||||
.unwrap_or(state.start + SIMULATED_DURATION_FOR_SINGLE_EVENT);
|
||||
let within_timeout = log_time - period_end < coalesce_timeout;
|
||||
let within_timeout = log_time - period_end < COALESCE_TIMEOUT;
|
||||
let environment_is_same = state.environment == environment;
|
||||
let should_coaelesce = !within_timeout || !environment_is_same;
|
||||
|
||||
@@ -70,16 +68,13 @@ impl EventCoalescer {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use chrono::TimeZone;
|
||||
use clock::FakeSystemClock;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_same_context_exceeding_timeout() {
|
||||
let clock = Arc::new(FakeSystemClock::new(
|
||||
Utc.with_ymd_and_hms(1990, 4, 12, 0, 0, 0).unwrap(),
|
||||
));
|
||||
let clock = Arc::new(FakeSystemClock::new());
|
||||
let environment_1 = "environment_1";
|
||||
let mut event_coalescer = EventCoalescer::new(clock.clone());
|
||||
|
||||
@@ -98,7 +93,7 @@ mod tests {
|
||||
})
|
||||
);
|
||||
|
||||
let within_timeout_adjustment = Duration::from_std(COALESCE_TIMEOUT / 2).unwrap();
|
||||
let within_timeout_adjustment = COALESCE_TIMEOUT / 2;
|
||||
|
||||
// Ensure that many calls within the timeout don't start a new period
|
||||
for _ in 0..100 {
|
||||
@@ -118,7 +113,7 @@ mod tests {
|
||||
}
|
||||
|
||||
let period_end = clock.utc_now();
|
||||
let exceed_timeout_adjustment = Duration::from_std(COALESCE_TIMEOUT * 2).unwrap();
|
||||
let exceed_timeout_adjustment = COALESCE_TIMEOUT * 2;
|
||||
// Logging an event exceeding the timeout should start a new period
|
||||
clock.advance(exceed_timeout_adjustment);
|
||||
let new_period_start = clock.utc_now();
|
||||
@@ -137,9 +132,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_different_environment_under_timeout() {
|
||||
let clock = Arc::new(FakeSystemClock::new(
|
||||
Utc.with_ymd_and_hms(1990, 4, 12, 0, 0, 0).unwrap(),
|
||||
));
|
||||
let clock = Arc::new(FakeSystemClock::new());
|
||||
let environment_1 = "environment_1";
|
||||
let mut event_coalescer = EventCoalescer::new(clock.clone());
|
||||
|
||||
@@ -158,7 +151,7 @@ mod tests {
|
||||
})
|
||||
);
|
||||
|
||||
let within_timeout_adjustment = Duration::from_std(COALESCE_TIMEOUT / 2).unwrap();
|
||||
let within_timeout_adjustment = COALESCE_TIMEOUT / 2;
|
||||
clock.advance(within_timeout_adjustment);
|
||||
let period_end = clock.utc_now();
|
||||
let period_data = event_coalescer.log_event(environment_1);
|
||||
@@ -193,9 +186,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_switching_environment_while_within_timeout() {
|
||||
let clock = Arc::new(FakeSystemClock::new(
|
||||
Utc.with_ymd_and_hms(1990, 4, 12, 0, 0, 0).unwrap(),
|
||||
));
|
||||
let clock = Arc::new(FakeSystemClock::new());
|
||||
let environment_1 = "environment_1";
|
||||
let mut event_coalescer = EventCoalescer::new(clock.clone());
|
||||
|
||||
@@ -214,7 +205,7 @@ mod tests {
|
||||
})
|
||||
);
|
||||
|
||||
let within_timeout_adjustment = Duration::from_std(COALESCE_TIMEOUT / 2).unwrap();
|
||||
let within_timeout_adjustment = COALESCE_TIMEOUT / 2;
|
||||
clock.advance(within_timeout_adjustment);
|
||||
let period_end = clock.utc_now();
|
||||
let environment_2 = "environment_2";
|
||||
@@ -240,9 +231,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_switching_environment_while_exceeding_timeout() {
|
||||
let clock = Arc::new(FakeSystemClock::new(
|
||||
Utc.with_ymd_and_hms(1990, 4, 12, 0, 0, 0).unwrap(),
|
||||
));
|
||||
let clock = Arc::new(FakeSystemClock::new());
|
||||
let environment_1 = "environment_1";
|
||||
let mut event_coalescer = EventCoalescer::new(clock.clone());
|
||||
|
||||
@@ -261,7 +250,7 @@ mod tests {
|
||||
})
|
||||
);
|
||||
|
||||
let exceed_timeout_adjustment = Duration::from_std(COALESCE_TIMEOUT * 2).unwrap();
|
||||
let exceed_timeout_adjustment = COALESCE_TIMEOUT * 2;
|
||||
clock.advance(exceed_timeout_adjustment);
|
||||
let period_end = clock.utc_now();
|
||||
let environment_2 = "environment_2";
|
||||
|
||||
@@ -16,7 +16,6 @@ doctest = false
|
||||
test-support = ["dep:parking_lot"]
|
||||
|
||||
[dependencies]
|
||||
chrono.workspace = true
|
||||
parking_lot = { workspace = true, optional = true }
|
||||
serde.workspace = true
|
||||
smallvec.workspace = true
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use std::time::Instant;
|
||||
|
||||
pub trait SystemClock: Send + Sync {
|
||||
/// Returns the current date and time in UTC.
|
||||
fn utc_now(&self) -> DateTime<Utc>;
|
||||
fn utc_now(&self) -> Instant;
|
||||
}
|
||||
|
||||
pub struct RealSystemClock;
|
||||
|
||||
impl SystemClock for RealSystemClock {
|
||||
fn utc_now(&self) -> DateTime<Utc> {
|
||||
Utc::now()
|
||||
fn utc_now(&self) -> Instant {
|
||||
Instant::now()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub struct FakeSystemClockState {
|
||||
now: DateTime<Utc>,
|
||||
now: Instant,
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
@@ -24,36 +24,30 @@ pub struct FakeSystemClock {
|
||||
state: parking_lot::Mutex<FakeSystemClockState>,
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
impl Default for FakeSystemClock {
|
||||
fn default() -> Self {
|
||||
Self::new(Utc::now())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
impl FakeSystemClock {
|
||||
pub fn new(now: DateTime<Utc>) -> Self {
|
||||
let state = FakeSystemClockState { now };
|
||||
pub fn new() -> Self {
|
||||
let state = FakeSystemClockState {
|
||||
now: Instant::now(),
|
||||
};
|
||||
|
||||
Self {
|
||||
state: parking_lot::Mutex::new(state),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_now(&self, now: DateTime<Utc>) {
|
||||
pub fn set_now(&self, now: Instant) {
|
||||
self.state.lock().now = now;
|
||||
}
|
||||
|
||||
/// Advances the [`FakeSystemClock`] by the specified [`Duration`](chrono::Duration).
|
||||
pub fn advance(&self, duration: chrono::Duration) {
|
||||
pub fn advance(&self, duration: std::time::Duration) {
|
||||
self.state.lock().now += duration;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
impl SystemClock for FakeSystemClock {
|
||||
fn utc_now(&self) -> DateTime<Utc> {
|
||||
fn utc_now(&self) -> Instant {
|
||||
self.state.lock().now
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,6 +90,7 @@ collections = { workspace = true, features = ["test-support"] }
|
||||
ctor.workspace = true
|
||||
editor = { workspace = true, features = ["test-support"] }
|
||||
env_logger.workspace = true
|
||||
extension.workspace = true
|
||||
file_finder.workspace = true
|
||||
fs = { workspace = true, features = ["test-support"] }
|
||||
git = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -418,7 +418,7 @@ pub async fn post_events(
|
||||
if let Some(kinesis_client) = app.kinesis_client.clone() {
|
||||
if let Some(stream) = app.config.kinesis_stream.clone() {
|
||||
let mut request = kinesis_client.put_records().stream_name(stream);
|
||||
for row in for_snowflake(request_body.clone(), first_event_at) {
|
||||
for row in for_snowflake(request_body.clone(), first_event_at, country_code.clone()) {
|
||||
if let Some(data) = serde_json::to_vec(&row).log_err() {
|
||||
request = request.records(
|
||||
aws_sdk_kinesis::types::PutRecordsRequestEntry::builder()
|
||||
@@ -483,20 +483,7 @@ pub async fn post_events(
|
||||
checksum_matched,
|
||||
))
|
||||
}
|
||||
Event::Cpu(event) => to_upload.cpu_events.push(CpuEventRow::from_event(
|
||||
event.clone(),
|
||||
wrapper,
|
||||
&request_body,
|
||||
first_event_at,
|
||||
checksum_matched,
|
||||
)),
|
||||
Event::Memory(event) => to_upload.memory_events.push(MemoryEventRow::from_event(
|
||||
event.clone(),
|
||||
wrapper,
|
||||
&request_body,
|
||||
first_event_at,
|
||||
checksum_matched,
|
||||
)),
|
||||
Event::Cpu(_) | Event::Memory(_) => continue,
|
||||
Event::App(event) => to_upload.app_events.push(AppEventRow::from_event(
|
||||
event.clone(),
|
||||
wrapper,
|
||||
@@ -947,6 +934,7 @@ pub struct CpuEventRow {
|
||||
}
|
||||
|
||||
impl CpuEventRow {
|
||||
#[allow(unused)]
|
||||
fn from_event(
|
||||
event: CpuEvent,
|
||||
wrapper: &EventWrapper,
|
||||
@@ -1001,6 +989,7 @@ pub struct MemoryEventRow {
|
||||
}
|
||||
|
||||
impl MemoryEventRow {
|
||||
#[allow(unused)]
|
||||
fn from_event(
|
||||
event: MemoryEvent,
|
||||
wrapper: &EventWrapper,
|
||||
@@ -1392,8 +1381,9 @@ pub fn calculate_json_checksum(app: Arc<AppState>, json: &impl AsRef<[u8]>) -> O
|
||||
fn for_snowflake(
|
||||
body: EventRequestBody,
|
||||
first_event_at: chrono::DateTime<chrono::Utc>,
|
||||
country_code: Option<String>,
|
||||
) -> impl Iterator<Item = SnowflakeRow> {
|
||||
body.events.into_iter().map(move |event| {
|
||||
body.events.into_iter().flat_map(move |event| {
|
||||
let timestamp =
|
||||
first_event_at + Duration::milliseconds(event.milliseconds_since_first_event);
|
||||
let (event_type, mut event_properties) = match &event.event {
|
||||
@@ -1450,14 +1440,7 @@ fn for_snowflake(
|
||||
},
|
||||
serde_json::to_value(e).unwrap(),
|
||||
),
|
||||
Event::Cpu(e) => (
|
||||
"System CPU Sampled".to_string(),
|
||||
serde_json::to_value(e).unwrap(),
|
||||
),
|
||||
Event::Memory(e) => (
|
||||
"System Memory Sampled".to_string(),
|
||||
serde_json::to_value(e).unwrap(),
|
||||
),
|
||||
Event::Cpu(_) | Event::Memory(_) => return None,
|
||||
Event::App(e) => {
|
||||
let mut properties = json!({});
|
||||
let event_type = match e.operation.trim() {
|
||||
@@ -1571,13 +1554,19 @@ fn for_snowflake(
|
||||
body.release_channel.clone().into(),
|
||||
);
|
||||
map.insert("signed_in".to_string(), event.signed_in.into());
|
||||
if let Some(country_code) = country_code.as_ref() {
|
||||
map.insert("country".to_string(), country_code.clone().into());
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: most amplitude user properties are read out of our event_properties
|
||||
// dictionary. See https://app.amplitude.com/data/zed/Zed/sources/detail/production/falcon%3A159998
|
||||
// for how that is configured.
|
||||
let user_properties = Some(serde_json::json!({
|
||||
"is_staff": body.is_staff,
|
||||
}));
|
||||
|
||||
SnowflakeRow {
|
||||
Some(SnowflakeRow {
|
||||
time: timestamp,
|
||||
user_id: body.metrics_id.clone(),
|
||||
device_id: body.system_id.clone(),
|
||||
@@ -1585,7 +1574,7 @@ fn for_snowflake(
|
||||
event_properties,
|
||||
user_properties,
|
||||
insert_id: Some(Uuid::new_v4().to_string()),
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1599,48 +1588,3 @@ struct SnowflakeRow {
|
||||
pub user_properties: Option<serde_json::Value>,
|
||||
pub insert_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct SnowflakeData {
|
||||
/// Identifier unique to each Zed installation (differs for stable, preview, dev)
|
||||
pub installation_id: Option<String>,
|
||||
/// Identifier unique to each logged in Zed user (randomly generated on first sign in)
|
||||
/// Identifier unique to each Zed session (differs for each time you open Zed)
|
||||
pub session_id: Option<String>,
|
||||
pub metrics_id: Option<String>,
|
||||
/// True for Zed staff, otherwise false
|
||||
pub is_staff: Option<bool>,
|
||||
/// Zed version number
|
||||
pub app_version: String,
|
||||
pub os_name: String,
|
||||
pub os_version: Option<String>,
|
||||
pub architecture: String,
|
||||
/// Zed release channel (stable, preview, dev)
|
||||
pub release_channel: Option<String>,
|
||||
pub signed_in: bool,
|
||||
|
||||
#[serde(flatten)]
|
||||
pub editor_event: Option<EditorEvent>,
|
||||
#[serde(flatten)]
|
||||
pub inline_completion_event: Option<InlineCompletionEvent>,
|
||||
#[serde(flatten)]
|
||||
pub call_event: Option<CallEvent>,
|
||||
#[serde(flatten)]
|
||||
pub assistant_event: Option<AssistantEvent>,
|
||||
#[serde(flatten)]
|
||||
pub cpu_event: Option<CpuEvent>,
|
||||
#[serde(flatten)]
|
||||
pub memory_event: Option<MemoryEvent>,
|
||||
#[serde(flatten)]
|
||||
pub app_event: Option<AppEvent>,
|
||||
#[serde(flatten)]
|
||||
pub setting_event: Option<SettingEvent>,
|
||||
#[serde(flatten)]
|
||||
pub extension_event: Option<ExtensionEvent>,
|
||||
#[serde(flatten)]
|
||||
pub edit_event: Option<EditEvent>,
|
||||
#[serde(flatten)]
|
||||
pub repl_event: Option<ReplEvent>,
|
||||
#[serde(flatten)]
|
||||
pub action_event: Option<ActionEvent>,
|
||||
}
|
||||
|
||||
@@ -4030,12 +4030,18 @@ async fn get_llm_api_token(
|
||||
Err(anyhow!("terms of service not accepted"))?
|
||||
}
|
||||
|
||||
let mut account_created_at = user.created_at;
|
||||
if let Some(github_created_at) = user.github_user_created_at {
|
||||
account_created_at = account_created_at.min(github_created_at);
|
||||
}
|
||||
if Utc::now().naive_utc() - account_created_at < MIN_ACCOUNT_AGE_FOR_LLM_USE {
|
||||
Err(anyhow!("account too young"))?
|
||||
let has_llm_subscription = session.has_llm_subscription(&db).await?;
|
||||
|
||||
let bypass_account_age_check =
|
||||
has_llm_subscription || flags.iter().any(|flag| flag == "bypass-account-age-check");
|
||||
if !bypass_account_age_check {
|
||||
let mut account_created_at = user.created_at;
|
||||
if let Some(github_created_at) = user.github_user_created_at {
|
||||
account_created_at = account_created_at.min(github_created_at);
|
||||
}
|
||||
if Utc::now().naive_utc() - account_created_at < MIN_ACCOUNT_AGE_FOR_LLM_USE {
|
||||
Err(anyhow!("account too young"))?
|
||||
}
|
||||
}
|
||||
|
||||
let billing_preferences = db.get_billing_preferences(user.id).await?;
|
||||
@@ -4045,7 +4051,7 @@ async fn get_llm_api_token(
|
||||
session.is_staff(),
|
||||
billing_preferences,
|
||||
has_llm_closed_beta_feature_flag,
|
||||
session.has_llm_subscription(&db).await?,
|
||||
has_llm_subscription,
|
||||
session.current_plan(&db).await?,
|
||||
&session.app_state.config,
|
||||
)?;
|
||||
|
||||
@@ -835,7 +835,7 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||
.map_ok(|_| ())
|
||||
.boxed(),
|
||||
LspRequestKind::CodeAction => project
|
||||
.code_actions(&buffer, offset..offset, cx)
|
||||
.code_actions(&buffer, offset..offset, None, cx)
|
||||
.map(|_| Ok(()))
|
||||
.boxed(),
|
||||
LspRequestKind::Definition => project
|
||||
@@ -1323,11 +1323,8 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||
match (host_file, guest_file) {
|
||||
(Some(host_file), Some(guest_file)) => {
|
||||
assert_eq!(guest_file.path(), host_file.path());
|
||||
assert_eq!(guest_file.is_deleted(), host_file.is_deleted());
|
||||
assert_eq!(
|
||||
guest_file.mtime(),
|
||||
host_file.mtime(),
|
||||
"guest {} mtime does not match host {} for path {:?} in project {}",
|
||||
assert_eq!(guest_file.disk_state(), host_file.disk_state(),
|
||||
"guest {} disk_state does not match host {} for path {:?} in project {}",
|
||||
guest_user_id,
|
||||
host_user_id,
|
||||
guest_file.path(),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::tests::TestServer;
|
||||
use call::ActiveCall;
|
||||
use collections::HashSet;
|
||||
use extension::ExtensionHostProxy;
|
||||
use fs::{FakeFs, Fs as _};
|
||||
use futures::StreamExt as _;
|
||||
use gpui::{BackgroundExecutor, Context as _, SemanticVersion, TestAppContext, UpdateGlobal as _};
|
||||
@@ -81,6 +82,7 @@ async fn test_sharing_an_ssh_remote_project(
|
||||
http_client: remote_http_client,
|
||||
node_runtime: node,
|
||||
languages,
|
||||
extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
|
||||
},
|
||||
cx,
|
||||
)
|
||||
@@ -243,6 +245,7 @@ async fn test_ssh_collaboration_git_branches(
|
||||
http_client: remote_http_client,
|
||||
node_runtime: node,
|
||||
languages,
|
||||
extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
|
||||
},
|
||||
cx,
|
||||
)
|
||||
@@ -400,6 +403,7 @@ async fn test_ssh_collaboration_formatting_with_prettier(
|
||||
http_client: remote_http_client,
|
||||
node_runtime: NodeRuntime::unavailable(),
|
||||
languages,
|
||||
extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
|
||||
},
|
||||
cx,
|
||||
)
|
||||
|
||||
@@ -168,7 +168,7 @@ impl TestServer {
|
||||
client::init_settings(cx);
|
||||
});
|
||||
|
||||
let clock = Arc::new(FakeSystemClock::default());
|
||||
let clock = Arc::new(FakeSystemClock::new());
|
||||
let http = FakeHttpClient::with_404_response();
|
||||
let user_id = if let Ok(Some(user)) = self.app_state.db.get_user_by_github_login(name).await
|
||||
{
|
||||
|
||||
@@ -58,12 +58,11 @@ settings.workspace = true
|
||||
smallvec.workspace = true
|
||||
story = { workspace = true, optional = true }
|
||||
theme.workspace = true
|
||||
time_format.workspace = true
|
||||
time.workspace = true
|
||||
time_format.workspace = true
|
||||
title_bar.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
vcs_menu.workspace = true
|
||||
workspace.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -33,7 +33,6 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
||||
notification_panel::init(cx);
|
||||
notifications::init(app_state, cx);
|
||||
title_bar::init(cx);
|
||||
vcs_menu::init(cx);
|
||||
}
|
||||
|
||||
fn notification_window_options(
|
||||
|
||||
@@ -11,7 +11,7 @@ use command_palette_hooks::{
|
||||
};
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use gpui::{
|
||||
actions, Action, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Global,
|
||||
Action, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Global,
|
||||
ParentElement, Render, Styled, Task, UpdateGlobal, View, ViewContext, VisualContext, WeakView,
|
||||
};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
@@ -21,9 +21,7 @@ use settings::Settings;
|
||||
use ui::{h_flex, prelude::*, v_flex, HighlightedLabel, KeyBinding, ListItem, ListItemSpacing};
|
||||
use util::ResultExt;
|
||||
use workspace::{ModalView, Workspace, WorkspaceSettings};
|
||||
use zed_actions::OpenZedUrl;
|
||||
|
||||
actions!(command_palette, [Toggle]);
|
||||
use zed_actions::{command_palette::Toggle, OpenZedUrl};
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
client::init_settings(cx);
|
||||
|
||||
@@ -15,6 +15,7 @@ path = "src/context_servers.rs"
|
||||
anyhow.workspace = true
|
||||
collections.workspace = true
|
||||
command_palette_hooks.workspace = true
|
||||
extension.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
log.workspace = true
|
||||
|
||||
@@ -9,7 +9,7 @@ use serde_json::{value::RawValue, Value};
|
||||
use smol::{
|
||||
channel,
|
||||
io::{AsyncBufReadExt, AsyncWriteExt, BufReader},
|
||||
process::{self, Child},
|
||||
process::Child,
|
||||
};
|
||||
use std::{
|
||||
fmt,
|
||||
@@ -152,7 +152,7 @@ impl Client {
|
||||
&binary.args
|
||||
);
|
||||
|
||||
let mut command = process::Command::new(&binary.executable);
|
||||
let mut command = util::command::new_smol_command(&binary.executable);
|
||||
command
|
||||
.args(&binary.args)
|
||||
.envs(binary.env.unwrap_or_default())
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod client;
|
||||
mod extension_context_server;
|
||||
pub mod manager;
|
||||
pub mod protocol;
|
||||
mod registry;
|
||||
@@ -19,6 +20,7 @@ pub const CONTEXT_SERVERS_NAMESPACE: &'static str = "context_servers";
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
ContextServerSettings::register(cx);
|
||||
ContextServerFactoryRegistry::default_global(cx);
|
||||
extension_context_server::init(cx);
|
||||
|
||||
CommandPaletteFilter::update_global(cx, |filter, _cx| {
|
||||
filter.hide_namespace(CONTEXT_SERVERS_NAMESPACE);
|
||||
|
||||
78
crates/context_servers/src/extension_context_server.rs
Normal file
78
crates/context_servers/src/extension_context_server.rs
Normal file
@@ -0,0 +1,78 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use extension::{Extension, ExtensionContextServerProxy, ExtensionHostProxy, ProjectDelegate};
|
||||
use gpui::{AppContext, Model};
|
||||
|
||||
use crate::manager::ServerCommand;
|
||||
use crate::ContextServerFactoryRegistry;
|
||||
|
||||
struct ExtensionProject {
|
||||
worktree_ids: Vec<u64>,
|
||||
}
|
||||
|
||||
impl ProjectDelegate for ExtensionProject {
|
||||
fn worktree_ids(&self) -> Vec<u64> {
|
||||
self.worktree_ids.clone()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
let proxy = ExtensionHostProxy::default_global(cx);
|
||||
proxy.register_context_server_proxy(ContextServerFactoryRegistryProxy {
|
||||
context_server_factory_registry: ContextServerFactoryRegistry::global(cx),
|
||||
});
|
||||
}
|
||||
|
||||
struct ContextServerFactoryRegistryProxy {
|
||||
context_server_factory_registry: Model<ContextServerFactoryRegistry>,
|
||||
}
|
||||
|
||||
impl ExtensionContextServerProxy for ContextServerFactoryRegistryProxy {
|
||||
fn register_context_server(
|
||||
&self,
|
||||
extension: Arc<dyn Extension>,
|
||||
id: Arc<str>,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
self.context_server_factory_registry
|
||||
.update(cx, |registry, _| {
|
||||
registry.register_server_factory(
|
||||
id.clone(),
|
||||
Arc::new({
|
||||
move |project, cx| {
|
||||
log::info!(
|
||||
"loading command for context server {id} from extension {}",
|
||||
extension.manifest().id
|
||||
);
|
||||
|
||||
let id = id.clone();
|
||||
let extension = extension.clone();
|
||||
cx.spawn(|mut cx| async move {
|
||||
let extension_project =
|
||||
project.update(&mut cx, |project, cx| {
|
||||
Arc::new(ExtensionProject {
|
||||
worktree_ids: project
|
||||
.visible_worktrees(cx)
|
||||
.map(|worktree| worktree.read(cx).id().to_proto())
|
||||
.collect(),
|
||||
})
|
||||
})?;
|
||||
|
||||
let command = extension
|
||||
.context_server_command(id.clone(), extension_project)
|
||||
.await?;
|
||||
|
||||
log::info!("loaded command for context server {id}: {command:?}");
|
||||
|
||||
Ok(ServerCommand {
|
||||
path: command.command,
|
||||
args: command.args,
|
||||
env: Some(command.env.into_iter().collect()),
|
||||
})
|
||||
})
|
||||
}
|
||||
}),
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,8 @@ use gpui::{AsyncAppContext, EventEmitter, Model, ModelContext, Subscription, Tas
|
||||
use log;
|
||||
use parking_lot::RwLock;
|
||||
use project::Project;
|
||||
use schemars::gen::SchemaGenerator;
|
||||
use schemars::schema::{InstanceType, Schema, SchemaObject};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsSources, SettingsStore};
|
||||
@@ -36,16 +38,32 @@ use crate::{
|
||||
|
||||
#[derive(Deserialize, Serialize, Default, Clone, PartialEq, Eq, JsonSchema, Debug)]
|
||||
pub struct ContextServerSettings {
|
||||
/// Settings for context servers used in the Assistant.
|
||||
#[serde(default)]
|
||||
pub context_servers: HashMap<Arc<str>, ServerConfig>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema, Debug, Default)]
|
||||
pub struct ServerConfig {
|
||||
/// The command to run this context server.
|
||||
///
|
||||
/// This will override the command set by an extension.
|
||||
pub command: Option<ServerCommand>,
|
||||
/// The settings for this context server.
|
||||
///
|
||||
/// Consult the documentation for the context server to see what settings
|
||||
/// are supported.
|
||||
#[schemars(schema_with = "server_config_settings_json_schema")]
|
||||
pub settings: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
fn server_config_settings_json_schema(_generator: &mut SchemaGenerator) -> Schema {
|
||||
Schema::Object(SchemaObject {
|
||||
instance_type: Some(InstanceType::Object.into()),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema, Debug)]
|
||||
pub struct ServerCommand {
|
||||
pub path: String,
|
||||
|
||||
@@ -29,14 +29,14 @@ anyhow.workspace = true
|
||||
async-compression.workspace = true
|
||||
async-tar.workspace = true
|
||||
chrono.workspace = true
|
||||
collections.workspace = true
|
||||
client.workspace = true
|
||||
collections.workspace = true
|
||||
command_palette_hooks.workspace = true
|
||||
editor.workspace = true
|
||||
fs.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
http_client.workspace = true
|
||||
inline_completion.workspace = true
|
||||
language.workspace = true
|
||||
lsp.workspace = true
|
||||
menu.workspace = true
|
||||
@@ -44,12 +44,12 @@ node_runtime.workspace = true
|
||||
parking_lot.workspace = true
|
||||
paths.workspace = true
|
||||
project.workspace = true
|
||||
schemars = { workspace = true, optional = true }
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
schemars = { workspace = true, optional = true }
|
||||
strum.workspace = true
|
||||
settings.workspace = true
|
||||
smol.workspace = true
|
||||
strum.workspace = true
|
||||
task.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
|
||||
@@ -38,8 +38,8 @@ use std::{
|
||||
};
|
||||
use util::{fs::remove_matching, maybe, ResultExt};
|
||||
|
||||
pub use copilot_completion_provider::CopilotCompletionProvider;
|
||||
pub use sign_in::CopilotCodeVerification;
|
||||
pub use crate::copilot_completion_provider::CopilotCompletionProvider;
|
||||
pub use crate::sign_in::{initiate_sign_in, CopilotCodeVerification};
|
||||
|
||||
actions!(
|
||||
copilot,
|
||||
@@ -1229,8 +1229,10 @@ mod tests {
|
||||
Some(self)
|
||||
}
|
||||
|
||||
fn mtime(&self) -> Option<std::time::SystemTime> {
|
||||
unimplemented!()
|
||||
fn disk_state(&self) -> language::DiskState {
|
||||
language::DiskState::Present {
|
||||
mtime: ::fs::MTime::from_seconds_and_nanos(100, 42),
|
||||
}
|
||||
}
|
||||
|
||||
fn path(&self) -> &Arc<Path> {
|
||||
@@ -1245,10 +1247,6 @@ mod tests {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn is_deleted(&self) -> bool {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use crate::{Completion, Copilot};
|
||||
use anyhow::Result;
|
||||
use client::telemetry::Telemetry;
|
||||
use editor::{CompletionProposal, Direction, InlayProposal, InlineCompletionProvider};
|
||||
use gpui::{AppContext, EntityId, Model, ModelContext, Task};
|
||||
use inline_completion::{CompletionProposal, Direction, InlayProposal, InlineCompletionProvider};
|
||||
use language::{
|
||||
language_settings::{all_language_settings, AllLanguageSettings},
|
||||
Buffer, OffsetRangeExt, ToOffset,
|
||||
|
||||
@@ -5,10 +5,79 @@ use gpui::{
|
||||
Styled, Subscription, ViewContext,
|
||||
};
|
||||
use ui::{prelude::*, Button, Label, Vector, VectorName};
|
||||
use workspace::ModalView;
|
||||
use util::ResultExt as _;
|
||||
use workspace::notifications::NotificationId;
|
||||
use workspace::{ModalView, Toast, Workspace};
|
||||
|
||||
const COPILOT_SIGN_UP_URL: &str = "https://github.com/features/copilot";
|
||||
|
||||
struct CopilotStartingToast;
|
||||
|
||||
pub fn initiate_sign_in(cx: &mut WindowContext) {
|
||||
let Some(copilot) = Copilot::global(cx) else {
|
||||
return;
|
||||
};
|
||||
let status = copilot.read(cx).status();
|
||||
let Some(workspace) = cx.window_handle().downcast::<Workspace>() else {
|
||||
return;
|
||||
};
|
||||
match status {
|
||||
Status::Starting { task } => {
|
||||
let Some(workspace) = cx.window_handle().downcast::<Workspace>() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Ok(workspace) = workspace.update(cx, |workspace, cx| {
|
||||
workspace.show_toast(
|
||||
Toast::new(
|
||||
NotificationId::unique::<CopilotStartingToast>(),
|
||||
"Copilot is starting...",
|
||||
),
|
||||
cx,
|
||||
);
|
||||
workspace.weak_handle()
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
|
||||
cx.spawn(|mut cx| async move {
|
||||
task.await;
|
||||
if let Some(copilot) = cx.update(|cx| Copilot::global(cx)).ok().flatten() {
|
||||
workspace
|
||||
.update(&mut cx, |workspace, cx| match copilot.read(cx).status() {
|
||||
Status::Authorized => workspace.show_toast(
|
||||
Toast::new(
|
||||
NotificationId::unique::<CopilotStartingToast>(),
|
||||
"Copilot has started!",
|
||||
),
|
||||
cx,
|
||||
),
|
||||
_ => {
|
||||
workspace.dismiss_toast(
|
||||
&NotificationId::unique::<CopilotStartingToast>(),
|
||||
cx,
|
||||
);
|
||||
copilot
|
||||
.update(cx, |copilot, cx| copilot.sign_in(cx))
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
_ => {
|
||||
copilot.update(cx, |this, cx| this.sign_in(cx)).detach();
|
||||
workspace
|
||||
.update(cx, |this, cx| {
|
||||
this.toggle_modal(cx, |cx| CopilotCodeVerification::new(&copilot, cx));
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CopilotCodeVerification {
|
||||
status: Status,
|
||||
connect_clicked: bool,
|
||||
|
||||
@@ -776,7 +776,7 @@ impl Item for ProjectDiagnosticsEditor {
|
||||
}
|
||||
}
|
||||
|
||||
fn breadcrumb_location(&self) -> ToolbarItemLocation {
|
||||
fn breadcrumb_location(&self, _: &AppContext) -> ToolbarItemLocation {
|
||||
ToolbarItemLocation::PrimaryLeft
|
||||
}
|
||||
|
||||
|
||||
@@ -42,10 +42,12 @@ emojis.workspace = true
|
||||
file_icons.workspace = true
|
||||
futures.workspace = true
|
||||
fuzzy.workspace = true
|
||||
fs.workspace = true
|
||||
git.workspace = true
|
||||
gpui.workspace = true
|
||||
http_client.workspace = true
|
||||
indoc.workspace = true
|
||||
inline_completion.workspace = true
|
||||
itertools.workspace = true
|
||||
language.workspace = true
|
||||
linkify.workspace = true
|
||||
|
||||
@@ -271,6 +271,8 @@ gpui::actions!(
|
||||
Hover,
|
||||
Indent,
|
||||
JoinLines,
|
||||
KillRingCut,
|
||||
KillRingYank,
|
||||
LineDown,
|
||||
LineUp,
|
||||
MoveDown,
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::Context as _;
|
||||
use gpui::{View, ViewContext, WindowContext};
|
||||
use language::Language;
|
||||
@@ -54,9 +52,9 @@ pub fn switch_source_header(
|
||||
cx.spawn(|_editor, mut cx| async move {
|
||||
let switch_source_header = switch_source_header_task
|
||||
.await
|
||||
.with_context(|| format!("Switch source/header LSP request for path \"{}\" failed", source_file))?;
|
||||
.with_context(|| format!("Switch source/header LSP request for path \"{source_file}\" failed"))?;
|
||||
if switch_source_header.0.is_empty() {
|
||||
log::info!("Clangd returned an empty string when requesting to switch source/header from \"{}\"", source_file);
|
||||
log::info!("Clangd returned an empty string when requesting to switch source/header from \"{source_file}\"" );
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -67,14 +65,17 @@ pub fn switch_source_header(
|
||||
)
|
||||
})?;
|
||||
|
||||
let path = goto.to_file_path().map_err(|()| {
|
||||
anyhow::anyhow!("URL conversion to file path failed for \"{goto}\"")
|
||||
})?;
|
||||
|
||||
workspace
|
||||
.update(&mut cx, |workspace, view_cx| {
|
||||
workspace.open_abs_path(PathBuf::from(goto.path()), false, view_cx)
|
||||
workspace.open_abs_path(path, false, view_cx)
|
||||
})
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Switch source/header could not open \"{}\" in workspace",
|
||||
goto.path()
|
||||
"Switch source/header could not open \"{goto}\" in workspace"
|
||||
)
|
||||
})?
|
||||
.await
|
||||
|
||||
@@ -5,6 +5,7 @@ use gpui::{Task, ViewContext};
|
||||
|
||||
use crate::Editor;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DebouncedDelay {
|
||||
task: Option<Task<()>>,
|
||||
cancel_channel: Option<oneshot::Sender<()>>,
|
||||
|
||||
@@ -66,7 +66,7 @@ use std::{
|
||||
use sum_tree::{Bias, TreeMap};
|
||||
use tab_map::{TabMap, TabSnapshot};
|
||||
use text::LineIndent;
|
||||
use ui::{div, px, IntoElement, ParentElement, SharedString, Styled, WindowContext};
|
||||
use ui::{px, SharedString, WindowContext};
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
use wrap_map::{WrapMap, WrapSnapshot};
|
||||
|
||||
@@ -541,11 +541,17 @@ pub struct HighlightStyles {
|
||||
pub suggestion: Option<HighlightStyle>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum ChunkReplacement {
|
||||
Renderer(ChunkRenderer),
|
||||
Str(SharedString),
|
||||
}
|
||||
|
||||
pub struct HighlightedChunk<'a> {
|
||||
pub text: &'a str,
|
||||
pub style: Option<HighlightStyle>,
|
||||
pub is_tab: bool,
|
||||
pub renderer: Option<ChunkRenderer>,
|
||||
pub replacement: Option<ChunkReplacement>,
|
||||
}
|
||||
|
||||
impl<'a> HighlightedChunk<'a> {
|
||||
@@ -557,7 +563,7 @@ impl<'a> HighlightedChunk<'a> {
|
||||
let mut text = self.text;
|
||||
let style = self.style;
|
||||
let is_tab = self.is_tab;
|
||||
let renderer = self.renderer;
|
||||
let renderer = self.replacement;
|
||||
iter::from_fn(move || {
|
||||
let mut prefix_len = 0;
|
||||
while let Some(&ch) = chars.peek() {
|
||||
@@ -573,30 +579,33 @@ impl<'a> HighlightedChunk<'a> {
|
||||
text: prefix,
|
||||
style,
|
||||
is_tab,
|
||||
renderer: renderer.clone(),
|
||||
replacement: renderer.clone(),
|
||||
});
|
||||
}
|
||||
chars.next();
|
||||
let (prefix, suffix) = text.split_at(ch.len_utf8());
|
||||
text = suffix;
|
||||
if let Some(replacement) = replacement(ch) {
|
||||
let background = editor_style.status.hint_background;
|
||||
let underline = editor_style.status.hint;
|
||||
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()
|
||||
};
|
||||
let invisible_style = if let Some(mut style) = style {
|
||||
style.highlight(invisible_highlight);
|
||||
style
|
||||
} else {
|
||||
invisible_highlight
|
||||
};
|
||||
return Some(HighlightedChunk {
|
||||
text: prefix,
|
||||
style: None,
|
||||
style: Some(invisible_style),
|
||||
is_tab: false,
|
||||
renderer: Some(ChunkRenderer {
|
||||
render: Arc::new(move |_| {
|
||||
div()
|
||||
.child(replacement)
|
||||
.bg(background)
|
||||
.text_decoration_1()
|
||||
.text_decoration_color(underline)
|
||||
.into_any_element()
|
||||
}),
|
||||
constrain_width: false,
|
||||
}),
|
||||
replacement: Some(ChunkReplacement::Str(replacement.into())),
|
||||
});
|
||||
} else {
|
||||
let invisible_highlight = HighlightStyle {
|
||||
@@ -619,7 +628,7 @@ impl<'a> HighlightedChunk<'a> {
|
||||
text: prefix,
|
||||
style: Some(invisible_style),
|
||||
is_tab: false,
|
||||
renderer: renderer.clone(),
|
||||
replacement: renderer.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -631,7 +640,7 @@ impl<'a> HighlightedChunk<'a> {
|
||||
text: remainder,
|
||||
style,
|
||||
is_tab,
|
||||
renderer: renderer.clone(),
|
||||
replacement: renderer.clone(),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
@@ -895,7 +904,7 @@ impl DisplaySnapshot {
|
||||
text: chunk.text,
|
||||
style: highlight_style,
|
||||
is_tab: chunk.is_tab,
|
||||
renderer: chunk.renderer,
|
||||
replacement: chunk.renderer.map(ChunkReplacement::Renderer),
|
||||
}
|
||||
.highlight_invisibles(editor_style)
|
||||
})
|
||||
|
||||
@@ -28,7 +28,6 @@ mod hover_popover;
|
||||
mod hunk_diff;
|
||||
mod indent_guides;
|
||||
mod inlay_hint_cache;
|
||||
mod inline_completion_provider;
|
||||
pub mod items;
|
||||
mod linked_editing_ranges;
|
||||
mod lsp_ext;
|
||||
@@ -75,7 +74,7 @@ use gpui::{
|
||||
div, impl_actions, point, prelude::*, px, relative, size, uniform_list, Action, AnyElement,
|
||||
AppContext, AsyncWindowContext, AvailableSpace, BackgroundExecutor, Bounds, ClipboardEntry,
|
||||
ClipboardItem, Context, DispatchPhase, ElementId, EventEmitter, FocusHandle, FocusOutEvent,
|
||||
FocusableView, FontId, FontWeight, HighlightStyle, Hsla, InteractiveText, KeyContext,
|
||||
FocusableView, FontId, FontWeight, Global, HighlightStyle, Hsla, InteractiveText, KeyContext,
|
||||
ListSizingBehavior, Model, ModelContext, MouseButton, PaintQuad, ParentElement, Pixels, Render,
|
||||
ScrollStrategy, SharedString, Size, StrikethroughStyle, Styled, StyledText, Subscription, Task,
|
||||
TextStyle, TextStyleRefinement, UTF16Selection, UnderlineStyle, UniformListScrollHandle, View,
|
||||
@@ -87,7 +86,8 @@ pub(crate) use hunk_diff::HoveredHunk;
|
||||
use hunk_diff::{diff_hunk_to_display, ExpandedHunks};
|
||||
use indent_guides::ActiveIndentGuidesState;
|
||||
use inlay_hint_cache::{InlayHintCache, InlaySplice, InvalidationStrategy};
|
||||
pub use inline_completion_provider::*;
|
||||
pub use inline_completion::Direction;
|
||||
use inline_completion::{InlayProposal, InlineCompletionProvider, InlineCompletionProviderHandle};
|
||||
pub use items::MAX_TAB_TITLE_LEN;
|
||||
use itertools::Itertools;
|
||||
use language::{
|
||||
@@ -273,12 +273,6 @@ enum DocumentHighlightRead {}
|
||||
enum DocumentHighlightWrite {}
|
||||
enum InputComposition {}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
pub enum Direction {
|
||||
Prev,
|
||||
Next,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
pub enum Navigated {
|
||||
Yes,
|
||||
@@ -540,6 +534,15 @@ pub enum IsVimMode {
|
||||
No,
|
||||
}
|
||||
|
||||
pub trait ActiveLineTrailerProvider {
|
||||
fn render_active_line_trailer(
|
||||
&mut self,
|
||||
style: &EditorStyle,
|
||||
focus_handle: &FocusHandle,
|
||||
cx: &mut WindowContext,
|
||||
) -> Option<AnyElement>;
|
||||
}
|
||||
|
||||
/// Zed's primary text input `View`, allowing users to edit a [`MultiBuffer`]
|
||||
///
|
||||
/// See the [module level documentation](self) for more information.
|
||||
@@ -667,6 +670,7 @@ pub struct Editor {
|
||||
next_scroll_position: NextScrollCursorCenterTopBottom,
|
||||
addons: HashMap<TypeId, Box<dyn Addon>>,
|
||||
_scroll_cursor_center_top_bottom_task: Task<()>,
|
||||
active_line_trailer_provider: Option<Box<dyn ActiveLineTrailerProvider>>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
|
||||
@@ -883,6 +887,7 @@ struct AutocloseRegion {
|
||||
struct SnippetState {
|
||||
ranges: Vec<Vec<Range<Anchor>>>,
|
||||
active_index: usize,
|
||||
choices: Vec<Option<Vec<String>>>,
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
@@ -1000,7 +1005,7 @@ enum ContextMenuOrigin {
|
||||
GutterIndicator(DisplayRow),
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Debug)]
|
||||
struct CompletionsMenu {
|
||||
id: CompletionId,
|
||||
sort_completions: bool,
|
||||
@@ -1011,10 +1016,105 @@ struct CompletionsMenu {
|
||||
matches: Arc<[StringMatch]>,
|
||||
selected_item: usize,
|
||||
scroll_handle: UniformListScrollHandle,
|
||||
selected_completion_documentation_resolve_debounce: Arc<Mutex<DebouncedDelay>>,
|
||||
selected_completion_documentation_resolve_debounce: Option<Arc<Mutex<DebouncedDelay>>>,
|
||||
}
|
||||
|
||||
impl CompletionsMenu {
|
||||
fn new(
|
||||
id: CompletionId,
|
||||
sort_completions: bool,
|
||||
initial_position: Anchor,
|
||||
buffer: Model<Buffer>,
|
||||
completions: Box<[Completion]>,
|
||||
) -> Self {
|
||||
let match_candidates = completions
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(id, completion)| {
|
||||
StringMatchCandidate::new(
|
||||
id,
|
||||
completion.label.text[completion.label.filter_range.clone()].into(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
Self {
|
||||
id,
|
||||
sort_completions,
|
||||
initial_position,
|
||||
buffer,
|
||||
completions: Arc::new(RwLock::new(completions)),
|
||||
match_candidates,
|
||||
matches: Vec::new().into(),
|
||||
selected_item: 0,
|
||||
scroll_handle: UniformListScrollHandle::new(),
|
||||
selected_completion_documentation_resolve_debounce: Some(Arc::new(Mutex::new(
|
||||
DebouncedDelay::new(),
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
fn new_snippet_choices(
|
||||
id: CompletionId,
|
||||
sort_completions: bool,
|
||||
choices: &Vec<String>,
|
||||
selection: Range<Anchor>,
|
||||
buffer: Model<Buffer>,
|
||||
) -> Self {
|
||||
let completions = choices
|
||||
.iter()
|
||||
.map(|choice| Completion {
|
||||
old_range: selection.start.text_anchor..selection.end.text_anchor,
|
||||
new_text: choice.to_string(),
|
||||
label: CodeLabel {
|
||||
text: choice.to_string(),
|
||||
runs: Default::default(),
|
||||
filter_range: Default::default(),
|
||||
},
|
||||
server_id: LanguageServerId(usize::MAX),
|
||||
documentation: None,
|
||||
lsp_completion: Default::default(),
|
||||
confirm: None,
|
||||
})
|
||||
.collect();
|
||||
|
||||
let match_candidates = choices
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(id, completion)| StringMatchCandidate::new(id, completion.to_string()))
|
||||
.collect();
|
||||
let matches = choices
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(id, completion)| StringMatch {
|
||||
candidate_id: id,
|
||||
score: 1.,
|
||||
positions: vec![],
|
||||
string: completion.clone(),
|
||||
})
|
||||
.collect();
|
||||
Self {
|
||||
id,
|
||||
sort_completions,
|
||||
initial_position: selection.start,
|
||||
buffer,
|
||||
completions: Arc::new(RwLock::new(completions)),
|
||||
match_candidates,
|
||||
matches,
|
||||
selected_item: 0,
|
||||
scroll_handle: UniformListScrollHandle::new(),
|
||||
selected_completion_documentation_resolve_debounce: Some(Arc::new(Mutex::new(
|
||||
DebouncedDelay::new(),
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
fn suppress_documentation_resolution(mut self) -> Self {
|
||||
self.selected_completion_documentation_resolve_debounce
|
||||
.take();
|
||||
self
|
||||
}
|
||||
|
||||
fn select_first(
|
||||
&mut self,
|
||||
provider: Option<&dyn CompletionProvider>,
|
||||
@@ -1115,6 +1215,12 @@ impl CompletionsMenu {
|
||||
let Some(provider) = provider else {
|
||||
return;
|
||||
};
|
||||
let Some(documentation_resolve) = self
|
||||
.selected_completion_documentation_resolve_debounce
|
||||
.as_ref()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let resolve_task = provider.resolve_completions(
|
||||
self.buffer.clone(),
|
||||
@@ -1127,15 +1233,13 @@ impl CompletionsMenu {
|
||||
EditorSettings::get_global(cx).completion_documentation_secondary_query_debounce;
|
||||
let delay = Duration::from_millis(delay_ms);
|
||||
|
||||
self.selected_completion_documentation_resolve_debounce
|
||||
.lock()
|
||||
.fire_new(delay, cx, |_, cx| {
|
||||
cx.spawn(move |this, mut cx| async move {
|
||||
if let Some(true) = resolve_task.await.log_err() {
|
||||
this.update(&mut cx, |_, cx| cx.notify()).ok();
|
||||
}
|
||||
})
|
||||
});
|
||||
documentation_resolve.lock().fire_new(delay, cx, |_, cx| {
|
||||
cx.spawn(move |this, mut cx| async move {
|
||||
if let Some(true) = resolve_task.await.log_err() {
|
||||
this.update(&mut cx, |_, cx| cx.notify()).ok();
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
fn visible(&self) -> bool {
|
||||
@@ -1418,6 +1522,7 @@ impl CompletionsMenu {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct AvailableCodeAction {
|
||||
excerpt_id: ExcerptId,
|
||||
action: CodeAction,
|
||||
@@ -2104,6 +2209,7 @@ impl Editor {
|
||||
addons: HashMap::default(),
|
||||
_scroll_cursor_center_top_bottom_task: Task::ready(()),
|
||||
text_style_refinement: None,
|
||||
active_line_trailer_provider: None,
|
||||
};
|
||||
this.tasks_update_task = Some(this.refresh_runnables(cx));
|
||||
this._subscriptions.extend(project_subscriptions);
|
||||
@@ -2392,6 +2498,16 @@ impl Editor {
|
||||
self.refresh_inline_completion(false, false, cx);
|
||||
}
|
||||
|
||||
pub fn set_active_line_trailer_provider<T>(
|
||||
&mut self,
|
||||
provider: Option<T>,
|
||||
_cx: &mut ViewContext<Self>,
|
||||
) where
|
||||
T: ActiveLineTrailerProvider + 'static,
|
||||
{
|
||||
self.active_line_trailer_provider = provider.map(|provider| Box::new(provider) as Box<_>);
|
||||
}
|
||||
|
||||
pub fn placeholder_text(&self, _cx: &WindowContext) -> Option<&str> {
|
||||
self.placeholder_text.as_deref()
|
||||
}
|
||||
@@ -4386,6 +4502,10 @@ impl Editor {
|
||||
return;
|
||||
};
|
||||
|
||||
if !self.snippet_stack.is_empty() && self.context_menu.read().as_ref().is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
let position = self.selections.newest_anchor().head();
|
||||
let (buffer, buffer_position) =
|
||||
if let Some(output) = self.buffer.read(cx).text_anchor_for_position(position, cx) {
|
||||
@@ -4431,30 +4551,13 @@ impl Editor {
|
||||
})?;
|
||||
let completions = completions.await.log_err();
|
||||
let menu = if let Some(completions) = completions {
|
||||
let mut menu = CompletionsMenu {
|
||||
let mut menu = CompletionsMenu::new(
|
||||
id,
|
||||
sort_completions,
|
||||
initial_position: position,
|
||||
match_candidates: completions
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(id, completion)| {
|
||||
StringMatchCandidate::new(
|
||||
id,
|
||||
completion.label.text[completion.label.filter_range.clone()]
|
||||
.into(),
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
buffer: buffer.clone(),
|
||||
completions: Arc::new(RwLock::new(completions.into())),
|
||||
matches: Vec::new().into(),
|
||||
selected_item: 0,
|
||||
scroll_handle: UniformListScrollHandle::new(),
|
||||
selected_completion_documentation_resolve_debounce: Arc::new(Mutex::new(
|
||||
DebouncedDelay::new(),
|
||||
)),
|
||||
};
|
||||
position,
|
||||
buffer.clone(),
|
||||
completions.into(),
|
||||
);
|
||||
menu.filter(query.as_deref(), cx.background_executor().clone())
|
||||
.await;
|
||||
|
||||
@@ -4657,7 +4760,11 @@ impl Editor {
|
||||
self.transact(cx, |this, cx| {
|
||||
if let Some(mut snippet) = snippet {
|
||||
snippet.text = text.to_string();
|
||||
for tabstop in snippet.tabstops.iter_mut().flatten() {
|
||||
for tabstop in snippet
|
||||
.tabstops
|
||||
.iter_mut()
|
||||
.flat_map(|tabstop| tabstop.ranges.iter_mut())
|
||||
{
|
||||
tabstop.start -= common_prefix_len as isize;
|
||||
tabstop.end -= common_prefix_len as isize;
|
||||
}
|
||||
@@ -5693,6 +5800,27 @@ impl Editor {
|
||||
context_menu
|
||||
}
|
||||
|
||||
fn show_snippet_choices(
|
||||
&mut self,
|
||||
choices: &Vec<String>,
|
||||
selection: Range<Anchor>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
if selection.start.buffer_id.is_none() {
|
||||
return;
|
||||
}
|
||||
let buffer_id = selection.start.buffer_id.unwrap();
|
||||
let buffer = self.buffer().read(cx).buffer(buffer_id);
|
||||
let id = post_inc(&mut self.next_completion_id);
|
||||
|
||||
if let Some(buffer) = buffer {
|
||||
*self.context_menu.write() = Some(ContextMenu::Completions(
|
||||
CompletionsMenu::new_snippet_choices(id, true, choices, selection, buffer)
|
||||
.suppress_documentation_resolution(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert_snippet(
|
||||
&mut self,
|
||||
insertion_ranges: &[Range<usize>],
|
||||
@@ -5702,6 +5830,7 @@ impl Editor {
|
||||
struct Tabstop<T> {
|
||||
is_end_tabstop: bool,
|
||||
ranges: Vec<Range<T>>,
|
||||
choices: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
let tabstops = self.buffer.update(cx, |buffer, cx| {
|
||||
@@ -5721,10 +5850,11 @@ impl Editor {
|
||||
.tabstops
|
||||
.iter()
|
||||
.map(|tabstop| {
|
||||
let is_end_tabstop = tabstop.first().map_or(false, |tabstop| {
|
||||
let is_end_tabstop = tabstop.ranges.first().map_or(false, |tabstop| {
|
||||
tabstop.is_empty() && tabstop.start == snippet.text.len() as isize
|
||||
});
|
||||
let mut tabstop_ranges = tabstop
|
||||
.ranges
|
||||
.iter()
|
||||
.flat_map(|tabstop_range| {
|
||||
let mut delta = 0_isize;
|
||||
@@ -5746,6 +5876,7 @@ impl Editor {
|
||||
Tabstop {
|
||||
is_end_tabstop,
|
||||
ranges: tabstop_ranges,
|
||||
choices: tabstop.choices.clone(),
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
@@ -5755,16 +5886,29 @@ impl Editor {
|
||||
s.select_ranges(tabstop.ranges.iter().cloned());
|
||||
});
|
||||
|
||||
if let Some(choices) = &tabstop.choices {
|
||||
if let Some(selection) = tabstop.ranges.first() {
|
||||
self.show_snippet_choices(choices, selection.clone(), cx)
|
||||
}
|
||||
}
|
||||
|
||||
// If we're already at the last tabstop and it's at the end of the snippet,
|
||||
// we're done, we don't need to keep the state around.
|
||||
if !tabstop.is_end_tabstop {
|
||||
let choices = tabstops
|
||||
.iter()
|
||||
.map(|tabstop| tabstop.choices.clone())
|
||||
.collect();
|
||||
|
||||
let ranges = tabstops
|
||||
.into_iter()
|
||||
.map(|tabstop| tabstop.ranges)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
self.snippet_stack.push(SnippetState {
|
||||
active_index: 0,
|
||||
ranges,
|
||||
choices,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5839,6 +5983,13 @@ impl Editor {
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.select_anchor_ranges(current_ranges.iter().cloned())
|
||||
});
|
||||
|
||||
if let Some(choices) = &snippet.choices[snippet.active_index] {
|
||||
if let Some(selection) = current_ranges.first() {
|
||||
self.show_snippet_choices(&choices, selection.clone(), cx);
|
||||
}
|
||||
}
|
||||
|
||||
// If snippet state is not at the last tabstop, push it back on the stack
|
||||
if snippet.active_index + 1 < snippet.ranges.len() {
|
||||
self.snippet_stack.push(snippet);
|
||||
@@ -7213,7 +7364,7 @@ impl Editor {
|
||||
.update(cx, |buffer, cx| buffer.edit(edits, None, cx));
|
||||
}
|
||||
|
||||
pub fn cut(&mut self, _: &Cut, cx: &mut ViewContext<Self>) {
|
||||
pub fn cut_common(&mut self, cx: &mut ViewContext<Self>) -> ClipboardItem {
|
||||
let mut text = String::new();
|
||||
let buffer = self.buffer.read(cx).snapshot(cx);
|
||||
let mut selections = self.selections.all::<Point>(cx);
|
||||
@@ -7257,11 +7408,38 @@ impl Editor {
|
||||
s.select(selections);
|
||||
});
|
||||
this.insert("", cx);
|
||||
cx.write_to_clipboard(ClipboardItem::new_string_with_json_metadata(
|
||||
text,
|
||||
clipboard_selections,
|
||||
));
|
||||
});
|
||||
ClipboardItem::new_string_with_json_metadata(text, clipboard_selections)
|
||||
}
|
||||
|
||||
pub fn cut(&mut self, _: &Cut, cx: &mut ViewContext<Self>) {
|
||||
let item = self.cut_common(cx);
|
||||
cx.write_to_clipboard(item);
|
||||
}
|
||||
|
||||
pub fn kill_ring_cut(&mut self, _: &KillRingCut, cx: &mut ViewContext<Self>) {
|
||||
self.change_selections(None, cx, |s| {
|
||||
s.move_with(|snapshot, sel| {
|
||||
if sel.is_empty() {
|
||||
sel.end = DisplayPoint::new(sel.end.row(), snapshot.line_len(sel.end.row()))
|
||||
}
|
||||
});
|
||||
});
|
||||
let item = self.cut_common(cx);
|
||||
cx.set_global(KillRing(item))
|
||||
}
|
||||
|
||||
pub fn kill_ring_yank(&mut self, _: &KillRingYank, cx: &mut ViewContext<Self>) {
|
||||
let (text, metadata) = if let Some(KillRing(item)) = cx.try_global() {
|
||||
if let Some(ClipboardEntry::String(kill_ring)) = item.entries().first() {
|
||||
(kill_ring.text().to_string(), kill_ring.metadata_json())
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
self.do_paste(&text, metadata, false, cx);
|
||||
}
|
||||
|
||||
pub fn copy(&mut self, _: &Copy, cx: &mut ViewContext<Self>) {
|
||||
@@ -11713,6 +11891,29 @@ impl Editor {
|
||||
&& self.has_blame_entries(cx)
|
||||
}
|
||||
|
||||
pub fn render_active_line_trailer(
|
||||
&mut self,
|
||||
style: &EditorStyle,
|
||||
cx: &mut WindowContext,
|
||||
) -> Option<AnyElement> {
|
||||
let selection = self.selections.newest::<Point>(cx);
|
||||
if !selection.is_empty() {
|
||||
return None;
|
||||
};
|
||||
|
||||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
||||
let buffer_row = MultiBufferRow(selection.head().row);
|
||||
|
||||
if snapshot.line_len(buffer_row) != 0 || self.has_active_inline_completion(cx) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let focus_handle = self.focus_handle.clone();
|
||||
self.active_line_trailer_provider
|
||||
.as_mut()?
|
||||
.render_active_line_trailer(style, &focus_handle, cx)
|
||||
}
|
||||
|
||||
fn has_blame_entries(&self, cx: &mut WindowContext) -> bool {
|
||||
self.blame()
|
||||
.map_or(false, |blame| blame.read(cx).has_generated_entries())
|
||||
@@ -13610,7 +13811,9 @@ impl CodeActionProvider for Model<Project> {
|
||||
range: Range<text::Anchor>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<Result<Vec<CodeAction>>> {
|
||||
self.update(cx, |project, cx| project.code_actions(buffer, range, cx))
|
||||
self.update(cx, |project, cx| {
|
||||
project.code_actions(buffer, range, None, cx)
|
||||
})
|
||||
}
|
||||
|
||||
fn apply_code_action(
|
||||
@@ -14254,15 +14457,16 @@ impl ViewInputHandler for Editor {
|
||||
fn text_for_range(
|
||||
&mut self,
|
||||
range_utf16: Range<usize>,
|
||||
adjusted_range: &mut Option<Range<usize>>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<String> {
|
||||
Some(
|
||||
self.buffer
|
||||
.read(cx)
|
||||
.read(cx)
|
||||
.text_for_range(OffsetUtf16(range_utf16.start)..OffsetUtf16(range_utf16.end))
|
||||
.collect(),
|
||||
)
|
||||
let snapshot = self.buffer.read(cx).read(cx);
|
||||
let start = snapshot.clip_offset_utf16(OffsetUtf16(range_utf16.start), Bias::Left);
|
||||
let end = snapshot.clip_offset_utf16(OffsetUtf16(range_utf16.end), Bias::Right);
|
||||
if (start.0..end.0) != range_utf16 {
|
||||
adjusted_range.replace(start.0..end.0);
|
||||
}
|
||||
Some(snapshot.text_for_range(start..end).collect())
|
||||
}
|
||||
|
||||
fn selected_text_range(
|
||||
@@ -14970,4 +15174,7 @@ fn check_multiline_range(buffer: &Buffer, range: Range<usize>) -> Range<usize> {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct KillRing(ClipboardItem);
|
||||
impl Global for KillRing {}
|
||||
|
||||
const UPDATE_DEBOUNCE: Duration = Duration::from_millis(50);
|
||||
|
||||
@@ -1398,6 +1398,15 @@ fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
|
||||
view.change_selections(None, cx, |s| {
|
||||
s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
|
||||
});
|
||||
|
||||
// moving above start of document should move selection to start of document,
|
||||
// but the next move down should still be at the original goal_x
|
||||
view.move_up(&MoveUp, cx);
|
||||
assert_eq!(
|
||||
view.selections.display_ranges(cx),
|
||||
&[empty_range(0, "".len())]
|
||||
);
|
||||
|
||||
view.move_down(&MoveDown, cx);
|
||||
assert_eq!(
|
||||
view.selections.display_ranges(cx),
|
||||
@@ -1422,6 +1431,25 @@ fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
|
||||
&[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
|
||||
);
|
||||
|
||||
// moving past end of document should not change goal_x
|
||||
view.move_down(&MoveDown, cx);
|
||||
assert_eq!(
|
||||
view.selections.display_ranges(cx),
|
||||
&[empty_range(5, "".len())]
|
||||
);
|
||||
|
||||
view.move_down(&MoveDown, cx);
|
||||
assert_eq!(
|
||||
view.selections.display_ranges(cx),
|
||||
&[empty_range(5, "".len())]
|
||||
);
|
||||
|
||||
view.move_up(&MoveUp, cx);
|
||||
assert_eq!(
|
||||
view.selections.display_ranges(cx),
|
||||
&[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
|
||||
);
|
||||
|
||||
view.move_up(&MoveUp, cx);
|
||||
assert_eq!(
|
||||
view.selections.display_ranges(cx),
|
||||
@@ -6551,6 +6579,45 @@ async fn test_auto_replace_emoji_shortcode(cx: &mut gpui::TestAppContext) {
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_snippet_placeholder_choices(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let (text, insertion_ranges) = marked_text_ranges(
|
||||
indoc! {"
|
||||
ˇ
|
||||
"},
|
||||
false,
|
||||
);
|
||||
|
||||
let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
|
||||
let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
|
||||
|
||||
_ = editor.update(cx, |editor, cx| {
|
||||
let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
|
||||
|
||||
editor
|
||||
.insert_snippet(&insertion_ranges, snippet, cx)
|
||||
.unwrap();
|
||||
|
||||
fn assert(editor: &mut Editor, cx: &mut ViewContext<Editor>, marked_text: &str) {
|
||||
let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
|
||||
assert_eq!(editor.text(cx), expected_text);
|
||||
assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
|
||||
}
|
||||
|
||||
assert(
|
||||
editor,
|
||||
cx,
|
||||
indoc! {"
|
||||
type «» =•
|
||||
"},
|
||||
);
|
||||
|
||||
assert!(editor.context_menu_visible(), "There should be a matches");
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_snippets(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
@@ -16,8 +16,8 @@ use crate::{
|
||||
items::BufferSearchHighlights,
|
||||
mouse_context_menu::{self, MenuPosition, MouseContextMenu},
|
||||
scroll::scroll_amount::ScrollAmount,
|
||||
BlockId, CodeActionsMenu, CursorShape, CustomBlockId, DisplayPoint, DisplayRow,
|
||||
DocumentHighlightRead, DocumentHighlightWrite, Editor, EditorMode, EditorSettings,
|
||||
BlockId, ChunkReplacement, CodeActionsMenu, CursorShape, CustomBlockId, DisplayPoint,
|
||||
DisplayRow, DocumentHighlightRead, DocumentHighlightWrite, Editor, EditorMode, EditorSettings,
|
||||
EditorSnapshot, EditorStyle, ExpandExcerpts, FocusedBlock, GutterDimensions, HalfPageDown,
|
||||
HalfPageUp, HandleInput, HoveredCursor, HoveredHunk, JumpData, LineDown, LineUp, OpenExcerpts,
|
||||
PageDown, PageUp, Point, RowExt, RowRangeExt, SelectPhase, Selection, SoftWrap, ToPoint,
|
||||
@@ -34,8 +34,8 @@ use gpui::{
|
||||
FontId, GlobalElementId, Hitbox, Hsla, InteractiveElement, IntoElement, Length,
|
||||
ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad,
|
||||
ParentElement, Pixels, ScrollDelta, ScrollWheelEvent, ShapedLine, SharedString, Size,
|
||||
StatefulInteractiveElement, Style, Styled, TextRun, TextStyle, TextStyleRefinement, View,
|
||||
ViewContext, WeakView, WindowContext,
|
||||
StatefulInteractiveElement, Style, Styled, TextRun, TextStyleRefinement, View, ViewContext,
|
||||
WeakView, WindowContext,
|
||||
};
|
||||
use gpui::{ClickEvent, Subscription};
|
||||
use itertools::Itertools;
|
||||
@@ -217,6 +217,8 @@ impl EditorElement {
|
||||
register_action(view, cx, Editor::transpose);
|
||||
register_action(view, cx, Editor::rewrap);
|
||||
register_action(view, cx, Editor::cut);
|
||||
register_action(view, cx, Editor::kill_ring_cut);
|
||||
register_action(view, cx, Editor::kill_ring_yank);
|
||||
register_action(view, cx, Editor::copy);
|
||||
register_action(view, cx, Editor::paste);
|
||||
register_action(view, cx, Editor::undo);
|
||||
@@ -1412,7 +1414,7 @@ impl EditorElement {
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn layout_inline_blame(
|
||||
fn layout_active_line_trailer(
|
||||
&self,
|
||||
display_row: DisplayRow,
|
||||
display_snapshot: &DisplaySnapshot,
|
||||
@@ -1424,61 +1426,71 @@ impl EditorElement {
|
||||
line_height: Pixels,
|
||||
cx: &mut WindowContext,
|
||||
) -> Option<AnyElement> {
|
||||
if !self
|
||||
let render_inline_blame = self
|
||||
.editor
|
||||
.update(cx, |editor, cx| editor.render_git_blame_inline(cx))
|
||||
{
|
||||
return None;
|
||||
}
|
||||
.update(cx, |editor, cx| editor.render_git_blame_inline(cx));
|
||||
if render_inline_blame {
|
||||
let workspace = self
|
||||
.editor
|
||||
.read(cx)
|
||||
.workspace
|
||||
.as_ref()
|
||||
.map(|(w, _)| w.clone());
|
||||
|
||||
let workspace = self
|
||||
.editor
|
||||
.read(cx)
|
||||
.workspace
|
||||
.as_ref()
|
||||
.map(|(w, _)| w.clone());
|
||||
let display_point = DisplayPoint::new(display_row, 0);
|
||||
let buffer_row = MultiBufferRow(display_point.to_point(display_snapshot).row);
|
||||
|
||||
let display_point = DisplayPoint::new(display_row, 0);
|
||||
let buffer_row = MultiBufferRow(display_point.to_point(display_snapshot).row);
|
||||
let blame = self.editor.read(cx).blame.clone()?;
|
||||
let blame_entry = blame
|
||||
.update(cx, |blame, cx| {
|
||||
blame.blame_for_rows([Some(buffer_row)], cx).next()
|
||||
})
|
||||
.flatten()?;
|
||||
|
||||
let blame = self.editor.read(cx).blame.clone()?;
|
||||
let blame_entry = blame
|
||||
.update(cx, |blame, cx| {
|
||||
blame.blame_for_rows([Some(buffer_row)], cx).next()
|
||||
})
|
||||
.flatten()?;
|
||||
let mut element =
|
||||
render_inline_blame_entry(&blame, blame_entry, &self.style, workspace, cx);
|
||||
|
||||
let mut element =
|
||||
render_inline_blame_entry(&blame, blame_entry, &self.style, workspace, cx);
|
||||
let start_y = content_origin.y
|
||||
+ line_height * (display_row.as_f32() - scroll_pixel_position.y / line_height);
|
||||
|
||||
let start_y = content_origin.y
|
||||
+ line_height * (display_row.as_f32() - scroll_pixel_position.y / line_height);
|
||||
let start_x = {
|
||||
const INLINE_BLAME_PADDING_EM_WIDTHS: f32 = 6.;
|
||||
|
||||
let start_x = {
|
||||
const INLINE_BLAME_PADDING_EM_WIDTHS: f32 = 6.;
|
||||
let line_end = if let Some(crease_trailer) = crease_trailer {
|
||||
crease_trailer.bounds.right()
|
||||
} else {
|
||||
content_origin.x - scroll_pixel_position.x + line_layout.width
|
||||
};
|
||||
let padded_line_end = line_end + em_width * INLINE_BLAME_PADDING_EM_WIDTHS;
|
||||
|
||||
let line_end = if let Some(crease_trailer) = crease_trailer {
|
||||
crease_trailer.bounds.right()
|
||||
} else {
|
||||
content_origin.x - scroll_pixel_position.x + line_layout.width
|
||||
let min_column_in_pixels = ProjectSettings::get_global(cx)
|
||||
.git
|
||||
.inline_blame
|
||||
.and_then(|settings| settings.min_column)
|
||||
.map(|col| self.column_pixels(col as usize, cx))
|
||||
.unwrap_or(px(0.));
|
||||
let min_start = content_origin.x - scroll_pixel_position.x + min_column_in_pixels;
|
||||
|
||||
cmp::max(padded_line_end, min_start)
|
||||
};
|
||||
let padded_line_end = line_end + em_width * INLINE_BLAME_PADDING_EM_WIDTHS;
|
||||
|
||||
let min_column_in_pixels = ProjectSettings::get_global(cx)
|
||||
.git
|
||||
.inline_blame
|
||||
.and_then(|settings| settings.min_column)
|
||||
.map(|col| self.column_pixels(col as usize, cx))
|
||||
.unwrap_or(px(0.));
|
||||
let min_start = content_origin.x - scroll_pixel_position.x + min_column_in_pixels;
|
||||
let absolute_offset = point(start_x, start_y);
|
||||
element.prepaint_as_root(absolute_offset, AvailableSpace::min_size(), cx);
|
||||
|
||||
cmp::max(padded_line_end, min_start)
|
||||
};
|
||||
Some(element)
|
||||
} else if let Some(mut element) = self.editor.update(cx, |editor, cx| {
|
||||
editor.render_active_line_trailer(&self.style, cx)
|
||||
}) {
|
||||
let start_y = content_origin.y
|
||||
+ line_height * (display_row.as_f32() - scroll_pixel_position.y / line_height);
|
||||
let start_x = content_origin.x - scroll_pixel_position.x + em_width;
|
||||
let absolute_offset = point(start_x, start_y);
|
||||
element.prepaint_as_root(absolute_offset, AvailableSpace::min_size(), cx);
|
||||
|
||||
let absolute_offset = point(start_x, start_y);
|
||||
element.prepaint_as_root(absolute_offset, AvailableSpace::min_size(), cx);
|
||||
|
||||
Some(element)
|
||||
Some(element)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
@@ -2019,7 +2031,7 @@ impl EditorElement {
|
||||
let chunks = snapshot.highlighted_chunks(rows.clone(), true, style);
|
||||
LineWithInvisibles::from_chunks(
|
||||
chunks,
|
||||
&style.text,
|
||||
&style,
|
||||
MAX_LINE_LEN,
|
||||
rows.len(),
|
||||
snapshot.mode,
|
||||
@@ -3454,7 +3466,7 @@ impl EditorElement {
|
||||
self.paint_lines(&invisible_display_ranges, layout, cx);
|
||||
self.paint_redactions(layout, cx);
|
||||
self.paint_cursors(layout, cx);
|
||||
self.paint_inline_blame(layout, cx);
|
||||
self.paint_active_line_trailer(layout, cx);
|
||||
cx.with_element_namespace("crease_trailers", |cx| {
|
||||
for trailer in layout.crease_trailers.iter_mut().flatten() {
|
||||
trailer.element.paint(cx);
|
||||
@@ -3936,10 +3948,10 @@ impl EditorElement {
|
||||
}
|
||||
}
|
||||
|
||||
fn paint_inline_blame(&mut self, layout: &mut EditorLayout, cx: &mut WindowContext) {
|
||||
if let Some(mut inline_blame) = layout.inline_blame.take() {
|
||||
fn paint_active_line_trailer(&mut self, layout: &mut EditorLayout, cx: &mut WindowContext) {
|
||||
if let Some(mut element) = layout.active_line_trailer.take() {
|
||||
cx.paint_layer(layout.text_hitbox.bounds, |cx| {
|
||||
inline_blame.paint(cx);
|
||||
element.paint(cx);
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -4372,7 +4384,7 @@ impl LineWithInvisibles {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn from_chunks<'a>(
|
||||
chunks: impl Iterator<Item = HighlightedChunk<'a>>,
|
||||
text_style: &TextStyle,
|
||||
editor_style: &EditorStyle,
|
||||
max_line_len: usize,
|
||||
max_line_count: usize,
|
||||
editor_mode: EditorMode,
|
||||
@@ -4380,6 +4392,7 @@ impl LineWithInvisibles {
|
||||
is_row_soft_wrapped: impl Copy + Fn(usize) -> bool,
|
||||
cx: &mut WindowContext,
|
||||
) -> Vec<Self> {
|
||||
let text_style = &editor_style.text;
|
||||
let mut layouts = Vec::with_capacity(max_line_count);
|
||||
let mut fragments: SmallVec<[LineFragment; 1]> = SmallVec::new();
|
||||
let mut line = String::new();
|
||||
@@ -4398,9 +4411,9 @@ impl LineWithInvisibles {
|
||||
text: "\n",
|
||||
style: None,
|
||||
is_tab: false,
|
||||
renderer: None,
|
||||
replacement: None,
|
||||
}]) {
|
||||
if let Some(renderer) = highlighted_chunk.renderer {
|
||||
if let Some(replacement) = highlighted_chunk.replacement {
|
||||
if !line.is_empty() {
|
||||
let shaped_line = cx
|
||||
.text_system()
|
||||
@@ -4413,42 +4426,71 @@ impl LineWithInvisibles {
|
||||
styles.clear();
|
||||
}
|
||||
|
||||
let available_width = if renderer.constrain_width {
|
||||
let chunk = if highlighted_chunk.text == ellipsis.as_ref() {
|
||||
ellipsis.clone()
|
||||
} else {
|
||||
SharedString::from(Arc::from(highlighted_chunk.text))
|
||||
};
|
||||
let shaped_line = cx
|
||||
.text_system()
|
||||
.shape_line(
|
||||
chunk,
|
||||
font_size,
|
||||
&[text_style.to_run(highlighted_chunk.text.len())],
|
||||
)
|
||||
.unwrap();
|
||||
AvailableSpace::Definite(shaped_line.width)
|
||||
} else {
|
||||
AvailableSpace::MinContent
|
||||
};
|
||||
match replacement {
|
||||
ChunkReplacement::Renderer(renderer) => {
|
||||
let available_width = if renderer.constrain_width {
|
||||
let chunk = if highlighted_chunk.text == ellipsis.as_ref() {
|
||||
ellipsis.clone()
|
||||
} else {
|
||||
SharedString::from(Arc::from(highlighted_chunk.text))
|
||||
};
|
||||
let shaped_line = cx
|
||||
.text_system()
|
||||
.shape_line(
|
||||
chunk,
|
||||
font_size,
|
||||
&[text_style.to_run(highlighted_chunk.text.len())],
|
||||
)
|
||||
.unwrap();
|
||||
AvailableSpace::Definite(shaped_line.width)
|
||||
} else {
|
||||
AvailableSpace::MinContent
|
||||
};
|
||||
|
||||
let mut element = (renderer.render)(&mut ChunkRendererContext {
|
||||
context: cx,
|
||||
max_width: text_width,
|
||||
});
|
||||
let line_height = text_style.line_height_in_pixels(cx.rem_size());
|
||||
let size = element.layout_as_root(
|
||||
size(available_width, AvailableSpace::Definite(line_height)),
|
||||
cx,
|
||||
);
|
||||
let mut element = (renderer.render)(&mut ChunkRendererContext {
|
||||
context: cx,
|
||||
max_width: text_width,
|
||||
});
|
||||
let line_height = text_style.line_height_in_pixels(cx.rem_size());
|
||||
let size = element.layout_as_root(
|
||||
size(available_width, AvailableSpace::Definite(line_height)),
|
||||
cx,
|
||||
);
|
||||
|
||||
width += size.width;
|
||||
len += highlighted_chunk.text.len();
|
||||
fragments.push(LineFragment::Element {
|
||||
element: Some(element),
|
||||
size,
|
||||
len: highlighted_chunk.text.len(),
|
||||
});
|
||||
width += size.width;
|
||||
len += highlighted_chunk.text.len();
|
||||
fragments.push(LineFragment::Element {
|
||||
element: Some(element),
|
||||
size,
|
||||
len: highlighted_chunk.text.len(),
|
||||
});
|
||||
}
|
||||
ChunkReplacement::Str(x) => {
|
||||
let text_style = if let Some(style) = highlighted_chunk.style {
|
||||
Cow::Owned(text_style.clone().highlight(style))
|
||||
} else {
|
||||
Cow::Borrowed(text_style)
|
||||
};
|
||||
|
||||
let run = TextRun {
|
||||
len: x.len(),
|
||||
font: text_style.font(),
|
||||
color: text_style.color,
|
||||
background_color: text_style.background_color,
|
||||
underline: text_style.underline,
|
||||
strikethrough: text_style.strikethrough,
|
||||
};
|
||||
let line_layout = cx
|
||||
.text_system()
|
||||
.shape_line(x, font_size, &[run])
|
||||
.unwrap()
|
||||
.with_len(highlighted_chunk.text.len());
|
||||
|
||||
width += line_layout.width;
|
||||
len += highlighted_chunk.text.len();
|
||||
fragments.push(LineFragment::Text(line_layout))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (ix, mut line_chunk) in highlighted_chunk.text.split('\n').enumerate() {
|
||||
if ix > 0 {
|
||||
@@ -5301,14 +5343,14 @@ impl Element for EditorElement {
|
||||
)
|
||||
});
|
||||
|
||||
let mut inline_blame = None;
|
||||
let mut active_line_trailer = None;
|
||||
if let Some(newest_selection_head) = newest_selection_head {
|
||||
let display_row = newest_selection_head.row();
|
||||
if (start_row..end_row).contains(&display_row) {
|
||||
let line_ix = display_row.minus(start_row) as usize;
|
||||
let line_layout = &line_layouts[line_ix];
|
||||
let crease_trailer_layout = crease_trailers[line_ix].as_ref();
|
||||
inline_blame = self.layout_inline_blame(
|
||||
active_line_trailer = self.layout_active_line_trailer(
|
||||
display_row,
|
||||
&snapshot.display_snapshot,
|
||||
line_layout,
|
||||
@@ -5627,7 +5669,7 @@ impl Element for EditorElement {
|
||||
line_elements,
|
||||
line_numbers,
|
||||
blamed_display_rows,
|
||||
inline_blame,
|
||||
active_line_trailer,
|
||||
blocks,
|
||||
cursors,
|
||||
visible_cursors,
|
||||
@@ -5764,7 +5806,7 @@ pub struct EditorLayout {
|
||||
line_numbers: Vec<Option<ShapedLine>>,
|
||||
display_hunks: Vec<(DisplayDiffHunk, Option<Hitbox>)>,
|
||||
blamed_display_rows: Option<Vec<AnyElement>>,
|
||||
inline_blame: Option<AnyElement>,
|
||||
active_line_trailer: Option<AnyElement>,
|
||||
blocks: Vec<BlockLayout>,
|
||||
highlighted_ranges: Vec<(Range<DisplayPoint>, Hsla)>,
|
||||
highlighted_gutter_ranges: Vec<(Range<DisplayPoint>, Hsla)>,
|
||||
@@ -5992,7 +6034,7 @@ fn layout_line(
|
||||
let chunks = snapshot.highlighted_chunks(row..row + DisplayRow(1), true, style);
|
||||
LineWithInvisibles::from_chunks(
|
||||
chunks,
|
||||
&style.text,
|
||||
&style,
|
||||
MAX_LINE_LEN,
|
||||
1,
|
||||
snapshot.mode,
|
||||
|
||||
@@ -16,7 +16,8 @@ use gpui::{
|
||||
VisualContext, WeakView, WindowContext,
|
||||
};
|
||||
use language::{
|
||||
proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, CharKind, Point, SelectionGoal,
|
||||
proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, CharKind, DiskState, Point,
|
||||
SelectionGoal,
|
||||
};
|
||||
use lsp::DiagnosticSeverity;
|
||||
use multi_buffer::AnchorRangeExt;
|
||||
@@ -641,7 +642,7 @@ impl Item for Editor {
|
||||
.read(cx)
|
||||
.as_singleton()
|
||||
.and_then(|buffer| buffer.read(cx).file())
|
||||
.map_or(false, |file| file.is_deleted() && file.is_created());
|
||||
.map_or(false, |file| file.disk_state() == DiskState::Deleted);
|
||||
|
||||
h_flex()
|
||||
.gap_2()
|
||||
@@ -840,7 +841,7 @@ impl Item for Editor {
|
||||
self.pixel_position_of_newest_cursor
|
||||
}
|
||||
|
||||
fn breadcrumb_location(&self) -> ToolbarItemLocation {
|
||||
fn breadcrumb_location(&self, _: &AppContext) -> ToolbarItemLocation {
|
||||
if self.show_breadcrumbs {
|
||||
ToolbarItemLocation::PrimaryLeft
|
||||
} else {
|
||||
@@ -1617,15 +1618,14 @@ fn path_for_file<'a>(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::editor_tests::init_test;
|
||||
use fs::Fs;
|
||||
|
||||
use super::*;
|
||||
use fs::MTime;
|
||||
use gpui::{AppContext, VisualTestContext};
|
||||
use language::{LanguageMatcher, TestFile};
|
||||
use project::FakeFs;
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
time::SystemTime,
|
||||
};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[gpui::test]
|
||||
fn test_path_for_file(cx: &mut AppContext) {
|
||||
@@ -1678,9 +1678,7 @@ mod tests {
|
||||
async fn test_deserialize(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let now = SystemTime::now();
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.set_next_mtime(now);
|
||||
fs.insert_file("/file.rs", Default::default()).await;
|
||||
|
||||
// Test case 1: Deserialize with path and contents
|
||||
@@ -1689,12 +1687,18 @@ mod tests {
|
||||
let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
|
||||
let workspace_id = workspace::WORKSPACE_DB.next_id().await.unwrap();
|
||||
let item_id = 1234 as ItemId;
|
||||
let mtime = fs
|
||||
.metadata(Path::new("/file.rs"))
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.mtime;
|
||||
|
||||
let serialized_editor = SerializedEditor {
|
||||
abs_path: Some(PathBuf::from("/file.rs")),
|
||||
contents: Some("fn main() {}".to_string()),
|
||||
language: Some("Rust".to_string()),
|
||||
mtime: Some(now),
|
||||
mtime: Some(mtime),
|
||||
};
|
||||
|
||||
DB.save_serialized_editor(item_id, workspace_id, serialized_editor.clone())
|
||||
@@ -1791,9 +1795,7 @@ mod tests {
|
||||
let workspace_id = workspace::WORKSPACE_DB.next_id().await.unwrap();
|
||||
|
||||
let item_id = 9345 as ItemId;
|
||||
let old_mtime = now
|
||||
.checked_sub(std::time::Duration::from_secs(60 * 60 * 24))
|
||||
.unwrap();
|
||||
let old_mtime = MTime::from_seconds_and_nanos(0, 50);
|
||||
let serialized_editor = SerializedEditor {
|
||||
abs_path: Some(PathBuf::from("/file.rs")),
|
||||
contents: Some("fn main() {}".to_string()),
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
use super::{Bias, DisplayPoint, DisplaySnapshot, SelectionGoal, ToDisplayPoint};
|
||||
use crate::{scroll::ScrollAnchor, CharKind, DisplayRow, EditorStyle, RowExt, ToOffset, ToPoint};
|
||||
use gpui::{px, Pixels, WindowTextSystem};
|
||||
use gpui::{Pixels, WindowTextSystem};
|
||||
use language::Point;
|
||||
use multi_buffer::{MultiBufferRow, MultiBufferSnapshot};
|
||||
use serde::Deserialize;
|
||||
@@ -120,7 +120,7 @@ pub(crate) fn up_by_rows(
|
||||
preserve_column_at_start: bool,
|
||||
text_layout_details: &TextLayoutDetails,
|
||||
) -> (DisplayPoint, SelectionGoal) {
|
||||
let mut goal_x = match goal {
|
||||
let goal_x = match goal {
|
||||
SelectionGoal::HorizontalPosition(x) => x.into(),
|
||||
SelectionGoal::WrappedHorizontalPosition((_, x)) => x.into(),
|
||||
SelectionGoal::HorizontalRange { end, .. } => end.into(),
|
||||
@@ -138,7 +138,6 @@ pub(crate) fn up_by_rows(
|
||||
return (start, goal);
|
||||
} else {
|
||||
point = DisplayPoint::new(DisplayRow(0), 0);
|
||||
goal_x = px(0.);
|
||||
}
|
||||
|
||||
let mut clipped_point = map.clip_point(point, Bias::Left);
|
||||
@@ -159,7 +158,7 @@ pub(crate) fn down_by_rows(
|
||||
preserve_column_at_end: bool,
|
||||
text_layout_details: &TextLayoutDetails,
|
||||
) -> (DisplayPoint, SelectionGoal) {
|
||||
let mut goal_x = match goal {
|
||||
let goal_x = match goal {
|
||||
SelectionGoal::HorizontalPosition(x) => x.into(),
|
||||
SelectionGoal::WrappedHorizontalPosition((_, x)) => x.into(),
|
||||
SelectionGoal::HorizontalRange { end, .. } => end.into(),
|
||||
@@ -174,7 +173,6 @@ pub(crate) fn down_by_rows(
|
||||
return (start, goal);
|
||||
} else {
|
||||
point = map.max_point();
|
||||
goal_x = map.x_for_display_point(point, text_layout_details)
|
||||
}
|
||||
|
||||
let mut clipped_point = map.clip_point(point, Bias::Right);
|
||||
@@ -610,7 +608,7 @@ mod tests {
|
||||
test::{editor_test_context::EditorTestContext, marked_display_snapshot},
|
||||
Buffer, DisplayMap, DisplayRow, ExcerptRange, FoldPlaceholder, InlayId, MultiBuffer,
|
||||
};
|
||||
use gpui::{font, Context as _};
|
||||
use gpui::{font, px, Context as _};
|
||||
use language::Capability;
|
||||
use project::Project;
|
||||
use settings::SettingsStore;
|
||||
@@ -977,7 +975,7 @@ mod tests {
|
||||
),
|
||||
(
|
||||
DisplayPoint::new(DisplayRow(2), 0),
|
||||
SelectionGoal::HorizontalPosition(0.0)
|
||||
SelectionGoal::HorizontalPosition(col_2_x.0),
|
||||
),
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -990,7 +988,7 @@ mod tests {
|
||||
),
|
||||
(
|
||||
DisplayPoint::new(DisplayRow(2), 0),
|
||||
SelectionGoal::HorizontalPosition(0.0)
|
||||
SelectionGoal::HorizontalPosition(0.0),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1059,7 +1057,7 @@ mod tests {
|
||||
let max_point_x = snapshot
|
||||
.x_for_display_point(DisplayPoint::new(DisplayRow(7), 2), &text_layout_details);
|
||||
|
||||
// Can't move down off the end
|
||||
// Can't move down off the end, and attempting to do so leaves the selection goal unchanged
|
||||
assert_eq!(
|
||||
down(
|
||||
&snapshot,
|
||||
@@ -1070,7 +1068,7 @@ mod tests {
|
||||
),
|
||||
(
|
||||
DisplayPoint::new(DisplayRow(7), 2),
|
||||
SelectionGoal::HorizontalPosition(max_point_x.0)
|
||||
SelectionGoal::HorizontalPosition(0.0)
|
||||
),
|
||||
);
|
||||
assert_eq!(
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use anyhow::Result;
|
||||
use db::sqlez::bindable::{Bind, Column, StaticColumnCount};
|
||||
use db::sqlez::statement::Statement;
|
||||
use fs::MTime;
|
||||
use std::path::PathBuf;
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
|
||||
use db::sqlez_macros::sql;
|
||||
use db::{define_connection, query};
|
||||
@@ -14,7 +14,7 @@ pub(crate) struct SerializedEditor {
|
||||
pub(crate) abs_path: Option<PathBuf>,
|
||||
pub(crate) contents: Option<String>,
|
||||
pub(crate) language: Option<String>,
|
||||
pub(crate) mtime: Option<SystemTime>,
|
||||
pub(crate) mtime: Option<MTime>,
|
||||
}
|
||||
|
||||
impl StaticColumnCount for SerializedEditor {
|
||||
@@ -29,16 +29,13 @@ impl Bind for SerializedEditor {
|
||||
let start_index = statement.bind(&self.contents, start_index)?;
|
||||
let start_index = statement.bind(&self.language, start_index)?;
|
||||
|
||||
let mtime = self.mtime.and_then(|mtime| {
|
||||
mtime
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.ok()
|
||||
.map(|duration| (duration.as_secs() as i64, duration.subsec_nanos() as i32))
|
||||
});
|
||||
let start_index = match mtime {
|
||||
let start_index = match self
|
||||
.mtime
|
||||
.and_then(|mtime| mtime.to_seconds_and_nanos_for_persistence())
|
||||
{
|
||||
Some((seconds, nanos)) => {
|
||||
let start_index = statement.bind(&seconds, start_index)?;
|
||||
statement.bind(&nanos, start_index)?
|
||||
let start_index = statement.bind(&(seconds as i64), start_index)?;
|
||||
statement.bind(&(nanos as i32), start_index)?
|
||||
}
|
||||
None => {
|
||||
let start_index = statement.bind::<Option<i64>>(&None, start_index)?;
|
||||
@@ -64,7 +61,7 @@ impl Column for SerializedEditor {
|
||||
|
||||
let mtime = mtime_seconds
|
||||
.zip(mtime_nanos)
|
||||
.map(|(seconds, nanos)| UNIX_EPOCH + Duration::new(seconds as u64, nanos as u32));
|
||||
.map(|(seconds, nanos)| MTime::from_seconds_and_nanos(seconds as u64, nanos as u32));
|
||||
|
||||
let editor = Self {
|
||||
abs_path,
|
||||
@@ -280,12 +277,11 @@ mod tests {
|
||||
assert_eq!(have, serialized_editor);
|
||||
|
||||
// Storing and retrieving mtime
|
||||
let now = SystemTime::now();
|
||||
let serialized_editor = SerializedEditor {
|
||||
abs_path: None,
|
||||
contents: None,
|
||||
language: None,
|
||||
mtime: Some(now),
|
||||
mtime: Some(MTime::from_seconds_and_nanos(100, 42)),
|
||||
};
|
||||
|
||||
DB.save_serialized_editor(1234, workspace_id, serialized_editor.clone())
|
||||
|
||||
@@ -30,9 +30,10 @@ languages.workspace = true
|
||||
node_runtime.workspace = true
|
||||
open_ai.workspace = true
|
||||
project.workspace = true
|
||||
reqwest_client.workspace = true
|
||||
semantic_index.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
smol.workspace = true
|
||||
reqwest_client.workspace = true
|
||||
util.workspace = true
|
||||
|
||||
@@ -27,7 +27,7 @@ use std::time::Duration;
|
||||
use std::{
|
||||
fs,
|
||||
path::Path,
|
||||
process::{exit, Command, Stdio},
|
||||
process::{exit, Stdio},
|
||||
sync::{
|
||||
atomic::{AtomicUsize, Ordering::SeqCst},
|
||||
Arc,
|
||||
@@ -667,7 +667,7 @@ async fn fetch_eval_repo(
|
||||
return;
|
||||
}
|
||||
if !repo_dir.join(".git").exists() {
|
||||
let init_output = Command::new("git")
|
||||
let init_output = util::command::new_std_command("git")
|
||||
.current_dir(&repo_dir)
|
||||
.args(&["init"])
|
||||
.output()
|
||||
@@ -682,13 +682,13 @@ async fn fetch_eval_repo(
|
||||
}
|
||||
}
|
||||
let url = format!("https://github.com/{}.git", repo);
|
||||
Command::new("git")
|
||||
util::command::new_std_command("git")
|
||||
.current_dir(&repo_dir)
|
||||
.args(&["remote", "add", "-f", "origin", &url])
|
||||
.stdin(Stdio::null())
|
||||
.output()
|
||||
.unwrap();
|
||||
let fetch_output = Command::new("git")
|
||||
let fetch_output = util::command::new_std_command("git")
|
||||
.current_dir(&repo_dir)
|
||||
.args(&["fetch", "--depth", "1", "origin", &sha])
|
||||
.stdin(Stdio::null())
|
||||
@@ -703,7 +703,7 @@ async fn fetch_eval_repo(
|
||||
);
|
||||
return;
|
||||
}
|
||||
let checkout_output = Command::new("git")
|
||||
let checkout_output = util::command::new_std_command("git")
|
||||
.current_dir(&repo_dir)
|
||||
.args(&["checkout", &sha])
|
||||
.output()
|
||||
|
||||
@@ -24,10 +24,12 @@ http_client.workspace = true
|
||||
language.workspace = true
|
||||
log.workspace = true
|
||||
lsp.workspace = true
|
||||
parking_lot.workspace = true
|
||||
semantic_version.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
toml.workspace = true
|
||||
util.workspace = true
|
||||
wasm-encoder.workspace = true
|
||||
wasmparser.workspace = true
|
||||
wit-component.workspace = true
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod extension_builder;
|
||||
mod extension_host_proxy;
|
||||
mod extension_manifest;
|
||||
mod types;
|
||||
|
||||
@@ -9,13 +10,19 @@ use ::lsp::LanguageServerName;
|
||||
use anyhow::{anyhow, bail, Context as _, Result};
|
||||
use async_trait::async_trait;
|
||||
use fs::normalize_path;
|
||||
use gpui::Task;
|
||||
use gpui::{AppContext, Task};
|
||||
use language::LanguageName;
|
||||
use semantic_version::SemanticVersion;
|
||||
|
||||
pub use crate::extension_host_proxy::*;
|
||||
pub use crate::extension_manifest::*;
|
||||
pub use crate::types::*;
|
||||
|
||||
/// Initializes the `extension` crate.
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
ExtensionHostProxy::default_global(cx);
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait WorktreeDelegate: Send + Sync + 'static {
|
||||
fn id(&self) -> u64;
|
||||
@@ -25,6 +32,10 @@ pub trait WorktreeDelegate: Send + Sync + 'static {
|
||||
async fn shell_env(&self) -> Vec<(String, String)>;
|
||||
}
|
||||
|
||||
pub trait ProjectDelegate: Send + Sync + 'static {
|
||||
fn worktree_ids(&self) -> Vec<u64>;
|
||||
}
|
||||
|
||||
pub trait KeyValueStoreDelegate: Send + Sync + 'static {
|
||||
fn insert(&self, key: String, docs: String) -> Task<Result<()>>;
|
||||
}
|
||||
@@ -87,6 +98,12 @@ pub trait Extension: Send + Sync + 'static {
|
||||
worktree: Option<Arc<dyn WorktreeDelegate>>,
|
||||
) -> Result<SlashCommandOutput>;
|
||||
|
||||
async fn context_server_command(
|
||||
&self,
|
||||
context_server_id: Arc<str>,
|
||||
project: Arc<dyn ProjectDelegate>,
|
||||
) -> Result<Command>;
|
||||
|
||||
async fn suggest_docs_packages(&self, provider: Arc<str>) -> Result<Vec<String>>;
|
||||
|
||||
async fn index_docs(
|
||||
|
||||
@@ -11,7 +11,7 @@ use serde::Deserialize;
|
||||
use std::{
|
||||
env, fs, mem,
|
||||
path::{Path, PathBuf},
|
||||
process::{Command, Stdio},
|
||||
process::Stdio,
|
||||
sync::Arc,
|
||||
};
|
||||
use wasm_encoder::{ComponentSectionId, Encode as _, RawSection, Section as _};
|
||||
@@ -130,7 +130,7 @@ impl ExtensionBuilder {
|
||||
"compiling Rust crate for extension {}",
|
||||
extension_dir.display()
|
||||
);
|
||||
let output = Command::new("cargo")
|
||||
let output = util::command::new_std_command("cargo")
|
||||
.args(["build", "--target", RUST_TARGET])
|
||||
.args(options.release.then_some("--release"))
|
||||
.arg("--target-dir")
|
||||
@@ -237,7 +237,7 @@ impl ExtensionBuilder {
|
||||
let scanner_path = src_path.join("scanner.c");
|
||||
|
||||
log::info!("compiling {grammar_name} parser");
|
||||
let clang_output = Command::new(&clang_path)
|
||||
let clang_output = util::command::new_std_command(&clang_path)
|
||||
.args(["-fPIC", "-shared", "-Os"])
|
||||
.arg(format!("-Wl,--export=tree_sitter_{grammar_name}"))
|
||||
.arg("-o")
|
||||
@@ -264,7 +264,7 @@ impl ExtensionBuilder {
|
||||
let git_dir = directory.join(".git");
|
||||
|
||||
if directory.exists() {
|
||||
let remotes_output = Command::new("git")
|
||||
let remotes_output = util::command::new_std_command("git")
|
||||
.arg("--git-dir")
|
||||
.arg(&git_dir)
|
||||
.args(["remote", "-v"])
|
||||
@@ -287,7 +287,7 @@ impl ExtensionBuilder {
|
||||
fs::create_dir_all(directory).with_context(|| {
|
||||
format!("failed to create grammar directory {}", directory.display(),)
|
||||
})?;
|
||||
let init_output = Command::new("git")
|
||||
let init_output = util::command::new_std_command("git")
|
||||
.arg("init")
|
||||
.current_dir(directory)
|
||||
.output()?;
|
||||
@@ -298,7 +298,7 @@ impl ExtensionBuilder {
|
||||
);
|
||||
}
|
||||
|
||||
let remote_add_output = Command::new("git")
|
||||
let remote_add_output = util::command::new_std_command("git")
|
||||
.arg("--git-dir")
|
||||
.arg(&git_dir)
|
||||
.args(["remote", "add", "origin", url])
|
||||
@@ -312,14 +312,14 @@ impl ExtensionBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
let fetch_output = Command::new("git")
|
||||
let fetch_output = util::command::new_std_command("git")
|
||||
.arg("--git-dir")
|
||||
.arg(&git_dir)
|
||||
.args(["fetch", "--depth", "1", "origin", rev])
|
||||
.output()
|
||||
.context("failed to execute `git fetch`")?;
|
||||
|
||||
let checkout_output = Command::new("git")
|
||||
let checkout_output = util::command::new_std_command("git")
|
||||
.arg("--git-dir")
|
||||
.arg(&git_dir)
|
||||
.args(["checkout", rev])
|
||||
@@ -346,7 +346,7 @@ impl ExtensionBuilder {
|
||||
}
|
||||
|
||||
fn install_rust_wasm_target_if_needed(&self) -> Result<()> {
|
||||
let rustc_output = Command::new("rustc")
|
||||
let rustc_output = util::command::new_std_command("rustc")
|
||||
.arg("--print")
|
||||
.arg("sysroot")
|
||||
.output()
|
||||
@@ -363,7 +363,7 @@ impl ExtensionBuilder {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let output = Command::new("rustup")
|
||||
let output = util::command::new_std_command("rustup")
|
||||
.args(["target", "add", RUST_TARGET])
|
||||
.stderr(Stdio::piped())
|
||||
.stdout(Stdio::inherit())
|
||||
|
||||
324
crates/extension/src/extension_host_proxy.rs
Normal file
324
crates/extension/src/extension_host_proxy.rs
Normal file
@@ -0,0 +1,324 @@
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use fs::Fs;
|
||||
use gpui::{AppContext, Global, ReadGlobal, SharedString, Task};
|
||||
use language::{LanguageMatcher, LanguageName, LanguageServerBinaryStatus, LoadedLanguage};
|
||||
use lsp::LanguageServerName;
|
||||
use parking_lot::RwLock;
|
||||
|
||||
use crate::{Extension, SlashCommand};
|
||||
|
||||
#[derive(Default)]
|
||||
struct GlobalExtensionHostProxy(Arc<ExtensionHostProxy>);
|
||||
|
||||
impl Global for GlobalExtensionHostProxy {}
|
||||
|
||||
/// A proxy for interacting with the extension host.
|
||||
///
|
||||
/// This object implements each of the individual proxy types so that their
|
||||
/// methods can be called directly on it.
|
||||
#[derive(Default)]
|
||||
pub struct ExtensionHostProxy {
|
||||
theme_proxy: RwLock<Option<Arc<dyn ExtensionThemeProxy>>>,
|
||||
grammar_proxy: RwLock<Option<Arc<dyn ExtensionGrammarProxy>>>,
|
||||
language_proxy: RwLock<Option<Arc<dyn ExtensionLanguageProxy>>>,
|
||||
language_server_proxy: RwLock<Option<Arc<dyn ExtensionLanguageServerProxy>>>,
|
||||
snippet_proxy: RwLock<Option<Arc<dyn ExtensionSnippetProxy>>>,
|
||||
slash_command_proxy: RwLock<Option<Arc<dyn ExtensionSlashCommandProxy>>>,
|
||||
context_server_proxy: RwLock<Option<Arc<dyn ExtensionContextServerProxy>>>,
|
||||
indexed_docs_provider_proxy: RwLock<Option<Arc<dyn ExtensionIndexedDocsProviderProxy>>>,
|
||||
}
|
||||
|
||||
impl ExtensionHostProxy {
|
||||
/// Returns the global [`ExtensionHostProxy`].
|
||||
pub fn global(cx: &AppContext) -> Arc<Self> {
|
||||
GlobalExtensionHostProxy::global(cx).0.clone()
|
||||
}
|
||||
|
||||
/// Returns the global [`ExtensionHostProxy`].
|
||||
///
|
||||
/// Inserts a default [`ExtensionHostProxy`] if one does not yet exist.
|
||||
pub fn default_global(cx: &mut AppContext) -> Arc<Self> {
|
||||
cx.default_global::<GlobalExtensionHostProxy>().0.clone()
|
||||
}
|
||||
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
theme_proxy: RwLock::default(),
|
||||
grammar_proxy: RwLock::default(),
|
||||
language_proxy: RwLock::default(),
|
||||
language_server_proxy: RwLock::default(),
|
||||
snippet_proxy: RwLock::default(),
|
||||
slash_command_proxy: RwLock::default(),
|
||||
context_server_proxy: RwLock::default(),
|
||||
indexed_docs_provider_proxy: RwLock::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn register_theme_proxy(&self, proxy: impl ExtensionThemeProxy) {
|
||||
self.theme_proxy.write().replace(Arc::new(proxy));
|
||||
}
|
||||
|
||||
pub fn register_grammar_proxy(&self, proxy: impl ExtensionGrammarProxy) {
|
||||
self.grammar_proxy.write().replace(Arc::new(proxy));
|
||||
}
|
||||
|
||||
pub fn register_language_proxy(&self, proxy: impl ExtensionLanguageProxy) {
|
||||
self.language_proxy.write().replace(Arc::new(proxy));
|
||||
}
|
||||
|
||||
pub fn register_language_server_proxy(&self, proxy: impl ExtensionLanguageServerProxy) {
|
||||
self.language_server_proxy.write().replace(Arc::new(proxy));
|
||||
}
|
||||
|
||||
pub fn register_snippet_proxy(&self, proxy: impl ExtensionSnippetProxy) {
|
||||
self.snippet_proxy.write().replace(Arc::new(proxy));
|
||||
}
|
||||
|
||||
pub fn register_slash_command_proxy(&self, proxy: impl ExtensionSlashCommandProxy) {
|
||||
self.slash_command_proxy.write().replace(Arc::new(proxy));
|
||||
}
|
||||
|
||||
pub fn register_context_server_proxy(&self, proxy: impl ExtensionContextServerProxy) {
|
||||
self.context_server_proxy.write().replace(Arc::new(proxy));
|
||||
}
|
||||
|
||||
pub fn register_indexed_docs_provider_proxy(
|
||||
&self,
|
||||
proxy: impl ExtensionIndexedDocsProviderProxy,
|
||||
) {
|
||||
self.indexed_docs_provider_proxy
|
||||
.write()
|
||||
.replace(Arc::new(proxy));
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ExtensionThemeProxy: Send + Sync + 'static {
|
||||
fn list_theme_names(&self, theme_path: PathBuf, fs: Arc<dyn Fs>) -> Task<Result<Vec<String>>>;
|
||||
|
||||
fn remove_user_themes(&self, themes: Vec<SharedString>);
|
||||
|
||||
fn load_user_theme(&self, theme_path: PathBuf, fs: Arc<dyn Fs>) -> Task<Result<()>>;
|
||||
|
||||
fn reload_current_theme(&self, cx: &mut AppContext);
|
||||
}
|
||||
|
||||
impl ExtensionThemeProxy for ExtensionHostProxy {
|
||||
fn list_theme_names(&self, theme_path: PathBuf, fs: Arc<dyn Fs>) -> Task<Result<Vec<String>>> {
|
||||
let Some(proxy) = self.theme_proxy.read().clone() else {
|
||||
return Task::ready(Ok(Vec::new()));
|
||||
};
|
||||
|
||||
proxy.list_theme_names(theme_path, fs)
|
||||
}
|
||||
|
||||
fn remove_user_themes(&self, themes: Vec<SharedString>) {
|
||||
let Some(proxy) = self.theme_proxy.read().clone() else {
|
||||
return;
|
||||
};
|
||||
|
||||
proxy.remove_user_themes(themes)
|
||||
}
|
||||
|
||||
fn load_user_theme(&self, theme_path: PathBuf, fs: Arc<dyn Fs>) -> Task<Result<()>> {
|
||||
let Some(proxy) = self.theme_proxy.read().clone() else {
|
||||
return Task::ready(Ok(()));
|
||||
};
|
||||
|
||||
proxy.load_user_theme(theme_path, fs)
|
||||
}
|
||||
|
||||
fn reload_current_theme(&self, cx: &mut AppContext) {
|
||||
let Some(proxy) = self.theme_proxy.read().clone() else {
|
||||
return;
|
||||
};
|
||||
|
||||
proxy.reload_current_theme(cx)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ExtensionGrammarProxy: Send + Sync + 'static {
|
||||
fn register_grammars(&self, grammars: Vec<(Arc<str>, PathBuf)>);
|
||||
}
|
||||
|
||||
impl ExtensionGrammarProxy for ExtensionHostProxy {
|
||||
fn register_grammars(&self, grammars: Vec<(Arc<str>, PathBuf)>) {
|
||||
let Some(proxy) = self.grammar_proxy.read().clone() else {
|
||||
return;
|
||||
};
|
||||
|
||||
proxy.register_grammars(grammars)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ExtensionLanguageProxy: Send + Sync + 'static {
|
||||
fn register_language(
|
||||
&self,
|
||||
language: LanguageName,
|
||||
grammar: Option<Arc<str>>,
|
||||
matcher: LanguageMatcher,
|
||||
load: Arc<dyn Fn() -> Result<LoadedLanguage> + Send + Sync + 'static>,
|
||||
);
|
||||
|
||||
fn remove_languages(
|
||||
&self,
|
||||
languages_to_remove: &[LanguageName],
|
||||
grammars_to_remove: &[Arc<str>],
|
||||
);
|
||||
}
|
||||
|
||||
impl ExtensionLanguageProxy for ExtensionHostProxy {
|
||||
fn register_language(
|
||||
&self,
|
||||
language: LanguageName,
|
||||
grammar: Option<Arc<str>>,
|
||||
matcher: LanguageMatcher,
|
||||
load: Arc<dyn Fn() -> Result<LoadedLanguage> + Send + Sync + 'static>,
|
||||
) {
|
||||
let Some(proxy) = self.language_proxy.read().clone() else {
|
||||
return;
|
||||
};
|
||||
|
||||
proxy.register_language(language, grammar, matcher, load)
|
||||
}
|
||||
|
||||
fn remove_languages(
|
||||
&self,
|
||||
languages_to_remove: &[LanguageName],
|
||||
grammars_to_remove: &[Arc<str>],
|
||||
) {
|
||||
let Some(proxy) = self.language_proxy.read().clone() else {
|
||||
return;
|
||||
};
|
||||
|
||||
proxy.remove_languages(languages_to_remove, grammars_to_remove)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ExtensionLanguageServerProxy: Send + Sync + 'static {
|
||||
fn register_language_server(
|
||||
&self,
|
||||
extension: Arc<dyn Extension>,
|
||||
language_server_id: LanguageServerName,
|
||||
language: LanguageName,
|
||||
);
|
||||
|
||||
fn remove_language_server(
|
||||
&self,
|
||||
language: &LanguageName,
|
||||
language_server_id: &LanguageServerName,
|
||||
);
|
||||
|
||||
fn update_language_server_status(
|
||||
&self,
|
||||
language_server_id: LanguageServerName,
|
||||
status: LanguageServerBinaryStatus,
|
||||
);
|
||||
}
|
||||
|
||||
impl ExtensionLanguageServerProxy for ExtensionHostProxy {
|
||||
fn register_language_server(
|
||||
&self,
|
||||
extension: Arc<dyn Extension>,
|
||||
language_server_id: LanguageServerName,
|
||||
language: LanguageName,
|
||||
) {
|
||||
let Some(proxy) = self.language_server_proxy.read().clone() else {
|
||||
return;
|
||||
};
|
||||
|
||||
proxy.register_language_server(extension, language_server_id, language)
|
||||
}
|
||||
|
||||
fn remove_language_server(
|
||||
&self,
|
||||
language: &LanguageName,
|
||||
language_server_id: &LanguageServerName,
|
||||
) {
|
||||
let Some(proxy) = self.language_server_proxy.read().clone() else {
|
||||
return;
|
||||
};
|
||||
|
||||
proxy.remove_language_server(language, language_server_id)
|
||||
}
|
||||
|
||||
fn update_language_server_status(
|
||||
&self,
|
||||
language_server_id: LanguageServerName,
|
||||
status: LanguageServerBinaryStatus,
|
||||
) {
|
||||
let Some(proxy) = self.language_server_proxy.read().clone() else {
|
||||
return;
|
||||
};
|
||||
|
||||
proxy.update_language_server_status(language_server_id, status)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ExtensionSnippetProxy: Send + Sync + 'static {
|
||||
fn register_snippet(&self, path: &PathBuf, snippet_contents: &str) -> Result<()>;
|
||||
}
|
||||
|
||||
impl ExtensionSnippetProxy for ExtensionHostProxy {
|
||||
fn register_snippet(&self, path: &PathBuf, snippet_contents: &str) -> Result<()> {
|
||||
let Some(proxy) = self.snippet_proxy.read().clone() else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
proxy.register_snippet(path, snippet_contents)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ExtensionSlashCommandProxy: Send + Sync + 'static {
|
||||
fn register_slash_command(&self, extension: Arc<dyn Extension>, command: SlashCommand);
|
||||
}
|
||||
|
||||
impl ExtensionSlashCommandProxy for ExtensionHostProxy {
|
||||
fn register_slash_command(&self, extension: Arc<dyn Extension>, command: SlashCommand) {
|
||||
let Some(proxy) = self.slash_command_proxy.read().clone() else {
|
||||
return;
|
||||
};
|
||||
|
||||
proxy.register_slash_command(extension, command)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ExtensionContextServerProxy: Send + Sync + 'static {
|
||||
fn register_context_server(
|
||||
&self,
|
||||
extension: Arc<dyn Extension>,
|
||||
server_id: Arc<str>,
|
||||
cx: &mut AppContext,
|
||||
);
|
||||
}
|
||||
|
||||
impl ExtensionContextServerProxy for ExtensionHostProxy {
|
||||
fn register_context_server(
|
||||
&self,
|
||||
extension: Arc<dyn Extension>,
|
||||
server_id: Arc<str>,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
let Some(proxy) = self.context_server_proxy.read().clone() else {
|
||||
return;
|
||||
};
|
||||
|
||||
proxy.register_context_server(extension, server_id, cx)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ExtensionIndexedDocsProviderProxy: Send + Sync + 'static {
|
||||
fn register_indexed_docs_provider(&self, extension: Arc<dyn Extension>, provider_id: Arc<str>);
|
||||
}
|
||||
|
||||
impl ExtensionIndexedDocsProviderProxy for ExtensionHostProxy {
|
||||
fn register_indexed_docs_provider(&self, extension: Arc<dyn Extension>, provider_id: Arc<str>) {
|
||||
let Some(proxy) = self.indexed_docs_provider_proxy.read().clone() else {
|
||||
return;
|
||||
};
|
||||
|
||||
proxy.register_indexed_docs_provider(extension, provider_id)
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ pub use slash_command::*;
|
||||
pub type EnvVars = Vec<(String, String)>;
|
||||
|
||||
/// A command.
|
||||
#[derive(Debug)]
|
||||
pub struct Command {
|
||||
/// The command to execute.
|
||||
pub command: String,
|
||||
|
||||
@@ -34,6 +34,7 @@ lsp.workspace = true
|
||||
node_runtime.workspace = true
|
||||
paths.workspace = true
|
||||
project.workspace = true
|
||||
remote.workspace = true
|
||||
release_channel.workspace = true
|
||||
schemars.workspace = true
|
||||
semantic_version.workspace = true
|
||||
@@ -42,6 +43,7 @@ serde_json.workspace = true
|
||||
serde_json_lenient.workspace = true
|
||||
settings.workspace = true
|
||||
task.workspace = true
|
||||
tempfile.workspace = true
|
||||
toml.workspace = true
|
||||
url.workspace = true
|
||||
util.workspace = true
|
||||
@@ -55,7 +57,9 @@ env_logger.workspace = true
|
||||
fs = { workspace = true, features = ["test-support"] }
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
language = { workspace = true, features = ["test-support"] }
|
||||
language_extension.workspace = true
|
||||
parking_lot.workspace = true
|
||||
project = { workspace = true, features = ["test-support"] }
|
||||
reqwest_client.workspace = true
|
||||
theme = { workspace = true, features = ["test-support"] }
|
||||
theme_extension.workspace = true
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
pub mod extension_lsp_adapter;
|
||||
pub mod extension_settings;
|
||||
pub mod headless_host;
|
||||
pub mod wasm_host;
|
||||
|
||||
#[cfg(test)]
|
||||
mod extension_store_test;
|
||||
|
||||
use crate::extension_lsp_adapter::ExtensionLspAdapter;
|
||||
use anyhow::{anyhow, bail, Context as _, Result};
|
||||
use async_compression::futures::bufread::GzipDecoder;
|
||||
use async_tar::Archive;
|
||||
use client::{telemetry::Telemetry, Client, ExtensionMetadata, GetExtensionsResponse};
|
||||
use collections::{btree_map, BTreeMap, HashSet};
|
||||
use client::{proto, telemetry::Telemetry, Client, ExtensionMetadata, GetExtensionsResponse};
|
||||
use collections::{btree_map, BTreeMap, HashMap, HashSet};
|
||||
use extension::extension_builder::{CompileExtensionOptions, ExtensionBuilder};
|
||||
use extension::Extension;
|
||||
pub use extension::ExtensionManifest;
|
||||
use extension::{
|
||||
ExtensionContextServerProxy, ExtensionGrammarProxy, ExtensionHostProxy,
|
||||
ExtensionIndexedDocsProviderProxy, ExtensionLanguageProxy, ExtensionLanguageServerProxy,
|
||||
ExtensionSlashCommandProxy, ExtensionSnippetProxy, ExtensionThemeProxy,
|
||||
};
|
||||
use fs::{Fs, RemoveOptions};
|
||||
use futures::{
|
||||
channel::{
|
||||
@@ -24,18 +27,18 @@ use futures::{
|
||||
select_biased, AsyncReadExt as _, Future, FutureExt as _, StreamExt as _,
|
||||
};
|
||||
use gpui::{
|
||||
actions, AppContext, AsyncAppContext, Context, EventEmitter, Global, Model, ModelContext,
|
||||
SharedString, Task, WeakModel,
|
||||
actions, AppContext, AsyncAppContext, Context, EventEmitter, Global, Model, ModelContext, Task,
|
||||
WeakModel,
|
||||
};
|
||||
use http_client::{AsyncBody, HttpClient, HttpClientWithUrl};
|
||||
use language::{
|
||||
LanguageConfig, LanguageMatcher, LanguageName, LanguageQueries, LoadedLanguage,
|
||||
QUERY_FILENAME_PREFIXES,
|
||||
};
|
||||
use lsp::LanguageServerName;
|
||||
use node_runtime::NodeRuntime;
|
||||
use project::ContextProviderWithTasks;
|
||||
use release_channel::ReleaseChannel;
|
||||
use remote::SshRemoteClient;
|
||||
use semantic_version::SemanticVersion;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::Settings;
|
||||
@@ -94,76 +97,8 @@ pub fn is_version_compatible(
|
||||
true
|
||||
}
|
||||
|
||||
pub trait ExtensionRegistrationHooks: Send + Sync + 'static {
|
||||
fn remove_user_themes(&self, _themes: Vec<SharedString>) {}
|
||||
|
||||
fn load_user_theme(&self, _theme_path: PathBuf, _fs: Arc<dyn Fs>) -> Task<Result<()>> {
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
|
||||
fn list_theme_names(
|
||||
&self,
|
||||
_theme_path: PathBuf,
|
||||
_fs: Arc<dyn Fs>,
|
||||
) -> Task<Result<Vec<String>>> {
|
||||
Task::ready(Ok(Vec::new()))
|
||||
}
|
||||
|
||||
fn reload_current_theme(&self, _cx: &mut AppContext) {}
|
||||
|
||||
fn register_language(
|
||||
&self,
|
||||
_language: LanguageName,
|
||||
_grammar: Option<Arc<str>>,
|
||||
_matcher: language::LanguageMatcher,
|
||||
_load: Arc<dyn Fn() -> Result<LoadedLanguage> + 'static + Send + Sync>,
|
||||
) {
|
||||
}
|
||||
|
||||
fn register_lsp_adapter(&self, _language: LanguageName, _adapter: ExtensionLspAdapter) {}
|
||||
|
||||
fn remove_lsp_adapter(&self, _language: &LanguageName, _server_name: &LanguageServerName) {}
|
||||
|
||||
fn register_wasm_grammars(&self, _grammars: Vec<(Arc<str>, PathBuf)>) {}
|
||||
|
||||
fn remove_languages(
|
||||
&self,
|
||||
_languages_to_remove: &[LanguageName],
|
||||
_grammars_to_remove: &[Arc<str>],
|
||||
) {
|
||||
}
|
||||
|
||||
fn register_slash_command(
|
||||
&self,
|
||||
_extension: Arc<dyn Extension>,
|
||||
_command: extension::SlashCommand,
|
||||
) {
|
||||
}
|
||||
|
||||
fn register_context_server(
|
||||
&self,
|
||||
_id: Arc<str>,
|
||||
_extension: WasmExtension,
|
||||
_cx: &mut AppContext,
|
||||
) {
|
||||
}
|
||||
|
||||
fn register_docs_provider(&self, _extension: Arc<dyn Extension>, _provider_id: Arc<str>) {}
|
||||
|
||||
fn register_snippets(&self, _path: &PathBuf, _snippet_contents: &str) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update_lsp_status(
|
||||
&self,
|
||||
_server_name: lsp::LanguageServerName,
|
||||
_status: language::LanguageServerBinaryStatus,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ExtensionStore {
|
||||
pub registration_hooks: Arc<dyn ExtensionRegistrationHooks>,
|
||||
pub proxy: Arc<ExtensionHostProxy>,
|
||||
pub builder: Arc<ExtensionBuilder>,
|
||||
pub extension_index: ExtensionIndex,
|
||||
pub fs: Arc<dyn Fs>,
|
||||
@@ -178,6 +113,8 @@ pub struct ExtensionStore {
|
||||
pub wasm_host: Arc<WasmHost>,
|
||||
pub wasm_extensions: Vec<(Arc<ExtensionManifest>, WasmExtension)>,
|
||||
pub tasks: Vec<Task<()>>,
|
||||
pub ssh_clients: HashMap<String, WeakModel<SshRemoteClient>>,
|
||||
pub ssh_registered_tx: UnboundedSender<()>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
@@ -231,7 +168,7 @@ pub struct ExtensionIndexLanguageEntry {
|
||||
actions!(zed, [ReloadExtensions]);
|
||||
|
||||
pub fn init(
|
||||
registration_hooks: Arc<dyn ExtensionRegistrationHooks>,
|
||||
extension_host_proxy: Arc<ExtensionHostProxy>,
|
||||
fs: Arc<dyn Fs>,
|
||||
client: Arc<Client>,
|
||||
node_runtime: NodeRuntime,
|
||||
@@ -243,7 +180,7 @@ pub fn init(
|
||||
ExtensionStore::new(
|
||||
paths::extensions_dir().clone(),
|
||||
None,
|
||||
registration_hooks,
|
||||
extension_host_proxy,
|
||||
fs,
|
||||
client.http_client().clone(),
|
||||
client.http_client().clone(),
|
||||
@@ -275,7 +212,7 @@ impl ExtensionStore {
|
||||
pub fn new(
|
||||
extensions_dir: PathBuf,
|
||||
build_dir: Option<PathBuf>,
|
||||
extension_api: Arc<dyn ExtensionRegistrationHooks>,
|
||||
extension_host_proxy: Arc<ExtensionHostProxy>,
|
||||
fs: Arc<dyn Fs>,
|
||||
http_client: Arc<HttpClientWithUrl>,
|
||||
builder_client: Arc<dyn HttpClient>,
|
||||
@@ -289,8 +226,9 @@ impl ExtensionStore {
|
||||
let index_path = extensions_dir.join("index.json");
|
||||
|
||||
let (reload_tx, mut reload_rx) = unbounded();
|
||||
let (connection_registered_tx, mut connection_registered_rx) = unbounded();
|
||||
let mut this = Self {
|
||||
registration_hooks: extension_api.clone(),
|
||||
proxy: extension_host_proxy.clone(),
|
||||
extension_index: Default::default(),
|
||||
installed_dir,
|
||||
index_path,
|
||||
@@ -302,7 +240,7 @@ impl ExtensionStore {
|
||||
fs.clone(),
|
||||
http_client.clone(),
|
||||
node_runtime,
|
||||
extension_api,
|
||||
extension_host_proxy,
|
||||
work_dir,
|
||||
cx,
|
||||
),
|
||||
@@ -312,6 +250,9 @@ impl ExtensionStore {
|
||||
telemetry,
|
||||
reload_tx,
|
||||
tasks: Vec::new(),
|
||||
|
||||
ssh_clients: HashMap::default(),
|
||||
ssh_registered_tx: connection_registered_tx,
|
||||
};
|
||||
|
||||
// The extensions store maintains an index file, which contains a complete
|
||||
@@ -337,7 +278,10 @@ impl ExtensionStore {
|
||||
if let (Ok(Some(index_metadata)), Ok(Some(extensions_metadata))) =
|
||||
(index_metadata, extensions_metadata)
|
||||
{
|
||||
if index_metadata.mtime > extensions_metadata.mtime {
|
||||
if index_metadata
|
||||
.mtime
|
||||
.bad_is_greater_than(extensions_metadata.mtime)
|
||||
{
|
||||
extension_index_needs_rebuild = false;
|
||||
}
|
||||
}
|
||||
@@ -386,6 +330,14 @@ impl ExtensionStore {
|
||||
.await;
|
||||
index_changed = false;
|
||||
}
|
||||
|
||||
Self::update_ssh_clients(&this, &mut cx).await?;
|
||||
}
|
||||
_ = connection_registered_rx.next() => {
|
||||
debounce_timer = cx
|
||||
.background_executor()
|
||||
.timer(RELOAD_DEBOUNCE_DURATION)
|
||||
.fuse();
|
||||
}
|
||||
extension_id = reload_rx.next() => {
|
||||
let Some(extension_id) = extension_id else { break; };
|
||||
@@ -1089,16 +1041,16 @@ impl ExtensionStore {
|
||||
grammars_to_remove.extend(extension.manifest.grammars.keys().cloned());
|
||||
for (language_server_name, config) in extension.manifest.language_servers.iter() {
|
||||
for language in config.languages() {
|
||||
self.registration_hooks
|
||||
.remove_lsp_adapter(&language, language_server_name);
|
||||
self.proxy
|
||||
.remove_language_server(&language, language_server_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.wasm_extensions
|
||||
.retain(|(extension, _)| !extensions_to_unload.contains(&extension.id));
|
||||
self.registration_hooks.remove_user_themes(themes_to_remove);
|
||||
self.registration_hooks
|
||||
self.proxy.remove_user_themes(themes_to_remove);
|
||||
self.proxy
|
||||
.remove_languages(&languages_to_remove, &grammars_to_remove);
|
||||
|
||||
let languages_to_add = new_index
|
||||
@@ -1133,8 +1085,7 @@ impl ExtensionStore {
|
||||
}));
|
||||
}
|
||||
|
||||
self.registration_hooks
|
||||
.register_wasm_grammars(grammars_to_add);
|
||||
self.proxy.register_grammars(grammars_to_add);
|
||||
|
||||
for (language_name, language) in languages_to_add {
|
||||
let mut language_path = self.installed_dir.clone();
|
||||
@@ -1142,7 +1093,7 @@ impl ExtensionStore {
|
||||
Path::new(language.extension.as_ref()),
|
||||
language.path.as_path(),
|
||||
]);
|
||||
self.registration_hooks.register_language(
|
||||
self.proxy.register_language(
|
||||
language_name.clone(),
|
||||
language.grammar.clone(),
|
||||
language.matcher.clone(),
|
||||
@@ -1172,7 +1123,7 @@ impl ExtensionStore {
|
||||
let fs = self.fs.clone();
|
||||
let wasm_host = self.wasm_host.clone();
|
||||
let root_dir = self.installed_dir.clone();
|
||||
let api = self.registration_hooks.clone();
|
||||
let proxy = self.proxy.clone();
|
||||
let extension_entries = extensions_to_load
|
||||
.iter()
|
||||
.filter_map(|name| new_index.extensions.get(name).cloned())
|
||||
@@ -1188,13 +1139,17 @@ impl ExtensionStore {
|
||||
let fs = fs.clone();
|
||||
async move {
|
||||
for theme_path in themes_to_add.into_iter() {
|
||||
api.load_user_theme(theme_path, fs.clone()).await.log_err();
|
||||
proxy
|
||||
.load_user_theme(theme_path, fs.clone())
|
||||
.await
|
||||
.log_err();
|
||||
}
|
||||
|
||||
for snippets_path in &snippets_to_add {
|
||||
if let Some(snippets_contents) = fs.load(snippets_path).await.log_err()
|
||||
{
|
||||
api.register_snippets(snippets_path, &snippets_contents)
|
||||
proxy
|
||||
.register_snippet(snippets_path, &snippets_contents)
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
@@ -1235,19 +1190,16 @@ impl ExtensionStore {
|
||||
|
||||
for (language_server_id, language_server_config) in &manifest.language_servers {
|
||||
for language in language_server_config.languages() {
|
||||
this.registration_hooks.register_lsp_adapter(
|
||||
this.proxy.register_language_server(
|
||||
extension.clone(),
|
||||
language_server_id.clone(),
|
||||
language.clone(),
|
||||
ExtensionLspAdapter {
|
||||
extension: extension.clone(),
|
||||
language_server_id: language_server_id.clone(),
|
||||
language_name: language.clone(),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (slash_command_name, slash_command) in &manifest.slash_commands {
|
||||
this.registration_hooks.register_slash_command(
|
||||
this.proxy.register_slash_command(
|
||||
extension.clone(),
|
||||
extension::SlashCommand {
|
||||
name: slash_command_name.to_string(),
|
||||
@@ -1262,21 +1214,18 @@ impl ExtensionStore {
|
||||
}
|
||||
|
||||
for (id, _context_server_entry) in &manifest.context_servers {
|
||||
this.registration_hooks.register_context_server(
|
||||
id.clone(),
|
||||
wasm_extension.clone(),
|
||||
cx,
|
||||
);
|
||||
this.proxy
|
||||
.register_context_server(extension.clone(), id.clone(), cx);
|
||||
}
|
||||
|
||||
for (provider_id, _provider) in &manifest.indexed_docs_providers {
|
||||
this.registration_hooks
|
||||
.register_docs_provider(extension.clone(), provider_id.clone());
|
||||
this.proxy
|
||||
.register_indexed_docs_provider(extension.clone(), provider_id.clone());
|
||||
}
|
||||
}
|
||||
|
||||
this.wasm_extensions.extend(wasm_extensions);
|
||||
this.registration_hooks.reload_current_theme(cx);
|
||||
this.proxy.reload_current_theme(cx);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
@@ -1287,7 +1236,7 @@ impl ExtensionStore {
|
||||
let work_dir = self.wasm_host.work_dir.clone();
|
||||
let extensions_dir = self.installed_dir.clone();
|
||||
let index_path = self.index_path.clone();
|
||||
let extension_api = self.registration_hooks.clone();
|
||||
let proxy = self.proxy.clone();
|
||||
cx.background_executor().spawn(async move {
|
||||
let start_time = Instant::now();
|
||||
let mut index = ExtensionIndex::default();
|
||||
@@ -1313,7 +1262,7 @@ impl ExtensionStore {
|
||||
fs.clone(),
|
||||
extension_dir,
|
||||
&mut index,
|
||||
extension_api.clone(),
|
||||
proxy.clone(),
|
||||
)
|
||||
.await
|
||||
.log_err();
|
||||
@@ -1336,7 +1285,7 @@ impl ExtensionStore {
|
||||
fs: Arc<dyn Fs>,
|
||||
extension_dir: PathBuf,
|
||||
index: &mut ExtensionIndex,
|
||||
extension_api: Arc<dyn ExtensionRegistrationHooks>,
|
||||
proxy: Arc<ExtensionHostProxy>,
|
||||
) -> Result<()> {
|
||||
let mut extension_manifest = ExtensionManifest::load(fs.clone(), &extension_dir).await?;
|
||||
let extension_id = extension_manifest.id.clone();
|
||||
@@ -1388,7 +1337,7 @@ impl ExtensionStore {
|
||||
continue;
|
||||
};
|
||||
|
||||
let Some(theme_families) = extension_api
|
||||
let Some(theme_families) = proxy
|
||||
.list_theme_names(theme_path.clone(), fs.clone())
|
||||
.await
|
||||
.log_err()
|
||||
@@ -1431,6 +1380,144 @@ impl ExtensionStore {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn prepare_remote_extension(
|
||||
&mut self,
|
||||
extension_id: Arc<str>,
|
||||
tmp_dir: PathBuf,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let src_dir = self.extensions_dir().join(extension_id.as_ref());
|
||||
let Some(loaded_extension) = self.extension_index.extensions.get(&extension_id).cloned()
|
||||
else {
|
||||
return Task::ready(Err(anyhow!("extension no longer installed")));
|
||||
};
|
||||
let fs = self.fs.clone();
|
||||
cx.background_executor().spawn(async move {
|
||||
for well_known_path in ["extension.toml", "extension.json", "extension.wasm"] {
|
||||
if fs.is_file(&src_dir.join(well_known_path)).await {
|
||||
fs.copy_file(
|
||||
&src_dir.join(well_known_path),
|
||||
&tmp_dir.join(well_known_path),
|
||||
fs::CopyOptions::default(),
|
||||
)
|
||||
.await?
|
||||
}
|
||||
}
|
||||
|
||||
for language_path in loaded_extension.manifest.languages.iter() {
|
||||
if fs
|
||||
.is_file(&src_dir.join(language_path).join("config.toml"))
|
||||
.await
|
||||
{
|
||||
fs.create_dir(&tmp_dir.join(language_path)).await?;
|
||||
fs.copy_file(
|
||||
&src_dir.join(language_path).join("config.toml"),
|
||||
&tmp_dir.join(language_path).join("config.toml"),
|
||||
fs::CopyOptions::default(),
|
||||
)
|
||||
.await?
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
async fn sync_extensions_over_ssh(
|
||||
this: &WeakModel<Self>,
|
||||
client: WeakModel<SshRemoteClient>,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
let extensions = this.update(cx, |this, _cx| {
|
||||
this.extension_index
|
||||
.extensions
|
||||
.iter()
|
||||
.filter_map(|(id, entry)| {
|
||||
if entry.manifest.language_servers.is_empty() {
|
||||
return None;
|
||||
}
|
||||
Some(proto::Extension {
|
||||
id: id.to_string(),
|
||||
version: entry.manifest.version.to_string(),
|
||||
dev: entry.dev,
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
})?;
|
||||
|
||||
let response = client
|
||||
.update(cx, |client, _cx| {
|
||||
client
|
||||
.proto_client()
|
||||
.request(proto::SyncExtensions { extensions })
|
||||
})?
|
||||
.await?;
|
||||
|
||||
for missing_extension in response.missing_extensions.into_iter() {
|
||||
let tmp_dir = tempfile::tempdir()?;
|
||||
this.update(cx, |this, cx| {
|
||||
this.prepare_remote_extension(
|
||||
missing_extension.id.clone().into(),
|
||||
tmp_dir.path().to_owned(),
|
||||
cx,
|
||||
)
|
||||
})?
|
||||
.await?;
|
||||
let dest_dir = PathBuf::from(&response.tmp_dir).join(missing_extension.clone().id);
|
||||
log::info!("Uploading extension {}", missing_extension.clone().id);
|
||||
|
||||
client
|
||||
.update(cx, |client, cx| {
|
||||
client.upload_directory(tmp_dir.path().to_owned(), dest_dir.clone(), cx)
|
||||
})?
|
||||
.await?;
|
||||
|
||||
client
|
||||
.update(cx, |client, _cx| {
|
||||
client.proto_client().request(proto::InstallExtension {
|
||||
tmp_dir: dest_dir.to_string_lossy().to_string(),
|
||||
extension: Some(missing_extension),
|
||||
})
|
||||
})?
|
||||
.await?;
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
}
|
||||
|
||||
pub async fn update_ssh_clients(
|
||||
this: &WeakModel<Self>,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
let clients = this.update(cx, |this, _cx| {
|
||||
this.ssh_clients.retain(|_k, v| v.upgrade().is_some());
|
||||
this.ssh_clients.values().cloned().collect::<Vec<_>>()
|
||||
})?;
|
||||
|
||||
for client in clients {
|
||||
Self::sync_extensions_over_ssh(&this, client, cx)
|
||||
.await
|
||||
.log_err();
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
}
|
||||
|
||||
pub fn register_ssh_client(
|
||||
&mut self,
|
||||
client: Model<SshRemoteClient>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
let connection_options = client.read(cx).connection_options();
|
||||
if self.ssh_clients.contains_key(&connection_options.ssh_url()) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.ssh_clients
|
||||
.insert(connection_options.ssh_url(), client.downgrade());
|
||||
self.ssh_registered_tx.unbounded_send(()).ok();
|
||||
}
|
||||
}
|
||||
|
||||
fn load_plugin_queries(root_path: &Path) -> LanguageQueries {
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
use crate::extension_lsp_adapter::ExtensionLspAdapter;
|
||||
use crate::{
|
||||
Event, ExtensionIndex, ExtensionIndexEntry, ExtensionIndexLanguageEntry,
|
||||
ExtensionIndexThemeEntry, ExtensionManifest, ExtensionSettings, ExtensionStore,
|
||||
GrammarManifestEntry, SchemaVersion, RELOAD_DEBOUNCE_DURATION,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use async_compression::futures::bufread::GzipEncoder;
|
||||
use collections::BTreeMap;
|
||||
use extension::ExtensionHostProxy;
|
||||
use fs::{FakeFs, Fs, RealFs};
|
||||
use futures::{io::BufReader, AsyncReadExt, StreamExt};
|
||||
use gpui::{BackgroundExecutor, Context, SemanticVersion, SharedString, Task, TestAppContext};
|
||||
use gpui::{Context, SemanticVersion, TestAppContext};
|
||||
use http_client::{FakeHttpClient, Response};
|
||||
use language::{LanguageMatcher, LanguageRegistry, LanguageServerBinaryStatus, LoadedLanguage};
|
||||
use language::{LanguageMatcher, LanguageRegistry, LanguageServerBinaryStatus};
|
||||
use lsp::LanguageServerName;
|
||||
use node_runtime::NodeRuntime;
|
||||
use parking_lot::Mutex;
|
||||
@@ -28,84 +27,6 @@ use std::{
|
||||
use theme::ThemeRegistry;
|
||||
use util::test::temp_tree;
|
||||
|
||||
use crate::ExtensionRegistrationHooks;
|
||||
|
||||
struct TestExtensionRegistrationHooks {
|
||||
executor: BackgroundExecutor,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
theme_registry: Arc<ThemeRegistry>,
|
||||
}
|
||||
|
||||
impl ExtensionRegistrationHooks for TestExtensionRegistrationHooks {
|
||||
fn list_theme_names(&self, path: PathBuf, fs: Arc<dyn Fs>) -> Task<Result<Vec<String>>> {
|
||||
self.executor.spawn(async move {
|
||||
let themes = theme::read_user_theme(&path, fs).await?;
|
||||
Ok(themes.themes.into_iter().map(|theme| theme.name).collect())
|
||||
})
|
||||
}
|
||||
|
||||
fn load_user_theme(&self, theme_path: PathBuf, fs: Arc<dyn fs::Fs>) -> Task<Result<()>> {
|
||||
let theme_registry = self.theme_registry.clone();
|
||||
self.executor
|
||||
.spawn(async move { theme_registry.load_user_theme(&theme_path, fs).await })
|
||||
}
|
||||
|
||||
fn remove_user_themes(&self, themes: Vec<SharedString>) {
|
||||
self.theme_registry.remove_user_themes(&themes);
|
||||
}
|
||||
|
||||
fn register_language(
|
||||
&self,
|
||||
language: language::LanguageName,
|
||||
grammar: Option<Arc<str>>,
|
||||
matcher: language::LanguageMatcher,
|
||||
load: Arc<dyn Fn() -> Result<LoadedLanguage> + 'static + Send + Sync>,
|
||||
) {
|
||||
self.language_registry
|
||||
.register_language(language, grammar, matcher, load)
|
||||
}
|
||||
|
||||
fn remove_languages(
|
||||
&self,
|
||||
languages_to_remove: &[language::LanguageName],
|
||||
grammars_to_remove: &[Arc<str>],
|
||||
) {
|
||||
self.language_registry
|
||||
.remove_languages(&languages_to_remove, &grammars_to_remove);
|
||||
}
|
||||
|
||||
fn register_wasm_grammars(&self, grammars: Vec<(Arc<str>, PathBuf)>) {
|
||||
self.language_registry.register_wasm_grammars(grammars)
|
||||
}
|
||||
|
||||
fn register_lsp_adapter(
|
||||
&self,
|
||||
language_name: language::LanguageName,
|
||||
adapter: ExtensionLspAdapter,
|
||||
) {
|
||||
self.language_registry
|
||||
.register_lsp_adapter(language_name, Arc::new(adapter));
|
||||
}
|
||||
|
||||
fn update_lsp_status(
|
||||
&self,
|
||||
server_name: lsp::LanguageServerName,
|
||||
status: LanguageServerBinaryStatus,
|
||||
) {
|
||||
self.language_registry
|
||||
.update_lsp_status(server_name, status);
|
||||
}
|
||||
|
||||
fn remove_lsp_adapter(
|
||||
&self,
|
||||
language_name: &language::LanguageName,
|
||||
server_name: &lsp::LanguageServerName,
|
||||
) {
|
||||
self.language_registry
|
||||
.remove_lsp_adapter(language_name, server_name);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[ctor::ctor]
|
||||
fn init_logger() {
|
||||
@@ -337,20 +258,18 @@ async fn test_extension_store(cx: &mut TestAppContext) {
|
||||
.collect(),
|
||||
};
|
||||
|
||||
let language_registry = Arc::new(LanguageRegistry::test(cx.executor()));
|
||||
let proxy = Arc::new(ExtensionHostProxy::new());
|
||||
let theme_registry = Arc::new(ThemeRegistry::new(Box::new(())));
|
||||
let registration_hooks = Arc::new(TestExtensionRegistrationHooks {
|
||||
executor: cx.executor(),
|
||||
language_registry: language_registry.clone(),
|
||||
theme_registry: theme_registry.clone(),
|
||||
});
|
||||
theme_extension::init(proxy.clone(), theme_registry.clone(), cx.executor());
|
||||
let language_registry = Arc::new(LanguageRegistry::test(cx.executor()));
|
||||
language_extension::init(proxy.clone(), language_registry.clone());
|
||||
let node_runtime = NodeRuntime::unavailable();
|
||||
|
||||
let store = cx.new_model(|cx| {
|
||||
ExtensionStore::new(
|
||||
PathBuf::from("/the-extension-dir"),
|
||||
None,
|
||||
registration_hooks.clone(),
|
||||
proxy.clone(),
|
||||
fs.clone(),
|
||||
http_client.clone(),
|
||||
http_client.clone(),
|
||||
@@ -475,7 +394,7 @@ async fn test_extension_store(cx: &mut TestAppContext) {
|
||||
ExtensionStore::new(
|
||||
PathBuf::from("/the-extension-dir"),
|
||||
None,
|
||||
registration_hooks,
|
||||
proxy,
|
||||
fs.clone(),
|
||||
http_client.clone(),
|
||||
http_client.clone(),
|
||||
@@ -558,13 +477,11 @@ async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) {
|
||||
|
||||
let project = Project::test(fs.clone(), [project_dir.as_path()], cx).await;
|
||||
|
||||
let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
|
||||
let proxy = Arc::new(ExtensionHostProxy::new());
|
||||
let theme_registry = Arc::new(ThemeRegistry::new(Box::new(())));
|
||||
let registration_hooks = Arc::new(TestExtensionRegistrationHooks {
|
||||
executor: cx.executor(),
|
||||
language_registry: language_registry.clone(),
|
||||
theme_registry: theme_registry.clone(),
|
||||
});
|
||||
theme_extension::init(proxy.clone(), theme_registry.clone(), cx.executor());
|
||||
let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
|
||||
language_extension::init(proxy.clone(), language_registry.clone());
|
||||
let node_runtime = NodeRuntime::unavailable();
|
||||
|
||||
let mut status_updates = language_registry.language_server_binary_statuses();
|
||||
@@ -658,7 +575,7 @@ async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) {
|
||||
ExtensionStore::new(
|
||||
extensions_dir.clone(),
|
||||
Some(cache_dir),
|
||||
registration_hooks,
|
||||
proxy,
|
||||
fs.clone(),
|
||||
extension_client.clone(),
|
||||
builder_client,
|
||||
|
||||
319
crates/extension_host/src/headless_host.rs
Normal file
319
crates/extension_host/src/headless_host.rs
Normal file
@@ -0,0 +1,319 @@
|
||||
use std::{path::PathBuf, sync::Arc};
|
||||
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use client::{proto, TypedEnvelope};
|
||||
use collections::{HashMap, HashSet};
|
||||
use extension::{
|
||||
Extension, ExtensionHostProxy, ExtensionLanguageProxy, ExtensionLanguageServerProxy,
|
||||
ExtensionManifest,
|
||||
};
|
||||
use fs::{Fs, RemoveOptions, RenameOptions};
|
||||
use gpui::{AppContext, AsyncAppContext, Context, Model, ModelContext, Task, WeakModel};
|
||||
use http_client::HttpClient;
|
||||
use language::{LanguageConfig, LanguageName, LanguageQueries, LoadedLanguage};
|
||||
use lsp::LanguageServerName;
|
||||
use node_runtime::NodeRuntime;
|
||||
|
||||
use crate::wasm_host::{WasmExtension, WasmHost};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ExtensionVersion {
|
||||
pub id: String,
|
||||
pub version: String,
|
||||
pub dev: bool,
|
||||
}
|
||||
|
||||
pub struct HeadlessExtensionStore {
|
||||
pub fs: Arc<dyn Fs>,
|
||||
pub extension_dir: PathBuf,
|
||||
pub proxy: Arc<ExtensionHostProxy>,
|
||||
pub wasm_host: Arc<WasmHost>,
|
||||
pub loaded_extensions: HashMap<Arc<str>, Arc<str>>,
|
||||
pub loaded_languages: HashMap<Arc<str>, Vec<LanguageName>>,
|
||||
pub loaded_language_servers: HashMap<Arc<str>, Vec<(LanguageServerName, LanguageName)>>,
|
||||
}
|
||||
|
||||
impl HeadlessExtensionStore {
|
||||
pub fn new(
|
||||
fs: Arc<dyn Fs>,
|
||||
http_client: Arc<dyn HttpClient>,
|
||||
extension_dir: PathBuf,
|
||||
extension_host_proxy: Arc<ExtensionHostProxy>,
|
||||
node_runtime: NodeRuntime,
|
||||
cx: &mut AppContext,
|
||||
) -> Model<Self> {
|
||||
cx.new_model(|cx| Self {
|
||||
fs: fs.clone(),
|
||||
wasm_host: WasmHost::new(
|
||||
fs.clone(),
|
||||
http_client.clone(),
|
||||
node_runtime,
|
||||
extension_host_proxy.clone(),
|
||||
extension_dir.join("work"),
|
||||
cx,
|
||||
),
|
||||
extension_dir,
|
||||
proxy: extension_host_proxy,
|
||||
loaded_extensions: Default::default(),
|
||||
loaded_languages: Default::default(),
|
||||
loaded_language_servers: Default::default(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn sync_extensions(
|
||||
&mut self,
|
||||
extensions: Vec<ExtensionVersion>,
|
||||
cx: &ModelContext<Self>,
|
||||
) -> Task<Result<Vec<ExtensionVersion>>> {
|
||||
let on_client = HashSet::from_iter(extensions.iter().map(|e| e.id.as_str()));
|
||||
let to_remove: Vec<Arc<str>> = self
|
||||
.loaded_extensions
|
||||
.keys()
|
||||
.filter(|id| !on_client.contains(id.as_ref()))
|
||||
.cloned()
|
||||
.collect();
|
||||
let to_load: Vec<ExtensionVersion> = extensions
|
||||
.into_iter()
|
||||
.filter(|e| {
|
||||
if e.dev {
|
||||
return true;
|
||||
}
|
||||
!self
|
||||
.loaded_extensions
|
||||
.get(e.id.as_str())
|
||||
.is_some_and(|loaded| loaded.as_ref() == e.version.as_str())
|
||||
})
|
||||
.collect();
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let mut missing = Vec::new();
|
||||
|
||||
for extension_id in to_remove {
|
||||
log::info!("removing extension: {}", extension_id);
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.uninstall_extension(&extension_id, cx)
|
||||
})?
|
||||
.await?;
|
||||
}
|
||||
|
||||
for extension in to_load {
|
||||
if let Err(e) = Self::load_extension(this.clone(), extension.clone(), &mut cx).await
|
||||
{
|
||||
log::info!("failed to load extension: {}, {:?}", extension.id, e);
|
||||
missing.push(extension)
|
||||
} else if extension.dev {
|
||||
missing.push(extension)
|
||||
}
|
||||
}
|
||||
|
||||
Ok(missing)
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn load_extension(
|
||||
this: WeakModel<Self>,
|
||||
extension: ExtensionVersion,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
let (fs, wasm_host, extension_dir) = this.update(cx, |this, _cx| {
|
||||
this.loaded_extensions.insert(
|
||||
extension.id.clone().into(),
|
||||
extension.version.clone().into(),
|
||||
);
|
||||
(
|
||||
this.fs.clone(),
|
||||
this.wasm_host.clone(),
|
||||
this.extension_dir.join(&extension.id),
|
||||
)
|
||||
})?;
|
||||
|
||||
let manifest = Arc::new(ExtensionManifest::load(fs.clone(), &extension_dir).await?);
|
||||
|
||||
debug_assert!(!manifest.languages.is_empty() || !manifest.language_servers.is_empty());
|
||||
|
||||
if manifest.version.as_ref() != extension.version.as_str() {
|
||||
anyhow::bail!(
|
||||
"mismatched versions: ({}) != ({})",
|
||||
manifest.version,
|
||||
extension.version
|
||||
)
|
||||
}
|
||||
|
||||
for language_path in &manifest.languages {
|
||||
let language_path = extension_dir.join(language_path);
|
||||
let config = fs.load(&language_path.join("config.toml")).await?;
|
||||
let mut config = ::toml::from_str::<LanguageConfig>(&config)?;
|
||||
|
||||
this.update(cx, |this, _cx| {
|
||||
this.loaded_languages
|
||||
.entry(manifest.id.clone())
|
||||
.or_default()
|
||||
.push(config.name.clone());
|
||||
|
||||
config.grammar = None;
|
||||
|
||||
this.proxy.register_language(
|
||||
config.name.clone(),
|
||||
None,
|
||||
config.matcher.clone(),
|
||||
Arc::new(move || {
|
||||
Ok(LoadedLanguage {
|
||||
config: config.clone(),
|
||||
queries: LanguageQueries::default(),
|
||||
context_provider: None,
|
||||
toolchain_provider: None,
|
||||
})
|
||||
}),
|
||||
);
|
||||
})?;
|
||||
}
|
||||
|
||||
if manifest.language_servers.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let wasm_extension: Arc<dyn Extension> =
|
||||
Arc::new(WasmExtension::load(extension_dir, &manifest, wasm_host.clone(), &cx).await?);
|
||||
|
||||
for (language_server_id, language_server_config) in &manifest.language_servers {
|
||||
for language in language_server_config.languages() {
|
||||
this.update(cx, |this, _cx| {
|
||||
this.loaded_language_servers
|
||||
.entry(manifest.id.clone())
|
||||
.or_default()
|
||||
.push((language_server_id.clone(), language.clone()));
|
||||
this.proxy.register_language_server(
|
||||
wasm_extension.clone(),
|
||||
language_server_id.clone(),
|
||||
language.clone(),
|
||||
);
|
||||
})?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn uninstall_extension(
|
||||
&mut self,
|
||||
extension_id: &Arc<str>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
self.loaded_extensions.remove(extension_id);
|
||||
|
||||
let languages_to_remove = self
|
||||
.loaded_languages
|
||||
.remove(extension_id)
|
||||
.unwrap_or_default();
|
||||
self.proxy.remove_languages(&languages_to_remove, &[]);
|
||||
|
||||
for (language_server_name, language) in self
|
||||
.loaded_language_servers
|
||||
.remove(extension_id)
|
||||
.unwrap_or_default()
|
||||
{
|
||||
self.proxy
|
||||
.remove_language_server(&language, &language_server_name);
|
||||
}
|
||||
|
||||
let path = self.extension_dir.join(&extension_id.to_string());
|
||||
let fs = self.fs.clone();
|
||||
cx.spawn(|_, _| async move {
|
||||
fs.remove_dir(
|
||||
&path,
|
||||
RemoveOptions {
|
||||
recursive: true,
|
||||
ignore_if_not_exists: true,
|
||||
},
|
||||
)
|
||||
.await
|
||||
})
|
||||
}
|
||||
|
||||
pub fn install_extension(
|
||||
&mut self,
|
||||
extension: ExtensionVersion,
|
||||
tmp_path: PathBuf,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let path = self.extension_dir.join(&extension.id);
|
||||
let fs = self.fs.clone();
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
if fs.is_dir(&path).await {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.uninstall_extension(&extension.id.clone().into(), cx)
|
||||
})?
|
||||
.await?;
|
||||
}
|
||||
|
||||
fs.rename(&tmp_path, &path, RenameOptions::default())
|
||||
.await?;
|
||||
|
||||
Self::load_extension(this, extension, &mut cx).await
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn handle_sync_extensions(
|
||||
extension_store: Model<HeadlessExtensionStore>,
|
||||
envelope: TypedEnvelope<proto::SyncExtensions>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<proto::SyncExtensionsResponse> {
|
||||
let requested_extensions =
|
||||
envelope
|
||||
.payload
|
||||
.extensions
|
||||
.into_iter()
|
||||
.map(|p| ExtensionVersion {
|
||||
id: p.id,
|
||||
version: p.version,
|
||||
dev: p.dev,
|
||||
});
|
||||
let missing_extensions = extension_store
|
||||
.update(&mut cx, |extension_store, cx| {
|
||||
extension_store.sync_extensions(requested_extensions.collect(), cx)
|
||||
})?
|
||||
.await?;
|
||||
|
||||
Ok(proto::SyncExtensionsResponse {
|
||||
missing_extensions: missing_extensions
|
||||
.into_iter()
|
||||
.map(|e| proto::Extension {
|
||||
id: e.id,
|
||||
version: e.version,
|
||||
dev: e.dev,
|
||||
})
|
||||
.collect(),
|
||||
tmp_dir: paths::remote_extensions_uploads_dir()
|
||||
.to_string_lossy()
|
||||
.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn handle_install_extension(
|
||||
extensions: Model<HeadlessExtensionStore>,
|
||||
envelope: TypedEnvelope<proto::InstallExtension>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<proto::Ack> {
|
||||
let extension = envelope
|
||||
.payload
|
||||
.extension
|
||||
.with_context(|| anyhow!("Invalid InstallExtension request"))?;
|
||||
|
||||
extensions
|
||||
.update(&mut cx, |extensions, cx| {
|
||||
extensions.install_extension(
|
||||
ExtensionVersion {
|
||||
id: extension.id,
|
||||
version: extension.version,
|
||||
dev: extension.dev,
|
||||
},
|
||||
PathBuf::from(envelope.payload.tmp_dir),
|
||||
cx,
|
||||
)
|
||||
})?
|
||||
.await?;
|
||||
|
||||
Ok(proto::Ack {})
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
pub mod wit;
|
||||
|
||||
use crate::{ExtensionManifest, ExtensionRegistrationHooks};
|
||||
use crate::ExtensionManifest;
|
||||
use anyhow::{anyhow, bail, Context as _, Result};
|
||||
use async_trait::async_trait;
|
||||
use extension::{
|
||||
CodeLabel, Command, Completion, KeyValueStoreDelegate, SlashCommand,
|
||||
SlashCommandArgumentCompletion, SlashCommandOutput, Symbol, WorktreeDelegate,
|
||||
CodeLabel, Command, Completion, ExtensionHostProxy, KeyValueStoreDelegate, ProjectDelegate,
|
||||
SlashCommand, SlashCommandArgumentCompletion, SlashCommandOutput, Symbol, WorktreeDelegate,
|
||||
};
|
||||
use fs::{normalize_path, Fs};
|
||||
use futures::future::LocalBoxFuture;
|
||||
@@ -34,14 +34,13 @@ use wasmtime::{
|
||||
};
|
||||
use wasmtime_wasi::{self as wasi, WasiView};
|
||||
use wit::Extension;
|
||||
pub use wit::ExtensionProject;
|
||||
|
||||
pub struct WasmHost {
|
||||
engine: Engine,
|
||||
release_channel: ReleaseChannel,
|
||||
http_client: Arc<dyn HttpClient>,
|
||||
node_runtime: NodeRuntime,
|
||||
pub registration_hooks: Arc<dyn ExtensionRegistrationHooks>,
|
||||
pub(crate) proxy: Arc<ExtensionHostProxy>,
|
||||
fs: Arc<dyn Fs>,
|
||||
pub work_dir: PathBuf,
|
||||
_main_thread_message_task: Task<()>,
|
||||
@@ -238,6 +237,25 @@ impl extension::Extension for WasmExtension {
|
||||
.await
|
||||
}
|
||||
|
||||
async fn context_server_command(
|
||||
&self,
|
||||
context_server_id: Arc<str>,
|
||||
project: Arc<dyn ProjectDelegate>,
|
||||
) -> Result<Command> {
|
||||
self.call(|extension, store| {
|
||||
async move {
|
||||
let project_resource = store.data_mut().table().push(project)?;
|
||||
let command = extension
|
||||
.call_context_server_command(store, context_server_id.clone(), project_resource)
|
||||
.await?
|
||||
.map_err(|err| anyhow!("{err}"))?;
|
||||
anyhow::Ok(command.into())
|
||||
}
|
||||
.boxed()
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn suggest_docs_packages(&self, provider: Arc<str>) -> Result<Vec<String>> {
|
||||
self.call(|extension, store| {
|
||||
async move {
|
||||
@@ -312,7 +330,7 @@ impl WasmHost {
|
||||
fs: Arc<dyn Fs>,
|
||||
http_client: Arc<dyn HttpClient>,
|
||||
node_runtime: NodeRuntime,
|
||||
registration_hooks: Arc<dyn ExtensionRegistrationHooks>,
|
||||
proxy: Arc<ExtensionHostProxy>,
|
||||
work_dir: PathBuf,
|
||||
cx: &mut AppContext,
|
||||
) -> Arc<Self> {
|
||||
@@ -328,7 +346,7 @@ impl WasmHost {
|
||||
work_dir,
|
||||
http_client,
|
||||
node_runtime,
|
||||
registration_hooks,
|
||||
proxy,
|
||||
release_channel: ReleaseChannel::global(cx),
|
||||
_main_thread_message_task: task,
|
||||
main_thread_message_tx: tx,
|
||||
|
||||
@@ -3,7 +3,7 @@ use crate::wasm_host::wit::since_v0_0_4;
|
||||
use crate::wasm_host::WasmState;
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use extension::WorktreeDelegate;
|
||||
use extension::{ExtensionLanguageServerProxy, WorktreeDelegate};
|
||||
use language::LanguageServerBinaryStatus;
|
||||
use semantic_version::SemanticVersion;
|
||||
use std::sync::{Arc, OnceLock};
|
||||
@@ -149,8 +149,9 @@ impl ExtensionImports for WasmState {
|
||||
};
|
||||
|
||||
self.host
|
||||
.registration_hooks
|
||||
.update_lsp_status(lsp::LanguageServerName(server_name.into()), status);
|
||||
.proxy
|
||||
.update_language_server_status(lsp::LanguageServerName(server_name.into()), status);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ use anyhow::{anyhow, bail, Context, Result};
|
||||
use async_compression::futures::bufread::GzipDecoder;
|
||||
use async_tar::Archive;
|
||||
use async_trait::async_trait;
|
||||
use extension::{KeyValueStoreDelegate, WorktreeDelegate};
|
||||
use extension::{ExtensionLanguageServerProxy, KeyValueStoreDelegate, WorktreeDelegate};
|
||||
use futures::{io::BufReader, FutureExt as _};
|
||||
use futures::{lock::Mutex, AsyncReadExt};
|
||||
use language::LanguageName;
|
||||
@@ -495,8 +495,9 @@ impl ExtensionImports for WasmState {
|
||||
};
|
||||
|
||||
self.host
|
||||
.registration_hooks
|
||||
.update_lsp_status(::lsp::LanguageServerName(server_name.into()), status);
|
||||
.proxy
|
||||
.update_language_server_status(::lsp::LanguageServerName(server_name.into()), status);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,9 @@ use async_compression::futures::bufread::GzipDecoder;
|
||||
use async_tar::Archive;
|
||||
use async_trait::async_trait;
|
||||
use context_servers::manager::ContextServerSettings;
|
||||
use extension::{KeyValueStoreDelegate, WorktreeDelegate};
|
||||
use extension::{
|
||||
ExtensionLanguageServerProxy, KeyValueStoreDelegate, ProjectDelegate, WorktreeDelegate,
|
||||
};
|
||||
use futures::{io::BufReader, FutureExt as _};
|
||||
use futures::{lock::Mutex, AsyncReadExt};
|
||||
use language::{language_settings::AllLanguageSettings, LanguageName, LanguageServerBinaryStatus};
|
||||
@@ -44,13 +46,10 @@ mod settings {
|
||||
}
|
||||
|
||||
pub type ExtensionWorktree = Arc<dyn WorktreeDelegate>;
|
||||
pub type ExtensionProject = Arc<dyn ProjectDelegate>;
|
||||
pub type ExtensionKeyValueStore = Arc<dyn KeyValueStoreDelegate>;
|
||||
pub type ExtensionHttpResponseStream = Arc<Mutex<::http_client::Response<AsyncBody>>>;
|
||||
|
||||
pub struct ExtensionProject {
|
||||
pub worktree_ids: Vec<u64>,
|
||||
}
|
||||
|
||||
pub fn linker() -> &'static Linker<WasmState> {
|
||||
static LINKER: OnceLock<Linker<WasmState>> = OnceLock::new();
|
||||
LINKER.get_or_init(|| super::new_linker(Extension::add_to_linker))
|
||||
@@ -273,7 +272,7 @@ impl HostProject for WasmState {
|
||||
project: Resource<ExtensionProject>,
|
||||
) -> wasmtime::Result<Vec<u64>> {
|
||||
let project = self.table.get(&project)?;
|
||||
Ok(project.worktree_ids.clone())
|
||||
Ok(project.worktree_ids())
|
||||
}
|
||||
|
||||
fn drop(&mut self, _project: Resource<Project>) -> Result<()> {
|
||||
@@ -685,8 +684,9 @@ impl ExtensionImports for WasmState {
|
||||
};
|
||||
|
||||
self.host
|
||||
.registration_hooks
|
||||
.update_lsp_status(::lsp::LanguageServerName(server_name.into()), status);
|
||||
.proxy
|
||||
.update_language_server_status(::lsp::LanguageServerName(server_name.into()), status);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -13,21 +13,15 @@ path = "src/extensions_ui.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
assistant_slash_command.workspace = true
|
||||
client.workspace = true
|
||||
collections.workspace = true
|
||||
context_servers.workspace = true
|
||||
db.workspace = true
|
||||
editor.workspace = true
|
||||
extension.workspace = true
|
||||
extension_host.workspace = true
|
||||
fs.workspace = true
|
||||
fuzzy.workspace = true
|
||||
gpui.workspace = true
|
||||
indexed_docs.workspace = true
|
||||
language.workspace = true
|
||||
log.workspace = true
|
||||
lsp.workspace = true
|
||||
num-format.workspace = true
|
||||
picker.workspace = true
|
||||
project.workspace = true
|
||||
@@ -36,14 +30,12 @@ semantic_version.workspace = true
|
||||
serde.workspace = true
|
||||
settings.workspace = true
|
||||
smallvec.workspace = true
|
||||
snippet_provider.workspace = true
|
||||
theme.workspace = true
|
||||
theme_selector.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
vim.workspace = true
|
||||
wasmtime-wasi.workspace = true
|
||||
vim_mode_setting.workspace = true
|
||||
workspace.workspace = true
|
||||
zed_actions.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
editor = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -1,212 +0,0 @@
|
||||
use std::{path::PathBuf, sync::Arc};
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use assistant_slash_command::{ExtensionSlashCommand, SlashCommandRegistry};
|
||||
use context_servers::manager::ServerCommand;
|
||||
use context_servers::ContextServerFactoryRegistry;
|
||||
use db::smol::future::FutureExt as _;
|
||||
use extension::Extension;
|
||||
use extension_host::wasm_host::ExtensionProject;
|
||||
use extension_host::{extension_lsp_adapter::ExtensionLspAdapter, wasm_host};
|
||||
use fs::Fs;
|
||||
use gpui::{AppContext, BackgroundExecutor, Model, Task};
|
||||
use indexed_docs::{ExtensionIndexedDocsProvider, IndexedDocsRegistry, ProviderId};
|
||||
use language::{LanguageRegistry, LanguageServerBinaryStatus, LoadedLanguage};
|
||||
use snippet_provider::SnippetRegistry;
|
||||
use theme::{ThemeRegistry, ThemeSettings};
|
||||
use ui::SharedString;
|
||||
use wasmtime_wasi::WasiView as _;
|
||||
|
||||
pub struct ConcreteExtensionRegistrationHooks {
|
||||
slash_command_registry: Arc<SlashCommandRegistry>,
|
||||
theme_registry: Arc<ThemeRegistry>,
|
||||
indexed_docs_registry: Arc<IndexedDocsRegistry>,
|
||||
snippet_registry: Arc<SnippetRegistry>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
context_server_factory_registry: Model<ContextServerFactoryRegistry>,
|
||||
executor: BackgroundExecutor,
|
||||
}
|
||||
|
||||
impl ConcreteExtensionRegistrationHooks {
|
||||
pub fn new(
|
||||
theme_registry: Arc<ThemeRegistry>,
|
||||
slash_command_registry: Arc<SlashCommandRegistry>,
|
||||
indexed_docs_registry: Arc<IndexedDocsRegistry>,
|
||||
snippet_registry: Arc<SnippetRegistry>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
context_server_factory_registry: Model<ContextServerFactoryRegistry>,
|
||||
cx: &AppContext,
|
||||
) -> Arc<dyn extension_host::ExtensionRegistrationHooks> {
|
||||
Arc::new(Self {
|
||||
theme_registry,
|
||||
slash_command_registry,
|
||||
indexed_docs_registry,
|
||||
snippet_registry,
|
||||
language_registry,
|
||||
context_server_factory_registry,
|
||||
executor: cx.background_executor().clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl extension_host::ExtensionRegistrationHooks for ConcreteExtensionRegistrationHooks {
|
||||
fn remove_user_themes(&self, themes: Vec<SharedString>) {
|
||||
self.theme_registry.remove_user_themes(&themes);
|
||||
}
|
||||
|
||||
fn load_user_theme(&self, theme_path: PathBuf, fs: Arc<dyn fs::Fs>) -> Task<Result<()>> {
|
||||
let theme_registry = self.theme_registry.clone();
|
||||
self.executor
|
||||
.spawn(async move { theme_registry.load_user_theme(&theme_path, fs).await })
|
||||
}
|
||||
|
||||
fn register_slash_command(
|
||||
&self,
|
||||
extension: Arc<dyn Extension>,
|
||||
command: extension::SlashCommand,
|
||||
) {
|
||||
self.slash_command_registry
|
||||
.register_command(ExtensionSlashCommand::new(extension, command), false)
|
||||
}
|
||||
|
||||
fn register_context_server(
|
||||
&self,
|
||||
id: Arc<str>,
|
||||
extension: wasm_host::WasmExtension,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
self.context_server_factory_registry
|
||||
.update(cx, |registry, _| {
|
||||
registry.register_server_factory(
|
||||
id.clone(),
|
||||
Arc::new({
|
||||
move |project, cx| {
|
||||
log::info!(
|
||||
"loading command for context server {id} from extension {}",
|
||||
extension.manifest.id
|
||||
);
|
||||
|
||||
let id = id.clone();
|
||||
let extension = extension.clone();
|
||||
cx.spawn(|mut cx| async move {
|
||||
let extension_project =
|
||||
project.update(&mut cx, |project, cx| ExtensionProject {
|
||||
worktree_ids: project
|
||||
.visible_worktrees(cx)
|
||||
.map(|worktree| worktree.read(cx).id().to_proto())
|
||||
.collect(),
|
||||
})?;
|
||||
|
||||
let command = extension
|
||||
.call({
|
||||
let id = id.clone();
|
||||
|extension, store| {
|
||||
async move {
|
||||
let project = store
|
||||
.data_mut()
|
||||
.table()
|
||||
.push(extension_project)?;
|
||||
let command = extension
|
||||
.call_context_server_command(
|
||||
store,
|
||||
id.clone(),
|
||||
project,
|
||||
)
|
||||
.await?
|
||||
.map_err(|e| anyhow!("{}", e))?;
|
||||
anyhow::Ok(command)
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
})
|
||||
.await?;
|
||||
|
||||
log::info!("loaded command for context server {id}: {command:?}");
|
||||
|
||||
Ok(ServerCommand {
|
||||
path: command.command,
|
||||
args: command.args,
|
||||
env: Some(command.env.into_iter().collect()),
|
||||
})
|
||||
})
|
||||
}
|
||||
}),
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
fn register_docs_provider(&self, extension: Arc<dyn Extension>, provider_id: Arc<str>) {
|
||||
self.indexed_docs_registry
|
||||
.register_provider(Box::new(ExtensionIndexedDocsProvider::new(
|
||||
extension,
|
||||
ProviderId(provider_id),
|
||||
)));
|
||||
}
|
||||
|
||||
fn register_snippets(&self, path: &PathBuf, snippet_contents: &str) -> Result<()> {
|
||||
self.snippet_registry
|
||||
.register_snippets(path, snippet_contents)
|
||||
}
|
||||
|
||||
fn update_lsp_status(
|
||||
&self,
|
||||
server_name: lsp::LanguageServerName,
|
||||
status: LanguageServerBinaryStatus,
|
||||
) {
|
||||
self.language_registry
|
||||
.update_lsp_status(server_name, status);
|
||||
}
|
||||
|
||||
fn register_lsp_adapter(
|
||||
&self,
|
||||
language_name: language::LanguageName,
|
||||
adapter: ExtensionLspAdapter,
|
||||
) {
|
||||
self.language_registry
|
||||
.register_lsp_adapter(language_name, Arc::new(adapter));
|
||||
}
|
||||
|
||||
fn remove_lsp_adapter(
|
||||
&self,
|
||||
language_name: &language::LanguageName,
|
||||
server_name: &lsp::LanguageServerName,
|
||||
) {
|
||||
self.language_registry
|
||||
.remove_lsp_adapter(language_name, server_name);
|
||||
}
|
||||
|
||||
fn remove_languages(
|
||||
&self,
|
||||
languages_to_remove: &[language::LanguageName],
|
||||
grammars_to_remove: &[Arc<str>],
|
||||
) {
|
||||
self.language_registry
|
||||
.remove_languages(&languages_to_remove, &grammars_to_remove);
|
||||
}
|
||||
|
||||
fn register_wasm_grammars(&self, grammars: Vec<(Arc<str>, PathBuf)>) {
|
||||
self.language_registry.register_wasm_grammars(grammars)
|
||||
}
|
||||
|
||||
fn register_language(
|
||||
&self,
|
||||
language: language::LanguageName,
|
||||
grammar: Option<Arc<str>>,
|
||||
matcher: language::LanguageMatcher,
|
||||
load: Arc<dyn Fn() -> Result<LoadedLanguage> + 'static + Send + Sync>,
|
||||
) {
|
||||
self.language_registry
|
||||
.register_language(language, grammar, matcher, load)
|
||||
}
|
||||
|
||||
fn reload_current_theme(&self, cx: &mut AppContext) {
|
||||
ThemeSettings::reload_current_theme(cx)
|
||||
}
|
||||
|
||||
fn list_theme_names(&self, path: PathBuf, fs: Arc<dyn Fs>) -> Task<Result<Vec<String>>> {
|
||||
self.executor.spawn(async move {
|
||||
let themes = theme::read_user_theme(&path, fs).await?;
|
||||
Ok(themes.themes.into_iter().map(|theme| theme.name).collect())
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,7 @@
|
||||
mod components;
|
||||
mod extension_registration_hooks;
|
||||
mod extension_suggest;
|
||||
mod extension_version_selector;
|
||||
|
||||
pub use extension_registration_hooks::ConcreteExtensionRegistrationHooks;
|
||||
|
||||
use std::ops::DerefMut;
|
||||
use std::sync::OnceLock;
|
||||
use std::time::Duration;
|
||||
@@ -17,9 +14,9 @@ use editor::{Editor, EditorElement, EditorStyle};
|
||||
use extension_host::{ExtensionManifest, ExtensionOperation, ExtensionStore};
|
||||
use fuzzy::{match_strings, StringMatchCandidate};
|
||||
use gpui::{
|
||||
actions, uniform_list, AppContext, EventEmitter, Flatten, FocusableView, InteractiveElement,
|
||||
KeyContext, ParentElement, Render, Styled, Task, TextStyle, UniformListScrollHandle, View,
|
||||
ViewContext, VisualContext, WeakView, WindowContext,
|
||||
actions, uniform_list, Action, AppContext, EventEmitter, Flatten, FocusableView,
|
||||
InteractiveElement, KeyContext, ParentElement, Render, Styled, Task, TextStyle,
|
||||
UniformListScrollHandle, View, ViewContext, VisualContext, WeakView, WindowContext,
|
||||
};
|
||||
use num_format::{Locale, ToFormattedString};
|
||||
use project::DirectoryLister;
|
||||
@@ -27,7 +24,7 @@ use release_channel::ReleaseChannel;
|
||||
use settings::Settings;
|
||||
use theme::ThemeSettings;
|
||||
use ui::{prelude::*, CheckboxWithLabel, ContextMenu, PopoverMenu, ToggleButton, Tooltip};
|
||||
use vim::VimModeSetting;
|
||||
use vim_mode_setting::VimModeSetting;
|
||||
use workspace::{
|
||||
item::{Item, ItemEvent},
|
||||
Workspace, WorkspaceId,
|
||||
@@ -38,12 +35,12 @@ use crate::extension_version_selector::{
|
||||
ExtensionVersionSelector, ExtensionVersionSelectorDelegate,
|
||||
};
|
||||
|
||||
actions!(zed, [Extensions, InstallDevExtension]);
|
||||
actions!(zed, [InstallDevExtension]);
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.observe_new_views(move |workspace: &mut Workspace, cx| {
|
||||
workspace
|
||||
.register_action(move |workspace, _: &Extensions, cx| {
|
||||
.register_action(move |workspace, _: &zed_actions::Extensions, cx| {
|
||||
let existing = workspace
|
||||
.active_pane()
|
||||
.read(cx)
|
||||
@@ -254,14 +251,13 @@ impl ExtensionsPage {
|
||||
.collect::<Vec<_>>();
|
||||
if !themes.is_empty() {
|
||||
workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
theme_selector::toggle(
|
||||
workspace,
|
||||
&theme_selector::Toggle {
|
||||
.update(cx, |_workspace, cx| {
|
||||
cx.dispatch_action(
|
||||
zed_actions::theme_selector::Toggle {
|
||||
themes_filter: Some(themes),
|
||||
},
|
||||
cx,
|
||||
)
|
||||
}
|
||||
.boxed_clone(),
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
@@ -22,8 +22,8 @@ db.workspace = true
|
||||
editor.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
human_bytes = "0.4.1"
|
||||
http_client.workspace = true
|
||||
human_bytes = "0.4.1"
|
||||
language.workspace = true
|
||||
log.workspace = true
|
||||
menu.workspace = true
|
||||
@@ -39,6 +39,7 @@ ui.workspace = true
|
||||
urlencoding = "2.1.2"
|
||||
util.workspace = true
|
||||
workspace.workspace = true
|
||||
zed_actions.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
editor = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -5,8 +5,6 @@ use workspace::Workspace;
|
||||
|
||||
pub mod feedback_modal;
|
||||
|
||||
actions!(feedback, [GiveFeedback, SubmitFeedback]);
|
||||
|
||||
mod system_specs;
|
||||
|
||||
actions!(
|
||||
|
||||
@@ -18,8 +18,9 @@ use serde_derive::Serialize;
|
||||
use ui::{prelude::*, Button, ButtonStyle, IconPosition, Tooltip};
|
||||
use util::ResultExt;
|
||||
use workspace::{DismissDecision, ModalView, Workspace};
|
||||
use zed_actions::feedback::GiveFeedback;
|
||||
|
||||
use crate::{system_specs::SystemSpecs, GiveFeedback, OpenZedRepo};
|
||||
use crate::{system_specs::SystemSpecs, OpenZedRepo};
|
||||
|
||||
// For UI testing purposes
|
||||
const SEND_SUCCESS_IN_DEV_MODE: bool = true;
|
||||
|
||||
@@ -10,7 +10,7 @@ pub use open_path_prompt::OpenPathDelegate;
|
||||
|
||||
use collections::HashMap;
|
||||
use editor::{scroll::Autoscroll, Bias, Editor};
|
||||
use file_finder_settings::FileFinderSettings;
|
||||
use file_finder_settings::{FileFinderSettings, FileFinderWidth};
|
||||
use file_icons::FileIcons;
|
||||
use fuzzy::{CharBag, PathMatch, PathMatchCandidate};
|
||||
use gpui::{
|
||||
@@ -244,6 +244,22 @@ impl FileFinder {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn modal_max_width(
|
||||
width_setting: Option<FileFinderWidth>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Pixels {
|
||||
let window_width = cx.viewport_size().width;
|
||||
let small_width = Pixels(545.);
|
||||
|
||||
match width_setting {
|
||||
None | Some(FileFinderWidth::Small) => small_width,
|
||||
Some(FileFinderWidth::Full) => window_width,
|
||||
Some(FileFinderWidth::XLarge) => (window_width - Pixels(512.)).max(small_width),
|
||||
Some(FileFinderWidth::Large) => (window_width - Pixels(768.)).max(small_width),
|
||||
Some(FileFinderWidth::Medium) => (window_width - Pixels(1024.)).max(small_width),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<DismissEvent> for FileFinder {}
|
||||
@@ -258,13 +274,12 @@ impl Render for FileFinder {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let key_context = self.picker.read(cx).delegate.key_context(cx);
|
||||
|
||||
let window_max_width: Pixels = cx.viewport_size().width;
|
||||
let modal_choice = FileFinderSettings::get_global(cx).modal_width;
|
||||
let width = modal_choice.calc_width(window_max_width);
|
||||
let file_finder_settings = FileFinderSettings::get_global(cx);
|
||||
let modal_max_width = Self::modal_max_width(file_finder_settings.modal_max_width, cx);
|
||||
|
||||
v_flex()
|
||||
.key_context(key_context)
|
||||
.w(width)
|
||||
.w(modal_max_width)
|
||||
.on_modifiers_changed(cx.listener(Self::handle_modifiers_changed))
|
||||
.on_action(cx.listener(Self::handle_select_prev))
|
||||
.on_action(cx.listener(Self::handle_open_menu))
|
||||
|
||||
@@ -2,13 +2,11 @@ use anyhow::Result;
|
||||
use schemars::JsonSchema;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsSources};
|
||||
use std::cmp;
|
||||
use ui::Pixels;
|
||||
|
||||
#[derive(Deserialize, Debug, Clone, Copy, PartialEq)]
|
||||
pub struct FileFinderSettings {
|
||||
pub file_icons: bool,
|
||||
pub modal_width: FileFinderWidth,
|
||||
pub modal_max_width: Option<FileFinderWidth>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
@@ -17,10 +15,10 @@ pub struct FileFinderSettingsContent {
|
||||
///
|
||||
/// Default: true
|
||||
pub file_icons: Option<bool>,
|
||||
/// The width of the file finder modal.
|
||||
/// Determines how much space the file finder can take up in relation to the available window width.
|
||||
///
|
||||
/// Default: "medium"
|
||||
pub modal_width: Option<FileFinderWidth>,
|
||||
/// Default: small
|
||||
pub modal_max_width: Option<FileFinderWidth>,
|
||||
}
|
||||
|
||||
impl Settings for FileFinderSettings {
|
||||
@@ -36,40 +34,10 @@ impl Settings for FileFinderSettings {
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy, Default, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum FileFinderWidth {
|
||||
Small,
|
||||
#[default]
|
||||
Small,
|
||||
Medium,
|
||||
Large,
|
||||
XLarge,
|
||||
Full,
|
||||
}
|
||||
|
||||
impl FileFinderWidth {
|
||||
const MIN_MODAL_WIDTH_PX: f32 = 384.;
|
||||
|
||||
pub fn padding_px(&self) -> Pixels {
|
||||
let padding_val = match self {
|
||||
FileFinderWidth::Small => 1280.,
|
||||
FileFinderWidth::Medium => 1024.,
|
||||
FileFinderWidth::Large => 768.,
|
||||
FileFinderWidth::XLarge => 512.,
|
||||
FileFinderWidth::Full => 0.,
|
||||
};
|
||||
|
||||
Pixels(padding_val)
|
||||
}
|
||||
|
||||
pub fn calc_width(&self, window_width: Pixels) -> Pixels {
|
||||
if self == &FileFinderWidth::Full {
|
||||
return window_width;
|
||||
}
|
||||
|
||||
let min_modal_width_px = Pixels(FileFinderWidth::MIN_MODAL_WIDTH_PX);
|
||||
|
||||
let padding_px = self.padding_px();
|
||||
let width_val = window_width - padding_px;
|
||||
let finder_width = cmp::max(min_modal_width_px, width_val);
|
||||
|
||||
finder_width
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,8 +71,16 @@ impl Match {
|
||||
fn project_path(&self, project: &Project, cx: &WindowContext) -> Option<ProjectPath> {
|
||||
let worktree_id = if let Some(path_match) = &self.path_match {
|
||||
WorktreeId::from_usize(path_match.worktree_id)
|
||||
} else if let Some(worktree) = project.visible_worktrees(cx).find(|worktree| {
|
||||
worktree
|
||||
.read(cx)
|
||||
.root_entry()
|
||||
.is_some_and(|entry| entry.is_dir())
|
||||
}) {
|
||||
worktree.read(cx).id()
|
||||
} else {
|
||||
project.worktrees(cx).next()?.read(cx).id()
|
||||
// todo(): we should find_or_create a workspace.
|
||||
return None;
|
||||
};
|
||||
|
||||
let path = PathBuf::from(self.relative_path());
|
||||
|
||||
@@ -24,6 +24,7 @@ libc.workspace = true
|
||||
parking_lot.workspace = true
|
||||
paths.workspace = true
|
||||
rope.workspace = true
|
||||
proto.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
smol.workspace = true
|
||||
|
||||
@@ -27,13 +27,14 @@ use futures::{future::BoxFuture, AsyncRead, Stream, StreamExt};
|
||||
use git::repository::{GitRepository, RealGitRepository};
|
||||
use gpui::{AppContext, Global, ReadGlobal};
|
||||
use rope::Rope;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use smol::io::AsyncWriteExt;
|
||||
use std::{
|
||||
io::{self, Write},
|
||||
path::{Component, Path, PathBuf},
|
||||
pin::Pin,
|
||||
sync::Arc,
|
||||
time::{Duration, SystemTime},
|
||||
time::{Duration, SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
use tempfile::{NamedTempFile, TempDir};
|
||||
use text::LineEnding;
|
||||
@@ -179,13 +180,62 @@ pub struct RemoveOptions {
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub struct Metadata {
|
||||
pub inode: u64,
|
||||
pub mtime: SystemTime,
|
||||
pub mtime: MTime,
|
||||
pub is_symlink: bool,
|
||||
pub is_dir: bool,
|
||||
pub len: u64,
|
||||
pub is_fifo: bool,
|
||||
}
|
||||
|
||||
/// Filesystem modification time. The purpose of this newtype is to discourage use of operations
|
||||
/// that do not make sense for mtimes. In particular, it is not always valid to compare mtimes using
|
||||
/// `<` or `>`, as there are many things that can cause the mtime of a file to be earlier than it
|
||||
/// was. See ["mtime comparison considered harmful" - apenwarr](https://apenwarr.ca/log/20181113).
|
||||
///
|
||||
/// Do not derive Ord, PartialOrd, or arithmetic operation traits.
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct MTime(SystemTime);
|
||||
|
||||
impl MTime {
|
||||
/// Conversion intended for persistence and testing.
|
||||
pub fn from_seconds_and_nanos(secs: u64, nanos: u32) -> Self {
|
||||
MTime(UNIX_EPOCH + Duration::new(secs, nanos))
|
||||
}
|
||||
|
||||
/// Conversion intended for persistence.
|
||||
pub fn to_seconds_and_nanos_for_persistence(self) -> Option<(u64, u32)> {
|
||||
self.0
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.ok()
|
||||
.map(|duration| (duration.as_secs(), duration.subsec_nanos()))
|
||||
}
|
||||
|
||||
/// Returns the value wrapped by this `MTime`, for presentation to the user. The name including
|
||||
/// "_for_user" is to discourage misuse - this method should not be used when making decisions
|
||||
/// about file dirtiness.
|
||||
pub fn timestamp_for_user(self) -> SystemTime {
|
||||
self.0
|
||||
}
|
||||
|
||||
/// Temporary method to split out the behavior changes from introduction of this newtype.
|
||||
pub fn bad_is_greater_than(self, other: MTime) -> bool {
|
||||
self.0 > other.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<proto::Timestamp> for MTime {
|
||||
fn from(timestamp: proto::Timestamp) -> Self {
|
||||
MTime(timestamp.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MTime> for proto::Timestamp {
|
||||
fn from(mtime: MTime) -> Self {
|
||||
mtime.0.into()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct RealFs {
|
||||
git_hosting_provider_registry: Arc<GitHostingProviderRegistry>,
|
||||
@@ -558,7 +608,7 @@ impl Fs for RealFs {
|
||||
|
||||
Ok(Some(Metadata {
|
||||
inode,
|
||||
mtime: metadata.modified().unwrap(),
|
||||
mtime: MTime(metadata.modified().unwrap()),
|
||||
len: metadata.len(),
|
||||
is_symlink,
|
||||
is_dir: metadata.file_type().is_dir(),
|
||||
@@ -818,13 +868,13 @@ struct FakeFsState {
|
||||
enum FakeFsEntry {
|
||||
File {
|
||||
inode: u64,
|
||||
mtime: SystemTime,
|
||||
mtime: MTime,
|
||||
len: u64,
|
||||
content: Vec<u8>,
|
||||
},
|
||||
Dir {
|
||||
inode: u64,
|
||||
mtime: SystemTime,
|
||||
mtime: MTime,
|
||||
len: u64,
|
||||
entries: BTreeMap<String, Arc<Mutex<FakeFsEntry>>>,
|
||||
git_repo_state: Option<Arc<Mutex<git::repository::FakeGitRepositoryState>>>,
|
||||
@@ -836,6 +886,18 @@ enum FakeFsEntry {
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
impl FakeFsState {
|
||||
fn get_and_increment_mtime(&mut self) -> MTime {
|
||||
let mtime = self.next_mtime;
|
||||
self.next_mtime += FakeFs::SYSTEMTIME_INTERVAL;
|
||||
MTime(mtime)
|
||||
}
|
||||
|
||||
fn get_and_increment_inode(&mut self) -> u64 {
|
||||
let inode = self.next_inode;
|
||||
self.next_inode += 1;
|
||||
inode
|
||||
}
|
||||
|
||||
fn read_path(&self, target: &Path) -> Result<Arc<Mutex<FakeFsEntry>>> {
|
||||
Ok(self
|
||||
.try_read_path(target, true)
|
||||
@@ -959,7 +1021,7 @@ pub static FS_DOT_GIT: std::sync::LazyLock<&'static OsStr> =
|
||||
impl FakeFs {
|
||||
/// We need to use something large enough for Windows and Unix to consider this a new file.
|
||||
/// https://doc.rust-lang.org/nightly/std/time/struct.SystemTime.html#platform-specific-behavior
|
||||
const SYSTEMTIME_INTERVAL: u64 = 100;
|
||||
const SYSTEMTIME_INTERVAL: Duration = Duration::from_nanos(100);
|
||||
|
||||
pub fn new(executor: gpui::BackgroundExecutor) -> Arc<Self> {
|
||||
let (tx, mut rx) = smol::channel::bounded::<PathBuf>(10);
|
||||
@@ -969,13 +1031,13 @@ impl FakeFs {
|
||||
state: Mutex::new(FakeFsState {
|
||||
root: Arc::new(Mutex::new(FakeFsEntry::Dir {
|
||||
inode: 0,
|
||||
mtime: SystemTime::UNIX_EPOCH,
|
||||
mtime: MTime(UNIX_EPOCH),
|
||||
len: 0,
|
||||
entries: Default::default(),
|
||||
git_repo_state: None,
|
||||
})),
|
||||
git_event_tx: tx,
|
||||
next_mtime: SystemTime::UNIX_EPOCH,
|
||||
next_mtime: UNIX_EPOCH + Self::SYSTEMTIME_INTERVAL,
|
||||
next_inode: 1,
|
||||
event_txs: Default::default(),
|
||||
buffered_events: Vec::new(),
|
||||
@@ -1007,13 +1069,16 @@ impl FakeFs {
|
||||
state.next_mtime = next_mtime;
|
||||
}
|
||||
|
||||
pub fn get_and_increment_mtime(&self) -> MTime {
|
||||
let mut state = self.state.lock();
|
||||
state.get_and_increment_mtime()
|
||||
}
|
||||
|
||||
pub async fn touch_path(&self, path: impl AsRef<Path>) {
|
||||
let mut state = self.state.lock();
|
||||
let path = path.as_ref();
|
||||
let new_mtime = state.next_mtime;
|
||||
let new_inode = state.next_inode;
|
||||
state.next_inode += 1;
|
||||
state.next_mtime += Duration::from_nanos(Self::SYSTEMTIME_INTERVAL);
|
||||
let new_mtime = state.get_and_increment_mtime();
|
||||
let new_inode = state.get_and_increment_inode();
|
||||
state
|
||||
.write_path(path, move |entry| {
|
||||
match entry {
|
||||
@@ -1062,19 +1127,14 @@ impl FakeFs {
|
||||
|
||||
fn write_file_internal(&self, path: impl AsRef<Path>, content: Vec<u8>) -> Result<()> {
|
||||
let mut state = self.state.lock();
|
||||
let path = path.as_ref();
|
||||
let inode = state.next_inode;
|
||||
let mtime = state.next_mtime;
|
||||
state.next_inode += 1;
|
||||
state.next_mtime += Duration::from_nanos(Self::SYSTEMTIME_INTERVAL);
|
||||
let file = Arc::new(Mutex::new(FakeFsEntry::File {
|
||||
inode,
|
||||
mtime,
|
||||
inode: state.get_and_increment_inode(),
|
||||
mtime: state.get_and_increment_mtime(),
|
||||
len: content.len() as u64,
|
||||
content,
|
||||
}));
|
||||
let mut kind = None;
|
||||
state.write_path(path, {
|
||||
state.write_path(path.as_ref(), {
|
||||
let kind = &mut kind;
|
||||
move |entry| {
|
||||
match entry {
|
||||
@@ -1090,7 +1150,7 @@ impl FakeFs {
|
||||
Ok(())
|
||||
}
|
||||
})?;
|
||||
state.emit_event([(path, kind)]);
|
||||
state.emit_event([(path.as_ref(), kind)]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1383,16 +1443,6 @@ impl FakeFsEntry {
|
||||
}
|
||||
}
|
||||
|
||||
fn set_file_content(&mut self, path: &Path, new_content: Vec<u8>) -> Result<()> {
|
||||
if let Self::File { content, mtime, .. } = self {
|
||||
*mtime = SystemTime::now();
|
||||
*content = new_content;
|
||||
Ok(())
|
||||
} else {
|
||||
Err(anyhow!("not a file: {}", path.display()))
|
||||
}
|
||||
}
|
||||
|
||||
fn dir_entries(
|
||||
&mut self,
|
||||
path: &Path,
|
||||
@@ -1456,10 +1506,8 @@ impl Fs for FakeFs {
|
||||
}
|
||||
let mut state = self.state.lock();
|
||||
|
||||
let inode = state.next_inode;
|
||||
let mtime = state.next_mtime;
|
||||
state.next_mtime += Duration::from_nanos(Self::SYSTEMTIME_INTERVAL);
|
||||
state.next_inode += 1;
|
||||
let inode = state.get_and_increment_inode();
|
||||
let mtime = state.get_and_increment_mtime();
|
||||
state.write_path(&cur_path, |entry| {
|
||||
entry.or_insert_with(|| {
|
||||
created_dirs.push((cur_path.clone(), Some(PathEventKind::Created)));
|
||||
@@ -1482,10 +1530,8 @@ impl Fs for FakeFs {
|
||||
async fn create_file(&self, path: &Path, options: CreateOptions) -> Result<()> {
|
||||
self.simulate_random_delay().await;
|
||||
let mut state = self.state.lock();
|
||||
let inode = state.next_inode;
|
||||
let mtime = state.next_mtime;
|
||||
state.next_mtime += Duration::from_nanos(Self::SYSTEMTIME_INTERVAL);
|
||||
state.next_inode += 1;
|
||||
let inode = state.get_and_increment_inode();
|
||||
let mtime = state.get_and_increment_mtime();
|
||||
let file = Arc::new(Mutex::new(FakeFsEntry::File {
|
||||
inode,
|
||||
mtime,
|
||||
@@ -1625,13 +1671,12 @@ impl Fs for FakeFs {
|
||||
let source = normalize_path(source);
|
||||
let target = normalize_path(target);
|
||||
let mut state = self.state.lock();
|
||||
let mtime = state.next_mtime;
|
||||
let inode = util::post_inc(&mut state.next_inode);
|
||||
state.next_mtime += Duration::from_nanos(Self::SYSTEMTIME_INTERVAL);
|
||||
let mtime = state.get_and_increment_mtime();
|
||||
let inode = state.get_and_increment_inode();
|
||||
let source_entry = state.read_path(&source)?;
|
||||
let content = source_entry.lock().file_content(&source)?.clone();
|
||||
let mut kind = Some(PathEventKind::Created);
|
||||
let entry = state.write_path(&target, |e| match e {
|
||||
state.write_path(&target, |e| match e {
|
||||
btree_map::Entry::Occupied(e) => {
|
||||
if options.overwrite {
|
||||
kind = Some(PathEventKind::Changed);
|
||||
@@ -1647,14 +1692,11 @@ impl Fs for FakeFs {
|
||||
inode,
|
||||
mtime,
|
||||
len: content.len() as u64,
|
||||
content: Vec::new(),
|
||||
content,
|
||||
})))
|
||||
.clone(),
|
||||
)),
|
||||
})?;
|
||||
if let Some(entry) = entry {
|
||||
entry.lock().set_file_content(&target, content)?;
|
||||
}
|
||||
state.emit_event([(target, kind)]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -31,10 +31,6 @@ time.workspace = true
|
||||
url.workspace = true
|
||||
util.workspace = true
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
windows.workspace = true
|
||||
|
||||
|
||||
[dev-dependencies]
|
||||
unindent.workspace = true
|
||||
serde_json.workspace = true
|
||||
|
||||
@@ -4,7 +4,7 @@ use anyhow::{anyhow, Context, Result};
|
||||
use collections::{HashMap, HashSet};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::io::Write;
|
||||
use std::process::{Command, Stdio};
|
||||
use std::process::Stdio;
|
||||
use std::sync::Arc;
|
||||
use std::{ops::Range, path::Path};
|
||||
use text::Rope;
|
||||
@@ -80,9 +80,7 @@ fn run_git_blame(
|
||||
path: &Path,
|
||||
contents: &Rope,
|
||||
) -> Result<String> {
|
||||
let mut child = Command::new(git_binary);
|
||||
|
||||
child
|
||||
let child = util::command::new_std_command(git_binary)
|
||||
.current_dir(working_directory)
|
||||
.arg("blame")
|
||||
.arg("--incremental")
|
||||
@@ -91,15 +89,7 @@ fn run_git_blame(
|
||||
.arg(path.as_os_str())
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
use std::os::windows::process::CommandExt;
|
||||
child.creation_flags(windows::Win32::System::Threading::CREATE_NO_WINDOW.0);
|
||||
}
|
||||
|
||||
let child = child
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.map_err(|e| anyhow!("Failed to start git blame process: {}", e))?;
|
||||
|
||||
|
||||
@@ -2,10 +2,6 @@ use crate::Oid;
|
||||
use anyhow::{anyhow, Result};
|
||||
use collections::HashMap;
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
|
||||
#[cfg(windows)]
|
||||
use std::os::windows::process::CommandExt;
|
||||
|
||||
pub fn get_messages(working_directory: &Path, shas: &[Oid]) -> Result<HashMap<Oid, String>> {
|
||||
if shas.is_empty() {
|
||||
@@ -14,19 +10,12 @@ pub fn get_messages(working_directory: &Path, shas: &[Oid]) -> Result<HashMap<Oi
|
||||
|
||||
const MARKER: &str = "<MARKER>";
|
||||
|
||||
let mut command = Command::new("git");
|
||||
|
||||
command
|
||||
let output = util::command::new_std_command("git")
|
||||
.current_dir(working_directory)
|
||||
.arg("show")
|
||||
.arg("-s")
|
||||
.arg(format!("--format=%B{}", MARKER))
|
||||
.args(shas.iter().map(ToString::to_string));
|
||||
|
||||
#[cfg(windows)]
|
||||
command.creation_flags(windows::Win32::System::Threading::CREATE_NO_WINDOW.0);
|
||||
|
||||
let output = command
|
||||
.args(shas.iter().map(ToString::to_string))
|
||||
.output()
|
||||
.map_err(|e| anyhow!("Failed to start git blame process: {}", e))?;
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ use crate::repository::{GitFileStatus, RepoPath};
|
||||
use anyhow::{anyhow, Result};
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
process::{Command, Stdio},
|
||||
process::Stdio,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
@@ -17,9 +17,7 @@ impl GitStatus {
|
||||
working_directory: &Path,
|
||||
path_prefixes: &[PathBuf],
|
||||
) -> Result<Self> {
|
||||
let mut child = Command::new(git_binary);
|
||||
|
||||
child
|
||||
let child = util::command::new_std_command(git_binary)
|
||||
.current_dir(working_directory)
|
||||
.args([
|
||||
"--no-optional-locks",
|
||||
@@ -37,15 +35,7 @@ impl GitStatus {
|
||||
}))
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
use std::os::windows::process::CommandExt;
|
||||
child.creation_flags(windows::Win32::System::Threading::CREATE_NO_WINDOW.0);
|
||||
}
|
||||
|
||||
let child = child
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.map_err(|e| anyhow!("Failed to start git status process: {}", e))?;
|
||||
|
||||
|
||||
@@ -61,4 +61,4 @@ In addition to the systems above, GPUI provides a range of smaller services that
|
||||
|
||||
- The `[gpui::test]` macro provides a convenient way to write tests for your GPUI applications. Tests also have their own kind of context, a `TestAppContext` which provides ways of simulating common platform input. See `app::test_context` and `test` modules for more details.
|
||||
|
||||
Currently, the best way to learn about these APIs is to read the Zed source code, ask us about it at a fireside hack, or drop a question in the [Zed Discord](https://discord.gg/zed-community). We're working on improving the documentation, creating more examples, and will be publishing more guides to GPUI on our [blog](https://zed.dev/blog).
|
||||
Currently, the best way to learn about these APIs is to read the Zed source code, ask us about it at a fireside hack, or drop a question in the [Zed Discord](https://zed.dev/community-links). We're working on improving the documentation, creating more examples, and will be publishing more guides to GPUI on our [blog](https://zed.dev/blog).
|
||||
|
||||
@@ -15,7 +15,10 @@ actions!(
|
||||
SelectAll,
|
||||
Home,
|
||||
End,
|
||||
ShowCharacterPalette
|
||||
ShowCharacterPalette,
|
||||
Paste,
|
||||
Cut,
|
||||
Copy,
|
||||
]
|
||||
);
|
||||
|
||||
@@ -107,6 +110,28 @@ impl TextInput {
|
||||
cx.show_character_palette();
|
||||
}
|
||||
|
||||
fn paste(&mut self, _: &Paste, cx: &mut ViewContext<Self>) {
|
||||
if let Some(text) = cx.read_from_clipboard().and_then(|item| item.text()) {
|
||||
self.replace_text_in_range(None, &text.replace("\n", " "), cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn copy(&mut self, _: &Copy, cx: &mut ViewContext<Self>) {
|
||||
if !self.selected_range.is_empty() {
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(
|
||||
(&self.content[self.selected_range.clone()]).to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
fn cut(&mut self, _: &Copy, cx: &mut ViewContext<Self>) {
|
||||
if !self.selected_range.is_empty() {
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(
|
||||
(&self.content[self.selected_range.clone()]).to_string(),
|
||||
));
|
||||
self.replace_text_in_range(None, "", cx)
|
||||
}
|
||||
}
|
||||
|
||||
fn move_to(&mut self, offset: usize, cx: &mut ViewContext<Self>) {
|
||||
self.selected_range = offset..offset;
|
||||
cx.notify()
|
||||
@@ -219,9 +244,11 @@ impl ViewInputHandler for TextInput {
|
||||
fn text_for_range(
|
||||
&mut self,
|
||||
range_utf16: Range<usize>,
|
||||
actual_range: &mut Option<Range<usize>>,
|
||||
_cx: &mut ViewContext<Self>,
|
||||
) -> Option<String> {
|
||||
let range = self.range_from_utf16(&range_utf16);
|
||||
actual_range.replace(self.range_to_utf16(&range));
|
||||
Some(self.content[range].to_string())
|
||||
}
|
||||
|
||||
@@ -497,6 +524,9 @@ impl Render for TextInput {
|
||||
.on_action(cx.listener(Self::home))
|
||||
.on_action(cx.listener(Self::end))
|
||||
.on_action(cx.listener(Self::show_character_palette))
|
||||
.on_action(cx.listener(Self::paste))
|
||||
.on_action(cx.listener(Self::cut))
|
||||
.on_action(cx.listener(Self::copy))
|
||||
.on_mouse_down(MouseButton::Left, cx.listener(Self::on_mouse_down))
|
||||
.on_mouse_up(MouseButton::Left, cx.listener(Self::on_mouse_up))
|
||||
.on_mouse_up_out(MouseButton::Left, cx.listener(Self::on_mouse_up))
|
||||
@@ -581,8 +611,8 @@ impl Render for InputExample {
|
||||
format!(
|
||||
"{:} {}",
|
||||
ks.unparse(),
|
||||
if let Some(ime_key) = ks.ime_key.as_ref() {
|
||||
format!("-> {:?}", ime_key)
|
||||
if let Some(key_char) = ks.key_char.as_ref() {
|
||||
format!("-> {:?}", key_char)
|
||||
} else {
|
||||
"".to_owned()
|
||||
}
|
||||
@@ -602,6 +632,9 @@ fn main() {
|
||||
KeyBinding::new("shift-left", SelectLeft, None),
|
||||
KeyBinding::new("shift-right", SelectRight, None),
|
||||
KeyBinding::new("cmd-a", SelectAll, None),
|
||||
KeyBinding::new("cmd-v", Paste, None),
|
||||
KeyBinding::new("cmd-c", Copy, None),
|
||||
KeyBinding::new("cmd-x", Cut, None),
|
||||
KeyBinding::new("home", Home, None),
|
||||
KeyBinding::new("end", End, None),
|
||||
KeyBinding::new("ctrl-cmd-space", ShowCharacterPalette, None),
|
||||
|
||||
@@ -263,7 +263,7 @@ impl TextLayout {
|
||||
.line_height
|
||||
.to_pixels(font_size.into(), cx.rem_size());
|
||||
|
||||
let runs = if let Some(runs) = runs {
|
||||
let mut runs = if let Some(runs) = runs {
|
||||
runs
|
||||
} else {
|
||||
vec![text_style.to_run(text.len())]
|
||||
@@ -306,7 +306,7 @@ impl TextLayout {
|
||||
|
||||
let mut line_wrapper = cx.text_system().line_wrapper(text_style.font(), font_size);
|
||||
let text = if let Some(truncate_width) = truncate_width {
|
||||
line_wrapper.truncate_line(text.clone(), truncate_width, ellipsis)
|
||||
line_wrapper.truncate_line(text.clone(), truncate_width, ellipsis, &mut runs)
|
||||
} else {
|
||||
text.clone()
|
||||
};
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
//! and [`test`] modules for more details.
|
||||
//!
|
||||
//! Currently, the best way to learn about these APIs is to read the Zed source code, ask us about it at a fireside hack, or drop
|
||||
//! a question in the [Zed Discord](https://discord.gg/zed-community). We're working on improving the documentation, creating more examples,
|
||||
//! a question in the [Zed Discord](https://zed.dev/community-links). We're working on improving the documentation, creating more examples,
|
||||
//! and will be publishing more guides to GPUI on our [blog](https://zed.dev/blog).
|
||||
|
||||
#![deny(missing_docs)]
|
||||
|
||||
@@ -9,8 +9,12 @@ use std::ops::Range;
|
||||
/// See [`InputHandler`] for details on how to implement each method.
|
||||
pub trait ViewInputHandler: 'static + Sized {
|
||||
/// See [`InputHandler::text_for_range`] for details
|
||||
fn text_for_range(&mut self, range: Range<usize>, cx: &mut ViewContext<Self>)
|
||||
-> Option<String>;
|
||||
fn text_for_range(
|
||||
&mut self,
|
||||
range: Range<usize>,
|
||||
adjusted_range: &mut Option<Range<usize>>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<String>;
|
||||
|
||||
/// See [`InputHandler::selected_text_range`] for details
|
||||
fn selected_text_range(
|
||||
@@ -89,10 +93,12 @@ impl<V: ViewInputHandler> InputHandler for ElementInputHandler<V> {
|
||||
fn text_for_range(
|
||||
&mut self,
|
||||
range_utf16: Range<usize>,
|
||||
adjusted_range: &mut Option<Range<usize>>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Option<String> {
|
||||
self.view
|
||||
.update(cx, |view, cx| view.text_for_range(range_utf16, cx))
|
||||
self.view.update(cx, |view, cx| {
|
||||
view.text_for_range(range_utf16, adjusted_range, cx)
|
||||
})
|
||||
}
|
||||
|
||||
fn replace_text_in_range(
|
||||
|
||||
@@ -46,6 +46,7 @@ use smallvec::SmallVec;
|
||||
use std::borrow::Cow;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::io::Cursor;
|
||||
use std::ops;
|
||||
use std::time::{Duration, Instant};
|
||||
use std::{
|
||||
fmt::{self, Debug},
|
||||
@@ -561,6 +562,42 @@ pub(crate) trait PlatformAtlas: Send + Sync {
|
||||
key: &AtlasKey,
|
||||
build: &mut dyn FnMut() -> Result<Option<(Size<DevicePixels>, Cow<'a, [u8]>)>>,
|
||||
) -> Result<Option<AtlasTile>>;
|
||||
fn remove(&self, key: &AtlasKey);
|
||||
}
|
||||
|
||||
struct AtlasTextureList<T> {
|
||||
textures: Vec<Option<T>>,
|
||||
free_list: Vec<usize>,
|
||||
}
|
||||
|
||||
impl<T> Default for AtlasTextureList<T> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
textures: Vec::default(),
|
||||
free_list: Vec::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> ops::Index<usize> for AtlasTextureList<T> {
|
||||
type Output = Option<T>;
|
||||
|
||||
fn index(&self, index: usize) -> &Self::Output {
|
||||
&self.textures[index]
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> AtlasTextureList<T> {
|
||||
#[allow(unused)]
|
||||
fn drain(&mut self) -> std::vec::Drain<Option<T>> {
|
||||
self.free_list.clear();
|
||||
self.textures.drain(..)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn iter_mut(&mut self) -> impl DoubleEndedIterator<Item = &mut T> {
|
||||
self.textures.iter_mut().flatten()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
@@ -643,9 +680,13 @@ impl PlatformInputHandler {
|
||||
}
|
||||
|
||||
#[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
|
||||
fn text_for_range(&mut self, range_utf16: Range<usize>) -> Option<String> {
|
||||
fn text_for_range(
|
||||
&mut self,
|
||||
range_utf16: Range<usize>,
|
||||
adjusted: &mut Option<Range<usize>>,
|
||||
) -> Option<String> {
|
||||
self.cx
|
||||
.update(|cx| self.handler.text_for_range(range_utf16, cx))
|
||||
.update(|cx| self.handler.text_for_range(range_utf16, adjusted, cx))
|
||||
.ok()
|
||||
.flatten()
|
||||
}
|
||||
@@ -712,6 +753,7 @@ impl PlatformInputHandler {
|
||||
|
||||
/// A struct representing a selection in a text buffer, in UTF16 characters.
|
||||
/// This is different from a range because the head may be before the tail.
|
||||
#[derive(Debug)]
|
||||
pub struct UTF16Selection {
|
||||
/// The range of text in the document this selection corresponds to
|
||||
/// in UTF16 characters.
|
||||
@@ -749,6 +791,7 @@ pub trait InputHandler: 'static {
|
||||
fn text_for_range(
|
||||
&mut self,
|
||||
range_utf16: Range<usize>,
|
||||
adjusted_range: &mut Option<Range<usize>>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Option<String>;
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user