Compare commits
17 Commits
collab-v0.
...
v0.999999.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b1d732e06c | ||
|
|
3ecbcf0cff | ||
|
|
2f7fad8480 | ||
|
|
d66e4901ea | ||
|
|
1375c6b9e5 | ||
|
|
4319745c52 | ||
|
|
69d2b36788 | ||
|
|
eebc785e4c | ||
|
|
d109220e0d | ||
|
|
a88a2e601d | ||
|
|
7cefb16086 | ||
|
|
c2c30f6615 | ||
|
|
bf9a1091e3 | ||
|
|
e1f1a706a6 | ||
|
|
75ebaabc2c | ||
|
|
d608331f08 | ||
|
|
448c472ee6 |
7
.github/workflows/ci.yml
vendored
7
.github/workflows/ci.yml
vendored
@@ -45,11 +45,8 @@ jobs:
|
||||
- name: Run tests
|
||||
run: cargo test --workspace --no-fail-fast
|
||||
|
||||
- name: Build collab
|
||||
run: cargo build -p collab
|
||||
|
||||
- name: Build other binaries
|
||||
run: cargo build --workspace --bins --all-features
|
||||
- name: Build collab binaries
|
||||
run: cargo build --bins --all-features
|
||||
|
||||
bundle:
|
||||
name: Bundle app
|
||||
|
||||
10
.github/workflows/release_actions.yml
vendored
10
.github/workflows/release_actions.yml
vendored
@@ -13,12 +13,12 @@ jobs:
|
||||
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
|
||||
content: |
|
||||
📣 Zed ${{ github.event.release.tag_name }} was just released!
|
||||
|
||||
|
||||
Restart your Zed or head to https://zed.dev/releases to grab it.
|
||||
|
||||
|
||||
```md
|
||||
# Changelog
|
||||
|
||||
### Changelog
|
||||
|
||||
${{ github.event.release.body }}
|
||||
```
|
||||
mixpanel_release:
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
architecture: "x64"
|
||||
cache: "pip"
|
||||
- run: pip install -r script/mixpanel_release/requirements.txt
|
||||
- run: >
|
||||
- run: >
|
||||
python script/mixpanel_release/main.py
|
||||
${{ github.event.release.tag_name }}
|
||||
${{ secrets.MIXPANEL_PROJECT_ID }}
|
||||
|
||||
76
Cargo.lock
generated
76
Cargo.lock
generated
@@ -1028,7 +1028,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "collab"
|
||||
version = "0.2.6"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -1953,18 +1953,6 @@ version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7bad48618fdb549078c333a7a8528acb57af271d0433bdecd523eb620628364e"
|
||||
|
||||
[[package]]
|
||||
name = "flume"
|
||||
version = "0.10.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1657b4441c3403d9f7b3409e47575237dac27b1b5726df654a6ecbf92f0f7577"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"pin-project",
|
||||
"spin 0.9.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fnv"
|
||||
version = "1.0.7"
|
||||
@@ -3017,12 +3005,10 @@ dependencies = [
|
||||
"text",
|
||||
"theme",
|
||||
"tree-sitter",
|
||||
"tree-sitter-embedded-template",
|
||||
"tree-sitter-html",
|
||||
"tree-sitter-javascript",
|
||||
"tree-sitter-json 0.19.0",
|
||||
"tree-sitter-python",
|
||||
"tree-sitter-ruby",
|
||||
"tree-sitter-rust",
|
||||
"tree-sitter-typescript",
|
||||
"unindent",
|
||||
@@ -3035,7 +3021,7 @@ version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
||||
dependencies = [
|
||||
"spin 0.5.2",
|
||||
"spin",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4025,7 +4011,6 @@ dependencies = [
|
||||
"env_logger",
|
||||
"gpui",
|
||||
"menu",
|
||||
"parking_lot 0.11.2",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"theme",
|
||||
@@ -4278,7 +4263,6 @@ name = "project_panel"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"context_menu",
|
||||
"drag_and_drop",
|
||||
"editor",
|
||||
"futures 0.3.24",
|
||||
"gpui",
|
||||
@@ -4739,7 +4723,7 @@ dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"spin 0.5.2",
|
||||
"spin",
|
||||
"untrusted",
|
||||
"web-sys",
|
||||
"winapi 0.3.9",
|
||||
@@ -5577,15 +5561,6 @@ version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
|
||||
|
||||
[[package]]
|
||||
name = "spin"
|
||||
version = "0.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f6002a767bff9e83f8eeecf883ecb8011875a21ae8da43bffb817a57e78cc09"
|
||||
dependencies = [
|
||||
"lock_api",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spsc-buffer"
|
||||
version = "0.1.1"
|
||||
@@ -5606,7 +5581,8 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "sqlx"
|
||||
version = "0.6.2"
|
||||
source = "git+https://github.com/launchbadge/sqlx?rev=4b7053807c705df312bcb9b6281e184bf7534eb3#4b7053807c705df312bcb9b6281e184bf7534eb3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9249290c05928352f71c077cc44a464d880c63f26f7534728cca008e135c0428"
|
||||
dependencies = [
|
||||
"sqlx-core",
|
||||
"sqlx-macros",
|
||||
@@ -5615,7 +5591,8 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "sqlx-core"
|
||||
version = "0.6.2"
|
||||
source = "git+https://github.com/launchbadge/sqlx?rev=4b7053807c705df312bcb9b6281e184bf7534eb3#4b7053807c705df312bcb9b6281e184bf7534eb3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dcbc16ddba161afc99e14d1713a453747a2b07fc097d2009f4c300ec99286105"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"atoi",
|
||||
@@ -5629,10 +5606,8 @@ dependencies = [
|
||||
"dotenvy",
|
||||
"either",
|
||||
"event-listener",
|
||||
"flume",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-executor",
|
||||
"futures-intrusive",
|
||||
"futures-util",
|
||||
"hashlink",
|
||||
@@ -5642,7 +5617,6 @@ dependencies = [
|
||||
"indexmap",
|
||||
"itoa",
|
||||
"libc",
|
||||
"libsqlite3-sys",
|
||||
"log",
|
||||
"md-5",
|
||||
"memchr",
|
||||
@@ -5672,7 +5646,8 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "sqlx-macros"
|
||||
version = "0.6.2"
|
||||
source = "git+https://github.com/launchbadge/sqlx?rev=4b7053807c705df312bcb9b6281e184bf7534eb3#4b7053807c705df312bcb9b6281e184bf7534eb3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b850fa514dc11f2ee85be9d055c512aa866746adfacd1cb42d867d68e6a5b0d9"
|
||||
dependencies = [
|
||||
"dotenvy",
|
||||
"either",
|
||||
@@ -5680,7 +5655,6 @@ dependencies = [
|
||||
"once_cell",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"serde_json",
|
||||
"sha2 0.10.6",
|
||||
"sqlx-core",
|
||||
"sqlx-rt",
|
||||
@@ -5691,7 +5665,8 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "sqlx-rt"
|
||||
version = "0.6.2"
|
||||
source = "git+https://github.com/launchbadge/sqlx?rev=4b7053807c705df312bcb9b6281e184bf7534eb3#4b7053807c705df312bcb9b6281e184bf7534eb3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24c5b2d25fa654cc5f841750b8e1cdedbe21189bf9a9382ee90bfa9dd3562396"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"tokio",
|
||||
@@ -6404,8 +6379,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter"
|
||||
version = "0.20.9"
|
||||
source = "git+https://github.com/tree-sitter/tree-sitter?rev=36b5b6c89e55ad1a502f8b3234bb3e12ec83a5da#36b5b6c89e55ad1a502f8b3234bb3e12ec83a5da"
|
||||
version = "0.20.8"
|
||||
source = "git+https://github.com/tree-sitter/tree-sitter?rev=366210ae925d7ea0891bc7a0c738f60c77c04d7b#366210ae925d7ea0891bc7a0c738f60c77c04d7b"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"regex",
|
||||
@@ -6449,16 +6424,6 @@ dependencies = [
|
||||
"tree-sitter",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter-embedded-template"
|
||||
version = "0.20.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "33817ade928c73a32d4f904a602321e09de9fc24b71d106f3b4b3f8ab30dcc38"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"tree-sitter",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter-go"
|
||||
version = "0.19.1"
|
||||
@@ -6526,16 +6491,6 @@ dependencies = [
|
||||
"tree-sitter",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter-ruby"
|
||||
version = "0.20.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ac30cbb1560363ae76e1ccde543d6d99087421e228cc47afcec004b86bb711a"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"tree-sitter",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter-rust"
|
||||
version = "0.20.3"
|
||||
@@ -6785,7 +6740,6 @@ name = "util"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"backtrace",
|
||||
"futures 0.3.24",
|
||||
"git2",
|
||||
"lazy_static",
|
||||
@@ -7674,7 +7628,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.67.0"
|
||||
version = "0.63.4"
|
||||
dependencies = [
|
||||
"activity_indicator",
|
||||
"anyhow",
|
||||
@@ -7753,13 +7707,11 @@ dependencies = [
|
||||
"tree-sitter-cpp",
|
||||
"tree-sitter-css",
|
||||
"tree-sitter-elixir",
|
||||
"tree-sitter-embedded-template",
|
||||
"tree-sitter-go",
|
||||
"tree-sitter-html",
|
||||
"tree-sitter-json 0.20.0",
|
||||
"tree-sitter-markdown",
|
||||
"tree-sitter-python",
|
||||
"tree-sitter-ruby",
|
||||
"tree-sitter-rust",
|
||||
"tree-sitter-toml",
|
||||
"tree-sitter-typescript",
|
||||
|
||||
@@ -65,7 +65,7 @@ serde_json = { version = "1.0", features = ["preserve_order", "raw_value"] }
|
||||
rand = { version = "0.8" }
|
||||
|
||||
[patch.crates-io]
|
||||
tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "36b5b6c89e55ad1a502f8b3234bb3e12ec83a5da" }
|
||||
tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "366210ae925d7ea0891bc7a0c738f60c77c04d7b" }
|
||||
async-task = { git = "https://github.com/zed-industries/async-task", rev = "341b57d6de98cdfd7b418567b8de2022ca993a6e" }
|
||||
|
||||
# TODO - Remove when a version is released with this PR: https://github.com/servo/core-foundation-rs/pull/457
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# syntax = docker/dockerfile:1.2
|
||||
|
||||
FROM rust:1.65-bullseye as builder
|
||||
FROM rust:1.64-bullseye as builder
|
||||
WORKDIR app
|
||||
COPY . .
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@
|
||||
"ctrl-n": "editor::MoveDown",
|
||||
"ctrl-b": "editor::MoveLeft",
|
||||
"ctrl-f": "editor::MoveRight",
|
||||
"ctrl-l": "editor::NextScreen",
|
||||
"ctrl-l": "editor::CenterScreen",
|
||||
"alt-left": "editor::MoveToPreviousWordStart",
|
||||
"alt-b": "editor::MoveToPreviousWordStart",
|
||||
"alt-right": "editor::MoveToNextWordEnd",
|
||||
@@ -472,15 +472,6 @@
|
||||
"terminal::SendText",
|
||||
"\u0001"
|
||||
],
|
||||
// Terminal.app compatability
|
||||
"alt-left": [
|
||||
"terminal::SendText",
|
||||
"\u001bb"
|
||||
],
|
||||
"alt-right": [
|
||||
"terminal::SendText",
|
||||
"\u001bf"
|
||||
],
|
||||
// There are conflicting bindings for these keys in the global context.
|
||||
// these bindings override them, remove at your own risk:
|
||||
"up": [
|
||||
|
||||
@@ -1,233 +1,230 @@
|
||||
{
|
||||
// The name of the Zed theme to use for the UI
|
||||
"theme": "One Dark",
|
||||
// The name of a font to use for rendering text in the editor
|
||||
"buffer_font_family": "Zed Mono",
|
||||
// The default font size for text in the editor
|
||||
"buffer_font_size": 15,
|
||||
// The factor to grow the active pane by. Defaults to 1.0
|
||||
// which gives the same size as all other panes.
|
||||
"active_pane_magnification": 1.0,
|
||||
// Whether to enable vim modes and key bindings
|
||||
"vim_mode": false,
|
||||
// Whether to show the informational hover box when moving the mouse
|
||||
// over symbols in the editor.
|
||||
"hover_popover_enabled": true,
|
||||
// Whether the cursor blinks in the editor.
|
||||
"cursor_blink": true,
|
||||
// Whether to pop the completions menu while typing in an editor without
|
||||
// explicitly requesting it.
|
||||
"show_completions_on_input": true,
|
||||
// Whether new projects should start out 'online'. Online projects
|
||||
// appear in the contacts panel under your name, so that your contacts
|
||||
// can see which projects you are working on. Regardless of this
|
||||
// setting, projects keep their last online status when you reopen them.
|
||||
"projects_online_by_default": true,
|
||||
// Whether to use language servers to provide code intelligence.
|
||||
"enable_language_server": true,
|
||||
// When to automatically save edited buffers. This setting can
|
||||
// take four values.
|
||||
//
|
||||
// 1. Never automatically save:
|
||||
// "autosave": "off",
|
||||
// 2. Save when changing focus away from the Zed window:
|
||||
// "autosave": "on_window_change",
|
||||
// 3. Save when changing focus away from a specific buffer:
|
||||
// "autosave": "on_focus_change",
|
||||
// 4. Save when idle for a certain amount of time:
|
||||
// "autosave": { "after_delay": {"milliseconds": 500} },
|
||||
"autosave": "off",
|
||||
// Where to place the dock by default. This setting can take three
|
||||
// values:
|
||||
//
|
||||
// 1. Position the dock attached to the bottom of the workspace
|
||||
// "default_dock_anchor": "bottom"
|
||||
// 2. Position the dock to the right of the workspace like a side panel
|
||||
// "default_dock_anchor": "right"
|
||||
// 3. Position the dock full screen over the entire workspace"
|
||||
// "default_dock_anchor": "expanded"
|
||||
"default_dock_anchor": "right",
|
||||
// Whether or not to perform a buffer format before saving
|
||||
"format_on_save": "on",
|
||||
// How to perform a buffer format. This setting can take two values:
|
||||
//
|
||||
// 1. Format code using the current language server:
|
||||
// "format_on_save": "language_server"
|
||||
// 2. Format code using an external command:
|
||||
// "format_on_save": {
|
||||
// "external": {
|
||||
// "command": "prettier",
|
||||
// "arguments": ["--stdin-filepath", "{buffer_path}"]
|
||||
// }
|
||||
// The name of the Zed theme to use for the UI
|
||||
"theme": "One Dark",
|
||||
// The name of a font to use for rendering text in the editor
|
||||
"buffer_font_family": "Zed Mono",
|
||||
// The default font size for text in the editor
|
||||
"buffer_font_size": 15,
|
||||
// Whether to enable vim modes and key bindings
|
||||
"vim_mode": false,
|
||||
// Whether to show the informational hover box when moving the mouse
|
||||
// over symbols in the editor.
|
||||
"hover_popover_enabled": true,
|
||||
// Whether the cursor blinks in the editor.
|
||||
"cursor_blink": true,
|
||||
// Whether to pop the completions menu while typing in an editor without
|
||||
// explicitly requesting it.
|
||||
"show_completions_on_input": true,
|
||||
// Whether new projects should start out 'online'. Online projects
|
||||
// appear in the contacts panel under your name, so that your contacts
|
||||
// can see which projects you are working on. Regardless of this
|
||||
// setting, projects keep their last online status when you reopen them.
|
||||
"projects_online_by_default": true,
|
||||
// Whether to use language servers to provide code intelligence.
|
||||
"enable_language_server": true,
|
||||
// When to automatically save edited buffers. This setting can
|
||||
// take four values.
|
||||
//
|
||||
// 1. Never automatically save:
|
||||
// "autosave": "off",
|
||||
// 2. Save when changing focus away from the Zed window:
|
||||
// "autosave": "on_window_change",
|
||||
// 3. Save when changing focus away from a specific buffer:
|
||||
// "autosave": "on_focus_change",
|
||||
// 4. Save when idle for a certain amount of time:
|
||||
// "autosave": { "after_delay": {"milliseconds": 500} },
|
||||
"autosave": "off",
|
||||
// Where to place the dock by default. This setting can take three
|
||||
// values:
|
||||
//
|
||||
// 1. Position the dock attached to the bottom of the workspace
|
||||
// "default_dock_anchor": "bottom"
|
||||
// 2. Position the dock to the right of the workspace like a side panel
|
||||
// "default_dock_anchor": "right"
|
||||
// 3. Position the dock full screen over the entire workspace"
|
||||
// "default_dock_anchor": "expanded"
|
||||
"default_dock_anchor": "right",
|
||||
// Whether or not to perform a buffer format before saving
|
||||
"format_on_save": "on",
|
||||
// How to perform a buffer format. This setting can take two values:
|
||||
//
|
||||
// 1. Format code using the current language server:
|
||||
// "format_on_save": "language_server"
|
||||
// 2. Format code using an external command:
|
||||
// "format_on_save": {
|
||||
// "external": {
|
||||
// "command": "prettier",
|
||||
// "arguments": ["--stdin-filepath", "{buffer_path}"]
|
||||
// }
|
||||
// }
|
||||
"formatter": "language_server",
|
||||
// How to soft-wrap long lines of text. This setting can take
|
||||
// three values:
|
||||
//
|
||||
// 1. Do not soft wrap.
|
||||
// "soft_wrap": "none",
|
||||
// 2. Soft wrap lines that overflow the editor:
|
||||
// "soft_wrap": "editor_width",
|
||||
// 3. Soft wrap lines at the preferred line length
|
||||
// "soft_wrap": "preferred_line_length",
|
||||
"soft_wrap": "none",
|
||||
// The column at which to soft-wrap lines, for buffers where soft-wrap
|
||||
// is enabled.
|
||||
"preferred_line_length": 80,
|
||||
// Whether to indent lines using tab characters, as opposed to multiple
|
||||
// spaces.
|
||||
"hard_tabs": false,
|
||||
// How many columns a tab should occupy.
|
||||
"tab_size": 4,
|
||||
// Git gutter behavior configuration.
|
||||
"git": {
|
||||
// Control whether the git gutter is shown. May take 2 values:
|
||||
// 1. Show the gutter
|
||||
// "git_gutter": "tracked_files"
|
||||
// 2. Hide the gutter
|
||||
// "git_gutter": "hide"
|
||||
"git_gutter": "tracked_files"
|
||||
},
|
||||
// Settings specific to journaling
|
||||
"journal": {
|
||||
// The path of the directory where journal entries are stored
|
||||
"path": "~",
|
||||
// What format to display the hours in
|
||||
// May take 2 values:
|
||||
// 1. hour12
|
||||
// 2. hour24
|
||||
"hour_format": "hour12"
|
||||
},
|
||||
// Settings specific to the terminal
|
||||
"terminal": {
|
||||
// What shell to use when opening a terminal. May take 3 values:
|
||||
// 1. Use the system's default terminal configuration (e.g. $TERM).
|
||||
// "shell": "system"
|
||||
// 2. A program:
|
||||
// "shell": {
|
||||
// "program": "sh"
|
||||
// }
|
||||
// 3. A program with arguments:
|
||||
// "shell": {
|
||||
// "with_arguments": {
|
||||
// "program": "/bin/bash",
|
||||
// "arguments": ["--login"]
|
||||
// }
|
||||
// }
|
||||
"formatter": "language_server",
|
||||
// How to soft-wrap long lines of text. This setting can take
|
||||
// three values:
|
||||
"shell": "system",
|
||||
// What working directory to use when launching the terminal.
|
||||
// May take 4 values:
|
||||
// 1. Use the current file's project directory. Will Fallback to the
|
||||
// first project directory strategy if unsuccessful
|
||||
// "working_directory": "current_project_directory"
|
||||
// 2. Use the first project in this workspace's directory
|
||||
// "working_directory": "first_project_directory"
|
||||
// 3. Always use this platform's home directory (if we can find it)
|
||||
// "working_directory": "always_home"
|
||||
// 4. Always use a specific directory. This value will be shell expanded.
|
||||
// If this path is not a valid directory the terminal will default to
|
||||
// this platform's home directory (if we can find it)
|
||||
// "working_directory": {
|
||||
// "always": {
|
||||
// "directory": "~/zed/projects/"
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// 1. Do not soft wrap.
|
||||
// "soft_wrap": "none",
|
||||
// 2. Soft wrap lines that overflow the editor:
|
||||
// "soft_wrap": "editor_width",
|
||||
// 3. Soft wrap lines at the preferred line length
|
||||
// "soft_wrap": "preferred_line_length",
|
||||
"soft_wrap": "none",
|
||||
// The column at which to soft-wrap lines, for buffers where soft-wrap
|
||||
// is enabled.
|
||||
"preferred_line_length": 80,
|
||||
// Whether to indent lines using tab characters, as opposed to multiple
|
||||
// spaces.
|
||||
"hard_tabs": false,
|
||||
// How many columns a tab should occupy.
|
||||
"tab_size": 4,
|
||||
// Git gutter behavior configuration.
|
||||
"git": {
|
||||
// Control whether the git gutter is shown. May take 2 values:
|
||||
// 1. Show the gutter
|
||||
// "git_gutter": "tracked_files"
|
||||
// 2. Hide the gutter
|
||||
// "git_gutter": "hide"
|
||||
"git_gutter": "tracked_files"
|
||||
},
|
||||
// Settings specific to journaling
|
||||
"journal": {
|
||||
// The path of the directory where journal entries are stored
|
||||
"path": "~",
|
||||
// What format to display the hours in
|
||||
// May take 2 values:
|
||||
// 1. hour12
|
||||
// 2. hour24
|
||||
"hour_format": "hour12"
|
||||
},
|
||||
// Settings specific to the terminal
|
||||
"terminal": {
|
||||
// What shell to use when opening a terminal. May take 3 values:
|
||||
// 1. Use the system's default terminal configuration (e.g. $TERM).
|
||||
// "shell": "system"
|
||||
// 2. A program:
|
||||
// "shell": {
|
||||
// "program": "sh"
|
||||
// }
|
||||
// 3. A program with arguments:
|
||||
// "shell": {
|
||||
// "with_arguments": {
|
||||
// "program": "/bin/bash",
|
||||
// "arguments": ["--login"]
|
||||
// }
|
||||
// }
|
||||
"shell": "system",
|
||||
// What working directory to use when launching the terminal.
|
||||
// May take 4 values:
|
||||
// 1. Use the current file's project directory. Will Fallback to the
|
||||
// first project directory strategy if unsuccessful
|
||||
// "working_directory": "current_project_directory"
|
||||
// 2. Use the first project in this workspace's directory
|
||||
// "working_directory": "first_project_directory"
|
||||
// 3. Always use this platform's home directory (if we can find it)
|
||||
// "working_directory": "always_home"
|
||||
// 4. Always use a specific directory. This value will be shell expanded.
|
||||
// If this path is not a valid directory the terminal will default to
|
||||
// this platform's home directory (if we can find it)
|
||||
// "working_directory": {
|
||||
// "always": {
|
||||
// "directory": "~/zed/projects/"
|
||||
// }
|
||||
// }
|
||||
//
|
||||
//
|
||||
"working_directory": "current_project_directory",
|
||||
// Set the cursor blinking behavior in the terminal.
|
||||
// May take 4 values:
|
||||
// 1. Never blink the cursor, ignoring the terminal mode
|
||||
// "blinking": "off",
|
||||
// 2. Default the cursor blink to off, but allow the terminal to
|
||||
// set blinking
|
||||
// "blinking": "terminal_controlled",
|
||||
// 3. Always blink the cursor, ignoring the terminal mode
|
||||
// "blinking": "on",
|
||||
"blinking": "terminal_controlled",
|
||||
// Set whether Alternate Scroll mode (code: ?1007) is active by default.
|
||||
// Alternate Scroll mode converts mouse scroll events into up / down key
|
||||
// presses when in the alternate screen (e.g. when running applications
|
||||
// like vim or less). The terminal can still set and unset this mode.
|
||||
// May take 2 values:
|
||||
// 1. Default alternate scroll mode to on
|
||||
// "alternate_scroll": "on",
|
||||
// 2. Default alternate scroll mode to off
|
||||
// "alternate_scroll": "off",
|
||||
"alternate_scroll": "off",
|
||||
// Set whether the option key behaves as the meta key.
|
||||
// May take 2 values:
|
||||
// 1. Rely on default platform handling of option key, on macOS
|
||||
// this means generating certain unicode characters
|
||||
// "option_to_meta": false,
|
||||
// 2. Make the option keys behave as a 'meta' key, e.g. for emacs
|
||||
// "option_to_meta": true,
|
||||
"option_as_meta": false,
|
||||
// Whether or not selecting text in the terminal will automatically
|
||||
// copy to the system clipboard.
|
||||
"copy_on_select": false,
|
||||
// Any key-value pairs added to this list will be added to the terminal's
|
||||
// enviroment. Use `:` to seperate multiple values.
|
||||
"env": {
|
||||
// "KEY": "value1:value2"
|
||||
}
|
||||
// Set the terminal's font size. If this option is not included,
|
||||
// the terminal will default to matching the buffer's font size.
|
||||
// "font_size": "15"
|
||||
// Set the terminal's font family. If this option is not included,
|
||||
// the terminal will default to matching the buffer's font family.
|
||||
// "font_family": "Zed Mono"
|
||||
},
|
||||
// Different settings for specific languages.
|
||||
"languages": {
|
||||
"Plain Text": {
|
||||
"soft_wrap": "preferred_line_length"
|
||||
},
|
||||
"C": {
|
||||
"tab_size": 2
|
||||
},
|
||||
"C++": {
|
||||
"tab_size": 2
|
||||
},
|
||||
"Elixir": {
|
||||
"tab_size": 2
|
||||
},
|
||||
"Go": {
|
||||
"tab_size": 4,
|
||||
"hard_tabs": true
|
||||
},
|
||||
"Markdown": {
|
||||
"soft_wrap": "preferred_line_length"
|
||||
},
|
||||
"Rust": {
|
||||
"tab_size": 4
|
||||
},
|
||||
"JavaScript": {
|
||||
"tab_size": 2
|
||||
},
|
||||
"TypeScript": {
|
||||
"tab_size": 2
|
||||
},
|
||||
"TSX": {
|
||||
"tab_size": 2
|
||||
}
|
||||
},
|
||||
// LSP Specific settings.
|
||||
"lsp": {
|
||||
// Specify the LSP name as a key here.
|
||||
// As of 8/10/22, supported LSPs are:
|
||||
// pyright
|
||||
// gopls
|
||||
// rust-analyzer
|
||||
// typescript-language-server
|
||||
// vscode-json-languageserver
|
||||
// "rust_analyzer": {
|
||||
// //These initialization options are merged into Zed's defaults
|
||||
// "initialization_options": {
|
||||
// "checkOnSave": {
|
||||
// "command": "clippy"
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
"working_directory": "current_project_directory",
|
||||
// Set the cursor blinking behavior in the terminal.
|
||||
// May take 4 values:
|
||||
// 1. Never blink the cursor, ignoring the terminal mode
|
||||
// "blinking": "off",
|
||||
// 2. Default the cursor blink to off, but allow the terminal to
|
||||
// set blinking
|
||||
// "blinking": "terminal_controlled",
|
||||
// 3. Always blink the cursor, ignoring the terminal mode
|
||||
// "blinking": "on",
|
||||
"blinking": "terminal_controlled",
|
||||
// Set whether Alternate Scroll mode (code: ?1007) is active by default.
|
||||
// Alternate Scroll mode converts mouse scroll events into up / down key
|
||||
// presses when in the alternate screen (e.g. when running applications
|
||||
// like vim or less). The terminal can still set and unset this mode.
|
||||
// May take 2 values:
|
||||
// 1. Default alternate scroll mode to on
|
||||
// "alternate_scroll": "on",
|
||||
// 2. Default alternate scroll mode to off
|
||||
// "alternate_scroll": "off",
|
||||
"alternate_scroll": "off",
|
||||
// Set whether the option key behaves as the meta key.
|
||||
// May take 2 values:
|
||||
// 1. Rely on default platform handling of option key, on macOS
|
||||
// this means generating certain unicode characters
|
||||
// "option_to_meta": false,
|
||||
// 2. Make the option keys behave as a 'meta' key, e.g. for emacs
|
||||
// "option_to_meta": true,
|
||||
"option_as_meta": false,
|
||||
// Whether or not selecting text in the terminal will automatically
|
||||
// copy to the system clipboard.
|
||||
"copy_on_select": false,
|
||||
// Any key-value pairs added to this list will be added to the terminal's
|
||||
// enviroment. Use `:` to seperate multiple values.
|
||||
"env": {
|
||||
// "KEY": "value1:value2"
|
||||
}
|
||||
// Set the terminal's font size. If this option is not included,
|
||||
// the terminal will default to matching the buffer's font size.
|
||||
// "font_size": "15"
|
||||
// Set the terminal's font family. If this option is not included,
|
||||
// the terminal will default to matching the buffer's font family.
|
||||
// "font_family": "Zed Mono"
|
||||
},
|
||||
// Different settings for specific languages.
|
||||
"languages": {
|
||||
"Plain Text": {
|
||||
"soft_wrap": "preferred_line_length"
|
||||
},
|
||||
"C": {
|
||||
"tab_size": 2
|
||||
},
|
||||
"C++": {
|
||||
"tab_size": 2
|
||||
},
|
||||
"Elixir": {
|
||||
"tab_size": 2
|
||||
},
|
||||
"Go": {
|
||||
"tab_size": 4,
|
||||
"hard_tabs": true
|
||||
},
|
||||
"Markdown": {
|
||||
"soft_wrap": "preferred_line_length"
|
||||
},
|
||||
"Rust": {
|
||||
"tab_size": 4
|
||||
},
|
||||
"JavaScript": {
|
||||
"tab_size": 2
|
||||
},
|
||||
"TypeScript": {
|
||||
"tab_size": 2
|
||||
},
|
||||
"TSX": {
|
||||
"tab_size": 2
|
||||
}
|
||||
},
|
||||
// LSP Specific settings.
|
||||
"lsp": {
|
||||
// Specify the LSP name as a key here.
|
||||
// As of 8/10/22, supported LSPs are:
|
||||
// pyright
|
||||
// gopls
|
||||
// rust-analyzer
|
||||
// typescript-language-server
|
||||
// vscode-json-languageserver
|
||||
// "rust_analyzer": {
|
||||
// //These initialization options are merged into Zed's defaults
|
||||
// "initialization_options": {
|
||||
// "checkOnSave": {
|
||||
// "command": "clippy"
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ actions!(lsp_status, [ShowErrorMessage]);
|
||||
|
||||
const DOWNLOAD_ICON: &str = "icons/download_12.svg";
|
||||
const WARNING_ICON: &str = "icons/triangle_exclamation_12.svg";
|
||||
const DONE_ICON: &str = "icons/circle_check_12.svg";
|
||||
|
||||
pub enum Event {
|
||||
ShowError { lsp_name: Arc<str>, error: String },
|
||||
@@ -236,6 +237,7 @@ impl ActivityIndicator {
|
||||
|
||||
// Show any application auto-update info.
|
||||
if let Some(updater) = &self.auto_updater {
|
||||
// let theme = &cx.global::<Settings>().theme.workspace.status_bar;
|
||||
match &updater.read(cx).status() {
|
||||
AutoUpdateStatus::Checking => (
|
||||
Some(DOWNLOAD_ICON),
|
||||
@@ -252,7 +254,9 @@ impl ActivityIndicator {
|
||||
"Installing Zed update…".to_string(),
|
||||
None,
|
||||
),
|
||||
AutoUpdateStatus::Updated => (None, "Restart to update Zed".to_string(), None),
|
||||
AutoUpdateStatus::Updated => {
|
||||
(Some(DONE_ICON), "Restart to update Zed".to_string(), None)
|
||||
}
|
||||
AutoUpdateStatus::Errored => (
|
||||
Some(WARNING_ICON),
|
||||
"Auto update failed".to_string(),
|
||||
|
||||
820
crates/client/src/channel.rs
Normal file
820
crates/client/src/channel.rs
Normal file
@@ -0,0 +1,820 @@
|
||||
use super::{
|
||||
proto,
|
||||
user::{User, UserStore},
|
||||
Client, Status, Subscription, TypedEnvelope,
|
||||
};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use futures::lock::Mutex;
|
||||
use gpui::{
|
||||
AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task, WeakModelHandle,
|
||||
};
|
||||
use postage::prelude::Stream;
|
||||
use rand::prelude::*;
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
mem,
|
||||
ops::Range,
|
||||
sync::Arc,
|
||||
};
|
||||
use sum_tree::{Bias, SumTree};
|
||||
use time::OffsetDateTime;
|
||||
use util::{post_inc, ResultExt as _, TryFutureExt};
|
||||
|
||||
pub struct ChannelList {
|
||||
available_channels: Option<Vec<ChannelDetails>>,
|
||||
channels: HashMap<u64, WeakModelHandle<Channel>>,
|
||||
client: Arc<Client>,
|
||||
user_store: ModelHandle<UserStore>,
|
||||
_task: Task<Option<()>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct ChannelDetails {
|
||||
pub id: u64,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
pub struct Channel {
|
||||
details: ChannelDetails,
|
||||
messages: SumTree<ChannelMessage>,
|
||||
loaded_all_messages: bool,
|
||||
next_pending_message_id: usize,
|
||||
user_store: ModelHandle<UserStore>,
|
||||
rpc: Arc<Client>,
|
||||
outgoing_messages_lock: Arc<Mutex<()>>,
|
||||
rng: StdRng,
|
||||
_subscription: Subscription,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ChannelMessage {
|
||||
pub id: ChannelMessageId,
|
||||
pub body: String,
|
||||
pub timestamp: OffsetDateTime,
|
||||
pub sender: Arc<User>,
|
||||
pub nonce: u128,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum ChannelMessageId {
|
||||
Saved(u64),
|
||||
Pending(usize),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct ChannelMessageSummary {
|
||||
max_id: ChannelMessageId,
|
||||
count: usize,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
|
||||
struct Count(usize);
|
||||
|
||||
pub enum ChannelListEvent {}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum ChannelEvent {
|
||||
MessagesUpdated {
|
||||
old_range: Range<usize>,
|
||||
new_count: usize,
|
||||
},
|
||||
}
|
||||
|
||||
impl Entity for ChannelList {
|
||||
type Event = ChannelListEvent;
|
||||
}
|
||||
|
||||
impl ChannelList {
|
||||
pub fn new(
|
||||
user_store: ModelHandle<UserStore>,
|
||||
rpc: Arc<Client>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Self {
|
||||
let _task = cx.spawn_weak(|this, mut cx| {
|
||||
let rpc = rpc.clone();
|
||||
async move {
|
||||
let mut status = rpc.status();
|
||||
while let Some((status, this)) = status.recv().await.zip(this.upgrade(&cx)) {
|
||||
match status {
|
||||
Status::Connected { .. } => {
|
||||
let response = rpc
|
||||
.request(proto::GetChannels {})
|
||||
.await
|
||||
.context("failed to fetch available channels")?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.available_channels =
|
||||
Some(response.channels.into_iter().map(Into::into).collect());
|
||||
|
||||
let mut to_remove = Vec::new();
|
||||
for (channel_id, channel) in &this.channels {
|
||||
if let Some(channel) = channel.upgrade(cx) {
|
||||
channel.update(cx, |channel, cx| channel.rejoin(cx))
|
||||
} else {
|
||||
to_remove.push(*channel_id);
|
||||
}
|
||||
}
|
||||
|
||||
for channel_id in to_remove {
|
||||
this.channels.remove(&channel_id);
|
||||
}
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
Status::SignedOut { .. } => {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.available_channels = None;
|
||||
this.channels.clear();
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
.log_err()
|
||||
});
|
||||
|
||||
Self {
|
||||
available_channels: None,
|
||||
channels: Default::default(),
|
||||
user_store,
|
||||
client: rpc,
|
||||
_task,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn available_channels(&self) -> Option<&[ChannelDetails]> {
|
||||
self.available_channels.as_deref()
|
||||
}
|
||||
|
||||
pub fn get_channel(
|
||||
&mut self,
|
||||
id: u64,
|
||||
cx: &mut MutableAppContext,
|
||||
) -> Option<ModelHandle<Channel>> {
|
||||
if let Some(channel) = self.channels.get(&id).and_then(|c| c.upgrade(cx)) {
|
||||
return Some(channel);
|
||||
}
|
||||
|
||||
let channels = self.available_channels.as_ref()?;
|
||||
let details = channels.iter().find(|details| details.id == id)?.clone();
|
||||
let channel = cx.add_model(|cx| {
|
||||
Channel::new(details, self.user_store.clone(), self.client.clone(), cx)
|
||||
});
|
||||
self.channels.insert(id, channel.downgrade());
|
||||
Some(channel)
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for Channel {
|
||||
type Event = ChannelEvent;
|
||||
|
||||
fn release(&mut self, _: &mut MutableAppContext) {
|
||||
self.rpc
|
||||
.send(proto::LeaveChannel {
|
||||
channel_id: self.details.id,
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
|
||||
impl Channel {
|
||||
pub fn init(rpc: &Arc<Client>) {
|
||||
rpc.add_model_message_handler(Self::handle_message_sent);
|
||||
}
|
||||
|
||||
pub fn new(
|
||||
details: ChannelDetails,
|
||||
user_store: ModelHandle<UserStore>,
|
||||
rpc: Arc<Client>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Self {
|
||||
let _subscription = rpc.add_model_for_remote_entity(details.id, cx);
|
||||
|
||||
{
|
||||
let user_store = user_store.clone();
|
||||
let rpc = rpc.clone();
|
||||
let channel_id = details.id;
|
||||
cx.spawn(|channel, mut cx| {
|
||||
async move {
|
||||
let response = rpc.request(proto::JoinChannel { channel_id }).await?;
|
||||
let messages =
|
||||
messages_from_proto(response.messages, &user_store, &mut cx).await?;
|
||||
let loaded_all_messages = response.done;
|
||||
|
||||
channel.update(&mut cx, |channel, cx| {
|
||||
channel.insert_messages(messages, cx);
|
||||
channel.loaded_all_messages = loaded_all_messages;
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
.log_err()
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
Self {
|
||||
details,
|
||||
user_store,
|
||||
rpc,
|
||||
outgoing_messages_lock: Default::default(),
|
||||
messages: Default::default(),
|
||||
loaded_all_messages: false,
|
||||
next_pending_message_id: 0,
|
||||
rng: StdRng::from_entropy(),
|
||||
_subscription,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &str {
|
||||
&self.details.name
|
||||
}
|
||||
|
||||
pub fn send_message(
|
||||
&mut self,
|
||||
body: String,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Result<Task<Result<()>>> {
|
||||
if body.is_empty() {
|
||||
Err(anyhow!("message body can't be empty"))?;
|
||||
}
|
||||
|
||||
let current_user = self
|
||||
.user_store
|
||||
.read(cx)
|
||||
.current_user()
|
||||
.ok_or_else(|| anyhow!("current_user is not present"))?;
|
||||
|
||||
let channel_id = self.details.id;
|
||||
let pending_id = ChannelMessageId::Pending(post_inc(&mut self.next_pending_message_id));
|
||||
let nonce = self.rng.gen();
|
||||
self.insert_messages(
|
||||
SumTree::from_item(
|
||||
ChannelMessage {
|
||||
id: pending_id,
|
||||
body: body.clone(),
|
||||
sender: current_user,
|
||||
timestamp: OffsetDateTime::now_utc(),
|
||||
nonce,
|
||||
},
|
||||
&(),
|
||||
),
|
||||
cx,
|
||||
);
|
||||
let user_store = self.user_store.clone();
|
||||
let rpc = self.rpc.clone();
|
||||
let outgoing_messages_lock = self.outgoing_messages_lock.clone();
|
||||
Ok(cx.spawn(|this, mut cx| async move {
|
||||
let outgoing_message_guard = outgoing_messages_lock.lock().await;
|
||||
let request = rpc.request(proto::SendChannelMessage {
|
||||
channel_id,
|
||||
body,
|
||||
nonce: Some(nonce.into()),
|
||||
});
|
||||
let response = request.await?;
|
||||
drop(outgoing_message_guard);
|
||||
let message = ChannelMessage::from_proto(
|
||||
response.message.ok_or_else(|| anyhow!("invalid message"))?,
|
||||
&user_store,
|
||||
&mut cx,
|
||||
)
|
||||
.await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.insert_messages(SumTree::from_item(message, &()), cx);
|
||||
Ok(())
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn load_more_messages(&mut self, cx: &mut ModelContext<Self>) -> bool {
|
||||
if !self.loaded_all_messages {
|
||||
let rpc = self.rpc.clone();
|
||||
let user_store = self.user_store.clone();
|
||||
let channel_id = self.details.id;
|
||||
if let Some(before_message_id) =
|
||||
self.messages.first().and_then(|message| match message.id {
|
||||
ChannelMessageId::Saved(id) => Some(id),
|
||||
ChannelMessageId::Pending(_) => None,
|
||||
})
|
||||
{
|
||||
cx.spawn(|this, mut cx| {
|
||||
async move {
|
||||
let response = rpc
|
||||
.request(proto::GetChannelMessages {
|
||||
channel_id,
|
||||
before_message_id,
|
||||
})
|
||||
.await?;
|
||||
let loaded_all_messages = response.done;
|
||||
let messages =
|
||||
messages_from_proto(response.messages, &user_store, &mut cx).await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.loaded_all_messages = loaded_all_messages;
|
||||
this.insert_messages(messages, cx);
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
.log_err()
|
||||
})
|
||||
.detach();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub fn rejoin(&mut self, cx: &mut ModelContext<Self>) {
|
||||
let user_store = self.user_store.clone();
|
||||
let rpc = self.rpc.clone();
|
||||
let channel_id = self.details.id;
|
||||
cx.spawn(|this, mut cx| {
|
||||
async move {
|
||||
let response = rpc.request(proto::JoinChannel { channel_id }).await?;
|
||||
let messages = messages_from_proto(response.messages, &user_store, &mut cx).await?;
|
||||
let loaded_all_messages = response.done;
|
||||
|
||||
let pending_messages = this.update(&mut cx, |this, cx| {
|
||||
if let Some((first_new_message, last_old_message)) =
|
||||
messages.first().zip(this.messages.last())
|
||||
{
|
||||
if first_new_message.id > last_old_message.id {
|
||||
let old_messages = mem::take(&mut this.messages);
|
||||
cx.emit(ChannelEvent::MessagesUpdated {
|
||||
old_range: 0..old_messages.summary().count,
|
||||
new_count: 0,
|
||||
});
|
||||
this.loaded_all_messages = loaded_all_messages;
|
||||
}
|
||||
}
|
||||
|
||||
this.insert_messages(messages, cx);
|
||||
if loaded_all_messages {
|
||||
this.loaded_all_messages = loaded_all_messages;
|
||||
}
|
||||
|
||||
this.pending_messages().cloned().collect::<Vec<_>>()
|
||||
});
|
||||
|
||||
for pending_message in pending_messages {
|
||||
let request = rpc.request(proto::SendChannelMessage {
|
||||
channel_id,
|
||||
body: pending_message.body,
|
||||
nonce: Some(pending_message.nonce.into()),
|
||||
});
|
||||
let response = request.await?;
|
||||
let message = ChannelMessage::from_proto(
|
||||
response.message.ok_or_else(|| anyhow!("invalid message"))?,
|
||||
&user_store,
|
||||
&mut cx,
|
||||
)
|
||||
.await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.insert_messages(SumTree::from_item(message, &()), cx);
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
.log_err()
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub fn message_count(&self) -> usize {
|
||||
self.messages.summary().count
|
||||
}
|
||||
|
||||
pub fn messages(&self) -> &SumTree<ChannelMessage> {
|
||||
&self.messages
|
||||
}
|
||||
|
||||
pub fn message(&self, ix: usize) -> &ChannelMessage {
|
||||
let mut cursor = self.messages.cursor::<Count>();
|
||||
cursor.seek(&Count(ix), Bias::Right, &());
|
||||
cursor.item().unwrap()
|
||||
}
|
||||
|
||||
pub fn messages_in_range(&self, range: Range<usize>) -> impl Iterator<Item = &ChannelMessage> {
|
||||
let mut cursor = self.messages.cursor::<Count>();
|
||||
cursor.seek(&Count(range.start), Bias::Right, &());
|
||||
cursor.take(range.len())
|
||||
}
|
||||
|
||||
pub fn pending_messages(&self) -> impl Iterator<Item = &ChannelMessage> {
|
||||
let mut cursor = self.messages.cursor::<ChannelMessageId>();
|
||||
cursor.seek(&ChannelMessageId::Pending(0), Bias::Left, &());
|
||||
cursor
|
||||
}
|
||||
|
||||
async fn handle_message_sent(
|
||||
this: ModelHandle<Self>,
|
||||
message: TypedEnvelope<proto::ChannelMessageSent>,
|
||||
_: Arc<Client>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
let user_store = this.read_with(&cx, |this, _| this.user_store.clone());
|
||||
let message = message
|
||||
.payload
|
||||
.message
|
||||
.ok_or_else(|| anyhow!("empty message"))?;
|
||||
|
||||
let message = ChannelMessage::from_proto(message, &user_store, &mut cx).await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.insert_messages(SumTree::from_item(message, &()), cx)
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn insert_messages(&mut self, messages: SumTree<ChannelMessage>, cx: &mut ModelContext<Self>) {
|
||||
if let Some((first_message, last_message)) = messages.first().zip(messages.last()) {
|
||||
let nonces = messages
|
||||
.cursor::<()>()
|
||||
.map(|m| m.nonce)
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
let mut old_cursor = self.messages.cursor::<(ChannelMessageId, Count)>();
|
||||
let mut new_messages = old_cursor.slice(&first_message.id, Bias::Left, &());
|
||||
let start_ix = old_cursor.start().1 .0;
|
||||
let removed_messages = old_cursor.slice(&last_message.id, Bias::Right, &());
|
||||
let removed_count = removed_messages.summary().count;
|
||||
let new_count = messages.summary().count;
|
||||
let end_ix = start_ix + removed_count;
|
||||
|
||||
new_messages.push_tree(messages, &());
|
||||
|
||||
let mut ranges = Vec::<Range<usize>>::new();
|
||||
if new_messages.last().unwrap().is_pending() {
|
||||
new_messages.push_tree(old_cursor.suffix(&()), &());
|
||||
} else {
|
||||
new_messages.push_tree(
|
||||
old_cursor.slice(&ChannelMessageId::Pending(0), Bias::Left, &()),
|
||||
&(),
|
||||
);
|
||||
|
||||
while let Some(message) = old_cursor.item() {
|
||||
let message_ix = old_cursor.start().1 .0;
|
||||
if nonces.contains(&message.nonce) {
|
||||
if ranges.last().map_or(false, |r| r.end == message_ix) {
|
||||
ranges.last_mut().unwrap().end += 1;
|
||||
} else {
|
||||
ranges.push(message_ix..message_ix + 1);
|
||||
}
|
||||
} else {
|
||||
new_messages.push(message.clone(), &());
|
||||
}
|
||||
old_cursor.next(&());
|
||||
}
|
||||
}
|
||||
|
||||
drop(old_cursor);
|
||||
self.messages = new_messages;
|
||||
|
||||
for range in ranges.into_iter().rev() {
|
||||
cx.emit(ChannelEvent::MessagesUpdated {
|
||||
old_range: range,
|
||||
new_count: 0,
|
||||
});
|
||||
}
|
||||
cx.emit(ChannelEvent::MessagesUpdated {
|
||||
old_range: start_ix..end_ix,
|
||||
new_count,
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn messages_from_proto(
|
||||
proto_messages: Vec<proto::ChannelMessage>,
|
||||
user_store: &ModelHandle<UserStore>,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> Result<SumTree<ChannelMessage>> {
|
||||
let unique_user_ids = proto_messages
|
||||
.iter()
|
||||
.map(|m| m.sender_id)
|
||||
.collect::<HashSet<_>>()
|
||||
.into_iter()
|
||||
.collect();
|
||||
user_store
|
||||
.update(cx, |user_store, cx| {
|
||||
user_store.get_users(unique_user_ids, cx)
|
||||
})
|
||||
.await?;
|
||||
|
||||
let mut messages = Vec::with_capacity(proto_messages.len());
|
||||
for message in proto_messages {
|
||||
messages.push(ChannelMessage::from_proto(message, user_store, cx).await?);
|
||||
}
|
||||
let mut result = SumTree::new();
|
||||
result.extend(messages, &());
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
impl From<proto::Channel> for ChannelDetails {
|
||||
fn from(message: proto::Channel) -> Self {
|
||||
Self {
|
||||
id: message.id,
|
||||
name: message.name,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ChannelMessage {
|
||||
pub async fn from_proto(
|
||||
message: proto::ChannelMessage,
|
||||
user_store: &ModelHandle<UserStore>,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> Result<Self> {
|
||||
let sender = user_store
|
||||
.update(cx, |user_store, cx| {
|
||||
user_store.get_user(message.sender_id, cx)
|
||||
})
|
||||
.await?;
|
||||
Ok(ChannelMessage {
|
||||
id: ChannelMessageId::Saved(message.id),
|
||||
body: message.body,
|
||||
timestamp: OffsetDateTime::from_unix_timestamp(message.timestamp as i64)?,
|
||||
sender,
|
||||
nonce: message
|
||||
.nonce
|
||||
.ok_or_else(|| anyhow!("nonce is required"))?
|
||||
.into(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn is_pending(&self) -> bool {
|
||||
matches!(self.id, ChannelMessageId::Pending(_))
|
||||
}
|
||||
}
|
||||
|
||||
impl sum_tree::Item for ChannelMessage {
|
||||
type Summary = ChannelMessageSummary;
|
||||
|
||||
fn summary(&self) -> Self::Summary {
|
||||
ChannelMessageSummary {
|
||||
max_id: self.id,
|
||||
count: 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ChannelMessageId {
|
||||
fn default() -> Self {
|
||||
Self::Saved(0)
|
||||
}
|
||||
}
|
||||
|
||||
impl sum_tree::Summary for ChannelMessageSummary {
|
||||
type Context = ();
|
||||
|
||||
fn add_summary(&mut self, summary: &Self, _: &()) {
|
||||
self.max_id = summary.max_id;
|
||||
self.count += summary.count;
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> sum_tree::Dimension<'a, ChannelMessageSummary> for ChannelMessageId {
|
||||
fn add_summary(&mut self, summary: &'a ChannelMessageSummary, _: &()) {
|
||||
debug_assert!(summary.max_id > *self);
|
||||
*self = summary.max_id;
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> sum_tree::Dimension<'a, ChannelMessageSummary> for Count {
|
||||
fn add_summary(&mut self, summary: &'a ChannelMessageSummary, _: &()) {
|
||||
self.0 += summary.count;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::test::{FakeHttpClient, FakeServer};
|
||||
use gpui::TestAppContext;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_channel_messages(cx: &mut TestAppContext) {
|
||||
cx.foreground().forbid_parking();
|
||||
|
||||
let user_id = 5;
|
||||
let http_client = FakeHttpClient::with_404_response();
|
||||
let client = cx.update(|cx| Client::new(http_client.clone(), cx));
|
||||
let server = FakeServer::for_client(user_id, &client, cx).await;
|
||||
|
||||
Channel::init(&client);
|
||||
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
|
||||
|
||||
let channel_list = cx.add_model(|cx| ChannelList::new(user_store, client.clone(), cx));
|
||||
channel_list.read_with(cx, |list, _| assert_eq!(list.available_channels(), None));
|
||||
|
||||
// Get the available channels.
|
||||
let get_channels = server.receive::<proto::GetChannels>().await.unwrap();
|
||||
server
|
||||
.respond(
|
||||
get_channels.receipt(),
|
||||
proto::GetChannelsResponse {
|
||||
channels: vec![proto::Channel {
|
||||
id: 5,
|
||||
name: "the-channel".to_string(),
|
||||
}],
|
||||
},
|
||||
)
|
||||
.await;
|
||||
channel_list.next_notification(cx).await;
|
||||
channel_list.read_with(cx, |list, _| {
|
||||
assert_eq!(
|
||||
list.available_channels().unwrap(),
|
||||
&[ChannelDetails {
|
||||
id: 5,
|
||||
name: "the-channel".into(),
|
||||
}]
|
||||
)
|
||||
});
|
||||
|
||||
let get_users = server.receive::<proto::GetUsers>().await.unwrap();
|
||||
assert_eq!(get_users.payload.user_ids, vec![5]);
|
||||
server
|
||||
.respond(
|
||||
get_users.receipt(),
|
||||
proto::UsersResponse {
|
||||
users: vec![proto::User {
|
||||
id: 5,
|
||||
github_login: "nathansobo".into(),
|
||||
avatar_url: "http://avatar.com/nathansobo".into(),
|
||||
}],
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
// Join a channel and populate its existing messages.
|
||||
let channel = channel_list
|
||||
.update(cx, |list, cx| {
|
||||
let channel_id = list.available_channels().unwrap()[0].id;
|
||||
list.get_channel(channel_id, cx)
|
||||
})
|
||||
.unwrap();
|
||||
channel.read_with(cx, |channel, _| assert!(channel.messages().is_empty()));
|
||||
let join_channel = server.receive::<proto::JoinChannel>().await.unwrap();
|
||||
server
|
||||
.respond(
|
||||
join_channel.receipt(),
|
||||
proto::JoinChannelResponse {
|
||||
messages: vec![
|
||||
proto::ChannelMessage {
|
||||
id: 10,
|
||||
body: "a".into(),
|
||||
timestamp: 1000,
|
||||
sender_id: 5,
|
||||
nonce: Some(1.into()),
|
||||
},
|
||||
proto::ChannelMessage {
|
||||
id: 11,
|
||||
body: "b".into(),
|
||||
timestamp: 1001,
|
||||
sender_id: 6,
|
||||
nonce: Some(2.into()),
|
||||
},
|
||||
],
|
||||
done: false,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
// Client requests all users for the received messages
|
||||
let mut get_users = server.receive::<proto::GetUsers>().await.unwrap();
|
||||
get_users.payload.user_ids.sort();
|
||||
assert_eq!(get_users.payload.user_ids, vec![6]);
|
||||
server
|
||||
.respond(
|
||||
get_users.receipt(),
|
||||
proto::UsersResponse {
|
||||
users: vec![proto::User {
|
||||
id: 6,
|
||||
github_login: "maxbrunsfeld".into(),
|
||||
avatar_url: "http://avatar.com/maxbrunsfeld".into(),
|
||||
}],
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
channel.next_event(cx).await,
|
||||
ChannelEvent::MessagesUpdated {
|
||||
old_range: 0..0,
|
||||
new_count: 2,
|
||||
}
|
||||
);
|
||||
channel.read_with(cx, |channel, _| {
|
||||
assert_eq!(
|
||||
channel
|
||||
.messages_in_range(0..2)
|
||||
.map(|message| (message.sender.github_login.clone(), message.body.clone()))
|
||||
.collect::<Vec<_>>(),
|
||||
&[
|
||||
("nathansobo".into(), "a".into()),
|
||||
("maxbrunsfeld".into(), "b".into())
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
// Receive a new message.
|
||||
server.send(proto::ChannelMessageSent {
|
||||
channel_id: channel.read_with(cx, |channel, _| channel.details.id),
|
||||
message: Some(proto::ChannelMessage {
|
||||
id: 12,
|
||||
body: "c".into(),
|
||||
timestamp: 1002,
|
||||
sender_id: 7,
|
||||
nonce: Some(3.into()),
|
||||
}),
|
||||
});
|
||||
|
||||
// Client requests user for message since they haven't seen them yet
|
||||
let get_users = server.receive::<proto::GetUsers>().await.unwrap();
|
||||
assert_eq!(get_users.payload.user_ids, vec![7]);
|
||||
server
|
||||
.respond(
|
||||
get_users.receipt(),
|
||||
proto::UsersResponse {
|
||||
users: vec![proto::User {
|
||||
id: 7,
|
||||
github_login: "as-cii".into(),
|
||||
avatar_url: "http://avatar.com/as-cii".into(),
|
||||
}],
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
channel.next_event(cx).await,
|
||||
ChannelEvent::MessagesUpdated {
|
||||
old_range: 2..2,
|
||||
new_count: 1,
|
||||
}
|
||||
);
|
||||
channel.read_with(cx, |channel, _| {
|
||||
assert_eq!(
|
||||
channel
|
||||
.messages_in_range(2..3)
|
||||
.map(|message| (message.sender.github_login.clone(), message.body.clone()))
|
||||
.collect::<Vec<_>>(),
|
||||
&[("as-cii".into(), "c".into())]
|
||||
)
|
||||
});
|
||||
|
||||
// Scroll up to view older messages.
|
||||
channel.update(cx, |channel, cx| {
|
||||
assert!(channel.load_more_messages(cx));
|
||||
});
|
||||
let get_messages = server.receive::<proto::GetChannelMessages>().await.unwrap();
|
||||
assert_eq!(get_messages.payload.channel_id, 5);
|
||||
assert_eq!(get_messages.payload.before_message_id, 10);
|
||||
server
|
||||
.respond(
|
||||
get_messages.receipt(),
|
||||
proto::GetChannelMessagesResponse {
|
||||
done: true,
|
||||
messages: vec![
|
||||
proto::ChannelMessage {
|
||||
id: 8,
|
||||
body: "y".into(),
|
||||
timestamp: 998,
|
||||
sender_id: 5,
|
||||
nonce: Some(4.into()),
|
||||
},
|
||||
proto::ChannelMessage {
|
||||
id: 9,
|
||||
body: "z".into(),
|
||||
timestamp: 999,
|
||||
sender_id: 6,
|
||||
nonce: Some(5.into()),
|
||||
},
|
||||
],
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
channel.next_event(cx).await,
|
||||
ChannelEvent::MessagesUpdated {
|
||||
old_range: 0..0,
|
||||
new_count: 2,
|
||||
}
|
||||
);
|
||||
channel.read_with(cx, |channel, _| {
|
||||
assert_eq!(
|
||||
channel
|
||||
.messages_in_range(0..2)
|
||||
.map(|message| (message.sender.github_login.clone(), message.body.clone()))
|
||||
.collect::<Vec<_>>(),
|
||||
&[
|
||||
("nathansobo".into(), "y".into()),
|
||||
("maxbrunsfeld".into(), "z".into())
|
||||
]
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub mod test;
|
||||
|
||||
pub mod channel;
|
||||
pub mod http;
|
||||
pub mod telemetry;
|
||||
pub mod user;
|
||||
@@ -43,6 +44,7 @@ use thiserror::Error;
|
||||
use url::Url;
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
|
||||
pub use channel::*;
|
||||
pub use rpc::*;
|
||||
pub use user::*;
|
||||
|
||||
|
||||
@@ -150,6 +150,7 @@ impl UserStore {
|
||||
client.telemetry.set_authenticated_user_info(None, false);
|
||||
}
|
||||
|
||||
client.telemetry.report_event("sign in", Default::default());
|
||||
current_user_tx.send(user).await.ok();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"]
|
||||
default-run = "collab"
|
||||
edition = "2021"
|
||||
name = "collab"
|
||||
version = "0.2.6"
|
||||
version = "0.2.0"
|
||||
|
||||
[[bin]]
|
||||
name = "collab"
|
||||
@@ -50,9 +50,8 @@ tracing-log = "0.1.3"
|
||||
tracing-subscriber = { version = "0.3.11", features = ["env-filter", "json"] }
|
||||
|
||||
[dependencies.sqlx]
|
||||
git = "https://github.com/launchbadge/sqlx"
|
||||
rev = "4b7053807c705df312bcb9b6281e184bf7534eb3"
|
||||
features = ["runtime-tokio-rustls", "postgres", "json", "time", "uuid"]
|
||||
version = "0.6"
|
||||
features = ["runtime-tokio-rustls", "postgres", "time", "uuid"]
|
||||
|
||||
[dev-dependencies]
|
||||
collections = { path = "../collections", features = ["test-support"] }
|
||||
@@ -79,10 +78,5 @@ lazy_static = "1.4"
|
||||
serde_json = { version = "1.0", features = ["preserve_order"] }
|
||||
unindent = "0.1"
|
||||
|
||||
[dev-dependencies.sqlx]
|
||||
git = "https://github.com/launchbadge/sqlx"
|
||||
rev = "4b7053807c705df312bcb9b6281e184bf7534eb3"
|
||||
features = ["sqlite"]
|
||||
|
||||
[features]
|
||||
seed-support = ["clap", "lipsum", "reqwest"]
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
CREATE TABLE IF NOT EXISTS "users" (
|
||||
"id" INTEGER PRIMARY KEY,
|
||||
"github_login" VARCHAR,
|
||||
"admin" BOOLEAN,
|
||||
"email_address" VARCHAR(255) DEFAULT NULL,
|
||||
"invite_code" VARCHAR(64),
|
||||
"invite_count" INTEGER NOT NULL DEFAULT 0,
|
||||
"inviter_id" INTEGER REFERENCES users (id),
|
||||
"connected_once" BOOLEAN NOT NULL DEFAULT false,
|
||||
"created_at" TIMESTAMP NOT NULL DEFAULT now,
|
||||
"metrics_id" VARCHAR(255),
|
||||
"github_user_id" INTEGER
|
||||
);
|
||||
CREATE UNIQUE INDEX "index_users_github_login" ON "users" ("github_login");
|
||||
CREATE UNIQUE INDEX "index_invite_code_users" ON "users" ("invite_code");
|
||||
CREATE INDEX "index_users_on_email_address" ON "users" ("email_address");
|
||||
CREATE INDEX "index_users_on_github_user_id" ON "users" ("github_user_id");
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "access_tokens" (
|
||||
"id" INTEGER PRIMARY KEY,
|
||||
"user_id" INTEGER REFERENCES users (id),
|
||||
"hash" VARCHAR(128)
|
||||
);
|
||||
CREATE INDEX "index_access_tokens_user_id" ON "access_tokens" ("user_id");
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "contacts" (
|
||||
"id" INTEGER PRIMARY KEY,
|
||||
"user_id_a" INTEGER REFERENCES users (id) NOT NULL,
|
||||
"user_id_b" INTEGER REFERENCES users (id) NOT NULL,
|
||||
"a_to_b" BOOLEAN NOT NULL,
|
||||
"should_notify" BOOLEAN NOT NULL,
|
||||
"accepted" BOOLEAN NOT NULL
|
||||
);
|
||||
CREATE UNIQUE INDEX "index_contacts_user_ids" ON "contacts" ("user_id_a", "user_id_b");
|
||||
CREATE INDEX "index_contacts_user_id_b" ON "contacts" ("user_id_b");
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "projects" (
|
||||
"id" INTEGER PRIMARY KEY,
|
||||
"host_user_id" INTEGER REFERENCES users (id) NOT NULL,
|
||||
"unregistered" BOOLEAN NOT NULL DEFAULT false
|
||||
);
|
||||
@@ -1,2 +0,0 @@
|
||||
ALTER TABLE "signups"
|
||||
ADD "added_to_mailing_list" BOOLEAN NOT NULL DEFAULT FALSE;
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
auth,
|
||||
db::{Invite, NewUserParams, Signup, User, UserId, WaitlistSummary},
|
||||
db::{Invite, NewUserParams, ProjectId, Signup, User, UserId, WaitlistSummary},
|
||||
rpc::{self, ResultExt},
|
||||
AppState, Error, Result,
|
||||
};
|
||||
@@ -16,7 +16,9 @@ use axum::{
|
||||
};
|
||||
use axum_extra::response::ErasedJson;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use serde_json::json;
|
||||
use std::{sync::Arc, time::Duration};
|
||||
use time::OffsetDateTime;
|
||||
use tower::ServiceBuilder;
|
||||
use tracing::instrument;
|
||||
|
||||
@@ -30,6 +32,16 @@ pub fn routes(rpc_server: Arc<rpc::Server>, state: Arc<AppState>) -> Router<Body
|
||||
.route("/invite_codes/:code", get(get_user_for_invite_code))
|
||||
.route("/panic", post(trace_panic))
|
||||
.route("/rpc_server_snapshot", get(get_rpc_server_snapshot))
|
||||
.route(
|
||||
"/user_activity/summary",
|
||||
get(get_top_users_activity_summary),
|
||||
)
|
||||
.route(
|
||||
"/user_activity/timeline/:user_id",
|
||||
get(get_user_activity_timeline),
|
||||
)
|
||||
.route("/user_activity/counts", get(get_active_user_counts))
|
||||
.route("/project_metadata", get(get_project_metadata))
|
||||
.route("/signups", post(create_signup))
|
||||
.route("/signups_summary", get(get_waitlist_summary))
|
||||
.route("/user_invites", post(create_invite_from_code))
|
||||
@@ -271,6 +283,93 @@ async fn get_rpc_server_snapshot(
|
||||
Ok(ErasedJson::pretty(rpc_server.snapshot().await))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct TimePeriodParams {
|
||||
#[serde(with = "time::serde::iso8601")]
|
||||
start: OffsetDateTime,
|
||||
#[serde(with = "time::serde::iso8601")]
|
||||
end: OffsetDateTime,
|
||||
}
|
||||
|
||||
async fn get_top_users_activity_summary(
|
||||
Query(params): Query<TimePeriodParams>,
|
||||
Extension(app): Extension<Arc<AppState>>,
|
||||
) -> Result<ErasedJson> {
|
||||
let summary = app
|
||||
.db
|
||||
.get_top_users_activity_summary(params.start..params.end, 100)
|
||||
.await?;
|
||||
Ok(ErasedJson::pretty(summary))
|
||||
}
|
||||
|
||||
async fn get_user_activity_timeline(
|
||||
Path(user_id): Path<i32>,
|
||||
Query(params): Query<TimePeriodParams>,
|
||||
Extension(app): Extension<Arc<AppState>>,
|
||||
) -> Result<ErasedJson> {
|
||||
let summary = app
|
||||
.db
|
||||
.get_user_activity_timeline(params.start..params.end, UserId(user_id))
|
||||
.await?;
|
||||
Ok(ErasedJson::pretty(summary))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ActiveUserCountParams {
|
||||
#[serde(flatten)]
|
||||
period: TimePeriodParams,
|
||||
durations_in_minutes: String,
|
||||
#[serde(default)]
|
||||
only_collaborative: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ActiveUserSet {
|
||||
active_time_in_minutes: u64,
|
||||
user_count: usize,
|
||||
}
|
||||
|
||||
async fn get_active_user_counts(
|
||||
Query(params): Query<ActiveUserCountParams>,
|
||||
Extension(app): Extension<Arc<AppState>>,
|
||||
) -> Result<ErasedJson> {
|
||||
let durations_in_minutes = params.durations_in_minutes.split(',');
|
||||
let mut user_sets = Vec::new();
|
||||
for duration in durations_in_minutes {
|
||||
let duration = duration
|
||||
.parse()
|
||||
.map_err(|_| anyhow!("invalid duration: {duration}"))?;
|
||||
user_sets.push(ActiveUserSet {
|
||||
active_time_in_minutes: duration,
|
||||
user_count: app
|
||||
.db
|
||||
.get_active_user_count(
|
||||
params.period.start..params.period.end,
|
||||
Duration::from_secs(duration * 60),
|
||||
params.only_collaborative,
|
||||
)
|
||||
.await?,
|
||||
})
|
||||
}
|
||||
Ok(ErasedJson::pretty(user_sets))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct GetProjectMetadataParams {
|
||||
project_id: u64,
|
||||
}
|
||||
|
||||
async fn get_project_metadata(
|
||||
Query(params): Query<GetProjectMetadataParams>,
|
||||
Extension(app): Extension<Arc<AppState>>,
|
||||
) -> Result<ErasedJson> {
|
||||
let extensions = app
|
||||
.db
|
||||
.get_project_extensions(ProjectId::from_proto(params.project_id))
|
||||
.await?;
|
||||
Ok(ErasedJson::pretty(json!({ "extensions": extensions })))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct CreateAccessTokenQueryParams {
|
||||
public_key: String,
|
||||
@@ -338,7 +437,7 @@ async fn create_signup(
|
||||
Json(params): Json<Signup>,
|
||||
Extension(app): Extension<Arc<AppState>>,
|
||||
) -> Result<()> {
|
||||
app.db.create_signup(¶ms).await?;
|
||||
app.db.create_signup(params).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ pub async fn validate_header<B>(mut req: Request<B>, next: Next<B>) -> impl Into
|
||||
|
||||
const MAX_ACCESS_TOKENS_TO_STORE: usize = 8;
|
||||
|
||||
pub async fn create_access_token(db: &db::DefaultDb, user_id: UserId) -> Result<String> {
|
||||
pub async fn create_access_token(db: &dyn db::Db, user_id: UserId) -> Result<String> {
|
||||
let access_token = rpc::auth::random_token();
|
||||
let access_token_hash =
|
||||
hash_access_token(&access_token).context("failed to hash access token")?;
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
use collab::{Error, Result};
|
||||
use db::DefaultDb;
|
||||
use db::{Db, PostgresDb, UserId};
|
||||
use rand::prelude::*;
|
||||
use serde::{de::DeserializeOwned, Deserialize};
|
||||
use std::fmt::Write;
|
||||
use time::{Duration, OffsetDateTime};
|
||||
|
||||
#[allow(unused)]
|
||||
#[path = "../db.rs"]
|
||||
@@ -16,8 +18,9 @@ struct GitHubUser {
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let mut rng = StdRng::from_entropy();
|
||||
let database_url = std::env::var("DATABASE_URL").expect("missing DATABASE_URL env var");
|
||||
let db = DefaultDb::new(&database_url, 5)
|
||||
let db = PostgresDb::new(&database_url, 5)
|
||||
.await
|
||||
.expect("failed to connect to postgres database");
|
||||
let github_token = std::env::var("GITHUB_TOKEN").expect("missing GITHUB_TOKEN env var");
|
||||
@@ -61,14 +64,16 @@ async fn main() {
|
||||
}
|
||||
}
|
||||
|
||||
let mut zed_user_ids = Vec::<UserId>::new();
|
||||
for (github_user, admin) in zed_users {
|
||||
if db
|
||||
if let Some(user) = db
|
||||
.get_user_by_github_account(&github_user.login, Some(github_user.id))
|
||||
.await
|
||||
.expect("failed to fetch user")
|
||||
.is_none()
|
||||
{
|
||||
if let Some(email) = &github_user.email {
|
||||
zed_user_ids.push(user.id);
|
||||
} else if let Some(email) = &github_user.email {
|
||||
zed_user_ids.push(
|
||||
db.create_user(
|
||||
email,
|
||||
admin,
|
||||
@@ -79,8 +84,11 @@ async fn main() {
|
||||
},
|
||||
)
|
||||
.await
|
||||
.expect("failed to insert user");
|
||||
} else if admin {
|
||||
.expect("failed to insert user")
|
||||
.user_id,
|
||||
);
|
||||
} else if admin {
|
||||
zed_user_ids.push(
|
||||
db.create_user(
|
||||
&format!("{}@zed.dev", github_user.login),
|
||||
admin,
|
||||
@@ -91,10 +99,62 @@ async fn main() {
|
||||
},
|
||||
)
|
||||
.await
|
||||
.expect("failed to insert user");
|
||||
}
|
||||
.expect("failed to insert user")
|
||||
.user_id,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let zed_org_id = if let Some(org) = db
|
||||
.find_org_by_slug("zed")
|
||||
.await
|
||||
.expect("failed to fetch org")
|
||||
{
|
||||
org.id
|
||||
} else {
|
||||
db.create_org("Zed", "zed")
|
||||
.await
|
||||
.expect("failed to insert org")
|
||||
};
|
||||
|
||||
let general_channel_id = if let Some(channel) = db
|
||||
.get_org_channels(zed_org_id)
|
||||
.await
|
||||
.expect("failed to fetch channels")
|
||||
.iter()
|
||||
.find(|c| c.name == "General")
|
||||
{
|
||||
channel.id
|
||||
} else {
|
||||
let channel_id = db
|
||||
.create_org_channel(zed_org_id, "General")
|
||||
.await
|
||||
.expect("failed to insert channel");
|
||||
|
||||
let now = OffsetDateTime::now_utc();
|
||||
let max_seconds = Duration::days(100).as_seconds_f64();
|
||||
let mut timestamps = (0..1000)
|
||||
.map(|_| now - Duration::seconds_f64(rng.gen_range(0_f64..=max_seconds)))
|
||||
.collect::<Vec<_>>();
|
||||
timestamps.sort();
|
||||
for timestamp in timestamps {
|
||||
let sender_id = *zed_user_ids.choose(&mut rng).unwrap();
|
||||
let body = lipsum::lipsum_words(rng.gen_range(1..=50));
|
||||
db.create_channel_message(channel_id, sender_id, &body, timestamp, rng.gen())
|
||||
.await
|
||||
.expect("failed to insert message");
|
||||
}
|
||||
channel_id
|
||||
};
|
||||
|
||||
for user_id in zed_user_ids {
|
||||
db.add_org_member(zed_org_id, user_id, true)
|
||||
.await
|
||||
.expect("failed to insert org membership");
|
||||
db.add_channel_member(general_channel_id, user_id, true)
|
||||
.await
|
||||
.expect("failed to insert channel membership");
|
||||
}
|
||||
}
|
||||
|
||||
async fn fetch_github<T: DeserializeOwned>(
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,14 +1,14 @@
|
||||
use crate::{
|
||||
db::{NewUserParams, ProjectId, SqliteTestDb as TestDb, UserId},
|
||||
rpc::{Executor, Server},
|
||||
db::{NewUserParams, ProjectId, TestDb, UserId},
|
||||
rpc::{Executor, Server, Store},
|
||||
AppState,
|
||||
};
|
||||
use ::rpc::Peer;
|
||||
use anyhow::anyhow;
|
||||
use call::{room, ActiveCall, ParticipantLocation, Room};
|
||||
use client::{
|
||||
self, test::FakeHttpClient, Client, Connection, Credentials, EstablishConnectionError, PeerId,
|
||||
User, UserStore, RECEIVE_TIMEOUT,
|
||||
self, test::FakeHttpClient, Channel, ChannelDetails, ChannelList, Client, Connection,
|
||||
Credentials, EstablishConnectionError, PeerId, User, UserStore, RECEIVE_TIMEOUT,
|
||||
};
|
||||
use collections::{BTreeMap, HashMap, HashSet};
|
||||
use editor::{
|
||||
@@ -16,7 +16,10 @@ use editor::{
|
||||
ToggleCodeActions, Undo,
|
||||
};
|
||||
use fs::{FakeFs, Fs as _, HomeDir, LineEnding};
|
||||
use futures::{channel::oneshot, Future, StreamExt as _};
|
||||
use futures::{
|
||||
channel::{mpsc, oneshot},
|
||||
Future, StreamExt as _,
|
||||
};
|
||||
use gpui::{
|
||||
executor::{self, Deterministic},
|
||||
geometry::vector::vec2f,
|
||||
@@ -36,6 +39,7 @@ use project::{
|
||||
use rand::prelude::*;
|
||||
use serde_json::json;
|
||||
use settings::{Formatter, Settings};
|
||||
use sqlx::types::time::OffsetDateTime;
|
||||
use std::{
|
||||
cell::{Cell, RefCell},
|
||||
env, mem,
|
||||
@@ -69,10 +73,7 @@ async fn test_basic_calls(
|
||||
cx_c: &mut TestAppContext,
|
||||
) {
|
||||
deterministic.forbid_parking();
|
||||
let mut server = TestServer::start(cx_a.background()).await;
|
||||
|
||||
let start = std::time::Instant::now();
|
||||
|
||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
let client_c = server.create_client(cx_c, "user_c").await;
|
||||
@@ -258,8 +259,6 @@ async fn test_basic_calls(
|
||||
pending: Default::default()
|
||||
}
|
||||
);
|
||||
|
||||
eprintln!("finished test {:?}", start.elapsed());
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
@@ -272,7 +271,7 @@ async fn test_room_uniqueness(
|
||||
cx_c: &mut TestAppContext,
|
||||
) {
|
||||
deterministic.forbid_parking();
|
||||
let mut server = TestServer::start(cx_a.background()).await;
|
||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let _client_a2 = server.create_client(cx_a2, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
@@ -377,7 +376,7 @@ async fn test_leaving_room_on_disconnection(
|
||||
cx_b: &mut TestAppContext,
|
||||
) {
|
||||
deterministic.forbid_parking();
|
||||
let mut server = TestServer::start(cx_a.background()).await;
|
||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
server
|
||||
@@ -506,7 +505,7 @@ async fn test_calls_on_multiple_connections(
|
||||
cx_b2: &mut TestAppContext,
|
||||
) {
|
||||
deterministic.forbid_parking();
|
||||
let mut server = TestServer::start(cx_a.background()).await;
|
||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b1 = server.create_client(cx_b1, "user_b").await;
|
||||
let client_b2 = server.create_client(cx_b2, "user_b").await;
|
||||
@@ -655,7 +654,7 @@ async fn test_share_project(
|
||||
) {
|
||||
deterministic.forbid_parking();
|
||||
let (_, window_b) = cx_b.add_window(|_| EmptyView);
|
||||
let mut server = TestServer::start(cx_a.background()).await;
|
||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
let client_c = server.create_client(cx_c, "user_c").await;
|
||||
@@ -792,7 +791,7 @@ async fn test_unshare_project(
|
||||
cx_c: &mut TestAppContext,
|
||||
) {
|
||||
deterministic.forbid_parking();
|
||||
let mut server = TestServer::start(cx_a.background()).await;
|
||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
let client_c = server.create_client(cx_c, "user_c").await;
|
||||
@@ -875,7 +874,7 @@ async fn test_host_disconnect(
|
||||
) {
|
||||
cx_b.update(editor::init);
|
||||
deterministic.forbid_parking();
|
||||
let mut server = TestServer::start(cx_a.background()).await;
|
||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
let client_c = server.create_client(cx_c, "user_c").await;
|
||||
@@ -909,7 +908,7 @@ async fn test_host_disconnect(
|
||||
cx_b.add_window(|cx| Workspace::new(project_b.clone(), |_, _| unimplemented!(), cx));
|
||||
let editor_b = workspace_b
|
||||
.update(cx_b, |workspace, cx| {
|
||||
workspace.open_path((worktree_id, "b.txt"), None, true, cx)
|
||||
workspace.open_path((worktree_id, "b.txt"), true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
@@ -980,7 +979,7 @@ async fn test_active_call_events(
|
||||
cx_b: &mut TestAppContext,
|
||||
) {
|
||||
deterministic.forbid_parking();
|
||||
let mut server = TestServer::start(cx_a.background()).await;
|
||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
client_a.fs.insert_tree("/a", json!({})).await;
|
||||
@@ -1069,7 +1068,7 @@ async fn test_room_location(
|
||||
cx_b: &mut TestAppContext,
|
||||
) {
|
||||
deterministic.forbid_parking();
|
||||
let mut server = TestServer::start(cx_a.background()).await;
|
||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
client_a.fs.insert_tree("/a", json!({})).await;
|
||||
@@ -1235,7 +1234,7 @@ async fn test_propagate_saves_and_fs_changes(
|
||||
cx_c: &mut TestAppContext,
|
||||
) {
|
||||
deterministic.forbid_parking();
|
||||
let mut server = TestServer::start(cx_a.background()).await;
|
||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
let client_c = server.create_client(cx_c, "user_c").await;
|
||||
@@ -1410,7 +1409,7 @@ async fn test_git_diff_base_change(
|
||||
cx_b: &mut TestAppContext,
|
||||
) {
|
||||
executor.forbid_parking();
|
||||
let mut server = TestServer::start(cx_a.background()).await;
|
||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
server
|
||||
@@ -1662,7 +1661,7 @@ async fn test_fs_operations(
|
||||
cx_b: &mut TestAppContext,
|
||||
) {
|
||||
executor.forbid_parking();
|
||||
let mut server = TestServer::start(cx_a.background()).await;
|
||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
server
|
||||
@@ -1928,7 +1927,7 @@ async fn test_fs_operations(
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_buffer_conflict_after_save(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
|
||||
cx_a.foreground().forbid_parking();
|
||||
let mut server = TestServer::start(cx_a.background()).await;
|
||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
server
|
||||
@@ -1982,7 +1981,7 @@ async fn test_buffer_conflict_after_save(cx_a: &mut TestAppContext, cx_b: &mut T
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_buffer_reloading(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
|
||||
cx_a.foreground().forbid_parking();
|
||||
let mut server = TestServer::start(cx_a.background()).await;
|
||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
server
|
||||
@@ -2041,7 +2040,7 @@ async fn test_editing_while_guest_opens_buffer(
|
||||
cx_b: &mut TestAppContext,
|
||||
) {
|
||||
cx_a.foreground().forbid_parking();
|
||||
let mut server = TestServer::start(cx_a.background()).await;
|
||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
server
|
||||
@@ -2088,7 +2087,7 @@ async fn test_leaving_worktree_while_opening_buffer(
|
||||
cx_b: &mut TestAppContext,
|
||||
) {
|
||||
cx_a.foreground().forbid_parking();
|
||||
let mut server = TestServer::start(cx_a.background()).await;
|
||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
server
|
||||
@@ -2133,7 +2132,7 @@ async fn test_canceling_buffer_opening(
|
||||
) {
|
||||
deterministic.forbid_parking();
|
||||
|
||||
let mut server = TestServer::start(cx_a.background()).await;
|
||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
server
|
||||
@@ -2184,7 +2183,7 @@ async fn test_leaving_project(
|
||||
cx_c: &mut TestAppContext,
|
||||
) {
|
||||
deterministic.forbid_parking();
|
||||
let mut server = TestServer::start(cx_a.background()).await;
|
||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
let client_c = server.create_client(cx_c, "user_c").await;
|
||||
@@ -2317,7 +2316,7 @@ async fn test_collaborating_with_diagnostics(
|
||||
cx_c: &mut TestAppContext,
|
||||
) {
|
||||
deterministic.forbid_parking();
|
||||
let mut server = TestServer::start(cx_a.background()).await;
|
||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
let client_c = server.create_client(cx_c, "user_c").await;
|
||||
@@ -2582,7 +2581,7 @@ async fn test_collaborating_with_diagnostics(
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
|
||||
cx_a.foreground().forbid_parking();
|
||||
let mut server = TestServer::start(cx_a.background()).await;
|
||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
server
|
||||
@@ -2756,7 +2755,7 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_reloading_buffer_manually(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
|
||||
cx_a.foreground().forbid_parking();
|
||||
let mut server = TestServer::start(cx_a.background()).await;
|
||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
server
|
||||
@@ -2849,7 +2848,7 @@ async fn test_reloading_buffer_manually(cx_a: &mut TestAppContext, cx_b: &mut Te
|
||||
async fn test_formatting_buffer(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
|
||||
use project::FormatTrigger;
|
||||
|
||||
let mut server = TestServer::start(cx_a.background()).await;
|
||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
server
|
||||
@@ -2950,7 +2949,7 @@ async fn test_formatting_buffer(cx_a: &mut TestAppContext, cx_b: &mut TestAppCon
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_definition(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
|
||||
cx_a.foreground().forbid_parking();
|
||||
let mut server = TestServer::start(cx_a.background()).await;
|
||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
server
|
||||
@@ -3094,7 +3093,7 @@ async fn test_definition(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_references(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
|
||||
cx_a.foreground().forbid_parking();
|
||||
let mut server = TestServer::start(cx_a.background()).await;
|
||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
server
|
||||
@@ -3195,7 +3194,7 @@ async fn test_references(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_project_search(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
|
||||
cx_a.foreground().forbid_parking();
|
||||
let mut server = TestServer::start(cx_a.background()).await;
|
||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
server
|
||||
@@ -3274,7 +3273,7 @@ async fn test_project_search(cx_a: &mut TestAppContext, cx_b: &mut TestAppContex
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_document_highlights(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
|
||||
cx_a.foreground().forbid_parking();
|
||||
let mut server = TestServer::start(cx_a.background()).await;
|
||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
server
|
||||
@@ -3376,7 +3375,7 @@ async fn test_document_highlights(cx_a: &mut TestAppContext, cx_b: &mut TestAppC
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_lsp_hover(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
|
||||
cx_a.foreground().forbid_parking();
|
||||
let mut server = TestServer::start(cx_a.background()).await;
|
||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
server
|
||||
@@ -3479,7 +3478,7 @@ async fn test_lsp_hover(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_project_symbols(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
|
||||
cx_a.foreground().forbid_parking();
|
||||
let mut server = TestServer::start(cx_a.background()).await;
|
||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
server
|
||||
@@ -3587,7 +3586,7 @@ async fn test_open_buffer_while_getting_definition_pointing_to_it(
|
||||
mut rng: StdRng,
|
||||
) {
|
||||
cx_a.foreground().forbid_parking();
|
||||
let mut server = TestServer::start(cx_a.background()).await;
|
||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
server
|
||||
@@ -3663,7 +3662,7 @@ async fn test_collaborating_with_code_actions(
|
||||
) {
|
||||
cx_a.foreground().forbid_parking();
|
||||
cx_b.update(editor::init);
|
||||
let mut server = TestServer::start(cx_a.background()).await;
|
||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
server
|
||||
@@ -3705,7 +3704,7 @@ async fn test_collaborating_with_code_actions(
|
||||
cx_b.add_window(|cx| Workspace::new(project_b.clone(), |_, _| unimplemented!(), cx));
|
||||
let editor_b = workspace_b
|
||||
.update(cx_b, |workspace, cx| {
|
||||
workspace.open_path((worktree_id, "main.rs"), None, true, cx)
|
||||
workspace.open_path((worktree_id, "main.rs"), true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
@@ -3874,7 +3873,7 @@ async fn test_collaborating_with_code_actions(
|
||||
async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
|
||||
cx_a.foreground().forbid_parking();
|
||||
cx_b.update(editor::init);
|
||||
let mut server = TestServer::start(cx_a.background()).await;
|
||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
server
|
||||
@@ -3926,7 +3925,7 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T
|
||||
cx_b.add_window(|cx| Workspace::new(project_b.clone(), |_, _| unimplemented!(), cx));
|
||||
let editor_b = workspace_b
|
||||
.update(cx_b, |workspace, cx| {
|
||||
workspace.open_path((worktree_id, "one.rs"), None, true, cx)
|
||||
workspace.open_path((worktree_id, "one.rs"), true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
@@ -4066,7 +4065,7 @@ async fn test_language_server_statuses(
|
||||
deterministic.forbid_parking();
|
||||
|
||||
cx_b.update(editor::init);
|
||||
let mut server = TestServer::start(cx_a.background()).await;
|
||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
server
|
||||
@@ -4170,6 +4169,415 @@ async fn test_language_server_statuses(
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_basic_chat(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
|
||||
cx_a.foreground().forbid_parking();
|
||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
|
||||
// Create an org that includes these 2 users.
|
||||
let db = &server.app_state.db;
|
||||
let org_id = db.create_org("Test Org", "test-org").await.unwrap();
|
||||
db.add_org_member(org_id, client_a.current_user_id(cx_a), false)
|
||||
.await
|
||||
.unwrap();
|
||||
db.add_org_member(org_id, client_b.current_user_id(cx_b), false)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Create a channel that includes all the users.
|
||||
let channel_id = db.create_org_channel(org_id, "test-channel").await.unwrap();
|
||||
db.add_channel_member(channel_id, client_a.current_user_id(cx_a), false)
|
||||
.await
|
||||
.unwrap();
|
||||
db.add_channel_member(channel_id, client_b.current_user_id(cx_b), false)
|
||||
.await
|
||||
.unwrap();
|
||||
db.create_channel_message(
|
||||
channel_id,
|
||||
client_b.current_user_id(cx_b),
|
||||
"hello A, it's B.",
|
||||
OffsetDateTime::now_utc(),
|
||||
1,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let channels_a =
|
||||
cx_a.add_model(|cx| ChannelList::new(client_a.user_store.clone(), client_a.clone(), cx));
|
||||
channels_a
|
||||
.condition(cx_a, |list, _| list.available_channels().is_some())
|
||||
.await;
|
||||
channels_a.read_with(cx_a, |list, _| {
|
||||
assert_eq!(
|
||||
list.available_channels().unwrap(),
|
||||
&[ChannelDetails {
|
||||
id: channel_id.to_proto(),
|
||||
name: "test-channel".to_string()
|
||||
}]
|
||||
)
|
||||
});
|
||||
let channel_a = channels_a.update(cx_a, |this, cx| {
|
||||
this.get_channel(channel_id.to_proto(), cx).unwrap()
|
||||
});
|
||||
channel_a.read_with(cx_a, |channel, _| assert!(channel.messages().is_empty()));
|
||||
channel_a
|
||||
.condition(cx_a, |channel, _| {
|
||||
channel_messages(channel)
|
||||
== [("user_b".to_string(), "hello A, it's B.".to_string(), false)]
|
||||
})
|
||||
.await;
|
||||
|
||||
let channels_b =
|
||||
cx_b.add_model(|cx| ChannelList::new(client_b.user_store.clone(), client_b.clone(), cx));
|
||||
channels_b
|
||||
.condition(cx_b, |list, _| list.available_channels().is_some())
|
||||
.await;
|
||||
channels_b.read_with(cx_b, |list, _| {
|
||||
assert_eq!(
|
||||
list.available_channels().unwrap(),
|
||||
&[ChannelDetails {
|
||||
id: channel_id.to_proto(),
|
||||
name: "test-channel".to_string()
|
||||
}]
|
||||
)
|
||||
});
|
||||
|
||||
let channel_b = channels_b.update(cx_b, |this, cx| {
|
||||
this.get_channel(channel_id.to_proto(), cx).unwrap()
|
||||
});
|
||||
channel_b.read_with(cx_b, |channel, _| assert!(channel.messages().is_empty()));
|
||||
channel_b
|
||||
.condition(cx_b, |channel, _| {
|
||||
channel_messages(channel)
|
||||
== [("user_b".to_string(), "hello A, it's B.".to_string(), false)]
|
||||
})
|
||||
.await;
|
||||
|
||||
channel_a
|
||||
.update(cx_a, |channel, cx| {
|
||||
channel
|
||||
.send_message("oh, hi B.".to_string(), cx)
|
||||
.unwrap()
|
||||
.detach();
|
||||
let task = channel.send_message("sup".to_string(), cx).unwrap();
|
||||
assert_eq!(
|
||||
channel_messages(channel),
|
||||
&[
|
||||
("user_b".to_string(), "hello A, it's B.".to_string(), false),
|
||||
("user_a".to_string(), "oh, hi B.".to_string(), true),
|
||||
("user_a".to_string(), "sup".to_string(), true)
|
||||
]
|
||||
);
|
||||
task
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
channel_b
|
||||
.condition(cx_b, |channel, _| {
|
||||
channel_messages(channel)
|
||||
== [
|
||||
("user_b".to_string(), "hello A, it's B.".to_string(), false),
|
||||
("user_a".to_string(), "oh, hi B.".to_string(), false),
|
||||
("user_a".to_string(), "sup".to_string(), false),
|
||||
]
|
||||
})
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
server
|
||||
.store()
|
||||
.await
|
||||
.channel(channel_id)
|
||||
.unwrap()
|
||||
.connection_ids
|
||||
.len(),
|
||||
2
|
||||
);
|
||||
cx_b.update(|_| drop(channel_b));
|
||||
server
|
||||
.condition(|state| state.channel(channel_id).unwrap().connection_ids.len() == 1)
|
||||
.await;
|
||||
|
||||
cx_a.update(|_| drop(channel_a));
|
||||
server
|
||||
.condition(|state| state.channel(channel_id).is_none())
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_chat_message_validation(cx_a: &mut TestAppContext) {
|
||||
cx_a.foreground().forbid_parking();
|
||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
|
||||
let db = &server.app_state.db;
|
||||
let org_id = db.create_org("Test Org", "test-org").await.unwrap();
|
||||
let channel_id = db.create_org_channel(org_id, "test-channel").await.unwrap();
|
||||
db.add_org_member(org_id, client_a.current_user_id(cx_a), false)
|
||||
.await
|
||||
.unwrap();
|
||||
db.add_channel_member(channel_id, client_a.current_user_id(cx_a), false)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let channels_a =
|
||||
cx_a.add_model(|cx| ChannelList::new(client_a.user_store.clone(), client_a.clone(), cx));
|
||||
channels_a
|
||||
.condition(cx_a, |list, _| list.available_channels().is_some())
|
||||
.await;
|
||||
let channel_a = channels_a.update(cx_a, |this, cx| {
|
||||
this.get_channel(channel_id.to_proto(), cx).unwrap()
|
||||
});
|
||||
|
||||
// Messages aren't allowed to be too long.
|
||||
channel_a
|
||||
.update(cx_a, |channel, cx| {
|
||||
let long_body = "this is long.\n".repeat(1024);
|
||||
channel.send_message(long_body, cx).unwrap()
|
||||
})
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
// Messages aren't allowed to be blank.
|
||||
channel_a.update(cx_a, |channel, cx| {
|
||||
channel.send_message(String::new(), cx).unwrap_err()
|
||||
});
|
||||
|
||||
// Leading and trailing whitespace are trimmed.
|
||||
channel_a
|
||||
.update(cx_a, |channel, cx| {
|
||||
channel
|
||||
.send_message("\n surrounded by whitespace \n".to_string(), cx)
|
||||
.unwrap()
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
db.get_channel_messages(channel_id, 10, None)
|
||||
.await
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|m| &m.body)
|
||||
.collect::<Vec<_>>(),
|
||||
&["surrounded by whitespace"]
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_chat_reconnection(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
|
||||
cx_a.foreground().forbid_parking();
|
||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
|
||||
let mut status_b = client_b.status();
|
||||
|
||||
// Create an org that includes these 2 users.
|
||||
let db = &server.app_state.db;
|
||||
let org_id = db.create_org("Test Org", "test-org").await.unwrap();
|
||||
db.add_org_member(org_id, client_a.current_user_id(cx_a), false)
|
||||
.await
|
||||
.unwrap();
|
||||
db.add_org_member(org_id, client_b.current_user_id(cx_b), false)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Create a channel that includes all the users.
|
||||
let channel_id = db.create_org_channel(org_id, "test-channel").await.unwrap();
|
||||
db.add_channel_member(channel_id, client_a.current_user_id(cx_a), false)
|
||||
.await
|
||||
.unwrap();
|
||||
db.add_channel_member(channel_id, client_b.current_user_id(cx_b), false)
|
||||
.await
|
||||
.unwrap();
|
||||
db.create_channel_message(
|
||||
channel_id,
|
||||
client_b.current_user_id(cx_b),
|
||||
"hello A, it's B.",
|
||||
OffsetDateTime::now_utc(),
|
||||
2,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let channels_a =
|
||||
cx_a.add_model(|cx| ChannelList::new(client_a.user_store.clone(), client_a.clone(), cx));
|
||||
channels_a
|
||||
.condition(cx_a, |list, _| list.available_channels().is_some())
|
||||
.await;
|
||||
|
||||
channels_a.read_with(cx_a, |list, _| {
|
||||
assert_eq!(
|
||||
list.available_channels().unwrap(),
|
||||
&[ChannelDetails {
|
||||
id: channel_id.to_proto(),
|
||||
name: "test-channel".to_string()
|
||||
}]
|
||||
)
|
||||
});
|
||||
let channel_a = channels_a.update(cx_a, |this, cx| {
|
||||
this.get_channel(channel_id.to_proto(), cx).unwrap()
|
||||
});
|
||||
channel_a.read_with(cx_a, |channel, _| assert!(channel.messages().is_empty()));
|
||||
channel_a
|
||||
.condition(cx_a, |channel, _| {
|
||||
channel_messages(channel)
|
||||
== [("user_b".to_string(), "hello A, it's B.".to_string(), false)]
|
||||
})
|
||||
.await;
|
||||
|
||||
let channels_b =
|
||||
cx_b.add_model(|cx| ChannelList::new(client_b.user_store.clone(), client_b.clone(), cx));
|
||||
channels_b
|
||||
.condition(cx_b, |list, _| list.available_channels().is_some())
|
||||
.await;
|
||||
channels_b.read_with(cx_b, |list, _| {
|
||||
assert_eq!(
|
||||
list.available_channels().unwrap(),
|
||||
&[ChannelDetails {
|
||||
id: channel_id.to_proto(),
|
||||
name: "test-channel".to_string()
|
||||
}]
|
||||
)
|
||||
});
|
||||
|
||||
let channel_b = channels_b.update(cx_b, |this, cx| {
|
||||
this.get_channel(channel_id.to_proto(), cx).unwrap()
|
||||
});
|
||||
channel_b.read_with(cx_b, |channel, _| assert!(channel.messages().is_empty()));
|
||||
channel_b
|
||||
.condition(cx_b, |channel, _| {
|
||||
channel_messages(channel)
|
||||
== [("user_b".to_string(), "hello A, it's B.".to_string(), false)]
|
||||
})
|
||||
.await;
|
||||
|
||||
// Disconnect client B, ensuring we can still access its cached channel data.
|
||||
server.forbid_connections();
|
||||
server.disconnect_client(client_b.peer_id().unwrap());
|
||||
cx_b.foreground().advance_clock(rpc::RECEIVE_TIMEOUT);
|
||||
while !matches!(
|
||||
status_b.next().await,
|
||||
Some(client::Status::ReconnectionError { .. })
|
||||
) {}
|
||||
|
||||
channels_b.read_with(cx_b, |channels, _| {
|
||||
assert_eq!(
|
||||
channels.available_channels().unwrap(),
|
||||
[ChannelDetails {
|
||||
id: channel_id.to_proto(),
|
||||
name: "test-channel".to_string()
|
||||
}]
|
||||
)
|
||||
});
|
||||
channel_b.read_with(cx_b, |channel, _| {
|
||||
assert_eq!(
|
||||
channel_messages(channel),
|
||||
[("user_b".to_string(), "hello A, it's B.".to_string(), false)]
|
||||
)
|
||||
});
|
||||
|
||||
// Send a message from client B while it is disconnected.
|
||||
channel_b
|
||||
.update(cx_b, |channel, cx| {
|
||||
let task = channel
|
||||
.send_message("can you see this?".to_string(), cx)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
channel_messages(channel),
|
||||
&[
|
||||
("user_b".to_string(), "hello A, it's B.".to_string(), false),
|
||||
("user_b".to_string(), "can you see this?".to_string(), true)
|
||||
]
|
||||
);
|
||||
task
|
||||
})
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
// Send a message from client A while B is disconnected.
|
||||
channel_a
|
||||
.update(cx_a, |channel, cx| {
|
||||
channel
|
||||
.send_message("oh, hi B.".to_string(), cx)
|
||||
.unwrap()
|
||||
.detach();
|
||||
let task = channel.send_message("sup".to_string(), cx).unwrap();
|
||||
assert_eq!(
|
||||
channel_messages(channel),
|
||||
&[
|
||||
("user_b".to_string(), "hello A, it's B.".to_string(), false),
|
||||
("user_a".to_string(), "oh, hi B.".to_string(), true),
|
||||
("user_a".to_string(), "sup".to_string(), true)
|
||||
]
|
||||
);
|
||||
task
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Give client B a chance to reconnect.
|
||||
server.allow_connections();
|
||||
cx_b.foreground().advance_clock(Duration::from_secs(10));
|
||||
|
||||
// Verify that B sees the new messages upon reconnection, as well as the message client B
|
||||
// sent while offline.
|
||||
channel_b
|
||||
.condition(cx_b, |channel, _| {
|
||||
channel_messages(channel)
|
||||
== [
|
||||
("user_b".to_string(), "hello A, it's B.".to_string(), false),
|
||||
("user_a".to_string(), "oh, hi B.".to_string(), false),
|
||||
("user_a".to_string(), "sup".to_string(), false),
|
||||
("user_b".to_string(), "can you see this?".to_string(), false),
|
||||
]
|
||||
})
|
||||
.await;
|
||||
|
||||
// Ensure client A and B can communicate normally after reconnection.
|
||||
channel_a
|
||||
.update(cx_a, |channel, cx| {
|
||||
channel.send_message("you online?".to_string(), cx).unwrap()
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
channel_b
|
||||
.condition(cx_b, |channel, _| {
|
||||
channel_messages(channel)
|
||||
== [
|
||||
("user_b".to_string(), "hello A, it's B.".to_string(), false),
|
||||
("user_a".to_string(), "oh, hi B.".to_string(), false),
|
||||
("user_a".to_string(), "sup".to_string(), false),
|
||||
("user_b".to_string(), "can you see this?".to_string(), false),
|
||||
("user_a".to_string(), "you online?".to_string(), false),
|
||||
]
|
||||
})
|
||||
.await;
|
||||
|
||||
channel_b
|
||||
.update(cx_b, |channel, cx| {
|
||||
channel.send_message("yep".to_string(), cx).unwrap()
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
channel_a
|
||||
.condition(cx_a, |channel, _| {
|
||||
channel_messages(channel)
|
||||
== [
|
||||
("user_b".to_string(), "hello A, it's B.".to_string(), false),
|
||||
("user_a".to_string(), "oh, hi B.".to_string(), false),
|
||||
("user_a".to_string(), "sup".to_string(), false),
|
||||
("user_b".to_string(), "can you see this?".to_string(), false),
|
||||
("user_a".to_string(), "you online?".to_string(), false),
|
||||
("user_b".to_string(), "yep".to_string(), false),
|
||||
]
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_contacts(
|
||||
deterministic: Arc<Deterministic>,
|
||||
@@ -4178,7 +4586,7 @@ async fn test_contacts(
|
||||
cx_c: &mut TestAppContext,
|
||||
) {
|
||||
cx_a.foreground().forbid_parking();
|
||||
let mut server = TestServer::start(cx_a.background()).await;
|
||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
let client_c = server.create_client(cx_c, "user_c").await;
|
||||
@@ -4504,7 +4912,7 @@ async fn test_contact_requests(
|
||||
cx_a.foreground().forbid_parking();
|
||||
|
||||
// Connect to a server as 3 clients.
|
||||
let mut server = TestServer::start(cx_a.background()).await;
|
||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_a2 = server.create_client(cx_a2, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
@@ -4685,7 +5093,7 @@ async fn test_following(
|
||||
cx_a.update(editor::init);
|
||||
cx_b.update(editor::init);
|
||||
|
||||
let mut server = TestServer::start(cx_a.background()).await;
|
||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
server
|
||||
@@ -4726,7 +5134,7 @@ async fn test_following(
|
||||
let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
|
||||
let editor_a1 = workspace_a
|
||||
.update(cx_a, |workspace, cx| {
|
||||
workspace.open_path((worktree_id, "1.txt"), None, true, cx)
|
||||
workspace.open_path((worktree_id, "1.txt"), true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
@@ -4734,7 +5142,7 @@ async fn test_following(
|
||||
.unwrap();
|
||||
let editor_a2 = workspace_a
|
||||
.update(cx_a, |workspace, cx| {
|
||||
workspace.open_path((worktree_id, "2.txt"), None, true, cx)
|
||||
workspace.open_path((worktree_id, "2.txt"), true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
@@ -4745,7 +5153,7 @@ async fn test_following(
|
||||
let workspace_b = client_b.build_workspace(&project_b, cx_b);
|
||||
let editor_b1 = workspace_b
|
||||
.update(cx_b, |workspace, cx| {
|
||||
workspace.open_path((worktree_id, "1.txt"), None, true, cx)
|
||||
workspace.open_path((worktree_id, "1.txt"), true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
@@ -4959,7 +5367,7 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T
|
||||
cx_a.update(editor::init);
|
||||
cx_b.update(editor::init);
|
||||
|
||||
let mut server = TestServer::start(cx_a.background()).await;
|
||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
server
|
||||
@@ -5003,7 +5411,7 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T
|
||||
let pane_a1 = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
|
||||
let _editor_a1 = workspace_a
|
||||
.update(cx_a, |workspace, cx| {
|
||||
workspace.open_path((worktree_id, "1.txt"), None, true, cx)
|
||||
workspace.open_path((worktree_id, "1.txt"), true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
@@ -5015,7 +5423,7 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T
|
||||
let pane_b1 = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
|
||||
let _editor_b1 = workspace_b
|
||||
.update(cx_b, |workspace, cx| {
|
||||
workspace.open_path((worktree_id, "2.txt"), None, true, cx)
|
||||
workspace.open_path((worktree_id, "2.txt"), true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
@@ -5066,7 +5474,7 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T
|
||||
|
||||
workspace_a
|
||||
.update(cx_a, |workspace, cx| {
|
||||
workspace.open_path((worktree_id, "3.txt"), None, true, cx)
|
||||
workspace.open_path((worktree_id, "3.txt"), true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -5077,7 +5485,7 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T
|
||||
workspace_b
|
||||
.update(cx_b, |workspace, cx| {
|
||||
assert_eq!(*workspace.active_pane(), pane_b1);
|
||||
workspace.open_path((worktree_id, "4.txt"), None, true, cx)
|
||||
workspace.open_path((worktree_id, "4.txt"), true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -5137,7 +5545,7 @@ async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppCont
|
||||
cx_b.update(editor::init);
|
||||
|
||||
// 2 clients connect to a server.
|
||||
let mut server = TestServer::start(cx_a.background()).await;
|
||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
server
|
||||
@@ -5178,7 +5586,7 @@ async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppCont
|
||||
let workspace_a = client_a.build_workspace(&project_a, cx_a);
|
||||
let _editor_a1 = workspace_a
|
||||
.update(cx_a, |workspace, cx| {
|
||||
workspace.open_path((worktree_id, "1.txt"), None, true, cx)
|
||||
workspace.open_path((worktree_id, "1.txt"), true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
@@ -5291,7 +5699,7 @@ async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppCont
|
||||
// When client B activates a different item in the original pane, it automatically stops following client A.
|
||||
workspace_b
|
||||
.update(cx_b, |workspace, cx| {
|
||||
workspace.open_path((worktree_id, "2.txt"), None, true, cx)
|
||||
workspace.open_path((worktree_id, "2.txt"), true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -5311,7 +5719,7 @@ async fn test_peers_simultaneously_following_each_other(
|
||||
cx_a.update(editor::init);
|
||||
cx_b.update(editor::init);
|
||||
|
||||
let mut server = TestServer::start(cx_a.background()).await;
|
||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
server
|
||||
@@ -5381,7 +5789,7 @@ async fn test_random_collaboration(
|
||||
.map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
|
||||
.unwrap_or(10);
|
||||
|
||||
let mut server = TestServer::start(cx.background()).await;
|
||||
let mut server = TestServer::start(cx.foreground(), cx.background()).await;
|
||||
let db = server.app_state.db.clone();
|
||||
|
||||
let mut available_guests = Vec::new();
|
||||
@@ -5579,13 +5987,6 @@ async fn test_random_collaboration(
|
||||
guest_client.username,
|
||||
id
|
||||
);
|
||||
assert_eq!(
|
||||
guest_snapshot.abs_path(),
|
||||
host_snapshot.abs_path(),
|
||||
"{} has different abs path than the host for worktree {}",
|
||||
guest_client.username,
|
||||
id
|
||||
);
|
||||
assert_eq!(
|
||||
guest_snapshot.entries(false).collect::<Vec<_>>(),
|
||||
host_snapshot.entries(false).collect::<Vec<_>>(),
|
||||
@@ -5675,6 +6076,8 @@ struct TestServer {
|
||||
peer: Arc<Peer>,
|
||||
app_state: Arc<AppState>,
|
||||
server: Arc<Server>,
|
||||
foreground: Rc<executor::Foreground>,
|
||||
notifications: mpsc::UnboundedReceiver<()>,
|
||||
connection_killers: Arc<Mutex<HashMap<PeerId, Arc<AtomicBool>>>>,
|
||||
forbid_connections: Arc<AtomicBool>,
|
||||
_test_db: TestDb,
|
||||
@@ -5682,10 +6085,13 @@ struct TestServer {
|
||||
}
|
||||
|
||||
impl TestServer {
|
||||
async fn start(background: Arc<executor::Background>) -> Self {
|
||||
async fn start(
|
||||
foreground: Rc<executor::Foreground>,
|
||||
background: Arc<executor::Background>,
|
||||
) -> Self {
|
||||
static NEXT_LIVE_KIT_SERVER_ID: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
let test_db = TestDb::new(background.clone());
|
||||
let test_db = TestDb::fake(background.clone());
|
||||
let live_kit_server_id = NEXT_LIVE_KIT_SERVER_ID.fetch_add(1, SeqCst);
|
||||
let live_kit_server = live_kit_client::TestServer::create(
|
||||
format!("http://livekit.{}.test", live_kit_server_id),
|
||||
@@ -5696,11 +6102,14 @@ impl TestServer {
|
||||
.unwrap();
|
||||
let app_state = Self::build_app_state(&test_db, &live_kit_server).await;
|
||||
let peer = Peer::new();
|
||||
let server = Server::new(app_state.clone());
|
||||
let notifications = mpsc::unbounded();
|
||||
let server = Server::new(app_state.clone(), Some(notifications.0));
|
||||
Self {
|
||||
peer,
|
||||
app_state,
|
||||
server,
|
||||
foreground,
|
||||
notifications: notifications.1,
|
||||
connection_killers: Default::default(),
|
||||
forbid_connections: Default::default(),
|
||||
_test_db: test_db,
|
||||
@@ -5738,7 +6147,7 @@ impl TestServer {
|
||||
},
|
||||
)
|
||||
.await
|
||||
.expect("creating user failed")
|
||||
.unwrap()
|
||||
.user_id
|
||||
};
|
||||
let client_name = name.to_string();
|
||||
@@ -5778,11 +6187,7 @@ impl TestServer {
|
||||
let (client_conn, server_conn, killed) =
|
||||
Connection::in_memory(cx.background());
|
||||
let (connection_id_tx, connection_id_rx) = oneshot::channel();
|
||||
let user = db
|
||||
.get_user_by_id(user_id)
|
||||
.await
|
||||
.expect("retrieving user failed")
|
||||
.unwrap();
|
||||
let user = db.get_user_by_id(user_id).await.unwrap().unwrap();
|
||||
cx.background()
|
||||
.spawn(server.handle_connection(
|
||||
server_conn,
|
||||
@@ -5816,6 +6221,7 @@ impl TestServer {
|
||||
default_item_factory: |_, _| unimplemented!(),
|
||||
});
|
||||
|
||||
Channel::init(&client);
|
||||
Project::init(&client);
|
||||
cx.update(|cx| {
|
||||
workspace::init(app_state.clone(), cx);
|
||||
@@ -5916,6 +6322,21 @@ impl TestServer {
|
||||
config: Default::default(),
|
||||
})
|
||||
}
|
||||
|
||||
async fn condition<F>(&mut self, mut predicate: F)
|
||||
where
|
||||
F: FnMut(&Store) -> bool,
|
||||
{
|
||||
assert!(
|
||||
self.foreground.parking_forbidden(),
|
||||
"you must call forbid_parking to use server conditions so we don't block indefinitely"
|
||||
);
|
||||
while !(predicate)(&*self.server.store.lock().await) {
|
||||
self.foreground.start_waiting();
|
||||
self.notifications.next().await;
|
||||
self.foreground.finish_waiting();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for TestServer {
|
||||
@@ -6631,6 +7052,20 @@ impl Executor for Arc<gpui::executor::Background> {
|
||||
}
|
||||
}
|
||||
|
||||
fn channel_messages(channel: &Channel) -> Vec<(String, String, bool)> {
|
||||
channel
|
||||
.messages()
|
||||
.cursor::<()>()
|
||||
.map(|m| {
|
||||
(
|
||||
m.sender.github_login.clone(),
|
||||
m.body.clone(),
|
||||
m.is_pending(),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
struct RoomParticipants {
|
||||
remote: Vec<String>,
|
||||
|
||||
@@ -13,12 +13,12 @@ use crate::rpc::ResultExt as _;
|
||||
use anyhow::anyhow;
|
||||
use axum::{routing::get, Router};
|
||||
use collab::{Error, Result};
|
||||
use db::DefaultDb as Db;
|
||||
use db::{Db, PostgresDb};
|
||||
use serde::Deserialize;
|
||||
use std::{
|
||||
env::args,
|
||||
net::{SocketAddr, TcpListener},
|
||||
path::{Path, PathBuf},
|
||||
path::PathBuf,
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
@@ -49,14 +49,14 @@ pub struct MigrateConfig {
|
||||
}
|
||||
|
||||
pub struct AppState {
|
||||
db: Arc<Db>,
|
||||
db: Arc<dyn Db>,
|
||||
live_kit_client: Option<Arc<dyn live_kit_server::api::Client>>,
|
||||
config: Config,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
async fn new(config: Config) -> Result<Arc<Self>> {
|
||||
let db = Db::new(&config.database_url, 5).await?;
|
||||
let db = PostgresDb::new(&config.database_url, 5).await?;
|
||||
let live_kit_client = if let Some(((server, key), secret)) = config
|
||||
.live_kit_server
|
||||
.as_ref()
|
||||
@@ -96,12 +96,13 @@ async fn main() -> Result<()> {
|
||||
}
|
||||
Some("migrate") => {
|
||||
let config = envy::from_env::<MigrateConfig>().expect("error loading config");
|
||||
let db = Db::new(&config.database_url, 5).await?;
|
||||
let db = PostgresDb::new(&config.database_url, 5).await?;
|
||||
|
||||
let migrations_path = config
|
||||
.migrations_path
|
||||
.as_deref()
|
||||
.unwrap_or_else(|| Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/migrations")));
|
||||
.or(db::DEFAULT_MIGRATIONS_PATH.map(|s| s.as_ref()))
|
||||
.ok_or_else(|| anyhow!("missing MIGRATIONS_PATH environment variable"))?;
|
||||
|
||||
let migrations = db.migrate(&migrations_path, false).await?;
|
||||
for (migration, duration) in migrations {
|
||||
@@ -121,7 +122,9 @@ async fn main() -> Result<()> {
|
||||
let listener = TcpListener::bind(&format!("0.0.0.0:{}", state.config.http_port))
|
||||
.expect("failed to bind TCP listener");
|
||||
|
||||
let rpc_server = rpc::Server::new(state.clone());
|
||||
let rpc_server = rpc::Server::new(state.clone(), None);
|
||||
rpc_server
|
||||
.start_recording_project_activity(Duration::from_secs(5 * 60), rpc::RealExecutor);
|
||||
|
||||
let app = api::routes(rpc_server.clone(), state.clone())
|
||||
.merge(rpc::routes(rpc_server.clone()))
|
||||
|
||||
@@ -2,7 +2,7 @@ mod store;
|
||||
|
||||
use crate::{
|
||||
auth,
|
||||
db::{self, ProjectId, User, UserId},
|
||||
db::{self, ChannelId, MessageId, ProjectId, User, UserId},
|
||||
AppState, Result,
|
||||
};
|
||||
use anyhow::anyhow;
|
||||
@@ -24,7 +24,7 @@ use axum::{
|
||||
};
|
||||
use collections::{HashMap, HashSet};
|
||||
use futures::{
|
||||
channel::oneshot,
|
||||
channel::{mpsc, oneshot},
|
||||
future::{self, BoxFuture},
|
||||
stream::FuturesUnordered,
|
||||
FutureExt, SinkExt, StreamExt, TryStreamExt,
|
||||
@@ -42,6 +42,7 @@ use std::{
|
||||
marker::PhantomData,
|
||||
net::SocketAddr,
|
||||
ops::{Deref, DerefMut},
|
||||
os::unix::prelude::OsStrExt,
|
||||
rc::Rc,
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering::SeqCst},
|
||||
@@ -50,6 +51,7 @@ use std::{
|
||||
time::Duration,
|
||||
};
|
||||
pub use store::{Store, Worktree};
|
||||
use time::OffsetDateTime;
|
||||
use tokio::{
|
||||
sync::{Mutex, MutexGuard},
|
||||
time::Sleep,
|
||||
@@ -60,6 +62,10 @@ use tracing::{info_span, instrument, Instrument};
|
||||
lazy_static! {
|
||||
static ref METRIC_CONNECTIONS: IntGauge =
|
||||
register_int_gauge!("connections", "number of connections").unwrap();
|
||||
static ref METRIC_REGISTERED_PROJECTS: IntGauge =
|
||||
register_int_gauge!("registered_projects", "number of registered projects").unwrap();
|
||||
static ref METRIC_ACTIVE_PROJECTS: IntGauge =
|
||||
register_int_gauge!("active_projects", "number of active projects").unwrap();
|
||||
static ref METRIC_SHARED_PROJECTS: IntGauge = register_int_gauge!(
|
||||
"shared_projects",
|
||||
"number of open projects with one or more guests"
|
||||
@@ -89,6 +95,7 @@ pub struct Server {
|
||||
pub(crate) store: Mutex<Store>,
|
||||
app_state: Arc<AppState>,
|
||||
handlers: HashMap<TypeId, MessageHandler>,
|
||||
notifications: Option<mpsc::UnboundedSender<()>>,
|
||||
}
|
||||
|
||||
pub trait Executor: Send + Clone {
|
||||
@@ -100,6 +107,9 @@ pub trait Executor: Send + Clone {
|
||||
#[derive(Clone)]
|
||||
pub struct RealExecutor;
|
||||
|
||||
const MESSAGE_COUNT_PER_PAGE: usize = 100;
|
||||
const MAX_MESSAGE_LEN: usize = 1024;
|
||||
|
||||
pub(crate) struct StoreGuard<'a> {
|
||||
guard: MutexGuard<'a, Store>,
|
||||
_not_send: PhantomData<Rc<()>>,
|
||||
@@ -122,12 +132,16 @@ where
|
||||
}
|
||||
|
||||
impl Server {
|
||||
pub fn new(app_state: Arc<AppState>) -> Arc<Self> {
|
||||
pub fn new(
|
||||
app_state: Arc<AppState>,
|
||||
notifications: Option<mpsc::UnboundedSender<()>>,
|
||||
) -> Arc<Self> {
|
||||
let mut server = Self {
|
||||
peer: Peer::new(),
|
||||
app_state,
|
||||
store: Default::default(),
|
||||
handlers: Default::default(),
|
||||
notifications,
|
||||
};
|
||||
|
||||
server
|
||||
@@ -144,7 +158,9 @@ impl Server {
|
||||
.add_request_handler(Server::join_project)
|
||||
.add_message_handler(Server::leave_project)
|
||||
.add_message_handler(Server::update_project)
|
||||
.add_message_handler(Server::register_project_activity)
|
||||
.add_request_handler(Server::update_worktree)
|
||||
.add_message_handler(Server::update_worktree_extensions)
|
||||
.add_message_handler(Server::start_language_server)
|
||||
.add_message_handler(Server::update_language_server)
|
||||
.add_message_handler(Server::update_diagnostic_summary)
|
||||
@@ -178,14 +194,19 @@ impl Server {
|
||||
.add_message_handler(Server::buffer_reloaded)
|
||||
.add_message_handler(Server::buffer_saved)
|
||||
.add_request_handler(Server::save_buffer)
|
||||
.add_request_handler(Server::get_channels)
|
||||
.add_request_handler(Server::get_users)
|
||||
.add_request_handler(Server::fuzzy_search_users)
|
||||
.add_request_handler(Server::request_contact)
|
||||
.add_request_handler(Server::remove_contact)
|
||||
.add_request_handler(Server::respond_to_contact_request)
|
||||
.add_request_handler(Server::join_channel)
|
||||
.add_message_handler(Server::leave_channel)
|
||||
.add_request_handler(Server::send_channel_message)
|
||||
.add_request_handler(Server::follow)
|
||||
.add_message_handler(Server::unfollow)
|
||||
.add_message_handler(Server::update_followers)
|
||||
.add_request_handler(Server::get_channel_messages)
|
||||
.add_message_handler(Server::update_diff_base)
|
||||
.add_request_handler(Server::get_private_user_info);
|
||||
|
||||
@@ -269,6 +290,58 @@ impl Server {
|
||||
})
|
||||
}
|
||||
|
||||
/// Start a long lived task that records which users are active in which projects.
|
||||
pub fn start_recording_project_activity<E: 'static + Executor>(
|
||||
self: &Arc<Self>,
|
||||
interval: Duration,
|
||||
executor: E,
|
||||
) {
|
||||
executor.spawn_detached({
|
||||
let this = Arc::downgrade(self);
|
||||
let executor = executor.clone();
|
||||
async move {
|
||||
let mut period_start = OffsetDateTime::now_utc();
|
||||
let mut active_projects = Vec::<(UserId, ProjectId)>::new();
|
||||
loop {
|
||||
let sleep = executor.sleep(interval);
|
||||
sleep.await;
|
||||
let this = if let Some(this) = this.upgrade() {
|
||||
this
|
||||
} else {
|
||||
break;
|
||||
};
|
||||
|
||||
active_projects.clear();
|
||||
active_projects.extend(this.store().await.projects().flat_map(
|
||||
|(project_id, project)| {
|
||||
project.guests.values().chain([&project.host]).filter_map(
|
||||
|collaborator| {
|
||||
if !collaborator.admin
|
||||
&& collaborator
|
||||
.last_activity
|
||||
.map_or(false, |activity| activity > period_start)
|
||||
{
|
||||
Some((collaborator.user_id, *project_id))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
));
|
||||
|
||||
let period_end = OffsetDateTime::now_utc();
|
||||
this.app_state
|
||||
.db
|
||||
.record_user_activity(period_start..period_end, &active_projects)
|
||||
.await
|
||||
.trace_err();
|
||||
period_start = period_end;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn handle_connection<E: Executor>(
|
||||
self: &Arc<Self>,
|
||||
connection: Connection,
|
||||
@@ -359,11 +432,18 @@ impl Server {
|
||||
let span = tracing::info_span!("receive message", %user_id, %login, %connection_id, %address, type_name);
|
||||
let span_enter = span.enter();
|
||||
if let Some(handler) = this.handlers.get(&message.payload_type_id()) {
|
||||
let notifications = this.notifications.clone();
|
||||
let is_background = message.is_background();
|
||||
let handle_message = (handler)(this.clone(), message);
|
||||
drop(span_enter);
|
||||
|
||||
let handle_message = handle_message.instrument(span);
|
||||
drop(span_enter);
|
||||
let handle_message = async move {
|
||||
handle_message.await;
|
||||
if let Some(mut notifications) = notifications {
|
||||
let _ = notifications.send(()).await;
|
||||
}
|
||||
}.instrument(span);
|
||||
|
||||
if is_background {
|
||||
executor.spawn_detached(handle_message);
|
||||
} else {
|
||||
@@ -944,7 +1024,7 @@ impl Server {
|
||||
id: *id,
|
||||
root_name: worktree.root_name.clone(),
|
||||
visible: worktree.visible,
|
||||
abs_path: worktree.abs_path.clone(),
|
||||
abs_path: worktree.abs_path.as_os_str().as_bytes().to_vec(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
@@ -995,7 +1075,7 @@ impl Server {
|
||||
let message = proto::UpdateWorktree {
|
||||
project_id: project_id.to_proto(),
|
||||
worktree_id: *worktree_id,
|
||||
abs_path: worktree.abs_path.clone(),
|
||||
abs_path: worktree.abs_path.as_os_str().as_bytes().to_vec(),
|
||||
root_name: worktree.root_name.clone(),
|
||||
updated_entries: worktree.entries.values().cloned().collect(),
|
||||
removed_entries: Default::default(),
|
||||
@@ -1092,6 +1172,17 @@ impl Server {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn register_project_activity(
|
||||
self: Arc<Server>,
|
||||
request: TypedEnvelope<proto::RegisterProjectActivity>,
|
||||
) -> Result<()> {
|
||||
self.store().await.register_project_activity(
|
||||
ProjectId::from_proto(request.payload.project_id),
|
||||
request.sender_id,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn update_worktree(
|
||||
self: Arc<Server>,
|
||||
request: TypedEnvelope<proto::UpdateWorktree>,
|
||||
@@ -1104,7 +1195,6 @@ impl Server {
|
||||
project_id,
|
||||
worktree_id,
|
||||
&request.payload.root_name,
|
||||
&request.payload.abs_path,
|
||||
&request.payload.removed_entries,
|
||||
&request.payload.updated_entries,
|
||||
request.payload.scan_id,
|
||||
@@ -1119,6 +1209,25 @@ impl Server {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn update_worktree_extensions(
|
||||
self: Arc<Server>,
|
||||
request: TypedEnvelope<proto::UpdateWorktreeExtensions>,
|
||||
) -> Result<()> {
|
||||
let project_id = ProjectId::from_proto(request.payload.project_id);
|
||||
let worktree_id = request.payload.worktree_id;
|
||||
let extensions = request
|
||||
.payload
|
||||
.extensions
|
||||
.into_iter()
|
||||
.zip(request.payload.counts)
|
||||
.collect();
|
||||
self.app_state
|
||||
.db
|
||||
.update_worktree_extensions(project_id, worktree_id, extensions)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn update_diagnostic_summary(
|
||||
self: Arc<Server>,
|
||||
request: TypedEnvelope<proto::UpdateDiagnosticSummary>,
|
||||
@@ -1254,7 +1363,8 @@ impl Server {
|
||||
) -> Result<()> {
|
||||
let project_id = ProjectId::from_proto(request.payload.project_id);
|
||||
let receiver_ids = {
|
||||
let store = self.store().await;
|
||||
let mut store = self.store().await;
|
||||
store.register_project_activity(project_id, request.sender_id)?;
|
||||
store.project_connection_ids(project_id, request.sender_id)?
|
||||
};
|
||||
|
||||
@@ -1320,13 +1430,15 @@ impl Server {
|
||||
let leader_id = ConnectionId(request.payload.leader_id);
|
||||
let follower_id = request.sender_id;
|
||||
{
|
||||
let store = self.store().await;
|
||||
let mut store = self.store().await;
|
||||
if !store
|
||||
.project_connection_ids(project_id, follower_id)?
|
||||
.contains(&leader_id)
|
||||
{
|
||||
Err(anyhow!("no such peer"))?;
|
||||
}
|
||||
|
||||
store.register_project_activity(project_id, follower_id)?;
|
||||
}
|
||||
|
||||
let mut response_payload = self
|
||||
@@ -1343,13 +1455,14 @@ impl Server {
|
||||
async fn unfollow(self: Arc<Self>, request: TypedEnvelope<proto::Unfollow>) -> Result<()> {
|
||||
let project_id = ProjectId::from_proto(request.payload.project_id);
|
||||
let leader_id = ConnectionId(request.payload.leader_id);
|
||||
let store = self.store().await;
|
||||
let mut store = self.store().await;
|
||||
if !store
|
||||
.project_connection_ids(project_id, request.sender_id)?
|
||||
.contains(&leader_id)
|
||||
{
|
||||
Err(anyhow!("no such peer"))?;
|
||||
}
|
||||
store.register_project_activity(project_id, request.sender_id)?;
|
||||
self.peer
|
||||
.forward_send(request.sender_id, leader_id, request.payload)?;
|
||||
Ok(())
|
||||
@@ -1360,7 +1473,8 @@ impl Server {
|
||||
request: TypedEnvelope<proto::UpdateFollowers>,
|
||||
) -> Result<()> {
|
||||
let project_id = ProjectId::from_proto(request.payload.project_id);
|
||||
let store = self.store().await;
|
||||
let mut store = self.store().await;
|
||||
store.register_project_activity(project_id, request.sender_id)?;
|
||||
let connection_ids = store.project_connection_ids(project_id, request.sender_id)?;
|
||||
let leader_id = request
|
||||
.payload
|
||||
@@ -1381,6 +1495,28 @@ impl Server {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_channels(
|
||||
self: Arc<Server>,
|
||||
request: TypedEnvelope<proto::GetChannels>,
|
||||
response: Response<proto::GetChannels>,
|
||||
) -> Result<()> {
|
||||
let user_id = self
|
||||
.store()
|
||||
.await
|
||||
.user_id_for_connection(request.sender_id)?;
|
||||
let channels = self.app_state.db.get_accessible_channels(user_id).await?;
|
||||
response.send(proto::GetChannelsResponse {
|
||||
channels: channels
|
||||
.into_iter()
|
||||
.map(|chan| proto::Channel {
|
||||
id: chan.id.to_proto(),
|
||||
name: chan.name,
|
||||
})
|
||||
.collect(),
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_users(
|
||||
self: Arc<Server>,
|
||||
request: TypedEnvelope<proto::GetUsers>,
|
||||
@@ -1576,6 +1712,175 @@ impl Server {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn join_channel(
|
||||
self: Arc<Self>,
|
||||
request: TypedEnvelope<proto::JoinChannel>,
|
||||
response: Response<proto::JoinChannel>,
|
||||
) -> Result<()> {
|
||||
let user_id = self
|
||||
.store()
|
||||
.await
|
||||
.user_id_for_connection(request.sender_id)?;
|
||||
let channel_id = ChannelId::from_proto(request.payload.channel_id);
|
||||
if !self
|
||||
.app_state
|
||||
.db
|
||||
.can_user_access_channel(user_id, channel_id)
|
||||
.await?
|
||||
{
|
||||
Err(anyhow!("access denied"))?;
|
||||
}
|
||||
|
||||
self.store()
|
||||
.await
|
||||
.join_channel(request.sender_id, channel_id);
|
||||
let messages = self
|
||||
.app_state
|
||||
.db
|
||||
.get_channel_messages(channel_id, MESSAGE_COUNT_PER_PAGE, None)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|msg| proto::ChannelMessage {
|
||||
id: msg.id.to_proto(),
|
||||
body: msg.body,
|
||||
timestamp: msg.sent_at.unix_timestamp() as u64,
|
||||
sender_id: msg.sender_id.to_proto(),
|
||||
nonce: Some(msg.nonce.as_u128().into()),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
response.send(proto::JoinChannelResponse {
|
||||
done: messages.len() < MESSAGE_COUNT_PER_PAGE,
|
||||
messages,
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn leave_channel(
|
||||
self: Arc<Self>,
|
||||
request: TypedEnvelope<proto::LeaveChannel>,
|
||||
) -> Result<()> {
|
||||
let user_id = self
|
||||
.store()
|
||||
.await
|
||||
.user_id_for_connection(request.sender_id)?;
|
||||
let channel_id = ChannelId::from_proto(request.payload.channel_id);
|
||||
if !self
|
||||
.app_state
|
||||
.db
|
||||
.can_user_access_channel(user_id, channel_id)
|
||||
.await?
|
||||
{
|
||||
Err(anyhow!("access denied"))?;
|
||||
}
|
||||
|
||||
self.store()
|
||||
.await
|
||||
.leave_channel(request.sender_id, channel_id);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_channel_message(
|
||||
self: Arc<Self>,
|
||||
request: TypedEnvelope<proto::SendChannelMessage>,
|
||||
response: Response<proto::SendChannelMessage>,
|
||||
) -> Result<()> {
|
||||
let channel_id = ChannelId::from_proto(request.payload.channel_id);
|
||||
let user_id;
|
||||
let connection_ids;
|
||||
{
|
||||
let state = self.store().await;
|
||||
user_id = state.user_id_for_connection(request.sender_id)?;
|
||||
connection_ids = state.channel_connection_ids(channel_id)?;
|
||||
}
|
||||
|
||||
// Validate the message body.
|
||||
let body = request.payload.body.trim().to_string();
|
||||
if body.len() > MAX_MESSAGE_LEN {
|
||||
return Err(anyhow!("message is too long"))?;
|
||||
}
|
||||
if body.is_empty() {
|
||||
return Err(anyhow!("message can't be blank"))?;
|
||||
}
|
||||
|
||||
let timestamp = OffsetDateTime::now_utc();
|
||||
let nonce = request
|
||||
.payload
|
||||
.nonce
|
||||
.ok_or_else(|| anyhow!("nonce can't be blank"))?;
|
||||
|
||||
let message_id = self
|
||||
.app_state
|
||||
.db
|
||||
.create_channel_message(channel_id, user_id, &body, timestamp, nonce.clone().into())
|
||||
.await?
|
||||
.to_proto();
|
||||
let message = proto::ChannelMessage {
|
||||
sender_id: user_id.to_proto(),
|
||||
id: message_id,
|
||||
body,
|
||||
timestamp: timestamp.unix_timestamp() as u64,
|
||||
nonce: Some(nonce),
|
||||
};
|
||||
broadcast(request.sender_id, connection_ids, |conn_id| {
|
||||
self.peer.send(
|
||||
conn_id,
|
||||
proto::ChannelMessageSent {
|
||||
channel_id: channel_id.to_proto(),
|
||||
message: Some(message.clone()),
|
||||
},
|
||||
)
|
||||
});
|
||||
response.send(proto::SendChannelMessageResponse {
|
||||
message: Some(message),
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_channel_messages(
|
||||
self: Arc<Self>,
|
||||
request: TypedEnvelope<proto::GetChannelMessages>,
|
||||
response: Response<proto::GetChannelMessages>,
|
||||
) -> Result<()> {
|
||||
let user_id = self
|
||||
.store()
|
||||
.await
|
||||
.user_id_for_connection(request.sender_id)?;
|
||||
let channel_id = ChannelId::from_proto(request.payload.channel_id);
|
||||
if !self
|
||||
.app_state
|
||||
.db
|
||||
.can_user_access_channel(user_id, channel_id)
|
||||
.await?
|
||||
{
|
||||
Err(anyhow!("access denied"))?;
|
||||
}
|
||||
|
||||
let messages = self
|
||||
.app_state
|
||||
.db
|
||||
.get_channel_messages(
|
||||
channel_id,
|
||||
MESSAGE_COUNT_PER_PAGE,
|
||||
Some(MessageId::from_proto(request.payload.before_message_id)),
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|msg| proto::ChannelMessage {
|
||||
id: msg.id.to_proto(),
|
||||
body: msg.body,
|
||||
timestamp: msg.sent_at.unix_timestamp() as u64,
|
||||
sender_id: msg.sender_id.to_proto(),
|
||||
nonce: Some(msg.nonce.as_u128().into()),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
response.send(proto::GetChannelMessagesResponse {
|
||||
done: messages.len() < MESSAGE_COUNT_PER_PAGE,
|
||||
messages,
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn update_diff_base(
|
||||
self: Arc<Server>,
|
||||
request: TypedEnvelope<proto::UpdateDiffBase>,
|
||||
@@ -1756,8 +2061,11 @@ pub async fn handle_websocket_request(
|
||||
}
|
||||
|
||||
pub async fn handle_metrics(Extension(server): Extension<Arc<Server>>) -> axum::response::Response {
|
||||
// We call `store_mut` here for its side effects of updating metrics.
|
||||
let metrics = server.store().await.metrics();
|
||||
METRIC_CONNECTIONS.set(metrics.connections as _);
|
||||
METRIC_REGISTERED_PROJECTS.set(metrics.registered_projects as _);
|
||||
METRIC_ACTIVE_PROJECTS.set(metrics.active_projects as _);
|
||||
METRIC_SHARED_PROJECTS.set(metrics.shared_projects as _);
|
||||
|
||||
let encoder = prometheus::TextEncoder::new();
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
use crate::db::{self, ProjectId, UserId};
|
||||
use crate::db::{self, ChannelId, ProjectId, UserId};
|
||||
use anyhow::{anyhow, Result};
|
||||
use collections::{btree_map, BTreeMap, BTreeSet, HashMap, HashSet};
|
||||
use nanoid::nanoid;
|
||||
use rpc::{proto, ConnectionId};
|
||||
use serde::Serialize;
|
||||
use std::{borrow::Cow, mem, path::PathBuf, str};
|
||||
use std::{borrow::Cow, mem, path::PathBuf, str, time::Duration};
|
||||
use time::OffsetDateTime;
|
||||
use tracing::instrument;
|
||||
use util::post_inc;
|
||||
|
||||
@@ -17,6 +18,8 @@ pub struct Store {
|
||||
next_room_id: RoomId,
|
||||
rooms: BTreeMap<RoomId, proto::Room>,
|
||||
projects: BTreeMap<ProjectId, Project>,
|
||||
#[serde(skip)]
|
||||
channels: BTreeMap<ChannelId, Channel>,
|
||||
}
|
||||
|
||||
#[derive(Default, Serialize)]
|
||||
@@ -30,6 +33,7 @@ struct ConnectionState {
|
||||
user_id: UserId,
|
||||
admin: bool,
|
||||
projects: BTreeSet<ProjectId>,
|
||||
channels: HashSet<ChannelId>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Serialize)]
|
||||
@@ -56,12 +60,14 @@ pub struct Project {
|
||||
pub struct Collaborator {
|
||||
pub replica_id: ReplicaId,
|
||||
pub user_id: UserId,
|
||||
#[serde(skip)]
|
||||
pub last_activity: Option<OffsetDateTime>,
|
||||
pub admin: bool,
|
||||
}
|
||||
|
||||
#[derive(Default, Serialize)]
|
||||
pub struct Worktree {
|
||||
pub abs_path: Vec<u8>,
|
||||
pub abs_path: PathBuf,
|
||||
pub root_name: String,
|
||||
pub visible: bool,
|
||||
#[serde(skip)]
|
||||
@@ -72,6 +78,11 @@ pub struct Worktree {
|
||||
pub is_complete: bool,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Channel {
|
||||
pub connection_ids: HashSet<ConnectionId>,
|
||||
}
|
||||
|
||||
pub type ReplicaId = u16;
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -102,23 +113,38 @@ pub struct LeftRoom<'a> {
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct Metrics {
|
||||
pub connections: usize,
|
||||
pub registered_projects: usize,
|
||||
pub active_projects: usize,
|
||||
pub shared_projects: usize,
|
||||
}
|
||||
|
||||
impl Store {
|
||||
pub fn metrics(&self) -> Metrics {
|
||||
const ACTIVE_PROJECT_TIMEOUT: Duration = Duration::from_secs(60);
|
||||
let active_window_start = OffsetDateTime::now_utc() - ACTIVE_PROJECT_TIMEOUT;
|
||||
|
||||
let connections = self.connections.values().filter(|c| !c.admin).count();
|
||||
let mut registered_projects = 0;
|
||||
let mut active_projects = 0;
|
||||
let mut shared_projects = 0;
|
||||
for project in self.projects.values() {
|
||||
if let Some(connection) = self.connections.get(&project.host_connection_id) {
|
||||
if !connection.admin {
|
||||
shared_projects += 1;
|
||||
registered_projects += 1;
|
||||
if project.is_active_since(active_window_start) {
|
||||
active_projects += 1;
|
||||
if !project.guests.is_empty() {
|
||||
shared_projects += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Metrics {
|
||||
connections,
|
||||
registered_projects,
|
||||
active_projects,
|
||||
shared_projects,
|
||||
}
|
||||
}
|
||||
@@ -136,6 +162,7 @@ impl Store {
|
||||
user_id,
|
||||
admin,
|
||||
projects: Default::default(),
|
||||
channels: Default::default(),
|
||||
},
|
||||
);
|
||||
let connected_user = self.connected_users.entry(user_id).or_default();
|
||||
@@ -174,12 +201,18 @@ impl Store {
|
||||
.ok_or_else(|| anyhow!("no such connection"))?;
|
||||
|
||||
let user_id = connection.user_id;
|
||||
let connection_channels = mem::take(&mut connection.channels);
|
||||
|
||||
let mut result = RemovedConnectionState {
|
||||
user_id,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// Leave all channels.
|
||||
for channel_id in connection_channels {
|
||||
self.leave_channel(connection_id, channel_id);
|
||||
}
|
||||
|
||||
let connected_user = self.connected_users.get(&user_id).unwrap();
|
||||
if let Some(active_call) = connected_user.active_call.as_ref() {
|
||||
let room_id = active_call.room_id;
|
||||
@@ -205,6 +238,34 @@ impl Store {
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn channel(&self, id: ChannelId) -> Option<&Channel> {
|
||||
self.channels.get(&id)
|
||||
}
|
||||
|
||||
pub fn join_channel(&mut self, connection_id: ConnectionId, channel_id: ChannelId) {
|
||||
if let Some(connection) = self.connections.get_mut(&connection_id) {
|
||||
connection.channels.insert(channel_id);
|
||||
self.channels
|
||||
.entry(channel_id)
|
||||
.or_default()
|
||||
.connection_ids
|
||||
.insert(connection_id);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn leave_channel(&mut self, connection_id: ConnectionId, channel_id: ChannelId) {
|
||||
if let Some(connection) = self.connections.get_mut(&connection_id) {
|
||||
connection.channels.remove(&channel_id);
|
||||
if let btree_map::Entry::Occupied(mut entry) = self.channels.entry(channel_id) {
|
||||
entry.get_mut().connection_ids.remove(&connection_id);
|
||||
if entry.get_mut().connection_ids.is_empty() {
|
||||
entry.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn user_id_for_connection(&self, connection_id: ConnectionId) -> Result<UserId> {
|
||||
Ok(self
|
||||
.connections
|
||||
@@ -699,6 +760,7 @@ impl Store {
|
||||
host: Collaborator {
|
||||
user_id: connection.user_id,
|
||||
replica_id: 0,
|
||||
last_activity: None,
|
||||
admin: connection.admin,
|
||||
},
|
||||
guests: Default::default(),
|
||||
@@ -711,11 +773,7 @@ impl Store {
|
||||
Worktree {
|
||||
root_name: worktree.root_name,
|
||||
visible: worktree.visible,
|
||||
abs_path: worktree.abs_path.clone(),
|
||||
entries: Default::default(),
|
||||
diagnostic_summaries: Default::default(),
|
||||
scan_id: Default::default(),
|
||||
is_complete: Default::default(),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
})
|
||||
@@ -794,11 +852,7 @@ impl Store {
|
||||
Worktree {
|
||||
root_name: worktree.root_name.clone(),
|
||||
visible: worktree.visible,
|
||||
abs_path: worktree.abs_path.clone(),
|
||||
entries: Default::default(),
|
||||
diagnostic_summaries: Default::default(),
|
||||
scan_id: Default::default(),
|
||||
is_complete: false,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -905,10 +959,12 @@ impl Store {
|
||||
Collaborator {
|
||||
replica_id,
|
||||
user_id: connection.user_id,
|
||||
last_activity: Some(OffsetDateTime::now_utc()),
|
||||
admin: connection.admin,
|
||||
},
|
||||
);
|
||||
|
||||
project.host.last_activity = Some(OffsetDateTime::now_utc());
|
||||
Ok((project, replica_id))
|
||||
}
|
||||
|
||||
@@ -950,7 +1006,6 @@ impl Store {
|
||||
project_id: ProjectId,
|
||||
worktree_id: u64,
|
||||
worktree_root_name: &str,
|
||||
worktree_abs_path: &[u8],
|
||||
removed_entries: &[u64],
|
||||
updated_entries: &[proto::Entry],
|
||||
scan_id: u64,
|
||||
@@ -961,7 +1016,6 @@ impl Store {
|
||||
let connection_ids = project.connection_ids();
|
||||
let mut worktree = project.worktrees.entry(worktree_id).or_default();
|
||||
worktree.root_name = worktree_root_name.to_string();
|
||||
worktree.abs_path = worktree_abs_path.to_vec();
|
||||
|
||||
for entry_id in removed_entries {
|
||||
worktree.entries.remove(entry_id);
|
||||
@@ -1002,12 +1056,44 @@ impl Store {
|
||||
.connection_ids())
|
||||
}
|
||||
|
||||
pub fn channel_connection_ids(&self, channel_id: ChannelId) -> Result<Vec<ConnectionId>> {
|
||||
Ok(self
|
||||
.channels
|
||||
.get(&channel_id)
|
||||
.ok_or_else(|| anyhow!("no such channel"))?
|
||||
.connection_ids())
|
||||
}
|
||||
|
||||
pub fn project(&self, project_id: ProjectId) -> Result<&Project> {
|
||||
self.projects
|
||||
.get(&project_id)
|
||||
.ok_or_else(|| anyhow!("no such project"))
|
||||
}
|
||||
|
||||
pub fn register_project_activity(
|
||||
&mut self,
|
||||
project_id: ProjectId,
|
||||
connection_id: ConnectionId,
|
||||
) -> Result<()> {
|
||||
let project = self
|
||||
.projects
|
||||
.get_mut(&project_id)
|
||||
.ok_or_else(|| anyhow!("no such project"))?;
|
||||
let collaborator = if connection_id == project.host_connection_id {
|
||||
&mut project.host
|
||||
} else if let Some(guest) = project.guests.get_mut(&connection_id) {
|
||||
guest
|
||||
} else {
|
||||
return Err(anyhow!("no such project"))?;
|
||||
};
|
||||
collaborator.last_activity = Some(OffsetDateTime::now_utc());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn projects(&self) -> impl Iterator<Item = (&ProjectId, &Project)> {
|
||||
self.projects.iter()
|
||||
}
|
||||
|
||||
pub fn read_project(
|
||||
&self,
|
||||
project_id: ProjectId,
|
||||
@@ -1068,7 +1154,10 @@ impl Store {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for channel_id in &connection.channels {
|
||||
let channel = self.channels.get(channel_id).unwrap();
|
||||
assert!(channel.connection_ids.contains(connection_id));
|
||||
}
|
||||
assert!(self
|
||||
.connected_users
|
||||
.get(&connection.user_id)
|
||||
@@ -1164,10 +1253,28 @@ impl Store {
|
||||
"project was not shared in room"
|
||||
);
|
||||
}
|
||||
|
||||
for (channel_id, channel) in &self.channels {
|
||||
for connection_id in &channel.connection_ids {
|
||||
let connection = self.connections.get(connection_id).unwrap();
|
||||
assert!(connection.channels.contains(channel_id));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Project {
|
||||
fn is_active_since(&self, start_time: OffsetDateTime) -> bool {
|
||||
self.guests
|
||||
.values()
|
||||
.chain([&self.host])
|
||||
.any(|collaborator| {
|
||||
collaborator
|
||||
.last_activity
|
||||
.map_or(false, |active_time| active_time > start_time)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn guest_connection_ids(&self) -> Vec<ConnectionId> {
|
||||
self.guests.keys().copied().collect()
|
||||
}
|
||||
@@ -1180,3 +1287,9 @@ impl Project {
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl Channel {
|
||||
fn connection_ids(&self) -> Vec<ConnectionId> {
|
||||
self.connection_ids.iter().copied().collect()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,8 +170,8 @@ impl ContactFinder {
|
||||
let this = cx.weak_handle();
|
||||
Self {
|
||||
picker: cx.add_view(|cx| {
|
||||
Picker::new("Search collaborator by username...", this, cx)
|
||||
.with_theme(|theme| theme.contact_finder.picker.clone())
|
||||
Picker::new(this, cx)
|
||||
.with_theme(|cx| &cx.global::<Settings>().theme.contact_finder.picker)
|
||||
}),
|
||||
potential_contacts: Arc::from([]),
|
||||
user_store,
|
||||
|
||||
@@ -175,9 +175,7 @@ impl ContactList {
|
||||
) -> Self {
|
||||
let filter_editor = cx.add_view(|cx| {
|
||||
let mut editor = Editor::single_line(
|
||||
Some(Arc::new(|theme| {
|
||||
theme.contact_list.user_query_editor.clone()
|
||||
})),
|
||||
Some(|theme| theme.contact_list.user_query_editor.clone()),
|
||||
cx,
|
||||
);
|
||||
editor.set_placeholder_text("Filter contacts", cx);
|
||||
|
||||
@@ -70,7 +70,7 @@ impl CommandPalette {
|
||||
})
|
||||
.collect();
|
||||
|
||||
let picker = cx.add_view(|cx| Picker::new("Execute a command...", this, cx));
|
||||
let picker = cx.add_view(|cx| Picker::new(this, cx));
|
||||
Self {
|
||||
picker,
|
||||
actions,
|
||||
|
||||
@@ -322,7 +322,7 @@ impl ProjectDiagnosticsEditor {
|
||||
);
|
||||
let excerpt_id = excerpts
|
||||
.insert_excerpts_after(
|
||||
prev_excerpt_id,
|
||||
&prev_excerpt_id,
|
||||
buffer.clone(),
|
||||
[ExcerptRange {
|
||||
context: excerpt_start..excerpt_end,
|
||||
@@ -384,7 +384,7 @@ impl ProjectDiagnosticsEditor {
|
||||
|
||||
groups_to_add.push(group_state);
|
||||
} else if let Some((group_ix, group_state)) = to_remove {
|
||||
excerpts.remove_excerpts(group_state.excerpts.iter().copied(), excerpts_cx);
|
||||
excerpts.remove_excerpts(group_state.excerpts.iter(), excerpts_cx);
|
||||
group_ixs_to_remove.push(group_ix);
|
||||
blocks_to_remove.extend(group_state.blocks.iter().copied());
|
||||
} else if let Some((_, group)) = to_keep {
|
||||
@@ -452,20 +452,15 @@ impl ProjectDiagnosticsEditor {
|
||||
} else {
|
||||
groups = self.path_states.get(path_ix)?.diagnostic_groups.as_slice();
|
||||
new_excerpt_ids_by_selection_id =
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| s.refresh());
|
||||
editor.change_selections(Some(Autoscroll::Fit), cx, |s| s.refresh());
|
||||
selections = editor.selections.all::<usize>(cx);
|
||||
}
|
||||
|
||||
// If any selection has lost its position, move it to start of the next primary diagnostic.
|
||||
let snapshot = editor.snapshot(cx);
|
||||
for selection in &mut selections {
|
||||
if let Some(new_excerpt_id) = new_excerpt_ids_by_selection_id.get(&selection.id) {
|
||||
let group_ix = match groups.binary_search_by(|probe| {
|
||||
probe
|
||||
.excerpts
|
||||
.last()
|
||||
.unwrap()
|
||||
.cmp(new_excerpt_id, &snapshot.buffer_snapshot)
|
||||
probe.excerpts.last().unwrap().cmp(new_excerpt_id)
|
||||
}) {
|
||||
Ok(ix) | Err(ix) => ix,
|
||||
};
|
||||
@@ -743,7 +738,7 @@ mod tests {
|
||||
DisplayPoint,
|
||||
};
|
||||
use gpui::TestAppContext;
|
||||
use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, PointUtf16, Unclipped};
|
||||
use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, PointUtf16};
|
||||
use serde_json::json;
|
||||
use unindent::Unindent as _;
|
||||
use workspace::AppState;
|
||||
@@ -793,7 +788,7 @@ mod tests {
|
||||
None,
|
||||
vec![
|
||||
DiagnosticEntry {
|
||||
range: Unclipped(PointUtf16::new(1, 8))..Unclipped(PointUtf16::new(1, 9)),
|
||||
range: PointUtf16::new(1, 8)..PointUtf16::new(1, 9),
|
||||
diagnostic: Diagnostic {
|
||||
message:
|
||||
"move occurs because `x` has type `Vec<char>`, which does not implement the `Copy` trait"
|
||||
@@ -806,7 +801,7 @@ mod tests {
|
||||
},
|
||||
},
|
||||
DiagnosticEntry {
|
||||
range: Unclipped(PointUtf16::new(2, 8))..Unclipped(PointUtf16::new(2, 9)),
|
||||
range: PointUtf16::new(2, 8)..PointUtf16::new(2, 9),
|
||||
diagnostic: Diagnostic {
|
||||
message:
|
||||
"move occurs because `y` has type `Vec<char>`, which does not implement the `Copy` trait"
|
||||
@@ -819,7 +814,7 @@ mod tests {
|
||||
},
|
||||
},
|
||||
DiagnosticEntry {
|
||||
range: Unclipped(PointUtf16::new(3, 6))..Unclipped(PointUtf16::new(3, 7)),
|
||||
range: PointUtf16::new(3, 6)..PointUtf16::new(3, 7),
|
||||
diagnostic: Diagnostic {
|
||||
message: "value moved here".to_string(),
|
||||
severity: DiagnosticSeverity::INFORMATION,
|
||||
@@ -830,7 +825,7 @@ mod tests {
|
||||
},
|
||||
},
|
||||
DiagnosticEntry {
|
||||
range: Unclipped(PointUtf16::new(4, 6))..Unclipped(PointUtf16::new(4, 7)),
|
||||
range: PointUtf16::new(4, 6)..PointUtf16::new(4, 7),
|
||||
diagnostic: Diagnostic {
|
||||
message: "value moved here".to_string(),
|
||||
severity: DiagnosticSeverity::INFORMATION,
|
||||
@@ -841,7 +836,7 @@ mod tests {
|
||||
},
|
||||
},
|
||||
DiagnosticEntry {
|
||||
range: Unclipped(PointUtf16::new(7, 6))..Unclipped(PointUtf16::new(7, 7)),
|
||||
range: PointUtf16::new(7, 6)..PointUtf16::new(7, 7),
|
||||
diagnostic: Diagnostic {
|
||||
message: "use of moved value\nvalue used here after move".to_string(),
|
||||
severity: DiagnosticSeverity::ERROR,
|
||||
@@ -852,7 +847,7 @@ mod tests {
|
||||
},
|
||||
},
|
||||
DiagnosticEntry {
|
||||
range: Unclipped(PointUtf16::new(8, 6))..Unclipped(PointUtf16::new(8, 7)),
|
||||
range: PointUtf16::new(8, 6)..PointUtf16::new(8, 7),
|
||||
diagnostic: Diagnostic {
|
||||
message: "use of moved value\nvalue used here after move".to_string(),
|
||||
severity: DiagnosticSeverity::ERROR,
|
||||
@@ -944,7 +939,7 @@ mod tests {
|
||||
PathBuf::from("/test/consts.rs"),
|
||||
None,
|
||||
vec![DiagnosticEntry {
|
||||
range: Unclipped(PointUtf16::new(0, 15))..Unclipped(PointUtf16::new(0, 15)),
|
||||
range: PointUtf16::new(0, 15)..PointUtf16::new(0, 15),
|
||||
diagnostic: Diagnostic {
|
||||
message: "mismatched types\nexpected `usize`, found `char`".to_string(),
|
||||
severity: DiagnosticSeverity::ERROR,
|
||||
@@ -1045,8 +1040,7 @@ mod tests {
|
||||
None,
|
||||
vec![
|
||||
DiagnosticEntry {
|
||||
range: Unclipped(PointUtf16::new(0, 15))
|
||||
..Unclipped(PointUtf16::new(0, 15)),
|
||||
range: PointUtf16::new(0, 15)..PointUtf16::new(0, 15),
|
||||
diagnostic: Diagnostic {
|
||||
message: "mismatched types\nexpected `usize`, found `char`"
|
||||
.to_string(),
|
||||
@@ -1058,8 +1052,7 @@ mod tests {
|
||||
},
|
||||
},
|
||||
DiagnosticEntry {
|
||||
range: Unclipped(PointUtf16::new(1, 15))
|
||||
..Unclipped(PointUtf16::new(1, 15)),
|
||||
range: PointUtf16::new(1, 15)..PointUtf16::new(1, 15),
|
||||
diagnostic: Diagnostic {
|
||||
message: "unresolved name `c`".to_string(),
|
||||
severity: DiagnosticSeverity::ERROR,
|
||||
|
||||
@@ -12,4 +12,4 @@ collections = { path = "../collections" }
|
||||
gpui = { path = "../gpui" }
|
||||
|
||||
[dev-dependencies]
|
||||
gpui = { path = "../gpui", features = ["test-support"] }
|
||||
gpui = { path = "../gpui", features = ["test-support"] }
|
||||
@@ -2,55 +2,29 @@ use std::{any::Any, rc::Rc};
|
||||
|
||||
use collections::HashSet;
|
||||
use gpui::{
|
||||
elements::{Empty, MouseEventHandler, Overlay},
|
||||
geometry::{rect::RectF, vector::Vector2F},
|
||||
scene::{MouseDown, MouseDrag},
|
||||
elements::{MouseEventHandler, Overlay},
|
||||
geometry::vector::Vector2F,
|
||||
scene::MouseDrag,
|
||||
CursorStyle, Element, ElementBox, EventContext, MouseButton, MutableAppContext, RenderContext,
|
||||
View, WeakViewHandle,
|
||||
};
|
||||
|
||||
enum State<V: View> {
|
||||
Down {
|
||||
region_offset: Vector2F,
|
||||
region: RectF,
|
||||
},
|
||||
Dragging {
|
||||
window_id: usize,
|
||||
position: Vector2F,
|
||||
region_offset: Vector2F,
|
||||
region: RectF,
|
||||
payload: Rc<dyn Any + 'static>,
|
||||
render: Rc<dyn Fn(Rc<dyn Any>, &mut RenderContext<V>) -> ElementBox>,
|
||||
},
|
||||
Canceled,
|
||||
struct State<V: View> {
|
||||
window_id: usize,
|
||||
position: Vector2F,
|
||||
region_offset: Vector2F,
|
||||
payload: Rc<dyn Any + 'static>,
|
||||
render: Rc<dyn Fn(Rc<dyn Any>, &mut RenderContext<V>) -> ElementBox>,
|
||||
}
|
||||
|
||||
impl<V: View> Clone for State<V> {
|
||||
fn clone(&self) -> Self {
|
||||
match self {
|
||||
&State::Down {
|
||||
region_offset,
|
||||
region,
|
||||
} => State::Down {
|
||||
region_offset,
|
||||
region,
|
||||
},
|
||||
State::Dragging {
|
||||
window_id,
|
||||
position,
|
||||
region_offset,
|
||||
region,
|
||||
payload,
|
||||
render,
|
||||
} => Self::Dragging {
|
||||
window_id: window_id.clone(),
|
||||
position: position.clone(),
|
||||
region_offset: region_offset.clone(),
|
||||
region: region.clone(),
|
||||
payload: payload.clone(),
|
||||
render: render.clone(),
|
||||
},
|
||||
State::Canceled => State::Canceled,
|
||||
Self {
|
||||
window_id: self.window_id.clone(),
|
||||
position: self.position.clone(),
|
||||
region_offset: self.region_offset.clone(),
|
||||
payload: self.payload.clone(),
|
||||
render: self.render.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -75,36 +49,24 @@ impl<V: View> DragAndDrop<V> {
|
||||
}
|
||||
|
||||
pub fn currently_dragged<T: Any>(&self, window_id: usize) -> Option<(Vector2F, Rc<T>)> {
|
||||
self.currently_dragged.as_ref().and_then(|state| {
|
||||
if let State::Dragging {
|
||||
position,
|
||||
payload,
|
||||
window_id: window_dragged_from,
|
||||
..
|
||||
} = state
|
||||
{
|
||||
self.currently_dragged.as_ref().and_then(
|
||||
|State {
|
||||
position,
|
||||
payload,
|
||||
window_id: window_dragged_from,
|
||||
..
|
||||
}| {
|
||||
if &window_id != window_dragged_from {
|
||||
return None;
|
||||
}
|
||||
|
||||
payload
|
||||
.is::<T>()
|
||||
.then(|| payload.clone().downcast::<T>().ok())
|
||||
.flatten()
|
||||
.clone()
|
||||
.downcast::<T>()
|
||||
.ok()
|
||||
.map(|payload| (position.clone(), payload))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn drag_started(event: MouseDown, cx: &mut EventContext) {
|
||||
cx.update_global(|this: &mut Self, _| {
|
||||
this.currently_dragged = Some(State::Down {
|
||||
region_offset: event.region.origin() - event.position,
|
||||
region: event.region,
|
||||
});
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub fn dragging<T: Any>(
|
||||
@@ -114,132 +76,75 @@ impl<V: View> DragAndDrop<V> {
|
||||
render: Rc<impl 'static + Fn(&T, &mut RenderContext<V>) -> ElementBox>,
|
||||
) {
|
||||
let window_id = cx.window_id();
|
||||
cx.update_global(|this: &mut Self, cx| {
|
||||
this.notify_containers_for_window(window_id, cx);
|
||||
cx.update_global::<Self, _, _>(|this, cx| {
|
||||
let region_offset = if let Some(previous_state) = this.currently_dragged.as_ref() {
|
||||
previous_state.region_offset
|
||||
} else {
|
||||
event.region.origin() - event.prev_mouse_position
|
||||
};
|
||||
|
||||
match this.currently_dragged.as_ref() {
|
||||
Some(&State::Down {
|
||||
region_offset,
|
||||
region,
|
||||
})
|
||||
| Some(&State::Dragging {
|
||||
region_offset,
|
||||
region,
|
||||
..
|
||||
}) => {
|
||||
this.currently_dragged = Some(State::Dragging {
|
||||
window_id,
|
||||
region_offset,
|
||||
region,
|
||||
position: event.position,
|
||||
payload,
|
||||
render: Rc::new(move |payload, cx| {
|
||||
render(payload.downcast_ref::<T>().unwrap(), cx)
|
||||
}),
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
this.currently_dragged = Some(State {
|
||||
window_id,
|
||||
region_offset,
|
||||
position: event.position,
|
||||
payload,
|
||||
render: Rc::new(move |payload, cx| {
|
||||
render(payload.downcast_ref::<T>().unwrap(), cx)
|
||||
}),
|
||||
});
|
||||
|
||||
this.notify_containers_for_window(window_id, cx);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn render(cx: &mut RenderContext<V>) -> Option<ElementBox> {
|
||||
enum DraggedElementHandler {}
|
||||
cx.global::<Self>()
|
||||
.currently_dragged
|
||||
.clone()
|
||||
.and_then(|state| {
|
||||
match state {
|
||||
State::Down { .. } => None,
|
||||
State::Dragging {
|
||||
window_id,
|
||||
region_offset,
|
||||
position,
|
||||
region,
|
||||
payload,
|
||||
render,
|
||||
} => {
|
||||
if cx.window_id() != window_id {
|
||||
return None;
|
||||
}
|
||||
let currently_dragged = cx.global::<Self>().currently_dragged.clone();
|
||||
|
||||
let position = position + region_offset;
|
||||
Some(
|
||||
Overlay::new(
|
||||
MouseEventHandler::<DraggedElementHandler>::new(0, cx, |_, cx| {
|
||||
render(payload, cx)
|
||||
})
|
||||
.with_cursor_style(CursorStyle::Arrow)
|
||||
.on_up(MouseButton::Left, |_, cx| {
|
||||
cx.defer(|cx| {
|
||||
cx.update_global::<Self, _, _>(|this, cx| {
|
||||
this.finish_dragging(cx)
|
||||
});
|
||||
});
|
||||
cx.propagate_event();
|
||||
})
|
||||
.on_up_out(MouseButton::Left, |_, cx| {
|
||||
cx.defer(|cx| {
|
||||
cx.update_global::<Self, _, _>(|this, cx| {
|
||||
this.finish_dragging(cx)
|
||||
});
|
||||
});
|
||||
})
|
||||
// Don't block hover events or invalidations
|
||||
.with_hoverable(false)
|
||||
.constrained()
|
||||
.with_width(region.width())
|
||||
.with_height(region.height())
|
||||
.boxed(),
|
||||
)
|
||||
.with_anchor_position(position)
|
||||
.boxed(),
|
||||
)
|
||||
}
|
||||
currently_dragged.and_then(
|
||||
|State {
|
||||
window_id,
|
||||
region_offset,
|
||||
position,
|
||||
payload,
|
||||
render,
|
||||
}| {
|
||||
if cx.window_id() != window_id {
|
||||
return None;
|
||||
}
|
||||
|
||||
State::Canceled => Some(
|
||||
MouseEventHandler::<DraggedElementHandler>::new(0, cx, |_, _| {
|
||||
Empty::new()
|
||||
.constrained()
|
||||
.with_width(0.)
|
||||
.with_height(0.)
|
||||
.boxed()
|
||||
let position = position + region_offset;
|
||||
|
||||
enum DraggedElementHandler {}
|
||||
Some(
|
||||
Overlay::new(
|
||||
MouseEventHandler::<DraggedElementHandler>::new(0, cx, |_, cx| {
|
||||
render(payload, cx)
|
||||
})
|
||||
.with_cursor_style(CursorStyle::Arrow)
|
||||
.on_up(MouseButton::Left, |_, cx| {
|
||||
cx.defer(|cx| {
|
||||
cx.update_global::<Self, _, _>(|this, _| {
|
||||
this.currently_dragged = None;
|
||||
});
|
||||
cx.update_global::<Self, _, _>(|this, cx| this.stop_dragging(cx));
|
||||
});
|
||||
cx.propagate_event();
|
||||
})
|
||||
.on_up_out(MouseButton::Left, |_, cx| {
|
||||
cx.defer(|cx| {
|
||||
cx.update_global::<Self, _, _>(|this, _| {
|
||||
this.currently_dragged = None;
|
||||
});
|
||||
cx.update_global::<Self, _, _>(|this, cx| this.stop_dragging(cx));
|
||||
});
|
||||
})
|
||||
// Don't block hover events or invalidations
|
||||
.with_hoverable(false)
|
||||
.boxed(),
|
||||
),
|
||||
}
|
||||
})
|
||||
)
|
||||
.with_anchor_position(position)
|
||||
.boxed(),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub fn cancel_dragging<P: Any>(&mut self, cx: &mut MutableAppContext) {
|
||||
if let Some(State::Dragging {
|
||||
payload, window_id, ..
|
||||
}) = &self.currently_dragged
|
||||
{
|
||||
if payload.is::<P>() {
|
||||
let window_id = *window_id;
|
||||
self.currently_dragged = Some(State::Canceled);
|
||||
self.notify_containers_for_window(window_id, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn finish_dragging(&mut self, cx: &mut MutableAppContext) {
|
||||
if let Some(State::Dragging { window_id, .. }) = self.currently_dragged.take() {
|
||||
fn stop_dragging(&mut self, cx: &mut MutableAppContext) {
|
||||
if let Some(State { window_id, .. }) = self.currently_dragged.take() {
|
||||
self.notify_containers_for_window(window_id, cx);
|
||||
}
|
||||
}
|
||||
@@ -279,11 +184,7 @@ impl<Tag> Draggable for MouseEventHandler<Tag> {
|
||||
{
|
||||
let payload = Rc::new(payload);
|
||||
let render = Rc::new(render);
|
||||
self.on_down(MouseButton::Left, move |e, cx| {
|
||||
cx.propagate_event();
|
||||
DragAndDrop::<V>::drag_started(e, cx);
|
||||
})
|
||||
.on_drag(MouseButton::Left, move |e, cx| {
|
||||
self.on_drag(MouseButton::Left, move |e, cx| {
|
||||
let payload = payload.clone();
|
||||
let render = render.clone();
|
||||
DragAndDrop::<V>::dragging(e, payload, cx, render)
|
||||
|
||||
@@ -93,9 +93,6 @@ impl BlinkManager {
|
||||
|
||||
pub fn enable(&mut self, cx: &mut ModelContext<Self>) {
|
||||
self.enabled = true;
|
||||
// Set cursors as invisible and start blinking: this causes cursors
|
||||
// to be visible during the next render.
|
||||
self.visible = false;
|
||||
self.blink_cursors(self.blink_epoch, cx);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ use super::{
|
||||
wrap_map::{self, WrapEdit, WrapPoint, WrapSnapshot},
|
||||
TextHighlights,
|
||||
};
|
||||
use crate::{Anchor, ExcerptId, ExcerptRange, ToPoint as _};
|
||||
use crate::{Anchor, ExcerptRange, ToPoint as _};
|
||||
use collections::{Bound, HashMap, HashSet};
|
||||
use gpui::{ElementBox, RenderContext};
|
||||
use language::{BufferSnapshot, Chunk, Patch, Point};
|
||||
@@ -107,7 +107,7 @@ struct Transform {
|
||||
pub enum TransformBlock {
|
||||
Custom(Arc<Block>),
|
||||
ExcerptHeader {
|
||||
id: ExcerptId,
|
||||
key: usize,
|
||||
buffer: BufferSnapshot,
|
||||
range: ExcerptRange<text::Anchor>,
|
||||
height: u8,
|
||||
@@ -371,7 +371,7 @@ impl BlockMap {
|
||||
.make_wrap_point(Point::new(excerpt_boundary.row, 0), Bias::Left)
|
||||
.row(),
|
||||
TransformBlock::ExcerptHeader {
|
||||
id: excerpt_boundary.id,
|
||||
key: excerpt_boundary.key,
|
||||
buffer: excerpt_boundary.buffer,
|
||||
range: excerpt_boundary.range,
|
||||
height: if excerpt_boundary.starts_new_buffer {
|
||||
|
||||
@@ -73,7 +73,6 @@ use std::{
|
||||
mem,
|
||||
num::NonZeroU32,
|
||||
ops::{Deref, DerefMut, Range, RangeInclusive},
|
||||
path::Path,
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
@@ -188,7 +187,7 @@ actions!(
|
||||
Paste,
|
||||
Undo,
|
||||
Redo,
|
||||
NextScreen,
|
||||
CenterScreen,
|
||||
MoveUp,
|
||||
PageUp,
|
||||
MoveDown,
|
||||
@@ -308,8 +307,7 @@ pub fn init(cx: &mut MutableAppContext) {
|
||||
cx.add_action(Editor::move_down);
|
||||
cx.add_action(Editor::move_page_down);
|
||||
cx.add_action(Editor::page_down);
|
||||
cx.add_action(Editor::next_screen);
|
||||
|
||||
cx.add_action(Editor::center_screen);
|
||||
cx.add_action(Editor::move_left);
|
||||
cx.add_action(Editor::move_right);
|
||||
cx.add_action(Editor::move_to_previous_word_start);
|
||||
@@ -411,42 +409,9 @@ pub enum SelectMode {
|
||||
|
||||
#[derive(PartialEq, Eq)]
|
||||
pub enum Autoscroll {
|
||||
Next,
|
||||
Strategy(AutoscrollStrategy),
|
||||
}
|
||||
|
||||
impl Autoscroll {
|
||||
pub fn fit() -> Self {
|
||||
Self::Strategy(AutoscrollStrategy::Fit)
|
||||
}
|
||||
|
||||
pub fn newest() -> Self {
|
||||
Self::Strategy(AutoscrollStrategy::Newest)
|
||||
}
|
||||
|
||||
pub fn center() -> Self {
|
||||
Self::Strategy(AutoscrollStrategy::Center)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Default)]
|
||||
pub enum AutoscrollStrategy {
|
||||
Fit,
|
||||
Newest,
|
||||
#[default]
|
||||
Center,
|
||||
Top,
|
||||
Bottom,
|
||||
}
|
||||
|
||||
impl AutoscrollStrategy {
|
||||
fn next(&self) -> Self {
|
||||
match self {
|
||||
AutoscrollStrategy::Center => AutoscrollStrategy::Top,
|
||||
AutoscrollStrategy::Top => AutoscrollStrategy::Bottom,
|
||||
_ => AutoscrollStrategy::Center,
|
||||
}
|
||||
}
|
||||
Newest,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
@@ -472,7 +437,8 @@ pub struct EditorStyle {
|
||||
|
||||
type CompletionId = usize;
|
||||
|
||||
type GetFieldEditorTheme = dyn Fn(&theme::Theme) -> theme::FieldEditor;
|
||||
pub type GetFieldEditorTheme = fn(&theme::Theme) -> theme::FieldEditor;
|
||||
|
||||
type OverrideTextStyle = dyn Fn(&EditorStyle) -> Option<HighlightStyle>;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
@@ -557,7 +523,7 @@ pub struct Editor {
|
||||
scroll_top_anchor: Anchor,
|
||||
autoscroll_request: Option<(Autoscroll, bool)>,
|
||||
soft_wrap_mode_override: Option<settings::SoftWrap>,
|
||||
get_field_editor_theme: Option<Arc<GetFieldEditorTheme>>,
|
||||
get_field_editor_theme: Option<GetFieldEditorTheme>,
|
||||
override_text_style: Option<Box<OverrideTextStyle>>,
|
||||
project: Option<ModelHandle<Project>>,
|
||||
focused: bool,
|
||||
@@ -588,7 +554,6 @@ pub struct Editor {
|
||||
hover_state: HoverState,
|
||||
link_go_to_definition_state: LinkGoToDefinitionState,
|
||||
visible_line_count: Option<f32>,
|
||||
last_autoscroll: Option<(Vector2F, f32, f32, AutoscrollStrategy)>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
@@ -1105,7 +1070,7 @@ enum GotoDefinitionKind {
|
||||
|
||||
impl Editor {
|
||||
pub fn single_line(
|
||||
field_editor_style: Option<Arc<GetFieldEditorTheme>>,
|
||||
field_editor_style: Option<GetFieldEditorTheme>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let buffer = cx.add_model(|cx| Buffer::new(0, String::new(), cx));
|
||||
@@ -1115,7 +1080,7 @@ impl Editor {
|
||||
|
||||
pub fn auto_height(
|
||||
max_lines: usize,
|
||||
field_editor_style: Option<Arc<GetFieldEditorTheme>>,
|
||||
field_editor_style: Option<GetFieldEditorTheme>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let buffer = cx.add_model(|cx| Buffer::new(0, String::new(), cx));
|
||||
@@ -1151,7 +1116,7 @@ impl Editor {
|
||||
self.mode,
|
||||
self.buffer.clone(),
|
||||
self.project.clone(),
|
||||
self.get_field_editor_theme.clone(),
|
||||
self.get_field_editor_theme,
|
||||
cx,
|
||||
);
|
||||
self.display_map.update(cx, |display_map, cx| {
|
||||
@@ -1162,7 +1127,7 @@ impl Editor {
|
||||
});
|
||||
clone.selections.set_state(&self.selections);
|
||||
clone.scroll_position = self.scroll_position;
|
||||
clone.scroll_top_anchor = self.scroll_top_anchor;
|
||||
clone.scroll_top_anchor = self.scroll_top_anchor.clone();
|
||||
clone.searchable = self.searchable;
|
||||
clone
|
||||
}
|
||||
@@ -1171,12 +1136,12 @@ impl Editor {
|
||||
mode: EditorMode,
|
||||
buffer: ModelHandle<MultiBuffer>,
|
||||
project: Option<ModelHandle<Project>>,
|
||||
get_field_editor_theme: Option<Arc<GetFieldEditorTheme>>,
|
||||
get_field_editor_theme: Option<GetFieldEditorTheme>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let display_map = cx.add_model(|cx| {
|
||||
let settings = cx.global::<Settings>();
|
||||
let style = build_style(&*settings, get_field_editor_theme.as_deref(), None, cx);
|
||||
let style = build_style(&*settings, get_field_editor_theme, None, cx);
|
||||
DisplayMap::new(
|
||||
buffer.clone(),
|
||||
style.text.font_id,
|
||||
@@ -1241,7 +1206,6 @@ impl Editor {
|
||||
hover_state: Default::default(),
|
||||
link_go_to_definition_state: Default::default(),
|
||||
visible_line_count: None,
|
||||
last_autoscroll: None,
|
||||
_subscriptions: vec![
|
||||
cx.observe(&buffer, Self::on_buffer_changed),
|
||||
cx.subscribe(&buffer, Self::on_buffer_event),
|
||||
@@ -1305,7 +1269,7 @@ impl Editor {
|
||||
display_snapshot: self.display_map.update(cx, |map, cx| map.snapshot(cx)),
|
||||
ongoing_scroll: self.ongoing_scroll,
|
||||
scroll_position: self.scroll_position,
|
||||
scroll_top_anchor: self.scroll_top_anchor,
|
||||
scroll_top_anchor: self.scroll_top_anchor.clone(),
|
||||
placeholder_text: self.placeholder_text.clone(),
|
||||
is_focused: self
|
||||
.handle
|
||||
@@ -1325,7 +1289,7 @@ impl Editor {
|
||||
fn style(&self, cx: &AppContext) -> EditorStyle {
|
||||
build_style(
|
||||
cx.global::<Settings>(),
|
||||
self.get_field_editor_theme.as_deref(),
|
||||
self.get_field_editor_theme,
|
||||
self.override_text_style.as_deref(),
|
||||
cx,
|
||||
)
|
||||
@@ -1472,7 +1436,7 @@ impl Editor {
|
||||
if let Some(highlighted_rows) = &self.highlighted_rows {
|
||||
first_cursor_top = highlighted_rows.start as f32;
|
||||
last_cursor_bottom = first_cursor_top + 1.;
|
||||
} else if autoscroll == Autoscroll::newest() {
|
||||
} else if autoscroll == Autoscroll::Newest {
|
||||
let newest_selection = self.selections.newest::<Point>(cx);
|
||||
first_cursor_top = newest_selection.head().to_display_point(&display_map).row() as f32;
|
||||
last_cursor_bottom = first_cursor_top + 1.;
|
||||
@@ -1502,27 +1466,8 @@ impl Editor {
|
||||
return false;
|
||||
}
|
||||
|
||||
let strategy = match autoscroll {
|
||||
Autoscroll::Strategy(strategy) => strategy,
|
||||
Autoscroll::Next => {
|
||||
let last_autoscroll = &self.last_autoscroll;
|
||||
if let Some(last_autoscroll) = last_autoscroll {
|
||||
if self.scroll_position == last_autoscroll.0
|
||||
&& first_cursor_top == last_autoscroll.1
|
||||
&& last_cursor_bottom == last_autoscroll.2
|
||||
{
|
||||
last_autoscroll.3.next()
|
||||
} else {
|
||||
AutoscrollStrategy::default()
|
||||
}
|
||||
} else {
|
||||
AutoscrollStrategy::default()
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
match strategy {
|
||||
AutoscrollStrategy::Fit | AutoscrollStrategy::Newest => {
|
||||
match autoscroll {
|
||||
Autoscroll::Fit | Autoscroll::Newest => {
|
||||
let margin = margin.min(self.vertical_scroll_margin);
|
||||
let target_top = (first_cursor_top - margin).max(0.0);
|
||||
let target_bottom = last_cursor_bottom + margin;
|
||||
@@ -1537,27 +1482,12 @@ impl Editor {
|
||||
self.set_scroll_position_internal(scroll_position, local, cx);
|
||||
}
|
||||
}
|
||||
AutoscrollStrategy::Center => {
|
||||
Autoscroll::Center => {
|
||||
scroll_position.set_y((first_cursor_top - margin).max(0.0));
|
||||
self.set_scroll_position_internal(scroll_position, local, cx);
|
||||
}
|
||||
AutoscrollStrategy::Top => {
|
||||
scroll_position.set_y((first_cursor_top).max(0.0));
|
||||
self.set_scroll_position_internal(scroll_position, local, cx);
|
||||
}
|
||||
AutoscrollStrategy::Bottom => {
|
||||
scroll_position.set_y((last_cursor_bottom - visible_lines).max(0.0));
|
||||
self.set_scroll_position_internal(scroll_position, local, cx);
|
||||
}
|
||||
}
|
||||
|
||||
self.last_autoscroll = Some((
|
||||
self.scroll_position,
|
||||
first_cursor_top,
|
||||
last_cursor_bottom,
|
||||
strategy,
|
||||
));
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
@@ -1791,19 +1721,21 @@ impl Editor {
|
||||
.pending_anchor()
|
||||
.expect("extend_selection not called with pending selection");
|
||||
if position >= tail {
|
||||
pending_selection.start = tail_anchor;
|
||||
pending_selection.start = tail_anchor.clone();
|
||||
} else {
|
||||
pending_selection.end = tail_anchor;
|
||||
pending_selection.end = tail_anchor.clone();
|
||||
pending_selection.reversed = true;
|
||||
}
|
||||
|
||||
let mut pending_mode = self.selections.pending_mode().unwrap();
|
||||
match &mut pending_mode {
|
||||
SelectMode::Word(range) | SelectMode::Line(range) => *range = tail_anchor..tail_anchor,
|
||||
SelectMode::Word(range) | SelectMode::Line(range) => {
|
||||
*range = tail_anchor.clone()..tail_anchor
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
s.set_pending(pending_selection, pending_mode)
|
||||
});
|
||||
}
|
||||
@@ -1864,7 +1796,7 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
self.change_selections(auto_scroll.then(|| Autoscroll::newest()), cx, |s| {
|
||||
self.change_selections(auto_scroll.then(|| Autoscroll::Newest), cx, |s| {
|
||||
if !add {
|
||||
s.clear_disjoint();
|
||||
} else if click_count > 1 {
|
||||
@@ -2081,7 +2013,7 @@ impl Editor {
|
||||
return;
|
||||
}
|
||||
|
||||
if self.change_selections(Some(Autoscroll::fit()), cx, |s| s.try_cancel()) {
|
||||
if self.change_selections(Some(Autoscroll::Fit), cx, |s| s.try_cancel()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -2143,9 +2075,10 @@ impl Editor {
|
||||
));
|
||||
if following_text_allows_autoclose && preceding_text_matches_prefix {
|
||||
let anchor = snapshot.anchor_before(selection.end);
|
||||
new_selections.push((selection.map(|_| anchor), text.len()));
|
||||
new_selections
|
||||
.push((selection.map(|_| anchor.clone()), text.len()));
|
||||
new_autoclose_regions.push((
|
||||
anchor,
|
||||
anchor.clone(),
|
||||
text.len(),
|
||||
selection.id,
|
||||
bracket_pair.clone(),
|
||||
@@ -2166,8 +2099,10 @@ impl Editor {
|
||||
&& text.as_ref() == region.pair.end.as_str();
|
||||
if should_skip {
|
||||
let anchor = snapshot.anchor_after(selection.end);
|
||||
new_selections
|
||||
.push((selection.map(|_| anchor), region.pair.end.len()));
|
||||
new_selections.push((
|
||||
selection.map(|_| anchor.clone()),
|
||||
region.pair.end.len(),
|
||||
));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -2199,7 +2134,7 @@ impl Editor {
|
||||
// text with the given input and move the selection to the end of the
|
||||
// newly inserted text.
|
||||
let anchor = snapshot.anchor_after(selection.end);
|
||||
new_selections.push((selection.map(|_| anchor), 0));
|
||||
new_selections.push((selection.map(|_| anchor.clone()), 0));
|
||||
edits.push((selection.start..selection.end, text.clone()));
|
||||
}
|
||||
|
||||
@@ -2244,7 +2179,7 @@ impl Editor {
|
||||
}
|
||||
|
||||
drop(snapshot);
|
||||
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(new_selections));
|
||||
this.change_selections(Some(Autoscroll::Fit), cx, |s| s.select(new_selections));
|
||||
this.trigger_completion_on_input(&text, cx);
|
||||
});
|
||||
}
|
||||
@@ -2301,7 +2236,7 @@ impl Editor {
|
||||
}
|
||||
|
||||
let anchor = buffer.anchor_after(end);
|
||||
let new_selection = selection.map(|_| anchor);
|
||||
let new_selection = selection.map(|_| anchor.clone());
|
||||
(
|
||||
(start..end, new_text),
|
||||
(insert_extra_newline, new_selection),
|
||||
@@ -2324,7 +2259,7 @@ impl Editor {
|
||||
})
|
||||
.collect();
|
||||
|
||||
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(new_selections));
|
||||
this.change_selections(Some(Autoscroll::Fit), cx, |s| s.select(new_selections));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2354,7 +2289,7 @@ impl Editor {
|
||||
self.transact(cx, |editor, cx| {
|
||||
editor.edit_with_autoindent(edits, cx);
|
||||
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
let mut index = 0;
|
||||
s.move_cursors_with(|map, _, _| {
|
||||
let row = rows[index];
|
||||
@@ -2381,7 +2316,7 @@ impl Editor {
|
||||
.iter()
|
||||
.map(|s| {
|
||||
let anchor = snapshot.anchor_after(s.end);
|
||||
s.map(|_| anchor)
|
||||
s.map(|_| anchor.clone())
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
@@ -2395,7 +2330,7 @@ impl Editor {
|
||||
anchors
|
||||
});
|
||||
|
||||
this.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
this.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
s.select_anchors(selection_anchors);
|
||||
})
|
||||
});
|
||||
@@ -3091,7 +3026,7 @@ impl Editor {
|
||||
});
|
||||
|
||||
if let Some(tabstop) = tabstops.first() {
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
s.select_ranges(tabstop.iter().cloned());
|
||||
});
|
||||
self.snippet_stack.push(SnippetState {
|
||||
@@ -3132,7 +3067,7 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
if let Some(current_ranges) = snippet.ranges.get(snippet.active_index) {
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
s.select_anchor_ranges(current_ranges.iter().cloned())
|
||||
});
|
||||
// If snippet state is not at the last tabstop, push it back on the stack
|
||||
@@ -3197,14 +3132,14 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections));
|
||||
this.change_selections(Some(Autoscroll::Fit), cx, |s| s.select(selections));
|
||||
this.insert("", cx);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn delete(&mut self, _: &Delete, cx: &mut ViewContext<Self>) {
|
||||
self.transact(cx, |this, cx| {
|
||||
this.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
this.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
let line_mode = s.line_mode;
|
||||
s.move_with(|map, selection| {
|
||||
if selection.is_empty() && !line_mode {
|
||||
@@ -3298,7 +3233,7 @@ impl Editor {
|
||||
|
||||
self.transact(cx, |this, cx| {
|
||||
this.buffer.update(cx, |b, cx| b.edit(edits, None, cx));
|
||||
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections))
|
||||
this.change_selections(Some(Autoscroll::Fit), cx, |s| s.select(selections))
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3321,7 +3256,7 @@ impl Editor {
|
||||
|
||||
self.transact(cx, |this, cx| {
|
||||
this.buffer.update(cx, |b, cx| b.edit(edits, None, cx));
|
||||
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections));
|
||||
this.change_selections(Some(Autoscroll::Fit), cx, |s| s.select(selections));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3453,7 +3388,7 @@ impl Editor {
|
||||
);
|
||||
});
|
||||
let selections = this.selections.all::<usize>(cx);
|
||||
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections));
|
||||
this.change_selections(Some(Autoscroll::Fit), cx, |s| s.select(selections));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3533,7 +3468,7 @@ impl Editor {
|
||||
})
|
||||
.collect();
|
||||
|
||||
this.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
this.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
s.select(new_selections);
|
||||
});
|
||||
});
|
||||
@@ -3575,7 +3510,7 @@ impl Editor {
|
||||
buffer.edit(edits, None, cx);
|
||||
});
|
||||
|
||||
this.request_autoscroll(Autoscroll::fit(), cx);
|
||||
this.request_autoscroll(Autoscroll::Fit, cx);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3645,7 +3580,7 @@ impl Editor {
|
||||
String::new(),
|
||||
));
|
||||
let insertion_anchor = buffer.anchor_after(insertion_point);
|
||||
edits.push((insertion_anchor..insertion_anchor, text));
|
||||
edits.push((insertion_anchor.clone()..insertion_anchor, text));
|
||||
|
||||
let row_delta = range_to_move.start.row - insertion_point.row + 1;
|
||||
|
||||
@@ -3685,7 +3620,7 @@ impl Editor {
|
||||
}
|
||||
});
|
||||
this.fold_ranges(refold_ranges, cx);
|
||||
this.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
this.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
s.select(new_selections);
|
||||
})
|
||||
});
|
||||
@@ -3750,7 +3685,7 @@ impl Editor {
|
||||
String::new(),
|
||||
));
|
||||
let insertion_anchor = buffer.anchor_after(insertion_point);
|
||||
edits.push((insertion_anchor..insertion_anchor, text));
|
||||
edits.push((insertion_anchor.clone()..insertion_anchor, text));
|
||||
|
||||
let row_delta = insertion_point.row - range_to_move.end.row + 1;
|
||||
|
||||
@@ -3790,13 +3725,13 @@ impl Editor {
|
||||
}
|
||||
});
|
||||
this.fold_ranges(refold_ranges, cx);
|
||||
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(new_selections));
|
||||
this.change_selections(Some(Autoscroll::Fit), cx, |s| s.select(new_selections));
|
||||
});
|
||||
}
|
||||
|
||||
pub fn transpose(&mut self, _: &Transpose, cx: &mut ViewContext<Self>) {
|
||||
self.transact(cx, |this, cx| {
|
||||
let edits = this.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
let edits = this.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
let mut edits: Vec<(Range<usize>, String)> = Default::default();
|
||||
let line_mode = s.line_mode;
|
||||
s.move_with(|display_map, selection| {
|
||||
@@ -3840,7 +3775,7 @@ impl Editor {
|
||||
this.buffer
|
||||
.update(cx, |buffer, cx| buffer.edit(edits, None, cx));
|
||||
let selections = this.selections.all::<usize>(cx);
|
||||
this.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
this.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
s.select(selections);
|
||||
});
|
||||
});
|
||||
@@ -3874,7 +3809,7 @@ impl Editor {
|
||||
}
|
||||
|
||||
self.transact(cx, |this, cx| {
|
||||
this.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
this.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
s.select(selections);
|
||||
});
|
||||
this.insert("", cx);
|
||||
@@ -3989,7 +3924,7 @@ impl Editor {
|
||||
});
|
||||
|
||||
let selections = this.selections.all::<usize>(cx);
|
||||
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections));
|
||||
this.change_selections(Some(Autoscroll::Fit), cx, |s| s.select(selections));
|
||||
} else {
|
||||
this.insert(&clipboard_text, cx);
|
||||
}
|
||||
@@ -4004,7 +3939,7 @@ impl Editor {
|
||||
s.select_anchors(selections.to_vec());
|
||||
});
|
||||
}
|
||||
self.request_autoscroll(Autoscroll::fit(), cx);
|
||||
self.request_autoscroll(Autoscroll::Fit, cx);
|
||||
self.unmark_text(cx);
|
||||
cx.emit(Event::Edited);
|
||||
}
|
||||
@@ -4018,7 +3953,7 @@ impl Editor {
|
||||
s.select_anchors(selections.to_vec());
|
||||
});
|
||||
}
|
||||
self.request_autoscroll(Autoscroll::fit(), cx);
|
||||
self.request_autoscroll(Autoscroll::Fit, cx);
|
||||
self.unmark_text(cx);
|
||||
cx.emit(Event::Edited);
|
||||
}
|
||||
@@ -4030,7 +3965,7 @@ impl Editor {
|
||||
}
|
||||
|
||||
pub fn move_left(&mut self, _: &MoveLeft, cx: &mut ViewContext<Self>) {
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
let line_mode = s.line_mode;
|
||||
s.move_with(|map, selection| {
|
||||
let cursor = if selection.is_empty() && !line_mode {
|
||||
@@ -4044,13 +3979,13 @@ impl Editor {
|
||||
}
|
||||
|
||||
pub fn select_left(&mut self, _: &SelectLeft, cx: &mut ViewContext<Self>) {
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
s.move_heads_with(|map, head, _| (movement::left(map, head), SelectionGoal::None));
|
||||
})
|
||||
}
|
||||
|
||||
pub fn move_right(&mut self, _: &MoveRight, cx: &mut ViewContext<Self>) {
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
let line_mode = s.line_mode;
|
||||
s.move_with(|map, selection| {
|
||||
let cursor = if selection.is_empty() && !line_mode {
|
||||
@@ -4064,12 +3999,12 @@ impl Editor {
|
||||
}
|
||||
|
||||
pub fn select_right(&mut self, _: &SelectRight, cx: &mut ViewContext<Self>) {
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
s.move_heads_with(|map, head, _| (movement::right(map, head), SelectionGoal::None));
|
||||
})
|
||||
}
|
||||
|
||||
pub fn next_screen(&mut self, _: &NextScreen, cx: &mut ViewContext<Editor>) {
|
||||
pub fn center_screen(&mut self, _: &CenterScreen, cx: &mut ViewContext<Self>) {
|
||||
if self.take_rename(true, cx).is_some() {
|
||||
return;
|
||||
}
|
||||
@@ -4083,7 +4018,7 @@ impl Editor {
|
||||
return;
|
||||
}
|
||||
|
||||
self.request_autoscroll(Autoscroll::Next, cx);
|
||||
self.request_autoscroll(Autoscroll::Center, cx);
|
||||
}
|
||||
|
||||
pub fn move_up(&mut self, _: &MoveUp, cx: &mut ViewContext<Self>) {
|
||||
@@ -4102,7 +4037,7 @@ impl Editor {
|
||||
return;
|
||||
}
|
||||
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
let line_mode = s.line_mode;
|
||||
s.move_with(|map, selection| {
|
||||
if !selection.is_empty() && !line_mode {
|
||||
@@ -4136,9 +4071,9 @@ impl Editor {
|
||||
};
|
||||
|
||||
let autoscroll = if action.center_cursor {
|
||||
Autoscroll::center()
|
||||
Autoscroll::Center
|
||||
} else {
|
||||
Autoscroll::fit()
|
||||
Autoscroll::Fit
|
||||
};
|
||||
|
||||
self.change_selections(Some(autoscroll), cx, |s| {
|
||||
@@ -4181,7 +4116,7 @@ impl Editor {
|
||||
}
|
||||
|
||||
pub fn select_up(&mut self, _: &SelectUp, cx: &mut ViewContext<Self>) {
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
s.move_heads_with(|map, head, goal| movement::up(map, head, goal, false))
|
||||
})
|
||||
}
|
||||
@@ -4200,7 +4135,7 @@ impl Editor {
|
||||
return;
|
||||
}
|
||||
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
let line_mode = s.line_mode;
|
||||
s.move_with(|map, selection| {
|
||||
if !selection.is_empty() && !line_mode {
|
||||
@@ -4234,9 +4169,9 @@ impl Editor {
|
||||
};
|
||||
|
||||
let autoscroll = if action.center_cursor {
|
||||
Autoscroll::center()
|
||||
Autoscroll::Center
|
||||
} else {
|
||||
Autoscroll::fit()
|
||||
Autoscroll::Fit
|
||||
};
|
||||
|
||||
self.change_selections(Some(autoscroll), cx, |s| {
|
||||
@@ -4279,7 +4214,7 @@ impl Editor {
|
||||
}
|
||||
|
||||
pub fn select_down(&mut self, _: &SelectDown, cx: &mut ViewContext<Self>) {
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
s.move_heads_with(|map, head, goal| movement::down(map, head, goal, false))
|
||||
});
|
||||
}
|
||||
@@ -4289,7 +4224,7 @@ impl Editor {
|
||||
_: &MoveToPreviousWordStart,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
s.move_cursors_with(|map, head, _| {
|
||||
(
|
||||
movement::previous_word_start(map, head),
|
||||
@@ -4304,7 +4239,7 @@ impl Editor {
|
||||
_: &MoveToPreviousSubwordStart,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
s.move_cursors_with(|map, head, _| {
|
||||
(
|
||||
movement::previous_subword_start(map, head),
|
||||
@@ -4319,7 +4254,7 @@ impl Editor {
|
||||
_: &SelectToPreviousWordStart,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
s.move_heads_with(|map, head, _| {
|
||||
(
|
||||
movement::previous_word_start(map, head),
|
||||
@@ -4334,7 +4269,7 @@ impl Editor {
|
||||
_: &SelectToPreviousSubwordStart,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
s.move_heads_with(|map, head, _| {
|
||||
(
|
||||
movement::previous_subword_start(map, head),
|
||||
@@ -4351,7 +4286,7 @@ impl Editor {
|
||||
) {
|
||||
self.transact(cx, |this, cx| {
|
||||
this.select_autoclose_pair(cx);
|
||||
this.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
this.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
let line_mode = s.line_mode;
|
||||
s.move_with(|map, selection| {
|
||||
if selection.is_empty() && !line_mode {
|
||||
@@ -4371,7 +4306,7 @@ impl Editor {
|
||||
) {
|
||||
self.transact(cx, |this, cx| {
|
||||
this.select_autoclose_pair(cx);
|
||||
this.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
this.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
let line_mode = s.line_mode;
|
||||
s.move_with(|map, selection| {
|
||||
if selection.is_empty() && !line_mode {
|
||||
@@ -4385,7 +4320,7 @@ impl Editor {
|
||||
}
|
||||
|
||||
pub fn move_to_next_word_end(&mut self, _: &MoveToNextWordEnd, cx: &mut ViewContext<Self>) {
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
s.move_cursors_with(|map, head, _| {
|
||||
(movement::next_word_end(map, head), SelectionGoal::None)
|
||||
});
|
||||
@@ -4397,7 +4332,7 @@ impl Editor {
|
||||
_: &MoveToNextSubwordEnd,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
s.move_cursors_with(|map, head, _| {
|
||||
(movement::next_subword_end(map, head), SelectionGoal::None)
|
||||
});
|
||||
@@ -4405,7 +4340,7 @@ impl Editor {
|
||||
}
|
||||
|
||||
pub fn select_to_next_word_end(&mut self, _: &SelectToNextWordEnd, cx: &mut ViewContext<Self>) {
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
s.move_heads_with(|map, head, _| {
|
||||
(movement::next_word_end(map, head), SelectionGoal::None)
|
||||
});
|
||||
@@ -4417,7 +4352,7 @@ impl Editor {
|
||||
_: &SelectToNextSubwordEnd,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
s.move_heads_with(|map, head, _| {
|
||||
(movement::next_subword_end(map, head), SelectionGoal::None)
|
||||
});
|
||||
@@ -4426,7 +4361,7 @@ impl Editor {
|
||||
|
||||
pub fn delete_to_next_word_end(&mut self, _: &DeleteToNextWordEnd, cx: &mut ViewContext<Self>) {
|
||||
self.transact(cx, |this, cx| {
|
||||
this.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
this.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
let line_mode = s.line_mode;
|
||||
s.move_with(|map, selection| {
|
||||
if selection.is_empty() && !line_mode {
|
||||
@@ -4445,7 +4380,7 @@ impl Editor {
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.transact(cx, |this, cx| {
|
||||
this.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
this.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
s.move_with(|map, selection| {
|
||||
if selection.is_empty() {
|
||||
let cursor = movement::next_subword_end(map, selection.head());
|
||||
@@ -4462,7 +4397,7 @@ impl Editor {
|
||||
_: &MoveToBeginningOfLine,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
s.move_cursors_with(|map, head, _| {
|
||||
(
|
||||
movement::indented_line_beginning(map, head, true),
|
||||
@@ -4477,7 +4412,7 @@ impl Editor {
|
||||
action: &SelectToBeginningOfLine,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
s.move_heads_with(|map, head, _| {
|
||||
(
|
||||
movement::indented_line_beginning(map, head, action.stop_at_soft_wraps),
|
||||
@@ -4493,7 +4428,7 @@ impl Editor {
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.transact(cx, |this, cx| {
|
||||
this.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
this.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
s.move_with(|_, selection| {
|
||||
selection.reversed = true;
|
||||
});
|
||||
@@ -4510,7 +4445,7 @@ impl Editor {
|
||||
}
|
||||
|
||||
pub fn move_to_end_of_line(&mut self, _: &MoveToEndOfLine, cx: &mut ViewContext<Self>) {
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
s.move_cursors_with(|map, head, _| {
|
||||
(movement::line_end(map, head, true), SelectionGoal::None)
|
||||
});
|
||||
@@ -4522,7 +4457,7 @@ impl Editor {
|
||||
action: &SelectToEndOfLine,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
s.move_heads_with(|map, head, _| {
|
||||
(
|
||||
movement::line_end(map, head, action.stop_at_soft_wraps),
|
||||
@@ -4562,7 +4497,7 @@ impl Editor {
|
||||
return;
|
||||
}
|
||||
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
s.select_ranges(vec![0..0]);
|
||||
});
|
||||
}
|
||||
@@ -4571,7 +4506,7 @@ impl Editor {
|
||||
let mut selection = self.selections.last::<Point>(cx);
|
||||
selection.set_head(Point::zero(), SelectionGoal::None);
|
||||
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
s.select(vec![selection]);
|
||||
});
|
||||
}
|
||||
@@ -4583,7 +4518,7 @@ impl Editor {
|
||||
}
|
||||
|
||||
let cursor = self.buffer.read(cx).read(cx).len();
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
s.select_ranges(vec![cursor..cursor])
|
||||
});
|
||||
}
|
||||
@@ -4620,7 +4555,7 @@ impl Editor {
|
||||
cursor_anchor: position,
|
||||
cursor_position: point,
|
||||
scroll_position: self.scroll_position,
|
||||
scroll_top_anchor: self.scroll_top_anchor,
|
||||
scroll_top_anchor: self.scroll_top_anchor.clone(),
|
||||
scroll_top_row,
|
||||
}),
|
||||
cx,
|
||||
@@ -4632,14 +4567,14 @@ impl Editor {
|
||||
let buffer = self.buffer.read(cx).snapshot(cx);
|
||||
let mut selection = self.selections.first::<usize>(cx);
|
||||
selection.set_head(buffer.len(), SelectionGoal::None);
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
s.select(vec![selection]);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn select_all(&mut self, _: &SelectAll, cx: &mut ViewContext<Self>) {
|
||||
let end = self.buffer.read(cx).read(cx).len();
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
s.select_ranges(vec![0..end]);
|
||||
});
|
||||
}
|
||||
@@ -4654,7 +4589,7 @@ impl Editor {
|
||||
selection.end = cmp::min(max_point, Point::new(rows.end, 0));
|
||||
selection.reversed = false;
|
||||
}
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
s.select(selections);
|
||||
});
|
||||
}
|
||||
@@ -4679,7 +4614,7 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
self.unfold_ranges(to_unfold, true, cx);
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
s.select_ranges(new_selection_ranges);
|
||||
});
|
||||
}
|
||||
@@ -4779,7 +4714,7 @@ impl Editor {
|
||||
state.stack.pop();
|
||||
}
|
||||
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
s.select(new_selections);
|
||||
});
|
||||
if state.stack.len() > 1 {
|
||||
@@ -4828,7 +4763,7 @@ impl Editor {
|
||||
|
||||
if let Some(next_selected_range) = next_selected_range {
|
||||
self.unfold_ranges([next_selected_range.clone()], false, cx);
|
||||
self.change_selections(Some(Autoscroll::newest()), cx, |s| {
|
||||
self.change_selections(Some(Autoscroll::Newest), cx, |s| {
|
||||
if action.replace_newest {
|
||||
s.delete(s.newest_anchor().id);
|
||||
}
|
||||
@@ -4861,7 +4796,7 @@ impl Editor {
|
||||
done: false,
|
||||
};
|
||||
self.unfold_ranges([selection.start..selection.end], false, cx);
|
||||
self.change_selections(Some(Autoscroll::newest()), cx, |s| {
|
||||
self.change_selections(Some(Autoscroll::Newest), cx, |s| {
|
||||
s.select(selections);
|
||||
});
|
||||
self.select_next_state = Some(select_state);
|
||||
@@ -5094,7 +5029,7 @@ impl Editor {
|
||||
}
|
||||
|
||||
drop(snapshot);
|
||||
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections));
|
||||
this.change_selections(Some(Autoscroll::Fit), cx, |s| s.select(selections));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5138,7 +5073,7 @@ impl Editor {
|
||||
|
||||
if selected_larger_node {
|
||||
stack.push(old_selections);
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
s.select(new_selections);
|
||||
});
|
||||
}
|
||||
@@ -5152,7 +5087,7 @@ impl Editor {
|
||||
) {
|
||||
let mut stack = mem::take(&mut self.select_larger_syntax_node_stack);
|
||||
if let Some(selections) = stack.pop() {
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
s.select(selections.to_vec());
|
||||
});
|
||||
}
|
||||
@@ -5183,7 +5118,7 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
s.select(selections);
|
||||
});
|
||||
}
|
||||
@@ -5195,7 +5130,7 @@ impl Editor {
|
||||
self.change_selections(None, cx, |s| s.select_anchors(entry.selections.to_vec()));
|
||||
self.select_next_state = entry.select_next_state;
|
||||
self.add_selections_state = entry.add_selections_state;
|
||||
self.request_autoscroll(Autoscroll::newest(), cx);
|
||||
self.request_autoscroll(Autoscroll::Newest, cx);
|
||||
}
|
||||
self.selection_history.mode = SelectionHistoryMode::Normal;
|
||||
}
|
||||
@@ -5207,7 +5142,7 @@ impl Editor {
|
||||
self.change_selections(None, cx, |s| s.select_anchors(entry.selections.to_vec()));
|
||||
self.select_next_state = entry.select_next_state;
|
||||
self.add_selections_state = entry.add_selections_state;
|
||||
self.request_autoscroll(Autoscroll::newest(), cx);
|
||||
self.request_autoscroll(Autoscroll::Newest, cx);
|
||||
}
|
||||
self.selection_history.mode = SelectionHistoryMode::Normal;
|
||||
}
|
||||
@@ -5229,7 +5164,7 @@ impl Editor {
|
||||
if let Some(popover) = self.hover_state.diagnostic_popover.as_ref() {
|
||||
let (group_id, jump_to) = popover.activation_info();
|
||||
if self.activate_diagnostics(group_id, cx) {
|
||||
self.change_selections(Some(Autoscroll::center()), cx, |s| {
|
||||
self.change_selections(Some(Autoscroll::Center), cx, |s| {
|
||||
let mut new_selection = s.newest_anchor().clone();
|
||||
new_selection.collapse_to(jump_to, SelectionGoal::None);
|
||||
s.select_anchors(vec![new_selection.clone()]);
|
||||
@@ -5275,7 +5210,7 @@ impl Editor {
|
||||
|
||||
if let Some((primary_range, group_id)) = group {
|
||||
if self.activate_diagnostics(group_id, cx) {
|
||||
self.change_selections(Some(Autoscroll::center()), cx, |s| {
|
||||
self.change_selections(Some(Autoscroll::Center), cx, |s| {
|
||||
s.select(vec![Selection {
|
||||
id: selection.id,
|
||||
start: primary_range.start,
|
||||
@@ -5350,7 +5285,7 @@ impl Editor {
|
||||
.dedup();
|
||||
|
||||
if let Some(hunk) = hunks.next() {
|
||||
this.change_selections(Some(Autoscroll::center()), cx, |s| {
|
||||
this.change_selections(Some(Autoscroll::Center), cx, |s| {
|
||||
let row = hunk.start_display_row();
|
||||
let point = DisplayPoint::new(row, 0);
|
||||
s.select_display_ranges([point..point]);
|
||||
@@ -5448,7 +5383,7 @@ impl Editor {
|
||||
if editor_handle != target_editor_handle {
|
||||
pane.update(cx, |pane, _| pane.disable_history());
|
||||
}
|
||||
target_editor.change_selections(Some(Autoscroll::center()), cx, |s| {
|
||||
target_editor.change_selections(Some(Autoscroll::Center), cx, |s| {
|
||||
s.select_ranges([range]);
|
||||
});
|
||||
|
||||
@@ -6070,7 +6005,7 @@ impl Editor {
|
||||
let mut ranges = ranges.into_iter().peekable();
|
||||
if ranges.peek().is_some() {
|
||||
self.display_map.update(cx, |map, cx| map.fold(ranges, cx));
|
||||
self.request_autoscroll(Autoscroll::fit(), cx);
|
||||
self.request_autoscroll(Autoscroll::Fit, cx);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
@@ -6085,7 +6020,7 @@ impl Editor {
|
||||
if ranges.peek().is_some() {
|
||||
self.display_map
|
||||
.update(cx, |map, cx| map.unfold(ranges, inclusive, cx));
|
||||
self.request_autoscroll(Autoscroll::fit(), cx);
|
||||
self.request_autoscroll(Autoscroll::Fit, cx);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
@@ -6098,7 +6033,7 @@ impl Editor {
|
||||
let blocks = self
|
||||
.display_map
|
||||
.update(cx, |display_map, cx| display_map.insert_blocks(blocks, cx));
|
||||
self.request_autoscroll(Autoscroll::fit(), cx);
|
||||
self.request_autoscroll(Autoscroll::Fit, cx);
|
||||
blocks
|
||||
}
|
||||
|
||||
@@ -6109,7 +6044,7 @@ impl Editor {
|
||||
) {
|
||||
self.display_map
|
||||
.update(cx, |display_map, _| display_map.replace_blocks(blocks));
|
||||
self.request_autoscroll(Autoscroll::fit(), cx);
|
||||
self.request_autoscroll(Autoscroll::Fit, cx);
|
||||
}
|
||||
|
||||
pub fn remove_blocks(&mut self, block_ids: HashSet<BlockId>, cx: &mut ViewContext<Self>) {
|
||||
@@ -6449,7 +6384,7 @@ impl Editor {
|
||||
for (buffer, ranges) in new_selections_by_buffer.into_iter() {
|
||||
let editor = workspace.open_project_item::<Self>(buffer, cx);
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.change_selections(Some(Autoscroll::newest()), cx, |s| {
|
||||
editor.change_selections(Some(Autoscroll::Newest), cx, |s| {
|
||||
s.select_ranges(ranges);
|
||||
});
|
||||
});
|
||||
@@ -6460,7 +6395,7 @@ impl Editor {
|
||||
}
|
||||
|
||||
fn jump(workspace: &mut Workspace, action: &Jump, cx: &mut ViewContext<Workspace>) {
|
||||
let editor = workspace.open_path(action.path.clone(), None, true, cx);
|
||||
let editor = workspace.open_path(action.path.clone(), true, cx);
|
||||
let position = action.position;
|
||||
let anchor = action.anchor;
|
||||
cx.spawn_weak(|_, mut cx| async move {
|
||||
@@ -6475,7 +6410,7 @@ impl Editor {
|
||||
};
|
||||
|
||||
let nav_history = editor.nav_history.take();
|
||||
editor.change_selections(Some(Autoscroll::newest()), cx, |s| {
|
||||
editor.change_selections(Some(Autoscroll::Newest), cx, |s| {
|
||||
s.select_ranges([cursor..cursor]);
|
||||
});
|
||||
editor.nav_history = nav_history;
|
||||
@@ -6532,13 +6467,15 @@ impl Editor {
|
||||
.as_singleton()
|
||||
.and_then(|b| b.read(cx).file()),
|
||||
) {
|
||||
let extension = Path::new(file.file_name(cx))
|
||||
.extension()
|
||||
.and_then(|e| e.to_str());
|
||||
project
|
||||
.read(cx)
|
||||
.client()
|
||||
.report_event(name, json!({ "File Extension": extension }));
|
||||
project.read(cx).client().report_event(
|
||||
name,
|
||||
json!({
|
||||
"File Extension": file
|
||||
.path()
|
||||
.extension()
|
||||
.and_then(|e| e.to_str())
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6909,7 +6846,7 @@ impl View for Editor {
|
||||
|
||||
fn build_style(
|
||||
settings: &Settings,
|
||||
get_field_editor_theme: Option<&GetFieldEditorTheme>,
|
||||
get_field_editor_theme: Option<GetFieldEditorTheme>,
|
||||
override_text_style: Option<&OverrideTextStyle>,
|
||||
cx: &AppContext,
|
||||
) -> EditorStyle {
|
||||
|
||||
@@ -542,7 +542,7 @@ fn test_navigation_history(cx: &mut gpui::MutableAppContext) {
|
||||
// Set scroll position to check later
|
||||
editor.set_scroll_position(Vector2F::new(5.5, 5.5), cx);
|
||||
let original_scroll_position = editor.scroll_position;
|
||||
let original_scroll_top_anchor = editor.scroll_top_anchor;
|
||||
let original_scroll_top_anchor = editor.scroll_top_anchor.clone();
|
||||
|
||||
// Jump to the end of the document and adjust scroll
|
||||
editor.move_to_end(&MoveToEnd, cx);
|
||||
@@ -556,12 +556,12 @@ fn test_navigation_history(cx: &mut gpui::MutableAppContext) {
|
||||
assert_eq!(editor.scroll_top_anchor, original_scroll_top_anchor);
|
||||
|
||||
// Ensure we don't panic when navigation data contains invalid anchors *and* points.
|
||||
let mut invalid_anchor = editor.scroll_top_anchor;
|
||||
let mut invalid_anchor = editor.scroll_top_anchor.clone();
|
||||
invalid_anchor.text_anchor.buffer_id = Some(999);
|
||||
let invalid_point = Point::new(9999, 0);
|
||||
editor.navigate(
|
||||
Box::new(NavigationData {
|
||||
cursor_anchor: invalid_anchor,
|
||||
cursor_anchor: invalid_anchor.clone(),
|
||||
cursor_position: invalid_point,
|
||||
scroll_top_anchor: invalid_anchor,
|
||||
scroll_top_row: invalid_point.row,
|
||||
@@ -4146,26 +4146,14 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
|
||||
|
||||
handle_resolve_completion_request(
|
||||
&mut cx,
|
||||
Some(vec![
|
||||
(
|
||||
//This overlaps with the primary completion edit which is
|
||||
//misbehavior from the LSP spec, test that we filter it out
|
||||
indoc! {"
|
||||
one.second_ˇcompletion
|
||||
two
|
||||
threeˇ
|
||||
"},
|
||||
"overlapping aditional edit",
|
||||
),
|
||||
(
|
||||
indoc! {"
|
||||
one.second_completion
|
||||
two
|
||||
threeˇ
|
||||
"},
|
||||
"\nadditional edit",
|
||||
),
|
||||
]),
|
||||
Some((
|
||||
indoc! {"
|
||||
one.second_completion
|
||||
two
|
||||
threeˇ
|
||||
"},
|
||||
"\nadditional edit",
|
||||
)),
|
||||
)
|
||||
.await;
|
||||
apply_additional_edits.await.unwrap();
|
||||
@@ -4315,24 +4303,19 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
|
||||
|
||||
async fn handle_resolve_completion_request<'a>(
|
||||
cx: &mut EditorLspTestContext<'a>,
|
||||
edits: Option<Vec<(&'static str, &'static str)>>,
|
||||
edit: Option<(&'static str, &'static str)>,
|
||||
) {
|
||||
let edits = edits.map(|edits| {
|
||||
edits
|
||||
.iter()
|
||||
.map(|(marked_string, new_text)| {
|
||||
let (_, marked_ranges) = marked_text_ranges(marked_string, false);
|
||||
let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
|
||||
lsp::TextEdit::new(replace_range, new_text.to_string())
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
let edit = edit.map(|(marked_string, new_text)| {
|
||||
let (_, marked_ranges) = marked_text_ranges(marked_string, false);
|
||||
let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
|
||||
vec![lsp::TextEdit::new(replace_range, new_text.to_string())]
|
||||
});
|
||||
|
||||
cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
|
||||
let edits = edits.clone();
|
||||
let edit = edit.clone();
|
||||
async move {
|
||||
Ok(lsp::CompletionItem {
|
||||
additional_text_edits: edits,
|
||||
additional_text_edits: edit,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
@@ -4718,7 +4701,9 @@ fn test_refresh_selections(cx: &mut gpui::MutableAppContext) {
|
||||
|
||||
// Refreshing selections is a no-op when excerpts haven't changed.
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.change_selections(None, cx, |s| s.refresh());
|
||||
editor.change_selections(None, cx, |s| {
|
||||
s.refresh();
|
||||
});
|
||||
assert_eq!(
|
||||
editor.selections.ranges(cx),
|
||||
[
|
||||
@@ -4729,7 +4714,7 @@ fn test_refresh_selections(cx: &mut gpui::MutableAppContext) {
|
||||
});
|
||||
|
||||
multibuffer.update(cx, |multibuffer, cx| {
|
||||
multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
|
||||
multibuffer.remove_excerpts([&excerpt1_id.unwrap()], cx);
|
||||
});
|
||||
editor.update(cx, |editor, cx| {
|
||||
// Removing an excerpt causes the first selection to become degenerate.
|
||||
@@ -4743,7 +4728,9 @@ fn test_refresh_selections(cx: &mut gpui::MutableAppContext) {
|
||||
|
||||
// Refreshing selections will relocate the first selection to the original buffer
|
||||
// location.
|
||||
editor.change_selections(None, cx, |s| s.refresh());
|
||||
editor.change_selections(None, cx, |s| {
|
||||
s.refresh();
|
||||
});
|
||||
assert_eq!(
|
||||
editor.selections.ranges(cx),
|
||||
[
|
||||
@@ -4797,7 +4784,7 @@ fn test_refresh_selections_while_selecting_with_mouse(cx: &mut gpui::MutableAppC
|
||||
});
|
||||
|
||||
multibuffer.update(cx, |multibuffer, cx| {
|
||||
multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
|
||||
multibuffer.remove_excerpts([&excerpt1_id.unwrap()], cx);
|
||||
});
|
||||
editor.update(cx, |editor, cx| {
|
||||
assert_eq!(
|
||||
@@ -4806,7 +4793,9 @@ fn test_refresh_selections_while_selecting_with_mouse(cx: &mut gpui::MutableAppC
|
||||
);
|
||||
|
||||
// Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
|
||||
editor.change_selections(None, cx, |s| s.refresh());
|
||||
editor.change_selections(None, cx, |s| {
|
||||
s.refresh();
|
||||
});
|
||||
assert_eq!(
|
||||
editor.selections.ranges(cx),
|
||||
[Point::new(0, 3)..Point::new(0, 3)]
|
||||
@@ -5022,7 +5011,7 @@ fn test_following(cx: &mut gpui::MutableAppContext) {
|
||||
// Update the selections and scroll position
|
||||
leader.update(cx, |leader, cx| {
|
||||
leader.change_selections(None, cx, |s| s.select_ranges([0..0]));
|
||||
leader.request_autoscroll(Autoscroll::newest(), cx);
|
||||
leader.request_autoscroll(Autoscroll::Newest, cx);
|
||||
leader.set_scroll_position(vec2f(1.5, 3.5), cx);
|
||||
});
|
||||
follower.update(cx, |follower, cx| {
|
||||
|
||||
@@ -192,14 +192,8 @@ impl EditorElement {
|
||||
.on_scroll({
|
||||
let position_map = position_map.clone();
|
||||
move |e, cx| {
|
||||
if !Self::scroll(
|
||||
e.position,
|
||||
*e.delta.raw(),
|
||||
e.delta.precise(),
|
||||
&position_map,
|
||||
bounds,
|
||||
cx,
|
||||
) {
|
||||
if !Self::scroll(e.position, e.delta, e.precise, &position_map, bounds, cx)
|
||||
{
|
||||
cx.propagate_event()
|
||||
}
|
||||
}
|
||||
@@ -1192,7 +1186,7 @@ impl EditorElement {
|
||||
}
|
||||
|
||||
// When the editor is empty and unfocused, then show the placeholder.
|
||||
if snapshot.is_empty() {
|
||||
if snapshot.is_empty() && !snapshot.is_focused() {
|
||||
let placeholder_style = self
|
||||
.style
|
||||
.placeholder_text
|
||||
@@ -1334,13 +1328,12 @@ impl EditorElement {
|
||||
})
|
||||
}
|
||||
TransformBlock::ExcerptHeader {
|
||||
id,
|
||||
key,
|
||||
buffer,
|
||||
range,
|
||||
starts_new_buffer,
|
||||
..
|
||||
} => {
|
||||
let id = *id;
|
||||
let jump_icon = project::File::from_dyn(buffer.file()).map(|file| {
|
||||
let jump_position = range
|
||||
.primary
|
||||
@@ -1357,7 +1350,7 @@ impl EditorElement {
|
||||
|
||||
enum JumpIcon {}
|
||||
cx.render(&editor, |_, cx| {
|
||||
MouseEventHandler::<JumpIcon>::new(id.into(), cx, |state, _| {
|
||||
MouseEventHandler::<JumpIcon>::new(*key, cx, |state, _| {
|
||||
let style = style.jump_icon.style_for(state, false);
|
||||
Svg::new("icons/arrow_up_right_8.svg")
|
||||
.with_color(style.color)
|
||||
@@ -1376,7 +1369,7 @@ impl EditorElement {
|
||||
cx.dispatch_action(jump_action.clone())
|
||||
})
|
||||
.with_tooltip::<JumpIcon, _>(
|
||||
id.into(),
|
||||
*key,
|
||||
"Jump to Buffer".to_string(),
|
||||
Some(Box::new(crate::OpenExcerpts)),
|
||||
tooltip_style.clone(),
|
||||
@@ -1607,13 +1600,16 @@ impl Element for EditorElement {
|
||||
|
||||
highlighted_rows = view.highlighted_rows();
|
||||
let theme = cx.global::<Settings>().theme.as_ref();
|
||||
highlighted_ranges =
|
||||
view.background_highlights_in_range(start_anchor..end_anchor, &display_map, theme);
|
||||
highlighted_ranges = view.background_highlights_in_range(
|
||||
start_anchor.clone()..end_anchor.clone(),
|
||||
&display_map,
|
||||
theme,
|
||||
);
|
||||
|
||||
let mut remote_selections = HashMap::default();
|
||||
for (replica_id, line_mode, cursor_shape, selection) in display_map
|
||||
.buffer_snapshot
|
||||
.remote_selections_in_range(&(start_anchor..end_anchor))
|
||||
.remote_selections_in_range(&(start_anchor.clone()..end_anchor.clone()))
|
||||
{
|
||||
// The local selections match the leader's selections.
|
||||
if Some(replica_id) == view.leader_replica_id {
|
||||
|
||||
@@ -221,7 +221,7 @@ fn show_hover(
|
||||
|
||||
start..end
|
||||
} else {
|
||||
anchor..anchor
|
||||
anchor.clone()..anchor.clone()
|
||||
};
|
||||
|
||||
Some(InfoPopover {
|
||||
|
||||
@@ -204,7 +204,7 @@ impl FollowableItem for Editor {
|
||||
|
||||
if !selections.is_empty() {
|
||||
self.set_selections_from_remote(selections, cx);
|
||||
self.request_autoscroll_remotely(Autoscroll::newest(), cx);
|
||||
self.request_autoscroll_remotely(Autoscroll::Newest, cx);
|
||||
} else if let Some(anchor) = message.scroll_top_anchor {
|
||||
self.set_scroll_top_anchor(
|
||||
Anchor {
|
||||
@@ -294,7 +294,7 @@ impl Item for Editor {
|
||||
let nav_history = self.nav_history.take();
|
||||
self.scroll_position = data.scroll_position;
|
||||
self.scroll_top_anchor = scroll_top_anchor;
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
s.select_ranges([offset..offset])
|
||||
});
|
||||
self.nav_history = nav_history;
|
||||
@@ -466,7 +466,7 @@ impl Item for Editor {
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let transaction = reload_buffers.log_err().await;
|
||||
this.update(&mut cx, |editor, cx| {
|
||||
editor.request_autoscroll(Autoscroll::fit(), cx)
|
||||
editor.request_autoscroll(Autoscroll::Fit, cx)
|
||||
});
|
||||
buffer.update(&mut cx, |buffer, _| {
|
||||
if let Some(transaction) = transaction {
|
||||
@@ -619,7 +619,7 @@ impl SearchableItem for Editor {
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.unfold_ranges([matches[index].clone()], false, cx);
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
s.select_ranges([matches[index].clone()])
|
||||
});
|
||||
}
|
||||
@@ -819,20 +819,11 @@ impl StatusItemView for CursorPosition {
|
||||
|
||||
fn path_for_buffer<'a>(
|
||||
buffer: &ModelHandle<MultiBuffer>,
|
||||
height: usize,
|
||||
include_filename: bool,
|
||||
cx: &'a AppContext,
|
||||
) -> Option<Cow<'a, Path>> {
|
||||
let file = buffer.read(cx).as_singleton()?.read(cx).file()?;
|
||||
path_for_file(file, height, include_filename, cx)
|
||||
}
|
||||
|
||||
fn path_for_file<'a>(
|
||||
file: &'a dyn language::File,
|
||||
mut height: usize,
|
||||
include_filename: bool,
|
||||
cx: &'a AppContext,
|
||||
) -> Option<Cow<'a, Path>> {
|
||||
let file = buffer.read(cx).as_singleton()?.read(cx).file()?;
|
||||
// Ensure we always render at least the filename.
|
||||
height += 1;
|
||||
|
||||
@@ -854,82 +845,13 @@ fn path_for_file<'a>(
|
||||
if include_filename {
|
||||
Some(full_path.into())
|
||||
} else {
|
||||
Some(full_path.parent()?.to_path_buf().into())
|
||||
Some(full_path.parent().unwrap().to_path_buf().into())
|
||||
}
|
||||
} else {
|
||||
let mut path = file.path().strip_prefix(prefix).ok()?;
|
||||
let mut path = file.path().strip_prefix(prefix).unwrap();
|
||||
if !include_filename {
|
||||
path = path.parent()?;
|
||||
path = path.parent().unwrap();
|
||||
}
|
||||
Some(path.into())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use gpui::MutableAppContext;
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
#[gpui::test]
|
||||
fn test_path_for_file(cx: &mut MutableAppContext) {
|
||||
let file = TestFile {
|
||||
path: Path::new("").into(),
|
||||
full_path: PathBuf::from(""),
|
||||
};
|
||||
assert_eq!(path_for_file(&file, 0, false, cx), None);
|
||||
}
|
||||
|
||||
struct TestFile {
|
||||
path: Arc<Path>,
|
||||
full_path: PathBuf,
|
||||
}
|
||||
|
||||
impl language::File for TestFile {
|
||||
fn path(&self) -> &Arc<Path> {
|
||||
&self.path
|
||||
}
|
||||
|
||||
fn full_path(&self, _: &gpui::AppContext) -> PathBuf {
|
||||
self.full_path.clone()
|
||||
}
|
||||
|
||||
fn as_local(&self) -> Option<&dyn language::LocalFile> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn mtime(&self) -> std::time::SystemTime {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn file_name<'a>(&'a self, _: &'a gpui::AppContext) -> &'a std::ffi::OsStr {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn is_deleted(&self) -> bool {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn save(
|
||||
&self,
|
||||
_: u64,
|
||||
_: language::Rope,
|
||||
_: clock::Global,
|
||||
_: project::LineEnding,
|
||||
_: &mut MutableAppContext,
|
||||
) -> gpui::Task<anyhow::Result<(clock::Global, String, std::time::SystemTime)>> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn to_proto(&self) -> rpc::proto::File {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -811,7 +811,7 @@ mod tests {
|
||||
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||
let anchor_range = snapshot.anchor_before(selection_range.start)
|
||||
..snapshot.anchor_after(selection_range.end);
|
||||
editor.change_selections(Some(crate::Autoscroll::fit()), cx, |s| {
|
||||
editor.change_selections(Some(crate::Autoscroll::Fit), cx, |s| {
|
||||
s.set_pending_anchor_range(anchor_range, crate::SelectMode::Character)
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,7 +11,7 @@ use language::{
|
||||
char_kind, AutoindentMode, Buffer, BufferChunks, BufferSnapshot, CharKind, Chunk, CursorShape,
|
||||
DiagnosticEntry, Event, File, IndentSize, Language, OffsetRangeExt, OffsetUtf16, Outline,
|
||||
OutlineItem, Point, PointUtf16, Selection, TextDimension, ToOffset as _, ToOffsetUtf16 as _,
|
||||
ToPoint as _, ToPointUtf16 as _, TransactionId, Unclipped,
|
||||
ToPoint as _, ToPointUtf16 as _, TransactionId,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
use std::{
|
||||
@@ -36,13 +36,13 @@ use util::post_inc;
|
||||
|
||||
const NEWLINES: &[u8] = &[b'\n'; u8::MAX as usize];
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct ExcerptId(usize);
|
||||
pub type ExcerptId = Locator;
|
||||
|
||||
pub struct MultiBuffer {
|
||||
snapshot: RefCell<MultiBufferSnapshot>,
|
||||
buffers: RefCell<HashMap<usize, BufferState>>,
|
||||
next_excerpt_id: usize,
|
||||
used_excerpt_ids: SumTree<ExcerptId>,
|
||||
next_excerpt_key: usize,
|
||||
subscriptions: Topic,
|
||||
singleton: bool,
|
||||
replica_id: ReplicaId,
|
||||
@@ -92,7 +92,7 @@ struct BufferState {
|
||||
last_diagnostics_update_count: usize,
|
||||
last_file_update_count: usize,
|
||||
last_git_diff_update_count: usize,
|
||||
excerpts: Vec<Locator>,
|
||||
excerpts: Vec<ExcerptId>,
|
||||
_subscriptions: [gpui::Subscription; 2],
|
||||
}
|
||||
|
||||
@@ -100,7 +100,6 @@ struct BufferState {
|
||||
pub struct MultiBufferSnapshot {
|
||||
singleton: bool,
|
||||
excerpts: SumTree<Excerpt>,
|
||||
excerpt_ids: SumTree<ExcerptIdMapping>,
|
||||
parse_count: usize,
|
||||
diagnostics_update_count: usize,
|
||||
trailing_excerpt_update_count: usize,
|
||||
@@ -112,6 +111,7 @@ pub struct MultiBufferSnapshot {
|
||||
|
||||
pub struct ExcerptBoundary {
|
||||
pub id: ExcerptId,
|
||||
pub key: usize,
|
||||
pub row: u32,
|
||||
pub buffer: BufferSnapshot,
|
||||
pub range: ExcerptRange<text::Anchor>,
|
||||
@@ -121,7 +121,7 @@ pub struct ExcerptBoundary {
|
||||
#[derive(Clone)]
|
||||
struct Excerpt {
|
||||
id: ExcerptId,
|
||||
locator: Locator,
|
||||
key: usize,
|
||||
buffer_id: usize,
|
||||
buffer: BufferSnapshot,
|
||||
range: ExcerptRange<text::Anchor>,
|
||||
@@ -130,12 +130,6 @@ struct Excerpt {
|
||||
has_trailing_newline: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct ExcerptIdMapping {
|
||||
id: ExcerptId,
|
||||
locator: Locator,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct ExcerptRange<T> {
|
||||
pub context: Range<T>,
|
||||
@@ -145,7 +139,6 @@ pub struct ExcerptRange<T> {
|
||||
#[derive(Clone, Debug, Default)]
|
||||
struct ExcerptSummary {
|
||||
excerpt_id: ExcerptId,
|
||||
excerpt_locator: Locator,
|
||||
max_buffer_row: u32,
|
||||
text: TextSummary,
|
||||
}
|
||||
@@ -185,7 +178,8 @@ impl MultiBuffer {
|
||||
Self {
|
||||
snapshot: Default::default(),
|
||||
buffers: Default::default(),
|
||||
next_excerpt_id: 1,
|
||||
used_excerpt_ids: Default::default(),
|
||||
next_excerpt_key: Default::default(),
|
||||
subscriptions: Default::default(),
|
||||
singleton: false,
|
||||
replica_id,
|
||||
@@ -224,7 +218,8 @@ impl MultiBuffer {
|
||||
Self {
|
||||
snapshot: RefCell::new(self.snapshot.borrow().clone()),
|
||||
buffers: RefCell::new(buffers),
|
||||
next_excerpt_id: 1,
|
||||
used_excerpt_ids: self.used_excerpt_ids.clone(),
|
||||
next_excerpt_key: self.next_excerpt_key,
|
||||
subscriptions: Default::default(),
|
||||
singleton: self.singleton,
|
||||
replica_id: self.replica_id,
|
||||
@@ -615,14 +610,11 @@ impl MultiBuffer {
|
||||
let mut selections_by_buffer: HashMap<usize, Vec<Selection<text::Anchor>>> =
|
||||
Default::default();
|
||||
let snapshot = self.read(cx);
|
||||
let mut cursor = snapshot.excerpts.cursor::<Option<&Locator>>();
|
||||
let mut cursor = snapshot.excerpts.cursor::<Option<&ExcerptId>>();
|
||||
for selection in selections {
|
||||
let start_locator = snapshot.excerpt_locator_for_id(selection.start.excerpt_id);
|
||||
let end_locator = snapshot.excerpt_locator_for_id(selection.end.excerpt_id);
|
||||
|
||||
cursor.seek(&Some(start_locator), Bias::Left, &());
|
||||
cursor.seek(&Some(&selection.start.excerpt_id), Bias::Left, &());
|
||||
while let Some(excerpt) = cursor.item() {
|
||||
if excerpt.locator > *end_locator {
|
||||
if excerpt.id > selection.end.excerpt_id {
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -753,7 +745,7 @@ impl MultiBuffer {
|
||||
where
|
||||
O: text::ToOffset,
|
||||
{
|
||||
self.insert_excerpts_after(ExcerptId::max(), buffer, ranges, cx)
|
||||
self.insert_excerpts_after(&ExcerptId::max(), buffer, ranges, cx)
|
||||
}
|
||||
|
||||
pub fn push_excerpts_with_context_lines<O>(
|
||||
@@ -826,7 +818,7 @@ impl MultiBuffer {
|
||||
|
||||
pub fn insert_excerpts_after<O>(
|
||||
&mut self,
|
||||
prev_excerpt_id: ExcerptId,
|
||||
prev_excerpt_id: &ExcerptId,
|
||||
buffer: ModelHandle<Buffer>,
|
||||
ranges: impl IntoIterator<Item = ExcerptRange<O>>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
@@ -862,12 +854,8 @@ impl MultiBuffer {
|
||||
});
|
||||
|
||||
let mut snapshot = self.snapshot.borrow_mut();
|
||||
|
||||
let mut prev_locator = snapshot.excerpt_locator_for_id(prev_excerpt_id).clone();
|
||||
let mut new_excerpt_ids = mem::take(&mut snapshot.excerpt_ids);
|
||||
let mut cursor = snapshot.excerpts.cursor::<Option<&Locator>>();
|
||||
let mut new_excerpts = cursor.slice(&prev_locator, Bias::Right, &());
|
||||
prev_locator = cursor.start().unwrap_or(Locator::min_ref()).clone();
|
||||
let mut cursor = snapshot.excerpts.cursor::<Option<&ExcerptId>>();
|
||||
let mut new_excerpts = cursor.slice(&Some(prev_excerpt_id), Bias::Right, &());
|
||||
|
||||
let edit_start = new_excerpts.summary().text.len;
|
||||
new_excerpts.update_last(
|
||||
@@ -877,17 +865,25 @@ impl MultiBuffer {
|
||||
&(),
|
||||
);
|
||||
|
||||
let next_locator = if let Some(excerpt) = cursor.item() {
|
||||
excerpt.locator.clone()
|
||||
let mut used_cursor = self.used_excerpt_ids.cursor::<Locator>();
|
||||
used_cursor.seek(prev_excerpt_id, Bias::Right, &());
|
||||
let mut prev_id = if let Some(excerpt_id) = used_cursor.prev_item() {
|
||||
excerpt_id.clone()
|
||||
} else {
|
||||
Locator::max()
|
||||
ExcerptId::min()
|
||||
};
|
||||
let next_id = if let Some(excerpt_id) = used_cursor.item() {
|
||||
excerpt_id.clone()
|
||||
} else {
|
||||
ExcerptId::max()
|
||||
};
|
||||
drop(used_cursor);
|
||||
|
||||
let mut ids = Vec::new();
|
||||
while let Some(range) = ranges.next() {
|
||||
let locator = Locator::between(&prev_locator, &next_locator);
|
||||
if let Err(ix) = buffer_state.excerpts.binary_search(&locator) {
|
||||
buffer_state.excerpts.insert(ix, locator.clone());
|
||||
let id = ExcerptId::between(&prev_id, &next_id);
|
||||
if let Err(ix) = buffer_state.excerpts.binary_search(&id) {
|
||||
buffer_state.excerpts.insert(ix, id.clone());
|
||||
}
|
||||
let range = ExcerptRange {
|
||||
context: buffer_snapshot.anchor_before(&range.context.start)
|
||||
@@ -897,20 +893,22 @@ impl MultiBuffer {
|
||||
..buffer_snapshot.anchor_after(&primary.end)
|
||||
}),
|
||||
};
|
||||
let id = ExcerptId(post_inc(&mut self.next_excerpt_id));
|
||||
let excerpt = Excerpt::new(
|
||||
id,
|
||||
locator.clone(),
|
||||
id.clone(),
|
||||
post_inc(&mut self.next_excerpt_key),
|
||||
buffer_id,
|
||||
buffer_snapshot.clone(),
|
||||
range,
|
||||
ranges.peek().is_some() || cursor.item().is_some(),
|
||||
);
|
||||
new_excerpts.push(excerpt, &());
|
||||
prev_locator = locator.clone();
|
||||
new_excerpt_ids.push(ExcerptIdMapping { id, locator }, &());
|
||||
prev_id = id.clone();
|
||||
ids.push(id);
|
||||
}
|
||||
self.used_excerpt_ids.edit(
|
||||
ids.iter().cloned().map(sum_tree::Edit::Insert).collect(),
|
||||
&(),
|
||||
);
|
||||
|
||||
let edit_end = new_excerpts.summary().text.len;
|
||||
|
||||
@@ -919,7 +917,6 @@ impl MultiBuffer {
|
||||
new_excerpts.push_tree(suffix, &());
|
||||
drop(cursor);
|
||||
snapshot.excerpts = new_excerpts;
|
||||
snapshot.excerpt_ids = new_excerpt_ids;
|
||||
if changed_trailing_excerpt {
|
||||
snapshot.trailing_excerpt_update_count += 1;
|
||||
}
|
||||
@@ -959,16 +956,16 @@ impl MultiBuffer {
|
||||
let mut excerpts = Vec::new();
|
||||
let snapshot = self.read(cx);
|
||||
let buffers = self.buffers.borrow();
|
||||
let mut cursor = snapshot.excerpts.cursor::<Option<&Locator>>();
|
||||
for locator in buffers
|
||||
let mut cursor = snapshot.excerpts.cursor::<Option<&ExcerptId>>();
|
||||
for excerpt_id in buffers
|
||||
.get(&buffer.id())
|
||||
.map(|state| &state.excerpts)
|
||||
.into_iter()
|
||||
.flatten()
|
||||
{
|
||||
cursor.seek_forward(&Some(locator), Bias::Left, &());
|
||||
cursor.seek_forward(&Some(excerpt_id), Bias::Left, &());
|
||||
if let Some(excerpt) = cursor.item() {
|
||||
if excerpt.locator == *locator {
|
||||
if excerpt.id == *excerpt_id {
|
||||
excerpts.push((excerpt.id.clone(), excerpt.range.clone()));
|
||||
}
|
||||
}
|
||||
@@ -978,11 +975,10 @@ impl MultiBuffer {
|
||||
}
|
||||
|
||||
pub fn excerpt_ids(&self) -> Vec<ExcerptId> {
|
||||
self.snapshot
|
||||
self.buffers
|
||||
.borrow()
|
||||
.excerpts
|
||||
.iter()
|
||||
.map(|entry| entry.id)
|
||||
.values()
|
||||
.flat_map(|state| state.excerpts.iter().cloned())
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -1065,34 +1061,32 @@ impl MultiBuffer {
|
||||
result
|
||||
}
|
||||
|
||||
pub fn remove_excerpts(
|
||||
pub fn remove_excerpts<'a>(
|
||||
&mut self,
|
||||
excerpt_ids: impl IntoIterator<Item = ExcerptId>,
|
||||
excerpt_ids: impl IntoIterator<Item = &'a ExcerptId>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
self.sync(cx);
|
||||
let mut buffers = self.buffers.borrow_mut();
|
||||
let mut snapshot = self.snapshot.borrow_mut();
|
||||
let mut new_excerpts = SumTree::new();
|
||||
let mut cursor = snapshot.excerpts.cursor::<(Option<&Locator>, usize)>();
|
||||
let mut cursor = snapshot.excerpts.cursor::<(Option<&ExcerptId>, usize)>();
|
||||
let mut edits = Vec::new();
|
||||
let mut excerpt_ids = excerpt_ids.into_iter().peekable();
|
||||
|
||||
while let Some(excerpt_id) = excerpt_ids.next() {
|
||||
while let Some(mut excerpt_id) = excerpt_ids.next() {
|
||||
// Seek to the next excerpt to remove, preserving any preceding excerpts.
|
||||
let locator = snapshot.excerpt_locator_for_id(excerpt_id);
|
||||
new_excerpts.push_tree(cursor.slice(&Some(locator), Bias::Left, &()), &());
|
||||
|
||||
new_excerpts.push_tree(cursor.slice(&Some(excerpt_id), Bias::Left, &()), &());
|
||||
if let Some(mut excerpt) = cursor.item() {
|
||||
if excerpt.id != excerpt_id {
|
||||
if excerpt.id != *excerpt_id {
|
||||
continue;
|
||||
}
|
||||
let mut old_start = cursor.start().1;
|
||||
|
||||
// Skip over the removed excerpt.
|
||||
'remove_excerpts: loop {
|
||||
loop {
|
||||
if let Some(buffer_state) = buffers.get_mut(&excerpt.buffer_id) {
|
||||
buffer_state.excerpts.retain(|l| l != &excerpt.locator);
|
||||
buffer_state.excerpts.retain(|id| id != excerpt_id);
|
||||
if buffer_state.excerpts.is_empty() {
|
||||
buffers.remove(&excerpt.buffer_id);
|
||||
}
|
||||
@@ -1100,16 +1094,14 @@ impl MultiBuffer {
|
||||
cursor.next(&());
|
||||
|
||||
// Skip over any subsequent excerpts that are also removed.
|
||||
while let Some(&next_excerpt_id) = excerpt_ids.peek() {
|
||||
let next_locator = snapshot.excerpt_locator_for_id(next_excerpt_id);
|
||||
if let Some(&next_excerpt_id) = excerpt_ids.peek() {
|
||||
if let Some(next_excerpt) = cursor.item() {
|
||||
if next_excerpt.locator == *next_locator {
|
||||
excerpt_ids.next();
|
||||
if next_excerpt.id == *next_excerpt_id {
|
||||
excerpt = next_excerpt;
|
||||
continue 'remove_excerpts;
|
||||
excerpt_id = excerpt_ids.next().unwrap();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
break;
|
||||
@@ -1136,7 +1128,6 @@ impl MultiBuffer {
|
||||
new_excerpts.push_tree(suffix, &());
|
||||
drop(cursor);
|
||||
snapshot.excerpts = new_excerpts;
|
||||
|
||||
if changed_trailing_excerpt {
|
||||
snapshot.trailing_excerpt_update_count += 1;
|
||||
}
|
||||
@@ -1316,7 +1307,7 @@ impl MultiBuffer {
|
||||
buffer_state
|
||||
.excerpts
|
||||
.iter()
|
||||
.map(|locator| (locator, buffer_state.buffer.clone(), buffer_edited)),
|
||||
.map(|excerpt_id| (excerpt_id, buffer_state.buffer.clone(), buffer_edited)),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1342,14 +1333,14 @@ impl MultiBuffer {
|
||||
snapshot.is_dirty = is_dirty;
|
||||
snapshot.has_conflict = has_conflict;
|
||||
|
||||
excerpts_to_edit.sort_unstable_by_key(|(locator, _, _)| *locator);
|
||||
excerpts_to_edit.sort_unstable_by_key(|(excerpt_id, _, _)| *excerpt_id);
|
||||
|
||||
let mut edits = Vec::new();
|
||||
let mut new_excerpts = SumTree::new();
|
||||
let mut cursor = snapshot.excerpts.cursor::<(Option<&Locator>, usize)>();
|
||||
let mut cursor = snapshot.excerpts.cursor::<(Option<&ExcerptId>, usize)>();
|
||||
|
||||
for (locator, buffer, buffer_edited) in excerpts_to_edit {
|
||||
new_excerpts.push_tree(cursor.slice(&Some(locator), Bias::Left, &()), &());
|
||||
for (id, buffer, buffer_edited) in excerpts_to_edit {
|
||||
new_excerpts.push_tree(cursor.slice(&Some(id), Bias::Left, &()), &());
|
||||
let old_excerpt = cursor.item().unwrap();
|
||||
let buffer_id = buffer.id();
|
||||
let buffer = buffer.read(cx);
|
||||
@@ -1374,8 +1365,8 @@ impl MultiBuffer {
|
||||
);
|
||||
|
||||
new_excerpt = Excerpt::new(
|
||||
old_excerpt.id,
|
||||
locator.clone(),
|
||||
id.clone(),
|
||||
old_excerpt.key,
|
||||
buffer_id,
|
||||
buffer.snapshot(),
|
||||
old_excerpt.range.clone(),
|
||||
@@ -1476,7 +1467,13 @@ impl MultiBuffer {
|
||||
continue;
|
||||
}
|
||||
|
||||
let excerpt_ids = self.excerpt_ids();
|
||||
let excerpt_ids = self
|
||||
.buffers
|
||||
.borrow()
|
||||
.values()
|
||||
.flat_map(|b| &b.excerpts)
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
if excerpt_ids.is_empty() || (rng.gen() && excerpt_ids.len() < max_excerpts) {
|
||||
let buffer_handle = if rng.gen() || self.buffers.borrow().is_empty() {
|
||||
let text = RandomCharIter::new(&mut *rng).take(10).collect::<String>();
|
||||
@@ -1514,26 +1511,24 @@ impl MultiBuffer {
|
||||
log::info!(
|
||||
"Inserting excerpts from buffer {} and ranges {:?}: {:?}",
|
||||
buffer_handle.id(),
|
||||
ranges.iter().map(|r| &r.context).collect::<Vec<_>>(),
|
||||
ranges,
|
||||
ranges
|
||||
.iter()
|
||||
.map(|r| &buffer_text[r.context.clone()])
|
||||
.map(|range| &buffer_text[range.context.clone()])
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
|
||||
let excerpt_id = self.push_excerpts(buffer_handle.clone(), ranges, cx);
|
||||
log::info!("Inserted with ids: {:?}", excerpt_id);
|
||||
log::info!("Inserted with id: {:?}", excerpt_id);
|
||||
} else {
|
||||
let remove_count = rng.gen_range(1..=excerpt_ids.len());
|
||||
let mut excerpts_to_remove = excerpt_ids
|
||||
.choose_multiple(rng, remove_count)
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
let snapshot = self.snapshot.borrow();
|
||||
excerpts_to_remove.sort_unstable_by(|a, b| a.cmp(b, &*snapshot));
|
||||
drop(snapshot);
|
||||
excerpts_to_remove.sort();
|
||||
log::info!("Removing excerpts {:?}", excerpts_to_remove);
|
||||
self.remove_excerpts(excerpts_to_remove, cx);
|
||||
self.remove_excerpts(&excerpts_to_remove, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1568,38 +1563,6 @@ impl MultiBuffer {
|
||||
} else {
|
||||
self.randomly_edit_excerpts(rng, mutation_count, cx);
|
||||
}
|
||||
|
||||
self.check_invariants(cx);
|
||||
}
|
||||
|
||||
fn check_invariants(&self, cx: &mut ModelContext<Self>) {
|
||||
let snapshot = self.read(cx);
|
||||
let excerpts = snapshot.excerpts.items(&());
|
||||
let excerpt_ids = snapshot.excerpt_ids.items(&());
|
||||
|
||||
for (ix, excerpt) in excerpts.iter().enumerate() {
|
||||
if ix == 0 {
|
||||
if excerpt.locator <= Locator::min() {
|
||||
panic!("invalid first excerpt locator {:?}", excerpt.locator);
|
||||
}
|
||||
} else {
|
||||
if excerpt.locator <= excerpts[ix - 1].locator {
|
||||
panic!("excerpts are out-of-order: {:?}", excerpts);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (ix, entry) in excerpt_ids.iter().enumerate() {
|
||||
if ix == 0 {
|
||||
if entry.id.cmp(&ExcerptId::min(), &*snapshot).is_le() {
|
||||
panic!("invalid first excerpt id {:?}", entry.id);
|
||||
}
|
||||
} else {
|
||||
if entry.id <= excerpt_ids[ix - 1].id {
|
||||
panic!("excerpt ids are out-of-order: {:?}", excerpt_ids);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1786,20 +1749,20 @@ impl MultiBufferSnapshot {
|
||||
*cursor.start() + overshoot
|
||||
}
|
||||
|
||||
pub fn clip_point_utf16(&self, point: Unclipped<PointUtf16>, bias: Bias) -> PointUtf16 {
|
||||
pub fn clip_point_utf16(&self, point: PointUtf16, bias: Bias) -> PointUtf16 {
|
||||
if let Some((_, _, buffer)) = self.as_singleton() {
|
||||
return buffer.clip_point_utf16(point, bias);
|
||||
}
|
||||
|
||||
let mut cursor = self.excerpts.cursor::<PointUtf16>();
|
||||
cursor.seek(&point.0, Bias::Right, &());
|
||||
cursor.seek(&point, Bias::Right, &());
|
||||
let overshoot = if let Some(excerpt) = cursor.item() {
|
||||
let excerpt_start = excerpt
|
||||
.buffer
|
||||
.offset_to_point_utf16(excerpt.range.context.start.to_offset(&excerpt.buffer));
|
||||
let buffer_point = excerpt
|
||||
.buffer
|
||||
.clip_point_utf16(Unclipped(excerpt_start + (point.0 - cursor.start())), bias);
|
||||
.clip_point_utf16(excerpt_start + (point - cursor.start()), bias);
|
||||
buffer_point.saturating_sub(excerpt_start)
|
||||
} else {
|
||||
PointUtf16::zero()
|
||||
@@ -2188,9 +2151,7 @@ impl MultiBufferSnapshot {
|
||||
D: TextDimension + Ord + Sub<D, Output = D>,
|
||||
{
|
||||
let mut cursor = self.excerpts.cursor::<ExcerptSummary>();
|
||||
let locator = self.excerpt_locator_for_id(anchor.excerpt_id);
|
||||
|
||||
cursor.seek(locator, Bias::Left, &());
|
||||
cursor.seek(&Some(&anchor.excerpt_id), Bias::Left, &());
|
||||
if cursor.item().is_none() {
|
||||
cursor.next(&());
|
||||
}
|
||||
@@ -2228,25 +2189,24 @@ impl MultiBufferSnapshot {
|
||||
let mut cursor = self.excerpts.cursor::<ExcerptSummary>();
|
||||
let mut summaries = Vec::new();
|
||||
while let Some(anchor) = anchors.peek() {
|
||||
let excerpt_id = anchor.excerpt_id;
|
||||
let excerpt_id = &anchor.excerpt_id;
|
||||
let excerpt_anchors = iter::from_fn(|| {
|
||||
let anchor = anchors.peek()?;
|
||||
if anchor.excerpt_id == excerpt_id {
|
||||
if anchor.excerpt_id == *excerpt_id {
|
||||
Some(&anchors.next().unwrap().text_anchor)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
let locator = self.excerpt_locator_for_id(excerpt_id);
|
||||
cursor.seek_forward(locator, Bias::Left, &());
|
||||
cursor.seek_forward(&Some(excerpt_id), Bias::Left, &());
|
||||
if cursor.item().is_none() {
|
||||
cursor.next(&());
|
||||
}
|
||||
|
||||
let position = D::from_text_summary(&cursor.start().text);
|
||||
if let Some(excerpt) = cursor.item() {
|
||||
if excerpt.id == excerpt_id {
|
||||
if excerpt.id == *excerpt_id {
|
||||
let excerpt_buffer_start =
|
||||
excerpt.range.context.start.summary::<D>(&excerpt.buffer);
|
||||
let excerpt_buffer_end =
|
||||
@@ -2280,18 +2240,13 @@ impl MultiBufferSnapshot {
|
||||
I: 'a + IntoIterator<Item = &'a Anchor>,
|
||||
{
|
||||
let mut anchors = anchors.into_iter().enumerate().peekable();
|
||||
let mut cursor = self.excerpts.cursor::<Option<&Locator>>();
|
||||
cursor.next(&());
|
||||
|
||||
let mut cursor = self.excerpts.cursor::<Option<&ExcerptId>>();
|
||||
let mut result = Vec::new();
|
||||
|
||||
while let Some((_, anchor)) = anchors.peek() {
|
||||
let old_excerpt_id = anchor.excerpt_id;
|
||||
let old_excerpt_id = &anchor.excerpt_id;
|
||||
|
||||
// Find the location where this anchor's excerpt should be.
|
||||
let old_locator = self.excerpt_locator_for_id(old_excerpt_id);
|
||||
cursor.seek_forward(&Some(old_locator), Bias::Left, &());
|
||||
|
||||
cursor.seek_forward(&Some(old_excerpt_id), Bias::Left, &());
|
||||
if cursor.item().is_none() {
|
||||
cursor.next(&());
|
||||
}
|
||||
@@ -2301,22 +2256,27 @@ impl MultiBufferSnapshot {
|
||||
|
||||
// Process all of the anchors for this excerpt.
|
||||
while let Some((_, anchor)) = anchors.peek() {
|
||||
if anchor.excerpt_id != old_excerpt_id {
|
||||
if anchor.excerpt_id != *old_excerpt_id {
|
||||
break;
|
||||
}
|
||||
let mut kept_position = false;
|
||||
let (anchor_ix, anchor) = anchors.next().unwrap();
|
||||
let mut anchor = *anchor;
|
||||
let mut anchor = anchor.clone();
|
||||
|
||||
let id_invalid =
|
||||
*old_excerpt_id == ExcerptId::max() || *old_excerpt_id == ExcerptId::min();
|
||||
let still_exists = next_excerpt.map_or(false, |excerpt| {
|
||||
excerpt.id == *old_excerpt_id && excerpt.contains(&anchor)
|
||||
});
|
||||
|
||||
// Leave min and max anchors unchanged if invalid or
|
||||
// if the old excerpt still exists at this location
|
||||
let mut kept_position = next_excerpt
|
||||
.map_or(false, |e| e.id == old_excerpt_id && e.contains(&anchor))
|
||||
|| old_excerpt_id == ExcerptId::max()
|
||||
|| old_excerpt_id == ExcerptId::min();
|
||||
|
||||
if id_invalid || still_exists {
|
||||
kept_position = true;
|
||||
}
|
||||
// If the old excerpt no longer exists at this location, then attempt to
|
||||
// find an equivalent position for this anchor in an adjacent excerpt.
|
||||
if !kept_position {
|
||||
else {
|
||||
for excerpt in [next_excerpt, prev_excerpt].iter().filter_map(|e| *e) {
|
||||
if excerpt.contains(&anchor) {
|
||||
anchor.excerpt_id = excerpt.id.clone();
|
||||
@@ -2325,7 +2285,6 @@ impl MultiBufferSnapshot {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If there's no adjacent excerpt that contains the anchor's position,
|
||||
// then report that the anchor has lost its position.
|
||||
if !kept_position {
|
||||
@@ -2395,7 +2354,7 @@ impl MultiBufferSnapshot {
|
||||
};
|
||||
}
|
||||
|
||||
let mut cursor = self.excerpts.cursor::<(usize, Option<ExcerptId>)>();
|
||||
let mut cursor = self.excerpts.cursor::<(usize, Option<&ExcerptId>)>();
|
||||
cursor.seek(&offset, Bias::Right, &());
|
||||
if cursor.item().is_none() && offset == cursor.start().0 && bias == Bias::Left {
|
||||
cursor.prev(&());
|
||||
@@ -2423,9 +2382,8 @@ impl MultiBufferSnapshot {
|
||||
}
|
||||
|
||||
pub fn anchor_in_excerpt(&self, excerpt_id: ExcerptId, text_anchor: text::Anchor) -> Anchor {
|
||||
let locator = self.excerpt_locator_for_id(excerpt_id);
|
||||
let mut cursor = self.excerpts.cursor::<Option<&Locator>>();
|
||||
cursor.seek(locator, Bias::Left, &());
|
||||
let mut cursor = self.excerpts.cursor::<Option<&ExcerptId>>();
|
||||
cursor.seek(&Some(&excerpt_id), Bias::Left, &());
|
||||
if let Some(excerpt) = cursor.item() {
|
||||
if excerpt.id == excerpt_id {
|
||||
let text_anchor = excerpt.clip_anchor(text_anchor);
|
||||
@@ -2443,7 +2401,7 @@ impl MultiBufferSnapshot {
|
||||
pub fn can_resolve(&self, anchor: &Anchor) -> bool {
|
||||
if anchor.excerpt_id == ExcerptId::min() || anchor.excerpt_id == ExcerptId::max() {
|
||||
true
|
||||
} else if let Some(excerpt) = self.excerpt(anchor.excerpt_id) {
|
||||
} else if let Some(excerpt) = self.excerpt(&anchor.excerpt_id) {
|
||||
excerpt.buffer.can_resolve(&anchor.text_anchor)
|
||||
} else {
|
||||
false
|
||||
@@ -2498,6 +2456,7 @@ impl MultiBufferSnapshot {
|
||||
let starts_new_buffer = Some(excerpt.buffer_id) != prev_buffer_id;
|
||||
let boundary = ExcerptBoundary {
|
||||
id: excerpt.id.clone(),
|
||||
key: excerpt.key,
|
||||
row: cursor.start().1.row,
|
||||
buffer: excerpt.buffer.clone(),
|
||||
range: excerpt.range.clone(),
|
||||
@@ -2719,8 +2678,8 @@ impl MultiBufferSnapshot {
|
||||
.flatten()
|
||||
.map(|item| OutlineItem {
|
||||
depth: item.depth,
|
||||
range: self.anchor_in_excerpt(excerpt_id, item.range.start)
|
||||
..self.anchor_in_excerpt(excerpt_id, item.range.end),
|
||||
range: self.anchor_in_excerpt(excerpt_id.clone(), item.range.start)
|
||||
..self.anchor_in_excerpt(excerpt_id.clone(), item.range.end),
|
||||
text: item.text,
|
||||
highlight_ranges: item.highlight_ranges,
|
||||
name_ranges: item.name_ranges,
|
||||
@@ -2729,29 +2688,11 @@ impl MultiBufferSnapshot {
|
||||
))
|
||||
}
|
||||
|
||||
fn excerpt_locator_for_id<'a>(&'a self, id: ExcerptId) -> &'a Locator {
|
||||
if id == ExcerptId::min() {
|
||||
Locator::min_ref()
|
||||
} else if id == ExcerptId::max() {
|
||||
Locator::max_ref()
|
||||
} else {
|
||||
let mut cursor = self.excerpt_ids.cursor::<ExcerptId>();
|
||||
cursor.seek(&id, Bias::Left, &());
|
||||
if let Some(entry) = cursor.item() {
|
||||
if entry.id == id {
|
||||
return &entry.locator;
|
||||
}
|
||||
}
|
||||
panic!("invalid excerpt id {:?}", id)
|
||||
}
|
||||
}
|
||||
|
||||
fn excerpt<'a>(&'a self, excerpt_id: ExcerptId) -> Option<&'a Excerpt> {
|
||||
let mut cursor = self.excerpts.cursor::<Option<&Locator>>();
|
||||
let locator = self.excerpt_locator_for_id(excerpt_id);
|
||||
cursor.seek(&Some(locator), Bias::Left, &());
|
||||
fn excerpt<'a>(&'a self, excerpt_id: &'a ExcerptId) -> Option<&'a Excerpt> {
|
||||
let mut cursor = self.excerpts.cursor::<Option<&ExcerptId>>();
|
||||
cursor.seek(&Some(excerpt_id), Bias::Left, &());
|
||||
if let Some(excerpt) = cursor.item() {
|
||||
if excerpt.id == excerpt_id {
|
||||
if excerpt.id == *excerpt_id {
|
||||
return Some(excerpt);
|
||||
}
|
||||
}
|
||||
@@ -2762,12 +2703,10 @@ impl MultiBufferSnapshot {
|
||||
&'a self,
|
||||
range: &'a Range<Anchor>,
|
||||
) -> impl 'a + Iterator<Item = (ReplicaId, bool, CursorShape, Selection<Anchor>)> {
|
||||
let mut cursor = self.excerpts.cursor::<ExcerptSummary>();
|
||||
let start_locator = self.excerpt_locator_for_id(range.start.excerpt_id);
|
||||
let end_locator = self.excerpt_locator_for_id(range.end.excerpt_id);
|
||||
cursor.seek(start_locator, Bias::Left, &());
|
||||
let mut cursor = self.excerpts.cursor::<Option<&ExcerptId>>();
|
||||
cursor.seek(&Some(&range.start.excerpt_id), Bias::Left, &());
|
||||
cursor
|
||||
.take_while(move |excerpt| excerpt.locator <= *end_locator)
|
||||
.take_while(move |excerpt| excerpt.id <= range.end.excerpt_id)
|
||||
.flat_map(move |excerpt| {
|
||||
let mut query_range = excerpt.range.context.start..excerpt.range.context.end;
|
||||
if excerpt.id == range.start.excerpt_id {
|
||||
@@ -2977,7 +2916,7 @@ impl History {
|
||||
impl Excerpt {
|
||||
fn new(
|
||||
id: ExcerptId,
|
||||
locator: Locator,
|
||||
key: usize,
|
||||
buffer_id: usize,
|
||||
buffer: BufferSnapshot,
|
||||
range: ExcerptRange<text::Anchor>,
|
||||
@@ -2985,7 +2924,7 @@ impl Excerpt {
|
||||
) -> Self {
|
||||
Excerpt {
|
||||
id,
|
||||
locator,
|
||||
key,
|
||||
max_buffer_row: range.context.end.to_point(&buffer).row,
|
||||
text_summary: buffer
|
||||
.text_summary_for_range::<TextSummary, _>(range.context.to_offset(&buffer)),
|
||||
@@ -3071,33 +3010,10 @@ impl Excerpt {
|
||||
}
|
||||
}
|
||||
|
||||
impl ExcerptId {
|
||||
pub fn min() -> Self {
|
||||
Self(0)
|
||||
}
|
||||
|
||||
pub fn max() -> Self {
|
||||
Self(usize::MAX)
|
||||
}
|
||||
|
||||
pub fn cmp(&self, other: &Self, snapshot: &MultiBufferSnapshot) -> cmp::Ordering {
|
||||
let a = snapshot.excerpt_locator_for_id(*self);
|
||||
let b = snapshot.excerpt_locator_for_id(*other);
|
||||
a.cmp(&b).then_with(|| self.0.cmp(&other.0))
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<usize> for ExcerptId {
|
||||
fn into(self) -> usize {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for Excerpt {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("Excerpt")
|
||||
.field("id", &self.id)
|
||||
.field("locator", &self.locator)
|
||||
.field("buffer_id", &self.buffer_id)
|
||||
.field("range", &self.range)
|
||||
.field("text_summary", &self.text_summary)
|
||||
@@ -3115,44 +3031,19 @@ impl sum_tree::Item for Excerpt {
|
||||
text += TextSummary::from("\n");
|
||||
}
|
||||
ExcerptSummary {
|
||||
excerpt_id: self.id,
|
||||
excerpt_locator: self.locator.clone(),
|
||||
excerpt_id: self.id.clone(),
|
||||
max_buffer_row: self.max_buffer_row,
|
||||
text,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl sum_tree::Item for ExcerptIdMapping {
|
||||
type Summary = ExcerptId;
|
||||
|
||||
fn summary(&self) -> Self::Summary {
|
||||
self.id
|
||||
}
|
||||
}
|
||||
|
||||
impl sum_tree::KeyedItem for ExcerptIdMapping {
|
||||
type Key = ExcerptId;
|
||||
|
||||
fn key(&self) -> Self::Key {
|
||||
self.id
|
||||
}
|
||||
}
|
||||
|
||||
impl sum_tree::Summary for ExcerptId {
|
||||
type Context = ();
|
||||
|
||||
fn add_summary(&mut self, other: &Self, _: &()) {
|
||||
*self = *other;
|
||||
}
|
||||
}
|
||||
|
||||
impl sum_tree::Summary for ExcerptSummary {
|
||||
type Context = ();
|
||||
|
||||
fn add_summary(&mut self, summary: &Self, _: &()) {
|
||||
debug_assert!(summary.excerpt_locator > self.excerpt_locator);
|
||||
self.excerpt_locator = summary.excerpt_locator.clone();
|
||||
debug_assert!(summary.excerpt_id > self.excerpt_id);
|
||||
self.excerpt_id = summary.excerpt_id.clone();
|
||||
self.text.add_summary(&summary.text, &());
|
||||
self.max_buffer_row = cmp::max(self.max_buffer_row, summary.max_buffer_row);
|
||||
}
|
||||
@@ -3176,15 +3067,9 @@ impl<'a> sum_tree::SeekTarget<'a, ExcerptSummary, ExcerptSummary> for usize {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> sum_tree::SeekTarget<'a, ExcerptSummary, Option<&'a Locator>> for Locator {
|
||||
fn cmp(&self, cursor_location: &Option<&'a Locator>, _: &()) -> cmp::Ordering {
|
||||
Ord::cmp(&Some(self), cursor_location)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> sum_tree::SeekTarget<'a, ExcerptSummary, ExcerptSummary> for Locator {
|
||||
impl<'a> sum_tree::SeekTarget<'a, ExcerptSummary, ExcerptSummary> for Option<&'a ExcerptId> {
|
||||
fn cmp(&self, cursor_location: &ExcerptSummary, _: &()) -> cmp::Ordering {
|
||||
Ord::cmp(self, &cursor_location.excerpt_locator)
|
||||
Ord::cmp(self, &Some(&cursor_location.excerpt_id))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3206,15 +3091,9 @@ impl<'a> sum_tree::Dimension<'a, ExcerptSummary> for PointUtf16 {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> sum_tree::Dimension<'a, ExcerptSummary> for Option<&'a Locator> {
|
||||
impl<'a> sum_tree::Dimension<'a, ExcerptSummary> for Option<&'a ExcerptId> {
|
||||
fn add_summary(&mut self, summary: &'a ExcerptSummary, _: &()) {
|
||||
*self = Some(&summary.excerpt_locator);
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> sum_tree::Dimension<'a, ExcerptSummary> for Option<ExcerptId> {
|
||||
fn add_summary(&mut self, summary: &'a ExcerptSummary, _: &()) {
|
||||
*self = Some(summary.excerpt_id);
|
||||
*self = Some(&summary.excerpt_id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3395,6 +3274,12 @@ impl ToOffset for Point {
|
||||
}
|
||||
}
|
||||
|
||||
impl ToOffset for PointUtf16 {
|
||||
fn to_offset<'a>(&self, snapshot: &MultiBufferSnapshot) -> usize {
|
||||
snapshot.point_utf16_to_offset(*self)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToOffset for usize {
|
||||
fn to_offset<'a>(&self, snapshot: &MultiBufferSnapshot) -> usize {
|
||||
assert!(*self <= snapshot.len(), "offset is out of range");
|
||||
@@ -3408,12 +3293,6 @@ impl ToOffset for OffsetUtf16 {
|
||||
}
|
||||
}
|
||||
|
||||
impl ToOffset for PointUtf16 {
|
||||
fn to_offset<'a>(&self, snapshot: &MultiBufferSnapshot) -> usize {
|
||||
snapshot.point_utf16_to_offset(*self)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToOffsetUtf16 for OffsetUtf16 {
|
||||
fn to_offset_utf16(&self, _snapshot: &MultiBufferSnapshot) -> OffsetUtf16 {
|
||||
*self
|
||||
@@ -3712,7 +3591,7 @@ mod tests {
|
||||
let snapshot = multibuffer.update(cx, |multibuffer, cx| {
|
||||
let (buffer_2_excerpt_id, _) =
|
||||
multibuffer.excerpts_for_buffer(&buffer_2, cx)[0].clone();
|
||||
multibuffer.remove_excerpts([buffer_2_excerpt_id], cx);
|
||||
multibuffer.remove_excerpts(&[buffer_2_excerpt_id], cx);
|
||||
multibuffer.snapshot(cx)
|
||||
});
|
||||
|
||||
@@ -3901,7 +3780,7 @@ mod tests {
|
||||
|
||||
// Replace the buffer 1 excerpt with new excerpts from buffer 2.
|
||||
let (excerpt_id_2, excerpt_id_3) = multibuffer.update(cx, |multibuffer, cx| {
|
||||
multibuffer.remove_excerpts([excerpt_id_1], cx);
|
||||
multibuffer.remove_excerpts([&excerpt_id_1], cx);
|
||||
let mut ids = multibuffer
|
||||
.push_excerpts(
|
||||
buffer_2.clone(),
|
||||
@@ -3931,8 +3810,9 @@ mod tests {
|
||||
assert_ne!(excerpt_id_2, excerpt_id_1);
|
||||
|
||||
// Resolve some anchors from the previous snapshot in the new snapshot.
|
||||
// The current excerpts are from a different buffer, so we don't attempt to
|
||||
// resolve the old text anchor in the new buffer.
|
||||
// Although there is still an excerpt with the same id, it is for
|
||||
// a different buffer, so we don't attempt to resolve the old text
|
||||
// anchor in the new buffer.
|
||||
assert_eq!(
|
||||
snapshot_2.summary_for_anchor::<usize>(&snapshot_1.anchor_before(2)),
|
||||
0
|
||||
@@ -3944,9 +3824,6 @@ mod tests {
|
||||
]),
|
||||
vec![0, 0]
|
||||
);
|
||||
|
||||
// Refresh anchors from the old snapshot. The return value indicates that both
|
||||
// anchors lost their original excerpt.
|
||||
let refresh =
|
||||
snapshot_2.refresh_anchors(&[snapshot_1.anchor_before(2), snapshot_1.anchor_after(3)]);
|
||||
assert_eq!(
|
||||
@@ -3960,10 +3837,10 @@ mod tests {
|
||||
// Replace the middle excerpt with a smaller excerpt in buffer 2,
|
||||
// that intersects the old excerpt.
|
||||
let excerpt_id_5 = multibuffer.update(cx, |multibuffer, cx| {
|
||||
multibuffer.remove_excerpts([excerpt_id_3], cx);
|
||||
multibuffer.remove_excerpts([&excerpt_id_3], cx);
|
||||
multibuffer
|
||||
.insert_excerpts_after(
|
||||
excerpt_id_2,
|
||||
&excerpt_id_3,
|
||||
buffer_2.clone(),
|
||||
[ExcerptRange {
|
||||
context: 5..8,
|
||||
@@ -3980,8 +3857,8 @@ mod tests {
|
||||
assert_ne!(excerpt_id_5, excerpt_id_3);
|
||||
|
||||
// Resolve some anchors from the previous snapshot in the new snapshot.
|
||||
// The third anchor can't be resolved, since its excerpt has been removed,
|
||||
// so it resolves to the same position as its predecessor.
|
||||
// The anchor in the middle excerpt snaps to the beginning of the
|
||||
// excerpt, since it is not
|
||||
let anchors = [
|
||||
snapshot_2.anchor_before(0),
|
||||
snapshot_2.anchor_after(2),
|
||||
@@ -3990,7 +3867,7 @@ mod tests {
|
||||
];
|
||||
assert_eq!(
|
||||
snapshot_3.summaries_for_anchors::<usize, _>(&anchors),
|
||||
&[0, 2, 9, 13]
|
||||
&[0, 2, 5, 13]
|
||||
);
|
||||
|
||||
let new_anchors = snapshot_3.refresh_anchors(&anchors);
|
||||
@@ -4012,7 +3889,7 @@ mod tests {
|
||||
|
||||
let mut buffers: Vec<ModelHandle<Buffer>> = Vec::new();
|
||||
let multibuffer = cx.add_model(|_| MultiBuffer::new(0));
|
||||
let mut excerpt_ids = Vec::<ExcerptId>::new();
|
||||
let mut excerpt_ids = Vec::new();
|
||||
let mut expected_excerpts = Vec::<(ModelHandle<Buffer>, Range<text::Anchor>)>::new();
|
||||
let mut anchors = Vec::new();
|
||||
let mut old_versions = Vec::new();
|
||||
@@ -4042,11 +3919,9 @@ mod tests {
|
||||
.collect::<String>(),
|
||||
);
|
||||
}
|
||||
let snapshot = multibuffer.read(cx).read(cx);
|
||||
ids_to_remove.sort_unstable_by(|a, b| a.cmp(&b, &snapshot));
|
||||
drop(snapshot);
|
||||
ids_to_remove.sort_unstable();
|
||||
multibuffer.update(cx, |multibuffer, cx| {
|
||||
multibuffer.remove_excerpts(ids_to_remove, cx)
|
||||
multibuffer.remove_excerpts(&ids_to_remove, cx)
|
||||
});
|
||||
}
|
||||
30..=39 if !expected_excerpts.is_empty() => {
|
||||
@@ -4070,6 +3945,7 @@ mod tests {
|
||||
// Ensure the newly-refreshed anchors point to a valid excerpt and don't
|
||||
// overshoot its boundaries.
|
||||
assert_eq!(anchors.len(), prev_len);
|
||||
let mut cursor = multibuffer.excerpts.cursor::<Option<&ExcerptId>>();
|
||||
for anchor in &anchors {
|
||||
if anchor.excerpt_id == ExcerptId::min()
|
||||
|| anchor.excerpt_id == ExcerptId::max()
|
||||
@@ -4077,7 +3953,8 @@ mod tests {
|
||||
continue;
|
||||
}
|
||||
|
||||
let excerpt = multibuffer.excerpt(anchor.excerpt_id).unwrap();
|
||||
cursor.seek_forward(&Some(&anchor.excerpt_id), Bias::Left, &());
|
||||
let excerpt = cursor.item().unwrap();
|
||||
assert_eq!(excerpt.id, anchor.excerpt_id);
|
||||
assert!(excerpt.contains(anchor));
|
||||
}
|
||||
@@ -4117,7 +3994,7 @@ mod tests {
|
||||
let excerpt_id = multibuffer.update(cx, |multibuffer, cx| {
|
||||
multibuffer
|
||||
.insert_excerpts_after(
|
||||
prev_excerpt_id,
|
||||
&prev_excerpt_id,
|
||||
buffer_handle.clone(),
|
||||
[ExcerptRange {
|
||||
context: start_ix..end_ix,
|
||||
@@ -4281,14 +4158,12 @@ mod tests {
|
||||
}
|
||||
|
||||
for _ in 0..ch.len_utf16() {
|
||||
let left_point_utf16 =
|
||||
snapshot.clip_point_utf16(Unclipped(point_utf16), Bias::Left);
|
||||
let right_point_utf16 =
|
||||
snapshot.clip_point_utf16(Unclipped(point_utf16), Bias::Right);
|
||||
let left_point_utf16 = snapshot.clip_point_utf16(point_utf16, Bias::Left);
|
||||
let right_point_utf16 = snapshot.clip_point_utf16(point_utf16, Bias::Right);
|
||||
let buffer_left_point_utf16 =
|
||||
buffer.clip_point_utf16(Unclipped(buffer_point_utf16), Bias::Left);
|
||||
buffer.clip_point_utf16(buffer_point_utf16, Bias::Left);
|
||||
let buffer_right_point_utf16 =
|
||||
buffer.clip_point_utf16(Unclipped(buffer_point_utf16), Bias::Right);
|
||||
buffer.clip_point_utf16(buffer_point_utf16, Bias::Right);
|
||||
assert_eq!(
|
||||
left_point_utf16,
|
||||
excerpt_start.lines_utf16()
|
||||
|
||||
@@ -6,7 +6,7 @@ use std::{
|
||||
};
|
||||
use sum_tree::Bias;
|
||||
|
||||
#[derive(Clone, Copy, Eq, PartialEq, Debug, Hash)]
|
||||
#[derive(Clone, Eq, PartialEq, Debug, Hash)]
|
||||
pub struct Anchor {
|
||||
pub(crate) buffer_id: Option<usize>,
|
||||
pub(crate) excerpt_id: ExcerptId,
|
||||
@@ -30,16 +30,16 @@ impl Anchor {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn excerpt_id(&self) -> ExcerptId {
|
||||
self.excerpt_id
|
||||
pub fn excerpt_id(&self) -> &ExcerptId {
|
||||
&self.excerpt_id
|
||||
}
|
||||
|
||||
pub fn cmp(&self, other: &Anchor, snapshot: &MultiBufferSnapshot) -> Ordering {
|
||||
let excerpt_id_cmp = self.excerpt_id.cmp(&other.excerpt_id, snapshot);
|
||||
let excerpt_id_cmp = self.excerpt_id.cmp(&other.excerpt_id);
|
||||
if excerpt_id_cmp.is_eq() {
|
||||
if self.excerpt_id == ExcerptId::min() || self.excerpt_id == ExcerptId::max() {
|
||||
Ordering::Equal
|
||||
} else if let Some(excerpt) = snapshot.excerpt(self.excerpt_id) {
|
||||
} else if let Some(excerpt) = snapshot.excerpt(&self.excerpt_id) {
|
||||
self.text_anchor.cmp(&other.text_anchor, &excerpt.buffer)
|
||||
} else {
|
||||
Ordering::Equal
|
||||
@@ -51,7 +51,7 @@ impl Anchor {
|
||||
|
||||
pub fn bias_left(&self, snapshot: &MultiBufferSnapshot) -> Anchor {
|
||||
if self.text_anchor.bias != Bias::Left {
|
||||
if let Some(excerpt) = snapshot.excerpt(self.excerpt_id) {
|
||||
if let Some(excerpt) = snapshot.excerpt(&self.excerpt_id) {
|
||||
return Self {
|
||||
buffer_id: self.buffer_id,
|
||||
excerpt_id: self.excerpt_id.clone(),
|
||||
@@ -64,7 +64,7 @@ impl Anchor {
|
||||
|
||||
pub fn bias_right(&self, snapshot: &MultiBufferSnapshot) -> Anchor {
|
||||
if self.text_anchor.bias != Bias::Right {
|
||||
if let Some(excerpt) = snapshot.excerpt(self.excerpt_id) {
|
||||
if let Some(excerpt) = snapshot.excerpt(&self.excerpt_id) {
|
||||
return Self {
|
||||
buffer_id: self.buffer_id,
|
||||
excerpt_id: self.excerpt_id.clone(),
|
||||
|
||||
@@ -544,21 +544,11 @@ impl<'a> MutableSelectionsCollection<'a> {
|
||||
T: ToOffset,
|
||||
{
|
||||
let buffer = self.buffer.read(self.cx).snapshot(self.cx);
|
||||
let ranges = ranges
|
||||
.into_iter()
|
||||
.map(|range| range.start.to_offset(&buffer)..range.end.to_offset(&buffer));
|
||||
self.select_offset_ranges(ranges);
|
||||
}
|
||||
|
||||
fn select_offset_ranges<I>(&mut self, ranges: I)
|
||||
where
|
||||
I: IntoIterator<Item = Range<usize>>,
|
||||
{
|
||||
let selections = ranges
|
||||
.into_iter()
|
||||
.map(|range| {
|
||||
let mut start = range.start;
|
||||
let mut end = range.end;
|
||||
let mut start = range.start.to_offset(&buffer);
|
||||
let mut end = range.end.to_offset(&buffer);
|
||||
let reversed = if start > end {
|
||||
mem::swap(&mut start, &mut end);
|
||||
true
|
||||
|
||||
@@ -76,9 +76,7 @@ impl<'a> EditorLspTestContext<'a> {
|
||||
|
||||
let file = cx.read(|cx| workspace.file_project_paths(cx)[0].clone());
|
||||
let item = workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.open_path(file, None, true, cx)
|
||||
})
|
||||
.update(cx, |workspace, cx| workspace.open_path(file, true, cx))
|
||||
.await
|
||||
.expect("Could not open test file");
|
||||
|
||||
|
||||
@@ -169,7 +169,7 @@ impl<'a> EditorTestContext<'a> {
|
||||
let (unmarked_text, selection_ranges) = marked_text_ranges(marked_text, true);
|
||||
self.editor.update(self.cx, |editor, cx| {
|
||||
editor.set_text(unmarked_text, cx);
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
s.select_ranges(selection_ranges)
|
||||
})
|
||||
});
|
||||
|
||||
@@ -104,7 +104,7 @@ impl FileFinder {
|
||||
match event {
|
||||
Event::Selected(project_path) => {
|
||||
workspace
|
||||
.open_path(project_path.clone(), None, true, cx)
|
||||
.open_path(project_path.clone(), true, cx)
|
||||
.detach_and_log_err(cx);
|
||||
workspace.dismiss_modal(cx);
|
||||
}
|
||||
@@ -119,7 +119,7 @@ impl FileFinder {
|
||||
cx.observe(&project, Self::project_updated).detach();
|
||||
Self {
|
||||
project,
|
||||
picker: cx.add_view(|cx| Picker::new("Search project files...", handle, cx)),
|
||||
picker: cx.add_view(|cx| Picker::new(handle, cx)),
|
||||
search_count: 0,
|
||||
latest_search_id: 0,
|
||||
latest_search_did_cancel: false,
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use editor::{display_map::ToDisplayPoint, Autoscroll, DisplayPoint, Editor};
|
||||
use gpui::{
|
||||
actions, elements::*, geometry::vector::Vector2F, AnyViewHandle, Axis, Entity,
|
||||
@@ -33,10 +31,7 @@ pub enum Event {
|
||||
impl GoToLine {
|
||||
pub fn new(active_editor: ViewHandle<Editor>, cx: &mut ViewContext<Self>) -> Self {
|
||||
let line_editor = cx.add_view(|cx| {
|
||||
Editor::single_line(
|
||||
Some(Arc::new(|theme| theme.picker.input_editor.clone())),
|
||||
cx,
|
||||
)
|
||||
Editor::single_line(Some(|theme| theme.picker.input_editor.clone()), cx)
|
||||
});
|
||||
cx.subscribe(&line_editor, Self::on_line_editor_event)
|
||||
.detach();
|
||||
@@ -83,7 +78,7 @@ impl GoToLine {
|
||||
if let Some(rows) = active_editor.highlighted_rows() {
|
||||
let snapshot = active_editor.snapshot(cx).display_snapshot;
|
||||
let position = DisplayPoint::new(rows.start, 0).to_point(&snapshot);
|
||||
active_editor.change_selections(Some(Autoscroll::center()), cx, |s| {
|
||||
active_editor.change_selections(Some(Autoscroll::Center), cx, |s| {
|
||||
s.select_ranges([position..position])
|
||||
});
|
||||
}
|
||||
@@ -127,7 +122,7 @@ impl GoToLine {
|
||||
let display_point = point.to_display_point(&snapshot);
|
||||
let row = display_point.row();
|
||||
active_editor.highlight_rows(Some(row..row + 1));
|
||||
active_editor.request_autoscroll(Autoscroll::center(), cx);
|
||||
active_editor.request_autoscroll(Autoscroll::Center, cx);
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
@@ -175,8 +170,8 @@ impl View for GoToLine {
|
||||
.boxed(),
|
||||
)
|
||||
.with_child(
|
||||
Container::new(Label::new(label, theme.no_matches.label.clone()).boxed())
|
||||
.with_style(theme.no_matches.container)
|
||||
Container::new(Label::new(label, theme.empty.label.clone()).boxed())
|
||||
.with_style(theme.empty.container)
|
||||
.boxed(),
|
||||
)
|
||||
.boxed(),
|
||||
|
||||
@@ -257,19 +257,17 @@ impl Element for Flex {
|
||||
let axis = self.axis;
|
||||
move |e, cx| {
|
||||
if remaining_space < 0. {
|
||||
let scroll_delta = e.delta.raw();
|
||||
|
||||
let mut delta = match axis {
|
||||
Axis::Horizontal => {
|
||||
if scroll_delta.x().abs() >= scroll_delta.y().abs() {
|
||||
scroll_delta.x()
|
||||
if e.delta.x().abs() >= e.delta.y().abs() {
|
||||
e.delta.x()
|
||||
} else {
|
||||
scroll_delta.y()
|
||||
e.delta.y()
|
||||
}
|
||||
}
|
||||
Axis::Vertical => scroll_delta.y(),
|
||||
Axis::Vertical => e.delta.y(),
|
||||
};
|
||||
if !e.delta.precise() {
|
||||
if !e.precise {
|
||||
delta *= 20.;
|
||||
}
|
||||
|
||||
|
||||
@@ -258,8 +258,8 @@ impl Element for List {
|
||||
state.0.borrow_mut().scroll(
|
||||
&scroll_top,
|
||||
height,
|
||||
*e.platform_event.delta.raw(),
|
||||
e.platform_event.delta.precise(),
|
||||
e.platform_event.delta,
|
||||
e.platform_event.precise,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -295,19 +295,15 @@ impl Element for UniformList {
|
||||
move |MouseScrollWheel {
|
||||
platform_event:
|
||||
ScrollWheelEvent {
|
||||
position, delta, ..
|
||||
position,
|
||||
delta,
|
||||
precise,
|
||||
..
|
||||
},
|
||||
..
|
||||
},
|
||||
cx| {
|
||||
if !Self::scroll(
|
||||
state.clone(),
|
||||
position,
|
||||
*delta.raw(),
|
||||
delta.precise(),
|
||||
scroll_max,
|
||||
cx,
|
||||
) {
|
||||
if !Self::scroll(state.clone(), position, delta, precise, scroll_max, cx) {
|
||||
cx.propagate_event();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
use std::ops::Deref;
|
||||
|
||||
use pathfinder_geometry::vector::vec2f;
|
||||
|
||||
use crate::{geometry::vector::Vector2F, keymap::Keystroke};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -46,45 +44,11 @@ pub enum TouchPhase {
|
||||
Ended,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum ScrollDelta {
|
||||
Pixels(Vector2F),
|
||||
Lines(Vector2F),
|
||||
}
|
||||
|
||||
impl Default for ScrollDelta {
|
||||
fn default() -> Self {
|
||||
Self::Lines(Default::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl ScrollDelta {
|
||||
pub fn raw(&self) -> &Vector2F {
|
||||
match self {
|
||||
ScrollDelta::Pixels(v) => v,
|
||||
ScrollDelta::Lines(v) => v,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn precise(&self) -> bool {
|
||||
match self {
|
||||
ScrollDelta::Pixels(_) => true,
|
||||
ScrollDelta::Lines(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pixel_delta(&self, line_height: f32) -> Vector2F {
|
||||
match self {
|
||||
ScrollDelta::Pixels(delta) => *delta,
|
||||
ScrollDelta::Lines(delta) => vec2f(delta.x() * line_height, delta.y() * line_height),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub struct ScrollWheelEvent {
|
||||
pub position: Vector2F,
|
||||
pub delta: ScrollDelta,
|
||||
pub delta: Vector2F,
|
||||
pub precise: bool,
|
||||
pub modifiers: Modifiers,
|
||||
/// If the platform supports returning the phase of a scroll wheel event, it will be stored here
|
||||
pub phase: Option<TouchPhase>,
|
||||
|
||||
@@ -3,7 +3,7 @@ use crate::{
|
||||
keymap::Keystroke,
|
||||
platform::{Event, NavigationDirection},
|
||||
KeyDownEvent, KeyUpEvent, Modifiers, ModifiersChangedEvent, MouseButton, MouseButtonEvent,
|
||||
MouseMovedEvent, ScrollDelta, ScrollWheelEvent, TouchPhase,
|
||||
MouseMovedEvent, ScrollWheelEvent, TouchPhase,
|
||||
};
|
||||
use cocoa::{
|
||||
appkit::{NSEvent, NSEventModifierFlags, NSEventPhase, NSEventType},
|
||||
@@ -164,24 +164,17 @@ impl Event {
|
||||
_ => Some(TouchPhase::Moved),
|
||||
};
|
||||
|
||||
let raw_data = vec2f(
|
||||
native_event.scrollingDeltaX() as f32,
|
||||
native_event.scrollingDeltaY() as f32,
|
||||
);
|
||||
|
||||
let delta = if native_event.hasPreciseScrollingDeltas() == YES {
|
||||
ScrollDelta::Pixels(raw_data)
|
||||
} else {
|
||||
ScrollDelta::Lines(raw_data)
|
||||
};
|
||||
|
||||
Self::ScrollWheel(ScrollWheelEvent {
|
||||
position: vec2f(
|
||||
native_event.locationInWindow().x as f32,
|
||||
window_height - native_event.locationInWindow().y as f32,
|
||||
),
|
||||
delta,
|
||||
delta: vec2f(
|
||||
native_event.scrollingDeltaX() as f32,
|
||||
native_event.scrollingDeltaY() as f32,
|
||||
),
|
||||
phase,
|
||||
precise: native_event.hasPreciseScrollingDeltas() == YES,
|
||||
modifiers: read_modifiers(native_event),
|
||||
})
|
||||
}),
|
||||
|
||||
@@ -475,35 +475,27 @@ impl Presenter {
|
||||
if let MouseEvent::Down(e) = &mouse_event {
|
||||
if valid_region
|
||||
.handlers
|
||||
.contains(MouseEvent::click_disc(), Some(e.button))
|
||||
.contains_handler(MouseEvent::click_disc(), Some(e.button))
|
||||
|| valid_region
|
||||
.handlers
|
||||
.contains(MouseEvent::drag_disc(), Some(e.button))
|
||||
.contains_handler(MouseEvent::drag_disc(), Some(e.button))
|
||||
{
|
||||
event_cx.handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
// `event_consumed` should only be true if there are any handlers for this event.
|
||||
let mut event_consumed = event_cx.handled;
|
||||
if let Some(callbacks) = valid_region.handlers.get(&mouse_event.handler_key()) {
|
||||
event_consumed = true;
|
||||
for callback in callbacks {
|
||||
event_cx.handled = true;
|
||||
event_cx.with_current_view(valid_region.id().view_id(), {
|
||||
let region_event = mouse_event.clone();
|
||||
|cx| callback(region_event, cx)
|
||||
});
|
||||
event_consumed &= event_cx.handled;
|
||||
any_event_handled |= event_cx.handled;
|
||||
}
|
||||
if let Some(callback) = valid_region.handlers.get(&mouse_event.handler_key()) {
|
||||
event_cx.handled = true;
|
||||
event_cx.with_current_view(valid_region.id().view_id(), {
|
||||
let region_event = mouse_event.clone();
|
||||
|cx| callback(region_event, cx)
|
||||
});
|
||||
}
|
||||
|
||||
any_event_handled |= event_cx.handled;
|
||||
|
||||
// For bubbling events, if the event was handled, don't continue dispatching.
|
||||
// This only makes sense for local events which return false from is_capturable.
|
||||
if event_consumed && mouse_event.is_capturable() {
|
||||
any_event_handled = any_event_handled || event_cx.handled;
|
||||
// For bubbling events, if the event was handled, don't continue dispatching
|
||||
// This only makes sense for local events.
|
||||
if event_cx.handled && mouse_event.is_capturable() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ use std::{
|
||||
|
||||
use pathfinder_geometry::{rect::RectF, vector::Vector2F};
|
||||
|
||||
use crate::{scene::mouse_region::HandlerKey, MouseButtonEvent, MouseMovedEvent, ScrollWheelEvent};
|
||||
use crate::{MouseButton, MouseButtonEvent, MouseMovedEvent, ScrollWheelEvent};
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct MouseMove {
|
||||
@@ -217,17 +217,17 @@ impl MouseEvent {
|
||||
discriminant(&MouseEvent::ScrollWheel(Default::default()))
|
||||
}
|
||||
|
||||
pub fn handler_key(&self) -> HandlerKey {
|
||||
pub fn handler_key(&self) -> (Discriminant<MouseEvent>, Option<MouseButton>) {
|
||||
match self {
|
||||
MouseEvent::Move(_) => HandlerKey::new(Self::move_disc(), None),
|
||||
MouseEvent::Drag(e) => HandlerKey::new(Self::drag_disc(), e.pressed_button),
|
||||
MouseEvent::Hover(_) => HandlerKey::new(Self::hover_disc(), None),
|
||||
MouseEvent::Down(e) => HandlerKey::new(Self::down_disc(), Some(e.button)),
|
||||
MouseEvent::Up(e) => HandlerKey::new(Self::up_disc(), Some(e.button)),
|
||||
MouseEvent::Click(e) => HandlerKey::new(Self::click_disc(), Some(e.button)),
|
||||
MouseEvent::UpOut(e) => HandlerKey::new(Self::up_out_disc(), Some(e.button)),
|
||||
MouseEvent::DownOut(e) => HandlerKey::new(Self::down_out_disc(), Some(e.button)),
|
||||
MouseEvent::ScrollWheel(_) => HandlerKey::new(Self::scroll_wheel_disc(), None),
|
||||
MouseEvent::Move(_) => (Self::move_disc(), None),
|
||||
MouseEvent::Drag(e) => (Self::drag_disc(), e.pressed_button),
|
||||
MouseEvent::Hover(_) => (Self::hover_disc(), None),
|
||||
MouseEvent::Down(e) => (Self::down_disc(), Some(e.button)),
|
||||
MouseEvent::Up(e) => (Self::up_disc(), Some(e.button)),
|
||||
MouseEvent::Click(e) => (Self::click_disc(), Some(e.button)),
|
||||
MouseEvent::UpOut(e) => (Self::up_out_disc(), Some(e.button)),
|
||||
MouseEvent::DownOut(e) => (Self::down_out_disc(), Some(e.button)),
|
||||
MouseEvent::ScrollWheel(_) => (Self::scroll_wheel_disc(), None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ use std::{any::TypeId, fmt::Debug, mem::Discriminant, rc::Rc};
|
||||
use collections::HashMap;
|
||||
|
||||
use pathfinder_geometry::rect::RectF;
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use crate::{EventContext, MouseButton};
|
||||
|
||||
@@ -178,105 +177,61 @@ impl MouseRegionId {
|
||||
}
|
||||
}
|
||||
|
||||
pub type HandlerCallback = Rc<dyn Fn(MouseEvent, &mut EventContext)>;
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Hash)]
|
||||
pub struct HandlerKey {
|
||||
event_kind: Discriminant<MouseEvent>,
|
||||
button: Option<MouseButton>,
|
||||
}
|
||||
|
||||
impl HandlerKey {
|
||||
pub fn new(event_kind: Discriminant<MouseEvent>, button: Option<MouseButton>) -> HandlerKey {
|
||||
HandlerKey { event_kind, button }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct HandlerSet {
|
||||
set: HashMap<HandlerKey, SmallVec<[HandlerCallback; 1]>>,
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub set: HashMap<
|
||||
(Discriminant<MouseEvent>, Option<MouseButton>),
|
||||
Rc<dyn Fn(MouseEvent, &mut EventContext)>,
|
||||
>,
|
||||
}
|
||||
|
||||
impl HandlerSet {
|
||||
pub fn capture_all() -> Self {
|
||||
let mut set: HashMap<HandlerKey, SmallVec<[HandlerCallback; 1]>> = HashMap::default();
|
||||
#[allow(clippy::type_complexity)]
|
||||
let mut set: HashMap<
|
||||
(Discriminant<MouseEvent>, Option<MouseButton>),
|
||||
Rc<dyn Fn(MouseEvent, &mut EventContext)>,
|
||||
> = Default::default();
|
||||
|
||||
set.insert(
|
||||
HandlerKey::new(MouseEvent::move_disc(), None),
|
||||
SmallVec::from_buf([Rc::new(|_, _| {})]),
|
||||
);
|
||||
set.insert(
|
||||
HandlerKey::new(MouseEvent::hover_disc(), None),
|
||||
SmallVec::from_buf([Rc::new(|_, _| {})]),
|
||||
);
|
||||
set.insert((MouseEvent::move_disc(), None), Rc::new(|_, _| {}));
|
||||
set.insert((MouseEvent::hover_disc(), None), Rc::new(|_, _| {}));
|
||||
for button in MouseButton::all() {
|
||||
set.insert((MouseEvent::drag_disc(), Some(button)), Rc::new(|_, _| {}));
|
||||
set.insert((MouseEvent::down_disc(), Some(button)), Rc::new(|_, _| {}));
|
||||
set.insert((MouseEvent::up_disc(), Some(button)), Rc::new(|_, _| {}));
|
||||
set.insert((MouseEvent::click_disc(), Some(button)), Rc::new(|_, _| {}));
|
||||
set.insert(
|
||||
HandlerKey::new(MouseEvent::drag_disc(), Some(button)),
|
||||
SmallVec::from_buf([Rc::new(|_, _| {})]),
|
||||
(MouseEvent::down_out_disc(), Some(button)),
|
||||
Rc::new(|_, _| {}),
|
||||
);
|
||||
set.insert(
|
||||
HandlerKey::new(MouseEvent::down_disc(), Some(button)),
|
||||
SmallVec::from_buf([Rc::new(|_, _| {})]),
|
||||
);
|
||||
set.insert(
|
||||
HandlerKey::new(MouseEvent::up_disc(), Some(button)),
|
||||
SmallVec::from_buf([Rc::new(|_, _| {})]),
|
||||
);
|
||||
set.insert(
|
||||
HandlerKey::new(MouseEvent::click_disc(), Some(button)),
|
||||
SmallVec::from_buf([Rc::new(|_, _| {})]),
|
||||
);
|
||||
set.insert(
|
||||
HandlerKey::new(MouseEvent::down_out_disc(), Some(button)),
|
||||
SmallVec::from_buf([Rc::new(|_, _| {})]),
|
||||
);
|
||||
set.insert(
|
||||
HandlerKey::new(MouseEvent::up_out_disc(), Some(button)),
|
||||
SmallVec::from_buf([Rc::new(|_, _| {})]),
|
||||
(MouseEvent::up_out_disc(), Some(button)),
|
||||
Rc::new(|_, _| {}),
|
||||
);
|
||||
}
|
||||
set.insert(
|
||||
HandlerKey::new(MouseEvent::scroll_wheel_disc(), None),
|
||||
SmallVec::from_buf([Rc::new(|_, _| {})]),
|
||||
);
|
||||
set.insert((MouseEvent::scroll_wheel_disc(), None), Rc::new(|_, _| {}));
|
||||
|
||||
HandlerSet { set }
|
||||
}
|
||||
|
||||
pub fn get(&self, key: &HandlerKey) -> Option<&[HandlerCallback]> {
|
||||
self.set.get(key).map(|vec| vec.as_slice())
|
||||
pub fn get(
|
||||
&self,
|
||||
key: &(Discriminant<MouseEvent>, Option<MouseButton>),
|
||||
) -> Option<Rc<dyn Fn(MouseEvent, &mut EventContext)>> {
|
||||
self.set.get(key).cloned()
|
||||
}
|
||||
|
||||
pub fn contains(
|
||||
pub fn contains_handler(
|
||||
&self,
|
||||
discriminant: Discriminant<MouseEvent>,
|
||||
event: Discriminant<MouseEvent>,
|
||||
button: Option<MouseButton>,
|
||||
) -> bool {
|
||||
self.set
|
||||
.contains_key(&HandlerKey::new(discriminant, button))
|
||||
}
|
||||
|
||||
fn insert(
|
||||
&mut self,
|
||||
event_kind: Discriminant<MouseEvent>,
|
||||
button: Option<MouseButton>,
|
||||
callback: HandlerCallback,
|
||||
) {
|
||||
use std::collections::hash_map::Entry;
|
||||
|
||||
match self.set.entry(HandlerKey::new(event_kind, button)) {
|
||||
Entry::Occupied(mut vec) => {
|
||||
vec.get_mut().push(callback);
|
||||
}
|
||||
|
||||
Entry::Vacant(entry) => {
|
||||
entry.insert(SmallVec::from_buf([callback]));
|
||||
}
|
||||
}
|
||||
self.set.contains_key(&(event, button))
|
||||
}
|
||||
|
||||
pub fn on_move(mut self, handler: impl Fn(MouseMove, &mut EventContext) + 'static) -> Self {
|
||||
self.insert(MouseEvent::move_disc(), None,
|
||||
self.set.insert((MouseEvent::move_disc(), None),
|
||||
Rc::new(move |region_event, cx| {
|
||||
if let MouseEvent::Move(e) = region_event {
|
||||
handler(e, cx);
|
||||
@@ -294,7 +249,7 @@ impl HandlerSet {
|
||||
button: MouseButton,
|
||||
handler: impl Fn(MouseDown, &mut EventContext) + 'static,
|
||||
) -> Self {
|
||||
self.insert(MouseEvent::down_disc(), Some(button),
|
||||
self.set.insert((MouseEvent::down_disc(), Some(button)),
|
||||
Rc::new(move |region_event, cx| {
|
||||
if let MouseEvent::Down(e) = region_event {
|
||||
handler(e, cx);
|
||||
@@ -312,7 +267,7 @@ impl HandlerSet {
|
||||
button: MouseButton,
|
||||
handler: impl Fn(MouseUp, &mut EventContext) + 'static,
|
||||
) -> Self {
|
||||
self.insert(MouseEvent::up_disc(), Some(button),
|
||||
self.set.insert((MouseEvent::up_disc(), Some(button)),
|
||||
Rc::new(move |region_event, cx| {
|
||||
if let MouseEvent::Up(e) = region_event {
|
||||
handler(e, cx);
|
||||
@@ -330,7 +285,7 @@ impl HandlerSet {
|
||||
button: MouseButton,
|
||||
handler: impl Fn(MouseClick, &mut EventContext) + 'static,
|
||||
) -> Self {
|
||||
self.insert(MouseEvent::click_disc(), Some(button),
|
||||
self.set.insert((MouseEvent::click_disc(), Some(button)),
|
||||
Rc::new(move |region_event, cx| {
|
||||
if let MouseEvent::Click(e) = region_event {
|
||||
handler(e, cx);
|
||||
@@ -348,7 +303,7 @@ impl HandlerSet {
|
||||
button: MouseButton,
|
||||
handler: impl Fn(MouseDownOut, &mut EventContext) + 'static,
|
||||
) -> Self {
|
||||
self.insert(MouseEvent::down_out_disc(), Some(button),
|
||||
self.set.insert((MouseEvent::down_out_disc(), Some(button)),
|
||||
Rc::new(move |region_event, cx| {
|
||||
if let MouseEvent::DownOut(e) = region_event {
|
||||
handler(e, cx);
|
||||
@@ -366,7 +321,7 @@ impl HandlerSet {
|
||||
button: MouseButton,
|
||||
handler: impl Fn(MouseUpOut, &mut EventContext) + 'static,
|
||||
) -> Self {
|
||||
self.insert(MouseEvent::up_out_disc(), Some(button),
|
||||
self.set.insert((MouseEvent::up_out_disc(), Some(button)),
|
||||
Rc::new(move |region_event, cx| {
|
||||
if let MouseEvent::UpOut(e) = region_event {
|
||||
handler(e, cx);
|
||||
@@ -384,7 +339,7 @@ impl HandlerSet {
|
||||
button: MouseButton,
|
||||
handler: impl Fn(MouseDrag, &mut EventContext) + 'static,
|
||||
) -> Self {
|
||||
self.insert(MouseEvent::drag_disc(), Some(button),
|
||||
self.set.insert((MouseEvent::drag_disc(), Some(button)),
|
||||
Rc::new(move |region_event, cx| {
|
||||
if let MouseEvent::Drag(e) = region_event {
|
||||
handler(e, cx);
|
||||
@@ -398,7 +353,7 @@ impl HandlerSet {
|
||||
}
|
||||
|
||||
pub fn on_hover(mut self, handler: impl Fn(MouseHover, &mut EventContext) + 'static) -> Self {
|
||||
self.insert(MouseEvent::hover_disc(), None,
|
||||
self.set.insert((MouseEvent::hover_disc(), None),
|
||||
Rc::new(move |region_event, cx| {
|
||||
if let MouseEvent::Hover(e) = region_event {
|
||||
handler(e, cx);
|
||||
@@ -415,7 +370,7 @@ impl HandlerSet {
|
||||
mut self,
|
||||
handler: impl Fn(MouseScrollWheel, &mut EventContext) + 'static,
|
||||
) -> Self {
|
||||
self.insert(MouseEvent::scroll_wheel_disc(), None,
|
||||
self.set.insert((MouseEvent::scroll_wheel_disc(), None),
|
||||
Rc::new(move |region_event, cx| {
|
||||
if let MouseEvent::ScrollWheel(e) = region_event {
|
||||
handler(e, cx);
|
||||
|
||||
@@ -16,4 +16,4 @@ chrono = "0.4"
|
||||
dirs = "4.0"
|
||||
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
|
||||
settings = { path = "../settings" }
|
||||
shellexpand = "2.1.0"
|
||||
shellexpand = "2.1.0"
|
||||
@@ -61,7 +61,7 @@ pub fn new_journal_entry(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
|
||||
if let Some(editor) = item.downcast::<Editor>() {
|
||||
editor.update(&mut cx, |editor, cx| {
|
||||
let len = editor.buffer().read(cx).len(cx);
|
||||
editor.change_selections(Some(Autoscroll::center()), cx, |s| {
|
||||
editor.change_selections(Some(Autoscroll::Center), cx, |s| {
|
||||
s.select_ranges([len..len])
|
||||
});
|
||||
if len > 0 {
|
||||
|
||||
@@ -71,6 +71,4 @@ tree-sitter-json = "*"
|
||||
tree-sitter-rust = "*"
|
||||
tree-sitter-python = "*"
|
||||
tree-sitter-typescript = "*"
|
||||
tree-sitter-ruby = "*"
|
||||
tree-sitter-embedded-template = "*"
|
||||
unindent = "0.1.7"
|
||||
|
||||
@@ -1764,7 +1764,6 @@ impl BufferSnapshot {
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut indent_ranges = Vec::<Range<Point>>::new();
|
||||
let mut outdent_positions = Vec::<Point>::new();
|
||||
while let Some(mat) = matches.peek() {
|
||||
let mut start: Option<Point> = None;
|
||||
let mut end: Option<Point> = None;
|
||||
@@ -1778,8 +1777,6 @@ impl BufferSnapshot {
|
||||
start = Some(Point::from_ts_point(capture.node.end_position()));
|
||||
} else if Some(capture.index) == config.end_capture_ix {
|
||||
end = Some(Point::from_ts_point(capture.node.start_position()));
|
||||
} else if Some(capture.index) == config.outdent_capture_ix {
|
||||
outdent_positions.push(Point::from_ts_point(capture.node.start_position()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1800,19 +1797,6 @@ impl BufferSnapshot {
|
||||
}
|
||||
}
|
||||
|
||||
outdent_positions.sort();
|
||||
for outdent_position in outdent_positions {
|
||||
// find the innermost indent range containing this outdent_position
|
||||
// set its end to the outdent position
|
||||
if let Some(range_to_truncate) = indent_ranges
|
||||
.iter_mut()
|
||||
.filter(|indent_range| indent_range.contains(&outdent_position))
|
||||
.last()
|
||||
{
|
||||
range_to_truncate.end = outdent_position;
|
||||
}
|
||||
}
|
||||
|
||||
// Find the suggested indentation increases and decreased based on regexes.
|
||||
let mut indent_change_rows = Vec::<(u32, Ordering)>::new();
|
||||
self.for_each_line(
|
||||
@@ -2225,12 +2209,11 @@ impl BufferSnapshot {
|
||||
range: Range<T>,
|
||||
) -> Option<(Range<usize>, Range<usize>)> {
|
||||
// Find bracket pairs that *inclusively* contain the given range.
|
||||
let range = range.start.to_offset(self)..range.end.to_offset(self);
|
||||
let mut matches = self.syntax.matches(
|
||||
range.start.saturating_sub(1)..self.len().min(range.end + 1),
|
||||
&self.text,
|
||||
|grammar| grammar.brackets_config.as_ref().map(|c| &c.query),
|
||||
);
|
||||
let range = range.start.to_offset(self).saturating_sub(1)
|
||||
..self.len().min(range.end.to_offset(self) + 1);
|
||||
let mut matches = self.syntax.matches(range, &self.text, |grammar| {
|
||||
grammar.brackets_config.as_ref().map(|c| &c.query)
|
||||
});
|
||||
let configs = matches
|
||||
.grammars()
|
||||
.iter()
|
||||
@@ -2253,20 +2236,18 @@ impl BufferSnapshot {
|
||||
|
||||
matches.advance();
|
||||
|
||||
let Some((open, close)) = open.zip(close) else { continue };
|
||||
if open.start > range.start || close.end < range.end {
|
||||
continue;
|
||||
}
|
||||
let len = close.end - open.start;
|
||||
if let Some((open, close)) = open.zip(close) {
|
||||
let len = close.end - open.start;
|
||||
|
||||
if let Some((existing_open, existing_close)) = &result {
|
||||
let existing_len = existing_close.end - existing_open.start;
|
||||
if len > existing_len {
|
||||
continue;
|
||||
if let Some((existing_open, existing_close)) = &result {
|
||||
let existing_len = existing_close.end - existing_open.start;
|
||||
if len > existing_len {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result = Some((open, close));
|
||||
result = Some((open, close));
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
|
||||
@@ -573,72 +573,14 @@ fn test_enclosing_bracket_ranges(cx: &mut MutableAppContext) {
|
||||
))
|
||||
);
|
||||
|
||||
// Regression test: avoid crash when querying at the end of the buffer.
|
||||
assert_eq!(
|
||||
buffer.enclosing_bracket_point_ranges(Point::new(4, 1)..Point::new(4, 1)),
|
||||
buffer.enclosing_bracket_point_ranges(buffer.len() - 1..buffer.len()),
|
||||
Some((
|
||||
Point::new(0, 6)..Point::new(0, 7),
|
||||
Point::new(4, 0)..Point::new(4, 1)
|
||||
))
|
||||
);
|
||||
|
||||
// Regression test: avoid crash when querying at the end of the buffer.
|
||||
assert_eq!(
|
||||
buffer.enclosing_bracket_point_ranges(Point::new(4, 1)..Point::new(5, 0)),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_enclosing_bracket_ranges_where_brackets_are_not_outermost_children(
|
||||
cx: &mut MutableAppContext,
|
||||
) {
|
||||
let javascript_language = Arc::new(
|
||||
Language::new(
|
||||
LanguageConfig {
|
||||
name: "JavaScript".into(),
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_javascript::language()),
|
||||
)
|
||||
.with_brackets_query(
|
||||
r#"
|
||||
("{" @open "}" @close)
|
||||
("(" @open ")" @close)
|
||||
"#,
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
cx.set_global(Settings::test(cx));
|
||||
let buffer = cx.add_model(|cx| {
|
||||
let text = "
|
||||
for (const a in b) {
|
||||
// a comment that's longer than the for-loop header
|
||||
}
|
||||
"
|
||||
.unindent();
|
||||
Buffer::new(0, text, cx).with_language(javascript_language, cx)
|
||||
});
|
||||
|
||||
let buffer = buffer.read(cx);
|
||||
assert_eq!(
|
||||
buffer.enclosing_bracket_point_ranges(Point::new(0, 18)..Point::new(0, 18)),
|
||||
Some((
|
||||
Point::new(0, 4)..Point::new(0, 5),
|
||||
Point::new(0, 17)..Point::new(0, 18)
|
||||
))
|
||||
);
|
||||
|
||||
// Regression test: even though the parent node of the parentheses (the for loop) does
|
||||
// intersect the given range, the parentheses themselves do not contain the range, so
|
||||
// they should not be returned. Only the curly braces contain the range.
|
||||
assert_eq!(
|
||||
buffer.enclosing_bracket_point_ranges(Point::new(0, 20)..Point::new(0, 20)),
|
||||
Some((
|
||||
Point::new(0, 19)..Point::new(0, 20),
|
||||
Point::new(2, 0)..Point::new(2, 1)
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
@@ -1208,49 +1150,6 @@ fn test_autoindent_with_injected_languages(cx: &mut MutableAppContext) {
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_autoindent_query_with_outdent_captures(cx: &mut MutableAppContext) {
|
||||
let mut settings = Settings::test(cx);
|
||||
settings.editor_defaults.tab_size = Some(2.try_into().unwrap());
|
||||
cx.set_global(settings);
|
||||
cx.add_model(|cx| {
|
||||
let mut buffer = Buffer::new(0, "", cx).with_language(Arc::new(ruby_lang()), cx);
|
||||
|
||||
let text = r#"
|
||||
class C
|
||||
def a(b, c)
|
||||
puts b
|
||||
puts c
|
||||
rescue
|
||||
puts "errored"
|
||||
exit 1
|
||||
end
|
||||
end
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
buffer.edit([(0..0, text)], Some(AutoindentMode::EachLine), cx);
|
||||
|
||||
assert_eq!(
|
||||
buffer.text(),
|
||||
r#"
|
||||
class C
|
||||
def a(b, c)
|
||||
puts b
|
||||
puts c
|
||||
rescue
|
||||
puts "errored"
|
||||
exit 1
|
||||
end
|
||||
end
|
||||
"#
|
||||
.unindent()
|
||||
);
|
||||
|
||||
buffer
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_serialization(cx: &mut gpui::MutableAppContext) {
|
||||
let mut now = Instant::now();
|
||||
@@ -1395,7 +1294,6 @@ fn test_random_collaboration(cx: &mut MutableAppContext, mut rng: StdRng) {
|
||||
(0..entry_count).map(|_| {
|
||||
let range = buffer.random_byte_range(0, &mut rng);
|
||||
let range = range.to_point_utf16(buffer);
|
||||
let range = range.start..range.end;
|
||||
DiagnosticEntry {
|
||||
range,
|
||||
diagnostic: Diagnostic {
|
||||
@@ -1599,26 +1497,6 @@ impl Buffer {
|
||||
}
|
||||
}
|
||||
|
||||
fn ruby_lang() -> Language {
|
||||
Language::new(
|
||||
LanguageConfig {
|
||||
name: "Ruby".into(),
|
||||
path_suffixes: vec!["rb".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_ruby::language()),
|
||||
)
|
||||
.with_indents_query(
|
||||
r#"
|
||||
(class "end" @end) @indent
|
||||
(method "end" @end) @indent
|
||||
(rescue) @outdent
|
||||
(then) @indent
|
||||
"#,
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn rust_lang() -> Language {
|
||||
Language::new(
|
||||
LanguageConfig {
|
||||
|
||||
@@ -71,7 +71,7 @@ impl DiagnosticSet {
|
||||
diagnostics: SumTree::from_iter(
|
||||
entries.into_iter().map(|entry| DiagnosticEntry {
|
||||
range: buffer.anchor_before(entry.range.start)
|
||||
..buffer.anchor_before(entry.range.end),
|
||||
..buffer.anchor_after(entry.range.end),
|
||||
diagnostic: entry.diagnostic,
|
||||
}),
|
||||
buffer,
|
||||
|
||||
@@ -28,7 +28,6 @@ use std::{
|
||||
any::Any,
|
||||
cell::RefCell,
|
||||
fmt::Debug,
|
||||
hash::Hash,
|
||||
mem,
|
||||
ops::Range,
|
||||
path::{Path, PathBuf},
|
||||
@@ -135,10 +134,6 @@ impl CachedLspAdapter {
|
||||
self.adapter.process_diagnostics(params).await
|
||||
}
|
||||
|
||||
pub async fn process_completion(&self, completion_item: &mut lsp::CompletionItem) {
|
||||
self.adapter.process_completion(completion_item).await
|
||||
}
|
||||
|
||||
pub async fn label_for_completion(
|
||||
&self,
|
||||
completion_item: &lsp::CompletionItem,
|
||||
@@ -179,8 +174,6 @@ pub trait LspAdapter: 'static + Send + Sync {
|
||||
|
||||
async fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {}
|
||||
|
||||
async fn process_completion(&self, _: &mut lsp::CompletionItem) {}
|
||||
|
||||
async fn label_for_completion(
|
||||
&self,
|
||||
_: &lsp::CompletionItem,
|
||||
@@ -319,7 +312,6 @@ struct IndentConfig {
|
||||
indent_capture_ix: u32,
|
||||
start_capture_ix: Option<u32>,
|
||||
end_capture_ix: Option<u32>,
|
||||
outdent_capture_ix: Option<u32>,
|
||||
}
|
||||
|
||||
struct OutlineConfig {
|
||||
@@ -333,13 +325,7 @@ struct InjectionConfig {
|
||||
query: Query,
|
||||
content_capture_ix: u32,
|
||||
language_capture_ix: Option<u32>,
|
||||
patterns: Vec<InjectionPatternConfig>,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
struct InjectionPatternConfig {
|
||||
language: Option<Box<str>>,
|
||||
combined: bool,
|
||||
languages_by_pattern_ix: Vec<Option<Box<str>>>,
|
||||
}
|
||||
|
||||
struct BracketConfig {
|
||||
@@ -650,10 +636,6 @@ impl Language {
|
||||
self.adapter.clone()
|
||||
}
|
||||
|
||||
pub fn id(&self) -> Option<usize> {
|
||||
self.grammar.as_ref().map(|g| g.id)
|
||||
}
|
||||
|
||||
pub fn with_highlights_query(mut self, source: &str) -> Result<Self> {
|
||||
let grammar = self.grammar_mut();
|
||||
grammar.highlights_query = Some(Query::new(grammar.ts_language, source)?);
|
||||
@@ -688,14 +670,12 @@ impl Language {
|
||||
let mut indent_capture_ix = None;
|
||||
let mut start_capture_ix = None;
|
||||
let mut end_capture_ix = None;
|
||||
let mut outdent_capture_ix = None;
|
||||
get_capture_indices(
|
||||
&query,
|
||||
&mut [
|
||||
("indent", &mut indent_capture_ix),
|
||||
("start", &mut start_capture_ix),
|
||||
("end", &mut end_capture_ix),
|
||||
("outdent", &mut outdent_capture_ix),
|
||||
],
|
||||
);
|
||||
if let Some(indent_capture_ix) = indent_capture_ix {
|
||||
@@ -704,7 +684,6 @@ impl Language {
|
||||
indent_capture_ix,
|
||||
start_capture_ix,
|
||||
end_capture_ix,
|
||||
outdent_capture_ix,
|
||||
});
|
||||
}
|
||||
Ok(self)
|
||||
@@ -747,21 +726,15 @@ impl Language {
|
||||
("content", &mut content_capture_ix),
|
||||
],
|
||||
);
|
||||
let patterns = (0..query.pattern_count())
|
||||
let languages_by_pattern_ix = (0..query.pattern_count())
|
||||
.map(|ix| {
|
||||
let mut config = InjectionPatternConfig::default();
|
||||
for setting in query.property_settings(ix) {
|
||||
match setting.key.as_ref() {
|
||||
"language" => {
|
||||
config.language = setting.value.clone();
|
||||
}
|
||||
"combined" => {
|
||||
config.combined = true;
|
||||
}
|
||||
_ => {}
|
||||
query.property_settings(ix).iter().find_map(|setting| {
|
||||
if setting.key.as_ref() == "language" {
|
||||
return setting.value.clone();
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
config
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
if let Some(content_capture_ix) = content_capture_ix {
|
||||
@@ -769,7 +742,7 @@ impl Language {
|
||||
query,
|
||||
language_capture_ix,
|
||||
content_capture_ix,
|
||||
patterns,
|
||||
languages_by_pattern_ix,
|
||||
});
|
||||
}
|
||||
Ok(self)
|
||||
@@ -832,12 +805,6 @@ impl Language {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn process_completion(self: &Arc<Self>, completion: &mut lsp::CompletionItem) {
|
||||
if let Some(adapter) = self.adapter.as_ref() {
|
||||
adapter.process_completion(completion).await;
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn label_for_completion(
|
||||
self: &Arc<Self>,
|
||||
completion: &lsp::CompletionItem,
|
||||
@@ -912,20 +879,6 @@ impl Language {
|
||||
}
|
||||
}
|
||||
|
||||
impl Hash for Language {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.id().hash(state)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Language {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.id().eq(&other.id())
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for Language {}
|
||||
|
||||
impl Debug for Language {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("Language")
|
||||
@@ -1053,8 +1006,8 @@ pub fn point_to_lsp(point: PointUtf16) -> lsp::Position {
|
||||
lsp::Position::new(point.row, point.column)
|
||||
}
|
||||
|
||||
pub fn point_from_lsp(point: lsp::Position) -> Unclipped<PointUtf16> {
|
||||
Unclipped(PointUtf16::new(point.line, point.character))
|
||||
pub fn point_from_lsp(point: lsp::Position) -> PointUtf16 {
|
||||
PointUtf16::new(point.line, point.character)
|
||||
}
|
||||
|
||||
pub fn range_to_lsp(range: Range<PointUtf16>) -> lsp::Range {
|
||||
@@ -1064,7 +1017,7 @@ pub fn range_to_lsp(range: Range<PointUtf16>) -> lsp::Range {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn range_from_lsp(range: lsp::Range) -> Range<Unclipped<PointUtf16>> {
|
||||
pub fn range_from_lsp(range: lsp::Range) -> Range<PointUtf16> {
|
||||
let mut start = point_from_lsp(range.start);
|
||||
let mut end = point_from_lsp(range.end);
|
||||
if start > end {
|
||||
|
||||
@@ -426,11 +426,10 @@ pub async fn deserialize_completion(
|
||||
.and_then(deserialize_anchor)
|
||||
.ok_or_else(|| anyhow!("invalid old end"))?;
|
||||
let lsp_completion = serde_json::from_slice(&completion.lsp_completion)?;
|
||||
|
||||
let mut label = None;
|
||||
if let Some(language) = language {
|
||||
label = language.label_for_completion(&lsp_completion).await;
|
||||
}
|
||||
let label = match language {
|
||||
Some(l) => l.label_for_completion(&lsp_completion).await,
|
||||
None => None,
|
||||
};
|
||||
|
||||
Ok(Completion {
|
||||
old_range: old_start..old_end,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -67,9 +67,7 @@ impl OutlineView {
|
||||
) -> Self {
|
||||
let handle = cx.weak_handle();
|
||||
Self {
|
||||
picker: cx.add_view(|cx| {
|
||||
Picker::new("Search buffer symbols...", handle, cx).with_max_size(800., 1200.)
|
||||
}),
|
||||
picker: cx.add_view(|cx| Picker::new(handle, cx).with_max_size(800., 1200.)),
|
||||
last_query: Default::default(),
|
||||
matches: Default::default(),
|
||||
selected_match_index: 0,
|
||||
@@ -122,7 +120,7 @@ impl OutlineView {
|
||||
let display_rows = start.to_display_point(&snapshot).row()
|
||||
..end.to_display_point(&snapshot).row() + 1;
|
||||
active_editor.highlight_rows(Some(display_rows));
|
||||
active_editor.request_autoscroll(Autoscroll::center(), cx);
|
||||
active_editor.request_autoscroll(Autoscroll::Center, cx);
|
||||
});
|
||||
}
|
||||
cx.notify();
|
||||
@@ -219,7 +217,7 @@ impl PickerDelegate for OutlineView {
|
||||
if let Some(rows) = active_editor.highlighted_rows() {
|
||||
let snapshot = active_editor.snapshot(cx).display_snapshot;
|
||||
let position = DisplayPoint::new(rows.start, 0).to_point(&snapshot);
|
||||
active_editor.change_selections(Some(Autoscroll::center()), cx, |s| {
|
||||
active_editor.change_selections(Some(Autoscroll::Center), cx, |s| {
|
||||
s.select_ranges([position..position])
|
||||
});
|
||||
}
|
||||
|
||||
@@ -16,8 +16,6 @@ util = { path = "../util" }
|
||||
theme = { path = "../theme" }
|
||||
workspace = { path = "../workspace" }
|
||||
|
||||
parking_lot = "0.11.1"
|
||||
|
||||
[dev-dependencies]
|
||||
gpui = { path = "../gpui", features = ["test-support"] }
|
||||
serde_json = { version = "1.0", features = ["preserve_order"] }
|
||||
|
||||
@@ -1,22 +1,25 @@
|
||||
use editor::Editor;
|
||||
use gpui::{
|
||||
elements::*,
|
||||
elements::{
|
||||
ChildView, Flex, Label, MouseEventHandler, ParentElement, ScrollTarget, UniformList,
|
||||
UniformListState,
|
||||
},
|
||||
geometry::vector::{vec2f, Vector2F},
|
||||
keymap,
|
||||
platform::CursorStyle,
|
||||
AnyViewHandle, AppContext, Axis, Entity, MouseButton, MouseState, MutableAppContext,
|
||||
RenderContext, Task, View, ViewContext, ViewHandle, WeakViewHandle,
|
||||
AnyViewHandle, AppContext, Axis, Element, ElementBox, Entity, MouseButton, MouseState,
|
||||
MutableAppContext, RenderContext, Task, View, ViewContext, ViewHandle, WeakViewHandle,
|
||||
};
|
||||
use menu::{Cancel, Confirm, SelectFirst, SelectIndex, SelectLast, SelectNext, SelectPrev};
|
||||
use parking_lot::Mutex;
|
||||
use std::{cmp, sync::Arc};
|
||||
use settings::Settings;
|
||||
use std::cmp;
|
||||
|
||||
pub struct Picker<D: PickerDelegate> {
|
||||
delegate: WeakViewHandle<D>,
|
||||
query_editor: ViewHandle<Editor>,
|
||||
list_state: UniformListState,
|
||||
max_size: Vector2F,
|
||||
theme: Arc<Mutex<Box<dyn Fn(&theme::Theme) -> theme::Picker>>>,
|
||||
theme: Box<dyn FnMut(&AppContext) -> &theme::Picker>,
|
||||
confirmed: bool,
|
||||
}
|
||||
|
||||
@@ -49,8 +52,8 @@ impl<D: PickerDelegate> View for Picker<D> {
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut RenderContext<Self>) -> gpui::ElementBox {
|
||||
let theme = (self.theme.lock())(&cx.global::<settings::Settings>().theme);
|
||||
let query = self.query(cx);
|
||||
let theme = (self.theme)(cx);
|
||||
let container_style = theme.container;
|
||||
let delegate = self.delegate.clone();
|
||||
let match_count = if let Some(delegate) = delegate.upgrade(cx.app) {
|
||||
delegate.read(cx).match_count()
|
||||
@@ -58,36 +61,19 @@ impl<D: PickerDelegate> View for Picker<D> {
|
||||
0
|
||||
};
|
||||
|
||||
let container_style;
|
||||
let editor_style;
|
||||
if query.is_empty() && match_count == 0 {
|
||||
container_style = theme.empty_container;
|
||||
editor_style = theme.empty_input_editor.container;
|
||||
} else {
|
||||
container_style = theme.container;
|
||||
editor_style = theme.input_editor.container;
|
||||
};
|
||||
|
||||
Flex::new(Axis::Vertical)
|
||||
.with_child(
|
||||
ChildView::new(&self.query_editor, cx)
|
||||
.contained()
|
||||
.with_style(editor_style)
|
||||
.with_style(theme.input_editor.container)
|
||||
.boxed(),
|
||||
)
|
||||
.with_children(if match_count == 0 {
|
||||
if query.is_empty() {
|
||||
None
|
||||
.with_child(
|
||||
if match_count == 0 {
|
||||
Label::new("No matches".into(), theme.empty.label.clone())
|
||||
.contained()
|
||||
.with_style(theme.empty.container)
|
||||
} else {
|
||||
Some(
|
||||
Label::new("No matches".into(), theme.no_matches.label.clone())
|
||||
.contained()
|
||||
.with_style(theme.no_matches.container)
|
||||
.boxed(),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Some(
|
||||
UniformList::new(
|
||||
self.list_state.clone(),
|
||||
match_count,
|
||||
@@ -112,10 +98,10 @@ impl<D: PickerDelegate> View for Picker<D> {
|
||||
)
|
||||
.contained()
|
||||
.with_margin_top(6.0)
|
||||
.flex(1., false)
|
||||
.boxed(),
|
||||
)
|
||||
})
|
||||
}
|
||||
.flex(1., false)
|
||||
.boxed(),
|
||||
)
|
||||
.contained()
|
||||
.with_style(container_style)
|
||||
.constrained()
|
||||
@@ -148,26 +134,9 @@ impl<D: PickerDelegate> Picker<D> {
|
||||
cx.add_action(Self::cancel);
|
||||
}
|
||||
|
||||
pub fn new<P>(placeholder: P, delegate: WeakViewHandle<D>, cx: &mut ViewContext<Self>) -> Self
|
||||
where
|
||||
P: Into<Arc<str>>,
|
||||
{
|
||||
let theme = Arc::new(Mutex::new(
|
||||
Box::new(|theme: &theme::Theme| theme.picker.clone())
|
||||
as Box<dyn Fn(&theme::Theme) -> theme::Picker>,
|
||||
));
|
||||
let query_editor = cx.add_view({
|
||||
let picker_theme = theme.clone();
|
||||
|cx| {
|
||||
let mut editor = Editor::single_line(
|
||||
Some(Arc::new(move |theme| {
|
||||
(picker_theme.lock())(theme).input_editor.clone()
|
||||
})),
|
||||
cx,
|
||||
);
|
||||
editor.set_placeholder_text(placeholder, cx);
|
||||
editor
|
||||
}
|
||||
pub fn new(delegate: WeakViewHandle<D>, cx: &mut ViewContext<Self>) -> Self {
|
||||
let query_editor = cx.add_view(|cx| {
|
||||
Editor::single_line(Some(|theme| theme.picker.input_editor.clone()), cx)
|
||||
});
|
||||
cx.subscribe(&query_editor, Self::on_query_editor_event)
|
||||
.detach();
|
||||
@@ -176,7 +145,7 @@ impl<D: PickerDelegate> Picker<D> {
|
||||
list_state: Default::default(),
|
||||
delegate,
|
||||
max_size: vec2f(540., 420.),
|
||||
theme,
|
||||
theme: Box::new(|cx| &cx.global::<Settings>().theme.picker),
|
||||
confirmed: false,
|
||||
};
|
||||
cx.defer(|this, cx| {
|
||||
@@ -193,11 +162,11 @@ impl<D: PickerDelegate> Picker<D> {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_theme<F>(self, theme: F) -> Self
|
||||
pub fn with_theme<F>(mut self, theme: F) -> Self
|
||||
where
|
||||
F: 'static + Fn(&theme::Theme) -> theme::Picker,
|
||||
F: 'static + FnMut(&AppContext) -> &theme::Picker,
|
||||
{
|
||||
*self.theme.lock() = Box::new(theme);
|
||||
self.theme = Box::new(theme);
|
||||
self
|
||||
}
|
||||
|
||||
|
||||
@@ -128,8 +128,8 @@ impl LspCommand for PrepareRename {
|
||||
) = message
|
||||
{
|
||||
let Range { start, end } = range_from_lsp(range);
|
||||
if buffer.clip_point_utf16(start, Bias::Left) == start.0
|
||||
&& buffer.clip_point_utf16(end, Bias::Left) == end.0
|
||||
if buffer.clip_point_utf16(start, Bias::Left) == start
|
||||
&& buffer.clip_point_utf16(end, Bias::Left) == end
|
||||
{
|
||||
return Ok(Some(buffer.anchor_after(start)..buffer.anchor_before(end)));
|
||||
}
|
||||
|
||||
@@ -26,7 +26,6 @@ use language::{
|
||||
CodeLabel, Completion, Diagnostic, DiagnosticEntry, DiagnosticSet, Event as BufferEvent,
|
||||
File as _, Language, LanguageRegistry, LanguageServerName, LocalFile, OffsetRangeExt,
|
||||
Operation, Patch, PointUtf16, TextBufferSnapshot, ToOffset, ToPointUtf16, Transaction,
|
||||
Unclipped,
|
||||
};
|
||||
use lsp::{
|
||||
DiagnosticSeverity, DiagnosticTag, DocumentHighlightKind, LanguageServer, LanguageString,
|
||||
@@ -253,7 +252,7 @@ pub struct Symbol {
|
||||
pub label: CodeLabel,
|
||||
pub name: String,
|
||||
pub kind: lsp::SymbolKind,
|
||||
pub range: Range<Unclipped<PointUtf16>>,
|
||||
pub range: Range<PointUtf16>,
|
||||
pub signature: [u8; 32],
|
||||
}
|
||||
|
||||
@@ -2598,7 +2597,7 @@ impl Project {
|
||||
language_server_id: usize,
|
||||
abs_path: PathBuf,
|
||||
version: Option<i32>,
|
||||
diagnostics: Vec<DiagnosticEntry<Unclipped<PointUtf16>>>,
|
||||
diagnostics: Vec<DiagnosticEntry<PointUtf16>>,
|
||||
cx: &mut ModelContext<Project>,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
let (worktree, relative_path) = self
|
||||
@@ -2636,7 +2635,7 @@ impl Project {
|
||||
fn update_buffer_diagnostics(
|
||||
&mut self,
|
||||
buffer: &ModelHandle<Buffer>,
|
||||
mut diagnostics: Vec<DiagnosticEntry<Unclipped<PointUtf16>>>,
|
||||
mut diagnostics: Vec<DiagnosticEntry<PointUtf16>>,
|
||||
version: Option<i32>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Result<()> {
|
||||
@@ -2660,7 +2659,7 @@ impl Project {
|
||||
let mut sanitized_diagnostics = Vec::new();
|
||||
let edits_since_save = Patch::new(
|
||||
snapshot
|
||||
.edits_since::<Unclipped<PointUtf16>>(buffer.read(cx).saved_version())
|
||||
.edits_since::<PointUtf16>(buffer.read(cx).saved_version())
|
||||
.collect(),
|
||||
);
|
||||
for entry in diagnostics {
|
||||
@@ -2680,14 +2679,13 @@ impl Project {
|
||||
let mut range = snapshot.clip_point_utf16(start, Bias::Left)
|
||||
..snapshot.clip_point_utf16(end, Bias::Right);
|
||||
|
||||
// Expand empty ranges by one codepoint
|
||||
// Expand empty ranges by one character
|
||||
if range.start == range.end {
|
||||
// This will be go to the next boundary when being clipped
|
||||
range.end.column += 1;
|
||||
range.end = snapshot.clip_point_utf16(Unclipped(range.end), Bias::Right);
|
||||
range.end = snapshot.clip_point_utf16(range.end, Bias::Right);
|
||||
if range.start == range.end && range.end.column > 0 {
|
||||
range.start.column -= 1;
|
||||
range.end = snapshot.clip_point_utf16(Unclipped(range.end), Bias::Left);
|
||||
range.start = snapshot.clip_point_utf16(range.start, Bias::Left);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3290,7 +3288,7 @@ impl Project {
|
||||
return Task::ready(Ok(Default::default()));
|
||||
};
|
||||
|
||||
let position = Unclipped(position.to_point_utf16(source_buffer));
|
||||
let position = position.to_point_utf16(source_buffer);
|
||||
let anchor = source_buffer.anchor_after(position);
|
||||
|
||||
if worktree.read(cx).as_local().is_some() {
|
||||
@@ -3309,7 +3307,7 @@ impl Project {
|
||||
lsp::TextDocumentIdentifier::new(
|
||||
lsp::Url::from_file_path(buffer_abs_path).unwrap(),
|
||||
),
|
||||
point_to_lsp(position.0),
|
||||
point_to_lsp(position),
|
||||
),
|
||||
context: Default::default(),
|
||||
work_done_progress_params: Default::default(),
|
||||
@@ -3331,91 +3329,88 @@ impl Project {
|
||||
let snapshot = this.snapshot();
|
||||
let clipped_position = this.clip_point_utf16(position, Bias::Left);
|
||||
let mut range_for_token = None;
|
||||
completions
|
||||
.into_iter()
|
||||
.filter_map(move |mut lsp_completion| {
|
||||
// For now, we can only handle additional edits if they are returned
|
||||
// when resolving the completion, not if they are present initially.
|
||||
if lsp_completion
|
||||
.additional_text_edits
|
||||
.as_ref()
|
||||
.map_or(false, |edits| !edits.is_empty())
|
||||
{
|
||||
return None;
|
||||
}
|
||||
completions.into_iter().filter_map(move |lsp_completion| {
|
||||
// For now, we can only handle additional edits if they are returned
|
||||
// when resolving the completion, not if they are present initially.
|
||||
if lsp_completion
|
||||
.additional_text_edits
|
||||
.as_ref()
|
||||
.map_or(false, |edits| !edits.is_empty())
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
let (old_range, mut new_text) = match lsp_completion.text_edit.as_ref()
|
||||
{
|
||||
// If the language server provides a range to overwrite, then
|
||||
// check that the range is valid.
|
||||
Some(lsp::CompletionTextEdit::Edit(edit)) => {
|
||||
let range = range_from_lsp(edit.range);
|
||||
let start = snapshot.clip_point_utf16(range.start, Bias::Left);
|
||||
let end = snapshot.clip_point_utf16(range.end, Bias::Left);
|
||||
if start != range.start.0 || end != range.end.0 {
|
||||
log::info!("completion out of expected range");
|
||||
return None;
|
||||
}
|
||||
(
|
||||
snapshot.anchor_before(start)..snapshot.anchor_after(end),
|
||||
edit.new_text.clone(),
|
||||
)
|
||||
}
|
||||
// If the language server does not provide a range, then infer
|
||||
// the range based on the syntax tree.
|
||||
None => {
|
||||
if position.0 != clipped_position {
|
||||
log::info!("completion out of expected range");
|
||||
return None;
|
||||
}
|
||||
let Range { start, end } = range_for_token
|
||||
.get_or_insert_with(|| {
|
||||
let offset = position.to_offset(&snapshot);
|
||||
let (range, kind) = snapshot.surrounding_word(offset);
|
||||
if kind == Some(CharKind::Word) {
|
||||
range
|
||||
} else {
|
||||
offset..offset
|
||||
}
|
||||
})
|
||||
.clone();
|
||||
let text = lsp_completion
|
||||
.insert_text
|
||||
.as_ref()
|
||||
.unwrap_or(&lsp_completion.label)
|
||||
.clone();
|
||||
(
|
||||
snapshot.anchor_before(start)..snapshot.anchor_after(end),
|
||||
text,
|
||||
)
|
||||
}
|
||||
Some(lsp::CompletionTextEdit::InsertAndReplace(_)) => {
|
||||
log::info!("unsupported insert/replace completion");
|
||||
let (old_range, mut new_text) = match lsp_completion.text_edit.as_ref() {
|
||||
// If the language server provides a range to overwrite, then
|
||||
// check that the range is valid.
|
||||
Some(lsp::CompletionTextEdit::Edit(edit)) => {
|
||||
let range = range_from_lsp(edit.range);
|
||||
let start = snapshot.clip_point_utf16(range.start, Bias::Left);
|
||||
let end = snapshot.clip_point_utf16(range.end, Bias::Left);
|
||||
if start != range.start || end != range.end {
|
||||
log::info!("completion out of expected range");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
(
|
||||
snapshot.anchor_before(start)..snapshot.anchor_after(end),
|
||||
edit.new_text.clone(),
|
||||
)
|
||||
}
|
||||
// If the language server does not provide a range, then infer
|
||||
// the range based on the syntax tree.
|
||||
None => {
|
||||
if position != clipped_position {
|
||||
log::info!("completion out of expected range");
|
||||
return None;
|
||||
}
|
||||
let Range { start, end } = range_for_token
|
||||
.get_or_insert_with(|| {
|
||||
let offset = position.to_offset(&snapshot);
|
||||
let (range, kind) = snapshot.surrounding_word(offset);
|
||||
if kind == Some(CharKind::Word) {
|
||||
range
|
||||
} else {
|
||||
offset..offset
|
||||
}
|
||||
})
|
||||
.clone();
|
||||
let text = lsp_completion
|
||||
.insert_text
|
||||
.as_ref()
|
||||
.unwrap_or(&lsp_completion.label)
|
||||
.clone();
|
||||
(
|
||||
snapshot.anchor_before(start)..snapshot.anchor_after(end),
|
||||
text,
|
||||
)
|
||||
}
|
||||
Some(lsp::CompletionTextEdit::InsertAndReplace(_)) => {
|
||||
log::info!("unsupported insert/replace completion");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
LineEnding::normalize(&mut new_text);
|
||||
let language = language.clone();
|
||||
Some(async move {
|
||||
let mut label = None;
|
||||
if let Some(language) = language {
|
||||
language.process_completion(&mut lsp_completion).await;
|
||||
label = language.label_for_completion(&lsp_completion).await;
|
||||
}
|
||||
Completion {
|
||||
old_range,
|
||||
new_text,
|
||||
label: label.unwrap_or_else(|| {
|
||||
CodeLabel::plain(
|
||||
lsp_completion.label.clone(),
|
||||
lsp_completion.filter_text.as_deref(),
|
||||
)
|
||||
}),
|
||||
lsp_completion,
|
||||
}
|
||||
})
|
||||
LineEnding::normalize(&mut new_text);
|
||||
let language = language.clone();
|
||||
Some(async move {
|
||||
let label = if let Some(language) = language {
|
||||
language.label_for_completion(&lsp_completion).await
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Completion {
|
||||
old_range,
|
||||
new_text,
|
||||
label: label.unwrap_or_else(|| {
|
||||
CodeLabel::plain(
|
||||
lsp_completion.label.clone(),
|
||||
lsp_completion.filter_text.as_deref(),
|
||||
)
|
||||
}),
|
||||
lsp_completion,
|
||||
}
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
Ok(futures::future::join_all(completions).await)
|
||||
@@ -3458,41 +3453,29 @@ impl Project {
|
||||
let buffer_id = buffer.remote_id();
|
||||
|
||||
if self.is_local() {
|
||||
let lang_server = match self.language_server_for_buffer(buffer, cx) {
|
||||
Some((_, server)) => server.clone(),
|
||||
_ => return Task::ready(Ok(Default::default())),
|
||||
let lang_server = if let Some((_, server)) = self.language_server_for_buffer(buffer, cx)
|
||||
{
|
||||
server.clone()
|
||||
} else {
|
||||
return Task::ready(Ok(Default::default()));
|
||||
};
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let resolved_completion = lang_server
|
||||
.request::<lsp::request::ResolveCompletionItem>(completion.lsp_completion)
|
||||
.await?;
|
||||
|
||||
if let Some(edits) = resolved_completion.additional_text_edits {
|
||||
let edits = this
|
||||
.update(&mut cx, |this, cx| {
|
||||
this.edits_from_lsp(&buffer_handle, edits, None, cx)
|
||||
})
|
||||
.await?;
|
||||
|
||||
buffer_handle.update(&mut cx, |buffer, cx| {
|
||||
buffer.finalize_last_transaction();
|
||||
buffer.start_transaction();
|
||||
|
||||
for (range, text) in edits {
|
||||
let primary = &completion.old_range;
|
||||
let start_within = primary.start.cmp(&range.start, buffer).is_le()
|
||||
&& primary.end.cmp(&range.start, buffer).is_ge();
|
||||
let end_within = range.start.cmp(&primary.end, buffer).is_le()
|
||||
&& range.end.cmp(&primary.end, buffer).is_ge();
|
||||
|
||||
//Skip addtional edits which overlap with the primary completion edit
|
||||
//https://github.com/zed-industries/zed/pull/1871
|
||||
if !start_within && !end_within {
|
||||
buffer.edit([(range, text)], None, cx);
|
||||
}
|
||||
buffer.edit([(range, text)], None, cx);
|
||||
}
|
||||
|
||||
let transaction = if buffer.end_transaction(cx).is_some() {
|
||||
let transaction = buffer.finalize_last_transaction().unwrap().clone();
|
||||
if !push_to_history {
|
||||
@@ -3591,7 +3574,6 @@ impl Project {
|
||||
context: lsp::CodeActionContext {
|
||||
diagnostics: relevant_diagnostics,
|
||||
only: Some(vec![
|
||||
lsp::CodeActionKind::EMPTY,
|
||||
lsp::CodeActionKind::QUICKFIX,
|
||||
lsp::CodeActionKind::REFACTOR,
|
||||
lsp::CodeActionKind::REFACTOR_EXTRACT,
|
||||
@@ -5119,30 +5101,22 @@ impl Project {
|
||||
_: Arc<Client>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<proto::GetCompletionsResponse> {
|
||||
let position = envelope
|
||||
.payload
|
||||
.position
|
||||
.and_then(language::proto::deserialize_anchor)
|
||||
.ok_or_else(|| anyhow!("invalid position"))?;
|
||||
let version = deserialize_version(envelope.payload.version);
|
||||
let buffer = this.read_with(&cx, |this, cx| {
|
||||
this.opened_buffers
|
||||
.get(&envelope.payload.buffer_id)
|
||||
.and_then(|buffer| buffer.upgrade(cx))
|
||||
.ok_or_else(|| anyhow!("unknown buffer id {}", envelope.payload.buffer_id))
|
||||
})?;
|
||||
|
||||
let position = envelope
|
||||
.payload
|
||||
.position
|
||||
.and_then(language::proto::deserialize_anchor)
|
||||
.map(|p| {
|
||||
buffer.read_with(&cx, |buffer, _| {
|
||||
buffer.clip_point_utf16(Unclipped(p.to_point_utf16(buffer)), Bias::Left)
|
||||
})
|
||||
})
|
||||
.ok_or_else(|| anyhow!("invalid position"))?;
|
||||
|
||||
let version = deserialize_version(envelope.payload.version);
|
||||
buffer
|
||||
.update(&mut cx, |buffer, _| buffer.wait_for_version(version))
|
||||
.await;
|
||||
let version = buffer.read_with(&cx, |buffer, _| buffer.version());
|
||||
|
||||
let completions = this
|
||||
.update(&mut cx, |this, cx| this.completions(&buffer, position, cx))
|
||||
.await?;
|
||||
@@ -5629,8 +5603,8 @@ impl Project {
|
||||
},
|
||||
|
||||
name: serialized_symbol.name,
|
||||
range: Unclipped(PointUtf16::new(start.row, start.column))
|
||||
..Unclipped(PointUtf16::new(end.row, end.column)),
|
||||
range: PointUtf16::new(start.row, start.column)
|
||||
..PointUtf16::new(end.row, end.column),
|
||||
kind,
|
||||
signature: serialized_symbol
|
||||
.signature
|
||||
@@ -5716,10 +5690,10 @@ impl Project {
|
||||
|
||||
let mut lsp_edits = lsp_edits.into_iter().peekable();
|
||||
let mut edits = Vec::new();
|
||||
while let Some((range, mut new_text)) = lsp_edits.next() {
|
||||
while let Some((mut range, mut new_text)) = lsp_edits.next() {
|
||||
// Clip invalid ranges provided by the language server.
|
||||
let mut range = snapshot.clip_point_utf16(range.start, Bias::Left)
|
||||
..snapshot.clip_point_utf16(range.end, Bias::Left);
|
||||
range.start = snapshot.clip_point_utf16(range.start, Bias::Left);
|
||||
range.end = snapshot.clip_point_utf16(range.end, Bias::Left);
|
||||
|
||||
// Combine any LSP edits that are adjacent.
|
||||
//
|
||||
@@ -5731,11 +5705,11 @@ impl Project {
|
||||
// In order for the diffing logic below to work properly, any edits that
|
||||
// cancel each other out must be combined into one.
|
||||
while let Some((next_range, next_text)) = lsp_edits.peek() {
|
||||
if next_range.start.0 > range.end {
|
||||
if next_range.start.0.row > range.end.row + 1
|
||||
|| next_range.start.0.column > 0
|
||||
if next_range.start > range.end {
|
||||
if next_range.start.row > range.end.row + 1
|
||||
|| next_range.start.column > 0
|
||||
|| snapshot.clip_point_utf16(
|
||||
Unclipped(PointUtf16::new(range.end.row, u32::MAX)),
|
||||
PointUtf16::new(range.end.row, u32::MAX),
|
||||
Bias::Left,
|
||||
) > range.end
|
||||
{
|
||||
@@ -5743,7 +5717,7 @@ impl Project {
|
||||
}
|
||||
new_text.push('\n');
|
||||
}
|
||||
range.end = snapshot.clip_point_utf16(next_range.end, Bias::Left);
|
||||
range.end = next_range.end;
|
||||
new_text.push_str(next_text);
|
||||
lsp_edits.next();
|
||||
}
|
||||
@@ -6064,13 +6038,13 @@ fn serialize_symbol(symbol: &Symbol) -> proto::Symbol {
|
||||
path: symbol.path.path.to_string_lossy().to_string(),
|
||||
name: symbol.name.clone(),
|
||||
kind: unsafe { mem::transmute(symbol.kind) },
|
||||
start: Some(proto::PointUtf16 {
|
||||
row: symbol.range.start.0.row,
|
||||
column: symbol.range.start.0.column,
|
||||
start: Some(proto::Point {
|
||||
row: symbol.range.start.row,
|
||||
column: symbol.range.start.column,
|
||||
}),
|
||||
end: Some(proto::PointUtf16 {
|
||||
row: symbol.range.end.0.row,
|
||||
column: symbol.range.end.0.column,
|
||||
end: Some(proto::Point {
|
||||
row: symbol.range.end.row,
|
||||
column: symbol.range.end.column,
|
||||
}),
|
||||
signature: symbol.signature.to_vec(),
|
||||
}
|
||||
|
||||
@@ -1239,7 +1239,7 @@ async fn test_empty_diagnostic_ranges(cx: &mut gpui::TestAppContext) {
|
||||
&buffer,
|
||||
vec![
|
||||
DiagnosticEntry {
|
||||
range: Unclipped(PointUtf16::new(0, 10))..Unclipped(PointUtf16::new(0, 10)),
|
||||
range: PointUtf16::new(0, 10)..PointUtf16::new(0, 10),
|
||||
diagnostic: Diagnostic {
|
||||
severity: DiagnosticSeverity::ERROR,
|
||||
message: "syntax error 1".to_string(),
|
||||
@@ -1247,7 +1247,7 @@ async fn test_empty_diagnostic_ranges(cx: &mut gpui::TestAppContext) {
|
||||
},
|
||||
},
|
||||
DiagnosticEntry {
|
||||
range: Unclipped(PointUtf16::new(1, 10))..Unclipped(PointUtf16::new(1, 10)),
|
||||
range: PointUtf16::new(1, 10)..PointUtf16::new(1, 10),
|
||||
diagnostic: Diagnostic {
|
||||
severity: DiagnosticSeverity::ERROR,
|
||||
message: "syntax error 2".to_string(),
|
||||
|
||||
@@ -20,7 +20,6 @@ use gpui::{
|
||||
executor, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext,
|
||||
Task,
|
||||
};
|
||||
use language::Unclipped;
|
||||
use language::{
|
||||
proto::{deserialize_version, serialize_line_ending, serialize_version},
|
||||
Buffer, DiagnosticEntry, PointUtf16, Rope,
|
||||
@@ -66,7 +65,7 @@ pub struct LocalWorktree {
|
||||
_background_scanner_task: Option<Task<()>>,
|
||||
poll_task: Option<Task<()>>,
|
||||
share: Option<ShareState>,
|
||||
diagnostics: HashMap<Arc<Path>, Vec<DiagnosticEntry<Unclipped<PointUtf16>>>>,
|
||||
diagnostics: HashMap<Arc<Path>, Vec<DiagnosticEntry<PointUtf16>>>,
|
||||
diagnostic_summaries: TreeMap<PathKey, DiagnosticSummary>,
|
||||
client: Arc<Client>,
|
||||
fs: Arc<dyn Fs>,
|
||||
@@ -500,10 +499,7 @@ impl LocalWorktree {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn diagnostics_for_path(
|
||||
&self,
|
||||
path: &Path,
|
||||
) -> Option<Vec<DiagnosticEntry<Unclipped<PointUtf16>>>> {
|
||||
pub fn diagnostics_for_path(&self, path: &Path) -> Option<Vec<DiagnosticEntry<PointUtf16>>> {
|
||||
self.diagnostics.get(path).cloned()
|
||||
}
|
||||
|
||||
@@ -511,7 +507,7 @@ impl LocalWorktree {
|
||||
&mut self,
|
||||
language_server_id: usize,
|
||||
worktree_path: Arc<Path>,
|
||||
diagnostics: Vec<DiagnosticEntry<Unclipped<PointUtf16>>>,
|
||||
diagnostics: Vec<DiagnosticEntry<PointUtf16>>,
|
||||
_: &mut ModelContext<Worktree>,
|
||||
) -> Result<bool> {
|
||||
self.diagnostics.remove(&worktree_path);
|
||||
@@ -1183,10 +1179,6 @@ impl Snapshot {
|
||||
self.id
|
||||
}
|
||||
|
||||
pub fn abs_path(&self) -> &Arc<Path> {
|
||||
&self.abs_path
|
||||
}
|
||||
|
||||
pub fn contains_entry(&self, entry_id: ProjectEntryId) -> bool {
|
||||
self.entries_by_id.get(&entry_id, &()).is_some()
|
||||
}
|
||||
@@ -1378,6 +1370,10 @@ impl Snapshot {
|
||||
}
|
||||
|
||||
impl LocalSnapshot {
|
||||
pub fn abs_path(&self) -> &Arc<Path> {
|
||||
&self.abs_path
|
||||
}
|
||||
|
||||
pub fn extension_counts(&self) -> &HashMap<OsString, usize> {
|
||||
&self.extension_counts
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ doctest = false
|
||||
|
||||
[dependencies]
|
||||
context_menu = { path = "../context_menu" }
|
||||
drag_and_drop = { path = "../drag_and_drop" }
|
||||
editor = { path = "../editor" }
|
||||
gpui = { path = "../gpui" }
|
||||
menu = { path = "../menu" }
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
use context_menu::{ContextMenu, ContextMenuItem};
|
||||
use drag_and_drop::{DragAndDrop, Draggable};
|
||||
use editor::{Cancel, Editor};
|
||||
use futures::stream::StreamExt;
|
||||
use gpui::{
|
||||
actions,
|
||||
anyhow::{anyhow, Result},
|
||||
elements::{
|
||||
AnchorCorner, ChildView, ConstrainedBox, ContainerStyle, Empty, Flex, Label,
|
||||
MouseEventHandler, ParentElement, ScrollTarget, Stack, Svg, UniformList, UniformListState,
|
||||
AnchorCorner, ChildView, ConstrainedBox, Empty, Flex, Label, MouseEventHandler,
|
||||
ParentElement, ScrollTarget, Stack, Svg, UniformList, UniformListState,
|
||||
},
|
||||
geometry::vector::Vector2F,
|
||||
impl_internal_actions, keymap,
|
||||
@@ -24,9 +23,7 @@ use std::{
|
||||
ffi::OsStr,
|
||||
ops::Range,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
use theme::ProjectPanelEntry;
|
||||
use unicase::UniCase;
|
||||
use workspace::Workspace;
|
||||
|
||||
@@ -43,7 +40,6 @@ pub struct ProjectPanel {
|
||||
filename_editor: ViewHandle<Editor>,
|
||||
clipboard_entry: Option<ClipboardEntry>,
|
||||
context_menu: ViewHandle<ContextMenu>,
|
||||
dragged_entry_destination: Option<Arc<Path>>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
@@ -74,9 +70,8 @@ pub enum ClipboardEntry {
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct EntryDetails {
|
||||
struct EntryDetails {
|
||||
filename: String,
|
||||
path: Arc<Path>,
|
||||
depth: usize,
|
||||
kind: EntryKind,
|
||||
is_ignored: bool,
|
||||
@@ -96,13 +91,6 @@ pub struct Open {
|
||||
pub change_focus: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct MoveProjectEntry {
|
||||
pub entry_to_move: ProjectEntryId,
|
||||
pub destination: ProjectEntryId,
|
||||
pub destination_is_file: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct DeployContextMenu {
|
||||
pub position: Vector2F,
|
||||
@@ -125,10 +113,7 @@ actions!(
|
||||
ToggleFocus
|
||||
]
|
||||
);
|
||||
impl_internal_actions!(
|
||||
project_panel,
|
||||
[Open, ToggleExpanded, DeployContextMenu, MoveProjectEntry]
|
||||
);
|
||||
impl_internal_actions!(project_panel, [Open, ToggleExpanded, DeployContextMenu]);
|
||||
|
||||
pub fn init(cx: &mut MutableAppContext) {
|
||||
cx.add_action(ProjectPanel::deploy_context_menu);
|
||||
@@ -152,7 +137,6 @@ pub fn init(cx: &mut MutableAppContext) {
|
||||
this.paste(action, cx);
|
||||
},
|
||||
);
|
||||
cx.add_action(ProjectPanel::move_entry);
|
||||
}
|
||||
|
||||
pub enum Event {
|
||||
@@ -191,11 +175,11 @@ impl ProjectPanel {
|
||||
|
||||
let filename_editor = cx.add_view(|cx| {
|
||||
Editor::single_line(
|
||||
Some(Arc::new(|theme| {
|
||||
Some(|theme| {
|
||||
let mut style = theme.project_panel.filename_editor.clone();
|
||||
style.container.background_color.take();
|
||||
style
|
||||
})),
|
||||
}),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
@@ -231,12 +215,10 @@ impl ProjectPanel {
|
||||
filename_editor,
|
||||
clipboard_entry: None,
|
||||
context_menu: cx.add_view(ContextMenu::new),
|
||||
dragged_entry_destination: None,
|
||||
};
|
||||
this.update_visible_entries(None, cx);
|
||||
this
|
||||
});
|
||||
|
||||
cx.subscribe(&project_panel, {
|
||||
let project_panel = project_panel.downgrade();
|
||||
move |workspace, _, event, cx| match event {
|
||||
@@ -252,7 +234,6 @@ impl ProjectPanel {
|
||||
worktree_id: worktree.read(cx).id(),
|
||||
path: entry.path.clone(),
|
||||
},
|
||||
None,
|
||||
focus_opened_item,
|
||||
cx,
|
||||
)
|
||||
@@ -619,10 +600,6 @@ impl ProjectPanel {
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
cx.update_global(|drag_and_drop: &mut DragAndDrop<Workspace>, cx| {
|
||||
drag_and_drop.cancel_dragging::<ProjectEntryId>(cx);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -787,39 +764,6 @@ impl ProjectPanel {
|
||||
}
|
||||
}
|
||||
|
||||
fn move_entry(
|
||||
&mut self,
|
||||
&MoveProjectEntry {
|
||||
entry_to_move,
|
||||
destination,
|
||||
destination_is_file,
|
||||
}: &MoveProjectEntry,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let destination_worktree = self.project.update(cx, |project, cx| {
|
||||
let entry_path = project.path_for_entry(entry_to_move, cx)?;
|
||||
let destination_entry_path = project.path_for_entry(destination, cx)?.path.clone();
|
||||
|
||||
let mut destination_path = destination_entry_path.as_ref();
|
||||
if destination_is_file {
|
||||
destination_path = destination_path.parent()?;
|
||||
}
|
||||
|
||||
let mut new_path = destination_path.to_path_buf();
|
||||
new_path.push(entry_path.path.file_name()?);
|
||||
if new_path != entry_path.path.as_ref() {
|
||||
let task = project.rename_entry(entry_to_move, new_path, cx)?;
|
||||
cx.foreground().spawn(task).detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
Some(project.worktree_id_for_entry(destination, cx)?)
|
||||
});
|
||||
|
||||
if let Some(destination_worktree) = destination_worktree {
|
||||
self.expand_entry(destination_worktree, destination, cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn index_for_selection(&self, selection: Selection) -> Option<(usize, usize, usize)> {
|
||||
let mut entry_index = 0;
|
||||
let mut visible_entries_index = 0;
|
||||
@@ -1005,15 +949,14 @@ impl ProjectPanel {
|
||||
let end_ix = range.end.min(ix + visible_worktree_entries.len());
|
||||
if let Some(worktree) = self.project.read(cx).worktree_for_id(*worktree_id, cx) {
|
||||
let snapshot = worktree.read(cx).snapshot();
|
||||
let root_name = OsStr::new(snapshot.root_name());
|
||||
let expanded_entry_ids = self
|
||||
.expanded_dir_ids
|
||||
.get(&snapshot.id())
|
||||
.map(Vec::as_slice)
|
||||
.unwrap_or(&[]);
|
||||
|
||||
let entry_range = range.start.saturating_sub(ix)..end_ix - ix;
|
||||
for entry in &visible_worktree_entries[entry_range] {
|
||||
let root_name = OsStr::new(snapshot.root_name());
|
||||
for entry in &visible_worktree_entries[range.start.saturating_sub(ix)..end_ix - ix]
|
||||
{
|
||||
let mut details = EntryDetails {
|
||||
filename: entry
|
||||
.path
|
||||
@@ -1021,7 +964,6 @@ impl ProjectPanel {
|
||||
.unwrap_or(root_name)
|
||||
.to_string_lossy()
|
||||
.to_string(),
|
||||
path: entry.path.clone(),
|
||||
depth: entry.path.components().count(),
|
||||
kind: entry.kind,
|
||||
is_ignored: entry.is_ignored,
|
||||
@@ -1035,14 +977,12 @@ impl ProjectPanel {
|
||||
.clipboard_entry
|
||||
.map_or(false, |e| e.is_cut() && e.entry_id() == entry.id),
|
||||
};
|
||||
|
||||
if let Some(edit_state) = &self.edit_state {
|
||||
let is_edited_entry = if edit_state.is_new_entry {
|
||||
entry.id == NEW_ENTRY_ID
|
||||
} else {
|
||||
entry.id == edit_state.entry_id
|
||||
};
|
||||
|
||||
if is_edited_entry {
|
||||
if let Some(processing_filename) = &edit_state.processing_filename {
|
||||
details.is_processing = true;
|
||||
@@ -1064,115 +1004,77 @@ impl ProjectPanel {
|
||||
}
|
||||
}
|
||||
|
||||
fn render_entry_visual_element<V: View>(
|
||||
details: &EntryDetails,
|
||||
editor: Option<&ViewHandle<Editor>>,
|
||||
padding: f32,
|
||||
row_container_style: ContainerStyle,
|
||||
style: &ProjectPanelEntry,
|
||||
cx: &mut RenderContext<V>,
|
||||
) -> ElementBox {
|
||||
let kind = details.kind;
|
||||
let show_editor = details.is_editing && !details.is_processing;
|
||||
|
||||
Flex::row()
|
||||
.with_child(
|
||||
ConstrainedBox::new(if kind == EntryKind::Dir {
|
||||
if details.is_expanded {
|
||||
Svg::new("icons/chevron_down_8.svg")
|
||||
.with_color(style.icon_color)
|
||||
.boxed()
|
||||
} else {
|
||||
Svg::new("icons/chevron_right_8.svg")
|
||||
.with_color(style.icon_color)
|
||||
.boxed()
|
||||
}
|
||||
} else {
|
||||
Empty::new().boxed()
|
||||
})
|
||||
.with_max_width(style.icon_size)
|
||||
.with_max_height(style.icon_size)
|
||||
.aligned()
|
||||
.constrained()
|
||||
.with_width(style.icon_size)
|
||||
.boxed(),
|
||||
)
|
||||
.with_child(if show_editor && editor.is_some() {
|
||||
ChildView::new(editor.unwrap().clone(), cx)
|
||||
.contained()
|
||||
.with_margin_left(style.icon_spacing)
|
||||
.aligned()
|
||||
.left()
|
||||
.flex(1.0, true)
|
||||
.boxed()
|
||||
} else {
|
||||
Label::new(details.filename.clone(), style.text.clone())
|
||||
.contained()
|
||||
.with_margin_left(style.icon_spacing)
|
||||
.aligned()
|
||||
.left()
|
||||
.boxed()
|
||||
})
|
||||
.constrained()
|
||||
.with_height(style.height)
|
||||
.contained()
|
||||
.with_style(row_container_style)
|
||||
.with_padding_left(padding)
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn render_entry(
|
||||
entry_id: ProjectEntryId,
|
||||
details: EntryDetails,
|
||||
editor: &ViewHandle<Editor>,
|
||||
dragged_entry_destination: &mut Option<Arc<Path>>,
|
||||
theme: &theme::ProjectPanel,
|
||||
cx: &mut RenderContext<Self>,
|
||||
) -> ElementBox {
|
||||
let this = cx.handle();
|
||||
let kind = details.kind;
|
||||
let path = details.path.clone();
|
||||
let padding = theme.container.padding.left + details.depth as f32 * theme.indent_width;
|
||||
|
||||
let entry_style = if details.is_cut {
|
||||
&theme.cut_entry
|
||||
} else if details.is_ignored {
|
||||
&theme.ignored_entry
|
||||
} else {
|
||||
&theme.entry
|
||||
};
|
||||
|
||||
let show_editor = details.is_editing && !details.is_processing;
|
||||
|
||||
MouseEventHandler::<Self>::new(entry_id.to_usize(), cx, |state, cx| {
|
||||
let mut style = entry_style.style_for(state, details.is_selected).clone();
|
||||
let padding = theme.container.padding.left + details.depth as f32 * theme.indent_width;
|
||||
|
||||
if cx
|
||||
.global::<DragAndDrop<Workspace>>()
|
||||
.currently_dragged::<ProjectEntryId>(cx.window_id())
|
||||
.is_some()
|
||||
&& dragged_entry_destination
|
||||
.as_ref()
|
||||
.filter(|destination| details.path.starts_with(destination))
|
||||
.is_some()
|
||||
{
|
||||
style = entry_style.active.clone().unwrap();
|
||||
}
|
||||
let entry_style = if details.is_cut {
|
||||
&theme.cut_entry
|
||||
} else if details.is_ignored {
|
||||
&theme.ignored_entry
|
||||
} else {
|
||||
&theme.entry
|
||||
};
|
||||
|
||||
let style = entry_style.style_for(state, details.is_selected).clone();
|
||||
|
||||
let row_container_style = if show_editor {
|
||||
theme.filename_editor.container
|
||||
} else {
|
||||
style.container
|
||||
};
|
||||
|
||||
Self::render_entry_visual_element(
|
||||
&details,
|
||||
Some(editor),
|
||||
padding,
|
||||
row_container_style,
|
||||
&style,
|
||||
cx,
|
||||
)
|
||||
Flex::row()
|
||||
.with_child(
|
||||
ConstrainedBox::new(if kind == EntryKind::Dir {
|
||||
if details.is_expanded {
|
||||
Svg::new("icons/chevron_down_8.svg")
|
||||
.with_color(style.icon_color)
|
||||
.boxed()
|
||||
} else {
|
||||
Svg::new("icons/chevron_right_8.svg")
|
||||
.with_color(style.icon_color)
|
||||
.boxed()
|
||||
}
|
||||
} else {
|
||||
Empty::new().boxed()
|
||||
})
|
||||
.with_max_width(style.icon_size)
|
||||
.with_max_height(style.icon_size)
|
||||
.aligned()
|
||||
.constrained()
|
||||
.with_width(style.icon_size)
|
||||
.boxed(),
|
||||
)
|
||||
.with_child(if show_editor {
|
||||
ChildView::new(editor.clone(), cx)
|
||||
.contained()
|
||||
.with_margin_left(theme.entry.default.icon_spacing)
|
||||
.aligned()
|
||||
.left()
|
||||
.flex(1.0, true)
|
||||
.boxed()
|
||||
} else {
|
||||
Label::new(details.filename, style.text.clone())
|
||||
.contained()
|
||||
.with_margin_left(style.icon_spacing)
|
||||
.aligned()
|
||||
.left()
|
||||
.boxed()
|
||||
})
|
||||
.constrained()
|
||||
.with_height(theme.entry.default.height)
|
||||
.contained()
|
||||
.with_style(row_container_style)
|
||||
.with_padding_left(padding)
|
||||
.boxed()
|
||||
})
|
||||
.on_click(MouseButton::Left, move |e, cx| {
|
||||
if kind == EntryKind::Dir {
|
||||
@@ -1190,50 +1092,6 @@ impl ProjectPanel {
|
||||
position: e.position,
|
||||
})
|
||||
})
|
||||
.on_up(MouseButton::Left, move |_, cx| {
|
||||
if let Some((_, dragged_entry)) = cx
|
||||
.global::<DragAndDrop<Workspace>>()
|
||||
.currently_dragged::<ProjectEntryId>(cx.window_id())
|
||||
{
|
||||
cx.dispatch_action(MoveProjectEntry {
|
||||
entry_to_move: *dragged_entry,
|
||||
destination: entry_id,
|
||||
destination_is_file: matches!(details.kind, EntryKind::File(_)),
|
||||
});
|
||||
}
|
||||
})
|
||||
.on_move(move |_, cx| {
|
||||
if cx
|
||||
.global::<DragAndDrop<Workspace>>()
|
||||
.currently_dragged::<ProjectEntryId>(cx.window_id())
|
||||
.is_some()
|
||||
{
|
||||
if let Some(this) = this.upgrade(cx.app) {
|
||||
this.update(cx.app, |this, _| {
|
||||
this.dragged_entry_destination = if matches!(kind, EntryKind::File(_)) {
|
||||
path.parent().map(|parent| Arc::from(parent))
|
||||
} else {
|
||||
Some(path.clone())
|
||||
};
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
.as_draggable(entry_id, {
|
||||
let row_container_style = theme.dragged_entry.container;
|
||||
|
||||
move |_, cx: &mut RenderContext<Workspace>| {
|
||||
let theme = cx.global::<Settings>().theme.clone();
|
||||
Self::render_entry_visual_element(
|
||||
&details,
|
||||
None,
|
||||
padding,
|
||||
row_container_style,
|
||||
&theme.project_panel.dragged_entry,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.boxed()
|
||||
}
|
||||
@@ -1245,15 +1103,14 @@ impl View for ProjectPanel {
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> gpui::ElementBox {
|
||||
enum ProjectPanel {}
|
||||
enum Tag {}
|
||||
let theme = &cx.global::<Settings>().theme.project_panel;
|
||||
let mut container_style = theme.container;
|
||||
let padding = std::mem::take(&mut container_style.padding);
|
||||
let last_worktree_root_id = self.last_worktree_root_id;
|
||||
|
||||
Stack::new()
|
||||
.with_child(
|
||||
MouseEventHandler::<ProjectPanel>::new(0, cx, |_, cx| {
|
||||
MouseEventHandler::<Tag>::new(0, cx, |_, cx| {
|
||||
UniformList::new(
|
||||
self.list.clone(),
|
||||
self.visible_entries
|
||||
@@ -1263,19 +1120,15 @@ impl View for ProjectPanel {
|
||||
cx,
|
||||
move |this, range, items, cx| {
|
||||
let theme = cx.global::<Settings>().theme.clone();
|
||||
let mut dragged_entry_destination =
|
||||
this.dragged_entry_destination.clone();
|
||||
this.for_each_visible_entry(range, cx, |id, details, cx| {
|
||||
items.push(Self::render_entry(
|
||||
id,
|
||||
details,
|
||||
&this.filename_editor,
|
||||
&mut dragged_entry_destination,
|
||||
&theme.project_panel,
|
||||
cx,
|
||||
));
|
||||
});
|
||||
this.dragged_entry_destination = dragged_entry_destination;
|
||||
},
|
||||
)
|
||||
.with_padding_top(padding.top)
|
||||
|
||||
@@ -28,4 +28,4 @@ settings = { path = "../settings", features = ["test-support"] }
|
||||
gpui = { path = "../gpui", features = ["test-support"] }
|
||||
language = { path = "../language", features = ["test-support"] }
|
||||
lsp = { path = "../lsp", features = ["test-support"] }
|
||||
project = { path = "../project", features = ["test-support"] }
|
||||
project = { path = "../project", features = ["test-support"] }
|
||||
@@ -63,7 +63,7 @@ impl ProjectSymbolsView {
|
||||
let handle = cx.weak_handle();
|
||||
Self {
|
||||
project,
|
||||
picker: cx.add_view(|cx| Picker::new("Search project symbols...", handle, cx)),
|
||||
picker: cx.add_view(|cx| Picker::new(handle, cx)),
|
||||
selected_match_index: 0,
|
||||
symbols: Default::default(),
|
||||
visible_match_candidates: Default::default(),
|
||||
@@ -150,7 +150,7 @@ impl ProjectSymbolsView {
|
||||
|
||||
let editor = workspace.open_project_item::<Editor>(buffer, cx);
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.change_selections(Some(Autoscroll::center()), cx, |s| {
|
||||
editor.change_selections(Some(Autoscroll::Center), cx, |s| {
|
||||
s.select_ranges([position..position])
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,7 +12,7 @@ smallvec = { version = "1.6", features = ["union"] }
|
||||
sum_tree = { path = "../sum_tree" }
|
||||
arrayvec = "0.7.1"
|
||||
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
|
||||
util = { path = "../util" }
|
||||
|
||||
|
||||
[dev-dependencies]
|
||||
rand = "0.8.3"
|
||||
|
||||
@@ -1,23 +1,16 @@
|
||||
mod offset_utf16;
|
||||
mod point;
|
||||
mod point_utf16;
|
||||
mod unclipped;
|
||||
|
||||
use arrayvec::ArrayString;
|
||||
use bromberg_sl2::{DigestString, HashMatrix};
|
||||
use smallvec::SmallVec;
|
||||
use std::{
|
||||
cmp, fmt, io, mem,
|
||||
ops::{AddAssign, Range},
|
||||
str,
|
||||
};
|
||||
use std::{cmp, fmt, io, mem, ops::Range, str};
|
||||
use sum_tree::{Bias, Dimension, SumTree};
|
||||
use util::debug_panic;
|
||||
|
||||
pub use offset_utf16::OffsetUtf16;
|
||||
pub use point::Point;
|
||||
pub use point_utf16::PointUtf16;
|
||||
pub use unclipped::Unclipped;
|
||||
|
||||
#[cfg(test)]
|
||||
const CHUNK_BASE: usize = 6;
|
||||
@@ -267,14 +260,6 @@ impl Rope {
|
||||
}
|
||||
|
||||
pub fn point_utf16_to_offset(&self, point: PointUtf16) -> usize {
|
||||
self.point_utf16_to_offset_impl(point, false)
|
||||
}
|
||||
|
||||
pub fn unclipped_point_utf16_to_offset(&self, point: Unclipped<PointUtf16>) -> usize {
|
||||
self.point_utf16_to_offset_impl(point.0, true)
|
||||
}
|
||||
|
||||
fn point_utf16_to_offset_impl(&self, point: PointUtf16, clip: bool) -> usize {
|
||||
if point >= self.summary().lines_utf16() {
|
||||
return self.summary().len;
|
||||
}
|
||||
@@ -284,20 +269,20 @@ impl Rope {
|
||||
cursor.start().1
|
||||
+ cursor
|
||||
.item()
|
||||
.map_or(0, |chunk| chunk.point_utf16_to_offset(overshoot, clip))
|
||||
.map_or(0, |chunk| chunk.point_utf16_to_offset(overshoot))
|
||||
}
|
||||
|
||||
pub fn unclipped_point_utf16_to_point(&self, point: Unclipped<PointUtf16>) -> Point {
|
||||
if point.0 >= self.summary().lines_utf16() {
|
||||
pub fn point_utf16_to_point(&self, point: PointUtf16) -> Point {
|
||||
if point >= self.summary().lines_utf16() {
|
||||
return self.summary().lines;
|
||||
}
|
||||
let mut cursor = self.chunks.cursor::<(PointUtf16, Point)>();
|
||||
cursor.seek(&point.0, Bias::Left, &());
|
||||
let overshoot = Unclipped(point.0 - cursor.start().0);
|
||||
cursor.seek(&point, Bias::Left, &());
|
||||
let overshoot = point - cursor.start().0;
|
||||
cursor.start().1
|
||||
+ cursor.item().map_or(Point::zero(), |chunk| {
|
||||
chunk.unclipped_point_utf16_to_point(overshoot)
|
||||
})
|
||||
+ cursor
|
||||
.item()
|
||||
.map_or(Point::zero(), |chunk| chunk.point_utf16_to_point(overshoot))
|
||||
}
|
||||
|
||||
pub fn clip_offset(&self, mut offset: usize, bias: Bias) -> usize {
|
||||
@@ -345,11 +330,11 @@ impl Rope {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clip_point_utf16(&self, point: Unclipped<PointUtf16>, bias: Bias) -> PointUtf16 {
|
||||
pub fn clip_point_utf16(&self, point: PointUtf16, bias: Bias) -> PointUtf16 {
|
||||
let mut cursor = self.chunks.cursor::<PointUtf16>();
|
||||
cursor.seek(&point.0, Bias::Right, &());
|
||||
cursor.seek(&point, Bias::Right, &());
|
||||
if let Some(chunk) = cursor.item() {
|
||||
let overshoot = Unclipped(point.0 - cursor.start());
|
||||
let overshoot = point - cursor.start();
|
||||
*cursor.start() + chunk.clip_point_utf16(overshoot, bias)
|
||||
} else {
|
||||
self.summary().lines_utf16()
|
||||
@@ -680,33 +665,28 @@ impl Chunk {
|
||||
fn point_to_offset(&self, target: Point) -> usize {
|
||||
let mut offset = 0;
|
||||
let mut point = Point::new(0, 0);
|
||||
|
||||
for ch in self.0.chars() {
|
||||
if point >= target {
|
||||
if point > target {
|
||||
debug_panic!("point {target:?} is inside of character {ch:?}");
|
||||
panic!("point {:?} is inside of character {:?}", target, ch);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if ch == '\n' {
|
||||
point.row += 1;
|
||||
point.column = 0;
|
||||
|
||||
if point.row > target.row {
|
||||
debug_panic!(
|
||||
"point {target:?} is beyond the end of a line with length {}",
|
||||
point.column
|
||||
panic!(
|
||||
"point {:?} is beyond the end of a line with length {}",
|
||||
target, point.column
|
||||
);
|
||||
break;
|
||||
}
|
||||
point.column = 0;
|
||||
} else {
|
||||
point.column += ch.len_utf8() as u32;
|
||||
}
|
||||
|
||||
offset += ch.len_utf8();
|
||||
}
|
||||
|
||||
offset
|
||||
}
|
||||
|
||||
@@ -731,62 +711,45 @@ impl Chunk {
|
||||
point_utf16
|
||||
}
|
||||
|
||||
fn point_utf16_to_offset(&self, target: PointUtf16, clip: bool) -> usize {
|
||||
fn point_utf16_to_offset(&self, target: PointUtf16) -> usize {
|
||||
let mut offset = 0;
|
||||
let mut point = PointUtf16::new(0, 0);
|
||||
|
||||
for ch in self.0.chars() {
|
||||
if point == target {
|
||||
if point >= target {
|
||||
if point > target {
|
||||
panic!("point {:?} is inside of character {:?}", target, ch);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if ch == '\n' {
|
||||
point.row += 1;
|
||||
point.column = 0;
|
||||
|
||||
if point.row > target.row {
|
||||
if !clip {
|
||||
debug_panic!(
|
||||
"point {target:?} is beyond the end of a line with length {}",
|
||||
point.column
|
||||
);
|
||||
}
|
||||
// Return the offset of the newline
|
||||
return offset;
|
||||
panic!(
|
||||
"point {:?} is beyond the end of a line with length {}",
|
||||
target, point.column
|
||||
);
|
||||
}
|
||||
point.column = 0;
|
||||
} else {
|
||||
point.column += ch.len_utf16() as u32;
|
||||
}
|
||||
|
||||
if point > target {
|
||||
if !clip {
|
||||
debug_panic!("point {target:?} is inside of codepoint {ch:?}");
|
||||
}
|
||||
// Return the offset of the codepoint which we have landed within, bias left
|
||||
return offset;
|
||||
}
|
||||
|
||||
offset += ch.len_utf8();
|
||||
}
|
||||
|
||||
offset
|
||||
}
|
||||
|
||||
fn unclipped_point_utf16_to_point(&self, target: Unclipped<PointUtf16>) -> Point {
|
||||
fn point_utf16_to_point(&self, target: PointUtf16) -> Point {
|
||||
let mut point = Point::zero();
|
||||
let mut point_utf16 = PointUtf16::zero();
|
||||
|
||||
for ch in self.0.chars() {
|
||||
if point_utf16 == target.0 {
|
||||
if point_utf16 >= target {
|
||||
if point_utf16 > target {
|
||||
panic!("point {:?} is inside of character {:?}", target, ch);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if point_utf16 > target.0 {
|
||||
// If the point is past the end of a line or inside of a code point,
|
||||
// return the last valid point before the target.
|
||||
return point;
|
||||
}
|
||||
|
||||
if ch == '\n' {
|
||||
point_utf16 += PointUtf16::new(1, 0);
|
||||
point += Point::new(1, 0);
|
||||
@@ -795,7 +758,6 @@ impl Chunk {
|
||||
point += Point::new(0, ch.len_utf8() as u32);
|
||||
}
|
||||
}
|
||||
|
||||
point
|
||||
}
|
||||
|
||||
@@ -815,11 +777,11 @@ impl Chunk {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
fn clip_point_utf16(&self, target: Unclipped<PointUtf16>, bias: Bias) -> PointUtf16 {
|
||||
fn clip_point_utf16(&self, target: PointUtf16, bias: Bias) -> PointUtf16 {
|
||||
for (row, line) in self.0.split('\n').enumerate() {
|
||||
if row == target.0.row as usize {
|
||||
if row == target.row as usize {
|
||||
let mut code_units = line.encode_utf16();
|
||||
let mut column = code_units.by_ref().take(target.0.column as usize).count();
|
||||
let mut column = code_units.by_ref().take(target.column as usize).count();
|
||||
if char::decode_utf16(code_units).next().transpose().is_err() {
|
||||
match bias {
|
||||
Bias::Left => column -= 1,
|
||||
@@ -955,7 +917,7 @@ impl std::ops::Add<Self> for TextSummary {
|
||||
type Output = Self;
|
||||
|
||||
fn add(mut self, rhs: Self) -> Self::Output {
|
||||
AddAssign::add_assign(&mut self, &rhs);
|
||||
self.add_assign(&rhs);
|
||||
self
|
||||
}
|
||||
}
|
||||
@@ -1152,15 +1114,15 @@ mod tests {
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
rope.clip_point_utf16(Unclipped(PointUtf16::new(0, 1)), Bias::Left),
|
||||
rope.clip_point_utf16(PointUtf16::new(0, 1), Bias::Left),
|
||||
PointUtf16::new(0, 0)
|
||||
);
|
||||
assert_eq!(
|
||||
rope.clip_point_utf16(Unclipped(PointUtf16::new(0, 1)), Bias::Right),
|
||||
rope.clip_point_utf16(PointUtf16::new(0, 1), Bias::Right),
|
||||
PointUtf16::new(0, 2)
|
||||
);
|
||||
assert_eq!(
|
||||
rope.clip_point_utf16(Unclipped(PointUtf16::new(0, 3)), Bias::Right),
|
||||
rope.clip_point_utf16(PointUtf16::new(0, 3), Bias::Right),
|
||||
PointUtf16::new(0, 2)
|
||||
);
|
||||
|
||||
@@ -1276,7 +1238,7 @@ mod tests {
|
||||
}
|
||||
|
||||
let mut offset_utf16 = OffsetUtf16(0);
|
||||
let mut point_utf16 = Unclipped(PointUtf16::zero());
|
||||
let mut point_utf16 = PointUtf16::zero();
|
||||
for unit in expected.encode_utf16() {
|
||||
let left_offset = actual.clip_offset_utf16(offset_utf16, Bias::Left);
|
||||
let right_offset = actual.clip_offset_utf16(offset_utf16, Bias::Right);
|
||||
@@ -1288,15 +1250,15 @@ mod tests {
|
||||
let left_point = actual.clip_point_utf16(point_utf16, Bias::Left);
|
||||
let right_point = actual.clip_point_utf16(point_utf16, Bias::Right);
|
||||
assert!(right_point >= left_point);
|
||||
// Ensure translating valid UTF-16 points to offsets doesn't panic.
|
||||
// Ensure translating UTF-16 points to offsets doesn't panic.
|
||||
actual.point_utf16_to_offset(left_point);
|
||||
actual.point_utf16_to_offset(right_point);
|
||||
|
||||
offset_utf16.0 += 1;
|
||||
if unit == b'\n' as u16 {
|
||||
point_utf16.0 += PointUtf16::new(1, 0);
|
||||
point_utf16 += PointUtf16::new(1, 0);
|
||||
} else {
|
||||
point_utf16.0 += PointUtf16::new(0, 1);
|
||||
point_utf16 += PointUtf16::new(0, 1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
use crate::{ChunkSummary, TextDimension, TextSummary};
|
||||
use std::ops::{Add, AddAssign, Sub, SubAssign};
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct Unclipped<T>(pub T);
|
||||
|
||||
impl<T> From<T> for Unclipped<T> {
|
||||
fn from(value: T) -> Self {
|
||||
Unclipped(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T: sum_tree::Dimension<'a, ChunkSummary>> sum_tree::Dimension<'a, ChunkSummary>
|
||||
for Unclipped<T>
|
||||
{
|
||||
fn add_summary(&mut self, summary: &'a ChunkSummary, _: &()) {
|
||||
self.0.add_summary(summary, &());
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: TextDimension> TextDimension for Unclipped<T> {
|
||||
fn from_text_summary(summary: &TextSummary) -> Self {
|
||||
Unclipped(T::from_text_summary(summary))
|
||||
}
|
||||
|
||||
fn add_assign(&mut self, other: &Self) {
|
||||
TextDimension::add_assign(&mut self.0, &other.0);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Add<T, Output = T>> Add<Unclipped<T>> for Unclipped<T> {
|
||||
type Output = Unclipped<T>;
|
||||
|
||||
fn add(self, rhs: Unclipped<T>) -> Self::Output {
|
||||
Unclipped(self.0 + rhs.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Sub<T, Output = T>> Sub<Unclipped<T>> for Unclipped<T> {
|
||||
type Output = Unclipped<T>;
|
||||
|
||||
fn sub(self, rhs: Unclipped<T>) -> Self::Output {
|
||||
Unclipped(self.0 - rhs.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: AddAssign<T>> AddAssign<Unclipped<T>> for Unclipped<T> {
|
||||
fn add_assign(&mut self, rhs: Unclipped<T>) {
|
||||
self.0 += rhs.0;
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: SubAssign<T>> SubAssign<Unclipped<T>> for Unclipped<T> {
|
||||
fn sub_assign(&mut self, rhs: Unclipped<T>) {
|
||||
self.0 -= rhs.0;
|
||||
}
|
||||
}
|
||||
@@ -412,10 +412,8 @@ message Symbol {
|
||||
string name = 4;
|
||||
int32 kind = 5;
|
||||
string path = 6;
|
||||
// Cannot use generate anchors for unopend files,
|
||||
// so we are forced to use point coords instead
|
||||
PointUtf16 start = 7;
|
||||
PointUtf16 end = 8;
|
||||
Point start = 7;
|
||||
Point end = 8;
|
||||
bytes signature = 9;
|
||||
}
|
||||
|
||||
@@ -1044,7 +1042,7 @@ message Range {
|
||||
uint64 end = 2;
|
||||
}
|
||||
|
||||
message PointUtf16 {
|
||||
message Point {
|
||||
uint32 row = 1;
|
||||
uint32 column = 2;
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ use gpui::{
|
||||
use project::search::SearchQuery;
|
||||
use serde::Deserialize;
|
||||
use settings::Settings;
|
||||
use std::{any::Any, sync::Arc};
|
||||
use std::any::Any;
|
||||
use workspace::{
|
||||
searchable::{Direction, SearchEvent, SearchableItemHandle, WeakSearchableItemHandle},
|
||||
ItemHandle, Pane, ToolbarItemLocation, ToolbarItemView,
|
||||
@@ -232,11 +232,7 @@ impl ToolbarItemView for BufferSearchBar {
|
||||
impl BufferSearchBar {
|
||||
pub fn new(cx: &mut ViewContext<Self>) -> Self {
|
||||
let query_editor = cx.add_view(|cx| {
|
||||
Editor::auto_height(
|
||||
2,
|
||||
Some(Arc::new(|theme| theme.search.editor.input.clone())),
|
||||
cx,
|
||||
)
|
||||
Editor::auto_height(2, Some(|theme| theme.search.editor.input.clone()), cx)
|
||||
});
|
||||
cx.subscribe(&query_editor, Self::on_query_editor_event)
|
||||
.detach();
|
||||
|
||||
@@ -20,7 +20,6 @@ use std::{
|
||||
any::{Any, TypeId},
|
||||
ops::Range,
|
||||
path::PathBuf,
|
||||
sync::Arc,
|
||||
};
|
||||
use util::ResultExt as _;
|
||||
use workspace::{
|
||||
@@ -379,10 +378,8 @@ impl ProjectSearchView {
|
||||
.detach();
|
||||
|
||||
let query_editor = cx.add_view(|cx| {
|
||||
let mut editor = Editor::single_line(
|
||||
Some(Arc::new(|theme| theme.search.editor.input.clone())),
|
||||
cx,
|
||||
);
|
||||
let mut editor =
|
||||
Editor::single_line(Some(|theme| theme.search.editor.input.clone()), cx);
|
||||
editor.set_text(query_text, cx);
|
||||
editor
|
||||
});
|
||||
@@ -512,7 +509,7 @@ impl ProjectSearchView {
|
||||
let range_to_select = match_ranges[new_index].clone();
|
||||
self.results_editor.update(cx, |editor, cx| {
|
||||
editor.unfold_ranges([range_to_select.clone()], false, cx);
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
s.select_ranges([range_to_select])
|
||||
});
|
||||
});
|
||||
@@ -546,7 +543,7 @@ impl ProjectSearchView {
|
||||
} else {
|
||||
self.results_editor.update(cx, |editor, cx| {
|
||||
if reset_selections {
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
s.select_ranges(match_ranges.first().cloned())
|
||||
});
|
||||
}
|
||||
|
||||
@@ -28,7 +28,6 @@ pub struct Settings {
|
||||
pub buffer_font_family: FamilyId,
|
||||
pub default_buffer_font_size: f32,
|
||||
pub buffer_font_size: f32,
|
||||
pub active_pane_magnification: f32,
|
||||
pub cursor_blink: bool,
|
||||
pub hover_popover_enabled: bool,
|
||||
pub show_completions_on_input: bool,
|
||||
@@ -254,8 +253,6 @@ pub struct SettingsFileContent {
|
||||
#[serde(default)]
|
||||
pub buffer_font_size: Option<f32>,
|
||||
#[serde(default)]
|
||||
pub active_pane_magnification: Option<f32>,
|
||||
#[serde(default)]
|
||||
pub cursor_blink: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub hover_popover_enabled: Option<bool>,
|
||||
@@ -315,7 +312,6 @@ impl Settings {
|
||||
.load_family(&[defaults.buffer_font_family.as_ref().unwrap()])
|
||||
.unwrap(),
|
||||
buffer_font_size: defaults.buffer_font_size.unwrap(),
|
||||
active_pane_magnification: defaults.active_pane_magnification.unwrap(),
|
||||
default_buffer_font_size: defaults.buffer_font_size.unwrap(),
|
||||
cursor_blink: defaults.cursor_blink.unwrap(),
|
||||
hover_popover_enabled: defaults.hover_popover_enabled.unwrap(),
|
||||
@@ -371,10 +367,6 @@ impl Settings {
|
||||
data.projects_online_by_default,
|
||||
);
|
||||
merge(&mut self.buffer_font_size, data.buffer_font_size);
|
||||
merge(
|
||||
&mut self.active_pane_magnification,
|
||||
data.active_pane_magnification,
|
||||
);
|
||||
merge(&mut self.default_buffer_font_size, data.buffer_font_size);
|
||||
merge(&mut self.cursor_blink, data.cursor_blink);
|
||||
merge(&mut self.hover_popover_enabled, data.hover_popover_enabled);
|
||||
@@ -466,7 +458,6 @@ impl Settings {
|
||||
experiments: FeatureFlags::default(),
|
||||
buffer_font_family: cx.font_cache().load_family(&["Monaco"]).unwrap(),
|
||||
buffer_font_size: 14.,
|
||||
active_pane_magnification: 1.,
|
||||
default_buffer_font_size: 14.,
|
||||
cursor_blink: true,
|
||||
hover_popover_enabled: true,
|
||||
|
||||
@@ -36,6 +36,18 @@ impl Modifiers {
|
||||
}
|
||||
}
|
||||
|
||||
///This function checks if to_esc_str would work, assuming all terminal settings are off.
|
||||
///Note that this function is conservative. It can fail in cases where the actual to_esc_str succeeds.
|
||||
///This is unavoidable for our use case. GPUI cannot wait until we acquire the terminal
|
||||
///lock to determine whether we could actually send the keystroke with the current settings. Therefore,
|
||||
///This conservative guess is used instead. Note that in practice the case where this method
|
||||
///Returns false when the actual terminal would consume the keystroke never happens. All keystrokes
|
||||
///that depend on terminal modes also have a mapping that doesn't depend on the terminal mode.
|
||||
///This is fragile, but as these mappings are locked up in legacy compatibility, it's probably good enough
|
||||
pub fn might_convert(keystroke: &Keystroke) -> bool {
|
||||
to_esc_str(keystroke, &TermMode::NONE, false).is_some()
|
||||
}
|
||||
|
||||
pub fn to_esc_str(keystroke: &Keystroke, mode: &TermMode, alt_is_meta: bool) -> Option<String> {
|
||||
let modifiers = Modifiers::new(keystroke);
|
||||
|
||||
|
||||
@@ -97,7 +97,7 @@ impl MouseButton {
|
||||
}
|
||||
|
||||
fn from_scroll(e: &ScrollWheelEvent) -> Self {
|
||||
if e.delta.raw().y() > 0. {
|
||||
if e.delta.y() > 0. {
|
||||
MouseButton::ScrollUp
|
||||
} else {
|
||||
MouseButton::ScrollDown
|
||||
|
||||
@@ -407,18 +407,13 @@ impl TerminalBuilder {
|
||||
'outer: loop {
|
||||
let mut events = vec![];
|
||||
let mut timer = cx.background().timer(Duration::from_millis(4)).fuse();
|
||||
let mut wakeup = false;
|
||||
|
||||
loop {
|
||||
futures::select_biased! {
|
||||
_ = timer => break,
|
||||
event = self.events_rx.next() => {
|
||||
if let Some(event) = event {
|
||||
if matches!(event, AlacTermEvent::Wakeup) {
|
||||
wakeup = true;
|
||||
} else {
|
||||
events.push(event);
|
||||
}
|
||||
|
||||
events.push(event);
|
||||
if events.len() > 100 {
|
||||
break;
|
||||
}
|
||||
@@ -429,15 +424,11 @@ impl TerminalBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
if events.is_empty() && wakeup == false {
|
||||
if events.is_empty() {
|
||||
smol::future::yield_now().await;
|
||||
break 'outer;
|
||||
} else {
|
||||
this.upgrade(&cx)?.update(&mut cx, |this, cx| {
|
||||
if wakeup {
|
||||
this.process_event(&AlacTermEvent::Wakeup, cx);
|
||||
}
|
||||
|
||||
for event in events {
|
||||
this.process_event(&event, cx);
|
||||
}
|
||||
@@ -636,7 +627,7 @@ impl Terminal {
|
||||
term.grid_mut().reset_region(..cursor.line);
|
||||
|
||||
// Copy the current line up
|
||||
let line = term.grid()[cursor.line][..Column(term.grid().columns())]
|
||||
let line = term.grid()[cursor.line][..cursor.column]
|
||||
.iter()
|
||||
.cloned()
|
||||
.enumerate()
|
||||
@@ -1145,7 +1136,7 @@ impl Terminal {
|
||||
|
||||
fn determine_scroll_lines(&mut self, e: &MouseScrollWheel, mouse_mode: bool) -> Option<i32> {
|
||||
let scroll_multiplier = if mouse_mode { 1. } else { SCROLL_MULTIPLIER };
|
||||
let line_height = self.last_content.size.line_height;
|
||||
|
||||
match e.phase {
|
||||
/* Reset scroll state on started */
|
||||
Some(gpui::TouchPhase::Started) => {
|
||||
@@ -1154,11 +1145,11 @@ impl Terminal {
|
||||
}
|
||||
/* Calculate the appropriate scroll lines */
|
||||
Some(gpui::TouchPhase::Moved) => {
|
||||
let old_offset = (self.scroll_px / line_height) as i32;
|
||||
let old_offset = (self.scroll_px / self.last_content.size.line_height) as i32;
|
||||
|
||||
self.scroll_px += e.delta.pixel_delta(line_height).y() * scroll_multiplier;
|
||||
self.scroll_px += e.delta.y() * scroll_multiplier;
|
||||
|
||||
let new_offset = (self.scroll_px / line_height) as i32;
|
||||
let new_offset = (self.scroll_px / self.last_content.size.line_height) as i32;
|
||||
|
||||
// Whenever we hit the edges, reset our stored scroll to 0
|
||||
// so we can respond to changes in direction quickly
|
||||
@@ -1168,7 +1159,7 @@ impl Terminal {
|
||||
}
|
||||
/* Fall back to delta / line_height */
|
||||
None => Some(
|
||||
((e.delta.pixel_delta(line_height).y() * scroll_multiplier) / line_height) as i32,
|
||||
((e.delta.y() * scroll_multiplier) / self.last_content.size.line_height) as i32,
|
||||
),
|
||||
_ => None,
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ use smallvec::{smallvec, SmallVec};
|
||||
use std::iter;
|
||||
|
||||
lazy_static! {
|
||||
static ref MIN: Locator = Locator::min();
|
||||
static ref MAX: Locator = Locator::max();
|
||||
pub static ref MIN: Locator = Locator::min();
|
||||
pub static ref MAX: Locator = Locator::max();
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
@@ -19,14 +19,6 @@ impl Locator {
|
||||
Self(smallvec![u64::MAX])
|
||||
}
|
||||
|
||||
pub fn min_ref() -> &'static Self {
|
||||
&*MIN
|
||||
}
|
||||
|
||||
pub fn max_ref() -> &'static Self {
|
||||
&*MAX
|
||||
}
|
||||
|
||||
pub fn assign(&mut self, other: &Self) {
|
||||
self.0.resize(other.0.len(), 0);
|
||||
self.0.copy_from_slice(&other.0);
|
||||
|
||||
@@ -1594,12 +1594,8 @@ impl BufferSnapshot {
|
||||
self.visible_text.point_utf16_to_offset(point)
|
||||
}
|
||||
|
||||
pub fn unclipped_point_utf16_to_offset(&self, point: Unclipped<PointUtf16>) -> usize {
|
||||
self.visible_text.unclipped_point_utf16_to_offset(point)
|
||||
}
|
||||
|
||||
pub fn unclipped_point_utf16_to_point(&self, point: Unclipped<PointUtf16>) -> Point {
|
||||
self.visible_text.unclipped_point_utf16_to_point(point)
|
||||
pub fn point_utf16_to_point(&self, point: PointUtf16) -> Point {
|
||||
self.visible_text.point_utf16_to_point(point)
|
||||
}
|
||||
|
||||
pub fn offset_utf16_to_offset(&self, offset: OffsetUtf16) -> usize {
|
||||
@@ -1770,9 +1766,9 @@ impl BufferSnapshot {
|
||||
|
||||
fn fragment_id_for_anchor(&self, anchor: &Anchor) -> &Locator {
|
||||
if *anchor == Anchor::MIN {
|
||||
Locator::min_ref()
|
||||
&locator::MIN
|
||||
} else if *anchor == Anchor::MAX {
|
||||
Locator::max_ref()
|
||||
&locator::MAX
|
||||
} else {
|
||||
let anchor_key = InsertionFragmentKey {
|
||||
timestamp: anchor.timestamp,
|
||||
@@ -1807,10 +1803,7 @@ impl BufferSnapshot {
|
||||
}
|
||||
|
||||
pub fn anchor_at<T: ToOffset>(&self, position: T, bias: Bias) -> Anchor {
|
||||
self.anchor_at_offset(position.to_offset(self), bias)
|
||||
}
|
||||
|
||||
fn anchor_at_offset(&self, offset: usize, bias: Bias) -> Anchor {
|
||||
let offset = position.to_offset(self);
|
||||
if bias == Bias::Left && offset == 0 {
|
||||
Anchor::MIN
|
||||
} else if bias == Bias::Right && offset == self.len() {
|
||||
@@ -1847,7 +1840,7 @@ impl BufferSnapshot {
|
||||
self.visible_text.clip_offset_utf16(offset, bias)
|
||||
}
|
||||
|
||||
pub fn clip_point_utf16(&self, point: Unclipped<PointUtf16>, bias: Bias) -> PointUtf16 {
|
||||
pub fn clip_point_utf16(&self, point: PointUtf16, bias: Bias) -> PointUtf16 {
|
||||
self.visible_text.clip_point_utf16(point, bias)
|
||||
}
|
||||
|
||||
@@ -2361,20 +2354,32 @@ pub trait ToOffset {
|
||||
}
|
||||
|
||||
impl ToOffset for Point {
|
||||
fn to_offset(&self, snapshot: &BufferSnapshot) -> usize {
|
||||
fn to_offset<'a>(&self, snapshot: &BufferSnapshot) -> usize {
|
||||
snapshot.point_to_offset(*self)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToOffset for PointUtf16 {
|
||||
fn to_offset<'a>(&self, snapshot: &BufferSnapshot) -> usize {
|
||||
snapshot.point_utf16_to_offset(*self)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToOffset for usize {
|
||||
fn to_offset(&self, snapshot: &BufferSnapshot) -> usize {
|
||||
fn to_offset<'a>(&self, snapshot: &BufferSnapshot) -> usize {
|
||||
assert!(*self <= snapshot.len(), "offset {self} is out of range");
|
||||
*self
|
||||
}
|
||||
}
|
||||
|
||||
impl ToOffset for OffsetUtf16 {
|
||||
fn to_offset<'a>(&self, snapshot: &BufferSnapshot) -> usize {
|
||||
snapshot.offset_utf16_to_offset(*self)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToOffset for Anchor {
|
||||
fn to_offset(&self, snapshot: &BufferSnapshot) -> usize {
|
||||
fn to_offset<'a>(&self, snapshot: &BufferSnapshot) -> usize {
|
||||
snapshot.summary_for_anchor(self)
|
||||
}
|
||||
}
|
||||
@@ -2385,43 +2390,31 @@ impl<'a, T: ToOffset> ToOffset for &'a T {
|
||||
}
|
||||
}
|
||||
|
||||
impl ToOffset for PointUtf16 {
|
||||
fn to_offset(&self, snapshot: &BufferSnapshot) -> usize {
|
||||
snapshot.point_utf16_to_offset(*self)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToOffset for Unclipped<PointUtf16> {
|
||||
fn to_offset(&self, snapshot: &BufferSnapshot) -> usize {
|
||||
snapshot.unclipped_point_utf16_to_offset(*self)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ToPoint {
|
||||
fn to_point(&self, snapshot: &BufferSnapshot) -> Point;
|
||||
}
|
||||
|
||||
impl ToPoint for Anchor {
|
||||
fn to_point(&self, snapshot: &BufferSnapshot) -> Point {
|
||||
fn to_point<'a>(&self, snapshot: &BufferSnapshot) -> Point {
|
||||
snapshot.summary_for_anchor(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToPoint for usize {
|
||||
fn to_point(&self, snapshot: &BufferSnapshot) -> Point {
|
||||
fn to_point<'a>(&self, snapshot: &BufferSnapshot) -> Point {
|
||||
snapshot.offset_to_point(*self)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToPoint for Point {
|
||||
fn to_point(&self, _: &BufferSnapshot) -> Point {
|
||||
*self
|
||||
impl ToPoint for PointUtf16 {
|
||||
fn to_point<'a>(&self, snapshot: &BufferSnapshot) -> Point {
|
||||
snapshot.point_utf16_to_point(*self)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToPoint for Unclipped<PointUtf16> {
|
||||
fn to_point(&self, snapshot: &BufferSnapshot) -> Point {
|
||||
snapshot.unclipped_point_utf16_to_point(*self)
|
||||
impl ToPoint for Point {
|
||||
fn to_point<'a>(&self, _: &BufferSnapshot) -> Point {
|
||||
*self
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2430,25 +2423,25 @@ pub trait ToPointUtf16 {
|
||||
}
|
||||
|
||||
impl ToPointUtf16 for Anchor {
|
||||
fn to_point_utf16(&self, snapshot: &BufferSnapshot) -> PointUtf16 {
|
||||
fn to_point_utf16<'a>(&self, snapshot: &BufferSnapshot) -> PointUtf16 {
|
||||
snapshot.summary_for_anchor(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToPointUtf16 for usize {
|
||||
fn to_point_utf16(&self, snapshot: &BufferSnapshot) -> PointUtf16 {
|
||||
fn to_point_utf16<'a>(&self, snapshot: &BufferSnapshot) -> PointUtf16 {
|
||||
snapshot.offset_to_point_utf16(*self)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToPointUtf16 for PointUtf16 {
|
||||
fn to_point_utf16(&self, _: &BufferSnapshot) -> PointUtf16 {
|
||||
fn to_point_utf16<'a>(&self, _: &BufferSnapshot) -> PointUtf16 {
|
||||
*self
|
||||
}
|
||||
}
|
||||
|
||||
impl ToPointUtf16 for Point {
|
||||
fn to_point_utf16(&self, snapshot: &BufferSnapshot) -> PointUtf16 {
|
||||
fn to_point_utf16<'a>(&self, snapshot: &BufferSnapshot) -> PointUtf16 {
|
||||
snapshot.point_to_point_utf16(*self)
|
||||
}
|
||||
}
|
||||
@@ -2458,23 +2451,45 @@ pub trait ToOffsetUtf16 {
|
||||
}
|
||||
|
||||
impl ToOffsetUtf16 for Anchor {
|
||||
fn to_offset_utf16(&self, snapshot: &BufferSnapshot) -> OffsetUtf16 {
|
||||
fn to_offset_utf16<'a>(&self, snapshot: &BufferSnapshot) -> OffsetUtf16 {
|
||||
snapshot.summary_for_anchor(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToOffsetUtf16 for usize {
|
||||
fn to_offset_utf16(&self, snapshot: &BufferSnapshot) -> OffsetUtf16 {
|
||||
fn to_offset_utf16<'a>(&self, snapshot: &BufferSnapshot) -> OffsetUtf16 {
|
||||
snapshot.offset_to_offset_utf16(*self)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToOffsetUtf16 for OffsetUtf16 {
|
||||
fn to_offset_utf16(&self, _snapshot: &BufferSnapshot) -> OffsetUtf16 {
|
||||
fn to_offset_utf16<'a>(&self, _snapshot: &BufferSnapshot) -> OffsetUtf16 {
|
||||
*self
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Clip {
|
||||
fn clip(&self, bias: Bias, snapshot: &BufferSnapshot) -> Self;
|
||||
}
|
||||
|
||||
impl Clip for usize {
|
||||
fn clip(&self, bias: Bias, snapshot: &BufferSnapshot) -> Self {
|
||||
snapshot.clip_offset(*self, bias)
|
||||
}
|
||||
}
|
||||
|
||||
impl Clip for Point {
|
||||
fn clip(&self, bias: Bias, snapshot: &BufferSnapshot) -> Self {
|
||||
snapshot.clip_point(*self, bias)
|
||||
}
|
||||
}
|
||||
|
||||
impl Clip for PointUtf16 {
|
||||
fn clip(&self, bias: Bias, snapshot: &BufferSnapshot) -> Self {
|
||||
snapshot.clip_point_utf16(*self, bias)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait FromAnchor {
|
||||
fn from_anchor(anchor: &Anchor, snapshot: &BufferSnapshot) -> Self;
|
||||
}
|
||||
|
||||
@@ -326,7 +326,6 @@ pub struct ProjectPanel {
|
||||
#[serde(flatten)]
|
||||
pub container: ContainerStyle,
|
||||
pub entry: Interactive<ProjectPanelEntry>,
|
||||
pub dragged_entry: ProjectPanelEntry,
|
||||
pub ignored_entry: Interactive<ProjectPanelEntry>,
|
||||
pub cut_entry: Interactive<ProjectPanelEntry>,
|
||||
pub filename_editor: FieldEditor,
|
||||
@@ -424,14 +423,12 @@ pub struct ChannelName {
|
||||
pub name: TextStyle,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default)]
|
||||
#[derive(Deserialize, Default)]
|
||||
pub struct Picker {
|
||||
#[serde(flatten)]
|
||||
pub container: ContainerStyle,
|
||||
pub empty_container: ContainerStyle,
|
||||
pub empty: ContainedLabel,
|
||||
pub input_editor: FieldEditor,
|
||||
pub empty_input_editor: FieldEditor,
|
||||
pub no_matches: ContainedLabel,
|
||||
pub item: Interactive<ContainedLabel>,
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ pub enum Event {
|
||||
impl ThemeSelector {
|
||||
fn new(registry: Arc<ThemeRegistry>, cx: &mut ViewContext<Self>) -> Self {
|
||||
let handle = cx.weak_handle();
|
||||
let picker = cx.add_view(|cx| Picker::new("Select Theme...", handle, cx));
|
||||
let picker = cx.add_view(|cx| Picker::new(handle, cx));
|
||||
let settings = cx.global::<Settings>();
|
||||
|
||||
let original_theme = settings.theme.clone();
|
||||
|
||||
@@ -15,4 +15,4 @@ settings = { path = "../settings" }
|
||||
workspace = { path = "../workspace" }
|
||||
project = { path = "../project" }
|
||||
|
||||
smallvec = { version = "1.6", features = ["union"] }
|
||||
smallvec = { version = "1.6", features = ["union"] }
|
||||
@@ -11,7 +11,6 @@ test-support = ["serde_json", "tempdir", "git2"]
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.38"
|
||||
backtrace = "0.3"
|
||||
futures = "0.3"
|
||||
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
|
||||
lazy_static = "1.4.0"
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub mod test;
|
||||
|
||||
pub use backtrace::Backtrace;
|
||||
use futures::Future;
|
||||
use rand::{seq::SliceRandom, Rng};
|
||||
use std::{
|
||||
@@ -11,18 +10,6 @@ use std::{
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! debug_panic {
|
||||
( $($fmt_arg:tt)* ) => {
|
||||
if cfg!(debug_assertions) {
|
||||
panic!( $($fmt_arg)* );
|
||||
} else {
|
||||
let backtrace = $crate::Backtrace::new();
|
||||
log::error!("{}\n{:?}", format_args!($($fmt_arg)*), backtrace);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub fn truncate(s: &str, max_chars: usize) -> &str {
|
||||
match s.char_indices().nth(max_chars) {
|
||||
None => s,
|
||||
|
||||
@@ -42,4 +42,4 @@ language = { path = "../language", features = ["test-support"] }
|
||||
project = { path = "../project", features = ["test-support"] }
|
||||
util = { path = "../util", features = ["test-support"] }
|
||||
settings = { path = "../settings" }
|
||||
workspace = { path = "../workspace", features = ["test-support"] }
|
||||
workspace = { path = "../workspace", features = ["test-support"] }
|
||||
@@ -13,7 +13,7 @@ pub fn init(cx: &mut MutableAppContext) {
|
||||
fn normal_before(_: &mut Workspace, _: &NormalBefore, cx: &mut ViewContext<Workspace>) {
|
||||
Vim::update(cx, |state, cx| {
|
||||
state.update_active_editor(cx, |editor, cx| {
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
s.move_cursors_with(|map, mut cursor, _| {
|
||||
*cursor.column_mut() = cursor.column().saturating_sub(1);
|
||||
(map.clip_point(cursor, Bias::Left), SelectionGoal::None)
|
||||
|
||||
@@ -114,7 +114,7 @@ pub fn normal_object(object: Object, cx: &mut MutableAppContext) {
|
||||
|
||||
fn move_cursor(vim: &mut Vim, motion: Motion, times: usize, cx: &mut MutableAppContext) {
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
s.move_cursors_with(|map, cursor, goal| {
|
||||
motion
|
||||
.move_point(map, cursor, goal, times)
|
||||
@@ -128,7 +128,7 @@ fn insert_after(_: &mut Workspace, _: &InsertAfter, cx: &mut ViewContext<Workspa
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.switch_mode(Mode::Insert, false, cx);
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
s.maybe_move_cursors_with(|map, cursor, goal| {
|
||||
Motion::Right.move_point(map, cursor, goal, 1)
|
||||
});
|
||||
@@ -145,7 +145,7 @@ fn insert_first_non_whitespace(
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.switch_mode(Mode::Insert, false, cx);
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
s.maybe_move_cursors_with(|map, cursor, goal| {
|
||||
Motion::FirstNonWhitespace.move_point(map, cursor, goal, 1)
|
||||
});
|
||||
@@ -158,7 +158,7 @@ fn insert_end_of_line(_: &mut Workspace, _: &InsertEndOfLine, cx: &mut ViewConte
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.switch_mode(Mode::Insert, false, cx);
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
s.maybe_move_cursors_with(|map, cursor, goal| {
|
||||
Motion::EndOfLine.move_point(map, cursor, goal, 1)
|
||||
});
|
||||
@@ -187,7 +187,7 @@ fn insert_line_above(_: &mut Workspace, _: &InsertLineAbove, cx: &mut ViewContex
|
||||
(start_of_line..start_of_line, new_text)
|
||||
});
|
||||
editor.edit_with_autoindent(edits, cx);
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
s.move_cursors_with(|map, mut cursor, _| {
|
||||
*cursor.row_mut() -= 1;
|
||||
*cursor.column_mut() = map.line_len(cursor.row());
|
||||
@@ -218,7 +218,7 @@ fn insert_line_below(_: &mut Workspace, _: &InsertLineBelow, cx: &mut ViewContex
|
||||
new_text.push_str(&" ".repeat(indent as usize));
|
||||
(end_of_line..end_of_line, new_text)
|
||||
});
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
s.maybe_move_cursors_with(|map, cursor, goal| {
|
||||
Motion::EndOfLine.move_point(map, cursor, goal, 1)
|
||||
});
|
||||
@@ -336,7 +336,7 @@ fn paste(_: &mut Workspace, _: &Paste, cx: &mut ViewContext<Workspace>) {
|
||||
buffer.edit(edits, Some(AutoindentMode::EachLine), cx);
|
||||
});
|
||||
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
s.move_with(|map, selection| {
|
||||
if let Some(new_position) = new_selections.get(&selection.id) {
|
||||
match new_position {
|
||||
|
||||
@@ -15,7 +15,7 @@ pub fn change_motion(vim: &mut Vim, motion: Motion, times: usize, cx: &mut Mutab
|
||||
editor.transact(cx, |editor, cx| {
|
||||
// We are swapping to insert mode anyway. Just set the line end clipping behavior now
|
||||
editor.set_clip_at_line_ends(false, cx);
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
s.move_with(|map, selection| {
|
||||
motion_succeeded |= if let Motion::NextWordStart { ignore_punctuation } = motion
|
||||
{
|
||||
@@ -43,7 +43,7 @@ pub fn change_object(vim: &mut Vim, object: Object, around: bool, cx: &mut Mutab
|
||||
// We are swapping to insert mode anyway. Just set the line end clipping behavior now
|
||||
editor.set_clip_at_line_ends(false, cx);
|
||||
editor.transact(cx, |editor, cx| {
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
s.move_with(|map, selection| {
|
||||
objects_found |= object.expand_selection(map, selection, around);
|
||||
});
|
||||
|
||||
@@ -8,7 +8,7 @@ pub fn delete_motion(vim: &mut Vim, motion: Motion, times: usize, cx: &mut Mutab
|
||||
editor.transact(cx, |editor, cx| {
|
||||
editor.set_clip_at_line_ends(false, cx);
|
||||
let mut original_columns: HashMap<_, _> = Default::default();
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
s.move_with(|map, selection| {
|
||||
let original_head = selection.head();
|
||||
original_columns.insert(selection.id, original_head.column());
|
||||
@@ -20,7 +20,7 @@ pub fn delete_motion(vim: &mut Vim, motion: Motion, times: usize, cx: &mut Mutab
|
||||
|
||||
// Fixup cursor position after the deletion
|
||||
editor.set_clip_at_line_ends(true, cx);
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
s.move_with(|map, selection| {
|
||||
let mut cursor = selection.head();
|
||||
if motion.linewise() {
|
||||
@@ -43,7 +43,7 @@ pub fn delete_object(vim: &mut Vim, object: Object, around: bool, cx: &mut Mutab
|
||||
// Emulates behavior in vim where if we expanded backwards to include a newline
|
||||
// the cursor gets set back to the start of the line
|
||||
let mut should_move_to_start: HashSet<_> = Default::default();
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
s.move_with(|map, selection| {
|
||||
object.expand_selection(map, selection, around);
|
||||
let offset_range = selection.map(|p| p.to_offset(map, Bias::Left)).range();
|
||||
@@ -78,7 +78,7 @@ pub fn delete_object(vim: &mut Vim, object: Object, around: bool, cx: &mut Mutab
|
||||
|
||||
// Fixup cursor position after the deletion
|
||||
editor.set_clip_at_line_ends(true, cx);
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
s.move_with(|map, selection| {
|
||||
let mut cursor = selection.head();
|
||||
if should_move_to_start.contains(&selection.id) {
|
||||
|
||||
@@ -67,9 +67,7 @@ impl<'a> VimTestContext<'a> {
|
||||
|
||||
let file = cx.read(|cx| workspace.file_project_paths(cx)[0].clone());
|
||||
let item = workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.open_path(file, None, true, cx)
|
||||
})
|
||||
.update(cx, |workspace, cx| workspace.open_path(file, true, cx))
|
||||
.await
|
||||
.expect("Could not open test file");
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ pub fn init(cx: &mut MutableAppContext) {
|
||||
pub fn visual_motion(motion: Motion, times: usize, cx: &mut MutableAppContext) {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
s.move_with(|map, selection| {
|
||||
let was_reversed = selection.reversed;
|
||||
|
||||
@@ -58,7 +58,7 @@ pub fn visual_object(object: Object, cx: &mut MutableAppContext) {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
if let Operator::Object { around } = vim.pop_operator(cx) {
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
s.move_with(|map, selection| {
|
||||
let head = selection.head();
|
||||
if let Some(mut range) = object.range(map, head, around) {
|
||||
@@ -114,19 +114,19 @@ pub fn change(_: &mut Workspace, _: &VisualChange, cx: &mut ViewContext<Workspac
|
||||
};
|
||||
|
||||
edits.push((expanded_range, "\n"));
|
||||
new_selections.push(selection.map(|_| anchor));
|
||||
new_selections.push(selection.map(|_| anchor.clone()));
|
||||
} else {
|
||||
let range = selection.map(|p| p.to_point(map)).range();
|
||||
let anchor = map.buffer_snapshot.anchor_after(range.end);
|
||||
edits.push((range, ""));
|
||||
new_selections.push(selection.map(|_| anchor));
|
||||
new_selections.push(selection.map(|_| anchor.clone()));
|
||||
}
|
||||
selection.goal = SelectionGoal::None;
|
||||
});
|
||||
});
|
||||
copy_selections_content(editor, editor.selections.line_mode, cx);
|
||||
editor.edit_with_autoindent(edits, cx);
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
s.select_anchors(new_selections);
|
||||
});
|
||||
});
|
||||
@@ -140,7 +140,7 @@ pub fn delete(_: &mut Workspace, _: &VisualDelete, cx: &mut ViewContext<Workspac
|
||||
editor.set_clip_at_line_ends(false, cx);
|
||||
let mut original_columns: HashMap<_, _> = Default::default();
|
||||
let line_mode = editor.selections.line_mode;
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
s.move_with(|map, selection| {
|
||||
if line_mode {
|
||||
original_columns
|
||||
@@ -159,7 +159,7 @@ pub fn delete(_: &mut Workspace, _: &VisualDelete, cx: &mut ViewContext<Workspac
|
||||
|
||||
// Fixup cursor position after the deletion
|
||||
editor.set_clip_at_line_ends(true, cx);
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
s.move_with(|map, selection| {
|
||||
let mut cursor = selection.head().to_point(map);
|
||||
|
||||
@@ -298,7 +298,7 @@ pub fn paste(_: &mut Workspace, _: &VisualPaste, cx: &mut ViewContext<Workspace>
|
||||
buffer.edit(edits, Some(AutoindentMode::EachLine), cx);
|
||||
});
|
||||
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
s.select(new_selections)
|
||||
});
|
||||
} else {
|
||||
|
||||
@@ -46,4 +46,4 @@ client = { path = "../client", features = ["test-support"] }
|
||||
gpui = { path = "../gpui", features = ["test-support"] }
|
||||
project = { path = "../project", features = ["test-support"] }
|
||||
settings = { path = "../settings", features = ["test-support"] }
|
||||
fs = { path = "../fs", features = ["test-support"] }
|
||||
fs = { path = "../fs", features = ["test-support"] }
|
||||
@@ -7,13 +7,9 @@ use gpui::{
|
||||
AppContext, Element, ElementBox, EventContext, MouseButton, MouseState, Quad, RenderContext,
|
||||
WeakViewHandle,
|
||||
};
|
||||
use project::ProjectEntryId;
|
||||
use settings::Settings;
|
||||
|
||||
use crate::{
|
||||
MoveItem, OpenProjectEntryInPane, Pane, SplitDirection, SplitWithItem, SplitWithProjectEntry,
|
||||
Workspace,
|
||||
};
|
||||
use crate::{MoveItem, Pane, SplitDirection, SplitWithItem, Workspace};
|
||||
|
||||
use super::DraggedItem;
|
||||
|
||||
@@ -32,18 +28,12 @@ where
|
||||
MouseEventHandler::<Tag>::above(region_id, cx, |state, cx| {
|
||||
// Observing hovered will cause a render when the mouse enters regardless
|
||||
// of if mouse position was accessed before
|
||||
let drag_position = if state.hovered() {
|
||||
cx.global::<DragAndDrop<Workspace>>()
|
||||
.currently_dragged::<DraggedItem>(cx.window_id())
|
||||
.map(|(drag_position, _)| drag_position)
|
||||
.or_else(|| {
|
||||
cx.global::<DragAndDrop<Workspace>>()
|
||||
.currently_dragged::<ProjectEntryId>(cx.window_id())
|
||||
.map(|(drag_position, _)| drag_position)
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let hovered = state.hovered();
|
||||
let drag_position = cx
|
||||
.global::<DragAndDrop<Workspace>>()
|
||||
.currently_dragged::<DraggedItem>(cx.window_id())
|
||||
.filter(|_| hovered)
|
||||
.map(|(drag_position, _)| drag_position);
|
||||
|
||||
Stack::new()
|
||||
.with_child(render_child(state, cx))
|
||||
@@ -80,14 +70,10 @@ where
|
||||
}
|
||||
})
|
||||
.on_move(|_, cx| {
|
||||
let drag_and_drop = cx.global::<DragAndDrop<Workspace>>();
|
||||
|
||||
if drag_and_drop
|
||||
if cx
|
||||
.global::<DragAndDrop<Workspace>>()
|
||||
.currently_dragged::<DraggedItem>(cx.window_id())
|
||||
.is_some()
|
||||
|| drag_and_drop
|
||||
.currently_dragged::<ProjectEntryId>(cx.window_id())
|
||||
.is_some()
|
||||
{
|
||||
cx.notify();
|
||||
} else {
|
||||
@@ -104,60 +90,30 @@ pub fn handle_dropped_item(
|
||||
split_margin: Option<f32>,
|
||||
cx: &mut EventContext,
|
||||
) {
|
||||
enum Action {
|
||||
Move(WeakViewHandle<Pane>, usize),
|
||||
Open(ProjectEntryId),
|
||||
}
|
||||
let drag_and_drop = cx.global::<DragAndDrop<Workspace>>();
|
||||
let action = if let Some((_, dragged_item)) =
|
||||
drag_and_drop.currently_dragged::<DraggedItem>(cx.window_id)
|
||||
if let Some((_, dragged_item)) = cx
|
||||
.global::<DragAndDrop<Workspace>>()
|
||||
.currently_dragged::<DraggedItem>(cx.window_id)
|
||||
{
|
||||
Action::Move(dragged_item.pane.clone(), dragged_item.item.id())
|
||||
} else if let Some((_, project_entry)) =
|
||||
drag_and_drop.currently_dragged::<ProjectEntryId>(cx.window_id)
|
||||
{
|
||||
Action::Open(*project_entry)
|
||||
if let Some(split_direction) = split_margin
|
||||
.and_then(|margin| drop_split_direction(event.position, event.region, margin))
|
||||
{
|
||||
cx.dispatch_action(SplitWithItem {
|
||||
from: dragged_item.pane.clone(),
|
||||
item_id_to_move: dragged_item.item.id(),
|
||||
pane_to_split: pane.clone(),
|
||||
split_direction,
|
||||
});
|
||||
} else if pane != &dragged_item.pane || allow_same_pane {
|
||||
// If no split margin or not close enough to the edge, just move the item
|
||||
cx.dispatch_action(MoveItem {
|
||||
item_id: dragged_item.item.id(),
|
||||
from: dragged_item.pane.clone(),
|
||||
to: pane.clone(),
|
||||
destination_index: index,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
cx.propagate_event();
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some(split_direction) =
|
||||
split_margin.and_then(|margin| drop_split_direction(event.position, event.region, margin))
|
||||
{
|
||||
let pane_to_split = pane.clone();
|
||||
match action {
|
||||
Action::Move(from, item_id_to_move) => cx.dispatch_action(SplitWithItem {
|
||||
from,
|
||||
item_id_to_move,
|
||||
pane_to_split,
|
||||
split_direction,
|
||||
}),
|
||||
Action::Open(project_entry) => cx.dispatch_action(SplitWithProjectEntry {
|
||||
pane_to_split,
|
||||
split_direction,
|
||||
project_entry,
|
||||
}),
|
||||
};
|
||||
} else {
|
||||
match action {
|
||||
Action::Move(from, item_id) => {
|
||||
if pane != &from || allow_same_pane {
|
||||
cx.dispatch_action(MoveItem {
|
||||
item_id,
|
||||
from,
|
||||
to: pane.clone(),
|
||||
destination_index: index,
|
||||
})
|
||||
} else {
|
||||
cx.propagate_event();
|
||||
}
|
||||
}
|
||||
Action::Open(project_entry) => cx.dispatch_action(OpenProjectEntryInPane {
|
||||
pane: pane.clone(),
|
||||
project_entry,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ use gpui::{
|
||||
};
|
||||
use project::Project;
|
||||
use serde::Deserialize;
|
||||
use settings::Settings;
|
||||
use theme::Theme;
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
@@ -64,17 +63,10 @@ impl PaneGroup {
|
||||
theme: &Theme,
|
||||
follower_states: &FollowerStatesByLeader,
|
||||
active_call: Option<&ModelHandle<ActiveCall>>,
|
||||
active_pane: &ViewHandle<Pane>,
|
||||
cx: &mut RenderContext<Workspace>,
|
||||
) -> ElementBox {
|
||||
self.root.render(
|
||||
project,
|
||||
theme,
|
||||
follower_states,
|
||||
active_call,
|
||||
active_pane,
|
||||
cx,
|
||||
)
|
||||
self.root
|
||||
.render(project, theme, follower_states, active_call, cx)
|
||||
}
|
||||
|
||||
pub(crate) fn panes(&self) -> Vec<&ViewHandle<Pane>> {
|
||||
@@ -112,20 +104,12 @@ impl Member {
|
||||
Member::Axis(PaneAxis { axis, members })
|
||||
}
|
||||
|
||||
fn contains(&self, needle: &ViewHandle<Pane>) -> bool {
|
||||
match self {
|
||||
Member::Axis(axis) => axis.members.iter().any(|member| member.contains(needle)),
|
||||
Member::Pane(pane) => pane == needle,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render(
|
||||
&self,
|
||||
project: &ModelHandle<Project>,
|
||||
theme: &Theme,
|
||||
follower_states: &FollowerStatesByLeader,
|
||||
active_call: Option<&ModelHandle<ActiveCall>>,
|
||||
active_pane: &ViewHandle<Pane>,
|
||||
cx: &mut RenderContext<Workspace>,
|
||||
) -> ElementBox {
|
||||
enum FollowIntoExternalProject {}
|
||||
@@ -252,14 +236,7 @@ impl Member {
|
||||
.with_children(prompt)
|
||||
.boxed()
|
||||
}
|
||||
Member::Axis(axis) => axis.render(
|
||||
project,
|
||||
theme,
|
||||
follower_states,
|
||||
active_call,
|
||||
active_pane,
|
||||
cx,
|
||||
),
|
||||
Member::Axis(axis) => axis.render(project, theme, follower_states, active_call, cx),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -360,19 +337,12 @@ impl PaneAxis {
|
||||
theme: &Theme,
|
||||
follower_state: &FollowerStatesByLeader,
|
||||
active_call: Option<&ModelHandle<ActiveCall>>,
|
||||
active_pane: &ViewHandle<Pane>,
|
||||
cx: &mut RenderContext<Workspace>,
|
||||
) -> ElementBox {
|
||||
let last_member_ix = self.members.len() - 1;
|
||||
Flex::new(self.axis)
|
||||
.with_children(self.members.iter().enumerate().map(|(ix, member)| {
|
||||
let mut flex = 1.0;
|
||||
if member.contains(active_pane) {
|
||||
flex = cx.global::<Settings>().active_pane_magnification;
|
||||
}
|
||||
|
||||
let mut member =
|
||||
member.render(project, theme, follower_state, active_call, active_pane, cx);
|
||||
let mut member = member.render(project, theme, follower_state, active_call, cx);
|
||||
if ix < last_member_ix {
|
||||
let mut border = theme.workspace.pane_divider;
|
||||
border.left = false;
|
||||
@@ -386,7 +356,7 @@ impl PaneAxis {
|
||||
member = Container::new(member).with_border(border).boxed();
|
||||
}
|
||||
|
||||
FlexItem::new(member).flex(flex, true).boxed()
|
||||
FlexItem::new(member).flex(1.0, true).boxed()
|
||||
}))
|
||||
.boxed()
|
||||
}
|
||||
|
||||
@@ -128,23 +128,10 @@ pub struct OpenSharedScreen {
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct SplitWithItem {
|
||||
pane_to_split: WeakViewHandle<Pane>,
|
||||
split_direction: SplitDirection,
|
||||
from: WeakViewHandle<Pane>,
|
||||
item_id_to_move: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct SplitWithProjectEntry {
|
||||
pane_to_split: WeakViewHandle<Pane>,
|
||||
split_direction: SplitDirection,
|
||||
project_entry: ProjectEntryId,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct OpenProjectEntryInPane {
|
||||
pane: WeakViewHandle<Pane>,
|
||||
project_entry: ProjectEntryId,
|
||||
item_id_to_move: usize,
|
||||
}
|
||||
|
||||
impl_internal_actions!(
|
||||
@@ -156,8 +143,6 @@ impl_internal_actions!(
|
||||
OpenSharedScreen,
|
||||
RemoveWorktreeFromProject,
|
||||
SplitWithItem,
|
||||
SplitWithProjectEntry,
|
||||
OpenProjectEntryInPane,
|
||||
]
|
||||
);
|
||||
impl_actions!(workspace, [ActivatePane]);
|
||||
@@ -249,57 +234,6 @@ pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
|
||||
},
|
||||
);
|
||||
|
||||
cx.add_async_action(
|
||||
|workspace: &mut Workspace,
|
||||
SplitWithProjectEntry {
|
||||
pane_to_split,
|
||||
split_direction,
|
||||
project_entry,
|
||||
}: &_,
|
||||
cx| {
|
||||
pane_to_split.upgrade(cx).and_then(|pane_to_split| {
|
||||
let new_pane = workspace.add_pane(cx);
|
||||
workspace
|
||||
.center
|
||||
.split(&pane_to_split, &new_pane, *split_direction)
|
||||
.unwrap();
|
||||
|
||||
workspace
|
||||
.project
|
||||
.read(cx)
|
||||
.path_for_entry(*project_entry, cx)
|
||||
.map(|path| {
|
||||
let task = workspace.open_path(path, Some(new_pane.downgrade()), true, cx);
|
||||
cx.foreground().spawn(async move {
|
||||
task.await?;
|
||||
Ok(())
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
);
|
||||
|
||||
cx.add_async_action(
|
||||
|workspace: &mut Workspace,
|
||||
OpenProjectEntryInPane {
|
||||
pane,
|
||||
project_entry,
|
||||
}: &_,
|
||||
cx| {
|
||||
workspace
|
||||
.project
|
||||
.read(cx)
|
||||
.path_for_entry(*project_entry, cx)
|
||||
.map(|path| {
|
||||
let task = workspace.open_path(path, Some(pane.clone()), true, cx);
|
||||
cx.foreground().spawn(async move {
|
||||
task.await?;
|
||||
Ok(())
|
||||
})
|
||||
})
|
||||
},
|
||||
);
|
||||
|
||||
let client = &app_state.client;
|
||||
client.add_view_request_handler(Workspace::handle_follow);
|
||||
client.add_view_message_handler(Workspace::handle_unfollow);
|
||||
@@ -1465,7 +1399,7 @@ impl Workspace {
|
||||
mut abs_paths: Vec<PathBuf>,
|
||||
visible: bool,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Task<Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>> {
|
||||
) -> Task<Vec<Option<Result<Box<dyn ItemHandle>, Arc<anyhow::Error>>>>> {
|
||||
let fs = self.fs.clone();
|
||||
|
||||
// Sort the paths to ensure we add worktrees for parents before their children.
|
||||
@@ -1495,7 +1429,7 @@ impl Workspace {
|
||||
if fs.is_file(&abs_path).await {
|
||||
Some(
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.open_path(project_path, None, true, cx)
|
||||
this.open_path(project_path, true, cx)
|
||||
})
|
||||
.await,
|
||||
)
|
||||
@@ -1815,11 +1749,10 @@ impl Workspace {
|
||||
pub fn open_path(
|
||||
&mut self,
|
||||
path: impl Into<ProjectPath>,
|
||||
pane: Option<WeakViewHandle<Pane>>,
|
||||
focus_item: bool,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
|
||||
let pane = pane.unwrap_or_else(|| self.active_pane().downgrade());
|
||||
) -> Task<Result<Box<dyn ItemHandle>, Arc<anyhow::Error>>> {
|
||||
let pane = self.active_pane().downgrade();
|
||||
let task = self.load_path(path.into(), cx);
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let (project_entry_id, build_item) = task.await?;
|
||||
@@ -2787,7 +2720,6 @@ impl View for Workspace {
|
||||
&theme,
|
||||
&self.follower_states_by_leader,
|
||||
self.active_call(),
|
||||
self.active_pane(),
|
||||
cx,
|
||||
))
|
||||
.flex(1., true)
|
||||
@@ -2942,7 +2874,7 @@ pub fn open_paths(
|
||||
cx: &mut MutableAppContext,
|
||||
) -> Task<(
|
||||
ViewHandle<Workspace>,
|
||||
Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
|
||||
Vec<Option<Result<Box<dyn ItemHandle>, Arc<anyhow::Error>>>>,
|
||||
)> {
|
||||
log::info!("open paths {:?}", abs_paths);
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user