Compare commits

..

17 Commits

Author SHA1 Message Date
Max Brunsfeld
b1d732e06c zed 0.63.4 2022-11-10 09:28:16 -08:00
Max Brunsfeld
3ecbcf0cff Fix bump-zed-patch-version script 2022-11-10 09:27:35 -08:00
Max Brunsfeld
2f7fad8480 Fix release channel parsing on stable 2022-11-10 09:25:43 -08:00
Nate Butler
d66e4901ea Merge pull request #1868 from zed-industries/readd-search-match-highlight
Update search match highlight and occurrence style
2022-11-09 17:08:16 -08:00
Max Brunsfeld
1375c6b9e5 0.63.x stable 2022-11-09 13:48:07 -08:00
Antonio Scandurra
4319745c52 zed 0.63.3 2022-11-08 18:23:01 +01:00
Antonio Scandurra
69d2b36788 Merge pull request #1862 from zed-industries/fix-catalina
Weakly link ReplayKit to ensure this library can be used on macOS 10.15
2022-11-08 18:19:48 +01:00
Joseph T Lyons
eebc785e4c zed 0.63.2 2022-11-04 18:35:09 -04:00
Kay Simmons
d109220e0d Merge pull request #1857 from zed-industries/fix-unicode-vim-left
fixes issue with left motion in vim mode clipping incorrectly
2022-11-04 18:33:11 -04:00
Nate Butler
a88a2e601d Swap the color of diagnostic underlines to fix low contrast issue. 2022-11-04 18:33:11 -04:00
Kay Simmons
7cefb16086 Merge pull request #1845 from zed-industries/vim-dd-fix
Vim dd fix
2022-11-04 18:33:11 -04:00
Joseph T. Lyons
c2c30f6615 Merge pull request #1855 from zed-industries/make-app-a-user-property-in-mixpanel
Make `App` a user property in Mixpanel
2022-11-04 14:45:15 -04:00
Max Brunsfeld
bf9a1091e3 zed 0.63.1 2022-11-04 09:30:49 -07:00
Max Brunsfeld
e1f1a706a6 Merge pull request #1851 from zed-industries/rust-let-else
Bump tree-sitter-rust for let-else, let-chains
2022-11-03 18:04:49 -07:00
Max Brunsfeld
75ebaabc2c Merge pull request #1853 from zed-industries/diagnostics-focus-loop
Fix infinite focus transfer loop in project diagnostics
2022-11-03 18:03:05 -07:00
Joseph T. Lyons
d608331f08 Merge pull request #1850 from zed-industries/add-automatic-annotations-for-mixpanel
Add automatic annotations for mixpanel
2022-11-03 13:28:00 -04:00
Max Brunsfeld
448c472ee6 v0.63.x preview 2022-11-02 10:22:52 -07:00
127 changed files with 6372 additions and 5460 deletions

View File

@@ -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

View File

@@ -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
View File

@@ -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",

View File

@@ -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

View File

@@ -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 . .

View File

@@ -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": [

View File

@@ -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"
// }
// }
// }
}
}

View File

@@ -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(),

View 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())
]
);
});
}
}

View File

@@ -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::*;

View File

@@ -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();
}
}

View File

@@ -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"]

View File

@@ -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
);

View File

@@ -1,2 +0,0 @@
ALTER TABLE "signups"
ADD "added_to_mailing_list" BOOLEAN NOT NULL DEFAULT FALSE;

View File

@@ -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(&params).await?;
app.db.create_signup(params).await?;
Ok(())
}

View File

@@ -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")?;

View File

@@ -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

View File

@@ -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>,

View File

@@ -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()))

View File

@@ -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();

View File

@@ -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()
}
}

View File

@@ -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,

View File

@@ -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);

View File

@@ -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,

View File

@@ -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,

View File

@@ -12,4 +12,4 @@ collections = { path = "../collections" }
gpui = { path = "../gpui" }
[dev-dependencies]
gpui = { path = "../gpui", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] }

View File

@@ -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)

View File

@@ -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);
}

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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| {

View File

@@ -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 {

View File

@@ -221,7 +221,7 @@ fn show_hover(
start..end
} else {
anchor..anchor
anchor.clone()..anchor.clone()
};
Some(InfoPopover {

View File

@@ -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!()
}
}
}

View File

@@ -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)
});
});

View File

@@ -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()

View File

@@ -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(),

View File

@@ -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

View File

@@ -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");

View 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)
})
});

View File

@@ -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,

View File

@@ -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(),

View File

@@ -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.;
}

View File

@@ -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,
)
}

View File

@@ -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();
}
}

View File

@@ -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>,

View File

@@ -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),
})
}),

View File

@@ -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;
}
}

View File

@@ -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),
}
}
}

View File

@@ -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);

View File

@@ -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"

View File

@@ -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 {

View File

@@ -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"

View File

@@ -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

View File

@@ -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 {

View File

@@ -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,

View File

@@ -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 {

View File

@@ -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

View File

@@ -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])
});
}

View File

@@ -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"] }

View File

@@ -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
}

View File

@@ -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)));
}

View File

@@ -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(),
}

View File

@@ -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(),

View File

@@ -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
}

View File

@@ -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" }

View File

@@ -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)

View File

@@ -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"] }

View File

@@ -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])
});
});

View File

@@ -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"

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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();

View File

@@ -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())
});
}

View File

@@ -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,

View File

@@ -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);

View File

@@ -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

View File

@@ -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,
}

View File

@@ -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);

View File

@@ -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;
}

View File

@@ -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>,
}

View File

@@ -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();

View File

@@ -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"] }

View File

@@ -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"

View File

@@ -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,

View File

@@ -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"] }

View File

@@ -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)

View File

@@ -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 {

View File

@@ -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);
});

View File

@@ -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) {

View File

@@ -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");

View 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 {

View File

@@ -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"] }

View File

@@ -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,
}),
}
}
}

View File

@@ -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()
}

View File

@@ -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